From 9455c83604d2ebee890a8566b8896d6d02e817d5 Mon Sep 17 00:00:00 2001 From: Satrajit Ghosh Date: Fri, 3 Jul 2026 15:19:11 -0400 Subject: [PATCH 1/3] docs(027): spec, plan, tasks for abstractatlas rename + pluggable ingestors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stage 27 foundation: rename ohbm2026→abstractatlas + ohbmcli→aacli (data preserved, byte-identical), a pluggable Ingestor architecture + registry, and a standardized LinkML ingest schema; port OHBM + NeuroScape as the first two ingestors. New-source ingestors deferred. Full Spec Kit artifacts + analyze remediations (SC-001 wording, cache-resumability check, NeuroScape-port scouting). Co-Authored-By: Claude Opus 4.8 --- .specify/feature.json | 2 +- .../checklists/requirements.md | 36 ++++ .../contracts/cli-aacli.md | 45 +++++ .../contracts/ingest-schema.linkml.yaml | 84 +++++++++ .../contracts/ingestor-interface.md | 67 +++++++ .../contracts/rename-map.md | 44 +++++ .../data-model.md | 85 +++++++++ .../plan.md | 125 +++++++++++++ .../quickstart.md | 71 ++++++++ .../research.md | 124 +++++++++++++ .../spec.md | 165 +++++++++++++++++ .../tasks.md | 168 ++++++++++++++++++ src/{ohbm2026 => abstractatlas}/__init__.py | 0 .../analyze/__init__.py | 0 .../analyze/category_evaluation.py | 0 .../analyze/category_rollup.py | 0 .../analyze/centroids.py | 0 .../analyze/clusters.py | 0 .../analyze/communities.py | 0 .../analyze/errors.py | 0 .../analyze/llm_adapter.py | 0 .../analyze/projections.py | 0 .../analyze/provenance.py | 0 .../analyze/rollup.py | 0 .../analyze/runners.py | 0 .../analyze/stage.py | 0 .../analyze/storage.py | 0 .../analyze/topic_clusters.py | 0 .../analyze/topics.py | 0 .../analyze/umap.py | 0 src/{ohbm2026 => abstractatlas}/artifacts.py | 0 src/{ohbm2026 => abstractatlas}/assets.py | 0 .../atlas_hosting/__init__.py | 0 .../atlas_hosting/cli.py | 0 .../atlas_hosting/compare.py | 0 .../atlas_hosting/content_hash.py | 0 .../atlas_hosting/manifest.py | 0 .../atlas_hosting/r2_client.py | 0 .../atlas_hosting/uploader.py | 0 .../atlas_package/__init__.py | 0 .../atlas_package/cli.py | 0 .../atlas_package/cluster_palette.py | 0 .../atlas_package/decimation.py | 0 .../atlas_package/link_check.py | 0 .../atlas_package/lod.py | 0 .../atlas_package/neighbour_index.py | 0 .../atlas_package/neuroscape_loader.py | 0 .../atlas_package/ohbm_projector.py | 0 .../atlas_package/orchestrator.py | 0 .../atlas_package/parquet_writer.py | 0 .../atlas_package/provenance.py | 0 .../atlas_package/semantic_index.py | 0 .../atlas_package/umap_fit.py | 0 .../atlas_package/vectors_compute.py | 0 .../book/__init__.py | 0 .../book/assemble_pdf.py | 0 .../book/author_index.py | 0 src/{ohbm2026 => abstractatlas}/book/cache.py | 0 src/{ohbm2026 => abstractatlas}/book/cli.py | 0 .../book/corpus.py | 0 .../book/figure_check.py | 0 .../book/html_to_md.py | 0 src/{ohbm2026 => abstractatlas}/book/model.py | 0 .../book/provenance.py | 0 .../book/render_markdown.py | 0 .../book/render_per_abstract.py | 0 .../book/render_via_pandoc.py | 0 .../book/sections.py | 0 src/{ohbm2026 => abstractatlas}/book/sort.py | 0 .../book/templates/.keep | 0 .../book/templates/book.md.template | 0 .../book/templates/header-includes-loose.tex | 0 .../book/templates/header-includes-tufte.tex | 0 .../book/templates/header-includes.tex | 0 .../book/templates/per-abstract.tex.template | 0 src/{ohbm2026 => abstractatlas}/cli.py | 0 .../data/__init__.py | 0 .../data/__pycache__/__init__.cpython-314.pyc | Bin .../data/eco_top_codes.json | 0 .../embed/__init__.py | 0 .../embed/components.py | 0 .../embed/compose.py | 0 src/{ohbm2026 => abstractatlas}/embed/hf.py | 0 .../embed/neuroscape.py | 0 .../embed/openai.py | 0 .../embed/provenance.py | 0 .../embed/stage.py | 0 .../embed/storage.py | 0 .../embed/voyage.py | 0 .../enrich/__init__.py | 0 .../enrich/claims.py | 0 .../enrich/figures.py | 0 .../enrich/flex_tier.py | 0 .../enrich/image_quality.py | 0 .../enrich/markdown_render.py | 0 .../enrich/openalex.py | 0 .../enrich/references.py | 0 .../enrich/stage.py | 0 .../enrich/storage.py | 0 .../enrich/text.py | 0 src/{ohbm2026 => abstractatlas}/exceptions.py | 0 .../fetch/__init__.py | 0 .../fetch/graphql_api.py | 0 .../fetch/schema_diff.py | 0 .../fetch/stage.py | 0 .../layout/__init__.py | 0 .../layout/nocd_experiments.py | 0 .../layout/poster_layout.py | 0 .../layout/poster_sequencing.py | 0 src/{ohbm2026 => abstractatlas}/standby.py | 0 src/{ohbm2026 => abstractatlas}/titles.py | 0 .../ui_data/__init__.py | 0 .../ui_data/abstracts.py | 0 .../ui_data/authors.py | 0 .../ui_data/builder.py | 0 .../ui_data/cells.py | 0 .../ui_data/dimensions.py | 0 .../ui_data/enrichment.py | 0 .../ui_data/formats/__init__.py | 0 .../ui_data/formats/gzip_json_shards.py | 0 .../ui_data/formats/parquet_single.py | 0 .../ui_data/link_check.py | 0 .../ui_data/manifest.py | 0 .../ui_data/neighbors.py | 0 .../ui_data/questions.py | 0 .../ui_data/standby_slots.py | 0 .../ui_data/state_key.py | 0 .../ui_data/topics.py | 0 .../ui_data/types.py | 0 .../ui_data/vectors.py | 0 .../util/__init__.py | 0 .../util/json_io.py | 0 132 files changed, 1015 insertions(+), 1 deletion(-) create mode 100644 specs/027-abstractatlas-rename-ingestors/checklists/requirements.md create mode 100644 specs/027-abstractatlas-rename-ingestors/contracts/cli-aacli.md create mode 100644 specs/027-abstractatlas-rename-ingestors/contracts/ingest-schema.linkml.yaml create mode 100644 specs/027-abstractatlas-rename-ingestors/contracts/ingestor-interface.md create mode 100644 specs/027-abstractatlas-rename-ingestors/contracts/rename-map.md create mode 100644 specs/027-abstractatlas-rename-ingestors/data-model.md create mode 100644 specs/027-abstractatlas-rename-ingestors/plan.md create mode 100644 specs/027-abstractatlas-rename-ingestors/quickstart.md create mode 100644 specs/027-abstractatlas-rename-ingestors/research.md create mode 100644 specs/027-abstractatlas-rename-ingestors/spec.md create mode 100644 specs/027-abstractatlas-rename-ingestors/tasks.md rename src/{ohbm2026 => abstractatlas}/__init__.py (100%) rename src/{ohbm2026 => abstractatlas}/analyze/__init__.py (100%) rename src/{ohbm2026 => abstractatlas}/analyze/category_evaluation.py (100%) rename src/{ohbm2026 => abstractatlas}/analyze/category_rollup.py (100%) rename src/{ohbm2026 => abstractatlas}/analyze/centroids.py (100%) rename src/{ohbm2026 => abstractatlas}/analyze/clusters.py (100%) rename src/{ohbm2026 => abstractatlas}/analyze/communities.py (100%) rename src/{ohbm2026 => abstractatlas}/analyze/errors.py (100%) rename src/{ohbm2026 => abstractatlas}/analyze/llm_adapter.py (100%) rename src/{ohbm2026 => abstractatlas}/analyze/projections.py (100%) rename src/{ohbm2026 => abstractatlas}/analyze/provenance.py (100%) rename src/{ohbm2026 => abstractatlas}/analyze/rollup.py (100%) rename src/{ohbm2026 => abstractatlas}/analyze/runners.py (100%) rename src/{ohbm2026 => abstractatlas}/analyze/stage.py (100%) rename src/{ohbm2026 => abstractatlas}/analyze/storage.py (100%) rename src/{ohbm2026 => abstractatlas}/analyze/topic_clusters.py (100%) rename src/{ohbm2026 => abstractatlas}/analyze/topics.py (100%) rename src/{ohbm2026 => abstractatlas}/analyze/umap.py (100%) rename src/{ohbm2026 => abstractatlas}/artifacts.py (100%) rename src/{ohbm2026 => abstractatlas}/assets.py (100%) rename src/{ohbm2026 => abstractatlas}/atlas_hosting/__init__.py (100%) rename src/{ohbm2026 => abstractatlas}/atlas_hosting/cli.py (100%) rename src/{ohbm2026 => abstractatlas}/atlas_hosting/compare.py (100%) rename src/{ohbm2026 => abstractatlas}/atlas_hosting/content_hash.py (100%) rename src/{ohbm2026 => abstractatlas}/atlas_hosting/manifest.py (100%) rename src/{ohbm2026 => abstractatlas}/atlas_hosting/r2_client.py (100%) rename src/{ohbm2026 => abstractatlas}/atlas_hosting/uploader.py (100%) rename src/{ohbm2026 => abstractatlas}/atlas_package/__init__.py (100%) rename src/{ohbm2026 => abstractatlas}/atlas_package/cli.py (100%) rename src/{ohbm2026 => abstractatlas}/atlas_package/cluster_palette.py (100%) rename src/{ohbm2026 => abstractatlas}/atlas_package/decimation.py (100%) rename src/{ohbm2026 => abstractatlas}/atlas_package/link_check.py (100%) rename src/{ohbm2026 => abstractatlas}/atlas_package/lod.py (100%) rename src/{ohbm2026 => abstractatlas}/atlas_package/neighbour_index.py (100%) rename src/{ohbm2026 => abstractatlas}/atlas_package/neuroscape_loader.py (100%) rename src/{ohbm2026 => abstractatlas}/atlas_package/ohbm_projector.py (100%) rename src/{ohbm2026 => abstractatlas}/atlas_package/orchestrator.py (100%) rename src/{ohbm2026 => abstractatlas}/atlas_package/parquet_writer.py (100%) rename src/{ohbm2026 => abstractatlas}/atlas_package/provenance.py (100%) rename src/{ohbm2026 => abstractatlas}/atlas_package/semantic_index.py (100%) rename src/{ohbm2026 => abstractatlas}/atlas_package/umap_fit.py (100%) rename src/{ohbm2026 => abstractatlas}/atlas_package/vectors_compute.py (100%) rename src/{ohbm2026 => abstractatlas}/book/__init__.py (100%) rename src/{ohbm2026 => abstractatlas}/book/assemble_pdf.py (100%) rename src/{ohbm2026 => abstractatlas}/book/author_index.py (100%) rename src/{ohbm2026 => abstractatlas}/book/cache.py (100%) rename src/{ohbm2026 => abstractatlas}/book/cli.py (100%) rename src/{ohbm2026 => abstractatlas}/book/corpus.py (100%) rename src/{ohbm2026 => abstractatlas}/book/figure_check.py (100%) rename src/{ohbm2026 => abstractatlas}/book/html_to_md.py (100%) rename src/{ohbm2026 => abstractatlas}/book/model.py (100%) rename src/{ohbm2026 => abstractatlas}/book/provenance.py (100%) rename src/{ohbm2026 => abstractatlas}/book/render_markdown.py (100%) rename src/{ohbm2026 => abstractatlas}/book/render_per_abstract.py (100%) rename src/{ohbm2026 => abstractatlas}/book/render_via_pandoc.py (100%) rename src/{ohbm2026 => abstractatlas}/book/sections.py (100%) rename src/{ohbm2026 => abstractatlas}/book/sort.py (100%) rename src/{ohbm2026 => abstractatlas}/book/templates/.keep (100%) rename src/{ohbm2026 => abstractatlas}/book/templates/book.md.template (100%) rename src/{ohbm2026 => abstractatlas}/book/templates/header-includes-loose.tex (100%) rename src/{ohbm2026 => abstractatlas}/book/templates/header-includes-tufte.tex (100%) rename src/{ohbm2026 => abstractatlas}/book/templates/header-includes.tex (100%) rename src/{ohbm2026 => abstractatlas}/book/templates/per-abstract.tex.template (100%) rename src/{ohbm2026 => abstractatlas}/cli.py (100%) rename src/{ohbm2026 => abstractatlas}/data/__init__.py (100%) rename src/{ohbm2026 => abstractatlas}/data/__pycache__/__init__.cpython-314.pyc (100%) rename src/{ohbm2026 => abstractatlas}/data/eco_top_codes.json (100%) rename src/{ohbm2026 => abstractatlas}/embed/__init__.py (100%) rename src/{ohbm2026 => abstractatlas}/embed/components.py (100%) rename src/{ohbm2026 => abstractatlas}/embed/compose.py (100%) rename src/{ohbm2026 => abstractatlas}/embed/hf.py (100%) rename src/{ohbm2026 => abstractatlas}/embed/neuroscape.py (100%) rename src/{ohbm2026 => abstractatlas}/embed/openai.py (100%) rename src/{ohbm2026 => abstractatlas}/embed/provenance.py (100%) rename src/{ohbm2026 => abstractatlas}/embed/stage.py (100%) rename src/{ohbm2026 => abstractatlas}/embed/storage.py (100%) rename src/{ohbm2026 => abstractatlas}/embed/voyage.py (100%) rename src/{ohbm2026 => abstractatlas}/enrich/__init__.py (100%) rename src/{ohbm2026 => abstractatlas}/enrich/claims.py (100%) rename src/{ohbm2026 => abstractatlas}/enrich/figures.py (100%) rename src/{ohbm2026 => abstractatlas}/enrich/flex_tier.py (100%) rename src/{ohbm2026 => abstractatlas}/enrich/image_quality.py (100%) rename src/{ohbm2026 => abstractatlas}/enrich/markdown_render.py (100%) rename src/{ohbm2026 => abstractatlas}/enrich/openalex.py (100%) rename src/{ohbm2026 => abstractatlas}/enrich/references.py (100%) rename src/{ohbm2026 => abstractatlas}/enrich/stage.py (100%) rename src/{ohbm2026 => abstractatlas}/enrich/storage.py (100%) rename src/{ohbm2026 => abstractatlas}/enrich/text.py (100%) rename src/{ohbm2026 => abstractatlas}/exceptions.py (100%) rename src/{ohbm2026 => abstractatlas}/fetch/__init__.py (100%) rename src/{ohbm2026 => abstractatlas}/fetch/graphql_api.py (100%) rename src/{ohbm2026 => abstractatlas}/fetch/schema_diff.py (100%) rename src/{ohbm2026 => abstractatlas}/fetch/stage.py (100%) rename src/{ohbm2026 => abstractatlas}/layout/__init__.py (100%) rename src/{ohbm2026 => abstractatlas}/layout/nocd_experiments.py (100%) rename src/{ohbm2026 => abstractatlas}/layout/poster_layout.py (100%) rename src/{ohbm2026 => abstractatlas}/layout/poster_sequencing.py (100%) rename src/{ohbm2026 => abstractatlas}/standby.py (100%) rename src/{ohbm2026 => abstractatlas}/titles.py (100%) rename src/{ohbm2026 => abstractatlas}/ui_data/__init__.py (100%) rename src/{ohbm2026 => abstractatlas}/ui_data/abstracts.py (100%) rename src/{ohbm2026 => abstractatlas}/ui_data/authors.py (100%) rename src/{ohbm2026 => abstractatlas}/ui_data/builder.py (100%) rename src/{ohbm2026 => abstractatlas}/ui_data/cells.py (100%) rename src/{ohbm2026 => abstractatlas}/ui_data/dimensions.py (100%) rename src/{ohbm2026 => abstractatlas}/ui_data/enrichment.py (100%) rename src/{ohbm2026 => abstractatlas}/ui_data/formats/__init__.py (100%) rename src/{ohbm2026 => abstractatlas}/ui_data/formats/gzip_json_shards.py (100%) rename src/{ohbm2026 => abstractatlas}/ui_data/formats/parquet_single.py (100%) rename src/{ohbm2026 => abstractatlas}/ui_data/link_check.py (100%) rename src/{ohbm2026 => abstractatlas}/ui_data/manifest.py (100%) rename src/{ohbm2026 => abstractatlas}/ui_data/neighbors.py (100%) rename src/{ohbm2026 => abstractatlas}/ui_data/questions.py (100%) rename src/{ohbm2026 => abstractatlas}/ui_data/standby_slots.py (100%) rename src/{ohbm2026 => abstractatlas}/ui_data/state_key.py (100%) rename src/{ohbm2026 => abstractatlas}/ui_data/topics.py (100%) rename src/{ohbm2026 => abstractatlas}/ui_data/types.py (100%) rename src/{ohbm2026 => abstractatlas}/ui_data/vectors.py (100%) rename src/{ohbm2026 => abstractatlas}/util/__init__.py (100%) rename src/{ohbm2026 => abstractatlas}/util/json_io.py (100%) diff --git a/.specify/feature.json b/.specify/feature.json index 49770e5f..128bb9ad 100644 --- a/.specify/feature.json +++ b/.specify/feature.json @@ -1,3 +1,3 @@ { - "feature_directory": "specs/026-neuroscape-year-density" + "feature_directory": "specs/027-abstractatlas-rename-ingestors" } diff --git a/specs/027-abstractatlas-rename-ingestors/checklists/requirements.md b/specs/027-abstractatlas-rename-ingestors/checklists/requirements.md new file mode 100644 index 00000000..1166a90a --- /dev/null +++ b/specs/027-abstractatlas-rename-ingestors/checklists/requirements.md @@ -0,0 +1,36 @@ +# Specification Quality Checklist: Abstract Atlas Rename + Pluggable LinkML Ingestors + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-07-02 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- Three scope-setting decisions pre-resolved with the requester: build = foundation + port existing (no new sources); rename reach = code/CLI/package/docs with data preserved (no re-publish, byte-identical); CLI name = `aacli`. +- Deliberately bounded: new-source ingestors (arXiv/bioRxiv/medRxiv/other conferences) are explicit non-goals / follow-on specs. +- Large blast radius (~86 Python files reference `ohbm2026`) — the rename risk is mitigated by SC-001/SC-002 (identical artifacts + full suite passing) and the data-preservation constraint (FR-004). diff --git a/specs/027-abstractatlas-rename-ingestors/contracts/cli-aacli.md b/specs/027-abstractatlas-rename-ingestors/contracts/cli-aacli.md new file mode 100644 index 00000000..115886bf --- /dev/null +++ b/specs/027-abstractatlas-rename-ingestors/contracts/cli-aacli.md @@ -0,0 +1,45 @@ +# Contract: `aacli` command surface + deprecation + +`aacli` replaces `ohbmcli` with an identical subcommand surface and options +(FR-002). Behavior is byte-for-byte the former `ohbmcli` for every existing +subcommand (SC-001). + +## Surface + +- `aacli ` — every existing subcommand (fetch-abstracts, + fetch-withdrawn, refresh-assets, enrich-abstracts, title-audit, + embed-matrix, semantic-analysis, cluster-benchmark, umap-plot, + compare-projections, optimize-projections, build-atlas-package, + upload-atlas-package, compare-data-hosting, book, write-manifest, …) — + unchanged names, args, and behavior. +- Module form: `python -m abstractatlas.cli ` (docs/CI updated + from `-m ohbm2026.cli`). + +## New ingestor-facing surface (additive) + +- `aacli ingest --source ` — run a registered ingestor by name + (e.g. `ohbm-2026`, `neuroscape-pubmed`). Unknown name → precise error + listing registered names (FR-010). +- `aacli list-ingestors` — print the runtime-discovered registry (names + + source_type). Optional convenience; the existing per-source subcommands + (`fetch-abstracts`, `build-atlas-package`) continue to work unchanged and + may delegate to their ingestor internally. + +> The existing subcommands remain the canonical way to run the two current +> sources (no behavior change); `ingest`/`list-ingestors` are the general +> surface that makes future sources first-class without new bespoke +> subcommands. + +## Deprecation + +- `ohbmcli ` → stderr: `ohbmcli is deprecated; use 'aacli'. Delegating…` + then runs `aacli `. Removed next cycle. +- `python -m ohbm2026.cli` → shim module warns + delegates. + +## Verification + +- SC-001: for each existing subcommand, `aacli ` output + written + artifacts identical to the pre-rename `ohbmcli ` on fixtures. +- `aacli ingest --source ohbm-2026` reproduces `fetch-abstracts` normalized + output; `--source nope` errors with the known-names list. +- `ohbmcli` emits the deprecation notice and still succeeds (SC-007). diff --git a/specs/027-abstractatlas-rename-ingestors/contracts/ingest-schema.linkml.yaml b/specs/027-abstractatlas-rename-ingestors/contracts/ingest-schema.linkml.yaml new file mode 100644 index 00000000..f758f0b6 --- /dev/null +++ b/specs/027-abstractatlas-rename-ingestors/contracts/ingest-schema.linkml.yaml @@ -0,0 +1,84 @@ +id: https://abstractatlas.brainkb.org/schemas/ingest +name: abstractatlas-ingest +title: Abstract Atlas standardized ingest schema +description: >- + The normalized record contract every ingestor emits and every downstream + stage consumes. A common core (IngestedDocument) plus source-type + extensions (ConferenceDocument, LiteratureDocument). Designed to CAPTURE + the existing OHBM normalized shape so the ported output validates + unchanged (spec 027, FR-007/FR-008) — it documents/validates, it does not + reshape already-published data. +default_range: string +prefixes: + linkml: https://w3id.org/linkml/ + aa: https://abstractatlas.brainkb.org/schemas/ingest/ +default_prefix: aa +imports: + - linkml:types + +enums: + SourceType: + permissible_values: + conference: {description: "A conference/meeting abstract source (e.g. OHBM via Oxford Abstracts)."} + literature_index: {description: "A literature index/corpus source (e.g. PubMed/NeuroScape; future arXiv/bioRxiv/medRxiv)."} + +classes: + SourceProvenance: + description: Which ingestor/source produced a record. + attributes: + ingestor: {required: true, description: "Registered ingestor name, e.g. ohbm-2026."} + source_type: {required: true, range: SourceType} + origin: {required: true, description: "Origin identifier, e.g. oxford-abstracts, pubmed. No absolute/user-home paths."} + retrieved_at: {range: date} + + Author: + attributes: + name: {required: true} + affiliation: {} + order: {range: integer} + + ProgramSession: + description: Flattened conference program/session membership (preserves the OHBM program_sessions shape). + attributes: + session_id: {} + session_title: {} + role: {} + + IngestedDocument: + description: Common core emitted by every ingestor. + slots: [doc_id, source, title, authors, abstract_text, year] + slot_usage: + doc_id: {required: true} + source: {required: true} + title: {required: true} + authors: {required: true, multivalued: true, inlined_as_list: true} + abstract_text: {required: true} + + ConferenceDocument: + is_a: IngestedDocument + description: A conference abstract. source.source_type MUST be `conference`. + attributes: + poster_id: {description: "Preserved OHBM program_code mapping."} + program_sessions: {multivalued: true, inlined_as_list: true, range: ProgramSession} + + LiteratureDocument: + is_a: IngestedDocument + description: A literature-index record. source.source_type MUST be `literature_index`. + attributes: + doi: {} + venue: {description: "Journal or preprint server."} + index_id: {description: "Source index id, e.g. PubMed id, arXiv id (future)."} + +slots: + doc_id: {description: "Stable within-source identity."} + source: {range: SourceProvenance} + title: {} + authors: {range: Author} + abstract_text: {description: "Abstract/summary body."} + year: {range: integer} + +# Validation intent (enforced by ingest/schema.py + linkml validator, FR-008): +# - core required slots present; +# - a ConferenceDocument's source.source_type == conference (and vice versa +# for LiteratureDocument) — prevents source masquerade (spec edge case); +# - malformed → precise, source-attributed error; not passed downstream. diff --git a/specs/027-abstractatlas-rename-ingestors/contracts/ingestor-interface.md b/specs/027-abstractatlas-rename-ingestors/contracts/ingestor-interface.md new file mode 100644 index 00000000..dce51d2f --- /dev/null +++ b/specs/027-abstractatlas-rename-ingestors/contracts/ingestor-interface.md @@ -0,0 +1,67 @@ +# Contract: Ingestor interface + registry + +Module: `src/abstractatlas/ingest/{base,registry,schema}.py`. Additive — +wraps existing normalization, changes no downstream stage. + +## `Ingestor` (ABC) + +```python +class SourceType(str, Enum): + conference = "conference" + literature_index = "literature_index" + +class Ingestor(ABC): + name: str # unique, registered (e.g. "ohbm-2026") + source_type: SourceType + + @abstractmethod + def pull(self, **opts) -> RawRecords: ... + # acquire raw records from the origin; resumable/checkpointed as the + # existing stages already are (Constitution III). No behavior change. + + @abstractmethod + def normalize(self, raw: RawRecords) -> Iterable[IngestedDocument]: ... + # source-specific mapping to the standardized schema; WRAPS existing + # logic (assets.normalize_abstract for OHBM; NeuroScape record map). + + def ingest(self, **opts) -> IngestResult: + # pull → normalize → validate(schema) → write; attaches SourceProvenance. + # Validation failure raises a precise, source-attributed typed error + # (no silent skip / partial success — Constitution VI, FR-008). +``` + +## Registry (runtime discovery — Constitution VII / FR-010) + +```python +@register # or entry-point based +class ConferenceOHBMIngestor(Ingestor): ... + +registry.names() # -> ["neuroscape-pubmed", "ohbm-2026"] (discovered, not hardcoded) +registry.get("xyz") # -> raises IngestorNotFound("unknown ingestor 'xyz'; known: [...]") +``` + +- No downstream stage may hardcode a source list; they consume + `IngestedDocument` generically. +- Adding a new ingestor = implement the ABC + register → runnable via CLI, + zero downstream edits (SC-006). + +## First two instances (ports — SC-003/SC-004) + +| Ingestor | name | source_type | wraps | output | +|----------|------|-------------|-------|--------| +| Conference (OHBM) | `ohbm-2026` | conference | `fetch/stage.py` + `assets.normalize_abstract` | byte-identical `data/primary/abstracts.json` | +| Literature (NeuroScape) | `neuroscape-pubmed` | literature_index | NeuroScape record normalization (atlas_package inputs) | unchanged | + +## Required tests (failing-first — CA-002) + +1. `registry.names()` includes both ported ingestors; `get(unknown)` raises with the known list. +2. Each ingestor's `normalize` output validates against the LinkML schema. +3. A deliberately malformed record → precise source-attributed validation error; not written downstream. +4. Port fidelity: `ConferenceOHBMIngestor` reproduces the prior normalized OHBM corpus byte-for-byte on a fixture. +5. Registry registration is idempotent + discovered at runtime (no hardcoded list). + +## Typed errors + +Extend the existing `OhbmStageError`→(renamed) exception hierarchy with an +`IngestError` subtree: `IngestorNotFound`, `IngestSchemaValidationError` +(carries source + offending field), consistent with the fail-loud posture. diff --git a/specs/027-abstractatlas-rename-ingestors/contracts/rename-map.md b/specs/027-abstractatlas-rename-ingestors/contracts/rename-map.md new file mode 100644 index 00000000..690debe0 --- /dev/null +++ b/specs/027-abstractatlas-rename-ingestors/contracts/rename-map.md @@ -0,0 +1,44 @@ +# Contract: rename map (identity change, data preserved) + +The authoritative old→new mapping. **Data names are deliberately NOT +renamed** (FR-004). Verified by SC-001 (identical artifacts) + SC-002 (suite +green) + SC-003 (byte-identical published data). + +## Renamed (code / CLI / docs) + +| Kind | Old | New | +|------|-----|-----| +| Python package | `src/ohbm2026/` | `src/abstractatlas/` (`git mv`) | +| Import root | `ohbm2026.*` | `abstractatlas.*` | +| pyproject `name` | `ohbm2026` | `abstractatlas` | +| Canonical CLI | `ohbmcli` | `aacli` | +| Module invocation | `python -m ohbm2026.cli` | `python -m abstractatlas.cli` | +| Legacy scripts | `ohbm-ingest`, `ohbm-authors`, `ohbm-enrich`, `ohbm-analyze-figures`, `ohbm-embed-stage2`, `ohbm-apply-published-stage2`, `ohbm-cluster-benchmark`, `ohbm-semantic-analysis`, `ohbm-umap-plot`, `ohbm-compare-projections`, `ohbm-optimize-projections`, `ohbm-analyze-stage2`, `ohbm-reference-metadata`, `ohbm-write-manifest` | `aa-*` **iff still used**; otherwise dropped (functions stay importable). Disposition recorded per script in tasks. | +| Docs | README, CLAUDE.md, constitution naming refs | updated to `abstractatlas`/`aacli` (FR-012) | +| Site test strings | `ohbm2026` refs in `site/src/**` tests | updated where they name the package (NOT the `ohbm2026` data/site-mode/route which is the source identity) | + +## Deprecated shims (one transition period) + +| Legacy | Behavior | +|--------|----------| +| `ohbmcli` entry point | prints "renamed to `aacli`" deprecation notice, then delegates to `aacli` | +| `import ohbm2026` | thin shim re-exporting `abstractatlas` with a `DeprecationWarning`; labeled + follow-up to remove | + +Shims MUST be labeled in code with root cause + removal follow-up +(Constitution VI). Neither may partially work in a confusing way (FR-003). + +## NOT renamed (data identity — FR-004) + +| Preserved | Why | +|-----------|-----| +| `data/primary/`, `data/cache/`, `data/inputs/`, `data/outputs/` paths | no data regeneration | +| state-key / cache-key / checkpoint naming (incl. embedded `ohbm2026`) | prior expensive work stays discoverable/resumable | +| published `ohbm2026.parquet` (+ site data-package names) | byte-identical site data; no re-publish; the name is the OHBM *source's* data identity, not the component | +| `/ohbm2026/` site route, `SITE_MODE='ohbm'`, `OHBM2026_*` CI vars | source/deployment identity, out of rename scope | + +## Verification hooks + +- SC-001: fixture run under `aacli` produces artifacts identical (path+content) to a pre-rename run. +- SC-002: `.venv/bin/python -m unittest discover -s tests` fully green under new names. +- SC-007: invoking `ohbmcli` / `import ohbm2026` emits the labeled deprecation, never a silent/partial success. +- `git grep -n 'ohbm2026\|ohbmcli'` after the rename returns only: (a) intentional data-identity names in the NOT-renamed table, (b) the deprecation shims, (c) historical spec docs. diff --git a/specs/027-abstractatlas-rename-ingestors/data-model.md b/specs/027-abstractatlas-rename-ingestors/data-model.md new file mode 100644 index 00000000..f2bec1d8 --- /dev/null +++ b/specs/027-abstractatlas-rename-ingestors/data-model.md @@ -0,0 +1,85 @@ +# Phase 1 Data Model: Abstract Atlas Rename + Pluggable LinkML Ingestors + +No change to persisted data (FR-004). The "model" here is (a) the ingest +schema entities and (b) the code-level contract objects. The full schema +lives in `contracts/ingest-schema.linkml.yaml`; this is the conceptual view. + +## Ingest schema entities (LinkML) + +### IngestedDocument (core) +The normalized record every ingestor emits. Downstream stages consume this. + +| Slot | Type | Req | Notes | +|------|------|-----|-------| +| `doc_id` | string | ✓ | stable within-source identity | +| `source` | SourceProvenance | ✓ | which ingestor/source produced it | +| `title` | string | ✓ | | +| `authors` | Author[] | ✓ | ordered; may be empty for some sources | +| `abstract_text` | string | ✓ | abstract / summary body (markdown-ish) | +| `year` | integer | ○ | publication/presentation year when known | + +### SourceProvenance +| Slot | Type | Req | Notes | +|------|------|-----|-------| +| `ingestor` | string | ✓ | registered ingestor name (e.g. `ohbm-2026`, `neuroscape-pubmed`) | +| `source_type` | SourceType enum | ✓ | `conference` \| `literature_index` | +| `origin` | string | ✓ | origin identifier (e.g. `oxford-abstracts`, `pubmed`); no absolute/home paths | +| `retrieved_at` | date | ○ | when pulled | + +### ConferenceDocument (extends IngestedDocument) +| Slot | Type | Notes | +|------|------|-------| +| `poster_id` | string | preserved OHBM `program_code` mapping | +| `program_sessions` | ProgramSession[] | flattened session/program membership | + +### LiteratureDocument (extends IngestedDocument) +| Slot | Type | Notes | +|------|------|-------| +| `doi` | string | when available | +| `venue` | string | journal / server | +| `index_id` | string | e.g. PubMed id, arXiv id (future) | + +Validation rules (enforced at ingest, FR-008): +- Core required slots present; `source.source_type` matches the concrete + class (a `ConferenceDocument` MUST have `source_type == conference`). +- A conference record MUST NOT carry literature-only slots and vice versa + (prevents source masquerade — edge case). +- Malformed → precise, source-attributed error; record not passed downstream. + +## Code-level contract objects + +### Ingestor (ABC / Protocol) — `abstractatlas/ingest/base.py` +| Member | Signature | Notes | +|--------|-----------|-------| +| `name` | `str` | unique registered name | +| `source_type` | `SourceType` | conference \| literature_index | +| `pull(...)` | `-> RawRecords` | acquire raw records from the origin (resumable/checkpointed as today) | +| `normalize(raw)` | `-> Iterable[IngestedDocument]` | source-specific normalization (wraps existing logic) | +| `ingest(...)` | `-> IngestResult` | pull → normalize → validate(schema) → write; carries provenance | + +### IngestorRegistry — `abstractatlas/ingest/registry.py` +| Member | Signature | Notes | +|--------|-----------|-------| +| `register(ingestor)` | | idempotent registration (decorator/entry-point) | +| `get(name)` | `-> Ingestor` | precise error listing known names if absent (VII) | +| `names()` | `-> list[str]` | runtime-discovered catalog for the CLI | + +### First two instances (ports) +- `ConferenceOHBMIngestor` (`name="ohbm-2026"`, `conference`) — wraps + `fetch/stage.py` + `assets.normalize_abstract`; output byte-identical to + today's `data/primary/abstracts.json`. +- `LiteratureNeuroscapeIngestor` (`name="neuroscape-pubmed"`, + `literature_index`) — wraps the NeuroScape record normalization consumed + by `atlas_package`; output unchanged. + +## Rename mapping (identity, not data) + +| Old | New | Notes | +|-----|-----|-------| +| package `ohbm2026` | `abstractatlas` | `git mv` + import rewrite | +| CLI `ohbmcli` | `aacli` | canonical; `ohbmcli` = deprecated shim | +| entry points `ohbm-*` | `aa-*` (if used) / dropped | per rename-map | +| `import ohbm2026` | `import abstractatlas` | `ohbm2026` = deprecation shim | +| data paths, state-keys, `ohbm2026.parquet` | **UNCHANGED** | FR-004 — source data identity preserved | + +No published data bytes change; only code/CLI/doc identity changes. diff --git a/specs/027-abstractatlas-rename-ingestors/plan.md b/specs/027-abstractatlas-rename-ingestors/plan.md new file mode 100644 index 00000000..dc525ad7 --- /dev/null +++ b/specs/027-abstractatlas-rename-ingestors/plan.md @@ -0,0 +1,125 @@ +# Implementation Plan: Abstract Atlas Rename + Pluggable LinkML Ingestors + +**Branch**: `027-abstractatlas-rename-ingestors` | **Date**: 2026-07-02 | **Spec**: [spec.md](./spec.md) +**Input**: Feature specification from `/specs/027-abstractatlas-rename-ingestors/spec.md` + +## Summary + +Two coupled changes, sequenced so the risky-but-mechanical part lands and +is verified before the additive part: + +1. **Rename** the Python component `ohbm2026` → `abstractatlas` and the CLI + `ohbmcli` → `aacli` (plus the legacy `ohbm-*` entry points), updating + every import, string reference, doc, and test — while **preserving all + on-disk artifact paths, state-key/cache naming, and published + data-package names** (incl. `ohbm2026.parquet`) so no data is + regenerated and the live site needs no re-publish (byte-identical). +2. **Generalize ingestion** into a pluggable architecture: a new + `abstractatlas/ingest/` subpackage with an `Ingestor` contract + a + runtime-discoverable registry, and a **standardized LinkML ingest + schema** (common core + per-source-type extensions) that every ingestor + validates against. The two existing sources — the OHBM conference + (Oxford Abstracts, via `fetch/` + `assets.normalize_abstract`) and the + PubMed/NeuroScape corpus (via `atlas_package` inputs) — are **wrapped** + as the first two ingestor instances *without changing their outputs* or + any downstream stage. + +New source ingestors (arXiv/bioRxiv/medRxiv, other conferences) are +explicitly out of scope — the foundation makes them straightforward +follow-on specs. + +## Technical Context + +**Language/Version**: Python 3.14 via repo-local `.venv` (uv-managed); TypeScript site is only touched for a handful of `ohbm2026` string refs in tests +**Primary Dependencies**: existing (Oxford Abstracts GraphQL client, OpenAI, boto3, pyarrow, hyparquet on the site); **LinkML** for the ingest schema + validation (already the schema language for UI/export contracts — `specs/008/010/012/*.linkml.yaml`, validated via `scripts/validate_ui_data.sh`) +**Storage**: unchanged — `data/primary/`, `data/cache/`, `data/outputs/`, published parquet names all preserved (FR-004) +**Testing**: `.venv/bin/python -m unittest discover -s tests` (renamed test imports) + new unit tests for the ingestor registry/contract and LinkML validation; site `vitest run` for the few renamed string refs +**Target Platform**: local pipeline (CLI) + static site; no runtime/platform change +**Project Type**: Python package + CLI (Track A canonical pipeline) with a coupled SvelteKit site +**Performance Goals**: none changed — the rename is behavior-preserving; the ingestor wrap adds a validation pass over already-normalized records (bounded, one-time per ingest run) +**Constraints**: byte-identical published data (no re-publish); zero downstream-stage behavior change for existing sources; legacy names fail loudly or via a labeled-deprecated alias (no partial success) +**Scale/Scope**: ~86 Python files reference `ohbm2026`; ~15 `ohbm-*` entry points + `ohbmcli`; 3 governance/doc surfaces (README, CLAUDE.md, constitution) + a few site test strings; 2 sources ported + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +- **Venv-only Python** — all commands run through `.venv/bin/python` / `uv`; the rename updates `PYTHONPATH=src -m ohbm2026.cli` → `-m abstractatlas.cli` in docs/CI, still venv-scoped. PASS. +- **Immutable evidence / no committed data** — the rename **preserves** raw + derived artifact paths and published data names; no data is rewritten or regenerated; artifact roots stay gitignored. This is the central FR-004 guarantee. PASS. +- **Resumable pipelines** — state-key/cache/checkpoint keys are preserved verbatim, so prior expensive work is still discovered and reused after the rename (edge case). PASS. +- **Plan-first, test-first** — spec + this plan precede code; verification identified: full suite passes under new names (SC-002), ingestor/registry + LinkML-validation tests added failing-first (CA-002). PASS. +- **Secret-safe** — no credential change; env-var names for Oxford Abstracts / OpenAI / data host unchanged; commit in verified slices. PASS. +- **Fail loudly, no shortcuts** — schema-validation failures, unknown-ingestor lookups, and legacy-name usage raise precise typed errors (no silent skip/partial success); no `--no-verify`/skipped tests to force the rename green. PASS. +- **Discover external state** — the ingestor registry is runtime-discovered from registration/metadata, never a hardcoded source allow-list; unknown source → precise error naming what was searched (FR-010/CA-007). PASS. +- **Provenance** — each ingested record carries source/ingestor provenance; existing artifact provenance is preserved and remains interpretable; no absolute/home paths (FR-011/CA-008). PASS. +- **Docs in same change** — README, CLAUDE.md, and the constitution's naming references updated in the same change (FR-012); constitution naming refs coordinated with `/speckit-constitution` where an amendment is required. PASS. +- **Commit per slice + push** — sequenced commits: (a) rename, (b) ingest package + registry + schema, (c) two ports, (d) docs; push the branch. PASS. + +No violations. Complexity is high (blast radius) but not a constitution violation → Complexity Tracking notes the sequencing risk, not a gate failure. + +## Project Structure + +### Documentation (this feature) + +```text +specs/027-abstractatlas-rename-ingestors/ +├── plan.md # This file +├── spec.md # Feature spec +├── research.md # Phase 0 output +├── data-model.md # Phase 1 output +├── quickstart.md # Phase 1 output +├── contracts/ +│ ├── rename-map.md # old→new name mapping (package, CLI, entry points, docs) + legacy-alias policy +│ ├── ingestor-interface.md # the Ingestor contract + runtime registry +│ ├── ingest-schema.linkml.yaml # the standardized ingest schema (core + per-source extensions) +│ └── cli-aacli.md # renamed command surface + deprecation behavior +└── checklists/ + └── requirements.md +``` + +### Source Code (repository root) + +```text +src/abstractatlas/ # RENAMED from src/ohbm2026/ (all modules moved, imports rewritten) +├── cli.py # dispatch unchanged in behavior; module path renamed +├── ingest/ # NEW subpackage +│ ├── base.py # Ingestor ABC/Protocol (pull → normalize → validated records) + source-type enum +│ ├── registry.py # runtime-discoverable name→ingestor registry (VII) +│ ├── schema.py # loads + validates against contracts/ingest-schema.linkml.yaml +│ ├── conference_ohbm.py # wraps fetch/stage.py + assets.normalize_abstract (source: oxford-abstracts) +│ └── literature_neuroscape.py # wraps the NeuroScape/PubMed record normalization (source: pubmed-neuroscape) +├── fetch/ assets.py enrich/ embed/ analyze/ ui_data/ atlas_package/ atlas_hosting/ book/ layout/ util/ data/ exceptions.py artifacts.py titles.py standby.py +│ # all preserved; imports rewritten ohbm2026.* → abstractatlas.* + +tests/ # imports rewritten; behavior-identical +├── test_ingest_registry.py # NEW — registry runtime discovery + unknown-source error +├── test_ingest_schema.py # NEW — LinkML validation accepts valid / rejects malformed (loud) +└── test_ingestor_ports.py # NEW — OHBM + NeuroScape ingestors reproduce prior normalized output + +pyproject.toml # name ohbm2026→abstractatlas; scripts ohbmcli→aacli, ohbm-*→aa-* (or dropped); package-dir find +README.md, CLAUDE.md, .specify/memory/constitution.md # naming references updated (FR-012) +site/src/** # only ohbm2026 string refs in a few tests updated +``` + +**Structure Decision**: Keep the existing module layout intact under the +renamed package (a move + import rewrite, not a re-architecture) so the +rename stays mechanical and diff-reviewable. Ingestion generalization is +**additive**: a new `abstractatlas/ingest/` subpackage introduces the +contract, registry, and schema, and *wraps* the existing OHBM and +NeuroScape normalization rather than rewriting it — guaranteeing byte- +identical outputs and zero downstream change (SC-003/SC-004/FR-009). The +published parquet keeps the historical `ohbm2026.parquet` name (documented +divergence, FR-004). + +## Complexity Tracking + +> No Constitution Check violations. Recorded here: the rename's large blast +> radius (~86 files) is the main risk. Mitigation: land the rename as an +> isolated, mechanical, fully-verified slice (SC-001 identical artifacts + +> SC-002 full suite green) BEFORE the additive ingestor work, so a rename +> regression can't be confused with an architecture regression. + +| Risk | Why it's needed | Mitigation | +|------|-----------------|------------| +| Package/CLI rename touches ~86 files + entry points + docs + CI | The whole ask is re-framing the component name away from a single instance | Mechanical move + import rewrite in one slice; verify identical artifacts + full suite before any ingestor change; preserve all data names | +| Ingestor wrap could drift OHBM/NeuroScape outputs | Multi-source needs a common contract | Wrap (not rewrite) existing normalization; port test asserts byte-identical prior output | diff --git a/specs/027-abstractatlas-rename-ingestors/quickstart.md b/specs/027-abstractatlas-rename-ingestors/quickstart.md new file mode 100644 index 00000000..8ae7c675 --- /dev/null +++ b/specs/027-abstractatlas-rename-ingestors/quickstart.md @@ -0,0 +1,71 @@ +# Quickstart: Abstract Atlas Rename + Pluggable LinkML Ingestors + +Python component change (Track A) + a few site test-string updates. Data is +preserved — no regeneration, no re-publish. + +## Prerequisites + +```bash +UV_CACHE_DIR=.uv-cache uv venv --python 3.14 .venv +uv pip install --python .venv/bin/python linkml # ingest-schema validation (already used for UI contracts) +``` + +## Implement (sequenced so the mechanical rename is verified first) + +### Slice 1 — Rename (mechanical, verify identical before proceeding) + +1. `git mv src/ohbm2026 src/abstractatlas`. +2. Rewrite imports/refs `ohbm2026`→`abstractatlas` across `src/`, `tests/`, + `scripts/`, docs; update `pyproject.toml` (`name`, `[project.scripts]`: + `aacli = "abstractatlas.cli:main"`, `ohbm-*`→`aa-*`/drop per rename-map), + and `PYTHONPATH=src -m ohbm2026.cli` → `-m abstractatlas.cli` in docs/CI. +3. Add the deprecation shims: `ohbmcli` delegating entry point + an + `ohbm2026` import shim (labeled, follow-up to remove). +4. **Preserve** all data names (rename-map "NOT renamed" table) — do NOT + touch `data/**` paths, state-keys, or `ohbm2026.parquet`. +5. Verify BEFORE any ingestor work: + + ```bash + PYTHONPATH=src .venv/bin/python -m unittest discover -s tests # SC-002 + # SC-001: run a fixture subcommand under aacli, diff artifacts vs a pre-rename run + git grep -n 'ohbm2026\|ohbmcli' # only data-identity names, shims, historical specs remain + ``` + +### Slice 2 — Ingest package (tests first) + +6. Write failing tests (`tests/test_ingest_registry.py`, + `test_ingest_schema.py`) per `contracts/ingestor-interface.md`. +7. Add `abstractatlas/ingest/{base,registry,schema}.py` + + `contracts/ingest-schema.linkml.yaml` (copy into the package or load from + contracts) to green them. + +### Slice 3 — Port the two sources + +8. Write `tests/test_ingestor_ports.py` (OHBM output byte-identical) — + failing first. +9. Add `ingest/conference_ohbm.py` (wraps `fetch/stage.py` + + `assets.normalize_abstract`) and `ingest/literature_neuroscape.py` + (wraps NeuroScape normalization); register both. Green the port tests. +10. Wire `aacli ingest --source` / `list-ingestors`; existing subcommands + unchanged. + +### Slice 4 — Docs + +11. Update README, CLAUDE.md, and constitution naming references (FR-012); + document the intentional `ohbm2026.parquet` divergence. + +## Verify + +```bash +PYTHONPATH=src .venv/bin/python -m unittest discover -s tests # full suite green (SC-002) +PYTHONPATH=src .venv/bin/python -m unittest tests.test_ingest_registry tests.test_ingest_schema tests.test_ingestor_ports -v +.specify/scripts/bash/constitution-check.sh --full +cd site && pnpm exec vitest run # renamed string refs +``` + +## Done-when + +- Full suite green under `abstractatlas`/`aacli`; fixture artifacts identical to pre-rename (SC-001/002). +- Both sources are registered ingestors; ports touched zero downstream stage logic (SC-004); outputs validate against the schema (SC-005). +- Published data byte-identical, no re-publish (SC-003); legacy names emit labeled deprecation (SC-007). +- `git grep 'ohbm2026'` shows only intentional data-identity names, shims, and historical specs. diff --git a/specs/027-abstractatlas-rename-ingestors/research.md b/specs/027-abstractatlas-rename-ingestors/research.md new file mode 100644 index 00000000..aacf7121 --- /dev/null +++ b/specs/027-abstractatlas-rename-ingestors/research.md @@ -0,0 +1,124 @@ +# Phase 0 Research: Abstract Atlas Rename + Pluggable LinkML Ingestors + +The three highest-impact unknowns were resolved with the requester before +planning (build scope, rename reach, CLI name). The remaining decisions are +recorded here. + +## R1 — Rename mechanics: move + import-rewrite, verified as a mechanical slice + +**Decision**: Rename by (a) `git mv src/ohbm2026 src/abstractatlas`, (b) a +mechanical rewrite of `ohbm2026` → `abstractatlas` across imports and string +references, (c) `pyproject.toml` package name + entry points, (d) docs + +tests + CI `PYTHONPATH`/`-m` invocations. Land it as ONE slice and prove it +with SC-001 (identical artifacts on a fixture run) + SC-002 (full suite +green) before any ingestor work. + +**Rationale**: The rename is broad (~86 files) but mechanical; isolating it +keeps the diff reviewable and lets a regression be attributed to the rename, +not the architecture. `git mv` preserves history. + +**Alternatives considered**: +- *Rename incrementally alongside the ingestor work* — rejected: entangles a + mechanical rename with a design change, making regressions ambiguous. +- *Keep `ohbm2026` as the package, only rename the CLI* — rejected: the ask + is to re-frame the whole component; a half-rename leaves the confusing + single-instance name in every import. + +## R2 — Data-name preservation vs. package rename (the intentional divergence) + +**Decision**: Preserve every on-disk path, state-key/cache/checkpoint key, +and published data-package name — including the historical `ohbm2026.parquet` +— verbatim. The package is `abstractatlas`; the *OHBM source's* data keeps +its `ohbm2026` name because that is the name of that source's data, not the +component. Document this divergence prominently (README + rename-map). + +**Rationale**: The requester chose "preserve data" → no regeneration, no +site re-publish, byte-identical (FR-004, SC-003). Data names are a source +identity, not the component identity, so the divergence is correct, not +debt. Renaming published names would force a data rebuild + site loader/URL +registry changes — explicitly out of scope. + +**Alternatives considered**: rename data names too (the "Full rename" +option) — rejected by the requester for continuity. + +## R3 — Legacy name policy (`ohbmcli`, `import ohbm2026`) + +**Decision**: `aacli` / `abstractatlas` are canonical. Provide a +**deprecated shim** for one transition period: `ohbmcli` remains as an entry +point that prints a clear "renamed to `aacli`" deprecation notice then +delegates; `import ohbm2026` resolves via a thin shim module that re-exports +`abstractatlas` with a `DeprecationWarning`. Both are labeled and slated for +removal. + +**Rationale**: Avoids silently breaking existing scripts/cron while making +the new name canonical (FR-003, SC-007, edge case). A hard break is also +acceptable per the spec, but a labeled shim is lower-friction and still +loud. The shim carries a root-cause comment + follow-up per Constitution VI. + +**Alternatives considered**: hard removal with an error — acceptable but +more disruptive; chose the shim as the default, removable next cycle. + +## R4 — Ingestor contract: wrap, don't rewrite + +**Decision**: `abstractatlas/ingest/base.py` defines an `Ingestor` ABC: +`name`, `source_type` (enum: `conference` | `literature_index`), +`pull()` (acquire raw records from the origin), `normalize(raw)` → records +conforming to the ingest schema, and `ingest()` orchestrating pull → +normalize → validate. The OHBM ingestor wraps the existing +`fetch/stage.py` + `assets.normalize_abstract`; the NeuroScape ingestor +wraps the existing NeuroScape record normalization used by `atlas_package`. +Neither rewrites logic — they adapt existing code behind the contract. + +**Rationale**: Wrapping guarantees byte-identical outputs (SC-003) and zero +downstream change (SC-004/FR-009). The contract is the seam future sources +implement; the two ports are the worked examples (SC-006). + +**Alternatives considered**: rewrite both ingestion paths into a unified +new pipeline — rejected: high risk of output drift, violates the +data-preservation constraint, and is far larger than the foundation scope. + +## R5 — Registry: runtime discovery, not a hardcoded list + +**Decision**: `registry.py` maintains a name→`Ingestor` mapping populated by +registration (decorator/entry-point style), queried at runtime by the CLI + +orchestration. An unknown ingestor name raises a precise error listing the +registered names. No hardcoded source allow-list anywhere downstream. + +**Rationale**: Constitution VII / FR-010 — onboarding a new source must not +require editing a central list or downstream stages; mismatches surface +loudly. + +## R6 — Standardized ingest schema in LinkML + +**Decision**: `contracts/ingest-schema.linkml.yaml` defines an +`IngestedDocument` core class (identity, title, authors, abstract/summary +text, `source` provenance block) plus source-type extension classes/mixins: +`ConferenceDocument` (program/session/poster identifiers) and +`LiteratureDocument` (DOI, venue, index id, year). Each ingestor validates +its emitted records against the schema (via the `linkml` +validator, mirroring `scripts/validate_ui_data.sh`). The core is designed to +**capture the existing OHBM normalized shape** so the ported output +validates unchanged — the schema documents/validates, it does not reshape. + +**Rationale**: LinkML is already the project's schema language for UI/export +contracts, so the tooling + reviewer familiarity exist. A core+extensions +model expresses conference/literature asymmetry cleanly (edge case) while +giving downstream one contract (FR-007/FR-008, US3). + +**Alternatives considered**: +- *Pydantic-only models* — rejected: LinkML is the established contract + language here and gives machine-readable schema + validation for free. +- *One flat schema with every field optional* — rejected: lets a conference + record masquerade as literature (edge case); source-typed extensions + prevent that. + +## R7 — Entry-point sprawl (`ohbm-*` scripts) + +**Decision**: The canonical `ohbmcli` → `aacli`. The ~14 legacy `ohbm-*` +per-stage scripts (mostly superseded by `aacli` subcommands) are renamed to +`aa-*` **only if still used**; unused/legacy ones are dropped (their +functions remain importable). The rename-map enumerates each with its +disposition. + +**Rationale**: Keeps the canonical surface clean without silently removing a +script someone depends on; the rename-map is the auditable record. diff --git a/specs/027-abstractatlas-rename-ingestors/spec.md b/specs/027-abstractatlas-rename-ingestors/spec.md new file mode 100644 index 00000000..915f58dc --- /dev/null +++ b/specs/027-abstractatlas-rename-ingestors/spec.md @@ -0,0 +1,165 @@ +# Feature Specification: Abstract Atlas Rename + Pluggable LinkML Ingestors + +**Feature Branch**: `027-abstractatlas-rename-ingestors` +**Created**: 2026-07-02 +**Status**: Draft +**Input**: User description: "move organization and naming from the overall component being ohbm2026 to abstractatlas (already the new name of the repo). ohbm2026 was an instance of information being added. we would like to allow for different kinds of ingestors of information including extending the pubmed space, other indices like arxiv/biorxiv/medrxiv, as well as other conferences. this should include renaming the cli and re-organizing ingest pipelines into more standardized linkml schemas." + +## Overview + +The project began as a pipeline for a single event's abstracts ("OHBM 2026") and grew a second information source (the NeuroScape PubMed backdrop). The component is still named after that first instance (`ohbm2026` package, `ohbmcli` command), even though the product is now "Abstract Atlas" (the repository, the site, the domain). This feature renames the component to **abstractatlas** and re-frames the codebase around a general idea: **an atlas assembled from many *ingested sources*, where "OHBM 2026" is just one ingested source among many** (other conferences; literature indices such as PubMed extensions, arXiv, bioRxiv, medRxiv). + +To make that framing real, ingestion becomes a **pluggable architecture**: each information source is an *ingestor* that pulls records from its origin and normalizes them into a **standardized, LinkML-defined ingest schema**. The two existing sources — the OHBM conference (via Oxford Abstracts) and the PubMed/NeuroScape corpus — are ported to become the first two ingestor instances, proving the abstraction without adding any new source in this change. + +**Scope decisions (confirmed):** +- **Build:** the foundation only — rename, the pluggable ingestor architecture + standardized LinkML ingest schema, and porting the two existing sources. New source ingestors (arXiv, bioRxiv, medRxiv, additional conferences) are explicitly deferred to follow-on specs that this architecture enables. +- **Rename reach:** code, CLI, package, and docs. Existing on-disk data artifact paths and published data-package names (e.g. `ohbm2026.parquet`) are **preserved** for continuity — no data regeneration, no re-publish, the live site's data stays byte-identical. +- **CLI name:** `ohbmcli` → **`aacli`**. + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Operator runs the pipeline under the new name with no behavior change (Priority: P1) 🎯 MVP + +An operator who runs the canonical pipeline invokes the renamed command +(`aacli `) and imports the renamed package (`abstractatlas`) +in scripts. Every existing subcommand and workflow behaves identically and +writes the same artifacts to the same paths as before the rename. + +**Why this priority**: The rename is the core ask and the riskiest part +(broad blast radius). Landing it with zero behavioral/data change is the +shippable MVP and the foundation everything else builds on. + +**Independent Test**: Run the existing pipeline subcommands via `aacli` +against a fixture corpus and confirm the produced artifacts are identical +(same paths, same content) to a pre-rename run; confirm `import abstractatlas` +works and the old `ohbm2026` import path is gone (or a clearly-deprecated +alias). + +**Acceptance Scenarios**: + +1. **Given** a checkout on the renamed component, **When** the operator runs `aacli ` for each existing subcommand, **Then** each behaves identically to the former `ohbmcli ` and writes the same artifacts to the same locations. +2. **Given** existing on-disk artifacts and caches created before the rename, **When** the pipeline runs after the rename, **Then** those artifacts/caches are still discovered and reused (no forced recompute from a name change). +3. **Given** the published data package and the live site, **When** the rename lands, **Then** the site's data is byte-identical and no data re-publish is required. +4. **Given** documentation (README, agent/context docs), **When** the rename lands, **Then** all operator-facing docs reference `abstractatlas` / `aacli` and contain no stale `ohbm2026` / `ohbmcli` instructions. + +--- + +### User Story 2 - Maintainer adds a new information source without touching downstream stages (Priority: P2) + +A maintainer wants to onboard a new source (e.g. a literature index or +another conference). They implement a single, well-defined **ingestor** +contract — "how to pull records from this source and normalize them" — and +register it. Downstream stages (enrichment, embeddings, analysis, export, +UI packaging) consume the standardized ingest output and require no changes +to accept the new source. + +**Why this priority**: This is the durable value — turning a +single-instance pipeline into a multi-source atlas. It is validated in this +change by porting the two existing sources onto the ingestor contract; new +sources are follow-on work. + +**Independent Test**: Confirm the two existing sources (OHBM conference, +PubMed/NeuroScape) are each expressed as a named ingestor behind the common +contract and produce their former outputs, and that a documented "add an +ingestor" path exists that touches only ingestor code (no edits to +downstream stages) — demonstrated by the two ports changing zero downstream +stage logic. + +**Acceptance Scenarios**: + +1. **Given** the ingestor architecture, **When** the available ingestors are listed, **Then** the OHBM conference ingestor and the PubMed/NeuroScape ingestor both appear as named, discoverable instances. +2. **Given** a new source to onboard, **When** a maintainer implements the ingestor contract and registers it, **Then** it becomes runnable through the CLI and feeds downstream stages with no downstream code changes. +3. **Given** the ported OHBM ingestor, **When** it runs, **Then** it reproduces the former OHBM ingestion output exactly (same normalized corpus). + +--- + +### User Story 3 - Ingested records conform to a standardized schema across sources (Priority: P3) + +Records emitted by any ingestor validate against a **standardized LinkML +ingest schema** with a common core (identity, title, authors, abstract +text, source provenance) plus source-type-specific extensions (a +conference contributes session/program/poster fields; a literature index +contributes DOI/venue/index identifiers). This gives every downstream stage +a uniform, documented contract regardless of source. + +**Why this priority**: Standardization is what makes US2 safe and durable, +but it can land immediately after the architecture and the ports. It is the +contract layer, not new user-facing behavior. + +**Independent Test**: Run each ingestor and validate its emitted records +against the published LinkML ingest schema; confirm a record missing a +required core field or violating the schema is rejected with a precise +error rather than silently passed downstream. + +**Acceptance Scenarios**: + +1. **Given** the standardized ingest schema, **When** an ingestor emits records, **Then** those records validate against the schema (core fields present, source-extension fields well-formed). +2. **Given** a malformed ingested record, **When** it is validated, **Then** validation fails loudly with a precise, source-attributed error and the record is not passed to downstream stages. +3. **Given** the ported OHBM output, **When** validated against the schema, **Then** it conforms — proving the schema captures the existing shape without reshaping the preserved published data. + +--- + +### Edge Cases + +- **Naming divergence for preserved data**: the package is `abstractatlas` but the published parquet keeps the name `ohbm2026.parquet` (that source's data name). The system must tolerate — and clearly document — this intentional divergence rather than "fix" the data name (which would force a re-publish). +- **Legacy imports / command**: existing scripts, cron jobs, or muscle-memory using `ohbmcli` / `import ohbm2026` must fail with a clear pointer to the new name (or work via a clearly-labeled deprecated alias for one transition period), never a confusing partial success. +- **State-keyed caches/checkpoints**: caches and checkpoints keyed by names that embed `ohbm2026` must remain readable so the rename does not silently invalidate expensive prior work. +- **Source-type asymmetry**: a conference record has no DOI/journal; a literature record has no poster/session. The schema must make each set optional-by-source without letting a conference record masquerade as literature or vice versa. +- **Two sources, one atlas**: an ingestor's records carry which source they came from, so downstream can attribute, filter, and de-duplicate across sources. +- **Provenance continuity**: provenance records that referenced the old component name must remain interpretable; new provenance uses the new name. + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: The Python package MUST be renamed `ohbm2026` → `abstractatlas`, and all internal imports, references, and docs updated to the new name. +- **FR-002**: The canonical CLI MUST be renamed `ohbmcli` → `aacli`; every existing subcommand MUST remain available with identical behavior and options. +- **FR-003**: Legacy entry points (`ohbmcli`, `import ohbm2026`) MUST either be removed with a precise "renamed to …" error or provided as an explicitly-deprecated alias for one transition period; they MUST NOT partially work in a confusing way. +- **FR-004**: Existing on-disk data artifact paths, artifact/state-key naming, cache/checkpoint keys, and published data-package names (including `ohbm2026.parquet`) MUST be preserved so no data is regenerated and the live site requires no re-publish (byte-identical data). +- **FR-005**: Ingestion MUST be organized as a pluggable architecture: a common **ingestor contract** (pull records from a source → normalize) with a runtime-discoverable registry of named ingestors. +- **FR-006**: The two existing sources — the OHBM conference (Oxford Abstracts) and the PubMed/NeuroScape corpus — MUST be ported to become the first two ingestor instances behind the common contract, each reproducing its former normalized output exactly. +- **FR-007**: A standardized **LinkML ingest schema** MUST define the normalized record contract: a common core plus source-type-specific extensions; the schema is the single source of truth downstream stages rely on. +- **FR-008**: Each ingestor MUST validate its emitted records against the LinkML ingest schema; validation failures MUST surface as precise, source-attributed errors and MUST NOT pass malformed records downstream. +- **FR-009**: Downstream stages (enrichment, embeddings, analysis, export/UI packaging, atlas-package build) MUST require no behavioral changes to consume the standardized ingest output; their produced artifacts MUST remain byte-identical for the existing sources. +- **FR-010**: The ingestor registry MUST be discovered at runtime (from registration/metadata), not matched against a hardcoded source list; onboarding a new ingestor MUST NOT require editing downstream stages. +- **FR-011**: Each ingested record MUST carry machine-readable source provenance (which ingestor/source produced it) so downstream can attribute and filter by source. +- **FR-012**: Documentation that defines canonical commands, package layout, and workflows (README, agent/context docs, and naming references in project governance docs) MUST be updated in the same change to the new names. + +### Key Entities *(include if feature involves data)* + +- **Ingestor**: a named, registered unit that knows how to pull records from one source origin and normalize them into the standardized ingest schema. Attributes: name, source type (conference | literature-index), origin configuration, output = validated ingested records. +- **Ingested record (standardized)**: the normalized document emitted by any ingestor. Core: stable identity, title, authors, abstract/summary text, source provenance. Extensions by source type — conference: program/session/poster identifiers; literature index: DOI/venue/index identifiers/year. +- **Ingestor registry**: the runtime-discoverable catalog mapping ingestor names to their implementations; the CLI and orchestration read from it. +- **Ingest schema (LinkML)**: the machine-readable contract (core + per-source-type extensions) all ingestors validate against and all downstream stages consume. + +### Constitution Alignment *(mandatory)* + +- **CA-001**: All Python execution MUST use the repository-local `.venv/bin/python` or `uv` targeting it — unchanged by the rename. +- **CA-002**: Verification is identified first: the existing test suite (renamed) MUST pass under the new package/CLI names; new tests cover the ingestor registry/contract, LinkML schema validation (accept valid, reject malformed), and the two ports reproducing prior outputs — added/failing before implementation. +- **CA-003**: Docs that define canonical defaults, commands, inputs, outputs, and review surfaces (README, CLAUDE.md, governance naming references) MUST be updated in the same change (FR-012). +- **CA-004**: No new credentials; existing source credentials (e.g. Oxford Abstracts, OpenAI, data host) are referenced by env-var name only and are unchanged by the rename. +- **CA-005**: No new dataset/cache/export is introduced; existing artifact roots stay gitignored and preserved; the LinkML schema is source, not data. +- **CA-006**: Error paths are explicit and loud: schema-validation failures, unknown-ingestor requests, and legacy-name usage MUST raise precise, typed errors — no silent skips or partial successes; no verification-gate bypass to force the rename green. +- **CA-007**: The ingestor registry and any source enumerations MUST be discovered at runtime from registration/metadata, never a hardcoded allow-list; an unknown or mismatched source surfaces a precise error naming what was searched/found. +- **CA-008**: Provenance for organizer-facing/downstream artifacts is preserved and extended: each ingested record + each produced artifact ships machine-readable provenance (source/ingestor, inputs, config, revision) with no absolute/user-home paths; existing provenance remains interpretable. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: Every existing subcommand dispatches under `aacli` with unchanged names/args, the full test suite passes (SC-002), AND at least one representative fixture subcommand produces artifacts identical (same path + content) to a pre-rename run. (Subcommands requiring live external services are covered by dispatch + suite, not by re-running them against those services.) +- **SC-002**: The full existing test suite passes under the renamed package/CLI with zero skips introduced by the rename. +- **SC-003**: The live site's data is byte-identical after the rename with no data re-publish (published data-package names preserved). +- **SC-004**: Both existing sources are expressed as named ingestors behind the common contract; porting them changed **zero** lines of downstream stage logic (measured by the diff touching only ingestor + rename code). +- **SC-005**: 100% of records emitted by each ingestor validate against the LinkML ingest schema; a deliberately malformed record is rejected with a precise, source-attributed error (0 malformed records reach downstream). +- **SC-006**: A maintainer can onboard a new source by implementing only the ingestor contract + registering it, with no edits to downstream stages — demonstrated by a documented "add an ingestor" guide and the two ports as worked examples. +- **SC-007**: Using a legacy name (`ohbmcli` / `import ohbm2026`) yields a clear rename pointer or a labeled-deprecated alias — never a silent or partial success. + +## Assumptions + +- "Foundation only": this change renames + establishes the ingestor architecture + LinkML ingest schema + ports the two existing sources. It does **not** add arXiv/bioRxiv/medRxiv or new-conference ingestors — those are follow-on specs enabled by this foundation. +- Data continuity is paramount: on-disk artifact layout and published data-package names are preserved verbatim (including the historical `ohbm2026.parquet` name), so the rename is a code/CLI/package/docs change with no data regeneration or site re-publish. +- The standardized LinkML ingest schema is designed to **capture the existing normalized shape** (so the ported OHBM output validates and downstream stays byte-identical), while being general enough to admit literature-index sources — it does not reshape already-published data. +- A deprecated `ohbmcli` alias (and/or an `ohbm2026` import shim) may be provided for one transition period; the canonical, documented interface is `aacli` / `abstractatlas`. +- Governance docs (constitution) contain naming references (`ohbmcli`, `src/ohbm2026/`, `data/abstracts.json`); updating those references is in scope for doc-sync, coordinated with the project's constitution-amendment process where required. +- Downstream stages already treat the corpus generically enough that a standardized ingest contract can be introduced without changing their outputs for the existing sources. diff --git a/specs/027-abstractatlas-rename-ingestors/tasks.md b/specs/027-abstractatlas-rename-ingestors/tasks.md new file mode 100644 index 00000000..5b1573b4 --- /dev/null +++ b/specs/027-abstractatlas-rename-ingestors/tasks.md @@ -0,0 +1,168 @@ +--- + +description: "Task list for Abstract Atlas Rename + Pluggable LinkML Ingestors" +--- + +# Tasks: Abstract Atlas Rename + Pluggable LinkML Ingestors + +**Input**: Design documents from `/specs/027-abstractatlas-rename-ingestors/` +**Prerequisites**: plan.md, spec.md, research.md, data-model.md, contracts/ + +**Tests**: REQUIRED — behavior + contract change (CA-002). The rename is +verified by the full existing suite passing under new names + identical +fixture artifacts; the ingest work has failing-first unit tests +(registry/contract, LinkML validation, port fidelity). + +**Organization**: US1 (rename) is the shippable MVP and is **foundational** +— US2 (ingestor architecture) and US3 (schema validation) build on the +renamed package and must follow it. Sequence: Setup → baseline → US1 → US2 → US3 → Polish. + +## Format: `[ID] [P?] [Story] Description` + +- **[P]**: different files, no dependency on incomplete tasks +- Python paths under `src/abstractatlas/` (post-rename); tests under `tests/` + +## Path Conventions + +- Package: `src/abstractatlas/` (renamed from `src/ohbm2026/`) +- Ingest subpackage: `src/abstractatlas/ingest/{base,registry,schema,conference_ohbm,literature_neuroscape}.py` +- New tests: `tests/test_ingest_registry.py`, `tests/test_ingest_schema.py`, `tests/test_ingestor_ports.py` +- Run tests: `PYTHONPATH=src .venv/bin/python -m unittest discover -s tests` (venv-only, CA-001) + +--- + +## Phase 1: Setup + +- [x] T001 Refresh the repo-local venv and add the schema tool: `UV_CACHE_DIR=.uv-cache uv venv --python 3.14 .venv` (if needed) + `uv pip install --python .venv/bin/python linkml`. Confirm `linkml` validation tooling is importable (already used by `scripts/validate_ui_data.sh`). + +--- + +## Phase 2: Foundational (Blocking Prerequisite for US1 verification) + +**Purpose**: Capture a pre-rename baseline so the rename can be proven byte-identical (SC-001). + +- [x] T002 Capture pre-rename baseline: run a small fixture subcommand under the CURRENT `ohbmcli` (e.g. `refresh-assets`/`title-audit` on a fixture, or an existing unit-covered path) and save its produced artifacts + `git rev-parse HEAD` to a scratch location outside version control, to diff against after the rename (SC-001). Record the current full-suite pass state (`PYTHONPATH=src .venv/bin/python -m unittest discover -s tests`) as the SC-002 baseline. + +**Checkpoint**: baseline recorded — the rename can now be verified against it. + +--- + +## Phase 3: User Story 1 - Rename with no behavior change (Priority: P1) 🎯 MVP + +**Goal**: `ohbm2026`→`abstractatlas`, `ohbmcli`→`aacli`, everything behaves identically, data preserved. + +**Independent Test**: `aacli ` reproduces `ohbmcli ` artifacts exactly; full suite green under new names; `import abstractatlas` works; legacy names emit labeled deprecation; no `data/**` path or published name changed. + +### Implementation for User Story 1 + +- [x] T003 [US1] `git mv src/ohbm2026 src/abstractatlas`, then rewrite `ohbm2026`→`abstractatlas` across all imports/refs in `src/` (per `contracts/rename-map.md`). Do NOT touch data-identity strings in the rename-map "NOT renamed" table (data paths, state-keys, `ohbm2026.parquet`, `/ohbm2026/` route, `SITE_MODE='ohbm'`, `OHBM2026_*`). +- [x] T004 [US1] Update `pyproject.toml`: `name` `ohbm2026`→`abstractatlas`; `[project.scripts]` `ohbmcli`→`aacli = "abstractatlas.cli:main"`; rename `ohbm-*` entry points to `aa-*` where still used or drop them (record disposition per rename-map). Keep `package-dir`/`packages.find` under `src`. +- [x] T005 [US1] Add labeled deprecation shims (FR-003, research R3): `ohbmcli` entry point that warns "renamed to aacli" then delegates; an `ohbm2026` import shim module re-exporting `abstractatlas` with a `DeprecationWarning`. Label both in code with root cause + removal follow-up (Constitution VI). +- [x] T006 [P] [US1] Rewrite `ohbm2026`→`abstractatlas` imports/refs across `tests/` and `scripts/`; update `PYTHONPATH=src -m ohbm2026.cli`→`-m abstractatlas.cli` in scripts + `.github/workflows/**` + `.githooks/`. +- [x] T007 [P] [US1] Update the few `ohbm2026` string refs in `site/src/**` tests that name the PACKAGE (not the `ohbm2026` data/route/site-mode identity). — VERIFIED NO-OP: audited `site/src` — every `ohbm2026` ref is data/route/storage-key identity (`/ohbm2026/` routes, `ohbm2026.parquet`, `ohbm2026.ui.*`/`ohbm2026.analytics.*` localStorage keys, conference `kind`); the TypeScript site never imports the Python package, so nothing to rename (2 stale comment path-mentions left as historical, like the src ones). +- [x] T008 [US1] Verify the rename: `PYTHONPATH=src .venv/bin/python -m unittest discover -s tests` fully green (SC-002); re-run the T002 fixture under `aacli` and diff artifacts — identical (SC-001); `ohbmcli`/`import ohbm2026` emit the labeled deprecation and still succeed (SC-007); confirm an existing `data/cache/**` (or checkpoint) entry created before the rename is still discovered/reused — no forced recompute from the name change (Constitution III / spec edge case); `git grep -n 'ohbm2026\|ohbmcli'` returns only intentional data-identity names, shims, and historical spec docs. + +**Checkpoint**: Rename landed + verified. Shippable MVP. US2/US3 build on this. + +--- + +## Phase 4: User Story 2 - Pluggable ingestor architecture + port existing sources (Priority: P2) + +**Goal**: A common `Ingestor` contract + runtime registry; OHBM + NeuroScape wrapped as the first two ingestors with byte-identical outputs and zero downstream change. + +**Independent Test**: registry lists both ingestors; `get(unknown)` errors with the known list; each ingestor reproduces its prior normalized output; porting touched zero downstream stage logic. + +### Tests for User Story 2 (write FIRST, ensure they FAIL) ⚠️ + +- [ ] T009 [P] [US2] Failing-first `tests/test_ingest_registry.py`: registry runtime-discovers registered ingestors (both ports present), `get(unknown)` raises `IngestorNotFound` listing known names, registration idempotent (contracts/ingestor-interface.md tests 1,5). +- [ ] T010 [P] [US2] Failing-first `tests/test_ingestor_ports.py`: `ConferenceOHBMIngestor.normalize` reproduces the prior OHBM normalized corpus byte-for-byte on a fixture (SC-003/SC-004); both ingestors expose correct `name`/`source_type`. + +### Implementation for User Story 2 + +- [ ] T011 [US2] Add `src/abstractatlas/ingest/base.py`: `SourceType` enum + `Ingestor` ABC (`name`, `source_type`, `pull`, `normalize`, `ingest`) per contracts/ingestor-interface.md. Extend the (renamed) exception hierarchy with an `IngestError` subtree (`IngestorNotFound`, `IngestSchemaValidationError`). +- [ ] T012 [US2] Add `src/abstractatlas/ingest/registry.py`: runtime-discoverable name→Ingestor registry (`register`/`get`/`names`), no hardcoded source list (CA-007). Make T009 green. +- [ ] T013 [US2] Add `src/abstractatlas/ingest/conference_ohbm.py`: `ConferenceOHBMIngestor` (`name="ohbm-2026"`, conference) WRAPPING `fetch/stage.py` + `assets.normalize_abstract` (no logic rewrite). Register it. Make T010 green. +- [ ] T014 [US2] Add `src/abstractatlas/ingest/literature_neuroscape.py`: `LiteratureNeuroscapeIngestor` (`name="neuroscape-pubmed"`, literature_index). FIRST scout `atlas_package` (orchestrator + inputs) for a wrappable record-normalization seam — NeuroScape is a vendored release (HDF5/CSV), so a discrete `normalize` fn like `assets.normalize_abstract` may NOT exist. If a clean seam exists, wrap it; if not, scope this ingestor as a THIN adapter that reads the existing NeuroScape build inputs and emits `LiteratureDocument` records (documents the source without refactoring `atlas_package`) — do NOT rewrite `atlas_package` here (that would risk downstream drift and exceed the foundation scope). Register it either way. +- [ ] T015 [US2] Wire the CLI: `aacli ingest --source ` (runs a registered ingestor; unknown → precise error) + `aacli list-ingestors` (prints the runtime registry) per contracts/cli-aacli.md. Existing subcommands (`fetch-abstracts`, `build-atlas-package`) unchanged. +- [ ] T016 [US2] Verify zero downstream drift: confirm the diff for T011–T015 touches only `ingest/` + CLI wiring (no edits to `enrich/`, `embed/`, `analyze/`, `ui_data/`, `atlas_package/` stage logic — SC-004); full suite still green. + +**Checkpoint**: Two sources are registered ingestors behind the common contract; runnable via `aacli ingest`. + +--- + +## Phase 5: User Story 3 - Standardized LinkML ingest schema + validation (Priority: P3) + +**Goal**: All ingestors validate emitted records against the LinkML schema; malformed records rejected loudly. + +**Independent Test**: valid records pass; a malformed record is rejected with a precise, source-attributed error; the ported OHBM output validates unchanged. + +### Tests for User Story 3 (write FIRST, ensure they FAIL) ⚠️ + +- [ ] T017 [P] [US3] Failing-first `tests/test_ingest_schema.py`: a valid `IngestedDocument`/`ConferenceDocument`/`LiteratureDocument` validates; a record missing a core field or with a source-type/class mismatch (conference carrying literature-only slots) is rejected with `IngestSchemaValidationError` naming source + field (contracts/ingestor-interface.md tests 2,3; edge case). + +### Implementation for User Story 3 + +- [ ] T018 [US3] Add the schema `src/abstractatlas/ingest/ingest_schema.linkml.yaml` (from `contracts/ingest-schema.linkml.yaml`) and `src/abstractatlas/ingest/schema.py` (load + validate via the `linkml` validator, mirroring `scripts/validate_ui_data.sh`). Make T017 green. +- [ ] T019 [US3] Wire validation into `Ingestor.ingest()`: after `normalize`, validate each record against the schema; on failure raise the precise source-attributed error, do NOT write/pass downstream (FR-008). Attach `SourceProvenance` (ingestor/source_type/origin, no absolute/home paths — CA-008). +- [ ] T020 [US3] Verify the ported OHBM output validates against the schema unchanged (SC-003 — schema captures the existing shape, does not reshape); the malformed-record path errors loudly (SC-005). + +**Checkpoint**: Every ingestor's output is schema-validated; downstream consumes one contract. + +--- + +## Phase 6: Polish & Cross-Cutting Concerns + +- [ ] T021 [P] Update docs (FR-012): README + CLAUDE.md to `abstractatlas`/`aacli`; document the intentional `ohbm2026.parquet` (+ `/ohbm2026/` route, `SITE_MODE`) divergence; add an "add an ingestor" guide referencing the two ports as worked examples (SC-006). +- [ ] T022 Update naming references in `.specify/memory/constitution.md` (`ohbmcli`, `src/ohbm2026/`, `data/abstracts.json`) — coordinate with `/speckit-constitution` if a formal amendment is required (FR-012). +- [ ] T023 [P] Run the full suite `PYTHONPATH=src .venv/bin/python -m unittest discover -s tests` + `cd site && pnpm exec vitest run` — all green; confirm published data is byte-identical / no re-publish needed (SC-003). +- [ ] T024 Run `.specify/scripts/bash/constitution-check.sh --full` and resolve any reported violations; final `git grep -n 'ohbm2026\|ohbmcli'` audit matches the rename-map "NOT renamed" + shims + historical specs only. + +--- + +## Dependencies & Execution Order + +### Phase Dependencies + +- **Setup (T001)** → **Foundational baseline (T002)** → **US1 rename (T003–T008)**. +- **US2 (T009–T016)** depends on US1 (builds in the renamed package). +- **US3 (T017–T020)** depends on US2 (validates the ingestors' output; `ingest()` calls the validator). +- **Polish (T021–T024)** after US1–US3. + +### Within Each Story + +- US1: `git mv`+rewrite (T003) → pyproject (T004) → shims (T005) → tests/scripts/CI + site (T006/T007) → verify (T008). T006/T007 are [P] (different trees). +- US2: failing tests (T009/T010, [P]) FIRST → base (T011) → registry (T012) → ports (T013/T014) → CLI (T015) → drift check (T016). +- US3: failing test (T017) FIRST → schema+validator (T018) → wire into ingest() (T019) → verify (T020). + +### Parallel Opportunities + +- T006 ∥ T007 (tests/scripts/CI vs site). T009 ∥ T010; T021 ∥ T023. +- Note: the T003 mechanical rewrite is a single large atomic edit — not parallelizable with T004/T005 (same files/pyproject). + +--- + +## Implementation Strategy + +### MVP (US1 only) — shippable on its own + +1. Setup + baseline (T001–T002). +2. Rename (T003–T007) → verify identical + green (T008). +3. **STOP and VALIDATE**: `aacli` reproduces `ohbmcli`; suite green; data untouched. This alone is a coherent, shippable rename. + +### Incremental delivery + +4. US2 → ingestor architecture + two ports (runnable `aacli ingest`). +5. US3 → schema validation gate. +6. Polish → docs, constitution refs, full audit. + +Each story is a separate reviewable slice; US1 can ship before US2/US3 land. + +--- + +## Notes + +- Venv-only Python throughout (CA-001); commit per verified slice (rename; ingest arch; ports; schema; docs) and push. +- **Data preservation is the hard constraint** (FR-004): never touch `data/**` paths, state-keys, `ohbm2026.parquet`, the `/ohbm2026/` route, `SITE_MODE='ohbm'`, or `OHBM2026_*` — those are source/deploy identity, not the component name. +- Ingestors WRAP existing normalization; they must not rewrite it (guarantees byte-identical outputs + zero downstream change). +- No data regeneration, no site re-publish; byte-identical guarantee preserved. +- Fail loudly: unknown ingestor, schema-validation failure, and legacy-name use all raise precise typed errors — never silent/partial success. diff --git a/src/ohbm2026/__init__.py b/src/abstractatlas/__init__.py similarity index 100% rename from src/ohbm2026/__init__.py rename to src/abstractatlas/__init__.py diff --git a/src/ohbm2026/analyze/__init__.py b/src/abstractatlas/analyze/__init__.py similarity index 100% rename from src/ohbm2026/analyze/__init__.py rename to src/abstractatlas/analyze/__init__.py diff --git a/src/ohbm2026/analyze/category_evaluation.py b/src/abstractatlas/analyze/category_evaluation.py similarity index 100% rename from src/ohbm2026/analyze/category_evaluation.py rename to src/abstractatlas/analyze/category_evaluation.py diff --git a/src/ohbm2026/analyze/category_rollup.py b/src/abstractatlas/analyze/category_rollup.py similarity index 100% rename from src/ohbm2026/analyze/category_rollup.py rename to src/abstractatlas/analyze/category_rollup.py diff --git a/src/ohbm2026/analyze/centroids.py b/src/abstractatlas/analyze/centroids.py similarity index 100% rename from src/ohbm2026/analyze/centroids.py rename to src/abstractatlas/analyze/centroids.py diff --git a/src/ohbm2026/analyze/clusters.py b/src/abstractatlas/analyze/clusters.py similarity index 100% rename from src/ohbm2026/analyze/clusters.py rename to src/abstractatlas/analyze/clusters.py diff --git a/src/ohbm2026/analyze/communities.py b/src/abstractatlas/analyze/communities.py similarity index 100% rename from src/ohbm2026/analyze/communities.py rename to src/abstractatlas/analyze/communities.py diff --git a/src/ohbm2026/analyze/errors.py b/src/abstractatlas/analyze/errors.py similarity index 100% rename from src/ohbm2026/analyze/errors.py rename to src/abstractatlas/analyze/errors.py diff --git a/src/ohbm2026/analyze/llm_adapter.py b/src/abstractatlas/analyze/llm_adapter.py similarity index 100% rename from src/ohbm2026/analyze/llm_adapter.py rename to src/abstractatlas/analyze/llm_adapter.py diff --git a/src/ohbm2026/analyze/projections.py b/src/abstractatlas/analyze/projections.py similarity index 100% rename from src/ohbm2026/analyze/projections.py rename to src/abstractatlas/analyze/projections.py diff --git a/src/ohbm2026/analyze/provenance.py b/src/abstractatlas/analyze/provenance.py similarity index 100% rename from src/ohbm2026/analyze/provenance.py rename to src/abstractatlas/analyze/provenance.py diff --git a/src/ohbm2026/analyze/rollup.py b/src/abstractatlas/analyze/rollup.py similarity index 100% rename from src/ohbm2026/analyze/rollup.py rename to src/abstractatlas/analyze/rollup.py diff --git a/src/ohbm2026/analyze/runners.py b/src/abstractatlas/analyze/runners.py similarity index 100% rename from src/ohbm2026/analyze/runners.py rename to src/abstractatlas/analyze/runners.py diff --git a/src/ohbm2026/analyze/stage.py b/src/abstractatlas/analyze/stage.py similarity index 100% rename from src/ohbm2026/analyze/stage.py rename to src/abstractatlas/analyze/stage.py diff --git a/src/ohbm2026/analyze/storage.py b/src/abstractatlas/analyze/storage.py similarity index 100% rename from src/ohbm2026/analyze/storage.py rename to src/abstractatlas/analyze/storage.py diff --git a/src/ohbm2026/analyze/topic_clusters.py b/src/abstractatlas/analyze/topic_clusters.py similarity index 100% rename from src/ohbm2026/analyze/topic_clusters.py rename to src/abstractatlas/analyze/topic_clusters.py diff --git a/src/ohbm2026/analyze/topics.py b/src/abstractatlas/analyze/topics.py similarity index 100% rename from src/ohbm2026/analyze/topics.py rename to src/abstractatlas/analyze/topics.py diff --git a/src/ohbm2026/analyze/umap.py b/src/abstractatlas/analyze/umap.py similarity index 100% rename from src/ohbm2026/analyze/umap.py rename to src/abstractatlas/analyze/umap.py diff --git a/src/ohbm2026/artifacts.py b/src/abstractatlas/artifacts.py similarity index 100% rename from src/ohbm2026/artifacts.py rename to src/abstractatlas/artifacts.py diff --git a/src/ohbm2026/assets.py b/src/abstractatlas/assets.py similarity index 100% rename from src/ohbm2026/assets.py rename to src/abstractatlas/assets.py diff --git a/src/ohbm2026/atlas_hosting/__init__.py b/src/abstractatlas/atlas_hosting/__init__.py similarity index 100% rename from src/ohbm2026/atlas_hosting/__init__.py rename to src/abstractatlas/atlas_hosting/__init__.py diff --git a/src/ohbm2026/atlas_hosting/cli.py b/src/abstractatlas/atlas_hosting/cli.py similarity index 100% rename from src/ohbm2026/atlas_hosting/cli.py rename to src/abstractatlas/atlas_hosting/cli.py diff --git a/src/ohbm2026/atlas_hosting/compare.py b/src/abstractatlas/atlas_hosting/compare.py similarity index 100% rename from src/ohbm2026/atlas_hosting/compare.py rename to src/abstractatlas/atlas_hosting/compare.py diff --git a/src/ohbm2026/atlas_hosting/content_hash.py b/src/abstractatlas/atlas_hosting/content_hash.py similarity index 100% rename from src/ohbm2026/atlas_hosting/content_hash.py rename to src/abstractatlas/atlas_hosting/content_hash.py diff --git a/src/ohbm2026/atlas_hosting/manifest.py b/src/abstractatlas/atlas_hosting/manifest.py similarity index 100% rename from src/ohbm2026/atlas_hosting/manifest.py rename to src/abstractatlas/atlas_hosting/manifest.py diff --git a/src/ohbm2026/atlas_hosting/r2_client.py b/src/abstractatlas/atlas_hosting/r2_client.py similarity index 100% rename from src/ohbm2026/atlas_hosting/r2_client.py rename to src/abstractatlas/atlas_hosting/r2_client.py diff --git a/src/ohbm2026/atlas_hosting/uploader.py b/src/abstractatlas/atlas_hosting/uploader.py similarity index 100% rename from src/ohbm2026/atlas_hosting/uploader.py rename to src/abstractatlas/atlas_hosting/uploader.py diff --git a/src/ohbm2026/atlas_package/__init__.py b/src/abstractatlas/atlas_package/__init__.py similarity index 100% rename from src/ohbm2026/atlas_package/__init__.py rename to src/abstractatlas/atlas_package/__init__.py diff --git a/src/ohbm2026/atlas_package/cli.py b/src/abstractatlas/atlas_package/cli.py similarity index 100% rename from src/ohbm2026/atlas_package/cli.py rename to src/abstractatlas/atlas_package/cli.py diff --git a/src/ohbm2026/atlas_package/cluster_palette.py b/src/abstractatlas/atlas_package/cluster_palette.py similarity index 100% rename from src/ohbm2026/atlas_package/cluster_palette.py rename to src/abstractatlas/atlas_package/cluster_palette.py diff --git a/src/ohbm2026/atlas_package/decimation.py b/src/abstractatlas/atlas_package/decimation.py similarity index 100% rename from src/ohbm2026/atlas_package/decimation.py rename to src/abstractatlas/atlas_package/decimation.py diff --git a/src/ohbm2026/atlas_package/link_check.py b/src/abstractatlas/atlas_package/link_check.py similarity index 100% rename from src/ohbm2026/atlas_package/link_check.py rename to src/abstractatlas/atlas_package/link_check.py diff --git a/src/ohbm2026/atlas_package/lod.py b/src/abstractatlas/atlas_package/lod.py similarity index 100% rename from src/ohbm2026/atlas_package/lod.py rename to src/abstractatlas/atlas_package/lod.py diff --git a/src/ohbm2026/atlas_package/neighbour_index.py b/src/abstractatlas/atlas_package/neighbour_index.py similarity index 100% rename from src/ohbm2026/atlas_package/neighbour_index.py rename to src/abstractatlas/atlas_package/neighbour_index.py diff --git a/src/ohbm2026/atlas_package/neuroscape_loader.py b/src/abstractatlas/atlas_package/neuroscape_loader.py similarity index 100% rename from src/ohbm2026/atlas_package/neuroscape_loader.py rename to src/abstractatlas/atlas_package/neuroscape_loader.py diff --git a/src/ohbm2026/atlas_package/ohbm_projector.py b/src/abstractatlas/atlas_package/ohbm_projector.py similarity index 100% rename from src/ohbm2026/atlas_package/ohbm_projector.py rename to src/abstractatlas/atlas_package/ohbm_projector.py diff --git a/src/ohbm2026/atlas_package/orchestrator.py b/src/abstractatlas/atlas_package/orchestrator.py similarity index 100% rename from src/ohbm2026/atlas_package/orchestrator.py rename to src/abstractatlas/atlas_package/orchestrator.py diff --git a/src/ohbm2026/atlas_package/parquet_writer.py b/src/abstractatlas/atlas_package/parquet_writer.py similarity index 100% rename from src/ohbm2026/atlas_package/parquet_writer.py rename to src/abstractatlas/atlas_package/parquet_writer.py diff --git a/src/ohbm2026/atlas_package/provenance.py b/src/abstractatlas/atlas_package/provenance.py similarity index 100% rename from src/ohbm2026/atlas_package/provenance.py rename to src/abstractatlas/atlas_package/provenance.py diff --git a/src/ohbm2026/atlas_package/semantic_index.py b/src/abstractatlas/atlas_package/semantic_index.py similarity index 100% rename from src/ohbm2026/atlas_package/semantic_index.py rename to src/abstractatlas/atlas_package/semantic_index.py diff --git a/src/ohbm2026/atlas_package/umap_fit.py b/src/abstractatlas/atlas_package/umap_fit.py similarity index 100% rename from src/ohbm2026/atlas_package/umap_fit.py rename to src/abstractatlas/atlas_package/umap_fit.py diff --git a/src/ohbm2026/atlas_package/vectors_compute.py b/src/abstractatlas/atlas_package/vectors_compute.py similarity index 100% rename from src/ohbm2026/atlas_package/vectors_compute.py rename to src/abstractatlas/atlas_package/vectors_compute.py diff --git a/src/ohbm2026/book/__init__.py b/src/abstractatlas/book/__init__.py similarity index 100% rename from src/ohbm2026/book/__init__.py rename to src/abstractatlas/book/__init__.py diff --git a/src/ohbm2026/book/assemble_pdf.py b/src/abstractatlas/book/assemble_pdf.py similarity index 100% rename from src/ohbm2026/book/assemble_pdf.py rename to src/abstractatlas/book/assemble_pdf.py diff --git a/src/ohbm2026/book/author_index.py b/src/abstractatlas/book/author_index.py similarity index 100% rename from src/ohbm2026/book/author_index.py rename to src/abstractatlas/book/author_index.py diff --git a/src/ohbm2026/book/cache.py b/src/abstractatlas/book/cache.py similarity index 100% rename from src/ohbm2026/book/cache.py rename to src/abstractatlas/book/cache.py diff --git a/src/ohbm2026/book/cli.py b/src/abstractatlas/book/cli.py similarity index 100% rename from src/ohbm2026/book/cli.py rename to src/abstractatlas/book/cli.py diff --git a/src/ohbm2026/book/corpus.py b/src/abstractatlas/book/corpus.py similarity index 100% rename from src/ohbm2026/book/corpus.py rename to src/abstractatlas/book/corpus.py diff --git a/src/ohbm2026/book/figure_check.py b/src/abstractatlas/book/figure_check.py similarity index 100% rename from src/ohbm2026/book/figure_check.py rename to src/abstractatlas/book/figure_check.py diff --git a/src/ohbm2026/book/html_to_md.py b/src/abstractatlas/book/html_to_md.py similarity index 100% rename from src/ohbm2026/book/html_to_md.py rename to src/abstractatlas/book/html_to_md.py diff --git a/src/ohbm2026/book/model.py b/src/abstractatlas/book/model.py similarity index 100% rename from src/ohbm2026/book/model.py rename to src/abstractatlas/book/model.py diff --git a/src/ohbm2026/book/provenance.py b/src/abstractatlas/book/provenance.py similarity index 100% rename from src/ohbm2026/book/provenance.py rename to src/abstractatlas/book/provenance.py diff --git a/src/ohbm2026/book/render_markdown.py b/src/abstractatlas/book/render_markdown.py similarity index 100% rename from src/ohbm2026/book/render_markdown.py rename to src/abstractatlas/book/render_markdown.py diff --git a/src/ohbm2026/book/render_per_abstract.py b/src/abstractatlas/book/render_per_abstract.py similarity index 100% rename from src/ohbm2026/book/render_per_abstract.py rename to src/abstractatlas/book/render_per_abstract.py diff --git a/src/ohbm2026/book/render_via_pandoc.py b/src/abstractatlas/book/render_via_pandoc.py similarity index 100% rename from src/ohbm2026/book/render_via_pandoc.py rename to src/abstractatlas/book/render_via_pandoc.py diff --git a/src/ohbm2026/book/sections.py b/src/abstractatlas/book/sections.py similarity index 100% rename from src/ohbm2026/book/sections.py rename to src/abstractatlas/book/sections.py diff --git a/src/ohbm2026/book/sort.py b/src/abstractatlas/book/sort.py similarity index 100% rename from src/ohbm2026/book/sort.py rename to src/abstractatlas/book/sort.py diff --git a/src/ohbm2026/book/templates/.keep b/src/abstractatlas/book/templates/.keep similarity index 100% rename from src/ohbm2026/book/templates/.keep rename to src/abstractatlas/book/templates/.keep diff --git a/src/ohbm2026/book/templates/book.md.template b/src/abstractatlas/book/templates/book.md.template similarity index 100% rename from src/ohbm2026/book/templates/book.md.template rename to src/abstractatlas/book/templates/book.md.template diff --git a/src/ohbm2026/book/templates/header-includes-loose.tex b/src/abstractatlas/book/templates/header-includes-loose.tex similarity index 100% rename from src/ohbm2026/book/templates/header-includes-loose.tex rename to src/abstractatlas/book/templates/header-includes-loose.tex diff --git a/src/ohbm2026/book/templates/header-includes-tufte.tex b/src/abstractatlas/book/templates/header-includes-tufte.tex similarity index 100% rename from src/ohbm2026/book/templates/header-includes-tufte.tex rename to src/abstractatlas/book/templates/header-includes-tufte.tex diff --git a/src/ohbm2026/book/templates/header-includes.tex b/src/abstractatlas/book/templates/header-includes.tex similarity index 100% rename from src/ohbm2026/book/templates/header-includes.tex rename to src/abstractatlas/book/templates/header-includes.tex diff --git a/src/ohbm2026/book/templates/per-abstract.tex.template b/src/abstractatlas/book/templates/per-abstract.tex.template similarity index 100% rename from src/ohbm2026/book/templates/per-abstract.tex.template rename to src/abstractatlas/book/templates/per-abstract.tex.template diff --git a/src/ohbm2026/cli.py b/src/abstractatlas/cli.py similarity index 100% rename from src/ohbm2026/cli.py rename to src/abstractatlas/cli.py diff --git a/src/ohbm2026/data/__init__.py b/src/abstractatlas/data/__init__.py similarity index 100% rename from src/ohbm2026/data/__init__.py rename to src/abstractatlas/data/__init__.py diff --git a/src/ohbm2026/data/__pycache__/__init__.cpython-314.pyc b/src/abstractatlas/data/__pycache__/__init__.cpython-314.pyc similarity index 100% rename from src/ohbm2026/data/__pycache__/__init__.cpython-314.pyc rename to src/abstractatlas/data/__pycache__/__init__.cpython-314.pyc diff --git a/src/ohbm2026/data/eco_top_codes.json b/src/abstractatlas/data/eco_top_codes.json similarity index 100% rename from src/ohbm2026/data/eco_top_codes.json rename to src/abstractatlas/data/eco_top_codes.json diff --git a/src/ohbm2026/embed/__init__.py b/src/abstractatlas/embed/__init__.py similarity index 100% rename from src/ohbm2026/embed/__init__.py rename to src/abstractatlas/embed/__init__.py diff --git a/src/ohbm2026/embed/components.py b/src/abstractatlas/embed/components.py similarity index 100% rename from src/ohbm2026/embed/components.py rename to src/abstractatlas/embed/components.py diff --git a/src/ohbm2026/embed/compose.py b/src/abstractatlas/embed/compose.py similarity index 100% rename from src/ohbm2026/embed/compose.py rename to src/abstractatlas/embed/compose.py diff --git a/src/ohbm2026/embed/hf.py b/src/abstractatlas/embed/hf.py similarity index 100% rename from src/ohbm2026/embed/hf.py rename to src/abstractatlas/embed/hf.py diff --git a/src/ohbm2026/embed/neuroscape.py b/src/abstractatlas/embed/neuroscape.py similarity index 100% rename from src/ohbm2026/embed/neuroscape.py rename to src/abstractatlas/embed/neuroscape.py diff --git a/src/ohbm2026/embed/openai.py b/src/abstractatlas/embed/openai.py similarity index 100% rename from src/ohbm2026/embed/openai.py rename to src/abstractatlas/embed/openai.py diff --git a/src/ohbm2026/embed/provenance.py b/src/abstractatlas/embed/provenance.py similarity index 100% rename from src/ohbm2026/embed/provenance.py rename to src/abstractatlas/embed/provenance.py diff --git a/src/ohbm2026/embed/stage.py b/src/abstractatlas/embed/stage.py similarity index 100% rename from src/ohbm2026/embed/stage.py rename to src/abstractatlas/embed/stage.py diff --git a/src/ohbm2026/embed/storage.py b/src/abstractatlas/embed/storage.py similarity index 100% rename from src/ohbm2026/embed/storage.py rename to src/abstractatlas/embed/storage.py diff --git a/src/ohbm2026/embed/voyage.py b/src/abstractatlas/embed/voyage.py similarity index 100% rename from src/ohbm2026/embed/voyage.py rename to src/abstractatlas/embed/voyage.py diff --git a/src/ohbm2026/enrich/__init__.py b/src/abstractatlas/enrich/__init__.py similarity index 100% rename from src/ohbm2026/enrich/__init__.py rename to src/abstractatlas/enrich/__init__.py diff --git a/src/ohbm2026/enrich/claims.py b/src/abstractatlas/enrich/claims.py similarity index 100% rename from src/ohbm2026/enrich/claims.py rename to src/abstractatlas/enrich/claims.py diff --git a/src/ohbm2026/enrich/figures.py b/src/abstractatlas/enrich/figures.py similarity index 100% rename from src/ohbm2026/enrich/figures.py rename to src/abstractatlas/enrich/figures.py diff --git a/src/ohbm2026/enrich/flex_tier.py b/src/abstractatlas/enrich/flex_tier.py similarity index 100% rename from src/ohbm2026/enrich/flex_tier.py rename to src/abstractatlas/enrich/flex_tier.py diff --git a/src/ohbm2026/enrich/image_quality.py b/src/abstractatlas/enrich/image_quality.py similarity index 100% rename from src/ohbm2026/enrich/image_quality.py rename to src/abstractatlas/enrich/image_quality.py diff --git a/src/ohbm2026/enrich/markdown_render.py b/src/abstractatlas/enrich/markdown_render.py similarity index 100% rename from src/ohbm2026/enrich/markdown_render.py rename to src/abstractatlas/enrich/markdown_render.py diff --git a/src/ohbm2026/enrich/openalex.py b/src/abstractatlas/enrich/openalex.py similarity index 100% rename from src/ohbm2026/enrich/openalex.py rename to src/abstractatlas/enrich/openalex.py diff --git a/src/ohbm2026/enrich/references.py b/src/abstractatlas/enrich/references.py similarity index 100% rename from src/ohbm2026/enrich/references.py rename to src/abstractatlas/enrich/references.py diff --git a/src/ohbm2026/enrich/stage.py b/src/abstractatlas/enrich/stage.py similarity index 100% rename from src/ohbm2026/enrich/stage.py rename to src/abstractatlas/enrich/stage.py diff --git a/src/ohbm2026/enrich/storage.py b/src/abstractatlas/enrich/storage.py similarity index 100% rename from src/ohbm2026/enrich/storage.py rename to src/abstractatlas/enrich/storage.py diff --git a/src/ohbm2026/enrich/text.py b/src/abstractatlas/enrich/text.py similarity index 100% rename from src/ohbm2026/enrich/text.py rename to src/abstractatlas/enrich/text.py diff --git a/src/ohbm2026/exceptions.py b/src/abstractatlas/exceptions.py similarity index 100% rename from src/ohbm2026/exceptions.py rename to src/abstractatlas/exceptions.py diff --git a/src/ohbm2026/fetch/__init__.py b/src/abstractatlas/fetch/__init__.py similarity index 100% rename from src/ohbm2026/fetch/__init__.py rename to src/abstractatlas/fetch/__init__.py diff --git a/src/ohbm2026/fetch/graphql_api.py b/src/abstractatlas/fetch/graphql_api.py similarity index 100% rename from src/ohbm2026/fetch/graphql_api.py rename to src/abstractatlas/fetch/graphql_api.py diff --git a/src/ohbm2026/fetch/schema_diff.py b/src/abstractatlas/fetch/schema_diff.py similarity index 100% rename from src/ohbm2026/fetch/schema_diff.py rename to src/abstractatlas/fetch/schema_diff.py diff --git a/src/ohbm2026/fetch/stage.py b/src/abstractatlas/fetch/stage.py similarity index 100% rename from src/ohbm2026/fetch/stage.py rename to src/abstractatlas/fetch/stage.py diff --git a/src/ohbm2026/layout/__init__.py b/src/abstractatlas/layout/__init__.py similarity index 100% rename from src/ohbm2026/layout/__init__.py rename to src/abstractatlas/layout/__init__.py diff --git a/src/ohbm2026/layout/nocd_experiments.py b/src/abstractatlas/layout/nocd_experiments.py similarity index 100% rename from src/ohbm2026/layout/nocd_experiments.py rename to src/abstractatlas/layout/nocd_experiments.py diff --git a/src/ohbm2026/layout/poster_layout.py b/src/abstractatlas/layout/poster_layout.py similarity index 100% rename from src/ohbm2026/layout/poster_layout.py rename to src/abstractatlas/layout/poster_layout.py diff --git a/src/ohbm2026/layout/poster_sequencing.py b/src/abstractatlas/layout/poster_sequencing.py similarity index 100% rename from src/ohbm2026/layout/poster_sequencing.py rename to src/abstractatlas/layout/poster_sequencing.py diff --git a/src/ohbm2026/standby.py b/src/abstractatlas/standby.py similarity index 100% rename from src/ohbm2026/standby.py rename to src/abstractatlas/standby.py diff --git a/src/ohbm2026/titles.py b/src/abstractatlas/titles.py similarity index 100% rename from src/ohbm2026/titles.py rename to src/abstractatlas/titles.py diff --git a/src/ohbm2026/ui_data/__init__.py b/src/abstractatlas/ui_data/__init__.py similarity index 100% rename from src/ohbm2026/ui_data/__init__.py rename to src/abstractatlas/ui_data/__init__.py diff --git a/src/ohbm2026/ui_data/abstracts.py b/src/abstractatlas/ui_data/abstracts.py similarity index 100% rename from src/ohbm2026/ui_data/abstracts.py rename to src/abstractatlas/ui_data/abstracts.py diff --git a/src/ohbm2026/ui_data/authors.py b/src/abstractatlas/ui_data/authors.py similarity index 100% rename from src/ohbm2026/ui_data/authors.py rename to src/abstractatlas/ui_data/authors.py diff --git a/src/ohbm2026/ui_data/builder.py b/src/abstractatlas/ui_data/builder.py similarity index 100% rename from src/ohbm2026/ui_data/builder.py rename to src/abstractatlas/ui_data/builder.py diff --git a/src/ohbm2026/ui_data/cells.py b/src/abstractatlas/ui_data/cells.py similarity index 100% rename from src/ohbm2026/ui_data/cells.py rename to src/abstractatlas/ui_data/cells.py diff --git a/src/ohbm2026/ui_data/dimensions.py b/src/abstractatlas/ui_data/dimensions.py similarity index 100% rename from src/ohbm2026/ui_data/dimensions.py rename to src/abstractatlas/ui_data/dimensions.py diff --git a/src/ohbm2026/ui_data/enrichment.py b/src/abstractatlas/ui_data/enrichment.py similarity index 100% rename from src/ohbm2026/ui_data/enrichment.py rename to src/abstractatlas/ui_data/enrichment.py diff --git a/src/ohbm2026/ui_data/formats/__init__.py b/src/abstractatlas/ui_data/formats/__init__.py similarity index 100% rename from src/ohbm2026/ui_data/formats/__init__.py rename to src/abstractatlas/ui_data/formats/__init__.py diff --git a/src/ohbm2026/ui_data/formats/gzip_json_shards.py b/src/abstractatlas/ui_data/formats/gzip_json_shards.py similarity index 100% rename from src/ohbm2026/ui_data/formats/gzip_json_shards.py rename to src/abstractatlas/ui_data/formats/gzip_json_shards.py diff --git a/src/ohbm2026/ui_data/formats/parquet_single.py b/src/abstractatlas/ui_data/formats/parquet_single.py similarity index 100% rename from src/ohbm2026/ui_data/formats/parquet_single.py rename to src/abstractatlas/ui_data/formats/parquet_single.py diff --git a/src/ohbm2026/ui_data/link_check.py b/src/abstractatlas/ui_data/link_check.py similarity index 100% rename from src/ohbm2026/ui_data/link_check.py rename to src/abstractatlas/ui_data/link_check.py diff --git a/src/ohbm2026/ui_data/manifest.py b/src/abstractatlas/ui_data/manifest.py similarity index 100% rename from src/ohbm2026/ui_data/manifest.py rename to src/abstractatlas/ui_data/manifest.py diff --git a/src/ohbm2026/ui_data/neighbors.py b/src/abstractatlas/ui_data/neighbors.py similarity index 100% rename from src/ohbm2026/ui_data/neighbors.py rename to src/abstractatlas/ui_data/neighbors.py diff --git a/src/ohbm2026/ui_data/questions.py b/src/abstractatlas/ui_data/questions.py similarity index 100% rename from src/ohbm2026/ui_data/questions.py rename to src/abstractatlas/ui_data/questions.py diff --git a/src/ohbm2026/ui_data/standby_slots.py b/src/abstractatlas/ui_data/standby_slots.py similarity index 100% rename from src/ohbm2026/ui_data/standby_slots.py rename to src/abstractatlas/ui_data/standby_slots.py diff --git a/src/ohbm2026/ui_data/state_key.py b/src/abstractatlas/ui_data/state_key.py similarity index 100% rename from src/ohbm2026/ui_data/state_key.py rename to src/abstractatlas/ui_data/state_key.py diff --git a/src/ohbm2026/ui_data/topics.py b/src/abstractatlas/ui_data/topics.py similarity index 100% rename from src/ohbm2026/ui_data/topics.py rename to src/abstractatlas/ui_data/topics.py diff --git a/src/ohbm2026/ui_data/types.py b/src/abstractatlas/ui_data/types.py similarity index 100% rename from src/ohbm2026/ui_data/types.py rename to src/abstractatlas/ui_data/types.py diff --git a/src/ohbm2026/ui_data/vectors.py b/src/abstractatlas/ui_data/vectors.py similarity index 100% rename from src/ohbm2026/ui_data/vectors.py rename to src/abstractatlas/ui_data/vectors.py diff --git a/src/ohbm2026/util/__init__.py b/src/abstractatlas/util/__init__.py similarity index 100% rename from src/ohbm2026/util/__init__.py rename to src/abstractatlas/util/__init__.py diff --git a/src/ohbm2026/util/json_io.py b/src/abstractatlas/util/json_io.py similarity index 100% rename from src/ohbm2026/util/json_io.py rename to src/abstractatlas/util/json_io.py From f25c0d05b3a0a8a5f66572d7e1ae53d9a7a58168 Mon Sep 17 00:00:00 2001 From: Satrajit Ghosh Date: Fri, 3 Jul 2026 15:19:11 -0400 Subject: [PATCH 2/3] =?UTF-8?q?refactor(027)!:=20rename=20ohbm2026?= =?UTF-8?q?=E2=86=92abstractatlas=20+=20ohbmcli=E2=86=92aacli=20(US1,=20da?= =?UTF-8?q?ta=20preserved)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mechanical, behavior-preserving rename (US1 / the shippable MVP slice): - `git mv src/ohbm2026 → src/abstractatlas`; surgically rewrite imports + `ohbm2026.` refs across src/, tests/, scripts/, .github/, docs/ (README + CLAUDE.md pre-SPECKIT). Two passes: `(from|import) ohbm2026` and `ohbm2026.` EXCEPT `ohbm2026.parquet`. - pyproject: package name→abstractatlas; `aacli = abstractatlas.cli:main`; legacy per-stage `ohbm-*` scripts dropped (most pointed at Stage-5-moved modules; functions remain importable). - Deprecation shims (FR-003/SC-007): `ohbmcli` entry point warns then delegates to aacli; `src/ohbm2026/__init__.py` meta-path shim redirects `import ohbm2026[.*]` → `abstractatlas[.*]` with a DeprecationWarning. Both labeled with removal follow-up. DATA PRESERVED (FR-004) — NOT renamed: `ohbm2026.parquet`, `/ohbm2026/` route, `SITE_MODE='ohbm'`, `OHBM2026_*` CI vars, conference `kind`, `ohbm2026.ui.*`/`analytics` localStorage keys, `--ohbm2026-parquet` flag, `ohbm2026_state_key`, on-disk paths/state-keys. No data regen, no site re-publish, byte-identical. Site (TypeScript) untouched — it never imports the Python package (T007 verified no-op). Verified: full suite `Ran 1034 — OK (skipped=1)`, identical to baseline (SC-002); import smoke test OK; constitution-check --full exit 0; residual `git grep 'ohbm2026|ohbmcli'` = only data-identity, shims, historical specs. US2 (ingestor arch) + US3 (LinkML schema) follow in later slices. Co-Authored-By: Claude Opus 4.8 --- .github/scripts/resolve-data-channel.sh | 2 +- .github/workflows/deploy-ui.yml | 4 +- .github/workflows/pr-preview.yml | 4 +- CLAUDE.md | 57 +++++-- README.md | 146 +++++++++--------- docs/abstracts-book-plan.md | 12 +- docs/cllm-claims-plan.md | 8 +- docs/clustering-analysis-plan.md | 2 +- docs/ohbm26-plan.md | 32 ++-- docs/per-stage-pattern.md | 28 ++-- docs/poster-layout-optimizer-plan.md | 6 +- docs/poster-sequencing-benchmark-plan.md | 2 +- docs/repo-review-2026-03-28.md | 14 +- docs/reproducibility-vision.md | 30 ++-- docs/search-unification-plan.md | 2 +- pyproject.toml | 28 ++-- scripts/build_category_evaluation_rollup.py | 2 +- scripts/build_ui_data.py | 4 +- scripts/cluster_projection_silhouette.py | 4 +- scripts/derive_neuroscape_centroids.py | 2 +- scripts/distill_dimensions.py | 2 +- scripts/evaluate_label_systems.py | 2 +- scripts/fetch_ui_inputs.sh | 2 +- scripts/layout/analyze_poster_layout.py | 2 +- scripts/layout/benchmark_poster_sequencing.py | 4 +- scripts/layout/extract_layout_geometry.py | 2 +- ...generate_advanced_global_path_proposals.py | 4 +- .../generate_semantic_layout_proposals.py | 2 +- .../generate_semantic_path_proposals.py | 2 +- ...generate_target_poster_layout_proposals.py | 4 +- scripts/layout/optimize_poster_layout.py | 2 +- .../layout/plot_poster_layout_floorplan.py | 4 +- .../run_advanced_global_path_experiment.py | 4 +- .../run_nocd_checkpoint_sweep_experiment.py | 4 +- .../run_nocd_classic_predict_experiment.py | 4 +- scripts/layout/sweep_diffusion_variants.py | 4 +- .../sweep_global_path_mapalign_variants.py | 4 +- scripts/layout/sweep_global_path_variants.py | 4 +- scripts/layout/verify_proposed_listings.py | 4 +- scripts/openai_api_smoke.py | 2 +- scripts/openai_batch_smoke.py | 4 +- scripts/plot_voyage_stage2_umap_3d.py | 2 +- scripts/reference_split_regression_probe.py | 4 +- scripts/run_analyze_matrix.py | 4 +- scripts/run_build_atlas_package.py | 6 +- scripts/run_build_book.py | 8 +- scripts/run_embed_matrix.py | 4 +- scripts/run_enrich_abstracts.py | 6 +- scripts/run_fetch_abstracts.py | 6 +- scripts/run_fetch_withdrawn.py | 6 +- scripts/run_gmm_overlap_experiment.py | 4 +- scripts/validate_compose_recipe_baseline.py | 8 +- scripts/write_topic_group_report.py | 6 +- src/abstractatlas/analyze/__init__.py | 16 +- .../analyze/category_evaluation.py | 8 +- src/abstractatlas/analyze/category_rollup.py | 4 +- src/abstractatlas/analyze/centroids.py | 4 +- src/abstractatlas/analyze/clusters.py | 6 +- src/abstractatlas/analyze/communities.py | 4 +- src/abstractatlas/analyze/errors.py | 6 +- src/abstractatlas/analyze/llm_adapter.py | 4 +- src/abstractatlas/analyze/projections.py | 6 +- src/abstractatlas/analyze/provenance.py | 2 +- src/abstractatlas/analyze/rollup.py | 2 +- src/abstractatlas/analyze/runners.py | 28 ++-- src/abstractatlas/analyze/stage.py | 6 +- src/abstractatlas/analyze/storage.py | 6 +- src/abstractatlas/analyze/topic_clusters.py | 4 +- src/abstractatlas/analyze/topics.py | 2 +- src/abstractatlas/analyze/umap.py | 2 +- src/abstractatlas/assets.py | 6 +- src/abstractatlas/atlas_hosting/cli.py | 4 +- src/abstractatlas/atlas_hosting/compare.py | 2 +- src/abstractatlas/atlas_hosting/manifest.py | 6 +- src/abstractatlas/atlas_hosting/r2_client.py | 2 +- src/abstractatlas/atlas_hosting/uploader.py | 4 +- src/abstractatlas/atlas_package/cli.py | 8 +- src/abstractatlas/atlas_package/link_check.py | 2 +- .../atlas_package/neighbour_index.py | 2 +- .../atlas_package/neuroscape_loader.py | 2 +- .../atlas_package/ohbm_projector.py | 4 +- .../atlas_package/orchestrator.py | 2 +- .../atlas_package/parquet_writer.py | 2 +- src/abstractatlas/atlas_package/provenance.py | 4 +- .../atlas_package/semantic_index.py | 2 +- src/abstractatlas/atlas_package/umap_fit.py | 6 +- .../atlas_package/vectors_compute.py | 4 +- src/abstractatlas/book/__init__.py | 2 +- src/abstractatlas/book/assemble_pdf.py | 4 +- src/abstractatlas/book/author_index.py | 2 +- src/abstractatlas/book/cli.py | 16 +- src/abstractatlas/book/corpus.py | 12 +- src/abstractatlas/book/provenance.py | 8 +- src/abstractatlas/book/render_markdown.py | 10 +- src/abstractatlas/book/render_per_abstract.py | 18 +-- src/abstractatlas/book/render_via_pandoc.py | 20 +-- src/abstractatlas/book/sort.py | 2 +- src/abstractatlas/cli.py | 42 +++-- src/abstractatlas/data/__init__.py | 2 +- .../data/__pycache__/__init__.cpython-314.pyc | Bin 351 -> 361 bytes src/abstractatlas/embed/__init__.py | 10 +- src/abstractatlas/embed/components.py | 2 +- src/abstractatlas/embed/compose.py | 6 +- src/abstractatlas/embed/hf.py | 2 +- src/abstractatlas/embed/neuroscape.py | 4 +- src/abstractatlas/embed/openai.py | 2 +- src/abstractatlas/embed/provenance.py | 4 +- src/abstractatlas/embed/stage.py | 16 +- src/abstractatlas/embed/storage.py | 2 +- src/abstractatlas/embed/voyage.py | 2 +- src/abstractatlas/enrich/claims.py | 8 +- src/abstractatlas/enrich/figures.py | 6 +- src/abstractatlas/enrich/flex_tier.py | 2 +- src/abstractatlas/enrich/markdown_render.py | 2 +- src/abstractatlas/enrich/openalex.py | 8 +- src/abstractatlas/enrich/references.py | 4 +- src/abstractatlas/enrich/stage.py | 16 +- src/abstractatlas/exceptions.py | 34 ++-- src/abstractatlas/fetch/__init__.py | 2 +- src/abstractatlas/fetch/stage.py | 14 +- src/abstractatlas/layout/poster_layout.py | 6 +- src/abstractatlas/layout/poster_sequencing.py | 4 +- src/abstractatlas/titles.py | 4 +- src/abstractatlas/ui_data/abstracts.py | 20 +-- src/abstractatlas/ui_data/authors.py | 2 +- src/abstractatlas/ui_data/builder.py | 24 +-- src/abstractatlas/ui_data/cells.py | 4 +- src/abstractatlas/ui_data/dimensions.py | 2 +- src/abstractatlas/ui_data/formats/__init__.py | 2 +- .../ui_data/formats/gzip_json_shards.py | 2 +- .../ui_data/formats/parquet_single.py | 2 +- src/abstractatlas/ui_data/manifest.py | 2 +- src/abstractatlas/ui_data/neighbors.py | 2 +- src/abstractatlas/ui_data/questions.py | 6 +- src/abstractatlas/ui_data/state_key.py | 10 +- src/abstractatlas/ui_data/topics.py | 2 +- src/abstractatlas/ui_data/types.py | 2 +- src/ohbm2026/__init__.py | 58 +++++++ tests/test_analyze_centroids.py | 6 +- tests/test_analyze_cli.py | 12 +- tests/test_analyze_communities.py | 6 +- tests/test_analyze_provenance.py | 6 +- tests/test_analyze_rollup.py | 6 +- tests/test_analyze_stage.py | 10 +- tests/test_analyze_storage.py | 4 +- tests/test_analyze_topic_clusters.py | 4 +- tests/test_analyze_topics.py | 6 +- tests/test_analyze_umap.py | 6 +- tests/test_artifacts.py | 2 +- tests/test_assets.py | 38 ++--- tests/test_atlas_cli.py | 6 +- tests/test_atlas_exceptions.py | 2 +- tests/test_atlas_hosting_cli.py | 6 +- tests/test_atlas_hosting_compare.py | 4 +- tests/test_atlas_hosting_content_hash.py | 2 +- tests/test_atlas_hosting_manifest.py | 6 +- tests/test_atlas_hosting_r2_client.py | 4 +- tests/test_atlas_hosting_uploader.py | 8 +- tests/test_atlas_link_check.py | 6 +- tests/test_atlas_lod.py | 4 +- tests/test_atlas_orchestrator.py | 6 +- tests/test_atlas_parquet_writer.py | 10 +- tests/test_atlas_provenance.py | 6 +- tests/test_atlas_semantic_index.py | 6 +- tests/test_atlas_umap_fit.py | 6 +- tests/test_atlas_vectors_compute.py | 6 +- tests/test_book_assembly.py | 14 +- tests/test_book_author_index.py | 4 +- tests/test_book_author_index_buckets.py | 6 +- tests/test_book_cache.py | 10 +- tests/test_book_corpus.py | 6 +- tests/test_book_failure_isolation.py | 4 +- tests/test_book_figure_check.py | 2 +- tests/test_book_figure_normalise.py | 2 +- tests/test_book_html_to_md.py | 34 ++-- tests/test_book_margins.py | 6 +- tests/test_book_markdown.py | 22 +-- tests/test_book_no_ai_audit.py | 20 +-- tests/test_book_render_pdf.py | 14 +- tests/test_book_sort.py | 4 +- tests/test_book_standby.py | 14 +- tests/test_book_toc.py | 6 +- tests/test_category_evaluation.py | 2 +- tests/test_category_rollup.py | 2 +- tests/test_cli.py | 20 +-- tests/test_cluster_palette.py | 4 +- tests/test_compose_recipe.py | 6 +- tests/test_decimation.py | 4 +- tests/test_derive_neuroscape_centroids.py | 2 +- tests/test_docx_retirement.py | 4 +- tests/test_eco_vocabulary.py | 2 +- tests/test_embed_components.py | 2 +- tests/test_embed_provenance.py | 4 +- tests/test_embed_stage.py | 6 +- tests/test_embed_storage.py | 4 +- tests/test_enrich_markdown_render.py | 6 +- tests/test_enrich_stage.py | 40 ++--- tests/test_enrich_storage.py | 24 +-- tests/test_enrich_text.py | 6 +- tests/test_fetch_stage.py | 76 ++++----- tests/test_flex_tier.py | 6 +- tests/test_graphql_api.py | 26 ++-- tests/test_image_quality.py | 2 +- tests/test_neighbour_index.py | 6 +- tests/test_neuroscape.py | 26 ++-- tests/test_neuroscape_clusters_runner.py | 12 +- tests/test_neuroscape_derivation.py | 8 +- tests/test_neuroscape_loader.py | 6 +- tests/test_nocd_experiments.py | 2 +- tests/test_ohbm2026_parquet_rename.py | 6 +- tests/test_ohbm_projector.py | 8 +- tests/test_openalex.py | 50 +++--- tests/test_poster_layout.py | 2 +- tests/test_poster_sequencing.py | 2 +- tests/test_schema_diff.py | 66 ++++---- tests/test_stage20_exceptions.py | 2 +- tests/test_stage2_claims.py | 6 +- tests/test_stage2_figures.py | 6 +- tests/test_stage2_references.py | 6 +- tests/test_stage3_exceptions.py | 2 +- tests/test_standby_schema.py | 6 +- tests/test_state_key_rename.py | 4 +- tests/test_titles.py | 4 +- tests/test_ui_data_abstracts.py | 18 +-- tests/test_ui_data_acknowledgments.py | 2 +- tests/test_ui_data_ai_provenance.py | 2 +- tests/test_ui_data_authors.py | 4 +- tests/test_ui_data_builder.py | 2 +- tests/test_ui_data_cells.py | 2 +- tests/test_ui_data_dimensions.py | 20 +-- tests/test_ui_data_link_check.py | 2 +- tests/test_ui_data_manifest.py | 4 +- tests/test_ui_data_state_key.py | 2 +- 233 files changed, 1063 insertions(+), 964 deletions(-) create mode 100644 src/ohbm2026/__init__.py diff --git a/.github/scripts/resolve-data-channel.sh b/.github/scripts/resolve-data-channel.sh index c49814ed..a312ce76 100755 --- a/.github/scripts/resolve-data-channel.sh +++ b/.github/scripts/resolve-data-channel.sh @@ -45,7 +45,7 @@ # Dropbox shared links OR Cloudflare R2 (e.g. https://aadata.cirrusscience.org/ # /.parquet). This script and the site loader are unchanged — the # URL is opaque to both; loader.ts only rewrites Dropbox hosts, so R2 URLs pass -# through verbatim. `ohbmcli upload-atlas-package` emits a ready-to-paste +# through verbatim. `aacli upload-atlas-package` emits a ready-to-paste # channel entry in exactly the shape above. set -euo pipefail diff --git a/.github/workflows/deploy-ui.yml b/.github/workflows/deploy-ui.yml index 27c3bf83..6536a1bf 100644 --- a/.github/workflows/deploy-ui.yml +++ b/.github/workflows/deploy-ui.yml @@ -30,7 +30,7 @@ on: - 'site/**' - '.github/workflows/deploy-ui.yml' - 'specs/008-ui-rewrite/contracts/references.yaml' - - 'src/ohbm2026/ui_data/link_check.py' + - 'src/abstractatlas/ui_data/link_check.py' workflow_dispatch: inputs: target: @@ -191,7 +191,7 @@ jobs: # the deploy when a citation 4xx's or times out (T086 / T088). # Runs on both targets — a broken reference shouldn't reach # sandbox either. - run: PYTHONPATH=src python -m ohbm2026.ui_data.link_check specs/008-ui-rewrite/contracts/references.yaml + run: PYTHONPATH=src python -m abstractatlas.ui_data.link_check specs/008-ui-rewrite/contracts/references.yaml # Resolve the parquet URLs from the keyed registry variable, using the # channel this ref declares in site/data-channel.json. The same diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml index d20d3859..be8ec4b0 100644 --- a/.github/workflows/pr-preview.yml +++ b/.github/workflows/pr-preview.yml @@ -14,7 +14,7 @@ on: - 'site/**' - '.github/workflows/pr-preview.yml' - 'specs/008-ui-rewrite/contracts/references.yaml' - - 'src/ohbm2026/ui_data/link_check.py' + - 'src/abstractatlas/ui_data/link_check.py' workflow_dispatch: permissions: @@ -71,7 +71,7 @@ jobs: # Same check that gates the production deploy in deploy-ui.yml — # mirrored here so PR previews surface a broken reference BEFORE # the merge instead of failing at deploy time. - run: PYTHONPATH=src python -m ohbm2026.ui_data.link_check specs/008-ui-rewrite/contracts/references.yaml + run: PYTHONPATH=src python -m abstractatlas.ui_data.link_check specs/008-ui-rewrite/contracts/references.yaml # Resolve the parquet URLs from the keyed registry variable, using the # channel this PR branch declares in site/data-channel.json. A PR can diff --git a/CLAUDE.md b/CLAUDE.md index 906d1d82..e4bb6b11 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ Local pipeline that ingests OHBM 2026 accepted abstracts from Oxford Abstracts ( There are two coupled but distinct tracks: -- **Track A — canonical corpus pipeline**: driven by `ohbmcli` (`src/ohbm2026/cli.py`). Produces the authoritative artifacts under `data/primary/`, `data/cache/`, `data/outputs/experiments/`, and `data/outputs/exported-sites/`. +- **Track A — canonical corpus pipeline**: driven by `aacli` (`src/abstractatlas/cli.py`). Produces the authoritative artifacts under `data/primary/`, `data/cache/`, `data/outputs/experiments/`, and `data/outputs/exported-sites/`. - **Track B — exploratory layout/sequencing**: driven by standalone scripts in `scripts/` and recorded runs under `data/outputs/proposals/`. These produce comparative evidence, not silent replacements for canonical outputs. ## Non-negotiables (from `.specify/memory/constitution.md`) @@ -85,10 +85,10 @@ Optional dependency groups (only install what a workflow needs): ## CLI entrypoint -The canonical interface is `ohbmcli` (mapped in `pyproject.toml`): +The canonical interface is `aacli` (mapped in `pyproject.toml`): ```bash -PYTHONPATH=src .venv/bin/python -m ohbm2026.cli +PYTHONPATH=src .venv/bin/python -m abstractatlas.cli ``` Subcommands group into stages (see README for full options): @@ -99,18 +99,18 @@ Subcommands group into stages (see README for full options): - analysis: `semantic-analysis`, `cluster-benchmark`, `umap-plot`, `compare-projections`, `optimize-projections` - UI data package: `scripts/build_ui_data.py` (Stage 6 canonical; emits `site/static/data/` consumed by the SvelteKit site under `site/`). -Poster layout/sequencing is **not** in `ohbmcli` AND is **parked** as of Stage 5 (see `ohbm2026.layout` parked package and `scripts/layout/` for the 15 companion scripts: `scripts/layout/optimize_poster_layout.py`, `scripts/layout/analyze_poster_layout.py`, `scripts/layout/benchmark_poster_sequencing.py`, etc.). Always pass explicit input paths and a fresh `--output-root`/`--output-dir`; do not rely on stale baked-in defaults. +Poster layout/sequencing is **not** in `aacli` AND is **parked** as of Stage 5 (see `abstractatlas.layout` parked package and `scripts/layout/` for the 15 companion scripts: `scripts/layout/optimize_poster_layout.py`, `scripts/layout/analyze_poster_layout.py`, `scripts/layout/benchmark_poster_sequencing.py`, etc.). Always pass explicit input paths and a fresh `--output-root`/`--output-dir`; do not rely on stale baked-in defaults. ## Code architecture -All library code lives in `src/ohbm2026/`: +All library code lives in `src/abstractatlas/`: - `graphql_api.py` — Oxford Abstracts GraphQL client (env loading, batching, exponential-backoff retries). Defines `ABSTRACT_IDS_QUERY` (accepted), `WITHDRAWN_IDS_QUERY` (withdrawn), `ABSTRACT_CONTENTS_QUERY` (incl. `program_code` + `program_sessions_submissions` chain), `INTROSPECTION_QUERY` (canonical), and `fetch_schema_introspection`. - `assets.py` — figure asset download/refresh (reuse-aware via `asset_stem` matching), `normalize_abstract` (maps `program_code` → `poster_id`, flattens `program_sessions_submissions` → `program_sessions`), `fetch_content_batches` generator with per-batch + per-record callback hooks, `advance_record_state` state-machine validator. -- `fetch_stage.py` — **Stage 1 orchestrator** for `ohbmcli fetch-abstracts` / `fetch-withdrawn`; drives introspection → tiered schema diff → checkpoint lifecycle → batched fetch with figure download → atomic-write corpus + schema + provenance. +- `fetch_stage.py` — **Stage 1 orchestrator** for `aacli fetch-abstracts` / `fetch-withdrawn`; drives introspection → tiered schema diff → checkpoint lifecycle → batched fetch with figure download → atomic-write corpus + schema + provenance. - `schema_diff.py` — tiered HARD/SOFT/INFORMATIONAL field-level diff classifier; pure functions, no I/O. - `exceptions.py` — typed cross-stage exception hierarchy rooted at `OhbmStageError(RuntimeError)`. Stage 1 subtree: `Stage1Error` → `SchemaContractError`, `CheckpointError`, `FigureFailureError`. Stage 2 subtree: `Stage2Error` → `EnrichmentError`, `CacheVersionError`, `ComponentFailureThresholdError`. `ProvenanceError` is shared (both stages enforce the no-absolute-/-no-`~` path-boundary rule). Re-exports `GraphQLAPIError`. -- `enrich_stage.py` — **Stage 2 orchestrator** for `ohbmcli enrich-abstracts`; reads the accepted corpus, runs figures + claims + references components with per-component caching keyed by `sha256(input || model_id)`, writes the enriched corpus as SQLite + zlib(json) per row, writes provenance with model identifiers and cache hit/miss counts. Optional Parquet export via `--export-parquet PATH` (lazy-imports `pyarrow`). +- `enrich_stage.py` — **Stage 2 orchestrator** for `aacli enrich-abstracts`; reads the accepted corpus, runs figures + claims + references components with per-component caching keyed by `sha256(input || model_id)`, writes the enriched corpus as SQLite + zlib(json) per row, writes provenance with model identifiers and cache hit/miss counts. Optional Parquet export via `--export-parquet PATH` (lazy-imports `pyarrow`). - `enrich_storage.py` — `EnrichedCorpusWriter` SQLite I/O helper for Stage 2 (atomic temp→rename) plus `read_one_by_id` / `iter_enriched` / `corpus_metadata`. Stdlib only. - `enrichment.py` — Stage 2 building blocks (markdown conversion, legacy figure-analysis helpers). Wrapped (not refactored) by `enrich_stage.py`. Stage 2.1 replaces the `cllm`-based claim-extraction path with the agentic OpenAI Responses API call in `stage2_claims.py`. - `stage2_figures.py`, `stage2_claims.py`, `stage2_references.py` — Stage 2.1 per-component production runners. Figures: per-abstract grouped vision call with local JPEG-q85 compression + a four-field quality probe (`image_quality.py`). Claims: agentic Responses API call with three function tools (`verify_source_quote`, `lookup_eco_code`, `dedupe_check`) returning Pydantic-validated, ECO-annotated claims. References: thin adapter to the existing `openalex.collect_reference_metadata` pipeline. @@ -122,9 +122,9 @@ All library code lives in `src/ohbm2026/`: - `titles.py` — title normalization rules (used by `title-audit`). - `artifacts.py` — shared artifact-naming/state-key helpers used across stages. - `category_evaluation.py`, `category_rollup.py` — compare learned cluster families against submitter taxonomies. -- `layout/` (**parked** as of Stage 5 — specs/007-package-reorg/) — poster_layout, poster_sequencing, nocd_experiments. Preserved verbatim for future revival; not actively maintained. Tests under `tests/test_poster_*.py` + `tests/test_nocd_experiments.py` still run with import-paths updated to `ohbm2026.layout.*`. The 15 companion scripts live under `scripts/layout/`. Revive when a new organizer cycle needs poster-layout work. +- `layout/` (**parked** as of Stage 5 — specs/007-package-reorg/) — poster_layout, poster_sequencing, nocd_experiments. Preserved verbatim for future revival; not actively maintained. Tests under `tests/test_poster_*.py` + `tests/test_nocd_experiments.py` still run with import-paths updated to `abstractatlas.layout.*`. The 15 companion scripts live under `scripts/layout/`. Revive when a new organizer cycle needs poster-layout work. - `ui_data/` — **Stage 6** UI data-package builders. `manifest.py`, `abstracts.py`, `authors.py`, `cells.py`, `topics.py`, `neighbors.py`, `enrichment.py`, `vectors.py` produce per-shard envelopes (every shard carries a top-level `build_info` block — FR-019 + CA-008). `state_key.py` discovers the corpus + Stage 4 rollup state-keys at build time (CA-007). `builder.py` orchestrates + enforces the 8 cross-shard invariants from `specs/008-ui-rewrite/data-model.md` §8. `link_check.py` HEAD-validates `specs/008-ui-rewrite/contracts/references.yaml` (every external citation from the About page; non-zero exit blocks the deploy — FR-017). CLI entry: `scripts/build_ui_data.py`. Schema: every emitted JSON shard validates against `specs/008-ui-rewrite/contracts/ui_data.linkml.yaml` via `scripts/validate_ui_data.sh`. The SvelteKit site lives at `site/` (self-contained pnpm project; gh-pages deploy via `.github/workflows/{deploy-ui,pr-preview,pr-preview-cleanup}.yml`; runtime data fetched from the Dropbox tarball at `vars.OHBM2026_UI_DATA_PACKAGE_URL`). -- `atlas_hosting/` — **Stage 20** (spec 020-cloudflare-r2-migration) data-bundle publishing to Cloudflare R2 + Dropbox-vs-R2 comparison. `content_hash.py` (streamed sha256 + `/` content-addressed keys), `r2_client.py` (boto3 S3 client from `.env`; `head_object` existence; multipart upload; creds by name only), `uploader.py` (discover the four required parquets across two locations — `ohbm2026.parquet` via `--ohbm2026-parquet`, `neuroscape`/`atlas`/`neuroscape_vectors` from `--package-dir`; idempotent skip-if-present + size-guard; immutable, never overwrites), `manifest.py` (`UploadManifest` provenance under `data/provenance/atlas_upload_provenance__.json`), `compare.py` (Range/CORS/byte-parity probes → `data/outputs/data-hosting-comparison__.json`). CLI: `ohbmcli upload-atlas-package` + `compare-data-hosting`; typed `Stage20Error` subtree. R2 public base = `https://aadata.cirrusscience.org`; the site loader + `resolve-data-channel.sh` are UNCHANGED (R2 URLs are opaque). Dropbox stays the production default — cutover deferred. +- `atlas_hosting/` — **Stage 20** (spec 020-cloudflare-r2-migration) data-bundle publishing to Cloudflare R2 + Dropbox-vs-R2 comparison. `content_hash.py` (streamed sha256 + `/` content-addressed keys), `r2_client.py` (boto3 S3 client from `.env`; `head_object` existence; multipart upload; creds by name only), `uploader.py` (discover the four required parquets across two locations — `ohbm2026.parquet` via `--ohbm2026-parquet`, `neuroscape`/`atlas`/`neuroscape_vectors` from `--package-dir`; idempotent skip-if-present + size-guard; immutable, never overwrites), `manifest.py` (`UploadManifest` provenance under `data/provenance/atlas_upload_provenance__.json`), `compare.py` (Range/CORS/byte-parity probes → `data/outputs/data-hosting-comparison__.json`). CLI: `aacli upload-atlas-package` + `compare-data-hosting`; typed `Stage20Error` subtree. R2 public base = `https://aadata.cirrusscience.org`; the site loader + `resolve-data-channel.sh` are UNCHANGED (R2 URLs are opaque). Dropbox stays the production default — cutover deferred. - `cli.py` — single dispatch entrypoint that wires the above into subcommands. Tests in `tests/` mirror the module names and use `unittest`. @@ -149,11 +149,11 @@ The directory hierarchy is part of the contract — don't write to other roots: Current canonical defaults (the UI consumes these): -- Stage 2 single entry: `ohbmcli enrich-abstracts` (`scripts/run_enrich_abstracts.py`). Reads `data/primary/abstracts.json`, writes `data/primary/abstracts_enriched.sqlite` + per-component caches + `data/provenance/abstracts_enrich_provenance__.json`. Optional `--export-parquet PATH`. +- Stage 2 single entry: `aacli enrich-abstracts` (`scripts/run_enrich_abstracts.py`). Reads `data/primary/abstracts.json`, writes `data/primary/abstracts_enriched.sqlite` + per-component caches + `data/provenance/abstracts_enrich_provenance__.json`. Optional `--export-parquet PATH`. - figure-interpretation model: OpenAI `gpt-5.4-mini` (flex tier on by default), per-abstract grouped Responses API call with manuscript-text context + in-memory JPEG-q85@1024px compression + a four-field local quality probe. Cached under `data/cache/figure_analysis/.json`. - claims-extraction: agentic OpenAI Responses API call with `gpt-5.4-mini` (flex tier on by default) — three function tools (verify_source_quote, lookup_eco_code, dedupe_check); Pydantic-validated structured output annotated with ECO v1 codes. Cached under `data/cache/claim_analysis/.json` (key = `sha256(manuscript || model_id || vocabulary_version)`). The legacy `cllm` zero-shot path was removed in Stage 2.1. - reference-resolution strategy: `refs.v1+openai-gpt-5-nano` (multi-stage: LLM-assisted splitting → DOI/PMID → OpenAlex title search → Semantic Scholar fallback), cached under `data/cache/reference_metadata/.json`. -- Stage 3 single entry: `ohbmcli embed-matrix` (`scripts/run_embed_matrix.py`). Per-component bundles for voyage / minilm / openai / pubmedbert × {title, introduction, methods, results, conclusion, claims}. Output: `data/outputs/embeddings//__/{vectors.npy,ids.npy,metadata.json,provenance.json}`. State-key suffix on the bundle dir lets multiple historical versions coexist; clean stale corpora with `rm -rf data/outputs/embeddings/*/*__`. Run-level provenance at `data/provenance/embeddings_matrix_provenance__.json`. Per-abstract cache under `data/cache/embeddings//.json` (key = `sha256(text || model_id || model_version)`). +- Stage 3 single entry: `aacli embed-matrix` (`scripts/run_embed_matrix.py`). Per-component bundles for voyage / minilm / openai / pubmedbert × {title, introduction, methods, results, conclusion, claims}. Output: `data/outputs/embeddings//__/{vectors.npy,ids.npy,metadata.json,provenance.json}`. State-key suffix on the bundle dir lets multiple historical versions coexist; clean stale corpora with `rm -rf data/outputs/embeddings/*/*__`. Run-level provenance at `data/provenance/embeddings_matrix_provenance__.json`. Per-abstract cache under `data/cache/embeddings//.json` (key = `sha256(text || model_id || model_version)`). - embedding bundles in use by the UI (recipes composed at read time via `neuroscape.compose_recipe`): `voyage_stage2_published` (mean of voyage `title+introduction+methods+results+conclusion`, then optional NeuroScape Stage-2 transform) and `minilm_claims` (the per-component `minilm_claims` bundle directly). - UI projection: composed at consumption time from the per-component minilm bundles using `compose_recipe(["title", "introduction", "methods", "results", "conclusion"], model_key="minilm")`. @@ -169,7 +169,40 @@ Current canonical defaults (the UI consumes these): For additional context about technologies to be used, project structure, shell commands, and other important information, read the current plan -at `specs/026-neuroscape-year-density/plan.md`. Stage 26 (Track A, +at `specs/027-abstractatlas-rename-ingestors/plan.md`. Stage 27 (Track A, +Python package + CLI + docs; a few site test strings) does two sequenced +things: (1) **renames** the component `ohbm2026`→`abstractatlas` (package +`src/ohbm2026/`→`src/abstractatlas/` via `git mv` + import rewrite across +~86 files) and the CLI `ohbmcli`→**`aacli`** (module form +`python -m abstractatlas.cli`; legacy `ohbm-*` scripts→`aa-*` or dropped), +with `ohbmcli`/`import ohbm2026` kept as **labeled deprecation shims** for +one cycle; and (2) **generalizes ingestion** into a pluggable architecture — +a NEW `src/abstractatlas/ingest/` subpackage with an `Ingestor` ABC +(`pull`→`normalize`→`validate`), a runtime-discoverable `registry` +(never a hardcoded source list — CA-007), and a standardized **LinkML +ingest schema** `contracts/ingest-schema.linkml.yaml` (`IngestedDocument` +core + `ConferenceDocument`/`LiteratureDocument` extensions). The two +existing sources are **wrapped** (not rewritten) as the first ingestors: +`ohbm-2026` (conference, wraps `fetch/stage.py`+`assets.normalize_abstract`) +and `neuroscape-pubmed` (literature, wraps the NeuroScape normalization) — +byte-identical outputs, zero downstream-stage change (SC-003/SC-004). +CRITICAL constraint: **data is preserved** — all on-disk paths, state-keys, +and published data-package names (incl. the historical `ohbm2026.parquet`, +the `/ohbm2026/` route, `SITE_MODE='ohbm'`, `OHBM2026_*` CI vars) are NOT +renamed (source-data identity ≠ component identity), so no data regen and +no site re-publish (byte-identical, FR-004). Scope is FOUNDATION ONLY: +arXiv/bioRxiv/medRxiv + new-conference ingestors are explicit non-goals +(follow-on specs the architecture enables). Verified: full unittest suite +green under new names (SC-002) + fixture artifacts identical to pre-rename +(SC-001) + new failing-first tests for registry/schema-validation/port- +fidelity. Companions: `research.md`, `data-model.md`, +`contracts/{rename-map,ingestor-interface,ingest-schema.linkml.yaml,cli-aacli}.md`, +`quickstart.md`. NOTE for future sessions: after this ships, the package is +`abstractatlas` and the CLI is `aacli` — update muscle memory (`ohbmcli` +still works via a deprecation shim slated for removal). + +The immediately-prior Stage 26 plan is at +`specs/026-neuroscape-year-density/plan.md`. Stage 26 (Track A, UI-only, `site/`, neuroscape mode only) makes the NeuroScape scatter backdrop's density **year-aware** when a year filter is active: each year in the window contributes dots ∝ √(that year's count in the filtered set) diff --git a/README.md b/README.md index 915d8401..38b1f22a 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ original abstract download to the current latest step. Repository home: -- Git remote `origin`: `git@github.com:sensein/ohbm2026.git` +- Git remote `origin`: `git@github.com:sensein/abstractatlas.git` - GitHub URL: [github.com/sensein/ohbm2026](https://github.com/sensein/ohbm2026) Project conventions that should not be violated live in @@ -111,11 +111,11 @@ The latest end state of the project is: - **`/ohbm2026/`** — unchanged from prior stages (FR-022 / SC-008 functional intact). Data-loader path renamed `data.parquet → ohbm2026.parquet`. - **`/neuroscape/`** — new PubMed subsite. Browse home with title-only search across 461k articles + cluster + year facets; detail page at `/neuroscape/abstract//` paints local fields (title, year, cluster, k-NN neighbours) immediately and fetches authors / journal / abstract body / DOI at view time from NCBI E-utilities EFetch (in-memory Promise cache + token-bucket rate limiter + retry-with-backoff + body-offline state with Retry button). - Three parquets back the three sites: `ohbm2026.parquet` (renamed from `data.parquet`, content-identical), `neuroscape.parquet` (full NeuroScape 1999–2023 corpus + cluster table + k=20 neighbours), `atlas.parquet` (scatter rows pointing into the two siblings by stable id; embeds sibling state-keys for cross-parquet drift detection). The new `ohbmcli build-atlas-package` orchestrator fits a deterministic UMAP on the NeuroScape Stage-2 vectors (seed=0, n_neighbors=30, min_dist=0.10, metric=cosine) and projects OHBM 2026 via `umap.transform`. Multi-mode build matrix lives in `.github/workflows/deploy-ui.yml` (one `pnpm build` per `SITE_MODE`). Root `/404.html` is a smart shim that bounces every deep-linked path to the right SPA shell via `?spa=`; loop-safety prevents infinite redirects if a sibling subsite isn't deployed yet. + Three parquets back the three sites: `ohbm2026.parquet` (renamed from `data.parquet`, content-identical), `neuroscape.parquet` (full NeuroScape 1999–2023 corpus + cluster table + k=20 neighbours), `atlas.parquet` (scatter rows pointing into the two siblings by stable id; embeds sibling state-keys for cross-parquet drift detection). The new `aacli build-atlas-package` orchestrator fits a deterministic UMAP on the NeuroScape Stage-2 vectors (seed=0, n_neighbors=30, min_dist=0.10, metric=cosine) and projects OHBM 2026 via `umap.transform`. Multi-mode build matrix lives in `.github/workflows/deploy-ui.yml` (one `pnpm build` per `SITE_MODE`). Root `/404.html` is a smart shim that bounces every deep-linked path to the right SPA shell via `?spa=`; loop-safety prevents infinite redirects if a sibling subsite isn't deployed yet. ## Atlas UI -The site lives under `site/` (a self-contained SvelteKit project; SvelteKit 2 + Vite 6 + Svelte 5). The data-package builder lives under `src/ohbm2026/ui_data/`. Capabilities: typo-tolerant lexical search, transformers.js-backed semantic search (MiniLM-L6 ONNX in a Web Worker against an int8-quantised corpus matrix), 2D + 3D UMAP with lasso + cluster colour-coding, interactive facets, cart + email-my-list, a guided tour (shepherd.js), and an About page whose external citations are HEAD-checked at build time (`link_check.py`). Every OHBM 2026 surface (home, About, abstract permalink) lives under the `/ohbm2026/` URL subpath so the same domain can later host other conferences without URL-space collision. +The site lives under `site/` (a self-contained SvelteKit project; SvelteKit 2 + Vite 6 + Svelte 5). The data-package builder lives under `src/abstractatlas/ui_data/`. Capabilities: typo-tolerant lexical search, transformers.js-backed semantic search (MiniLM-L6 ONNX in a Web Worker against an int8-quantised corpus matrix), 2D + 3D UMAP with lasso + cluster colour-coding, interactive facets, cart + email-my-list, a guided tour (shepherd.js), and an About page whose external citations are HEAD-checked at build time (`link_check.py`). Every OHBM 2026 surface (home, About, abstract permalink) lives under the `/ohbm2026/` URL subpath so the same domain can later host other conferences without URL-space collision. **Browser support & mobile rendering (Stage 24, `specs/024-fix-ios-safari-load`).** Supported: current + previous major release of every evergreen browser, including **iPhone Safari** (WebKit). The atlas detects device capability **at runtime** (`site/src/lib/device/capability.ts` — viewport, `navigator.deviceMemory`, WebGL probe, reduced-motion; never a UA/iOS allow-list): on phones / low-budget devices it mounts the 2D map only (the 3D scatter is deferred behind an explicit "Show 3D map" toggle), disables auto-rotation, and skips the eager semantic-worker warm (search still initialises lazily on first query) — this keeps the page inside iOS WebKit's tight WebGL-context and per-tab memory limits. Any unrecoverable load failure now shows a visible message via `+error.svelte` / an in-page error state, never a blank screen or endless spinner. Verified by `vitest run` (capability gate + load-state machine) and a WebKit/iPhone Playwright project (`pnpm exec playwright test --project=iphone-webkit`; needs `playwright install webkit`). @@ -201,7 +201,7 @@ The `--proposal-listing` flag is optional — without it the `poster_standby` fi Per-PR previews surface in the **PR's Deployments box** (top-of-PR, via the `environment:` declaration in `.github/workflows/pr-preview.yml`) — NOT as a bot comment. The short committish (first 7 chars of git SHA) bakes into the page `` + the persistent footer affordance via the `VITE_BUILD_SHA` env var injected by the deploy workflows, so reviewers can verify each PR-preview reflects the latest pushed commit at-a-glance (FR-022 + SC-011). -### Publishing the data bundle to Cloudflare R2 (`ohbmcli upload-atlas-package`) +### Publishing the data bundle to Cloudflare R2 (`aacli upload-atlas-package`) Stage 20 (`specs/020-cloudflare-r2-migration/`) adds an alternative host for the data bundle: **Cloudflare R2**, an S3-compatible object store. Unlike the Dropbox in-place overwrite above, R2 uploads are **content-addressed and immutable** — every artifact is stored at `<sha256>/<filename>`, so a new build adds new keys and never clobbers a published URL. This makes future atlas updates a safe re-run rather than an overwrite. @@ -222,13 +222,13 @@ Install the client (`uv pip install --python .venv/bin/python ".[r2]"`), then: ```bash # Dry-run first (hashes + existence checks; no PUT, no manifest): -PYTHONPATH=src .venv/bin/python -m ohbm2026.cli upload-atlas-package \ +PYTHONPATH=src .venv/bin/python -m abstractatlas.cli upload-atlas-package \ --package-dir data/outputs/atlas-package__<state-key> \ --ohbm2026-parquet data/outputs/parquets/<corpus-key>/ohbm2026.parquet \ --dry-run # Upload (idempotent: a re-run on an unchanged build uploads zero bytes): -PYTHONPATH=src .venv/bin/python -m ohbm2026.cli upload-atlas-package \ +PYTHONPATH=src .venv/bin/python -m abstractatlas.cli upload-atlas-package \ --package-dir data/outputs/atlas-package__<state-key> \ --ohbm2026-parquet data/outputs/parquets/<corpus-key>/ohbm2026.parquet ``` @@ -239,7 +239,7 @@ Before trusting a cutover, generate the Dropbox-vs-R2 evidence (byte-parity / CO ```bash gh variable get OHBM2026_UI_DATA_PACKAGE_URLS > /tmp/registry.json # local only; never commit -PYTHONPATH=src .venv/bin/python -m ohbm2026.cli compare-data-hosting \ +PYTHONPATH=src .venv/bin/python -m abstractatlas.cli compare-data-hosting \ --registry /tmp/registry.json \ --dropbox-channel <current-prod-key> --r2-channel <new-r2-key> \ --origin https://abstractatlas.brainkb.org @@ -263,11 +263,11 @@ Optional, depending on which branch of the pipeline you run: - Voyage API access for Voyage embeddings - OpenAlex API key for authenticated reference matching -### Stage 15 prerequisites (`ohbmcli build-atlas-package`) +### Stage 15 prerequisites (`aacli build-atlas-package`) Stage 15 (spec `015-neuroscape-context`) builds the cross-conference atlas landing page and the new `/neuroscape/` subsite. Operators -running `ohbmcli build-atlas-package` need the NeuroScape v1.0.1 +running `aacli build-atlas-package` need the NeuroScape v1.0.1 release on disk under `data/inputs/neuroscape-source/v101/` with this layout (gitignored): @@ -316,7 +316,7 @@ Common keys: - `HF_TOKEN` - optional for Hugging Face model downloads - `NCBI_API_KEY` - - optional. Used by `ohbmcli build-atlas-package`'s link-check pass (validates a small set of PubMed + DOI URLs that the atlas-root page exposes). Provider rate limit only — NOT a write-access token. + - optional. Used by `aacli build-atlas-package`'s link-check pass (validates a small set of PubMed + DOI URLs that the atlas-root page exposes). Provider rate limit only — NOT a write-access token. - `VITE_NCBI_API_KEY` - optional. Baked into the `/neuroscape/` SvelteKit bundle at build time. Raises the runtime PubMed body-fetch rate limit on `/neuroscape/abstract/<pubmed_id>/` from 3 req/s (anon) to 10 req/s. - `VITE_DATA_PACKAGE_URL_OHBM2026` / `VITE_DATA_PACKAGE_URL_NEUROSCAPE` / `VITE_DATA_PACKAGE_URL_ATLAS` @@ -334,16 +334,16 @@ Use this as the quick answer to "what do I need before I run this step?" | Workflow | Required secret(s) | Extra local tool(s) | Notes | | --- | --- | --- | --- | -| `ohbmcli fetch-abstracts` / `fetch-withdrawn` | `OHBM2026_API` | none | Stage 1 — accepted + withdrawn corpora; authors fetched inline | -| `ohbmcli refresh-assets` | none | none | Uses the existing local normalized corpus | -| `ohbmcli enrich-abstracts` | `OPENAI_API_KEY`; optional `OPENALEX_API` (recommended) | `.[enrich]` optional extra (openai>=2.0 + Pillow>=10 + pydantic>=2) | Stage 2.1 — figures + agentic claims + references with per-component caches. Default model `gpt-5.4-mini`, flex tier ON by default. Flags: `--invalidate <component>`, `--no-flex-figures` / `--no-flex-claims`, `--concurrency-figures N` / `--concurrency-claims N` (default 30 each), `--figure-model-id` / `--claims-model-id` / `--reference-strategy-id`, `--export-parquet PATH` (needs the `parquet` extra). | -| `ohbmcli title-audit` | none | none | Reads local normalized corpus only | -| `ohbmcli embed-minilm` / `embed-hf` | optional `HF_TOKEN` | `sentence-transformers` | `HF_TOKEN` is only needed for gated/private Hub access | -| `ohbmcli embed-openai` | `OPENAI_API_KEY` | none | Hosted embedding route | -| `ohbmcli embed-voyage` | `VOYAGE_API` | none | Voyage embedding route | -| `ohbmcli apply-published-stage2` / `embed-stage2` | none | local model dependencies already in `.venv` | Uses local artifacts | -| `ohbmcli semantic-analysis` / `cluster-benchmark` / `umap-plot` / `compare-projections` / `optimize-projections` | none | optional `plotly`, `umap-learn` | Purely local once embeddings exist | -| `ohbmcli book` | none | `.[abstracts_book]` (markdownify, beautifulsoup4, pikepdf, Pillow, joblib) + system `pandoc` + LaTeX engine (Tectonic recommended) | Stage 11 + Stage 11.1 — composes Book of Abstracts; markdown bundle always; `--format pdf` runs the per-abstract pipeline with content-hash caching; outputs to `data/outputs/book/`. (DOCX retired in Stage 11.1 US3.) | +| `aacli fetch-abstracts` / `fetch-withdrawn` | `OHBM2026_API` | none | Stage 1 — accepted + withdrawn corpora; authors fetched inline | +| `aacli refresh-assets` | none | none | Uses the existing local normalized corpus | +| `aacli enrich-abstracts` | `OPENAI_API_KEY`; optional `OPENALEX_API` (recommended) | `.[enrich]` optional extra (openai>=2.0 + Pillow>=10 + pydantic>=2) | Stage 2.1 — figures + agentic claims + references with per-component caches. Default model `gpt-5.4-mini`, flex tier ON by default. Flags: `--invalidate <component>`, `--no-flex-figures` / `--no-flex-claims`, `--concurrency-figures N` / `--concurrency-claims N` (default 30 each), `--figure-model-id` / `--claims-model-id` / `--reference-strategy-id`, `--export-parquet PATH` (needs the `parquet` extra). | +| `aacli title-audit` | none | none | Reads local normalized corpus only | +| `aacli embed-minilm` / `embed-hf` | optional `HF_TOKEN` | `sentence-transformers` | `HF_TOKEN` is only needed for gated/private Hub access | +| `aacli embed-openai` | `OPENAI_API_KEY` | none | Hosted embedding route | +| `aacli embed-voyage` | `VOYAGE_API` | none | Voyage embedding route | +| `aacli apply-published-stage2` / `embed-stage2` | none | local model dependencies already in `.venv` | Uses local artifacts | +| `aacli semantic-analysis` / `cluster-benchmark` / `umap-plot` / `compare-projections` / `optimize-projections` | none | optional `plotly`, `umap-learn` | Purely local once embeddings exist | +| `aacli book` | none | `.[abstracts_book]` (markdownify, beautifulsoup4, pikepdf, Pillow, joblib) + system `pandoc` + LaTeX engine (Tectonic recommended) | Stage 11 + Stage 11.1 — composes Book of Abstracts; markdown bundle always; `--format pdf` runs the per-abstract pipeline with content-hash caching; outputs to `data/outputs/book/`. (DOCX retired in Stage 11.1 US3.) | | `scripts/optimize_poster_layout.py` / `scripts/analyze_poster_layout.py` | none | none | Uses local proposal inputs, authors, and layout assets | | poster sequencing scripts under `scripts/` | none | none | Use local proposals and embeddings | | `scripts/build_ui_data.py` | none | none | Stage 6: builds the static-JSON data package consumed by the SvelteKit site under `site/` | @@ -406,13 +406,13 @@ Pick the sequence that matches what you are trying to regenerate. Run these in order when rebuilding the main deliverable from upstream data: -1. `ohbmcli fetch-abstracts` (authors are fetched inline; replaces the former `ingest` + `authors` pair) -2. `ohbmcli enrich-abstracts` (replaces the former `analyze-figures` + `extract-claims` + `enrich` + `reference-metadata` quartet; one entry, per-component caches under `data/cache/{figure_analysis,claim_analysis,reference_metadata}`) -3. `ohbmcli title-audit` +1. `aacli fetch-abstracts` (authors are fetched inline; replaces the former `ingest` + `authors` pair) +2. `aacli enrich-abstracts` (replaces the former `analyze-figures` + `extract-claims` + `enrich` + `reference-metadata` quartet; one entry, per-component caches under `data/cache/{figure_analysis,claim_analysis,reference_metadata}`) +3. `aacli title-audit` 4. one or more embedding commands such as `embed-minilm`, `embed-voyage`, or `embed-openai` -5. `ohbmcli apply-published-stage2` if you want the published Voyage stage-2 space -6. `ohbmcli semantic-analysis`, `cluster-benchmark`, `umap-plot`, or `compare-projections` for the cluster and projection products you want the UI to consume -7. `ohbmcli analyze-matrix` (Stage 4 — produces the canonical `data/outputs/analysis/annotations__<state-key>.{parquet,sqlite}` rollup the Stage 6 site consumes) +5. `aacli apply-published-stage2` if you want the published Voyage stage-2 space +6. `aacli semantic-analysis`, `cluster-benchmark`, `umap-plot`, or `compare-projections` for the cluster and projection products you want the UI to consume +7. `aacli analyze-matrix` (Stage 4 — produces the canonical `data/outputs/analysis/annotations__<state-key>.{parquet,sqlite}` rollup the Stage 6 site consumes) 8. `scripts/build_ui_data.py` (Stage 6 — see the "Stage 6: UI" section above) ### Add Or Refresh A Cluster Family @@ -420,14 +420,14 @@ Run these in order when rebuilding the main deliverable from upstream data: Use this when you already have the corpora and want a new cluster output: 1. confirm the required embedding bundle exists under `data/outputs/experiments/embeddings/` -2. run `ohbmcli semantic-analysis` for community-detection style outputs -3. run `ohbmcli cluster-benchmark` for k-sweep style outputs +2. run `aacli semantic-analysis` for community-detection style outputs +3. run `aacli cluster-benchmark` for k-sweep style outputs 4. optionally run `scripts/evaluate_label_systems.py` to compare a new cluster family against the submitter taxonomy -5. point Stage 4 (`ohbmcli analyze-matrix`) or layout scripts at the new cluster directory +5. point Stage 4 (`aacli analyze-matrix`) or layout scripts at the new cluster directory ### Generate Or Refresh A Layout Proposal -> **The `layout/` package is parked as of Stage 5** (specs/007-package-reorg/) — poster-layout / sequencing / NOCD code is preserved verbatim under `src/ohbm2026/layout/` and `scripts/layout/`, but is not actively maintained. The instructions below remain accurate; only the script paths have moved (`scripts/optimize_poster_layout.py` → `scripts/layout/optimize_poster_layout.py`, etc.). Revive when a new organizer cycle needs poster work. +> **The `layout/` package is parked as of Stage 5** (specs/007-package-reorg/) — poster-layout / sequencing / NOCD code is preserved verbatim under `src/abstractatlas/layout/` and `scripts/layout/`, but is not actively maintained. The instructions below remain accurate; only the script paths have moved (`scripts/optimize_poster_layout.py` → `scripts/layout/optimize_poster_layout.py`, etc.). Revive when a new organizer cycle needs poster work. Use this when you want a new organizer-facing proposal: @@ -455,7 +455,7 @@ Use this when the data products already exist locally: ## End-To-End Workflow -Use `ohbmcli` for the corpus, enrichment, embedding, clustering, and UI +Use `aacli` for the corpus, enrichment, embedding, clustering, and UI pipeline. Use the script wrappers under `scripts/` for proposal generation, layout analysis, and sequencing experiments. @@ -471,10 +471,10 @@ namespace. PYTHONPATH=src .venv/bin/python scripts/run_fetch_abstracts.py ``` -Equivalent through `ohbmcli`: +Equivalent through `aacli`: ```bash -PYTHONPATH=src .venv/bin/python -m ohbm2026.cli fetch-abstracts +PYTHONPATH=src .venv/bin/python -m abstractatlas.cli fetch-abstracts ``` What it does: @@ -511,7 +511,7 @@ Important behavior: PYTHONPATH=src .venv/bin/python scripts/run_fetch_withdrawn.py ``` -Or `ohbmcli fetch-withdrawn`. Output: +Or `aacli fetch-withdrawn`. Output: `data/primary/abstracts_withdrawn.json`. Filter: `decision_status="Withdrawn" AND complete=true AND archived=false`. Same per-record shape as the accepted corpus. State-key namespace @@ -527,7 +527,7 @@ Use this if the raw JSON already exists and you only need to rebuild or prune local figure links. ```bash -PYTHONPATH=src .venv/bin/python -m ohbm2026.cli refresh-assets --reuse-existing-assets-only +PYTHONPATH=src .venv/bin/python -m abstractatlas.cli refresh-assets --reuse-existing-assets-only ``` ### 3. Enrich The Corpus @@ -543,10 +543,10 @@ are REPLACED by this single entry (FR-014). PYTHONPATH=src .venv/bin/python scripts/run_enrich_abstracts.py ``` -Equivalent through `ohbmcli`: +Equivalent through `aacli`: ```bash -PYTHONPATH=src .venv/bin/python -m ohbm2026.cli enrich-abstracts +PYTHONPATH=src .venv/bin/python -m abstractatlas.cli enrich-abstracts ``` What it does: @@ -606,7 +606,7 @@ normalize obvious title issues such as leading bullets, wrapping quotes, and stray outer whitespace. ```bash -PYTHONPATH=src .venv/bin/python -m ohbm2026.cli title-audit \ +PYTHONPATH=src .venv/bin/python -m abstractatlas.cli title-audit \ --input data/primary/abstracts.json \ --output data/outputs/experiments/title_audit/title_modifications.json ``` @@ -669,7 +669,7 @@ Single-model subcommands (`embed-voyage`, `embed-minilm`, `embed-openai`, Composing multi-component recipes downstream: ```python -from ohbm2026.neuroscape import compose_recipe +from abstractatlas.neuroscape import compose_recipe manuscript = compose_recipe( ["title", "introduction", "methods", "results", "conclusion"], model_key="voyage", @@ -689,13 +689,13 @@ Cost ballpark for the full 30-bundle matrix at fresh-cache: ~$1 USD Use this when you have a compatible Voyage stage-1 bundle. ```bash -PYTHONPATH=src .venv/bin/python -m ohbm2026.cli apply-published-stage2 +PYTHONPATH=src .venv/bin/python -m abstractatlas.cli apply-published-stage2 ``` #### Train a local stage-2 model ```bash -PYTHONPATH=src .venv/bin/python -m ohbm2026.cli embed-stage2 +PYTHONPATH=src .venv/bin/python -m abstractatlas.cli embed-stage2 ``` ### 7. Build Semantic Analysis And Cluster Outputs @@ -703,14 +703,14 @@ PYTHONPATH=src .venv/bin/python -m ohbm2026.cli embed-stage2 Community detection over an embedding bundle: ```bash -PYTHONPATH=src .venv/bin/python -m ohbm2026.cli semantic-analysis \ +PYTHONPATH=src .venv/bin/python -m abstractatlas.cli semantic-analysis \ --embeddings-dir data/outputs/experiments/embeddings/voyage_stage2_published ``` Clustering benchmark over an embedding bundle: ```bash -PYTHONPATH=src .venv/bin/python -m ohbm2026.cli cluster-benchmark \ +PYTHONPATH=src .venv/bin/python -m abstractatlas.cli cluster-benchmark \ --embeddings-dir data/outputs/experiments/embeddings/voyage_stage2_published \ --output-dir data/outputs/experiments/clustering_benchmark__<state-key> ``` @@ -718,7 +718,7 @@ PYTHONPATH=src .venv/bin/python -m ohbm2026.cli cluster-benchmark \ To benchmark a claims-only bundle around `25-30` clusters: ```bash -PYTHONPATH=src .venv/bin/python -m ohbm2026.cli cluster-benchmark \ +PYTHONPATH=src .venv/bin/python -m abstractatlas.cli cluster-benchmark \ --embeddings-dir data/outputs/experiments/embeddings/minilm_claims \ --output-dir data/outputs/experiments/clustering_benchmark_claims_25_30__<state-key> \ --k-min 25 \ @@ -742,9 +742,9 @@ PYTHONPATH=src .venv/bin/python scripts/evaluate_label_systems.py \ Projection outputs: ```bash -PYTHONPATH=src .venv/bin/python -m ohbm2026.cli umap-plot -PYTHONPATH=src .venv/bin/python -m ohbm2026.cli compare-projections -PYTHONPATH=src .venv/bin/python -m ohbm2026.cli optimize-projections +PYTHONPATH=src .venv/bin/python -m abstractatlas.cli umap-plot +PYTHONPATH=src .venv/bin/python -m abstractatlas.cli compare-projections +PYTHONPATH=src .venv/bin/python -m abstractatlas.cli optimize-projections ``` ### 7.5 Stage 4 Analysis Matrix (`analyze-matrix`) @@ -776,7 +776,7 @@ domain embedding, so voyage/minilm/openai/pubmedbert are auto-skipped for the `neuroscape_clusters` kind): ```bash -PYTHONPATH=src .venv/bin/python -m ohbm2026.cli analyze-matrix +PYTHONPATH=src .venv/bin/python -m abstractatlas.cli analyze-matrix ``` Run without an OpenAI key (topic keywords come from a local @@ -784,7 +784,7 @@ spaCy + c-TF-IDF pipeline; `Title`/`Description`/`Focus` are left empty): ```bash -PYTHONPATH=src .venv/bin/python -m ohbm2026.cli analyze-matrix \ +PYTHONPATH=src .venv/bin/python -m abstractatlas.cli analyze-matrix \ --skip-llm-topics ``` @@ -792,14 +792,14 @@ Restrict to one model (faster iteration; produces 6 bundles + 1 auto-skip event): ```bash -PYTHONPATH=src .venv/bin/python -m ohbm2026.cli analyze-matrix \ +PYTHONPATH=src .venv/bin/python -m abstractatlas.cli analyze-matrix \ --models voyage ``` Project a new abstract into an existing fitted UMAP bundle (US2): ```bash -PYTHONPATH=src .venv/bin/python -m ohbm2026.cli analyze-umap-project \ +PYTHONPATH=src .venv/bin/python -m abstractatlas.cli analyze-umap-project \ --fitted-bundle data/outputs/analysis/voyage_abstract/projections__<state-key>/ \ --input-vectors path/to/new_vectors.npy \ --algorithm native \ @@ -817,7 +817,7 @@ exact schemas. ### 8. Generate And Analyze Layout Proposals The stable route for proposal generation currently lives in the script wrappers -under `scripts/`, not in `ohbmcli`. +under `scripts/`, not in `aacli`. Generate a fresh proposal bundle: @@ -894,7 +894,7 @@ relying on older baked-in defaults. ### 10. Build The Static UI -The current latest delivery step is the **Atlas UI — SvelteKit site under `site/`**, fed by the Python data-package builder under `src/ohbm2026/ui_data/`. See the "Atlas UI" section near the top of this README for the canonical build recipe (`scripts/build_ui_data.py` then `pnpm dev` / `pnpm build`). The legacy `ohbmcli export-ui` / `build-ui` commands that wrote a hand-rolled HTML+JSON bundle into `data/outputs/exported-sites/ui-site__<state-key>/` have been removed. +The current latest delivery step is the **Atlas UI — SvelteKit site under `site/`**, fed by the Python data-package builder under `src/abstractatlas/ui_data/`. See the "Atlas UI" section near the top of this README for the canonical build recipe (`scripts/build_ui_data.py` then `pnpm dev` / `pnpm build`). The legacy `aacli export-ui` / `build-ui` commands that wrote a hand-rolled HTML+JSON bundle into `data/outputs/exported-sites/ui-site__<state-key>/` have been removed. ### 11. Book Of Abstracts (Stage 11 + Stage 11.1) @@ -935,7 +935,7 @@ uv pip install --python .venv/bin/python ".[abstracts_book]" Then invoke: ```bash -PYTHONPATH=src .venv/bin/python -m ohbm2026.cli book --format pdf --sort poster_id +PYTHONPATH=src .venv/bin/python -m abstractatlas.cli book --format pdf --sort poster_id ``` Useful Stage 11.1 flags: @@ -960,7 +960,7 @@ The deployed site lives at `abstractatlas.brainkb.org/ohbm2026/` (per spec 009-c If you already have raw abstracts: -- rerun `ohbmcli enrich-abstracts` (per-component caches make it cheap; pass `--invalidate <component>` if a single model identifier changed) +- rerun `aacli enrich-abstracts` (per-component caches make it cheap; pass `--invalidate <component>` if a single model identifier changed) - rerun `scripts/build_ui_data.py` to refresh the Stage 6 data package If you already have an enriched corpus and only changed UI code: @@ -969,20 +969,20 @@ If you already have an enriched corpus and only changed UI code: If only one component model changed (e.g., new figure model): -- rerun `ohbmcli enrich-abstracts --invalidate figures --figure-model-id <new>` +- rerun `aacli enrich-abstracts --invalidate figures --figure-model-id <new>` - rerun `scripts/build_ui_data.py` If you already have embeddings but want new cluster evaluations: - rerun `cluster-benchmark` - optionally rerun `scripts/evaluate_label_systems.py` -- optionally rerun `ohbmcli analyze-matrix` + `scripts/build_ui_data.py` +- optionally rerun `aacli analyze-matrix` + `scripts/build_ui_data.py` If you specifically want to refresh the claims-based semantic lens: - rerun `embed-minilm --fields claims --output-name minilm_claims` - rerun `cluster-benchmark --embeddings-dir data/outputs/experiments/embeddings/minilm_claims --output-dir data/outputs/experiments/embeddings/minilm_claims/clustering_benchmark_25_30 --k-min 25 --k-max 30` -- rerun `ohbmcli analyze-matrix` + `scripts/build_ui_data.py` +- rerun `aacli analyze-matrix` + `scripts/build_ui_data.py` If you want to regenerate a proposal without touching the corpora: @@ -996,63 +996,63 @@ If you want to rerun sequencing experiments on an existing proposal: ## Module Layout -- `src/ohbm2026/graphql_api.py` +- `src/abstractatlas/graphql_api.py` - GraphQL access, env loading, batching, retries; canonical `INTROSPECTION_QUERY`; `fetch_abstract_ids`, `fetch_withdrawn_ids`, `fetch_schema_introspection` -- `src/ohbm2026/assets.py` +- `src/abstractatlas/assets.py` - figure asset download/refresh (reuse-aware), normalization (`normalize_abstract` maps `program_code` → `poster_id` and flattens `program_sessions_submissions[]` → `program_sessions[]`), `fetch_content_batches` generator with per-batch + per-record callback hooks, `advance_record_state` state-machine validator -- `src/ohbm2026/fetch_stage.py` +- `src/abstractatlas/fetch_stage.py` - **Stage 1 orchestrator**. Entry point for - `ohbmcli fetch-abstracts` and `ohbmcli fetch-withdrawn`. + `aacli fetch-abstracts` and `aacli fetch-withdrawn`. Drives: introspection → schema diff (HARD / SOFT / INFORMATIONAL) → checkpoint lifecycle → batched fetch → atomic-write corpus + schema + provenance → delete checkpoint on success. The canonical reference for the per-stage contract (see [docs/per-stage-pattern.md](docs/per-stage-pattern.md)) -- `src/ohbm2026/schema_diff.py` +- `src/abstractatlas/schema_diff.py` - tiered field-level schema-drift classifier (HARD / SOFT / INFORMATIONAL); pure functions, no I/O -- `src/ohbm2026/exceptions.py` +- `src/abstractatlas/exceptions.py` - typed cross-stage exception hierarchy rooted at `OhbmStageError(RuntimeError)`. Stage 1: `Stage1Error` → `SchemaContractError`, `CheckpointError`, `FigureFailureError`. Stage 2: `Stage2Error` → `EnrichmentError`, `CacheVersionError`, `ComponentFailureThresholdError`. `ProvenanceError` shared. Re-exports `GraphQLAPIError`. -- `src/ohbm2026/artifacts.py` +- `src/abstractatlas/artifacts.py` - shared path helpers (`build_schema_artifact_path`, `build_provenance_path`, `build_fetch_checkpoint_path`, `build_enrich_provenance_path`, `build_enrich_cache_path`, `PRIMARY_ABSTRACTS_PATH`, `PRIMARY_WITHDRAWN_ABSTRACTS_PATH`, `PRIMARY_ENRICHED_CORPUS_PATH`), state-key derivation -- `src/ohbm2026/enrich_stage.py` +- `src/abstractatlas/enrich_stage.py` - **Stage 2 orchestrator**. Entry point for - `ohbmcli enrich-abstracts`. Drives: backend discovery → + `aacli enrich-abstracts`. Drives: backend discovery → per-abstract figures + claims + references with per-component caching → atomic SQLite + zlib write → provenance write → optional Parquet export. The multi-component reference for the per-stage contract (see [docs/per-stage-pattern.md](docs/per-stage-pattern.md)). -- `src/ohbm2026/enrich_storage.py` +- `src/abstractatlas/enrich_storage.py` - SQLite + zlib I/O helper for Stage 2: `EnrichedCorpusWriter` (atomic temp→rename), `read_one_by_id`, `iter_enriched`, `corpus_metadata`. Stdlib only. -- `src/ohbm2026/enrichment.py` +- `src/abstractatlas/enrichment.py` - markdown conversion, figure analysis, claim extraction building blocks (wrapped by `enrich_stage.py`) -- `src/ohbm2026/openalex.py` +- `src/abstractatlas/openalex.py` - reference parsing and OpenAlex matching (wrapped by `enrich_stage.py`'s references component) -- `src/ohbm2026/neuroscape.py` +- `src/abstractatlas/neuroscape.py` - embeddings, stage-2 paths, semantic analysis, clustering, projections -- `src/ohbm2026/ui_data/` +- `src/abstractatlas/ui_data/` - Stage 6 static-JSON data-package builders (per-shard envelopes + cross-shard invariants + state-key discovery + reference link-check); CLI entry at `scripts/build_ui_data.py`. Consumed by the SvelteKit site under `site/`. -- `src/ohbm2026/cli.py` +- `src/abstractatlas/cli.py` - unified CLI entrypoint ## Main Outputs By Stage diff --git a/docs/abstracts-book-plan.md b/docs/abstracts-book-plan.md index c9c85475..fdfce856 100644 --- a/docs/abstracts-book-plan.md +++ b/docs/abstracts-book-plan.md @@ -12,7 +12,7 @@ the per-abstract pipeline introduced in Stage 11.1 — each abstract pandoc-renders in parallel with content+toolchain caching, then a two-pass assembly attaches a page-numbered author index. -DOCX export was **retired in Stage 11.1 US3** (`ohbmcli book --format docx` +DOCX export was **retired in Stage 11.1 US3** (`aacli book --format docx` exits 2 with a pointer at `--format md` and `--format pdf`). The real-corpus DOCX hit 2.8 GB even after figure resizing — too large for Word to open — and the markdown bundle + PDF cover every use @@ -39,7 +39,7 @@ live under `specs/011-abstracts-book/`: optional Tufte styling). - `data-model.md` — three layers (Stage-1 inputs → markdown-bearing in-memory model → outputs with figure-filename contract). -- `contracts/cli.md` — `ohbmcli book` flags, error path table, +- `contracts/cli.md` — `aacli book` flags, error path table, known limitations. - `quickstart.md` — operator runbook for install + first run. @@ -53,13 +53,13 @@ uv pip install --python .venv/bin/python ".[abstracts_book]" brew install pandoc tectonic # Markdown-only first run (no system deps needed): -PYTHONPATH=src .venv/bin/python -m ohbm2026.cli book --format md +PYTHONPATH=src .venv/bin/python -m abstractatlas.cli book --format md # Full PDF run: -PYTHONPATH=src .venv/bin/python -m ohbm2026.cli book --format pdf --sort poster_id +PYTHONPATH=src .venv/bin/python -m abstractatlas.cli book --format pdf --sort poster_id # All three formats + Tufte typography for the PDF: -PYTHONPATH=src .venv/bin/python -m ohbm2026.cli book --format all --sort poster_id --style tufte +PYTHONPATH=src .venv/bin/python -m abstractatlas.cli book --format all --sort poster_id --style tufte ``` Outputs land at `data/outputs/book/book__<state-key>/`. Figure @@ -108,7 +108,7 @@ Operator flags: Single-abstract debug (re-render one chunk in isolation, populating the cache): ```bash -PYTHONPATH=src .venv/bin/python -m ohbm2026.book.render_per_abstract \ +PYTHONPATH=src .venv/bin/python -m abstractatlas.book.render_per_abstract \ --poster-id 0042 --style plain ``` diff --git a/docs/cllm-claims-plan.md b/docs/cllm-claims-plan.md index ff3693ff..8273b4c1 100644 --- a/docs/cllm-claims-plan.md +++ b/docs/cllm-claims-plan.md @@ -25,8 +25,8 @@ experience overwhelming. ## Execution -1. Run `ohbmcli analyze-figures` first if you want cached figure notes included in +1. Run `aacli analyze-figures` first if you want cached figure notes included in the claim manuscript. -2. Run `ohbmcli extract-claims` to populate or resume the claim cache. -3. Run `ohbmcli enrich` to merge claim extraction into the enriched abstract data. -4. Run `scripts/build_ui_data.py` (Stage 6) so the SvelteKit site's data package picks up the new fields. (The legacy `ohbmcli export-ui` / `build-ui` commands have been removed; this plan predates Stage 2.1 + Stage 6 and is kept for historical reference only.) +2. Run `aacli extract-claims` to populate or resume the claim cache. +3. Run `aacli enrich` to merge claim extraction into the enriched abstract data. +4. Run `scripts/build_ui_data.py` (Stage 6) so the SvelteKit site's data package picks up the new fields. (The legacy `aacli export-ui` / `build-ui` commands have been removed; this plan predates Stage 2.1 + Stage 6 and is kept for historical reference only.) diff --git a/docs/clustering-analysis-plan.md b/docs/clustering-analysis-plan.md index 313a8c26..1f531faa 100644 --- a/docs/clustering-analysis-plan.md +++ b/docs/clustering-analysis-plan.md @@ -102,7 +102,7 @@ Runtime checks on real data: ## Status -- [x] Added `ohbmcli cluster-benchmark` +- [x] Added `aacli cluster-benchmark` - [x] Implemented method sweep across `kmeans`, `agglomerative-ward`, `agglomerative-average`, `gaussian-mixture`, and `birch` - [x] Added label-independent ranking using silhouette, Davies-Bouldin, Calinski-Harabasz, inter/intracluster distance ratio, and cluster-balance terms - [x] Added test coverage for method execution, metric computation, ranking, CLI wiring, and output writing diff --git a/docs/ohbm26-plan.md b/docs/ohbm26-plan.md index b2c757dc..74a08006 100644 --- a/docs/ohbm26-plan.md +++ b/docs/ohbm26-plan.md @@ -37,36 +37,36 @@ This is the operational planning document for the repository. ## Architecture -- `src/ohbm2026/graphql_api.py` +- `src/abstractatlas/graphql_api.py` - GraphQL requests, batching, retries, env loading, author/submission fetches -- `src/ohbm2026/assets.py` +- `src/abstractatlas/assets.py` - local asset indexing, figure URL extraction, reuse-aware downloads, JSON-only asset refresh, and ingest CLI -- `src/ohbm2026/enrichment.py` +- `src/abstractatlas/enrichment.py` - author export, HTML-to-markdown conversion, section mapping, local Qwen figure analysis, enriched abstract assembly, and phase-2 CLI -- `src/ohbm2026/neuroscape.py` +- `src/abstractatlas/neuroscape.py` - stage-one embeddings, semantic graph generation, clustering, and NeuroScape handoff manifest -- `src/ohbm2026/cli.py` - - unified `ohbmcli` command +- `src/abstractatlas/cli.py` + - unified `aacli` command ## Unified CLI -- `ohbmcli ingest` +- `aacli ingest` - fetch abstracts and figure assets from the API -- `ohbmcli refresh-assets` +- `aacli refresh-assets` - rebuild `local_assets` from the local JSON database without rerunning abstract extraction -- `ohbmcli authors` +- `aacli authors` - export author metadata -- `ohbmcli enrich` +- `aacli enrich` - build enriched abstracts from local databases -- `ohbmcli analyze-figures` +- `aacli analyze-figures` - analyze local figures with Ollama -- `ohbmcli embed-minilm` +- `aacli embed-minilm` - generate local MiniLM embeddings -- `ohbmcli embed-voyage` +- `aacli embed-voyage` - generate Voyage embeddings -- `ohbmcli semantic-analysis` +- `aacli semantic-analysis` - build the semantic graph and semantic cluster summaries from any local embedding bundle -- `ohbmcli write-manifest` +- `aacli write-manifest` - write the NeuroScape handoff manifest ## Execution Order @@ -226,7 +226,7 @@ Checks: - [x] Create a local MiniLM embedding pipeline and neighbor database. - [x] Create a purely semantic graph-and-cluster pipeline for the single-year corpus. - [x] Split the codebase into API, asset, enrichment, and embedding modules. -- [x] Add a unified `ohbmcli` entrypoint. +- [x] Add a unified `aacli` entrypoint. - [ ] Complete a full cached figure-analysis sweep with local `qwen3.5:35b`. - [ ] Validate the NeuroScape retraining path against the PR-based workflow and artifact requirements. diff --git a/docs/per-stage-pattern.md b/docs/per-stage-pattern.md index 7c5853ae..987b0fa8 100644 --- a/docs/per-stage-pattern.md +++ b/docs/per-stage-pattern.md @@ -3,8 +3,8 @@ Every pipeline stage in this repo (Stage 1 fetch, Stage 2 enrich, Stage 3+ analyses, …) satisfies the same six contracts. This doc defines them and points at two canonical reference implementations -— **Stage 1 (`src/ohbm2026/fetch_stage.py`)** for a single-fetch -stage, and **Stage 2 (`src/ohbm2026/enrich_stage.py`)** for a +— **Stage 1 (`src/abstractatlas/fetch_stage.py`)** for a single-fetch +stage, and **Stage 2 (`src/abstractatlas/enrich_stage.py`)** for a multi-component stage — for each. A new stage author should be able to read this page plus @@ -111,7 +111,7 @@ Stage 2 reference: What failures the stage surfaces loudly, with what typed cause, and at what exit code. -- Typed exception hierarchy in `ohbm2026.exceptions` rooted at a +- Typed exception hierarchy in `abstractatlas.exceptions` rooted at a cross-stage `OhbmStageError(RuntimeError)`. Stage 1 subclass tree: `Stage1Error` → `SchemaContractError`, `CheckpointError`, `FigureFailureError`. Stage 2 subclass tree: `Stage2Error` → @@ -219,17 +219,17 @@ Stage 2 shipped the per-stage pattern scaffolding with (`specs/004-enrich-production-wiring/`) wires production runners through those seams and keeps the orchestrator surface unchanged: -- **Figures**: `src/ohbm2026/stage2_figures.py` — +- **Figures**: `src/abstractatlas/stage2_figures.py` — `run_figure_component(abstract, model_id, flex_enabled, client, cwd)` groups all figures of one abstract into a single OpenAI Responses API call with manuscript text as context. In-memory JPEG-q85 compression at 1024 px (`compress_image`) + a four-field local - quality probe (`src/ohbm2026/image_quality.py`: + quality probe (`src/abstractatlas/image_quality.py`: `laplacian_variance`, `mean_brightness`, `native_max_dim`, `compression_ratio`) populate `local_quality_estimate` on every record. Pydantic `FigureInterpretationResponse` + a fixed `model_quality_estimate` enum validate the response server-side. -- **Claims**: `src/ohbm2026/stage2_claims.py` — +- **Claims**: `src/abstractatlas/stage2_claims.py` — `run_claims_component(abstract, ..., figure_interpretations)` issues a single agentic Responses API call with three function tools registered (`verify_source_quote`, `lookup_eco_code`, @@ -237,10 +237,10 @@ through those seams and keeps the orchestrator surface unchanged: annotate → dedupe internally. Post-response validation drops claims whose source quote isn't a substring of the manuscript OR whose `evidence_eco_codes` include off-vocabulary ECO IDs. -- **References**: `src/ohbm2026/stage2_references.py` is a thin +- **References**: `src/abstractatlas/stage2_references.py` is a thin adapter to the existing `openalex.collect_reference_metadata` async pipeline. -- **Flex tier**: `src/ohbm2026/flex_tier.py` — +- **Flex tier**: `src/abstractatlas/flex_tier.py` — `call_with_flex_fallback(...)` wraps each LLM call with a 1-flex + 1-standard retry budget; defaults to flex ON; per-component disable flags `--no-flex-figures` / `--no-flex-claims`. @@ -248,7 +248,7 @@ through those seams and keeps the orchestrator surface unchanged: fans out per-abstract work across a `ThreadPoolExecutor` (default 30 in flight). Per-component caps via `--concurrency-figures` / `--concurrency-claims`. -- **ECO v1 vocabulary** lives at `src/ohbm2026/data/eco_top_codes.json` +- **ECO v1 vocabulary** lives at `src/abstractatlas/data/eco_top_codes.json` (committed source). The claims component's `lookup_eco_code` tool draws from it without network access. - **Provenance extensions**: per-component `flex_tier_enabled`, @@ -269,7 +269,7 @@ When you write Stage N: 2. **Author the test file first** (Principle IV). One test per contract element, organized into a class per contract. 3. **Implement the orchestrator** in - `src/ohbm2026/<stage_name>.py`. Use Stage 1's `fetch_stage.py` + `src/abstractatlas/<stage_name>.py`. Use Stage 1's `fetch_stage.py` as the layout reference. Share helpers via `artifacts.py`, `exceptions.py`, and `schema_diff.py` where possible. 4. **Add a CLI subcommand** in `cli.py` that delegates to your @@ -280,12 +280,12 @@ When you write Stage N: ## Common Helpers -- `ohbm2026.artifacts.build_*_path(state_key)` — path derivation +- `abstractatlas.artifacts.build_*_path(state_key)` — path derivation under gitignored roots. -- `ohbm2026.artifacts.build_dependency_basis` + `build_state_key` — +- `abstractatlas.artifacts.build_dependency_basis` + `build_state_key` — deterministic state key from input fingerprint. -- `ohbm2026.exceptions.*` — typed exception hierarchy. -- `ohbm2026.schema_diff.*` — schema-diff / discovery primitives +- `abstractatlas.exceptions.*` — typed exception hierarchy. +- `abstractatlas.schema_diff.*` — schema-diff / discovery primitives (reusable across stages that talk to GraphQL). - The constitution lint at `.specify/scripts/bash/constitution-check.sh` catches the diff --git a/docs/poster-layout-optimizer-plan.md b/docs/poster-layout-optimizer-plan.md index 20dcacc1..d66e1677 100644 --- a/docs/poster-layout-optimizer-plan.md +++ b/docs/poster-layout-optimizer-plan.md @@ -34,7 +34,7 @@ Confirmed decisions for the first pass: ## Proposed Deliverables -- `src/ohbm2026/poster_layout.py` +- `src/abstractatlas/poster_layout.py` - reusable loading, feature extraction, optimization, numbering, and scoring helpers - `scripts/optimize_poster_layout.py` - CLI entrypoint to assign posters to sessions and output a proposed numbering/layout @@ -199,7 +199,7 @@ PYTHONPATH=src .venv/bin/python scripts/analyze_poster_layout.py \ --output data/poster_layout/analysis.json ``` -If this matures well, we can later fold the same functionality into `ohbmcli`. +If this matures well, we can later fold the same functionality into `aacli`. ## Test Plan @@ -227,7 +227,7 @@ If this matures well, we can later fold the same functionality into `ohbmcli`. ## Implementation Phases - [x] Confirm session-assignment model and physical numbering assumptions -- [x] Add a poster-layout design module in `src/ohbm2026/poster_layout.py` +- [x] Add a poster-layout design module in `src/abstractatlas/poster_layout.py` - [x] Add synthetic tests for constraints and scoring before implementing search - [x] Implement poster feature loading and validation - [x] Implement conflict and locality scoring diff --git a/docs/poster-sequencing-benchmark-plan.md b/docs/poster-sequencing-benchmark-plan.md index 275b240b..db4c3837 100644 --- a/docs/poster-sequencing-benchmark-plan.md +++ b/docs/poster-sequencing-benchmark-plan.md @@ -18,7 +18,7 @@ We can think of this explicitly as a weighted graph reordering problem: ## Why This Phase Now -The current semantic-path ordering logic is still based on greedy nearest-neighbor walks in [src/ohbm2026/poster_layout.py](/Users/satra/software/temp/ohbm2026/src/ohbm2026/poster_layout.py). +The current semantic-path ordering logic is still based on greedy nearest-neighbor walks in [src/abstractatlas/poster_layout.py](/Users/satra/software/temp/ohbm2026/src/abstractatlas/poster_layout.py). Relevant functions: diff --git a/docs/repo-review-2026-03-28.md b/docs/repo-review-2026-03-28.md index 086fc828..765f52c4 100644 --- a/docs/repo-review-2026-03-28.md +++ b/docs/repo-review-2026-03-28.md @@ -23,7 +23,7 @@ to navigate confidently. ## Strengths To Preserve -- the canonical pipeline is resumable and centered on `ohbmcli` +- the canonical pipeline is resumable and centered on `aacli` - experiment immutability is explicit and documented - test coverage is broad across pipeline, UI export, poster layout, and experiment tooling @@ -37,15 +37,15 @@ to navigate confidently. Current hotspots: -- `src/ohbm2026/neuroscape.py` +- `src/abstractatlas/neuroscape.py` - about `2951` lines, `106` functions -- `src/ohbm2026/poster_sequencing.py` +- `src/abstractatlas/poster_sequencing.py` - about `2371` lines, `59` functions -- `src/ohbm2026/openalex.py` +- `src/abstractatlas/openalex.py` - about `2279` lines, `76` functions -- `src/ohbm2026/poster_layout.py` +- `src/abstractatlas/poster_layout.py` - about `2191` lines, `60` functions -- `src/ohbm2026/enrichment.py` +- `src/abstractatlas/enrichment.py` - about `1437` lines, `59` functions Why this matters: @@ -95,7 +95,7 @@ Mitigation landed in this pass: Recommended next step: - introduce a `scripts/README.md` or eventually promote the most stable - organizer flows into explicit `ohbmcli` or package-level namespaces + organizer flows into explicit `aacli` or package-level namespaces ### P2: Canonical versus exploratory outputs still need stronger machine-readable boundaries diff --git a/docs/reproducibility-vision.md b/docs/reproducibility-vision.md index b7147ffd..5021e6b7 100644 --- a/docs/reproducibility-vision.md +++ b/docs/reproducibility-vision.md @@ -50,7 +50,7 @@ Oxford Abstracts source corpus into local, reproducible artifacts: - `data/outputs/exported-sites/ui-site__<state-key>/` - `export/ui-site/` -The canonical operational interface for this track is `ohbmcli`. +The canonical operational interface for this track is `aacli`. ### Track B: Exploratory layout and experiment workflows @@ -60,7 +60,7 @@ tooling. **As of Stage 5 (specs/007-package-reorg/) this track is parked.** The poster-layout / sequencing / NOCD source code moved to the parked -`src/ohbm2026/layout/` package; the 15 companion scripts moved to +`src/abstractatlas/layout/` package; the 15 companion scripts moved to `scripts/layout/`. Code is preserved verbatim for future revival — expect to revive when a new organizer cycle needs poster work. @@ -90,7 +90,7 @@ practical commitments future operators should internalize are: `data/outputs/experiments/title_audit/title_modifications.json` - long-running API and LLM jobs should checkpoint incrementally and remain resumable -- `ohbmcli` is the canonical interface for the main pipeline +- `aacli` is the canonical interface for the main pipeline - organizer-facing review outputs must preserve machine-readable provenance alongside summaries and HTML views - behavior-changing work should update the nearest plan or spec and define @@ -167,7 +167,7 @@ The defaults that future users should treat as current project reality are: siblings by stable id (NOT duplicating bodies). Manifest embeds the sibling state-keys for runtime drift detection. - The new `ohbmcli build-atlas-package` orchestrator emits + The new `aacli build-atlas-package` orchestrator emits `neuroscape.parquet` + `atlas.parquet` from the NeuroScape v1.0.1 release inputs + the existing voyage_stage2_published recipe. UMAP is fit deterministically (seed=0, n_neighbors=30, @@ -220,8 +220,8 @@ enrichment, references, embeddings, clusters, or the UI. The usual order is: -1. `ohbmcli refresh-assets` -2. `ohbmcli enrich-abstracts` — single Stage 2 entry (replaces the +1. `aacli refresh-assets` +2. `aacli enrich-abstracts` — single Stage 2 entry (replaces the former `analyze-figures` / `extract-claims` / `enrich` / `reference-metadata` quartet). Writes the SQLite + zlib enriched corpus at `data/primary/abstracts_enriched.sqlite` plus @@ -231,10 +231,10 @@ The usual order is: Use `--invalidate <component>` to force a single component to re-run; pass `--export-parquet PATH` (with the `parquet` optional extra installed) to emit a Parquet copy. -3. `ohbmcli title-audit` +3. `aacli title-audit` 4. embedding commands such as `embed-minilm`, `embed-voyage`, or `apply-published-stage2` -5. `ohbmcli cluster-benchmark` and related semantic-analysis steps +5. `aacli cluster-benchmark` and related semantic-analysis steps 6. **Stage 11 Book of Abstracts** (optional, reader-facing deliverable). The book composes every accepted abstract into a publication-quality PDF + markdown bundle + DOCX, sourced @@ -244,7 +244,7 @@ The usual order is: Markdown is the canonical intermediate; PDF/DOCX derive from it via pandoc. Invocation: ``` - PYTHONPATH=src .venv/bin/python -m ohbm2026.cli book \ + PYTHONPATH=src .venv/bin/python -m abstractatlas.cli book \ --format pdf --sort poster_id ``` Output at `data/outputs/book/book__<state-key>/`. Full spec + @@ -269,7 +269,7 @@ The usual order is: ``` Then HEAD-check the references registry (FR-017): ``` - PYTHONPATH=src .venv/bin/python -m ohbm2026.ui_data.link_check \ + PYTHONPATH=src .venv/bin/python -m abstractatlas.ui_data.link_check \ specs/008-ui-rewrite/contracts/references.yaml ``` Re-tar onto the Dropbox shared link (in-place to preserve the inode + @@ -283,15 +283,15 @@ Useful when a new operator needs to replay the pipeline from the Oxford Abstracts API. 1. configure `.env` -2. run `ohbmcli fetch-abstracts` (accepted corpus; replaces the - former `ohbmcli ingest`) — writes `data/primary/abstracts.json` +2. run `aacli fetch-abstracts` (accepted corpus; replaces the + former `aacli ingest`) — writes `data/primary/abstracts.json` plus the persisted GraphQL schema introspection at `data/inputs/abstracts_graphql_schema__<state-key>.json` and a machine-readable provenance record at `data/provenance/abstracts_fetch_provenance__<state-key>.json`. The stage is resumable from per-record checkpoint and detects upstream schema drift (HARD / SOFT / INFORMATIONAL tiers). -3. optionally run `ohbmcli fetch-withdrawn` to fetch the SEPARATE +3. optionally run `aacli fetch-withdrawn` to fetch the SEPARATE withdrawn-decision corpus into `data/primary/abstracts_withdrawn.json` (never co-mingled with the accepted corpus). @@ -320,7 +320,7 @@ record silently. This is the right choice for traceability and future audit. The move from monolithic enrichment toward task commands was a foundational decision. It made the project easier to resume, test, and explain, and it -turned `ohbmcli` into the shared operating surface. +turned `aacli` into the shared operating surface. ### 3. Prefer OpenAI figure analysis for the canonical enriched corpus @@ -419,7 +419,7 @@ If your change affects canonical outputs, also ask: ## Practical Guidance For Future Operators -- prefer `ohbmcli` when working on the canonical pipeline +- prefer `aacli` when working on the canonical pipeline - use `.venv/bin/python` or `uv` targeting `.venv` for every Python command - treat `scripts/` as focused workflow entrypoints, especially for experiments and organizer tooling diff --git a/docs/search-unification-plan.md b/docs/search-unification-plan.md index ff087c1c..363af37a 100644 --- a/docs/search-unification-plan.md +++ b/docs/search-unification-plan.md @@ -8,7 +8,7 @@ 1. **Consistent search** semantics across `ohbm2026`, `atlas-root`, `neuroscape` — same query grammar, same ranking contract, same result-assembly shape. 2. **Exhaustive reachability** — despite NeuroScape/atlas-root scale (~461k articles), the algorithm must provide a path to search the *entire* corpus, not a sample or a single routed cluster. -3. **UMAP-independent search** — what is rendered in the scatter must never gate search results on atlas-root or neuroscape. The scatter is display-only. It may *filter* results only when the user explicitly acts (facet selection, lasso) — exactly like ohbm2026. +3. **UMAP-independent search** — what is rendered in the scatter must never gate search results on atlas-root or neuroscape. The scatter is display-only. It may *filter* results only when the user explicitly acts (facet selection, lasso) — exactly like abstractatlas. --- diff --git a/pyproject.toml b/pyproject.toml index c8db5e0e..f7e29378 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] -name = "ohbm2026" +name = "abstractatlas" version = "0.2.0" -description = "Local ingestion pipeline for OHBM 2026 Oxford Abstracts data" +description = "Abstract Atlas: pluggable ingestion + atlas pipeline (OHBM 2026 abstracts, NeuroScape/PubMed, and future sources)" readme = "README.md" requires-python = ">=3.11" @@ -39,21 +39,15 @@ abstracts_book = [ ] [project.scripts] -ohbm-ingest = "ohbm2026.assets:main" -ohbm-authors = "ohbm2026.enrichment:authors_main" -ohbm-enrich = "ohbm2026.enrichment:enrich_main" -ohbm-analyze-figures = "ohbm2026.enrichment:analyze_figures_main" -ohbm-embed-stage2 = "ohbm2026.embed.neuroscape:stage2_main" -ohbm-apply-published-stage2 = "ohbm2026.embed.neuroscape:apply_pretrained_stage2_main" -ohbm-cluster-benchmark = "ohbm2026.embed.neuroscape:cluster_benchmark_main" -ohbm-semantic-analysis = "ohbm2026.embed.neuroscape:semantic_analysis_main" -ohbm-umap-plot = "ohbm2026.embed.neuroscape:umap_main" -ohbm-compare-projections = "ohbm2026.embed.neuroscape:projection_compare_main" -ohbm-optimize-projections = "ohbm2026.embed.neuroscape:projection_optimize_main" -ohbm-analyze-stage2 = "ohbm2026.embed.neuroscape:stage2_analysis_main" -ohbm-reference-metadata = "ohbm2026.openalex:main" -ohbm-write-manifest = "ohbm2026.embed.neuroscape:manifest_main" -ohbmcli = "ohbm2026.cli:main" +# Canonical CLI (spec 027: ohbmcli → aacli). +aacli = "abstractatlas.cli:main" +# Deprecated alias — warns then delegates to aacli. Kept one transition +# cycle; remove next release. (Spec 027 FR-003 / research R3.) +ohbmcli = "abstractatlas.cli:_ohbmcli_deprecated_main" +# The legacy per-stage `ohbm-*` console scripts were dropped in spec 027: +# most already pointed at modules moved in the Stage-5 reorg +# (ohbm2026.enrichment, ohbm2026.openalex). Their functions remain +# importable via `abstractatlas.*` and runnable as `aacli` subcommands. [build-system] requires = ["setuptools>=69"] diff --git a/scripts/build_category_evaluation_rollup.py b/scripts/build_category_evaluation_rollup.py index 28b5cddd..2dd70010 100644 --- a/scripts/build_category_evaluation_rollup.py +++ b/scripts/build_category_evaluation_rollup.py @@ -1,6 +1,6 @@ from __future__ import annotations -from ohbm2026.analyze.category_rollup import main +from abstractatlas.analyze.category_rollup import main if __name__ == "__main__": diff --git a/scripts/build_ui_data.py b/scripts/build_ui_data.py index 66ab68bb..b52b5309 100755 --- a/scripts/build_ui_data.py +++ b/scripts/build_ui_data.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """Stage 6 — build the UI data package (T019). -Thin CLI wrapper around :func:`ohbm2026.ui_data.builder.build_ui_data_package`. +Thin CLI wrapper around :func:`abstractatlas.ui_data.builder.build_ui_data_package`. See specs/008-ui-rewrite/quickstart.md for the canonical invocations. """ @@ -118,7 +118,7 @@ def _parse_args(argv: list[str] | None = None) -> argparse.Namespace: def main(argv: list[str] | None = None) -> int: args = _parse_args(argv) - from ohbm2026.ui_data.builder import Stage6BuildError, build_ui_data_package + from abstractatlas.ui_data.builder import Stage6BuildError, build_ui_data_package try: return build_ui_data_package( diff --git a/scripts/cluster_projection_silhouette.py b/scripts/cluster_projection_silhouette.py index 5e67bc0c..c0402958 100644 --- a/scripts/cluster_projection_silhouette.py +++ b/scripts/cluster_projection_silhouette.py @@ -8,11 +8,11 @@ import numpy as np from sklearn.metrics import silhouette_score -from ohbm2026.analyze.projections import ( +from abstractatlas.analyze.projections import ( DEFAULT_UMAP_MIN_DIST, DEFAULT_UMAP_NEIGHBORS, ) -from ohbm2026.analyze.storage import ( +from abstractatlas.analyze.storage import ( NeuroScapeError, load_embedding_bundle, ) diff --git a/scripts/derive_neuroscape_centroids.py b/scripts/derive_neuroscape_centroids.py index f41261bc..e544fbe7 100755 --- a/scripts/derive_neuroscape_centroids.py +++ b/scripts/derive_neuroscape_centroids.py @@ -195,7 +195,7 @@ def _iter_h5_vectors(shard: Path) -> "Iterable[tuple[str, np.ndarray]]": def main(argv: list[str] | None = None) -> int: import numpy as np - from ohbm2026.analyze.centroids import spherical_mean + from abstractatlas.analyze.centroids import spherical_mean parser = argparse.ArgumentParser( prog="derive_neuroscape_centroids", diff --git a/scripts/distill_dimensions.py b/scripts/distill_dimensions.py index 16ba4160..457838b1 100644 --- a/scripts/distill_dimensions.py +++ b/scripts/distill_dimensions.py @@ -33,7 +33,7 @@ def _parse_args(argv: list[str] | None = None) -> argparse.Namespace: def main(argv: list[str] | None = None) -> int: args = _parse_args(argv) - from ohbm2026.ui_data.dimensions import DimensionInputError, distill_dimensions + from abstractatlas.ui_data.dimensions import DimensionInputError, distill_dimensions try: summary = distill_dimensions(args.in_path, args.out_path) diff --git a/scripts/evaluate_label_systems.py b/scripts/evaluate_label_systems.py index 346644ab..9b5feb99 100644 --- a/scripts/evaluate_label_systems.py +++ b/scripts/evaluate_label_systems.py @@ -1,6 +1,6 @@ from __future__ import annotations -from ohbm2026.analyze.category_evaluation import main +from abstractatlas.analyze.category_evaluation import main if __name__ == "__main__": diff --git a/scripts/fetch_ui_inputs.sh b/scripts/fetch_ui_inputs.sh index f4710048..889301ab 100755 --- a/scripts/fetch_ui_inputs.sh +++ b/scripts/fetch_ui_inputs.sh @@ -21,7 +21,7 @@ # the build-info-only placeholder. # # Per CA-007, no state-key is hardcoded; the rollup state-key is discovered -# inside `build_ui_data.py` via `ohbm2026.ui_data.state_key`. +# inside `build_ui_data.py` via `abstractatlas.ui_data.state_key`. set -euo pipefail diff --git a/scripts/layout/analyze_poster_layout.py b/scripts/layout/analyze_poster_layout.py index 646b17ff..c549ef65 100644 --- a/scripts/layout/analyze_poster_layout.py +++ b/scripts/layout/analyze_poster_layout.py @@ -1,6 +1,6 @@ from __future__ import annotations -from ohbm2026.layout.poster_layout import analyze_main +from abstractatlas.layout.poster_layout import analyze_main if __name__ == "__main__": diff --git a/scripts/layout/benchmark_poster_sequencing.py b/scripts/layout/benchmark_poster_sequencing.py index b909bf10..d176225b 100644 --- a/scripts/layout/benchmark_poster_sequencing.py +++ b/scripts/layout/benchmark_poster_sequencing.py @@ -3,8 +3,8 @@ import argparse from pathlib import Path -from ohbm2026.layout.poster_layout import DEFAULT_CLAIMS_CLUSTER_ASSIGNMENTS, DEFAULT_CLAIMS_CLUSTER_SUMMARIES, load_layout_inputs -from ohbm2026.layout.poster_sequencing import benchmark_from_files +from abstractatlas.layout.poster_layout import DEFAULT_CLAIMS_CLUSTER_ASSIGNMENTS, DEFAULT_CLAIMS_CLUSTER_SUMMARIES, load_layout_inputs +from abstractatlas.layout.poster_sequencing import benchmark_from_files def build_parser() -> argparse.ArgumentParser: diff --git a/scripts/layout/extract_layout_geometry.py b/scripts/layout/extract_layout_geometry.py index d0beba17..33120c0c 100644 --- a/scripts/layout/extract_layout_geometry.py +++ b/scripts/layout/extract_layout_geometry.py @@ -4,7 +4,7 @@ import json from pathlib import Path -from ohbm2026.layout.poster_layout import _default_layout_geometry_payload +from abstractatlas.layout.poster_layout import _default_layout_geometry_payload def build_parser() -> argparse.ArgumentParser: diff --git a/scripts/layout/generate_advanced_global_path_proposals.py b/scripts/layout/generate_advanced_global_path_proposals.py index 05fb8d4c..0700098e 100644 --- a/scripts/layout/generate_advanced_global_path_proposals.py +++ b/scripts/layout/generate_advanced_global_path_proposals.py @@ -7,8 +7,8 @@ from pathlib import Path from typing import Any -from ohbm2026.layout.poster_layout import analyze_layout_proposal, load_layout_inputs, write_json, write_layout_csv, write_listing_csv -from ohbm2026.layout.poster_sequencing import ( +from abstractatlas.layout.poster_layout import analyze_layout_proposal, load_layout_inputs, write_json, write_layout_csv, write_listing_csv +from abstractatlas.layout.poster_sequencing import ( build_global_path_split_proposal, derive_contiguous_layout_clusters, graph_reordering_metrics, diff --git a/scripts/layout/generate_semantic_layout_proposals.py b/scripts/layout/generate_semantic_layout_proposals.py index da221875..7ad452ed 100644 --- a/scripts/layout/generate_semantic_layout_proposals.py +++ b/scripts/layout/generate_semantic_layout_proposals.py @@ -4,7 +4,7 @@ from dataclasses import dataclass from pathlib import Path -from ohbm2026.layout.poster_layout import ( +from abstractatlas.layout.poster_layout import ( OptimizationWeights, analyze_layout_proposal, build_layout_proposal, diff --git a/scripts/layout/generate_semantic_path_proposals.py b/scripts/layout/generate_semantic_path_proposals.py index ee44d064..25e39dcd 100644 --- a/scripts/layout/generate_semantic_path_proposals.py +++ b/scripts/layout/generate_semantic_path_proposals.py @@ -3,7 +3,7 @@ import argparse from pathlib import Path -from ohbm2026.layout.poster_layout import ( +from abstractatlas.layout.poster_layout import ( PathProposalConfig, analyze_layout_proposal, build_semantic_path_proposal, diff --git a/scripts/layout/generate_target_poster_layout_proposals.py b/scripts/layout/generate_target_poster_layout_proposals.py index 7fb61c39..8d763d8f 100644 --- a/scripts/layout/generate_target_poster_layout_proposals.py +++ b/scripts/layout/generate_target_poster_layout_proposals.py @@ -9,7 +9,7 @@ import numpy as np -from ohbm2026.layout.poster_layout import ( +from abstractatlas.layout.poster_layout import ( analyze_layout_proposal, build_global_numeric_order, build_layout_proposal, @@ -19,7 +19,7 @@ write_layout_csv, write_listing_csv, ) -from ohbm2026.layout.poster_sequencing import build_global_path_split_proposal +from abstractatlas.layout.poster_sequencing import build_global_path_split_proposal @dataclass(frozen=True) diff --git a/scripts/layout/optimize_poster_layout.py b/scripts/layout/optimize_poster_layout.py index 3ca5fe9b..ac49d2bb 100644 --- a/scripts/layout/optimize_poster_layout.py +++ b/scripts/layout/optimize_poster_layout.py @@ -1,6 +1,6 @@ from __future__ import annotations -from ohbm2026.layout.poster_layout import optimize_main +from abstractatlas.layout.poster_layout import optimize_main if __name__ == "__main__": diff --git a/scripts/layout/plot_poster_layout_floorplan.py b/scripts/layout/plot_poster_layout_floorplan.py index cb10da82..36cf1a58 100644 --- a/scripts/layout/plot_poster_layout_floorplan.py +++ b/scripts/layout/plot_poster_layout_floorplan.py @@ -10,8 +10,8 @@ import plotly.graph_objects as go from plotly.offline.offline import get_plotlyjs -from ohbm2026.analyze.storage import build_distinct_color_map -from ohbm2026.layout.poster_layout import layout_slot_for_block_position, load_layout_geometry +from abstractatlas.analyze.storage import build_distinct_color_map +from abstractatlas.layout.poster_layout import layout_slot_for_block_position, load_layout_geometry DEFAULT_UI_UMAP = "export/ui-site/data/projection.umap.json" VOYAGE25_ASSIGNMENTS = Path("data/embeddings/voyage_stage2_published/clustering_benchmark/cluster_assignments.json") diff --git a/scripts/layout/run_advanced_global_path_experiment.py b/scripts/layout/run_advanced_global_path_experiment.py index 59d023cc..f7efc26b 100644 --- a/scripts/layout/run_advanced_global_path_experiment.py +++ b/scripts/layout/run_advanced_global_path_experiment.py @@ -3,8 +3,8 @@ import argparse from pathlib import Path -from ohbm2026.layout.poster_layout import DEFAULT_CLAIMS_CLUSTER_ASSIGNMENTS, DEFAULT_CLAIMS_CLUSTER_SUMMARIES, load_layout_inputs -from ohbm2026.layout.poster_sequencing import run_advanced_global_path_experiment_from_files +from abstractatlas.layout.poster_layout import DEFAULT_CLAIMS_CLUSTER_ASSIGNMENTS, DEFAULT_CLAIMS_CLUSTER_SUMMARIES, load_layout_inputs +from abstractatlas.layout.poster_sequencing import run_advanced_global_path_experiment_from_files def build_parser() -> argparse.ArgumentParser: diff --git a/scripts/layout/run_nocd_checkpoint_sweep_experiment.py b/scripts/layout/run_nocd_checkpoint_sweep_experiment.py index 090c26a5..1bd7d818 100644 --- a/scripts/layout/run_nocd_checkpoint_sweep_experiment.py +++ b/scripts/layout/run_nocd_checkpoint_sweep_experiment.py @@ -4,8 +4,8 @@ import json from pathlib import Path -from ohbm2026 import artifacts -from ohbm2026.layout.nocd_experiments import ( +from abstractatlas import artifacts +from abstractatlas.layout.nocd_experiments import ( annotate_community_structure_scores, checkpoint_sweep_summary_markdown, discover_checkpoint_configs, diff --git a/scripts/layout/run_nocd_classic_predict_experiment.py b/scripts/layout/run_nocd_classic_predict_experiment.py index c82b0c91..24f8bd40 100644 --- a/scripts/layout/run_nocd_classic_predict_experiment.py +++ b/scripts/layout/run_nocd_classic_predict_experiment.py @@ -4,8 +4,8 @@ import json from pathlib import Path -from ohbm2026 import artifacts -from ohbm2026.layout.nocd_experiments import ( +from abstractatlas import artifacts +from abstractatlas.layout.nocd_experiments import ( annotate_community_structure_scores, classic_summary_markdown, discover_checkpoint_configs, diff --git a/scripts/layout/sweep_diffusion_variants.py b/scripts/layout/sweep_diffusion_variants.py index a2f3aa7e..17db7250 100644 --- a/scripts/layout/sweep_diffusion_variants.py +++ b/scripts/layout/sweep_diffusion_variants.py @@ -3,8 +3,8 @@ import argparse from pathlib import Path -from ohbm2026.layout.poster_layout import DEFAULT_CLAIMS_CLUSTER_ASSIGNMENTS, DEFAULT_CLAIMS_CLUSTER_SUMMARIES, load_layout_inputs -from ohbm2026.layout.poster_sequencing import sweep_diffusion_from_files +from abstractatlas.layout.poster_layout import DEFAULT_CLAIMS_CLUSTER_ASSIGNMENTS, DEFAULT_CLAIMS_CLUSTER_SUMMARIES, load_layout_inputs +from abstractatlas.layout.poster_sequencing import sweep_diffusion_from_files def _parse_int_list(value: str) -> tuple[int, ...]: diff --git a/scripts/layout/sweep_global_path_mapalign_variants.py b/scripts/layout/sweep_global_path_mapalign_variants.py index a7a35d52..60ff2c77 100644 --- a/scripts/layout/sweep_global_path_mapalign_variants.py +++ b/scripts/layout/sweep_global_path_mapalign_variants.py @@ -3,8 +3,8 @@ import argparse from pathlib import Path -from ohbm2026.layout.poster_layout import DEFAULT_CLAIMS_CLUSTER_ASSIGNMENTS, DEFAULT_CLAIMS_CLUSTER_SUMMARIES, load_layout_inputs -from ohbm2026.layout.poster_sequencing import sweep_global_path_mapalign_from_files +from abstractatlas.layout.poster_layout import DEFAULT_CLAIMS_CLUSTER_ASSIGNMENTS, DEFAULT_CLAIMS_CLUSTER_SUMMARIES, load_layout_inputs +from abstractatlas.layout.poster_sequencing import sweep_global_path_mapalign_from_files def _parse_int_list(value: str) -> tuple[int, ...]: diff --git a/scripts/layout/sweep_global_path_variants.py b/scripts/layout/sweep_global_path_variants.py index 7489da5a..e1206d78 100644 --- a/scripts/layout/sweep_global_path_variants.py +++ b/scripts/layout/sweep_global_path_variants.py @@ -3,8 +3,8 @@ import argparse from pathlib import Path -from ohbm2026.layout.poster_layout import DEFAULT_CLAIMS_CLUSTER_ASSIGNMENTS, DEFAULT_CLAIMS_CLUSTER_SUMMARIES, load_layout_inputs -from ohbm2026.layout.poster_sequencing import sweep_global_path_diffusion_from_files +from abstractatlas.layout.poster_layout import DEFAULT_CLAIMS_CLUSTER_ASSIGNMENTS, DEFAULT_CLAIMS_CLUSTER_SUMMARIES, load_layout_inputs +from abstractatlas.layout.poster_sequencing import sweep_global_path_diffusion_from_files def _parse_int_list(value: str) -> tuple[int, ...]: diff --git a/scripts/layout/verify_proposed_listings.py b/scripts/layout/verify_proposed_listings.py index 3539bb37..f302c529 100644 --- a/scripts/layout/verify_proposed_listings.py +++ b/scripts/layout/verify_proposed_listings.py @@ -7,8 +7,8 @@ from pathlib import Path from typing import Any -from ohbm2026.layout.poster_layout import LISTING_TEMPLATE_COLUMNS -from ohbm2026.titles import cleaned_abstract_title +from abstractatlas.layout.poster_layout import LISTING_TEMPLATE_COLUMNS +from abstractatlas.titles import cleaned_abstract_title def load_json(path: Path) -> dict[str, Any]: diff --git a/scripts/openai_api_smoke.py b/scripts/openai_api_smoke.py index c18e6d56..ae012681 100644 --- a/scripts/openai_api_smoke.py +++ b/scripts/openai_api_smoke.py @@ -8,7 +8,7 @@ from openai import OpenAI from pydantic import BaseModel -from ohbm2026.fetch.graphql_api import load_dotenv +from abstractatlas.fetch.graphql_api import load_dotenv class SmokeResponse(BaseModel): diff --git a/scripts/openai_batch_smoke.py b/scripts/openai_batch_smoke.py index b0432fe4..5adcde1c 100644 --- a/scripts/openai_batch_smoke.py +++ b/scripts/openai_batch_smoke.py @@ -8,8 +8,8 @@ from openai import OpenAI -from ohbm2026.fetch.graphql_api import load_dotenv -from ohbm2026.enrich.openalex import ( +from abstractatlas.fetch.graphql_api import load_dotenv +from abstractatlas.enrich.openalex import ( reference_split_response_schema, reference_split_system_prompt, ) diff --git a/scripts/plot_voyage_stage2_umap_3d.py b/scripts/plot_voyage_stage2_umap_3d.py index cd597620..847207b7 100644 --- a/scripts/plot_voyage_stage2_umap_3d.py +++ b/scripts/plot_voyage_stage2_umap_3d.py @@ -11,7 +11,7 @@ import plotly.io as pio import umap -from ohbm2026.analyze.storage import build_distinct_color_map +from abstractatlas.analyze.storage import build_distinct_color_map def load_dataset(path: Path) -> dict[str, Any]: diff --git a/scripts/reference_split_regression_probe.py b/scripts/reference_split_regression_probe.py index 0d5f0dc6..e2b593ff 100644 --- a/scripts/reference_split_regression_probe.py +++ b/scripts/reference_split_regression_probe.py @@ -6,8 +6,8 @@ from pathlib import Path from typing import Any -from ohbm2026.enrich.text import html_to_markdown -from ohbm2026.enrich.openalex import ( +from abstractatlas.enrich.text import html_to_markdown +from abstractatlas.enrich.openalex import ( extract_reference_entries_heuristic, llm_reference_split_request, normalize_reference_match_text, diff --git a/scripts/run_analyze_matrix.py b/scripts/run_analyze_matrix.py index 1b3fb5a2..4385136d 100755 --- a/scripts/run_analyze_matrix.py +++ b/scripts/run_analyze_matrix.py @@ -3,7 +3,7 @@ Mirror of `scripts/run_embed_matrix.py`. Operators run this directly when they want the canonical Stage 4 matrix without going through -`ohbmcli`; downstream code paths are identical. +`aacli`; downstream code paths are identical. """ from __future__ import annotations @@ -16,7 +16,7 @@ if str(SRC) not in sys.path: sys.path.insert(0, str(SRC)) -from ohbm2026.analyze.stage import main # noqa: E402 +from abstractatlas.analyze.stage import main # noqa: E402 if __name__ == "__main__": diff --git a/scripts/run_build_atlas_package.py b/scripts/run_build_atlas_package.py index cba74b0a..7e080d1e 100755 --- a/scripts/run_build_atlas_package.py +++ b/scripts/run_build_atlas_package.py @@ -8,9 +8,9 @@ PYTHONPATH=src .venv/bin/python scripts/run_build_atlas_package.py [options] This wrapper exists so the README's Stage 15 section has a single -copy-pasteable invocation that does not depend on the ``ohbmcli`` +copy-pasteable invocation that does not depend on the ``aacli`` entry point's installation state. It forwards ``sys.argv[1:]`` to -:func:`ohbm2026.atlas_package.cli.main` and returns its exit code. +:func:`abstractatlas.atlas_package.cli.main` and returns its exit code. All flags + exit codes are documented in ``specs/015-neuroscape-context/contracts/cli-build-atlas-package.md`` @@ -27,7 +27,7 @@ if _SRC_DIR.exists() and str(_SRC_DIR) not in sys.path: sys.path.insert(0, str(_SRC_DIR)) -from ohbm2026.atlas_package.cli import main # noqa: E402 (post sys.path setup) +from abstractatlas.atlas_package.cli import main # noqa: E402 (post sys.path setup) if __name__ == "__main__": diff --git a/scripts/run_build_book.py b/scripts/run_build_book.py index 1f58deab..5ad1e15a 100755 --- a/scripts/run_build_book.py +++ b/scripts/run_build_book.py @@ -1,8 +1,8 @@ #!/usr/bin/env python -"""Thin shim for ad-hoc dev runs of `ohbmcli book`. +"""Thin shim for ad-hoc dev runs of `aacli book`. -Re-exports `ohbm2026.book.cli:main` so contributors can iterate -without the `ohbmcli` dispatch indirection. Matches the pattern of +Re-exports `abstractatlas.book.cli:main` so contributors can iterate +without the `aacli` dispatch indirection. Matches the pattern of `scripts/run_enrich_abstracts.py`. Usage (from repo root): @@ -15,7 +15,7 @@ import sys -from ohbm2026.book.cli import main +from abstractatlas.book.cli import main if __name__ == "__main__": # pragma: no cover diff --git a/scripts/run_embed_matrix.py b/scripts/run_embed_matrix.py index ce750422..ad8ec18d 100755 --- a/scripts/run_embed_matrix.py +++ b/scripts/run_embed_matrix.py @@ -5,7 +5,7 @@ PYTHONPATH=src .venv/bin/python scripts/run_embed_matrix.py [options] -Forwards ``sys.argv[1:]`` to ``ohbm2026.embed.stage.main`` and returns +Forwards ``sys.argv[1:]`` to ``abstractatlas.embed.stage.main`` and returns its exit code. Mirrors ``scripts/run_enrich_abstracts.py``. """ @@ -19,7 +19,7 @@ if _SRC_DIR.exists() and str(_SRC_DIR) not in sys.path: sys.path.insert(0, str(_SRC_DIR)) -from ohbm2026.embed.stage import main # noqa: E402 (post sys.path setup) +from abstractatlas.embed.stage import main # noqa: E402 (post sys.path setup) if __name__ == "__main__": diff --git a/scripts/run_enrich_abstracts.py b/scripts/run_enrich_abstracts.py index e05ba497..9b6302fe 100755 --- a/scripts/run_enrich_abstracts.py +++ b/scripts/run_enrich_abstracts.py @@ -7,9 +7,9 @@ PYTHONPATH=src .venv/bin/python scripts/run_enrich_abstracts.py [options] This wrapper exists so the README's Stage 2 section has a single -copy-pasteable invocation that does not depend on the `ohbmcli` entry +copy-pasteable invocation that does not depend on the `aacli` entry point's installation state. It forwards ``sys.argv[1:]`` to -``ohbm2026.enrich.stage.main`` and returns its exit code. +``abstractatlas.enrich.stage.main`` and returns its exit code. All flags + exit codes are documented in ``specs/003-enrich-abstracts/contracts/cli.md`` and exercised by @@ -26,7 +26,7 @@ if _SRC_DIR.exists() and str(_SRC_DIR) not in sys.path: sys.path.insert(0, str(_SRC_DIR)) -from ohbm2026.enrich.stage import main # noqa: E402 (post sys.path setup) +from abstractatlas.enrich.stage import main # noqa: E402 (post sys.path setup) if __name__ == "__main__": diff --git a/scripts/run_fetch_abstracts.py b/scripts/run_fetch_abstracts.py index 380b2af2..4baf4690 100755 --- a/scripts/run_fetch_abstracts.py +++ b/scripts/run_fetch_abstracts.py @@ -7,9 +7,9 @@ PYTHONPATH=src .venv/bin/python scripts/run_fetch_abstracts.py [options] This wrapper exists so the README's Stage 1 section has a single -copy-pasteable invocation that does not depend on the `ohbmcli` entry +copy-pasteable invocation that does not depend on the `aacli` entry point's installation state. It forwards ``sys.argv[1:]`` to -``ohbm2026.fetch.stage.main`` and returns its exit code. +``abstractatlas.fetch.stage.main`` and returns its exit code. All flags + exit codes are documented in ``specs/002-rewire-pipeline/contracts/cli.md`` and exercised by @@ -29,7 +29,7 @@ if _SRC_DIR.exists() and str(_SRC_DIR) not in sys.path: sys.path.insert(0, str(_SRC_DIR)) -from ohbm2026.fetch.stage import main # noqa: E402 (post sys.path setup) +from abstractatlas.fetch.stage import main # noqa: E402 (post sys.path setup) if __name__ == "__main__": diff --git a/scripts/run_fetch_withdrawn.py b/scripts/run_fetch_withdrawn.py index 4b11494a..149b64d5 100755 --- a/scripts/run_fetch_withdrawn.py +++ b/scripts/run_fetch_withdrawn.py @@ -7,9 +7,9 @@ PYTHONPATH=src .venv/bin/python scripts/run_fetch_withdrawn.py [options] -Equivalent through ``ohbmcli``: +Equivalent through ``aacli``: - PYTHONPATH=src .venv/bin/python -m ohbm2026.cli fetch-withdrawn [options] + PYTHONPATH=src .venv/bin/python -m abstractatlas.cli fetch-withdrawn [options] This wrapper forces ``--corpus-kind=withdrawn`` so accepted and withdrawn artifacts never share a state-key namespace. @@ -25,7 +25,7 @@ if _SRC_DIR.exists() and str(_SRC_DIR) not in sys.path: sys.path.insert(0, str(_SRC_DIR)) -from ohbm2026.fetch.stage import main # noqa: E402 (post sys.path setup) +from abstractatlas.fetch.stage import main # noqa: E402 (post sys.path setup) if __name__ == "__main__": diff --git a/scripts/run_gmm_overlap_experiment.py b/scripts/run_gmm_overlap_experiment.py index 29501dcc..b746bf2a 100644 --- a/scripts/run_gmm_overlap_experiment.py +++ b/scripts/run_gmm_overlap_experiment.py @@ -9,8 +9,8 @@ import numpy as np from sklearn.mixture import GaussianMixture -from ohbm2026.analyze.clusters import prepare_clustering_matrix -from ohbm2026.analyze.storage import load_embedding_bundle +from abstractatlas.analyze.clusters import prepare_clustering_matrix +from abstractatlas.analyze.storage import load_embedding_bundle def build_parser() -> argparse.ArgumentParser: diff --git a/scripts/validate_compose_recipe_baseline.py b/scripts/validate_compose_recipe_baseline.py index 14acb221..b4bdb753 100644 --- a/scripts/validate_compose_recipe_baseline.py +++ b/scripts/validate_compose_recipe_baseline.py @@ -38,10 +38,10 @@ import numpy as np -from ohbm2026.embed.compose import compose_recipe -from ohbm2026.embed.components import assemble_component -from ohbm2026.embed.hf import DEFAULT_MINILM_MODEL, DEFAULT_PUBMEDBERT_MODEL, HFBatchClient -from ohbm2026.embed.voyage import DEFAULT_VOYAGE_MODEL, VoyageBatchClient +from abstractatlas.embed.compose import compose_recipe +from abstractatlas.embed.components import assemble_component +from abstractatlas.embed.hf import DEFAULT_MINILM_MODEL, DEFAULT_PUBMEDBERT_MODEL, HFBatchClient +from abstractatlas.embed.voyage import DEFAULT_VOYAGE_MODEL, VoyageBatchClient SAMPLE_SIZE = 50 diff --git a/scripts/write_topic_group_report.py b/scripts/write_topic_group_report.py index 389ebceb..11bdf5df 100644 --- a/scripts/write_topic_group_report.py +++ b/scripts/write_topic_group_report.py @@ -5,13 +5,13 @@ from pathlib import Path from typing import Any -from ohbm2026 import artifacts -from ohbm2026.analyze.clusters import ( +from abstractatlas import artifacts +from abstractatlas.analyze.clusters import ( align_semantic_records, load_enriched_lookup, summarize_membership_groups, ) -from ohbm2026.analyze.storage import ( +from abstractatlas.analyze.storage import ( load_annotation_lookup, load_embedding_bundle, load_title_lookup, diff --git a/src/abstractatlas/analyze/__init__.py b/src/abstractatlas/analyze/__init__.py index 820f4f7b..402207c6 100644 --- a/src/abstractatlas/analyze/__init__.py +++ b/src/abstractatlas/analyze/__init__.py @@ -3,7 +3,7 @@ The flat `analyze.py` module was split into per-concern submodules: - `analyze.errors` — typed `AnalysisError` hierarchy (re-exported from - `ohbm2026.exceptions`). + `abstractatlas.exceptions`). - `analyze.provenance` — Stage 4 path-safe provenance writers. - `analyze.storage` — I/O helpers, bundle readers/writers, text-shaping utilities, and `write_analysis_bundle` for Stage 4's per-kind output. @@ -27,8 +27,8 @@ **No package-level re-exports.** Per spec clarification Session 2026-05-15 Q2, every caller imports from the explicit submodule that -owns the symbol — `from ohbm2026.analyze.storage import …`, -`from ohbm2026.analyze.clusters import …`, etc. The `runners` import +owns the symbol — `from abstractatlas.analyze.storage import …`, +`from abstractatlas.analyze.clusters import …`, etc. The `runners` import below is a side-effect-only import that registers per-kind runners with `analyze.stage.KIND_RUNNERS`; nothing else lives at the package top level. @@ -39,16 +39,16 @@ # Warm-up imports to break a pre-existing # `exceptions ↔ fetch.graphql_api ↔ fetch.stage ↔ exceptions` cycle. # When the first entry into the `analyze` package comes from a -# submodule that imports from `ohbm2026.exceptions` (e.g. test code -# that does `from ohbm2026.analyze.centroids import ...`), exceptions +# submodule that imports from `abstractatlas.exceptions` (e.g. test code +# that does `from abstractatlas.analyze.centroids import ...`), exceptions # would otherwise be loaded partially. Loading fetch + exceptions # here first warms the chain so downstream submodule imports see a # fully-initialized exceptions module. -from ohbm2026 import fetch as _fetch_warmup # noqa: F401 -from ohbm2026 import exceptions as _exceptions_warmup # noqa: F401 +from abstractatlas import fetch as _fetch_warmup # noqa: F401 +from abstractatlas import exceptions as _exceptions_warmup # noqa: F401 # Side-effect import: registers projections / communities / # neuroscape_clusters / topic_clusters runners with # `analyze.stage.KIND_RUNNERS`. Without this, the orchestrator's # dispatch can't find any runners. -from ohbm2026.analyze import runners # noqa: F401 +from abstractatlas.analyze import runners # noqa: F401 diff --git a/src/abstractatlas/analyze/category_evaluation.py b/src/abstractatlas/analyze/category_evaluation.py index 8e28be0d..e9e64246 100644 --- a/src/abstractatlas/analyze/category_evaluation.py +++ b/src/abstractatlas/analyze/category_evaluation.py @@ -11,16 +11,16 @@ import numpy as np -from ohbm2026 import artifacts -from ohbm2026.analyze.clusters import ( +from abstractatlas import artifacts +from abstractatlas.analyze.clusters import ( build_knn_graph, compute_clustering_metrics, ) -from ohbm2026.analyze.storage import ( +from abstractatlas.analyze.storage import ( load_embedding_bundle, parse_string_list_value, ) -from ohbm2026.util.json_io import write_json +from abstractatlas.util.json_io import write_json class CategoryEvaluationError(Exception): diff --git a/src/abstractatlas/analyze/category_rollup.py b/src/abstractatlas/analyze/category_rollup.py index adda5d78..49f7d411 100644 --- a/src/abstractatlas/analyze/category_rollup.py +++ b/src/abstractatlas/analyze/category_rollup.py @@ -6,8 +6,8 @@ from pathlib import Path from typing import Any -from ohbm2026 import artifacts -from ohbm2026.util.json_io import write_json +from abstractatlas import artifacts +from abstractatlas.util.json_io import write_json ROLLUP_BANDS = ("coarse", "mid", "fine") DEFAULT_TOP_CANDIDATES = 3 diff --git a/src/abstractatlas/analyze/centroids.py b/src/abstractatlas/analyze/centroids.py index a8e5f816..fea7b24c 100644 --- a/src/abstractatlas/analyze/centroids.py +++ b/src/abstractatlas/analyze/centroids.py @@ -34,7 +34,7 @@ import numpy as np -from ohbm2026.exceptions import ( +from abstractatlas.exceptions import ( AnalysisError, CentroidTableMissing, CentroidTableVersionMismatch, @@ -352,7 +352,7 @@ def write_neuroscape_clusters_bundle( The bundle does NOT ship a `topics.json` — labels join from `cluster_table.csv` via the rollup writer. """ - from ohbm2026.analyze.storage import write_analysis_bundle + from abstractatlas.analyze.storage import write_analysis_bundle if ids.shape[0] != cluster_ids.shape[0] or ids.shape[0] != distances.shape[0]: raise ValueError( diff --git a/src/abstractatlas/analyze/clusters.py b/src/abstractatlas/analyze/clusters.py index d21e7038..ed26345d 100644 --- a/src/abstractatlas/analyze/clusters.py +++ b/src/abstractatlas/analyze/clusters.py @@ -40,8 +40,8 @@ import numpy as np -from ohbm2026 import artifacts -from ohbm2026.analyze.storage import ( +from abstractatlas import artifacts +from abstractatlas.analyze.storage import ( ALLOWED_EMBEDDING_FIELDS, DEFAULT_EMBEDDING_FIELDS, NeuroScapeError, @@ -65,7 +65,7 @@ write_embedding_bundle, write_json, ) -from ohbm2026.titles import cleaned_abstract_title +from abstractatlas.titles import cleaned_abstract_title def _normalize_rows(matrix: Any) -> Any: diff --git a/src/abstractatlas/analyze/communities.py b/src/abstractatlas/analyze/communities.py index 21d0ef7b..e1fd2bb2 100644 --- a/src/abstractatlas/analyze/communities.py +++ b/src/abstractatlas/analyze/communities.py @@ -33,7 +33,7 @@ import numpy as np -from ohbm2026.exceptions import AnalysisError, CommunityResolutionDegenerate +from abstractatlas.exceptions import AnalysisError, CommunityResolutionDegenerate __all__ = [ @@ -453,7 +453,7 @@ def write_communities_bundle( left as `None` for the topics-skipping path (`--skip-llm-topics` without the local fallback wired yet). """ - from ohbm2026.analyze.storage import write_analysis_bundle + from abstractatlas.analyze.storage import write_analysis_bundle if ids.shape[0] != result.community_ids.shape[0]: raise ValueError( diff --git a/src/abstractatlas/analyze/errors.py b/src/abstractatlas/analyze/errors.py index d4b4a122..6e4496da 100644 --- a/src/abstractatlas/analyze/errors.py +++ b/src/abstractatlas/analyze/errors.py @@ -1,15 +1,15 @@ """Stage 4 typed exception hierarchy — re-export surface. -The class definitions live in `ohbm2026.exceptions` next to the other +The class definitions live in `abstractatlas.exceptions` next to the other per-stage error hierarchies (`Stage1Error`, `Stage2Error`, `Stage3Error`) so the project-wide import surface stays uniform. This module -re-exports them so Stage 4 callers can `from ohbm2026.analyze.errors +re-exports them so Stage 4 callers can `from abstractatlas.analyze.errors import AnalysisError` instead of reaching into `exceptions`. """ from __future__ import annotations -from ohbm2026.exceptions import ( +from abstractatlas.exceptions import ( AnalysisError, CentroidTableMissing, CentroidTableVersionMismatch, diff --git a/src/abstractatlas/analyze/llm_adapter.py b/src/abstractatlas/analyze/llm_adapter.py index a13a9ec7..d3fbeee0 100644 --- a/src/abstractatlas/analyze/llm_adapter.py +++ b/src/abstractatlas/analyze/llm_adapter.py @@ -12,7 +12,7 @@ import os from typing import Callable -from ohbm2026.exceptions import AnalysisError +from abstractatlas.exceptions import AnalysisError LLMCaller = Callable[[str, str], str] @@ -42,7 +42,7 @@ def build_topics_llm_adapter( "or rerun with `--skip-llm-topics`." ) from exc - from ohbm2026.enrich.flex_tier import call_with_flex_fallback + from abstractatlas.enrich.flex_tier import call_with_flex_fallback def adapter(prompt: str, model_id: str) -> str: if not os.environ.get("OPENAI_API_KEY"): diff --git a/src/abstractatlas/analyze/projections.py b/src/abstractatlas/analyze/projections.py index 3e8bc4c4..9013eb12 100644 --- a/src/abstractatlas/analyze/projections.py +++ b/src/abstractatlas/analyze/projections.py @@ -33,12 +33,12 @@ import numpy as np -from ohbm2026 import artifacts -from ohbm2026.analyze.clusters import ( +from abstractatlas import artifacts +from abstractatlas.analyze.clusters import ( _cluster_distance_metrics, detect_semantic_communities, ) -from ohbm2026.analyze.storage import ( +from abstractatlas.analyze.storage import ( DEFAULT_EMBEDDING_FIELDS, NeuroScapeError, build_distinct_color_map, diff --git a/src/abstractatlas/analyze/provenance.py b/src/abstractatlas/analyze/provenance.py index fbed9caf..956acbad 100644 --- a/src/abstractatlas/analyze/provenance.py +++ b/src/abstractatlas/analyze/provenance.py @@ -19,7 +19,7 @@ from pathlib import Path from typing import Iterable -from ohbm2026.exceptions import ProvenanceError +from abstractatlas.exceptions import ProvenanceError __all__ = [ "PROVENANCE_SCHEMA_VERSION", diff --git a/src/abstractatlas/analyze/rollup.py b/src/abstractatlas/analyze/rollup.py index 681e634c..8bd44fad 100644 --- a/src/abstractatlas/analyze/rollup.py +++ b/src/abstractatlas/analyze/rollup.py @@ -33,7 +33,7 @@ import numpy as np -from ohbm2026.analyze.storage import iter_analysis_bundles +from abstractatlas.analyze.storage import iter_analysis_bundles __all__ = [ "RollupRow", diff --git a/src/abstractatlas/analyze/runners.py b/src/abstractatlas/analyze/runners.py index 0762fad7..07e358c0 100644 --- a/src/abstractatlas/analyze/runners.py +++ b/src/abstractatlas/analyze/runners.py @@ -1,7 +1,7 @@ """Per-kind runner registrations. This module is imported for its side-effect of populating -`KIND_RUNNERS` in `ohbm2026.analyze.stage`. Each runner: +`KIND_RUNNERS` in `abstractatlas.analyze.stage`. Each runner: 1. Reads the Stage 3 input vectors for `(model, input_source)` via `embed.compose.compose_recipe` (for the `abstract` recipe) or the per-component bundle. @@ -23,12 +23,12 @@ import numpy as np -from ohbm2026.analyze.centroids import ( +from abstractatlas.analyze.centroids import ( assign_nearest_centroid, load_centroid_table, write_neuroscape_clusters_bundle, ) -from ohbm2026.analyze.communities import ( +from abstractatlas.analyze.communities import ( DEFAULT_KNN_K, DEFAULT_RESOLUTION_MAX, DEFAULT_RESOLUTION_MIN, @@ -36,24 +36,24 @@ detect_communities, write_communities_bundle, ) -from ohbm2026.analyze.topic_clusters import ( +from abstractatlas.analyze.topic_clusters import ( run_topic_clustering, write_topic_clusters_bundle, ) -from ohbm2026.analyze.topics import ( +from abstractatlas.analyze.topics import ( DEFAULT_LLM_MODEL_ID, DEFAULT_PROMPT_VERSION, build_topics_artifact, ) -from ohbm2026.analyze.provenance import write_bundle_provenance -from ohbm2026.analyze.stage import ( +from abstractatlas.analyze.provenance import write_bundle_provenance +from abstractatlas.analyze.stage import ( AnalysisConfig, PlanEntry, _bundle_output_path, _kind_state_key, register_kind_runner, ) -from ohbm2026.analyze.umap import ( +from abstractatlas.analyze.umap import ( DEFAULT_UMAP_METRIC, DEFAULT_UMAP_MIN_DIST, DEFAULT_UMAP_N_NEIGHBORS, @@ -63,7 +63,7 @@ fit_umap_3d, write_projections_bundle, ) -from ohbm2026.exceptions import AnalysisError +from abstractatlas.exceptions import AnalysisError __all__ = [ @@ -92,7 +92,7 @@ def _load_input_matrix( if entry.input_source == "abstract": from hashlib import sha256 - from ohbm2026.embed.compose import compose_recipe + from abstractatlas.embed.compose import compose_recipe result = compose_recipe( ["title", "introduction", "methods", "results", "conclusion"], @@ -334,7 +334,7 @@ def neuroscape_clusters_runner( centroid_sha = centroid_table.domain_model_checkpoint_sha256 stage3_sha = _read_stage3_neuroscape_checkpoint_sha(config, entry) if centroid_sha and stage3_sha and centroid_sha != stage3_sha: - from ohbm2026.exceptions import CentroidTableVersionMismatch + from abstractatlas.exceptions import CentroidTableVersionMismatch raise CentroidTableVersionMismatch( f"NeuroScape Stage-2 checkpoint mismatch: centroid metadata " @@ -476,7 +476,7 @@ def communities_runner(config: AnalysisConfig, entry: PlanEntry) -> dict[str, An # Build the per-cluster topics artifact (FR-009 hybrid pipeline). topics_payload: dict[int, dict[str, Any]] = {} if result.n_communities > 0: - from ohbm2026.analyze.llm_adapter import build_topics_llm_adapter + from abstractatlas.analyze.llm_adapter import build_topics_llm_adapter abstract_texts = load_abstract_texts(ids) llm_call = ( @@ -564,7 +564,7 @@ def load_abstract_texts( # Try enriched SQLite first if enriched_path.exists(): try: - from ohbm2026.enrich.storage import iter_enriched + from abstractatlas.enrich.storage import iter_enriched for record in iter_enriched(enriched_path): aid = record.get("id") @@ -648,7 +648,7 @@ def topic_clusters_runner( # Build the per-cluster topics artifact (spaCy + c-TF-IDF + optional LLM). topics_payload: dict[int, dict[str, Any]] = {} if result.n_topics > 0: - from ohbm2026.analyze.llm_adapter import build_topics_llm_adapter + from abstractatlas.analyze.llm_adapter import build_topics_llm_adapter abstract_texts = load_abstract_texts(ids) # Exclude HDBSCAN noise (cluster_id == -1) from the topics map. diff --git a/src/abstractatlas/analyze/stage.py b/src/abstractatlas/analyze/stage.py index df5a6ad2..c9df7d91 100644 --- a/src/abstractatlas/analyze/stage.py +++ b/src/abstractatlas/analyze/stage.py @@ -40,15 +40,15 @@ import numpy as np -from ohbm2026.analyze.provenance import ( +from abstractatlas.analyze.provenance import ( write_bundle_provenance, write_run_provenance, ) -from ohbm2026.analyze.rollup import ( +from abstractatlas.analyze.rollup import ( load_neuroscape_cluster_table, write_rollup, ) -from ohbm2026.exceptions import AnalysisError, InputBundleMissing +from abstractatlas.exceptions import AnalysisError, InputBundleMissing __all__ = [ "DEFAULT_MODELS", diff --git a/src/abstractatlas/analyze/storage.py b/src/abstractatlas/analyze/storage.py index 2c34368b..9ed80c5b 100644 --- a/src/abstractatlas/analyze/storage.py +++ b/src/abstractatlas/analyze/storage.py @@ -40,9 +40,9 @@ import numpy as np -from ohbm2026.fetch.graphql_api import chunked, load_dotenv -from ohbm2026.titles import cleaned_abstract_title -from ohbm2026.util.json_io import write_json +from abstractatlas.fetch.graphql_api import chunked, load_dotenv +from abstractatlas.titles import cleaned_abstract_title +from abstractatlas.util.json_io import write_json DEFAULT_VOYAGE_MODEL = "voyage-large-2-instruct" DEFAULT_MINILM_MODEL = "sentence-transformers/all-MiniLM-L6-v2" diff --git a/src/abstractatlas/analyze/topic_clusters.py b/src/abstractatlas/analyze/topic_clusters.py index b2568680..f0f00c1e 100644 --- a/src/abstractatlas/analyze/topic_clusters.py +++ b/src/abstractatlas/analyze/topic_clusters.py @@ -20,7 +20,7 @@ import numpy as np -from ohbm2026.exceptions import AnalysisError +from abstractatlas.exceptions import AnalysisError __all__ = [ @@ -190,7 +190,7 @@ def write_topic_clusters_bundle( metadata_extra: dict[str, Any] | None = None, ) -> Path: """Write a `topic_clusters` bundle per `contracts/bundle.md`.""" - from ohbm2026.analyze.storage import write_analysis_bundle + from abstractatlas.analyze.storage import write_analysis_bundle if ids.shape[0] != result.topic_cluster_ids.shape[0]: raise ValueError( diff --git a/src/abstractatlas/analyze/topics.py b/src/abstractatlas/analyze/topics.py index 6c59ae71..af536302 100644 --- a/src/abstractatlas/analyze/topics.py +++ b/src/abstractatlas/analyze/topics.py @@ -36,7 +36,7 @@ from pathlib import Path from typing import Any, Callable, Iterable -from ohbm2026.exceptions import AnalysisError, TopicGroupingHallucination +from abstractatlas.exceptions import AnalysisError, TopicGroupingHallucination __all__ = [ diff --git a/src/abstractatlas/analyze/umap.py b/src/abstractatlas/analyze/umap.py index 1a5b489f..e14372eb 100644 --- a/src/abstractatlas/analyze/umap.py +++ b/src/abstractatlas/analyze/umap.py @@ -30,7 +30,7 @@ import numpy as np -from ohbm2026.exceptions import ( +from abstractatlas.exceptions import ( AnalysisError, ProjectionDimensionMismatch, UnsupportedProjectionAlgorithm, diff --git a/src/abstractatlas/assets.py b/src/abstractatlas/assets.py index 5ad8ea47..f89c9925 100644 --- a/src/abstractatlas/assets.py +++ b/src/abstractatlas/assets.py @@ -14,8 +14,8 @@ from urllib.parse import urlparse from urllib.request import Request -from ohbm2026 import artifacts -from ohbm2026.fetch.graphql_api import ( +from abstractatlas import artifacts +from abstractatlas.fetch.graphql_api import ( DEFAULT_TIMEOUT_LIMIT_SECONDS, DEFAULT_TIMEOUT_START_SECONDS, GraphQLAPIError, @@ -135,7 +135,7 @@ def is_target_figure_question(question_name: str | None) -> bool: def extract_external_urls(values: list[str]) -> list[str]: - from ohbm2026.fetch.graphql_api import extract_external_urls as _extract + from abstractatlas.fetch.graphql_api import extract_external_urls as _extract return _extract(values, URL_PATTERN) diff --git a/src/abstractatlas/atlas_hosting/cli.py b/src/abstractatlas/atlas_hosting/cli.py index 67abc34d..3eed7c4f 100644 --- a/src/abstractatlas/atlas_hosting/cli.py +++ b/src/abstractatlas/atlas_hosting/cli.py @@ -17,8 +17,8 @@ import sys from pathlib import Path -from ohbm2026.artifacts import utc_now_isoformat -from ohbm2026.exceptions import ( +from abstractatlas.artifacts import utc_now_isoformat +from abstractatlas.exceptions import ( ArtifactDiscoveryError, ContentHashMismatchError, DataHostingCacheError, diff --git a/src/abstractatlas/atlas_hosting/compare.py b/src/abstractatlas/atlas_hosting/compare.py index f50d60da..a222c56e 100644 --- a/src/abstractatlas/atlas_hosting/compare.py +++ b/src/abstractatlas/atlas_hosting/compare.py @@ -19,7 +19,7 @@ from dataclasses import dataclass from typing import Callable, Optional -from ohbm2026.exceptions import HostingComparisonError +from abstractatlas.exceptions import HostingComparisonError SCHEMA_VERSION = "data_hosting_comparison.v1" _ORDER = ("ohbm2026", "neuroscape", "atlas", "neuroscape_vectors") diff --git a/src/abstractatlas/atlas_hosting/manifest.py b/src/abstractatlas/atlas_hosting/manifest.py index 30b64ddb..1fd6165a 100644 --- a/src/abstractatlas/atlas_hosting/manifest.py +++ b/src/abstractatlas/atlas_hosting/manifest.py @@ -8,7 +8,7 @@ records the run's code revision / command line / timestamp. It carries NO secret (only bucket name, public base URL, content hashes, and public URLs) and NO absolute or user-home path — ``source_package_dir`` -is forced through :func:`ohbm2026.atlas_package.provenance.normalise_path`. +is forced through :func:`abstractatlas.atlas_package.provenance.normalise_path`. """ from __future__ import annotations @@ -18,8 +18,8 @@ from pathlib import Path from typing import Optional -from ohbm2026.artifacts import _stable_hash -from ohbm2026.atlas_package.provenance import normalise_path +from abstractatlas.artifacts import _stable_hash +from abstractatlas.atlas_package.provenance import normalise_path SCHEMA_VERSION = "atlas_upload_manifest.v1" diff --git a/src/abstractatlas/atlas_hosting/r2_client.py b/src/abstractatlas/atlas_hosting/r2_client.py index 94597cff..4d8a589d 100644 --- a/src/abstractatlas/atlas_hosting/r2_client.py +++ b/src/abstractatlas/atlas_hosting/r2_client.py @@ -16,7 +16,7 @@ from pathlib import Path from typing import Optional, Tuple, Union -from ohbm2026.exceptions import R2CredentialsError, R2UploadError +from abstractatlas.exceptions import R2CredentialsError, R2UploadError PathLike = Union[str, Path] diff --git a/src/abstractatlas/atlas_hosting/uploader.py b/src/abstractatlas/atlas_hosting/uploader.py index 4aa8e566..09d6c20b 100644 --- a/src/abstractatlas/atlas_hosting/uploader.py +++ b/src/abstractatlas/atlas_hosting/uploader.py @@ -21,8 +21,8 @@ from pathlib import Path from typing import Optional, Protocol, Tuple -from ohbm2026.artifacts import utc_now_isoformat -from ohbm2026.exceptions import ArtifactDiscoveryError, ContentHashMismatchError +from abstractatlas.artifacts import utc_now_isoformat +from abstractatlas.exceptions import ArtifactDiscoveryError, ContentHashMismatchError from .content_hash import derive_object_key, public_url, sha256_file from .manifest import ( diff --git a/src/abstractatlas/atlas_package/cli.py b/src/abstractatlas/atlas_package/cli.py index ae7013eb..fef729d7 100644 --- a/src/abstractatlas/atlas_package/cli.py +++ b/src/abstractatlas/atlas_package/cli.py @@ -17,7 +17,7 @@ corpus ``state_key``. 2. Reading the abstracts row group to recover ``(submission_id, poster_id, title)`` triples. -3. Calling :func:`ohbm2026.embed.compose.compose_recipe` with the +3. Calling :func:`abstractatlas.embed.compose.compose_recipe` with the five-component voyage_stage2_published recipe to land the per-abstract Stage-2 vectors. 4. Joining the two by ``submission_id``. @@ -37,7 +37,7 @@ import numpy as np -from ohbm2026.exceptions import ( +from abstractatlas.exceptions import ( AtlasLinkCheckError, AtlasProvenanceError, CrossParquetDriftError, @@ -367,7 +367,7 @@ def load_ohbm_corpus( submission_id from the published parquet, but the embedding bundles still key on it. - Stage-2 vectors come via - :func:`ohbm2026.embed.compose.compose_recipe` with the five- + :func:`abstractatlas.embed.compose.compose_recipe` with the five- component voyage_stage2_published recipe. Abstracts present in the corpus but missing a Stage-2 vector are @@ -377,7 +377,7 @@ def load_ohbm_corpus( *input* omission, not a *projection* failure). """ - from ohbm2026.embed.compose import compose_recipe + from abstractatlas.embed.compose import compose_recipe ohbm2026_state_key = read_ohbm2026_state_key(ohbm2026_parquet) headers = _read_ohbm_corpus_headers( diff --git a/src/abstractatlas/atlas_package/link_check.py b/src/abstractatlas/atlas_package/link_check.py index 600f7fa0..8016f24f 100644 --- a/src/abstractatlas/atlas_package/link_check.py +++ b/src/abstractatlas/atlas_package/link_check.py @@ -40,7 +40,7 @@ import requests -from ohbm2026.exceptions import AtlasLinkCheckError +from abstractatlas.exceptions import AtlasLinkCheckError __all__ = [ "DEFAULT_LINKS", diff --git a/src/abstractatlas/atlas_package/neighbour_index.py b/src/abstractatlas/atlas_package/neighbour_index.py index e3bc5c98..74947180 100644 --- a/src/abstractatlas/atlas_package/neighbour_index.py +++ b/src/abstractatlas/atlas_package/neighbour_index.py @@ -33,7 +33,7 @@ import numpy as np -from ohbm2026.exceptions import KnnCacheError +from abstractatlas.exceptions import KnnCacheError __all__ = [ "KnnResult", diff --git a/src/abstractatlas/atlas_package/neuroscape_loader.py b/src/abstractatlas/atlas_package/neuroscape_loader.py index 78c7b96e..c62dd86a 100644 --- a/src/abstractatlas/atlas_package/neuroscape_loader.py +++ b/src/abstractatlas/atlas_package/neuroscape_loader.py @@ -46,7 +46,7 @@ import h5py import numpy as np -from ohbm2026.exceptions import NeuroScapeInputError +from abstractatlas.exceptions import NeuroScapeInputError __all__ = [ "InputBundle", diff --git a/src/abstractatlas/atlas_package/ohbm_projector.py b/src/abstractatlas/atlas_package/ohbm_projector.py index af7130fe..a6ea64a1 100644 --- a/src/abstractatlas/atlas_package/ohbm_projector.py +++ b/src/abstractatlas/atlas_package/ohbm_projector.py @@ -3,7 +3,7 @@ Spec: ``specs/015-neuroscape-context/`` — research R-002 + R-009. The Stage-15 orchestrator fits UMAP once on the NeuroScape corpus -(:func:`ohbm2026.atlas_package.umap_fit.fit`), then lands the ~3K +(:func:`abstractatlas.atlas_package.umap_fit.fit`), then lands the ~3K OHBM 2026 abstracts into the same coordinate frame via this module. We **never** re-fit UMAP on the OHBM 2026 vectors alone (R-002): a union-fit would shift the NeuroScape points across rebuilds, and a @@ -30,7 +30,7 @@ import numpy as np -from ohbm2026.exceptions import OhbmProjectionError +from abstractatlas.exceptions import OhbmProjectionError from .umap_fit import UmapFitResult diff --git a/src/abstractatlas/atlas_package/orchestrator.py b/src/abstractatlas/atlas_package/orchestrator.py index 04178578..f6cb9457 100644 --- a/src/abstractatlas/atlas_package/orchestrator.py +++ b/src/abstractatlas/atlas_package/orchestrator.py @@ -40,7 +40,7 @@ import numpy as np -from ohbm2026.exceptions import NeuroScapeInputError +from abstractatlas.exceptions import NeuroScapeInputError from . import ( cluster_palette as palette_mod, diff --git a/src/abstractatlas/atlas_package/parquet_writer.py b/src/abstractatlas/atlas_package/parquet_writer.py index 1a593c93..951e1e2b 100644 --- a/src/abstractatlas/atlas_package/parquet_writer.py +++ b/src/abstractatlas/atlas_package/parquet_writer.py @@ -34,7 +34,7 @@ import pyarrow as pa import pyarrow.parquet as pq -from ohbm2026.exceptions import CrossParquetDriftError +from abstractatlas.exceptions import CrossParquetDriftError from .neighbour_index import KnnResult from .neuroscape_loader import ArticleHeader, NeuroScapeCluster diff --git a/src/abstractatlas/atlas_package/provenance.py b/src/abstractatlas/atlas_package/provenance.py index d5f0fe8e..7fc7e362 100644 --- a/src/abstractatlas/atlas_package/provenance.py +++ b/src/abstractatlas/atlas_package/provenance.py @@ -10,7 +10,7 @@ This module exposes ``normalise_path``, the gate used by every Stage 15 callsite that records a filesystem path into provenance. The function returns a repo-relative ``str`` on the happy path and raises -:class:`ohbm2026.exceptions.AtlasProvenanceError` with structured +:class:`abstractatlas.exceptions.AtlasProvenanceError` with structured kwargs on violation. Per Principle VI the rejection is loud — there is no silent fallback @@ -22,7 +22,7 @@ from pathlib import Path, PurePosixPath from typing import Union -from ohbm2026.exceptions import AtlasProvenanceError +from abstractatlas.exceptions import AtlasProvenanceError __all__ = ["normalise_path"] diff --git a/src/abstractatlas/atlas_package/semantic_index.py b/src/abstractatlas/atlas_package/semantic_index.py index 3824ad46..937fee57 100644 --- a/src/abstractatlas/atlas_package/semantic_index.py +++ b/src/abstractatlas/atlas_package/semantic_index.py @@ -41,7 +41,7 @@ import pyarrow as pa import pyarrow.parquet as pq -from ohbm2026.exceptions import VectorsParquetWriteError +from abstractatlas.exceptions import VectorsParquetWriteError __all__ = ["write_neuroscape_vectors_parquet", "ROW_GROUP_SIZE", "VECTOR_DIM"] diff --git a/src/abstractatlas/atlas_package/umap_fit.py b/src/abstractatlas/atlas_package/umap_fit.py index 26aa7486..5cd67e8b 100644 --- a/src/abstractatlas/atlas_package/umap_fit.py +++ b/src/abstractatlas/atlas_package/umap_fit.py @@ -53,7 +53,7 @@ import numpy as np -from ohbm2026.exceptions import UmapCacheError, UmapFitError +from abstractatlas.exceptions import UmapCacheError, UmapFitError __all__ = [ "UmapFitParams", @@ -108,7 +108,7 @@ class UmapFitResult: ``embedded`` is the (N, n_components) float32 projection. ``model`` is the fitted ``umap.UMAP`` instance — the OHBM projector - (:mod:`ohbm2026.atlas_package.ohbm_projector`, T024) uses + (:mod:`abstractatlas.atlas_package.ohbm_projector`, T024) uses ``model.transform`` to land out-of-sample vectors in the same space without re-fitting. """ @@ -327,7 +327,7 @@ def fit( _validate_input(vectors) - # Lazy import keeps ``ohbm2026.atlas_package`` lightweight; umap is + # Lazy import keeps ``abstractatlas.atlas_package`` lightweight; umap is # only needed inside fit/transform paths. import umap # type: ignore[import-untyped] diff --git a/src/abstractatlas/atlas_package/vectors_compute.py b/src/abstractatlas/atlas_package/vectors_compute.py index 352b998e..dd300d86 100644 --- a/src/abstractatlas/atlas_package/vectors_compute.py +++ b/src/abstractatlas/atlas_package/vectors_compute.py @@ -37,7 +37,7 @@ import numpy as np -from ohbm2026.exceptions import EmbeddingComputeError +from abstractatlas.exceptions import EmbeddingComputeError __all__ = [ "compute_state_key", @@ -270,7 +270,7 @@ def compute_cluster_vectors( # cluster needs recompute and we don't have a stable scale.json yet). if encoder is None: try: - from ohbm2026.embed.hf import HFBatchClient + from abstractatlas.embed.hf import HFBatchClient except ImportError as exc: raise EmbeddingComputeError( f"sentence-transformers / embed.hf not importable: {exc}", diff --git a/src/abstractatlas/book/__init__.py b/src/abstractatlas/book/__init__.py index 8ae1b8a3..e9f6acc6 100644 --- a/src/abstractatlas/book/__init__.py +++ b/src/abstractatlas/book/__init__.py @@ -8,7 +8,7 @@ Spec: `specs/011-abstracts-book/`. """ -from ohbm2026.book.model import ( +from abstractatlas.book.model import ( Author, AuthorAffiliation, AuthorIndexEntry, diff --git a/src/abstractatlas/book/assemble_pdf.py b/src/abstractatlas/book/assemble_pdf.py index c85956da..6e1ce3a5 100644 --- a/src/abstractatlas/book/assemble_pdf.py +++ b/src/abstractatlas/book/assemble_pdf.py @@ -33,13 +33,13 @@ import pikepdf -from ohbm2026.book.model import ( +from abstractatlas.book.model import ( AbstractPdfChunk, AssembledBook, AuthorIndexEntry, PerAbstractFailure, ) -from ohbm2026.exceptions import BookBuildError +from abstractatlas.exceptions import BookBuildError def assemble( diff --git a/src/abstractatlas/book/author_index.py b/src/abstractatlas/book/author_index.py index 59361d9f..38c45be6 100644 --- a/src/abstractatlas/book/author_index.py +++ b/src/abstractatlas/book/author_index.py @@ -8,7 +8,7 @@ from __future__ import annotations -from ohbm2026.book.model import AuthorIndexEntry, BookEntry +from abstractatlas.book.model import AuthorIndexEntry, BookEntry def build_author_index( diff --git a/src/abstractatlas/book/cli.py b/src/abstractatlas/book/cli.py index 80887a39..4aa4b64d 100644 --- a/src/abstractatlas/book/cli.py +++ b/src/abstractatlas/book/cli.py @@ -13,12 +13,12 @@ import sys from dataclasses import replace -from ohbm2026.book.author_index import build_author_index -from ohbm2026.book.corpus import load_book -from ohbm2026.book.provenance import write_provenance -from ohbm2026.book.render_markdown import emit_book_md -from ohbm2026.book.sort import STRATEGIES -from ohbm2026.exceptions import BookBuildError, ProvenanceError +from abstractatlas.book.author_index import build_author_index +from abstractatlas.book.corpus import load_book +from abstractatlas.book.provenance import write_provenance +from abstractatlas.book.render_markdown import emit_book_md +from abstractatlas.book.sort import STRATEGIES +from abstractatlas.exceptions import BookBuildError, ProvenanceError _VALID_FORMATS = ("md", "pdf", "all") _RETIRED_FORMATS = ("docx",) @@ -223,7 +223,7 @@ def main(argv: list[str] | None = None) -> int: pandoc_version = None xelatex_version = None if need_pdf: - from ohbm2026.book.render_via_pandoc import preflight + from abstractatlas.book.render_via_pandoc import preflight try: versions = preflight(need_xelatex=need_pdf) @@ -293,7 +293,7 @@ def main(argv: list[str] | None = None) -> int: # PDF (US1 + Stage 11.1). assembled = None if need_pdf: - from ohbm2026.book.render_via_pandoc import to_pdf + from abstractatlas.book.render_via_pandoc import to_pdf try: assembled = to_pdf( diff --git a/src/abstractatlas/book/corpus.py b/src/abstractatlas/book/corpus.py index 51c1b147..0ddc8e61 100644 --- a/src/abstractatlas/book/corpus.py +++ b/src/abstractatlas/book/corpus.py @@ -16,9 +16,9 @@ import sys from typing import Iterable, Mapping, Sequence -from ohbm2026.book.figure_check import probe_figure -from ohbm2026.book.html_to_md import html_to_pandoc_md -from ohbm2026.book.model import ( +from abstractatlas.book.figure_check import probe_figure +from abstractatlas.book.html_to_md import html_to_pandoc_md +from abstractatlas.book.model import ( Author, AuthorAffiliation, BodySection, @@ -29,9 +29,9 @@ StandbySlot, StandbyTimes, ) -from ohbm2026.book.sections import BODY_SECTION_NAMES -from ohbm2026.exceptions import BookBuildError -from ohbm2026 import standby as _standby +from abstractatlas.book.sections import BODY_SECTION_NAMES +from abstractatlas.exceptions import BookBuildError +from abstractatlas import standby as _standby _ACCEPTED_FOR = {"Poster", "Oral"} diff --git a/src/abstractatlas/book/provenance.py b/src/abstractatlas/book/provenance.py index d4f096a4..b0524047 100644 --- a/src/abstractatlas/book/provenance.py +++ b/src/abstractatlas/book/provenance.py @@ -15,12 +15,12 @@ import pathlib import subprocess -from ohbm2026.book.figure_check import ( +from abstractatlas.book.figure_check import ( PUBLICATION_DPI_THRESHOLD, effective_dpi, ) -from ohbm2026.book.model import Book -from ohbm2026.exceptions import ProvenanceError +from abstractatlas.book.model import Book +from abstractatlas.exceptions import ProvenanceError _PROVENANCE_VERSION = 1 @@ -216,7 +216,7 @@ def _rel(p: pathlib.Path) -> str: # Stage 12 US2 / FR-006 — figure-normalisation audit fields. # Read the module-level fallback registry from render_markdown # then reset it so the next build starts clean. - from ohbm2026.book.render_markdown import ( + from abstractatlas.book.render_markdown import ( get_normalise_fallbacks, reset_normalise_fallbacks, ) diff --git a/src/abstractatlas/book/render_markdown.py b/src/abstractatlas/book/render_markdown.py index 29fddaf7..7b996fcd 100644 --- a/src/abstractatlas/book/render_markdown.py +++ b/src/abstractatlas/book/render_markdown.py @@ -35,7 +35,7 @@ except ImportError: pass -from ohbm2026.book.model import ( +from abstractatlas.book.model import ( Author, AuthorIndexEntry, Book, @@ -166,7 +166,7 @@ def entry_to_md(entry: BookEntry) -> str: """Render one abstract section as markdown (Stage 11.1 public helper). Stable wrapper around `_entry_md` for the per-abstract PDF pipeline - in :mod:`ohbm2026.book.render_per_abstract`. Discards the figure- + in :mod:`abstractatlas.book.render_per_abstract`. Discards the figure- filename side-effect list — figures are still written by :func:`emit_book_md` for the markdown bundle; the per-abstract render reads them via pandoc's ``--resource-path``. @@ -179,7 +179,7 @@ def entry_to_md(entry: BookEntry) -> str: Tectonic time (the dominant Stage-11.1 failure cluster). """ - from ohbm2026.book.html_to_md import normalise_for_latex + from abstractatlas.book.html_to_md import normalise_for_latex return normalise_for_latex(_entry_md(entry, fig_filenames=[])) @@ -308,7 +308,7 @@ def _author_index_back_matter_md(index: tuple[AuthorIndexEntry, ...]) -> str: def _read_template() -> str: # importlib.resources keeps the templates packaged-with-the-code. - pkg = resources.files("ohbm2026.book.templates") + pkg = resources.files("abstractatlas.book.templates") return pkg.joinpath("book.md.template").read_text(encoding="utf-8") @@ -362,7 +362,7 @@ def emit_book_md( # itself injected (e.g. `→` in the anchor-link back matter) that # never went through `html_to_pandoc_md`. Idempotent on # already-converted spans. - from ohbm2026.book.html_to_md import normalise_for_latex + from abstractatlas.book.html_to_md import normalise_for_latex full = normalise_for_latex(full) (output_dir / "book.md").write_text(full, encoding="utf-8") diff --git a/src/abstractatlas/book/render_per_abstract.py b/src/abstractatlas/book/render_per_abstract.py index 7bbed18a..ba124af3 100644 --- a/src/abstractatlas/book/render_per_abstract.py +++ b/src/abstractatlas/book/render_per_abstract.py @@ -17,7 +17,7 @@ The CLI provides a separate `__main__` so an operator can re-render one abstract in isolation for debugging: - PYTHONPATH=src .venv/bin/python -m ohbm2026.book.render_per_abstract \\ + PYTHONPATH=src .venv/bin/python -m abstractatlas.book.render_per_abstract \\ --corpus data/primary/abstracts.json --poster-id 0042 --style plain """ @@ -36,21 +36,21 @@ import pikepdf -from ohbm2026.book.cache import ( +from abstractatlas.book.cache import ( cache_pdf_path, compute_cache_key, hash_header_includes, load_cached_pdf, store_cached_pdf_from_path, ) -from ohbm2026.book.model import AbstractPdfChunk, BookEntry -from ohbm2026.book.render_markdown import entry_to_md -from ohbm2026.exceptions import BookBuildError +from abstractatlas.book.model import AbstractPdfChunk, BookEntry +from abstractatlas.book.render_markdown import entry_to_md +from abstractatlas.exceptions import BookBuildError def per_abstract_header_path() -> pathlib.Path: """Path to the stripped-down per-abstract LaTeX preamble.""" - pkg = resources.files("ohbm2026.book.templates") + pkg = resources.files("abstractatlas.book.templates") return pathlib.Path(str(pkg.joinpath("per-abstract.tex.template"))) @@ -210,7 +210,7 @@ def _tail(text: str, *, max_bytes: int) -> str: def _debug_main(argv: list[str] | None = None) -> int: p = argparse.ArgumentParser( - prog="python -m ohbm2026.book.render_per_abstract", + prog="python -m abstractatlas.book.render_per_abstract", description=( "Render a single abstract in isolation, populating the " "per-abstract PDF cache. Useful for diagnosing a chunk " @@ -235,8 +235,8 @@ def _debug_main(argv: list[str] | None = None) -> int: ) args = p.parse_args(argv) - from ohbm2026.book.corpus import load_book - from ohbm2026.book.render_via_pandoc import preflight, resolve_pdf_engine + from abstractatlas.book.corpus import load_book + from abstractatlas.book.render_via_pandoc import preflight, resolve_pdf_engine try: versions = preflight(need_xelatex=True) diff --git a/src/abstractatlas/book/render_via_pandoc.py b/src/abstractatlas/book/render_via_pandoc.py index f4cff1c3..45a7dbec 100644 --- a/src/abstractatlas/book/render_via_pandoc.py +++ b/src/abstractatlas/book/render_via_pandoc.py @@ -23,7 +23,7 @@ import subprocess from importlib import resources -from ohbm2026.exceptions import BookBuildError +from abstractatlas.exceptions import BookBuildError def _which_or_raise(binary: str, hint: str) -> str: @@ -111,7 +111,7 @@ def _prewarm_tectonic_cache(pandoc_path: str, engine_binary: str) -> None: # Lazy import to avoid the circular dep render_via_pandoc <→ # render_per_abstract (both reference each other for the # production pipeline). - from ohbm2026.book.render_per_abstract import per_abstract_header_path + from abstractatlas.book.render_per_abstract import per_abstract_header_path # Minimal markdown that exercises the per-abstract preamble: # geometry + longtable + textsuperscript + math. @@ -162,7 +162,7 @@ def _header_includes_path(style: str, *, margins: str = "tight") -> pathlib.Path The Tufte style is independent of the margins flag — Tufte's sidenote layout has its own geometry contract. """ - pkg = resources.files("ohbm2026.book.templates") + pkg = resources.files("abstractatlas.book.templates") if style == "tufte": return pathlib.Path(str(pkg.joinpath("header-includes-tufte.tex"))) if margins == "loose": @@ -187,7 +187,7 @@ def to_pdf( Parameters ---------- book : - Loaded :class:`ohbm2026.book.model.Book`. The renderer iterates + Loaded :class:`abstractatlas.book.model.Book`. The renderer iterates ``book.entries`` and uses ``book.author_index`` for the appendix. output_dir : @@ -214,12 +214,12 @@ def to_pdf( cache hit/miss counts. The CLI hands this to the provenance writer. """ - from ohbm2026.book.assemble_pdf import assemble - from ohbm2026.book.model import ( + from abstractatlas.book.assemble_pdf import assemble + from abstractatlas.book.model import ( AbstractPdfChunk, PerAbstractFailure, ) - from ohbm2026.book.render_per_abstract import ( + from abstractatlas.book.render_per_abstract import ( per_abstract_header_path, render_one, ) @@ -334,7 +334,7 @@ def to_pdf( # If v2's page count drifts from v1 (rare, only when title widths # vary enough to change line wrap), warn + use v2 anyway; the # offsets stay correct because `assemble` re-measures via pikepdf. - from ohbm2026.book.assemble_pdf import _build_toc_markdown + from abstractatlas.book.assemble_pdf import _build_toc_markdown sorted_entries = tuple(c for c in surviving) # already in --sort order # Map of poster_id → BookEntry for the TOC's title column. @@ -477,14 +477,14 @@ def _render_front_matter( import pikepdf - from ohbm2026.book.cache import ( + from abstractatlas.book.cache import ( cache_pdf_path, compute_cache_key, hash_header_includes, load_cached_pdf, store_cached_pdf_from_path, ) - from ohbm2026.book.model import AbstractPdfChunk + from abstractatlas.book.model import AbstractPdfChunk header_hash = hash_header_includes(header_includes_path) key = compute_cache_key( diff --git a/src/abstractatlas/book/sort.py b/src/abstractatlas/book/sort.py index 51f193fe..3c9b5af3 100644 --- a/src/abstractatlas/book/sort.py +++ b/src/abstractatlas/book/sort.py @@ -9,7 +9,7 @@ from typing import Callable, Protocol -from ohbm2026.book.model import BookEntry +from abstractatlas.book.model import BookEntry class SortStrategy(Protocol): diff --git a/src/abstractatlas/cli.py b/src/abstractatlas/cli.py index 51a49a28..554f9bf5 100644 --- a/src/abstractatlas/cli.py +++ b/src/abstractatlas/cli.py @@ -3,20 +3,20 @@ import argparse import sys -from ohbm2026 import ( +from abstractatlas import ( artifacts, assets, titles, ) -from ohbm2026.analyze import clusters as analyze_clusters -from ohbm2026.analyze import projections as analyze_projections -from ohbm2026.analyze import stage as analyze_stage -from ohbm2026.embed import neuroscape as embed_neuroscape -from ohbm2026.enrich import stage as enrich_stage -from ohbm2026.fetch import stage as fetch_stage -from ohbm2026.embed import stage as embed_stage -from ohbm2026.atlas_package import cli as atlas_cli -from ohbm2026.atlas_hosting import cli as atlas_hosting_cli +from abstractatlas.analyze import clusters as analyze_clusters +from abstractatlas.analyze import projections as analyze_projections +from abstractatlas.analyze import stage as analyze_stage +from abstractatlas.embed import neuroscape as embed_neuroscape +from abstractatlas.enrich import stage as enrich_stage +from abstractatlas.fetch import stage as fetch_stage +from abstractatlas.embed import stage as embed_stage +from abstractatlas.atlas_package import cli as atlas_cli +from abstractatlas.atlas_hosting import cli as atlas_hosting_cli def _copy_actions(target: argparse.ArgumentParser, source: argparse.ArgumentParser) -> None: @@ -175,7 +175,7 @@ def build_parser() -> argparse.ArgumentParser: "book", help="Compose the Book of Abstracts (md + pdf + docx) from Stage-1 artefacts", ) - from ohbm2026.book.cli import _build_parser as _book_build_parser + from abstractatlas.book.cli import _build_parser as _book_build_parser _copy_actions(book_parser, _book_build_parser()) @@ -189,8 +189,8 @@ def _run_analyze_umap_project(argv: list[str]) -> int: import numpy as _np - from ohbm2026.analyze.umap import project_into_umap - from ohbm2026.exceptions import ( + from abstractatlas.analyze.umap import project_into_umap + from abstractatlas.exceptions import ( AnalysisError, ProjectionDimensionMismatch, UnsupportedProjectionAlgorithm, @@ -245,6 +245,20 @@ def _run_refresh_assets(argv: list[str]) -> int: return assets.main(argv) +def _ohbmcli_deprecated_main(argv: list[str] | None = None) -> int: + """Deprecated entry point: `ohbmcli` was renamed to `aacli` (spec 027). + + Warns loudly on stderr, then delegates to :func:`main` so existing + scripts keep working for one transition cycle. Remove next release. + FOLLOW-UP: drop the `ohbmcli` entry in pyproject.toml [project.scripts]. + """ + print( + "ohbmcli is deprecated and will be removed; use 'aacli'. Delegating…", + file=sys.stderr, + ) + return main(argv) + + def main(argv: list[str] | None = None) -> int: argv = list(sys.argv[1:] if argv is None else argv) parser = build_parser() @@ -298,7 +312,7 @@ def main(argv: list[str] | None = None) -> int: if command == "write-manifest": return embed_neuroscape.manifest_main(subcommand_argv) if command == "book": - from ohbm2026.book.cli import main as book_main + from abstractatlas.book.cli import main as book_main return book_main(subcommand_argv) diff --git a/src/abstractatlas/data/__init__.py b/src/abstractatlas/data/__init__.py index b441c770..4dd10a35 100644 --- a/src/abstractatlas/data/__init__.py +++ b/src/abstractatlas/data/__init__.py @@ -1,4 +1,4 @@ -"""Package data for ohbm2026. +"""Package data for abstractatlas. Holds committed-source data files (e.g. the ECO v1 controlled vocabulary) that pipeline components load at runtime without diff --git a/src/abstractatlas/data/__pycache__/__init__.cpython-314.pyc b/src/abstractatlas/data/__pycache__/__init__.cpython-314.pyc index 133fd5d4df985646e5c4b30fe463a2b0ca6a90f8..86c3d8d6050a3b298e40a52e4bde2768fe13c887 100644 GIT binary patch delta 79 zcmcc5^pc5Jn~#@^0SF>FJhC=V<kga26OfpkotU1gkdj!EsF0Rlq>z|YTvC*nT#{Il WlUO`4a1o>D#4p;sD58_q87%<P5*ok& delta 69 zcmaFKbf1Y=n~#@^0SLZFaAmEV$g3r_JRmVSJ25>~AtkXSQ6VkANFhHXDc8ur$ZTTR RB1V^qKeahv0+S6HEdbiS6?p&v diff --git a/src/abstractatlas/embed/__init__.py b/src/abstractatlas/embed/__init__.py index 14a7d16b..1485ad6e 100644 --- a/src/abstractatlas/embed/__init__.py +++ b/src/abstractatlas/embed/__init__.py @@ -1,15 +1,15 @@ """Stage 3 — multi-model per-component embeddings matrix. -Public surface re-exported here so callers can `from ohbm2026.embed +Public surface re-exported here so callers can `from abstractatlas.embed import compose_recipe, run_single_bundle, …` without dipping into submodules. """ -from ohbm2026.embed.compose import ( +from abstractatlas.embed.compose import ( apply_published_stage2_to_matrix, compose_recipe, ) -from ohbm2026.embed.components import ( +from abstractatlas.embed.components import ( ALL_COMPONENTS, DEFAULT_COMPONENTS, PARTIAL_COMPONENTS, @@ -17,7 +17,7 @@ assemble_all_components, assemble_component, ) -from ohbm2026.embed.stage import ( +from abstractatlas.embed.stage import ( DEFAULT_MODELS, BundleResult, build_clients, @@ -27,7 +27,7 @@ run_matrix, run_single_bundle, ) -from ohbm2026.embed.storage import ( +from abstractatlas.embed.storage import ( BUNDLE_SCHEMA_VERSION, CACHE_VERSION, atomic_write_bytes, diff --git a/src/abstractatlas/embed/components.py b/src/abstractatlas/embed/components.py index 36290d59..78bd7649 100644 --- a/src/abstractatlas/embed/components.py +++ b/src/abstractatlas/embed/components.py @@ -14,7 +14,7 @@ import re from typing import Any, Iterable -from ohbm2026.enrich.text import html_to_markdown +from abstractatlas.enrich.text import html_to_markdown __all__ = [ "DEFAULT_COMPONENTS", diff --git a/src/abstractatlas/embed/compose.py b/src/abstractatlas/embed/compose.py index b68c854c..000503e0 100644 --- a/src/abstractatlas/embed/compose.py +++ b/src/abstractatlas/embed/compose.py @@ -8,7 +8,7 @@ with a single-line change. Lives here (not in `neuroscape.py`) so the Stage 3 surface stays -self-contained: every Stage 3 caller imports from `ohbm2026.embed_*`. +self-contained: every Stage 3 caller imports from `abstractatlas.embed_*`. The NeuroScape Stage-2 application helper is also Stage-3 logic (operates on Voyage matrices to produce NeuroScape-projected matrices) and lives here for the same reason. @@ -25,7 +25,7 @@ from pathlib import Path from typing import Any -from ohbm2026.exceptions import EmbeddingError +from abstractatlas.exceptions import EmbeddingError __all__ = [ "compose_recipe", @@ -197,7 +197,7 @@ def apply_published_stage2_to_matrix( """ import numpy as np # Lazy import to avoid pulling in torch on import time of this module. - from ohbm2026.embed import neuroscape as ns_stage2 + from abstractatlas.embed import neuroscape as ns_stage2 if voyage_matrix.shape[1] != 1024: raise EmbeddingError( diff --git a/src/abstractatlas/embed/hf.py b/src/abstractatlas/embed/hf.py index a7b5802c..1f66aefe 100644 --- a/src/abstractatlas/embed/hf.py +++ b/src/abstractatlas/embed/hf.py @@ -15,7 +15,7 @@ import time from typing import Any, Iterable -from ohbm2026.exceptions import ( +from abstractatlas.exceptions import ( EmbeddingContractError, EmbeddingProviderError, ) diff --git a/src/abstractatlas/embed/neuroscape.py b/src/abstractatlas/embed/neuroscape.py index c067b6a2..2f15574a 100644 --- a/src/abstractatlas/embed/neuroscape.py +++ b/src/abstractatlas/embed/neuroscape.py @@ -36,8 +36,8 @@ import numpy as np -from ohbm2026 import artifacts -from ohbm2026.analyze.storage import ( +from abstractatlas import artifacts +from abstractatlas.analyze.storage import ( DEFAULT_EMBEDDING_FIELDS, NeuroScapeError, compute_neighbors, diff --git a/src/abstractatlas/embed/openai.py b/src/abstractatlas/embed/openai.py index ba868a0f..4dd0faef 100644 --- a/src/abstractatlas/embed/openai.py +++ b/src/abstractatlas/embed/openai.py @@ -10,7 +10,7 @@ import time from typing import Any -from ohbm2026.exceptions import ( +from abstractatlas.exceptions import ( EmbeddingBudgetError, EmbeddingContractError, EmbeddingProviderError, diff --git a/src/abstractatlas/embed/provenance.py b/src/abstractatlas/embed/provenance.py index e1c365b1..b5e0eb27 100644 --- a/src/abstractatlas/embed/provenance.py +++ b/src/abstractatlas/embed/provenance.py @@ -13,8 +13,8 @@ from pathlib import Path from typing import Any, Iterable -from ohbm2026.embed import storage as embed_storage -from ohbm2026.exceptions import ProvenanceError +from abstractatlas.embed import storage as embed_storage +from abstractatlas.exceptions import ProvenanceError __all__ = [ "PROVENANCE_SCHEMA_VERSION", diff --git a/src/abstractatlas/embed/stage.py b/src/abstractatlas/embed/stage.py index 31ef4f1d..f5c39bce 100644 --- a/src/abstractatlas/embed/stage.py +++ b/src/abstractatlas/embed/stage.py @@ -37,13 +37,13 @@ from pathlib import Path from typing import Any, Callable, Iterable, Sequence -from ohbm2026.embed import components as embed_components -from ohbm2026.embed import hf as embed_hf -from ohbm2026.embed import openai as embed_openai -from ohbm2026.embed import provenance as embed_provenance -from ohbm2026.embed import storage as embed_storage -from ohbm2026.embed import voyage as embed_voyage -from ohbm2026.exceptions import ( +from abstractatlas.embed import components as embed_components +from abstractatlas.embed import hf as embed_hf +from abstractatlas.embed import openai as embed_openai +from abstractatlas.embed import provenance as embed_provenance +from abstractatlas.embed import storage as embed_storage +from abstractatlas.embed import voyage as embed_voyage +from abstractatlas.exceptions import ( ComponentAssemblyError, EmbeddingBudgetError, EmbeddingContractError, @@ -888,7 +888,7 @@ def _run_neuroscape_derivation( f"is missing; run `--models voyage,neuroscape` together" ) # Lazy-import to avoid pulling in torch when only local-text models are used. - from ohbm2026.embed import compose as embed_compose + from abstractatlas.embed import compose as embed_compose start = time.perf_counter() output_dir = Path(embeddings_root) / "neuroscape" / f"{component}__{corpus_state_key}" bundle = embed_storage.load_bundle(voyage_bundle_dir) diff --git a/src/abstractatlas/embed/storage.py b/src/abstractatlas/embed/storage.py index bc118c3b..fc9d07e5 100644 --- a/src/abstractatlas/embed/storage.py +++ b/src/abstractatlas/embed/storage.py @@ -29,7 +29,7 @@ import numpy as np -from ohbm2026.exceptions import EmbeddingContractError +from abstractatlas.exceptions import EmbeddingContractError __all__ = [ "CACHE_VERSION", diff --git a/src/abstractatlas/embed/voyage.py b/src/abstractatlas/embed/voyage.py index e30c83b7..d54cbdac 100644 --- a/src/abstractatlas/embed/voyage.py +++ b/src/abstractatlas/embed/voyage.py @@ -19,7 +19,7 @@ import urllib.request from typing import Any -from ohbm2026.exceptions import ( +from abstractatlas.exceptions import ( EmbeddingBudgetError, EmbeddingContractError, EmbeddingProviderError, diff --git a/src/abstractatlas/enrich/claims.py b/src/abstractatlas/enrich/claims.py index 7c4231bc..6ecfe606 100644 --- a/src/abstractatlas/enrich/claims.py +++ b/src/abstractatlas/enrich/claims.py @@ -28,12 +28,12 @@ from pydantic import BaseModel, Field -from ohbm2026.enrich import flex_tier as flex_tier -from ohbm2026.enrich.markdown_render import ( +from abstractatlas.enrich import flex_tier as flex_tier +from abstractatlas.enrich.markdown_render import ( build_claim_manuscript_markdown, build_sections_markdown, ) -from ohbm2026.exceptions import ContextLengthExceededError, EnrichmentError +from abstractatlas.exceptions import ContextLengthExceededError, EnrichmentError __all__ = [ "Claim", @@ -101,7 +101,7 @@ def load_eco_vocabulary() -> dict[str, Any]: global _VOCABULARY_CACHE if _VOCABULARY_CACHE is not None: return _VOCABULARY_CACHE - raw = resources.files("ohbm2026.data").joinpath("eco_top_codes.json").read_text(encoding="utf-8") + raw = resources.files("abstractatlas.data").joinpath("eco_top_codes.json").read_text(encoding="utf-8") data = json.loads(raw) data["_id_set"] = frozenset(entry["eco_id"] for entry in data["codes"]) _VOCABULARY_CACHE = data diff --git a/src/abstractatlas/enrich/figures.py b/src/abstractatlas/enrich/figures.py index 3f1f537d..c02bbc0e 100644 --- a/src/abstractatlas/enrich/figures.py +++ b/src/abstractatlas/enrich/figures.py @@ -22,9 +22,9 @@ from pydantic import BaseModel, Field -from ohbm2026.enrich import flex_tier as flex_tier -from ohbm2026.enrich import image_quality as image_quality -from ohbm2026.exceptions import ContextLengthExceededError, EnrichmentError +from abstractatlas.enrich import flex_tier as flex_tier +from abstractatlas.enrich import image_quality as image_quality +from abstractatlas.exceptions import ContextLengthExceededError, EnrichmentError __all__ = [ "FigureInterpretationItem", diff --git a/src/abstractatlas/enrich/flex_tier.py b/src/abstractatlas/enrich/flex_tier.py index fccb9b3f..c337010a 100644 --- a/src/abstractatlas/enrich/flex_tier.py +++ b/src/abstractatlas/enrich/flex_tier.py @@ -22,7 +22,7 @@ import openai -from ohbm2026.exceptions import ContextLengthExceededError, EnrichmentError +from abstractatlas.exceptions import ContextLengthExceededError, EnrichmentError __all__ = [ "FlexTierResult", diff --git a/src/abstractatlas/enrich/markdown_render.py b/src/abstractatlas/enrich/markdown_render.py index 18e37993..323d3c2f 100644 --- a/src/abstractatlas/enrich/markdown_render.py +++ b/src/abstractatlas/enrich/markdown_render.py @@ -14,7 +14,7 @@ import json from typing import Any -from ohbm2026.enrich.text import html_to_markdown +from abstractatlas.enrich.text import html_to_markdown SECTION_ORDER: list[tuple[str, str]] = [ ("introduction", "Introduction"), diff --git a/src/abstractatlas/enrich/openalex.py b/src/abstractatlas/enrich/openalex.py index cfb56027..8e0347ea 100644 --- a/src/abstractatlas/enrich/openalex.py +++ b/src/abstractatlas/enrich/openalex.py @@ -20,10 +20,10 @@ from openai import APIStatusError, AsyncOpenAI, RateLimitError -from ohbm2026 import artifacts -from ohbm2026.enrich.text import html_to_markdown -from ohbm2026.fetch.graphql_api import load_dotenv, urlopen_with_retries -from ohbm2026.util.json_io import load_json, write_json +from abstractatlas import artifacts +from abstractatlas.enrich.text import html_to_markdown +from abstractatlas.fetch.graphql_api import load_dotenv, urlopen_with_retries +from abstractatlas.util.json_io import load_json, write_json OPENALEX_API = "https://api.openalex.org/works" OPENALEX_RATE_LIMIT_API = "https://api.openalex.org/rate-limit" diff --git a/src/abstractatlas/enrich/references.py b/src/abstractatlas/enrich/references.py index 0960dda2..12928ac0 100644 --- a/src/abstractatlas/enrich/references.py +++ b/src/abstractatlas/enrich/references.py @@ -14,7 +14,7 @@ import re from typing import Any, Literal -from ohbm2026.exceptions import EnrichmentError +from abstractatlas.exceptions import EnrichmentError __all__ = [ "ReferencesRunSummary", @@ -129,7 +129,7 @@ def run_references_component( # seam requires a separate spec round (Stage 2.2 candidate). # Operators can pass an explicit resolver via the # `resolver=` kwarg to plug richer resolution in. - from ohbm2026.enrich import openalex as _openalex + from abstractatlas.enrich import openalex as _openalex def _local_resolver(reference_text: str, strategy_id: str) -> list[dict]: lines = _split_references(reference_text) diff --git a/src/abstractatlas/enrich/stage.py b/src/abstractatlas/enrich/stage.py index eb7addad..c0e8566c 100644 --- a/src/abstractatlas/enrich/stage.py +++ b/src/abstractatlas/enrich/stage.py @@ -42,17 +42,17 @@ from pathlib import Path from typing import Any, Callable, Iterable, Iterator, Optional -from ohbm2026 import artifacts as artifacts_module -from ohbm2026.enrich import storage as enrich_storage -from ohbm2026.enrich.markdown_render import ( +from abstractatlas import artifacts as artifacts_module +from abstractatlas.enrich import storage as enrich_storage +from abstractatlas.enrich.markdown_render import ( build_claim_manuscript_markdown, build_sections_markdown, ) -from ohbm2026.enrich import openalex as openalex_module -from ohbm2026.enrich import figures as stage2_figures -from ohbm2026.enrich import claims as stage2_claims -from ohbm2026.enrich import references as stage2_references -from ohbm2026.exceptions import ( +from abstractatlas.enrich import openalex as openalex_module +from abstractatlas.enrich import figures as stage2_figures +from abstractatlas.enrich import claims as stage2_claims +from abstractatlas.enrich import references as stage2_references +from abstractatlas.exceptions import ( CacheVersionError, ComponentFailureThresholdError, EnrichmentError, diff --git a/src/abstractatlas/exceptions.py b/src/abstractatlas/exceptions.py index 8e291079..0d56dbd7 100644 --- a/src/abstractatlas/exceptions.py +++ b/src/abstractatlas/exceptions.py @@ -20,19 +20,19 @@ if TYPE_CHECKING: # Re-exported via module-level __getattr__ to avoid a circular # import: fetch/__init__.py loads fetch.stage, which itself imports - # `from ohbm2026.exceptions import ...`. Eager-importing - # `ohbm2026.fetch.graphql_api` at the top of THIS module triggers + # `from abstractatlas.exceptions import ...`. Eager-importing + # `abstractatlas.fetch.graphql_api` at the top of THIS module triggers # fetch/__init__.py before exceptions.py finishes loading, which - # explodes any direct `from ohbm2026.exceptions import X` outside + # explodes any direct `from abstractatlas.exceptions import X` outside # the cli/dispatch sequence (e.g. a new stage module like - # `ohbm2026.book.corpus`). Deferring to runtime resolution via + # `abstractatlas.book.corpus`). Deferring to runtime resolution via # __getattr__ keeps the public surface unchanged. - from ohbm2026.fetch.graphql_api import GraphQLAPIError # noqa: F401 + from abstractatlas.fetch.graphql_api import GraphQLAPIError # noqa: F401 def __getattr__(name: str): if name == "GraphQLAPIError": - from ohbm2026.fetch.graphql_api import GraphQLAPIError + from abstractatlas.fetch.graphql_api import GraphQLAPIError return GraphQLAPIError raise AttributeError(f"module {__name__!r} has no attribute {name!r}") @@ -156,7 +156,7 @@ class UIBuildError(RuntimeError): class Stage6Error(OhbmStageError): """Base class for Stage 6 (UI data-package build) failures. - Subclassed by :class:`ohbm2026.ui_data.state_key.Stage6BuildError` and + Subclassed by :class:`abstractatlas.ui_data.state_key.Stage6BuildError` and other Stage 6 callsites. Distinct from :class:`UIBuildError` (which covers the legacy ``ui.py`` export path) so callers can differentiate. """ @@ -345,7 +345,7 @@ class TopicGroupingHallucination(AnalysisError): class BookBuildError(OhbmStageError): """Stage 11 (book-of-abstracts) precondition failed or render aborted. - Raised by `ohbm2026.book` when the canonical corpus inputs are + Raised by `abstractatlas.book` when the canonical corpus inputs are missing/empty, the filtered entry set is empty, the output root is unwritable, a required system dep (`pandoc` / `xelatex`) is absent from PATH, or pandoc returns non-zero. `details` captures @@ -443,7 +443,7 @@ def __init__( class UmapCacheError(Stage15Error): """The UMAP fit on-disk cache is unreadable or inconsistent. - Raised by :func:`ohbm2026.atlas_package.umap_fit.fit` when a cache + Raised by :func:`abstractatlas.atlas_package.umap_fit.fit` when a cache entry exists at the expected ``<cache_root>/<state_key>/`` path but cannot be loaded (corrupted joblib, missing companion file, embedded-shape mismatch with the requested ``n_components``). The @@ -469,7 +469,7 @@ class KnnCacheError(Stage15Error): """The k-NN neighbour-index on-disk cache is unreadable or inconsistent. - Raised by :func:`ohbm2026.atlas_package.neighbour_index.build_knn` + Raised by :func:`abstractatlas.atlas_package.neighbour_index.build_knn` when a cache entry exists at the expected ``<cache_root>/<state_key>/`` path but cannot be loaded (missing companion array, unreadable ``.npy``, shape mismatch with the @@ -604,7 +604,7 @@ class Stage19SemanticError(Stage15Error): class EmbeddingComputeError(Stage19SemanticError): """The corpus-side MiniLM embedding compute failed. - Raised by ``ohbm2026.atlas_package.vectors_compute`` when + Raised by ``abstractatlas.atlas_package.vectors_compute`` when sentence-transformers fails to load the pinned model (``Xenova/all-MiniLM-L6-v2``), or inference produces a vector matrix with the wrong shape / non-finite values. Carries (``reason``, @@ -628,7 +628,7 @@ class VectorsParquetWriteError(Stage19SemanticError): """The semantic-vectors parquet writer detected an invariant violation before/during write. - Raised by ``ohbm2026.atlas_package.semantic_index.write_neuroscape_vectors_parquet`` + Raised by ``abstractatlas.atlas_package.semantic_index.write_neuroscape_vectors_parquet`` when the set of ``pubmed_id`` rows about to be written does not match the articles table on ``neuroscape.parquet`` (INV-003), or when the per-cluster row-group sort order would defeat predicate @@ -702,7 +702,7 @@ class Stage20Error(OhbmStageError): class R2CredentialsError(Stage20Error): """A required R2 S3 credential is missing or blank. - Raised by ``ohbm2026.atlas_hosting.r2_client`` before any network + Raised by ``abstractatlas.atlas_hosting.r2_client`` before any network call when one of ``R2_ACCOUNT_ID`` / ``R2_ACCESS_KEY_ID`` / ``R2_SECRET_ACCESS_KEY`` / ``R2_BUCKET`` / ``R2_PUBLIC_BASE_URL`` is absent from the environment and the ``.env`` file. Carries the @@ -718,7 +718,7 @@ def __init__(self, message: str, *, var: str | None = None) -> None: class R2UploadError(Stage20Error): """An R2 S3 operation (head/upload) failed. - Raised by ``ohbm2026.atlas_hosting.r2_client`` when a ``head_object`` + Raised by ``abstractatlas.atlas_hosting.r2_client`` when a ``head_object`` or ``upload_file`` call fails for any reason other than the expected 404-not-found (which is handled as "object absent"). Carries (``key``, ``bucket``, ``op``, ``reason``) so the failure is @@ -748,7 +748,7 @@ class ContentHashMismatchError(Stage20Error): A key of the form ``<sha256>/<filename>`` must only ever hold bytes whose sha256 equals ``<sha256>``. Raised by - ``ohbm2026.atlas_hosting.uploader`` when ``head_object`` reports a + ``abstractatlas.atlas_hosting.uploader`` when ``head_object`` reports a ``ContentLength`` that differs from the local file's size for the same key — a corruption / key-scheme violation that must fail loudly rather than overwrite. Carries (``key``, ``expected``, ``actual``). @@ -772,7 +772,7 @@ class ArtifactDiscoveryError(Stage20Error): """The built atlas-package directory does not match the expected artifact set. - Raised by ``ohbm2026.atlas_hosting.uploader`` when a required parquet + Raised by ``abstractatlas.atlas_hosting.uploader`` when a required parquet (``ohbm2026`` / ``neuroscape`` / ``atlas``) is missing from the package dir, or when an unexpected ``*.parquet`` outside the known set is present (no silent inclusion — Principle VII). Carries @@ -798,7 +798,7 @@ class HostingComparisonError(Stage20Error): Distinct from a *failed verdict* (a probe that ran and returned a negative result, which is RECORDED in the report, not raised — see - FR-015): this is raised by ``ohbm2026.atlas_hosting.compare`` only + FR-015): this is raised by ``abstractatlas.atlas_hosting.compare`` only when a probe cannot be performed at all — a malformed URL, or a channel missing a required artifact. Carries (``url``, ``probe``, ``reason``). diff --git a/src/abstractatlas/fetch/__init__.py b/src/abstractatlas/fetch/__init__.py index e08d1206..b2823b66 100644 --- a/src/abstractatlas/fetch/__init__.py +++ b/src/abstractatlas/fetch/__init__.py @@ -4,6 +4,6 @@ import path instead of dipping into submodules. """ -from ohbm2026.fetch import schema_diff, stage +from abstractatlas.fetch import schema_diff, stage __all__ = ["schema_diff", "stage"] diff --git a/src/abstractatlas/fetch/stage.py b/src/abstractatlas/fetch/stage.py index 011d90e7..44e21b81 100644 --- a/src/abstractatlas/fetch/stage.py +++ b/src/abstractatlas/fetch/stage.py @@ -37,10 +37,10 @@ from pathlib import Path from typing import Any -from ohbm2026 import artifacts, assets -from ohbm2026.fetch import graphql_api as _gql -from ohbm2026.fetch import schema_diff as _schema_diff -from ohbm2026.exceptions import ( +from abstractatlas import artifacts, assets +from abstractatlas.fetch import graphql_api as _gql +from abstractatlas.fetch import schema_diff as _schema_diff +from abstractatlas.exceptions import ( CheckpointError, FigureFailureError, GraphQLAPIError, @@ -472,15 +472,15 @@ def _classify_schema_drift( def _iter_consumer_modules() -> list[object]: - """Return modules under ``ohbm2026.*`` that declare + """Return modules under ``abstractatlas.*`` that declare ``CONSUMED_ABSTRACT_FIELDS``. Stage 1 v1 has none populated; the list is collected at runtime so later stages can opt in just by adding the attribute to their module.""" import importlib out: list[object] = [] for mod_name in ( - "ohbm2026.assets", - "ohbm2026.fetch.stage", + "abstractatlas.assets", + "abstractatlas.fetch.stage", ): try: mod = importlib.import_module(mod_name) diff --git a/src/abstractatlas/layout/poster_layout.py b/src/abstractatlas/layout/poster_layout.py index e6f46940..d2acfa98 100644 --- a/src/abstractatlas/layout/poster_layout.py +++ b/src/abstractatlas/layout/poster_layout.py @@ -13,12 +13,12 @@ import numpy as np -from ohbm2026 import artifacts -from ohbm2026.analyze.storage import ( +from abstractatlas import artifacts +from abstractatlas.analyze.storage import ( load_embedding_bundle, parse_string_list_value, ) -from ohbm2026.titles import cleaned_abstract_title +from abstractatlas.titles import cleaned_abstract_title SESSION_IDS = (1, 2, 3, 4) SESSION_TO_BLOCK = {1: 1, 2: 1, 3: 2, 4: 2} diff --git a/src/abstractatlas/layout/poster_sequencing.py b/src/abstractatlas/layout/poster_sequencing.py index 72ad5cfa..114d045a 100644 --- a/src/abstractatlas/layout/poster_sequencing.py +++ b/src/abstractatlas/layout/poster_sequencing.py @@ -8,7 +8,7 @@ from typing import Any import numpy as np -from ohbm2026 import artifacts +from abstractatlas import artifacts from scipy.cluster.hierarchy import leaves_list, linkage, optimal_leaf_ordering from scipy.sparse import csr_matrix from scipy.sparse.csgraph import connected_components, laplacian @@ -18,7 +18,7 @@ from sklearn.feature_extraction.text import ENGLISH_STOP_WORDS, TfidfVectorizer from sklearn.neighbors import NearestNeighbors -from ohbm2026.layout.poster_layout import ( +from abstractatlas.layout.poster_layout import ( BLOCK_TO_SESSIONS, BLOCK_LABELS, SESSION_LABELS, diff --git a/src/abstractatlas/titles.py b/src/abstractatlas/titles.py index d35fba65..542c859a 100644 --- a/src/abstractatlas/titles.py +++ b/src/abstractatlas/titles.py @@ -7,8 +7,8 @@ from pathlib import Path from typing import Any -from ohbm2026 import artifacts -from ohbm2026.util.json_io import load_json, write_json +from abstractatlas import artifacts +from abstractatlas.util.json_io import load_json, write_json DEFAULT_TITLE_MODIFICATIONS_OUTPUT = str(artifacts.TITLE_MODIFICATIONS_PATH) diff --git a/src/abstractatlas/ui_data/abstracts.py b/src/abstractatlas/ui_data/abstracts.py index 06404078..fa424758 100644 --- a/src/abstractatlas/ui_data/abstracts.py +++ b/src/abstractatlas/ui_data/abstracts.py @@ -14,9 +14,9 @@ from pathlib import Path from typing import Any -from ohbm2026.analyze.storage import parse_string_list_value -from ohbm2026.titles import cleaned_abstract_title -from ohbm2026.ui_data.questions import ( +from abstractatlas.analyze.storage import parse_string_list_value +from abstractatlas.titles import cleaned_abstract_title +from abstractatlas.ui_data.questions import ( PRIMARY_TOPIC_QUESTION, QUESTION_MAP, SECONDARY_TOPIC_QUESTION, @@ -26,8 +26,8 @@ topic_pair_from_questions, topic_subcategory, ) -from ohbm2026.ui_data.dimensions import DIMENSION_KEYS -from ohbm2026.ui_data.state_key import Stage6BuildError +from abstractatlas.ui_data.dimensions import DIMENSION_KEYS +from abstractatlas.ui_data.state_key import Stage6BuildError SCHEMA_VERSION = "abstracts.v1" @@ -109,7 +109,7 @@ def _prepare_math_for_ui(text: str) -> str: book pipeline's `$\\alpha$` wrap-per-glyph would split math regions in unhelpful ways for UI display). """ - from ohbm2026.book.html_to_md import ( + from abstractatlas.book.html_to_md import ( _autowrap_bare_math, _normalise_latex_aliases, _strip_control_chars, @@ -289,7 +289,7 @@ def _load_references_by_id(references_path: Path | None) -> dict[int, list[dict[ def _load_enriched_by_id(enriched_path: Path | None) -> dict[int, dict[str, Any]]: """Load enriched-abstract markdown blobs from the Stage 2 SQLite store. - The schema lives in ``ohbm2026.enrich.storage``; we read by abstract id. + The schema lives in ``abstractatlas.enrich.storage``; we read by abstract id. Returns an empty mapping when the file is absent so the manifest build still succeeds in the placeholder/skeleton case. """ @@ -297,7 +297,7 @@ def _load_enriched_by_id(enriched_path: Path | None) -> dict[int, dict[str, Any] if enriched_path is None or not Path(enriched_path).exists(): return {} try: - from ohbm2026.enrich.storage import iter_enriched + from abstractatlas.enrich.storage import iter_enriched except ImportError: return {} out: dict[int, dict[str, Any]] = {} @@ -458,7 +458,7 @@ def iter_abstracts( """Yield per-abstract rows (accepted-only, poster_id-keyed). Stage-10 entry point: the format-agnostic row stream every candidate - emitter under ``ohbm2026.ui_data.formats`` consumes. Rows match the + emitter under ``abstractatlas.ui_data.formats`` consumes. Rows match the ``AbstractRow`` shape in ``types.py``. Identical record contents to ``build_abstracts_records()`` — that wrapper is now ``list(iter_abstracts(...))``. @@ -478,7 +478,7 @@ def iter_abstracts( # the program-committee FINAL schedule; the old CSV was a draft. standby_by_sid = _load_standby_times(standby_times_path) if standby_final_csv_path is not None and abstract_to_poster is not None: - from ohbm2026.standby import key_by_submission_id, load_standby_csv + from abstractatlas.standby import key_by_submission_id, load_standby_csv poster_to_sub = {pid: sid for sid, pid in abstract_to_poster.items()} try: diff --git a/src/abstractatlas/ui_data/authors.py b/src/abstractatlas/ui_data/authors.py index 0153de84..0f2cc01c 100644 --- a/src/abstractatlas/ui_data/authors.py +++ b/src/abstractatlas/ui_data/authors.py @@ -12,7 +12,7 @@ from pathlib import Path from typing import Any -from ohbm2026.ui_data.state_key import Stage6BuildError +from abstractatlas.ui_data.state_key import Stage6BuildError SCHEMA_VERSION = "authors.v1" diff --git a/src/abstractatlas/ui_data/builder.py b/src/abstractatlas/ui_data/builder.py index 6c4ac513..59f75017 100644 --- a/src/abstractatlas/ui_data/builder.py +++ b/src/abstractatlas/ui_data/builder.py @@ -19,22 +19,22 @@ # CDN content hashes). Epoch 0 trips some tools; use 2026-01-01 instead. DETERMINISTIC_MTIME = 1767225600 # 2026-01-01T00:00:00Z -from ohbm2026.ui_data.abstracts import build_abstract_to_poster_map, build_abstracts -from ohbm2026.ui_data.authors import build_authors -from ohbm2026.ui_data.cells import build_cells -from ohbm2026.ui_data.dimensions import ( +from abstractatlas.ui_data.abstracts import build_abstract_to_poster_map, build_abstracts +from abstractatlas.ui_data.authors import build_authors +from abstractatlas.ui_data.cells import build_cells +from abstractatlas.ui_data.dimensions import ( compute_dimension_coverage, load_research_dimensions, ) -from ohbm2026.ui_data.enrichment import build_enrichment -from ohbm2026.ui_data.manifest import build_manifest, make_build_info -from ohbm2026.ui_data.neighbors import build_neighbors -from ohbm2026.ui_data.state_key import ( +from abstractatlas.ui_data.enrichment import build_enrichment +from abstractatlas.ui_data.manifest import build_manifest, make_build_info +from abstractatlas.ui_data.neighbors import build_neighbors +from abstractatlas.ui_data.state_key import ( Stage6BuildError, discover_rollup_state_key, ) -from ohbm2026.ui_data.topics import build_topics -from ohbm2026.ui_data.vectors import build_minilm_vectors +from abstractatlas.ui_data.topics import build_topics +from abstractatlas.ui_data.vectors import build_minilm_vectors __all__ = ["build_ui_data_package", "Stage6BuildError"] @@ -96,12 +96,12 @@ def _get_writer(output_format: str): one branch here. """ if output_format == "parquet-single": - from ohbm2026.ui_data.formats import parquet_single + from abstractatlas.ui_data.formats import parquet_single return parquet_single if output_format == "gzip-json-shards": # Stage-6 legacy emitter — kept reachable for one-off dev # comparisons; not the canonical export. - from ohbm2026.ui_data.formats import gzip_json_shards + from abstractatlas.ui_data.formats import gzip_json_shards return gzip_json_shards raise Stage6BuildError(f"Unknown --output-format: {output_format!r}") diff --git a/src/abstractatlas/ui_data/cells.py b/src/abstractatlas/ui_data/cells.py index 98aba4e3..1567dbdd 100644 --- a/src/abstractatlas/ui_data/cells.py +++ b/src/abstractatlas/ui_data/cells.py @@ -16,8 +16,8 @@ from pathlib import Path from typing import Any -from ohbm2026.ui_data.manifest import discover_cells -from ohbm2026.ui_data.state_key import Stage6BuildError +from abstractatlas.ui_data.manifest import discover_cells +from abstractatlas.ui_data.state_key import Stage6BuildError SCHEMA_VERSION = "cell.v1" NEUROSCAPE_MODEL = "neuroscape" diff --git a/src/abstractatlas/ui_data/dimensions.py b/src/abstractatlas/ui_data/dimensions.py index c85ed61a..aecf0f14 100644 --- a/src/abstractatlas/ui_data/dimensions.py +++ b/src/abstractatlas/ui_data/dimensions.py @@ -23,7 +23,7 @@ from pathlib import Path from typing import Any -from ohbm2026.ui_data.state_key import Stage6BuildError +from abstractatlas.ui_data.state_key import Stage6BuildError __all__ = [ "DIMENSION_KEYS", diff --git a/src/abstractatlas/ui_data/formats/__init__.py b/src/abstractatlas/ui_data/formats/__init__.py index 6dc0b456..dde81043 100644 --- a/src/abstractatlas/ui_data/formats/__init__.py +++ b/src/abstractatlas/ui_data/formats/__init__.py @@ -19,7 +19,7 @@ def emit(*, output_dir: Path, row_streams: RowStreams, manifest: ManifestRow) -> None where ``RowStreams`` bundles every per-entity iterator from the -``ohbm2026.ui_data`` modules above. The candidate writes whatever +``abstractatlas.ui_data`` modules above. The candidate writes whatever files / tables / blobs its container demands under ``output_dir``. """ diff --git a/src/abstractatlas/ui_data/formats/gzip_json_shards.py b/src/abstractatlas/ui_data/formats/gzip_json_shards.py index 744d8a19..5c422d09 100644 --- a/src/abstractatlas/ui_data/formats/gzip_json_shards.py +++ b/src/abstractatlas/ui_data/formats/gzip_json_shards.py @@ -18,7 +18,7 @@ from pathlib import Path from typing import Any, Mapping -from ohbm2026.util.json_io import write_json +from abstractatlas.util.json_io import write_json # Stage-6 deterministic mtime (2026-01-01 UTC) — preserves tarball # byte-identity across rebuilds for Dropbox share-link inode stability. diff --git a/src/abstractatlas/ui_data/formats/parquet_single.py b/src/abstractatlas/ui_data/formats/parquet_single.py index b1061d5c..4cfb76c7 100644 --- a/src/abstractatlas/ui_data/formats/parquet_single.py +++ b/src/abstractatlas/ui_data/formats/parquet_single.py @@ -346,7 +346,7 @@ def write( # Stage 11.1 US2 — derive the standby_slots table once, then thread # the per-poster INT8 lookup map into the abstracts emitter. - from ohbm2026.ui_data.standby_slots import ( + from abstractatlas.ui_data.standby_slots import ( build_poster_to_index_map, derive_standby_slots, ) diff --git a/src/abstractatlas/ui_data/manifest.py b/src/abstractatlas/ui_data/manifest.py index 38154543..30ed6d54 100644 --- a/src/abstractatlas/ui_data/manifest.py +++ b/src/abstractatlas/ui_data/manifest.py @@ -17,7 +17,7 @@ from pathlib import Path from typing import Any -from ohbm2026.ui_data.state_key import ( +from abstractatlas.ui_data.state_key import ( Stage6BuildError, discover_corpus_state_key, discover_rollup_state_key, diff --git a/src/abstractatlas/ui_data/neighbors.py b/src/abstractatlas/ui_data/neighbors.py index c6fa850c..c5499289 100644 --- a/src/abstractatlas/ui_data/neighbors.py +++ b/src/abstractatlas/ui_data/neighbors.py @@ -31,7 +31,7 @@ from pathlib import Path from typing import Any -from ohbm2026.ui_data.state_key import Stage6BuildError +from abstractatlas.ui_data.state_key import Stage6BuildError SCHEMA_VERSION = "neighbors.v1" diff --git a/src/abstractatlas/ui_data/questions.py b/src/abstractatlas/ui_data/questions.py index e02db5ca..70be31bd 100644 --- a/src/abstractatlas/ui_data/questions.py +++ b/src/abstractatlas/ui_data/questions.py @@ -11,7 +11,7 @@ regions / brain networks) to the full text blob to derive the domain-facet axes that the UI's facet panel exposes. -These helpers used to live in the legacy ``ohbm2026.ui.payload`` module +These helpers used to live in the legacy ``abstractatlas.ui.payload`` module that drove the static UI bundle written by ``ohbmcli export-ui``. Now that the Stage 6 SvelteKit site has replaced that path, the helpers were extracted here and the legacy module retired. @@ -22,8 +22,8 @@ import re from typing import Any -from ohbm2026.analyze.storage import parse_string_list_value -from ohbm2026.titles import cleaned_abstract_title +from abstractatlas.analyze.storage import parse_string_list_value +from abstractatlas.titles import cleaned_abstract_title QUESTION_MAP = { "methods": "Please indicate which methods were used in your research:", diff --git a/src/abstractatlas/ui_data/state_key.py b/src/abstractatlas/ui_data/state_key.py index faa5f8e8..08e1f93d 100644 --- a/src/abstractatlas/ui_data/state_key.py +++ b/src/abstractatlas/ui_data/state_key.py @@ -15,8 +15,8 @@ class Stage6BuildError(RuntimeError): """Raised when the Stage 6 builder cannot resolve a required input. Defined as a bare ``RuntimeError`` subclass (rather than - :class:`ohbm2026.exceptions.Stage6Error`) so that importing this module - via ``python -m ohbm2026.ui_data.state_key`` doesn't trigger the + :class:`abstractatlas.exceptions.Stage6Error`) so that importing this module + via ``python -m abstractatlas.ui_data.state_key`` doesn't trigger the ``exceptions.py`` → ``fetch.graphql_api`` import chain (which has a circular-import hazard with ``fetch.stage``). ``Stage6Error`` is still available as a logical parent — callers can ``except Stage6Error`` and @@ -129,9 +129,9 @@ def main() -> int: """CLI entry — print a discovered path on stdout for shell substitution. Usage: - python -m ohbm2026.ui_data.state_key rollup data/outputs/analysis - python -m ohbm2026.ui_data.state_key minilm data/outputs/embeddings/minilm introduction - python -m ohbm2026.ui_data.state_key corpus data/primary/abstracts.json + python -m abstractatlas.ui_data.state_key rollup data/outputs/analysis + python -m abstractatlas.ui_data.state_key minilm data/outputs/embeddings/minilm introduction + python -m abstractatlas.ui_data.state_key corpus data/primary/abstracts.json """ import sys diff --git a/src/abstractatlas/ui_data/topics.py b/src/abstractatlas/ui_data/topics.py index 38b73bad..20f7f463 100644 --- a/src/abstractatlas/ui_data/topics.py +++ b/src/abstractatlas/ui_data/topics.py @@ -12,7 +12,7 @@ from pathlib import Path from typing import Any -from ohbm2026.ui_data.state_key import Stage6BuildError +from abstractatlas.ui_data.state_key import Stage6BuildError SCHEMA_VERSION = "topics.v1" diff --git a/src/abstractatlas/ui_data/types.py b/src/abstractatlas/ui_data/types.py index 4e330659..e24f7a02 100644 --- a/src/abstractatlas/ui_data/types.py +++ b/src/abstractatlas/ui_data/types.py @@ -5,7 +5,7 @@ ``specs/010-export-redesign/research.md``. Every candidate consumes the same row stream produced by the per-entity ``iter_*()`` functions; this module locks the row shapes so the candidate emitters under -``ohbm2026.ui_data.formats`` can share one canonical input contract. +``abstractatlas.ui_data.formats`` can share one canonical input contract. Why ``TypedDict`` and not ``dataclass`` / ``pydantic``: diff --git a/src/ohbm2026/__init__.py b/src/ohbm2026/__init__.py new file mode 100644 index 00000000..ead3c2f3 --- /dev/null +++ b/src/ohbm2026/__init__.py @@ -0,0 +1,58 @@ +"""Deprecated shim: the ``ohbm2026`` package was renamed to ``abstractatlas`` +(spec 027). Importing ``ohbm2026`` or any ``ohbm2026.*`` submodule warns with +a clear rename pointer and transparently redirects to the ``abstractatlas`` +equivalent, so existing external scripts keep working for one transition +cycle. + +FOLLOW-UP (root cause: package rename): delete ``src/ohbm2026/`` once no +external caller imports the old name. This shim exists only to satisfy the +"legacy name → clear pointer, never a silent/partial failure" guarantee +(spec 027 FR-003 / SC-007). The canonical package is ``abstractatlas``. +""" + +from __future__ import annotations + +import importlib +import sys +import warnings +from importlib.abc import Loader, MetaPathFinder +from importlib.machinery import ModuleSpec + +warnings.warn( + "The 'ohbm2026' package was renamed to 'abstractatlas'; update your imports " + "(e.g. 'from abstractatlas.X import Y'). This compatibility shim will be " + "removed in a future release.", + DeprecationWarning, + stacklevel=2, +) + +_OLD = "ohbm2026" +_NEW = "abstractatlas" + + +class _RedirectLoader(Loader): + def __init__(self, target: str) -> None: + self._target = target + + def create_module(self, spec: ModuleSpec): # noqa: D401 + module = importlib.import_module(self._target) + sys.modules[spec.name] = module + return module + + def exec_module(self, module) -> None: # already executed on import + return None + + +class _RedirectFinder(MetaPathFinder): + """Map ``ohbm2026`` / ``ohbm2026.*`` onto ``abstractatlas`` / ``abstractatlas.*``.""" + + def find_spec(self, fullname: str, path=None, target=None): # noqa: D401 + if fullname == _OLD or fullname.startswith(_OLD + "."): + redirected = _NEW + fullname[len(_OLD) :] + return ModuleSpec(fullname, _RedirectLoader(redirected)) + return None + + +sys.meta_path.insert(0, _RedirectFinder()) +# Alias the top-level package object itself to abstractatlas. +sys.modules[__name__] = importlib.import_module(_NEW) diff --git a/tests/test_analyze_centroids.py b/tests/test_analyze_centroids.py index 141b74d3..27cd69fc 100644 --- a/tests/test_analyze_centroids.py +++ b/tests/test_analyze_centroids.py @@ -1,4 +1,4 @@ -"""Tests for `ohbm2026.analyze.centroids` (US3). +"""Tests for `abstractatlas.analyze.centroids` (US3). Coverage per spec FR-008 / CA-002 / CA-007: - `spherical_mean` produces unit-norm direction means matching the @@ -26,7 +26,7 @@ import numpy as np -from ohbm2026.analyze.centroids import ( +from abstractatlas.analyze.centroids import ( STAGE2_DIM, CentroidTable, assign_nearest_centroid, @@ -34,7 +34,7 @@ spherical_mean, write_neuroscape_clusters_bundle, ) -from ohbm2026.exceptions import ( +from abstractatlas.exceptions import ( AnalysisError, CentroidTableMissing, CentroidTableVersionMismatch, diff --git a/tests/test_analyze_cli.py b/tests/test_analyze_cli.py index 62173b8e..7d8d1925 100644 --- a/tests/test_analyze_cli.py +++ b/tests/test_analyze_cli.py @@ -1,5 +1,5 @@ """Tests for the `analyze-matrix` + `analyze-umap-project` subcommand -wiring in `ohbm2026.cli`.""" +wiring in `abstractatlas.cli`.""" from __future__ import annotations @@ -15,7 +15,7 @@ import numpy as np -from ohbm2026 import cli +from abstractatlas import cli @contextmanager @@ -32,7 +32,7 @@ def _isolated_cwd(): class AnalyzeMatrixCLITests(unittest.TestCase): def test_analyze_matrix_delegates_to_stage_main(self) -> None: - from ohbm2026.analyze import stage as analyze_stage_mod + from abstractatlas.analyze import stage as analyze_stage_mod with mock.patch.object(analyze_stage_mod, "main", return_value=0) as analyze_main: result = cli.main(["analyze-matrix", "--dry-run"]) @@ -43,7 +43,7 @@ def test_analyze_matrix_delegates_to_stage_main(self) -> None: def test_analyze_matrix_passes_every_flag(self) -> None: """contracts/cli.md enumerates every flag; the subparser must accept all of them.""" - from ohbm2026.analyze import stage as analyze_stage_mod + from abstractatlas.analyze import stage as analyze_stage_mod argv = [ "analyze-matrix", @@ -86,7 +86,7 @@ def test_analyze_matrix_passes_every_flag(self) -> None: class AnalyzeUmapProjectCLITests(unittest.TestCase): def test_round_trip_knn_weighted(self) -> None: """End-to-end CLI run: build a bundle, then project new vectors.""" - from ohbm2026.analyze.umap import ( + from abstractatlas.analyze.umap import ( fit_umap_2d, fit_umap_3d, write_projections_bundle, @@ -127,7 +127,7 @@ def test_round_trip_knn_weighted(self) -> None: self.assertEqual(event["event"], "project_into_umap_complete") def test_dim_mismatch_returns_2(self) -> None: - from ohbm2026.analyze.umap import fit_umap_2d, fit_umap_3d, write_projections_bundle + from abstractatlas.analyze.umap import fit_umap_2d, fit_umap_3d, write_projections_bundle with _isolated_cwd(): rng = np.random.default_rng(7) diff --git a/tests/test_analyze_communities.py b/tests/test_analyze_communities.py index 746d3c7b..acdfb175 100644 --- a/tests/test_analyze_communities.py +++ b/tests/test_analyze_communities.py @@ -1,4 +1,4 @@ -"""Tests for `ohbm2026.analyze.communities` (US4). +"""Tests for `abstractatlas.analyze.communities` (US4). Coverage per spec FR-007 + CA-002: - FAISS `IndexFlatIP` kNN over L2-normalized vectors returns sensible @@ -20,7 +20,7 @@ import numpy as np -from ohbm2026.analyze.communities import ( +from abstractatlas.analyze.communities import ( DEFAULT_KNN_K, build_faiss_knn, detect_communities, @@ -31,7 +31,7 @@ select_plateau_resolution, ResolutionSweepEntry, ) -from ohbm2026.exceptions import CommunityResolutionDegenerate +from abstractatlas.exceptions import CommunityResolutionDegenerate def _three_cluster_corpus( diff --git a/tests/test_analyze_provenance.py b/tests/test_analyze_provenance.py index 10464051..3de5503e 100644 --- a/tests/test_analyze_provenance.py +++ b/tests/test_analyze_provenance.py @@ -1,4 +1,4 @@ -"""Tests for `ohbm2026.analyze.provenance`. +"""Tests for `abstractatlas.analyze.provenance`. Per CA-008 + Principle VIII, provenance MUST reject absolute / `~` / parent-escape paths so bundle records stay portable. @@ -11,13 +11,13 @@ import unittest from pathlib import Path -from ohbm2026.analyze.provenance import ( +from abstractatlas.analyze.provenance import ( assert_path_safe, assert_paths_safe, write_bundle_provenance, write_run_provenance, ) -from ohbm2026.exceptions import ProvenanceError +from abstractatlas.exceptions import ProvenanceError class AssertPathSafeTests(unittest.TestCase): diff --git a/tests/test_analyze_rollup.py b/tests/test_analyze_rollup.py index f7a14b39..cb1cb256 100644 --- a/tests/test_analyze_rollup.py +++ b/tests/test_analyze_rollup.py @@ -1,4 +1,4 @@ -"""Tests for `ohbm2026.analyze.rollup.write_rollup`. +"""Tests for `abstractatlas.analyze.rollup.write_rollup`. Per contracts/rollup.md, the parquet + sqlite forms are content-equivalent: same column count, same row count, same @@ -15,12 +15,12 @@ import numpy as np -from ohbm2026.analyze.rollup import ( +from abstractatlas.analyze.rollup import ( build_rollup_tables, load_neuroscape_cluster_table, write_rollup, ) -from ohbm2026.analyze.storage import write_analysis_bundle +from abstractatlas.analyze.storage import write_analysis_bundle def _prov(kind: str, input_key: str, bundle_dir_rel: str) -> dict: diff --git a/tests/test_analyze_stage.py b/tests/test_analyze_stage.py index 081dd20e..5295de96 100644 --- a/tests/test_analyze_stage.py +++ b/tests/test_analyze_stage.py @@ -1,4 +1,4 @@ -"""Tests for `ohbm2026.analyze.stage`. +"""Tests for `abstractatlas.analyze.stage`. Per spec FR-001 / FR-002 / FR-003 / FR-013, the Stage 4 orchestrator must: @@ -40,8 +40,8 @@ def _isolated_cwd(): os.chdir(original) shutil.rmtree(tmp, ignore_errors=True) -from ohbm2026.analyze import stage as stage_mod -from ohbm2026.analyze.stage import ( +from abstractatlas.analyze import stage as stage_mod +from abstractatlas.analyze.stage import ( AnalysisConfig, DEFAULT_KINDS, DEFAULT_MODELS, @@ -51,7 +51,7 @@ def _isolated_cwd(): register_kind_runner, run_matrix, ) -from ohbm2026.exceptions import ( +from abstractatlas.exceptions import ( AnalysisError, InputBundleMissing, ) @@ -200,7 +200,7 @@ def test_corpus_state_key_collision_refused(self) -> None: # Pre-write a bundle dir with a stale corpus_state_key recorded in its provenance. from dataclasses import replace - from ohbm2026.analyze.stage import _bundle_output_path, _kind_state_key + from abstractatlas.analyze.stage import _bundle_output_path, _kind_state_key entry = stage_mod.PlanEntry( model_key="voyage", diff --git a/tests/test_analyze_storage.py b/tests/test_analyze_storage.py index e41adf87..9252f533 100644 --- a/tests/test_analyze_storage.py +++ b/tests/test_analyze_storage.py @@ -1,4 +1,4 @@ -"""Tests for `ohbm2026.analyze.storage.write_analysis_bundle`. +"""Tests for `abstractatlas.analyze.storage.write_analysis_bundle`. Per spec FR-010 + contracts/bundle.md, every Stage 4 bundle ships an atomic-rename write of `ids.npy` + payload `*.npy` + `metadata.json` + @@ -16,7 +16,7 @@ import numpy as np -from ohbm2026.analyze.storage import write_analysis_bundle, iter_analysis_bundles +from abstractatlas.analyze.storage import write_analysis_bundle, iter_analysis_bundles class WriteAnalysisBundleTests(unittest.TestCase): diff --git a/tests/test_analyze_topic_clusters.py b/tests/test_analyze_topic_clusters.py index 581542fa..f3642487 100644 --- a/tests/test_analyze_topic_clusters.py +++ b/tests/test_analyze_topic_clusters.py @@ -1,4 +1,4 @@ -"""Tests for `ohbm2026.analyze.topic_clusters` (US5). +"""Tests for `abstractatlas.analyze.topic_clusters` (US5). Coverage per FR-009 + CA-002: - Synthetic 3-cluster corpus → `run_topic_clustering` recovers @@ -20,7 +20,7 @@ import numpy as np -from ohbm2026.analyze.topic_clusters import ( +from abstractatlas.analyze.topic_clusters import ( run_topic_clustering, write_topic_clusters_bundle, ) diff --git a/tests/test_analyze_topics.py b/tests/test_analyze_topics.py index 057b71fc..c005b309 100644 --- a/tests/test_analyze_topics.py +++ b/tests/test_analyze_topics.py @@ -1,4 +1,4 @@ -"""Tests for `ohbm2026.analyze.topics` (US5). +"""Tests for `abstractatlas.analyze.topics` (US5). Coverage per FR-009 + CA-002: - Phrase extraction returns canonicalized noun-chunks + entities. @@ -23,14 +23,14 @@ from contextlib import contextmanager from pathlib import Path -from ohbm2026.analyze.topics import ( +from abstractatlas.analyze.topics import ( DEFAULT_KEYWORD_OUT_N, build_topics_artifact, compute_ctfidf, extract_cluster_phrases_local, group_phrases_via_llm, ) -from ohbm2026.exceptions import AnalysisError, TopicGroupingHallucination +from abstractatlas.exceptions import AnalysisError, TopicGroupingHallucination @contextmanager diff --git a/tests/test_analyze_umap.py b/tests/test_analyze_umap.py index 0d980ad7..1175f369 100644 --- a/tests/test_analyze_umap.py +++ b/tests/test_analyze_umap.py @@ -1,4 +1,4 @@ -"""Tests for `ohbm2026.analyze.umap.project_into_umap` (US2). +"""Tests for `abstractatlas.analyze.umap.project_into_umap` (US2). Coverage per spec FR-005 / FR-006 / CA-002: - `native` round-trip + within-convex-hull behavior. @@ -21,14 +21,14 @@ import numpy as np -from ohbm2026.analyze.umap import ( +from abstractatlas.analyze.umap import ( fit_parametric_mlp, fit_umap_2d, fit_umap_3d, project_into_umap, write_projections_bundle, ) -from ohbm2026.exceptions import ( +from abstractatlas.exceptions import ( ProjectionDimensionMismatch, UnsupportedProjectionAlgorithm, AnalysisError, diff --git a/tests/test_artifacts.py b/tests/test_artifacts.py index 7e8fe63a..3bbf7f29 100644 --- a/tests/test_artifacts.py +++ b/tests/test_artifacts.py @@ -2,7 +2,7 @@ import unittest from pathlib import Path -from ohbm2026 import artifacts +from abstractatlas import artifacts class ArtifactHelpersTest(unittest.TestCase): diff --git a/tests/test_assets.py b/tests/test_assets.py index 8efb1d2a..66c8dd58 100644 --- a/tests/test_assets.py +++ b/tests/test_assets.py @@ -4,8 +4,8 @@ from tempfile import TemporaryDirectory from unittest import mock -from ohbm2026 import artifacts -from ohbm2026.assets import ( +from abstractatlas import artifacts +from abstractatlas.assets import ( asset_stem, build_database, build_parser, @@ -191,9 +191,9 @@ def test_build_database_writes_graphql_input_snapshot(self) -> None: snapshot_dir = root / "inputs" with ( - mock.patch("ohbm2026.assets.fetch_abstract_ids", return_value=([1], [123])), + mock.patch("abstractatlas.assets.fetch_abstract_ids", return_value=([1], [123])), mock.patch( - "ohbm2026.assets.fetch_abstract_content", + "abstractatlas.assets.fetch_abstract_content", return_value=[ { "id": 123, @@ -238,7 +238,7 @@ def _content_payload(self, sid: int) -> dict[str, object]: } def test_on_batch_complete_fires_once_per_batch(self) -> None: - from ohbm2026 import assets as assets_module + from abstractatlas import assets as assets_module captured_batches: list[list[int]] = [] @@ -265,7 +265,7 @@ def fake_fetch_abstract_content(api_key, submission_ids, **kwargs): self.assertEqual([r["id"] for r in results], [1, 2, 3, 4, 5]) def test_on_record_state_change_fires_for_every_record(self) -> None: - from ohbm2026 import assets as assets_module + from abstractatlas import assets as assets_module seen: list[tuple[int, str]] = [] @@ -307,12 +307,12 @@ class TestPerRecordStateTransitions(unittest.TestCase): """ def test_pending_can_advance_to_corpus_fetched(self) -> None: - from ohbm2026.assets import advance_record_state + from abstractatlas.assets import advance_record_state self.assertEqual(advance_record_state("pending", "corpus_fetched"), "corpus_fetched") def test_corpus_fetched_can_advance_to_figures_in_progress(self) -> None: - from ohbm2026.assets import advance_record_state + from abstractatlas.assets import advance_record_state self.assertEqual( advance_record_state("corpus_fetched", "figures_in_progress"), @@ -320,12 +320,12 @@ def test_corpus_fetched_can_advance_to_figures_in_progress(self) -> None: ) def test_corpus_fetched_can_advance_directly_to_done_when_no_figures(self) -> None: - from ohbm2026.assets import advance_record_state + from abstractatlas.assets import advance_record_state self.assertEqual(advance_record_state("corpus_fetched", "done"), "done") def test_figures_in_progress_can_advance_to_done_or_failed_retryable(self) -> None: - from ohbm2026.assets import advance_record_state + from abstractatlas.assets import advance_record_state self.assertEqual(advance_record_state("figures_in_progress", "done"), "done") self.assertEqual( @@ -334,7 +334,7 @@ def test_figures_in_progress_can_advance_to_done_or_failed_retryable(self) -> No ) def test_backwards_transition_raises(self) -> None: - from ohbm2026.assets import advance_record_state + from abstractatlas.assets import advance_record_state with self.assertRaises(ValueError): advance_record_state("done", "pending") @@ -367,7 +367,7 @@ def _content_with_figure(self, sid: int, source_url: str) -> dict[str, object]: } def test_figure_download_skipped_when_assets_dir_is_none(self) -> None: - from ohbm2026 import assets as assets_module + from abstractatlas import assets as assets_module def fake_fetch_abstract_content(api_key, ids, **kwargs): return [ @@ -392,8 +392,8 @@ def fake_fetch_abstract_content(api_key, ids, **kwargs): self.assertEqual(results[0]["local_assets"], []) def test_figure_download_runs_and_populates_local_assets(self) -> None: - from ohbm2026 import assets as assets_module - from ohbm2026.assets import AssetDownload + from abstractatlas import assets as assets_module + from abstractatlas.assets import AssetDownload def fake_fetch_abstract_content(api_key, ids, **kwargs): return [ @@ -438,8 +438,8 @@ def test_existing_on_disk_asset_is_reused_without_download_call(self) -> None: path here so the reuse semantics are exercised end to end — we just pre-seed the destination directory with a file at the expected stem.""" - from ohbm2026 import assets as assets_module - from ohbm2026.assets import asset_stem + from abstractatlas import assets as assets_module + from abstractatlas.assets import asset_stem source_url = "https://example.org/abstract-1.png" abstract_id = 1 @@ -482,7 +482,7 @@ class TestNormalizeAuthor(unittest.TestCase): affiliations order. ORCID and other non-PII fields are preserved.""" def test_email_is_stripped(self) -> None: - from ohbm2026.assets import normalize_author + from abstractatlas.assets import normalize_author raw = { "id": 42, @@ -499,7 +499,7 @@ def test_email_is_stripped(self) -> None: self.assertEqual(out["orcid_id"], "0000-0001-2345-6789") def test_affiliations_sorted_by_order_then_id(self) -> None: - from ohbm2026.assets import normalize_author + from abstractatlas.assets import normalize_author raw = { "id": 1, @@ -517,7 +517,7 @@ def test_affiliations_sorted_by_order_then_id(self) -> None: self.assertEqual(ids, [100, 150, 200]) def test_missing_fields_become_none(self) -> None: - from ohbm2026.assets import normalize_author + from abstractatlas.assets import normalize_author raw = {"id": 99, "affiliations": None} out = normalize_author(raw) diff --git a/tests/test_atlas_cli.py b/tests/test_atlas_cli.py index 31ae9a79..9e1be0ee 100644 --- a/tests/test_atlas_cli.py +++ b/tests/test_atlas_cli.py @@ -1,4 +1,4 @@ -"""Tests for ``ohbm2026.atlas_package.cli``. +"""Tests for ``abstractatlas.atlas_package.cli``. Spec: ``specs/015-neuroscape-context/`` — ``contracts/cli-build-atlas-package.md``. @@ -17,8 +17,8 @@ from tempfile import TemporaryDirectory from unittest.mock import patch -from ohbm2026 import exceptions -from ohbm2026.atlas_package import cli as atlas_cli +from abstractatlas import exceptions +from abstractatlas.atlas_package import cli as atlas_cli class BuildParserTests(unittest.TestCase): diff --git a/tests/test_atlas_exceptions.py b/tests/test_atlas_exceptions.py index 4d6cc755..36871b39 100644 --- a/tests/test_atlas_exceptions.py +++ b/tests/test_atlas_exceptions.py @@ -15,7 +15,7 @@ class carries structured kwargs so the orchestrator and tests can import unittest -from ohbm2026 import exceptions +from abstractatlas import exceptions class Stage15ExceptionTreeTests(unittest.TestCase): diff --git a/tests/test_atlas_hosting_cli.py b/tests/test_atlas_hosting_cli.py index d69d0d58..b3fd1d41 100644 --- a/tests/test_atlas_hosting_cli.py +++ b/tests/test_atlas_hosting_cli.py @@ -12,9 +12,9 @@ from tempfile import TemporaryDirectory from unittest import mock -from ohbm2026 import cli as top_cli -from ohbm2026.atlas_hosting import cli as hosting_cli -from ohbm2026.atlas_hosting.compare import ( +from abstractatlas import cli as top_cli +from abstractatlas.atlas_hosting import cli as hosting_cli +from abstractatlas.atlas_hosting.compare import ( ArtifactComparison, CacheProbe, ComparisonReport, diff --git a/tests/test_atlas_hosting_compare.py b/tests/test_atlas_hosting_compare.py index f7508355..b12008c4 100644 --- a/tests/test_atlas_hosting_compare.py +++ b/tests/test_atlas_hosting_compare.py @@ -14,8 +14,8 @@ import jsonschema -from ohbm2026.atlas_hosting import compare -from ohbm2026.exceptions import HostingComparisonError +from abstractatlas.atlas_hosting import compare +from abstractatlas.exceptions import HostingComparisonError ORIGIN = "https://abstractatlas.brainkb.org" _SCHEMA_PATH = ( diff --git a/tests/test_atlas_hosting_content_hash.py b/tests/test_atlas_hosting_content_hash.py index 5bc2a708..01f54c18 100644 --- a/tests/test_atlas_hosting_content_hash.py +++ b/tests/test_atlas_hosting_content_hash.py @@ -11,7 +11,7 @@ from pathlib import Path from tempfile import TemporaryDirectory -from ohbm2026.atlas_hosting import content_hash +from abstractatlas.atlas_hosting import content_hash class Sha256FileTests(unittest.TestCase): diff --git a/tests/test_atlas_hosting_manifest.py b/tests/test_atlas_hosting_manifest.py index c05cd9dc..5fe0f8e3 100644 --- a/tests/test_atlas_hosting_manifest.py +++ b/tests/test_atlas_hosting_manifest.py @@ -12,13 +12,13 @@ import jsonschema -from ohbm2026 import artifacts -from ohbm2026.atlas_hosting.manifest import ( +from abstractatlas import artifacts +from abstractatlas.atlas_hosting.manifest import ( ContentAddressedObject, UploadManifest, build_channel_entry, ) -from ohbm2026.exceptions import AtlasProvenanceError +from abstractatlas.exceptions import AtlasProvenanceError _SCHEMA_PATH = ( Path(__file__).resolve().parent.parent diff --git a/tests/test_atlas_hosting_r2_client.py b/tests/test_atlas_hosting_r2_client.py index 932d45d4..145c2ef3 100644 --- a/tests/test_atlas_hosting_r2_client.py +++ b/tests/test_atlas_hosting_r2_client.py @@ -14,8 +14,8 @@ from botocore.stub import Stubber -from ohbm2026.atlas_hosting import r2_client -from ohbm2026.exceptions import R2CredentialsError, R2UploadError +from abstractatlas.atlas_hosting import r2_client +from abstractatlas.exceptions import R2CredentialsError, R2UploadError def _settings() -> r2_client.R2Settings: diff --git a/tests/test_atlas_hosting_uploader.py b/tests/test_atlas_hosting_uploader.py index 6da08b16..53673059 100644 --- a/tests/test_atlas_hosting_uploader.py +++ b/tests/test_atlas_hosting_uploader.py @@ -23,10 +23,10 @@ from tempfile import TemporaryDirectory from typing import Optional, Tuple -from ohbm2026.atlas_hosting import uploader -from ohbm2026.atlas_hosting.content_hash import derive_object_key, sha256_file -from ohbm2026.atlas_hosting.r2_client import R2Settings -from ohbm2026.exceptions import ArtifactDiscoveryError, ContentHashMismatchError +from abstractatlas.atlas_hosting import uploader +from abstractatlas.atlas_hosting.content_hash import derive_object_key, sha256_file +from abstractatlas.atlas_hosting.r2_client import R2Settings +from abstractatlas.exceptions import ArtifactDiscoveryError, ContentHashMismatchError # What build-atlas-package writes into --package-dir (all required). PACKAGE_FILES = ("neuroscape.parquet", "atlas.parquet", "neuroscape_vectors.parquet") diff --git a/tests/test_atlas_link_check.py b/tests/test_atlas_link_check.py index ca715cb3..b5a5852c 100644 --- a/tests/test_atlas_link_check.py +++ b/tests/test_atlas_link_check.py @@ -1,4 +1,4 @@ -"""Tests for ``ohbm2026.atlas_package.link_check``. +"""Tests for ``abstractatlas.atlas_package.link_check``. Spec: ``specs/015-neuroscape-context/`` — FR-024 + research R-013 (narrowed scope: only the small fixed set of non-PubMed-record URLs @@ -18,8 +18,8 @@ from dataclasses import dataclass from typing import Mapping -from ohbm2026 import exceptions -from ohbm2026.atlas_package import link_check as lc +from abstractatlas import exceptions +from abstractatlas.atlas_package import link_check as lc # --------------------------------------------------------------------------- diff --git a/tests/test_atlas_lod.py b/tests/test_atlas_lod.py index 2a5388be..76b6404f 100644 --- a/tests/test_atlas_lod.py +++ b/tests/test_atlas_lod.py @@ -1,4 +1,4 @@ -"""Tests for ``ohbm2026.atlas_package.lod`` — quadtree blue-noise LOD. +"""Tests for ``abstractatlas.atlas_package.lod`` — quadtree blue-noise LOD. Spec: ``specs/019-neuroscape-semantic-search/plan-lod-backdrop.md``. @@ -17,7 +17,7 @@ import numpy as np -from ohbm2026.atlas_package import lod +from abstractatlas.atlas_package import lod def _grid_cell(coords: np.ndarray, resolution: int) -> np.ndarray: diff --git a/tests/test_atlas_orchestrator.py b/tests/test_atlas_orchestrator.py index da10c706..fbd002ba 100644 --- a/tests/test_atlas_orchestrator.py +++ b/tests/test_atlas_orchestrator.py @@ -1,4 +1,4 @@ -"""End-to-end test for :func:`ohbm2026.atlas_package.orchestrator.build_atlas_package`. +"""End-to-end test for :func:`abstractatlas.atlas_package.orchestrator.build_atlas_package`. Spec: ``specs/015-neuroscape-context/`` — T020 + R-005 + SC-004. @@ -26,7 +26,7 @@ import numpy as np -from ohbm2026.atlas_package import orchestrator, umap_fit +from abstractatlas.atlas_package import orchestrator, umap_fit from tests._atlas_fixtures import write_v101_fixture @@ -252,7 +252,7 @@ def test_semantic_index_enabled_emits_sibling_parquet_and_provenance(self) -> No # Monkeypatch the vectors_compute encoder to avoid downloading # the real MiniLM model in this test (the synthetic encoder # matches the production shape). - from ohbm2026.atlas_package import vectors_compute + from abstractatlas.atlas_package import vectors_compute real_compute = vectors_compute.compute_cluster_vectors encoder = self._synthetic_encoder() diff --git a/tests/test_atlas_parquet_writer.py b/tests/test_atlas_parquet_writer.py index a5844082..249db1c2 100644 --- a/tests/test_atlas_parquet_writer.py +++ b/tests/test_atlas_parquet_writer.py @@ -1,4 +1,4 @@ -"""Tests for ``ohbm2026.atlas_package.parquet_writer``. +"""Tests for ``abstractatlas.atlas_package.parquet_writer``. Spec: ``specs/015-neuroscape-context/`` — research R-008 + R-011 + data-model.md + ``contracts/parquet-schemas.md`` + R-009 @@ -31,15 +31,15 @@ import numpy as np import pyarrow.parquet as pq -from ohbm2026 import exceptions -from ohbm2026.atlas_package import ( +from abstractatlas import exceptions +from abstractatlas.atlas_package import ( cluster_palette as palette_mod, lod, neighbour_index, parquet_writer, ) -from ohbm2026.atlas_package.neuroscape_loader import ArticleHeader, NeuroScapeCluster -from ohbm2026.atlas_package.ohbm_projector import ProjectionResult +from abstractatlas.atlas_package.neuroscape_loader import ArticleHeader, NeuroScapeCluster +from abstractatlas.atlas_package.ohbm_projector import ProjectionResult # --------------------------------------------------------------------------- diff --git a/tests/test_atlas_provenance.py b/tests/test_atlas_provenance.py index 15cac53b..75475aa6 100644 --- a/tests/test_atlas_provenance.py +++ b/tests/test_atlas_provenance.py @@ -1,4 +1,4 @@ -"""Tests for ``ohbm2026.atlas_package.provenance.normalise_path``. +"""Tests for ``abstractatlas.atlas_package.provenance.normalise_path``. Spec: ``specs/015-neuroscape-context/`` — CA-008 + research R-009 ``AtlasProvenanceError`` contract. Every Stage 15 provenance field @@ -17,8 +17,8 @@ import unittest from pathlib import Path -from ohbm2026 import exceptions -from ohbm2026.atlas_package import provenance +from abstractatlas import exceptions +from abstractatlas.atlas_package import provenance class NormalisePathHappyPathTests(unittest.TestCase): diff --git a/tests/test_atlas_semantic_index.py b/tests/test_atlas_semantic_index.py index a814aa7b..f388f52d 100644 --- a/tests/test_atlas_semantic_index.py +++ b/tests/test_atlas_semantic_index.py @@ -1,4 +1,4 @@ -"""Tests for ``ohbm2026.atlas_package.semantic_index`` (spec 019, T009 + T018). +"""Tests for ``abstractatlas.atlas_package.semantic_index`` (spec 019, T009 + T018). Covers: - INV-003 enforcement (pubmed_id set mismatch raises VectorsParquetWriteError) @@ -20,8 +20,8 @@ import numpy as np import pyarrow.parquet as pq -from ohbm2026 import exceptions -from ohbm2026.atlas_package import semantic_index +from abstractatlas import exceptions +from abstractatlas.atlas_package import semantic_index def _sha256(path: Path) -> str: diff --git a/tests/test_atlas_umap_fit.py b/tests/test_atlas_umap_fit.py index 6db53b68..27f36ba0 100644 --- a/tests/test_atlas_umap_fit.py +++ b/tests/test_atlas_umap_fit.py @@ -1,4 +1,4 @@ -"""Tests for ``ohbm2026.atlas_package.umap_fit``. +"""Tests for ``abstractatlas.atlas_package.umap_fit``. Spec: ``specs/015-neuroscape-context/`` — research R-001 (UMAP fit parameters and seed) + R-009 (``UmapFitError``). @@ -23,8 +23,8 @@ import numpy as np -from ohbm2026 import exceptions -from ohbm2026.atlas_package import umap_fit +from abstractatlas import exceptions +from abstractatlas.atlas_package import umap_fit def _synthetic_vectors(n: int = 50, dim: int = 64, seed: int = 0) -> np.ndarray: diff --git a/tests/test_atlas_vectors_compute.py b/tests/test_atlas_vectors_compute.py index bf09a255..6219205c 100644 --- a/tests/test_atlas_vectors_compute.py +++ b/tests/test_atlas_vectors_compute.py @@ -1,4 +1,4 @@ -"""Tests for ``ohbm2026.atlas_package.vectors_compute`` (spec 019, T010 + T011). +"""Tests for ``abstractatlas.atlas_package.vectors_compute`` (spec 019, T010 + T011). Covers: - Per-cluster cache hit/miss (R-009): first run populates cache; second @@ -23,8 +23,8 @@ import numpy as np -from ohbm2026 import exceptions -from ohbm2026.atlas_package import vectors_compute +from abstractatlas import exceptions +from abstractatlas.atlas_package import vectors_compute def _make_encoder(seed: int = 7) -> Callable[[list[str]], np.ndarray]: diff --git a/tests/test_book_assembly.py b/tests/test_book_assembly.py index ec8e4eb2..3d23468a 100644 --- a/tests/test_book_assembly.py +++ b/tests/test_book_assembly.py @@ -33,8 +33,8 @@ def setUpClass(cls) -> None: except ImportError as exc: raise unittest.SkipTest(f"pikepdf not installed: {exc}") try: - from ohbm2026.book.assemble_pdf import assemble # noqa: F401 - from ohbm2026.book.model import AbstractPdfChunk # noqa: F401 + from abstractatlas.book.assemble_pdf import assemble # noqa: F401 + from abstractatlas.book.model import AbstractPdfChunk # noqa: F401 except ImportError as exc: raise unittest.SkipTest( f"assemble_pdf / AbstractPdfChunk not yet implemented: {exc}" @@ -68,7 +68,7 @@ def tearDown(self) -> None: self.tmp.cleanup() def _build_chunks(self): - from ohbm2026.book.model import AbstractPdfChunk + from abstractatlas.book.model import AbstractPdfChunk chunks = [] for i, name in enumerate(("chunk_a", "chunk_b", "chunk_c")): @@ -93,7 +93,7 @@ def _build_chunks(self): return front, chunks def test_chunk_offsets_measured_in_pass_one(self) -> None: - from ohbm2026.book.assemble_pdf import assemble + from abstractatlas.book.assemble_pdf import assemble front, chunks = self._build_chunks() out = self.dir / "book.pdf" @@ -103,7 +103,7 @@ def test_chunk_offsets_measured_in_pass_one(self) -> None: header = pathlib.Path( str( - resources.files("ohbm2026.book.templates").joinpath( + resources.files("abstractatlas.book.templates").joinpath( "header-includes.tex" ) ) @@ -138,7 +138,7 @@ def test_chunk_offsets_measured_in_pass_one(self) -> None: def test_final_pdf_contains_index_appendix(self) -> None: if not _pandoc_engine_ok(): self.skipTest("pandoc + engine required for pass-2 index appendix") - from ohbm2026.book.assemble_pdf import assemble + from abstractatlas.book.assemble_pdf import assemble import pikepdf from importlib import resources @@ -146,7 +146,7 @@ def test_final_pdf_contains_index_appendix(self) -> None: out = self.dir / "book.pdf" header = pathlib.Path( str( - resources.files("ohbm2026.book.templates").joinpath( + resources.files("abstractatlas.book.templates").joinpath( "header-includes.tex" ) ) diff --git a/tests/test_book_author_index.py b/tests/test_book_author_index.py index 810312c8..6ecd4c67 100644 --- a/tests/test_book_author_index.py +++ b/tests/test_book_author_index.py @@ -5,7 +5,7 @@ import pathlib import unittest -from ohbm2026.book.corpus import load_book +from abstractatlas.book.corpus import load_book _FIX = pathlib.Path(__file__).parent / "fixtures" / "book" @@ -22,7 +22,7 @@ def _book(): class TestAuthorIndex(unittest.TestCase): def setUp(self) -> None: try: - from ohbm2026.book.author_index import build_author_index + from abstractatlas.book.author_index import build_author_index except ImportError: self.skipTest("author_index not yet implemented") self.build = build_author_index diff --git a/tests/test_book_author_index_buckets.py b/tests/test_book_author_index_buckets.py index 8e80d796..7d3fb5b3 100644 --- a/tests/test_book_author_index_buckets.py +++ b/tests/test_book_author_index_buckets.py @@ -15,7 +15,7 @@ class TestBucketLetter(unittest.TestCase): @classmethod def setUpClass(cls) -> None: try: - from ohbm2026.book.assemble_pdf import _bucket_letter + from abstractatlas.book.assemble_pdf import _bucket_letter except ImportError as exc: raise unittest.SkipTest(f"_bucket_letter not yet implemented: {exc}") cls._bucket = staticmethod(_bucket_letter) @@ -46,8 +46,8 @@ class TestBuildIndexMarkdownBuckets(unittest.TestCase): @classmethod def setUpClass(cls) -> None: try: - from ohbm2026.book.assemble_pdf import _build_index_markdown - from ohbm2026.book.model import AuthorIndexEntry + from abstractatlas.book.assemble_pdf import _build_index_markdown + from abstractatlas.book.model import AuthorIndexEntry except ImportError as exc: raise unittest.SkipTest(f"index helpers not yet implemented: {exc}") cls._build = staticmethod(_build_index_markdown) diff --git a/tests/test_book_cache.py b/tests/test_book_cache.py index 8c0e77a1..901fbad1 100644 --- a/tests/test_book_cache.py +++ b/tests/test_book_cache.py @@ -26,10 +26,10 @@ class TestCacheKey(unittest.TestCase): @classmethod def setUpClass(cls) -> None: try: - from ohbm2026.book.cache import compute_cache_key + from abstractatlas.book.cache import compute_cache_key except ImportError as exc: raise unittest.SkipTest( - f"ohbm2026.book.cache not yet implemented: {exc}" + f"abstractatlas.book.cache not yet implemented: {exc}" ) cls.compute = staticmethod(compute_cache_key) cls.base = dict( @@ -81,13 +81,13 @@ class TestCacheLoadStore(unittest.TestCase): @classmethod def setUpClass(cls) -> None: try: - from ohbm2026.book.cache import ( + from abstractatlas.book.cache import ( load_cached_pdf, store_cached_pdf, ) except ImportError as exc: raise unittest.SkipTest( - f"ohbm2026.book.cache not yet implemented: {exc}" + f"abstractatlas.book.cache not yet implemented: {exc}" ) cls.load = staticmethod(load_cached_pdf) cls.store = staticmethod(store_cached_pdf) @@ -161,7 +161,7 @@ def test_store_from_path_avoids_bytes_round_trip(self) -> None: moves it into the cache). No read-bytes-then-write-temp round-trip; the source path no longer exists after the call. """ - from ohbm2026.book.cache import store_cached_pdf_from_path + from abstractatlas.book.cache import store_cached_pdf_from_path key = "1111222233334444" # Write a temp PDF (same shape as pandoc's output would be). diff --git a/tests/test_book_corpus.py b/tests/test_book_corpus.py index b53d5fb6..dda93ce2 100644 --- a/tests/test_book_corpus.py +++ b/tests/test_book_corpus.py @@ -5,14 +5,14 @@ import pathlib import unittest -from ohbm2026.book.corpus import load_book -from ohbm2026.exceptions import BookBuildError +from abstractatlas.book.corpus import load_book +from abstractatlas.exceptions import BookBuildError _FIX = pathlib.Path(__file__).parent / "fixtures" / "book" def _load() -> "Book": - from ohbm2026.book.model import Book + from abstractatlas.book.model import Book return load_book( corpus_path=_FIX / "abstracts.json", diff --git a/tests/test_book_failure_isolation.py b/tests/test_book_failure_isolation.py index 4baca9e4..3d9958b4 100644 --- a/tests/test_book_failure_isolation.py +++ b/tests/test_book_failure_isolation.py @@ -39,7 +39,7 @@ class TestFailureIsolation(unittest.TestCase): def setUp(self) -> None: # Build a temp corpus = fixture corpus + ONE broken abstract. try: - from ohbm2026.book.cli import main # noqa: F401 + from abstractatlas.book.cli import main # noqa: F401 except ImportError as exc: self.skipTest(f"book CLI not importable: {exc}") @@ -126,7 +126,7 @@ def tearDown(self) -> None: self.tmp.cleanup() def test_broken_abstract_drops_out_others_render(self) -> None: - from ohbm2026.book.cli import main + from abstractatlas.book.cli import main # cwd is self.workdir per setUp; use workdir-relative paths so # the provenance writer's project-relative-path guard passes. diff --git a/tests/test_book_figure_check.py b/tests/test_book_figure_check.py index 28eb2e7d..e5c555b5 100644 --- a/tests/test_book_figure_check.py +++ b/tests/test_book_figure_check.py @@ -6,7 +6,7 @@ import tempfile import unittest -from ohbm2026.book.figure_check import ( +from abstractatlas.book.figure_check import ( PUBLICATION_DPI_THRESHOLD, effective_dpi, probe_figure, diff --git a/tests/test_book_figure_normalise.py b/tests/test_book_figure_normalise.py index f82b0b24..4072597e 100644 --- a/tests/test_book_figure_normalise.py +++ b/tests/test_book_figure_normalise.py @@ -25,7 +25,7 @@ def setUpClass(cls) -> None: except ImportError: raise unittest.SkipTest("Pillow not installed") try: - from ohbm2026.book.render_markdown import ( + from abstractatlas.book.render_markdown import ( _copy_figure, _figure_normalise_fallbacks, get_normalise_fallbacks, diff --git a/tests/test_book_html_to_md.py b/tests/test_book_html_to_md.py index 96b29e08..002cfe24 100644 --- a/tests/test_book_html_to_md.py +++ b/tests/test_book_html_to_md.py @@ -4,7 +4,7 @@ import unittest -from ohbm2026.book.html_to_md import html_to_pandoc_md +from abstractatlas.book.html_to_md import html_to_pandoc_md class TestHtmlToMd(unittest.TestCase): @@ -109,14 +109,14 @@ class TestCaretSuperscriptToLatex(unittest.TestCase): """ def test_simple_caret_to_textsuperscript(self) -> None: - from ohbm2026.book.html_to_md import normalise_for_latex + from abstractatlas.book.html_to_md import normalise_for_latex self.assertIn("\\textsuperscript{3}", normalise_for_latex("4 mm^3^")) self.assertIn("\\textsuperscript{1,2}", normalise_for_latex("Doe^1,2^")) def test_caret_survives_math_span_adjacency(self) -> None: # The exact pattern that broke Stage 11.1: `$\times$3 mm^3^`. - from ohbm2026.book.html_to_md import normalise_for_latex + from abstractatlas.book.html_to_md import normalise_for_latex out = normalise_for_latex("3$\\times$3$\\times$4 mm^3^;") # No bare `^3^` remains — pandoc's math-mode parser can't @@ -128,13 +128,13 @@ def test_escaped_caret_not_touched(self) -> None: # Already-escaped `\^X^` (rare; usually authors who want a # literal caret) should NOT be re-converted. The regex uses # a negative lookbehind on `\\`. - from ohbm2026.book.html_to_md import normalise_for_latex + from abstractatlas.book.html_to_md import normalise_for_latex out = normalise_for_latex("a \\^x^ b") self.assertNotIn("\\textsuperscript{x}", out) def test_idempotent(self) -> None: - from ohbm2026.book.html_to_md import normalise_for_latex + from abstractatlas.book.html_to_md import normalise_for_latex once = normalise_for_latex("k^c^") twice = normalise_for_latex(once) @@ -150,25 +150,25 @@ class TestStripControlChars(unittest.TestCase): """ def test_c0_stripped(self) -> None: - from ohbm2026.book.html_to_md import normalise_for_latex + from abstractatlas.book.html_to_md import normalise_for_latex out = normalise_for_latex("Nat\x02ural Science Foundation") self.assertEqual(out, "Natural Science Foundation") def test_vertical_tab_stripped(self) -> None: - from ohbm2026.book.html_to_md import normalise_for_latex + from abstractatlas.book.html_to_md import normalise_for_latex out = normalise_for_latex("Brain Korea 21 Project,\x0bYonsei") self.assertEqual(out, "Brain Korea 21 Project,Yonsei") def test_newline_tab_preserved(self) -> None: - from ohbm2026.book.html_to_md import normalise_for_latex + from abstractatlas.book.html_to_md import normalise_for_latex out = normalise_for_latex("line1\nline2\tcol2") self.assertEqual(out, "line1\nline2\tcol2") def test_zero_width_stripped(self) -> None: - from ohbm2026.book.html_to_md import normalise_for_latex + from abstractatlas.book.html_to_md import normalise_for_latex # ZWSP (U+200B), BOM (U+FEFF), soft hyphen (U+00AD) out = normalise_for_latex("foo​barbaz­qux") @@ -184,7 +184,7 @@ class TestEscapeBareAmpersand(unittest.TestCase): """ def test_bare_amp_escaped(self) -> None: - from ohbm2026.book.html_to_md import normalise_for_latex + from abstractatlas.book.html_to_md import normalise_for_latex self.assertEqual(normalise_for_latex("H&Y staging"), "H\\&Y staging") self.assertEqual( @@ -192,13 +192,13 @@ def test_bare_amp_escaped(self) -> None: ) def test_already_escaped_not_doubled(self) -> None: - from ohbm2026.book.html_to_md import normalise_for_latex + from abstractatlas.book.html_to_md import normalise_for_latex # `\&` already an escape sequence → leave it alone. self.assertEqual(normalise_for_latex("R\\&D dept"), "R\\&D dept") def test_html_entities_not_escaped(self) -> None: - from ohbm2026.book.html_to_md import normalise_for_latex + from abstractatlas.book.html_to_md import normalise_for_latex # `&` `<` etc. — pandoc decodes these; don't pre-escape. self.assertEqual(normalise_for_latex("a & b"), "a & b") @@ -208,7 +208,7 @@ def test_html_entities_not_escaped(self) -> None: ) def test_idempotent(self) -> None: - from ohbm2026.book.html_to_md import normalise_for_latex + from abstractatlas.book.html_to_md import normalise_for_latex once = normalise_for_latex("H&Y and R\\&D") twice = normalise_for_latex(once) @@ -225,7 +225,7 @@ class TestDefangUnknownCommands(unittest.TestCase): """ def test_unknown_capitalised_defanged(self) -> None: - from ohbm2026.book.html_to_md import normalise_for_latex + from abstractatlas.book.html_to_md import normalise_for_latex out = normalise_for_latex( "\\textsuperscript{3} \\State Key Laboratory of Brain Cognition" @@ -236,7 +236,7 @@ def test_unknown_capitalised_defanged(self) -> None: self.assertIn("\\textsuperscript{3}", out) def test_unknown_single_letter_defanged(self) -> None: - from ohbm2026.book.html_to_md import normalise_for_latex + from abstractatlas.book.html_to_md import normalise_for_latex out = normalise_for_latex("Grant DHF\\R1\\textbackslash241022") self.assertNotIn("\\R1", out) @@ -245,7 +245,7 @@ def test_unknown_single_letter_defanged(self) -> None: self.assertIn("\\textbackslash", out) def test_known_command_preserved(self) -> None: - from ohbm2026.book.html_to_md import normalise_for_latex + from abstractatlas.book.html_to_md import normalise_for_latex # Greek letters, math operators — all whitelisted. out = normalise_for_latex("\\alpha + \\pm = \\Delta") @@ -254,7 +254,7 @@ def test_known_command_preserved(self) -> None: self.assertIn("\\Delta", out) def test_command_with_braces_left_alone(self) -> None: - from ohbm2026.book.html_to_md import normalise_for_latex + from abstractatlas.book.html_to_md import normalise_for_latex # `\command{...}` looks like a real call — let LaTeX decide # rather than mangling the call. diff --git a/tests/test_book_margins.py b/tests/test_book_margins.py index fc3167ce..a82b744c 100644 --- a/tests/test_book_margins.py +++ b/tests/test_book_margins.py @@ -12,7 +12,7 @@ from importlib import resources -_TEMPLATES = resources.files("ohbm2026.book.templates") +_TEMPLATES = resources.files("abstractatlas.book.templates") class TestGeometryPreamble(unittest.TestCase): @@ -52,7 +52,7 @@ class TestHeaderIncludesPathSelection(unittest.TestCase): @classmethod def setUpClass(cls) -> None: try: - from ohbm2026.book.render_via_pandoc import _header_includes_path + from abstractatlas.book.render_via_pandoc import _header_includes_path except ImportError as exc: raise unittest.SkipTest(f"_header_includes_path not yet implemented: {exc}") cls._select = staticmethod(_header_includes_path) @@ -79,7 +79,7 @@ class TestCliMarginsFlag(unittest.TestCase): @classmethod def setUpClass(cls) -> None: try: - from ohbm2026.book.cli import _build_parser + from abstractatlas.book.cli import _build_parser except ImportError as exc: raise unittest.SkipTest(f"book CLI not importable: {exc}") cls._build_parser = staticmethod(_build_parser) diff --git a/tests/test_book_markdown.py b/tests/test_book_markdown.py index cd74b1db..cf2dc25a 100644 --- a/tests/test_book_markdown.py +++ b/tests/test_book_markdown.py @@ -12,7 +12,7 @@ def _load_book(): - from ohbm2026.book.corpus import load_book + from abstractatlas.book.corpus import load_book return load_book( corpus_path=_FIX / "abstracts.json", @@ -24,9 +24,9 @@ def _load_book(): def _emit(outdir: pathlib.Path): try: - from ohbm2026.book.author_index import build_author_index - from ohbm2026.book.render_markdown import emit_book_md - from ohbm2026.book.sort import by_poster_id + from abstractatlas.book.author_index import build_author_index + from abstractatlas.book.render_markdown import emit_book_md + from abstractatlas.book.sort import by_poster_id except ImportError: return None book = _load_book() @@ -128,10 +128,10 @@ class TestFigureResize(unittest.TestCase): def setUp(self) -> None: try: from dataclasses import replace - from ohbm2026.book.author_index import build_author_index - from ohbm2026.book.corpus import load_book - from ohbm2026.book.render_markdown import emit_book_md - from ohbm2026.book.sort import by_poster_id + from abstractatlas.book.author_index import build_author_index + from abstractatlas.book.corpus import load_book + from abstractatlas.book.render_markdown import emit_book_md + from abstractatlas.book.sort import by_poster_id except ImportError: self.skipTest("renderers not yet implemented") self.replace = replace @@ -159,7 +159,7 @@ def test_resize_when_max_width_below_cap(self) -> None: # confirms the operator is OK with up to 1000 px; the # effective cap is min(975, 1000) = 975. from PIL import Image as _Image - from ohbm2026.book.render_markdown import FIGURE_WIDTH_CAP + from abstractatlas.book.render_markdown import FIGURE_WIDTH_CAP with tempfile.TemporaryDirectory() as tmp: out = pathlib.Path(tmp) @@ -173,7 +173,7 @@ def test_resize_when_max_width_below_cap(self) -> None: def test_max_width_none_uses_default_cap(self) -> None: # Stage 12: max_image_width=None → use the default 975 px cap. from PIL import Image as _Image - from ohbm2026.book.render_markdown import FIGURE_WIDTH_CAP + from abstractatlas.book.render_markdown import FIGURE_WIDTH_CAP with tempfile.TemporaryDirectory() as tmp: out = pathlib.Path(tmp) @@ -186,7 +186,7 @@ def test_max_width_above_cap_still_capped(self) -> None: # Stage 12: max_image_width=5000 still caps at 975 px (the # operator-supplied value can only tighten the cap, never loosen). from PIL import Image as _Image - from ohbm2026.book.render_markdown import FIGURE_WIDTH_CAP + from abstractatlas.book.render_markdown import FIGURE_WIDTH_CAP with tempfile.TemporaryDirectory() as tmp: out = pathlib.Path(tmp) diff --git a/tests/test_book_no_ai_audit.py b/tests/test_book_no_ai_audit.py index cd9ea9ad..02a0cca7 100644 --- a/tests/test_book_no_ai_audit.py +++ b/tests/test_book_no_ai_audit.py @@ -1,9 +1,9 @@ """T013 — SC-006: no AI-generated content reaches the book. Two-pronged check: -1. *By construction* — walk the import graph of every `ohbm2026.book.*` - module and assert nothing under `ohbm2026.stage2_*` / - `ohbm2026.enrich*` is reachable. +1. *By construction* — walk the import graph of every `abstractatlas.book.*` + module and assert nothing under `abstractatlas.stage2_*` / + `abstractatlas.enrich*` is reachable. 2. *In the output* — after a `book.md` is rendered, grep the bytes for every ECO code in `src/ohbm2026/data/eco_top_codes.json` and the Stage-2 agent-tool names; assert zero matches. @@ -21,16 +21,16 @@ import unittest _FIX = pathlib.Path(__file__).parent / "fixtures" / "book" -_BOOK_PKG_ROOT = pathlib.Path(__file__).parent.parent / "src" / "ohbm2026" / "book" +_BOOK_PKG_ROOT = pathlib.Path(__file__).parent.parent / "src" / "abstractatlas" / "book" def _collect_book_imports() -> set[str]: """Walk every .py file in `src/ohbm2026/book/` and return the set - of `from ohbm2026.X[.Y]` and `import ohbm2026.X[.Y]` module + of `from abstractatlas.X[.Y]` and `import abstractatlas.X[.Y]` module references — i.e. the SOURCE-level dependency surface. Static scan is sufficient: if no book source mentions an - `ohbm2026.stage2_*` or `ohbm2026.enrich*` import, then the + `abstractatlas.stage2_*` or `abstractatlas.enrich*` import, then the transitive runtime graph can't include them. Stays independent of test ordering and global `sys.modules` pollution. """ @@ -50,7 +50,7 @@ def _collect_book_imports() -> set[str]: class TestImportGraph(unittest.TestCase): def test_no_stage2_or_enrich_in_source(self) -> None: imports = _collect_book_imports() - forbidden_prefixes = ("ohbm2026.stage2_", "ohbm2026.enrich") + forbidden_prefixes = ("abstractatlas.stage2_", "abstractatlas.enrich") leaked = sorted( m for m in imports @@ -75,8 +75,8 @@ class TestNoStringLeakage(unittest.TestCase): def test_book_md_has_no_stage2_strings(self) -> None: try: - from ohbm2026.book.corpus import load_book - from ohbm2026.book.render_markdown import emit_book_md + from abstractatlas.book.corpus import load_book + from abstractatlas.book.render_markdown import emit_book_md except ImportError: self.skipTest("render_markdown not yet implemented") @@ -96,7 +96,7 @@ def test_book_md_has_no_stage2_strings(self) -> None: eco_path = ( pathlib.Path(__file__).parent.parent / "src" - / "ohbm2026" + / "abstractatlas" / "data" / "eco_top_codes.json" ) diff --git a/tests/test_book_render_pdf.py b/tests/test_book_render_pdf.py index 5d0c7a00..4bb39d0f 100644 --- a/tests/test_book_render_pdf.py +++ b/tests/test_book_render_pdf.py @@ -26,11 +26,11 @@ def _pandoc_ok() -> bool: class TestBookRenderPdf(unittest.TestCase): def setUp(self) -> None: try: - from ohbm2026.book.author_index import build_author_index - from ohbm2026.book.corpus import load_book - from ohbm2026.book.render_markdown import emit_book_md - from ohbm2026.book.render_via_pandoc import to_pdf - from ohbm2026.book.sort import by_poster_id + from abstractatlas.book.author_index import build_author_index + from abstractatlas.book.corpus import load_book + from abstractatlas.book.render_markdown import emit_book_md + from abstractatlas.book.render_via_pandoc import to_pdf + from abstractatlas.book.sort import by_poster_id except ImportError: self.skipTest("renderers not yet implemented") from dataclasses import replace @@ -100,8 +100,8 @@ def test_page_count_floor(self) -> None: ) def test_pdftotext_byte_identical_on_rerun(self) -> None: - from ohbm2026.book.render_via_pandoc import to_pdf - from ohbm2026.book.render_markdown import emit_book_md + from abstractatlas.book.render_via_pandoc import to_pdf + from abstractatlas.book.render_markdown import emit_book_md with tempfile.TemporaryDirectory() as tmp2: other_dir = pathlib.Path(tmp2) diff --git a/tests/test_book_sort.py b/tests/test_book_sort.py index c7c695a8..481e000a 100644 --- a/tests/test_book_sort.py +++ b/tests/test_book_sort.py @@ -5,8 +5,8 @@ import pathlib import unittest -from ohbm2026.book.corpus import load_book -from ohbm2026.book.sort import by_first_author, by_poster_id, by_title +from abstractatlas.book.corpus import load_book +from abstractatlas.book.sort import by_first_author, by_poster_id, by_title _FIX = pathlib.Path(__file__).parent / "fixtures" / "book" diff --git a/tests/test_book_standby.py b/tests/test_book_standby.py index 36ca82ea..1d34bb13 100644 --- a/tests/test_book_standby.py +++ b/tests/test_book_standby.py @@ -11,7 +11,7 @@ import tempfile import unittest -from ohbm2026.standby import ( +from abstractatlas.standby import ( StandbyTimes as _ParserStandbyTimes, load_standby_csv, parse_window, @@ -58,7 +58,7 @@ def test_missing_header_raises(self) -> None: class TestLoadBookWithStandby(unittest.TestCase): def setUp(self) -> None: - from ohbm2026.book.corpus import load_book + from abstractatlas.book.corpus import load_book self.book = load_book( corpus_path=_FIX / "abstracts.json", @@ -94,7 +94,7 @@ def test_start_utc_iso_normalised(self) -> None: ) def test_no_standby_when_path_omitted(self) -> None: - from ohbm2026.book.corpus import load_book + from abstractatlas.book.corpus import load_book book = load_book( corpus_path=_FIX / "abstracts.json", @@ -110,10 +110,10 @@ def test_no_standby_when_path_omitted(self) -> None: class TestMarkdownStandbyLine(unittest.TestCase): def setUp(self) -> None: from dataclasses import replace - from ohbm2026.book.author_index import build_author_index - from ohbm2026.book.corpus import load_book - from ohbm2026.book.render_markdown import emit_book_md - from ohbm2026.book.sort import by_poster_id + from abstractatlas.book.author_index import build_author_index + from abstractatlas.book.corpus import load_book + from abstractatlas.book.render_markdown import emit_book_md + from abstractatlas.book.sort import by_poster_id self.tmp = tempfile.TemporaryDirectory() self.outdir = pathlib.Path(self.tmp.name) diff --git a/tests/test_book_toc.py b/tests/test_book_toc.py index aa96894e..067087af 100644 --- a/tests/test_book_toc.py +++ b/tests/test_book_toc.py @@ -17,7 +17,7 @@ class TestLatexEscape(unittest.TestCase): @classmethod def setUpClass(cls) -> None: try: - from ohbm2026.book.assemble_pdf import _latex_escape + from abstractatlas.book.assemble_pdf import _latex_escape except ImportError as exc: raise unittest.SkipTest(f"_latex_escape not yet implemented: {exc}") cls._escape = staticmethod(_latex_escape) @@ -56,8 +56,8 @@ class TestBuildTocMarkdown(unittest.TestCase): @classmethod def setUpClass(cls) -> None: try: - from ohbm2026.book.assemble_pdf import _build_toc_markdown - from ohbm2026.book.model import ( + from abstractatlas.book.assemble_pdf import _build_toc_markdown + from abstractatlas.book.model import ( Author, BookEntry, ) diff --git a/tests/test_category_evaluation.py b/tests/test_category_evaluation.py index 51f53bc8..fdd1a68a 100644 --- a/tests/test_category_evaluation.py +++ b/tests/test_category_evaluation.py @@ -5,7 +5,7 @@ import numpy as np -from ohbm2026.analyze import category_evaluation +from abstractatlas.analyze import category_evaluation def _response(parent: str, subcategory: str) -> dict[str, str]: diff --git a/tests/test_category_rollup.py b/tests/test_category_rollup.py index c282fcef..d4de47a3 100644 --- a/tests/test_category_rollup.py +++ b/tests/test_category_rollup.py @@ -3,7 +3,7 @@ import unittest from pathlib import Path -from ohbm2026.analyze import category_rollup +from abstractatlas.analyze import category_rollup def _band_row( diff --git a/tests/test_cli.py b/tests/test_cli.py index aacdbe9b..fca29ae7 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,7 +1,7 @@ import unittest from unittest import mock -from ohbm2026 import cli +from abstractatlas import cli class CLITest(unittest.TestCase): @@ -17,8 +17,8 @@ def test_refresh_assets_appends_refresh_flag(self) -> None: def test_enrich_abstracts_subcommand_delegates_to_enrich_stage_main(self) -> None: # Import the module so we can patch its `.main` attribute. This # also serves as a red-phase guard: the test fails on - # ImportError if `ohbm2026.enrich.stage` does not yet exist. - from ohbm2026.enrich import stage as enrich_stage_module + # ImportError if `abstractatlas.enrich.stage` does not yet exist. + from abstractatlas.enrich import stage as enrich_stage_module with mock.patch.object(enrich_stage_module, "main", return_value=11) as es_main: result = cli.main(["enrich-abstracts", "--enriched-output", "out.sqlite"]) @@ -30,7 +30,7 @@ def test_enrich_abstracts_accepts_every_option_from_contract(self) -> None: """contracts/cli.md enumerates every flag; the subparser must accept all of them. We assert by passing each one through and checking the delegated argv.""" - from ohbm2026.enrich import stage as enrich_stage_module + from abstractatlas.enrich import stage as enrich_stage_module argv = [ "enrich-abstracts", @@ -156,13 +156,13 @@ def test_ingest_subcommand_is_not_a_known_choice(self) -> None: class TestFetchAbstractsSubcommand(unittest.TestCase): """T011 — `ohbmcli fetch-abstracts` wires to fetch_stage.main. - Hermetic safety net: if `ohbm2026.fetch.stage` does not exist + Hermetic safety net: if `abstractatlas.fetch.stage` does not exist yet (red phase), the test fails on ImportError before any other code runs — no live API call possible. """ def test_fetch_abstracts_delegates_to_fetch_stage_main(self) -> None: - from ohbm2026.fetch import stage as fetch_stage_module + from abstractatlas.fetch import stage as fetch_stage_module with mock.patch.object(fetch_stage_module, "main", return_value=0) as fs_main: result = cli.main(["fetch-abstracts", "--allow-empty"]) @@ -184,7 +184,7 @@ class TestLegacyEnrichmentSubcommandsRemoved(unittest.TestCase): """ def test_enrich_subcommand_is_not_a_known_choice(self) -> None: - # After Stage 5 / US1, `ohbm2026.enrichment` is gone — argparse + # After Stage 5 / US1, `abstractatlas.enrichment` is gone — argparse # rejects the legacy subcommand name before any dispatch fires. with self.assertRaises(SystemExit): cli.main(["enrich"]) @@ -198,7 +198,7 @@ def test_extract_claims_subcommand_is_not_a_known_choice(self) -> None: cli.main(["extract-claims"]) def test_reference_metadata_subcommand_is_not_a_known_choice(self) -> None: - from ohbm2026.enrich import openalex as openalex_mod + from abstractatlas.enrich import openalex as openalex_mod with mock.patch.object(openalex_mod, "main", return_value=0): with self.assertRaises(SystemExit): cli.main(["reference-metadata"]) @@ -210,7 +210,7 @@ class TestFetchWithdrawnSubcommand(unittest.TestCase): automatically appended.""" def test_fetch_withdrawn_appends_corpus_kind_withdrawn(self) -> None: - from ohbm2026.fetch import stage as fetch_stage_module + from abstractatlas.fetch import stage as fetch_stage_module with mock.patch.object(fetch_stage_module, "main", return_value=0) as fs_main: result = cli.main(["fetch-withdrawn", "--allow-empty"]) @@ -221,7 +221,7 @@ def test_fetch_withdrawn_appends_corpus_kind_withdrawn(self) -> None: def test_fetch_withdrawn_respects_explicit_corpus_kind(self) -> None: # If the operator explicitly passes --corpus-kind, we do NOT # override it (defensive — supports future read-only modes). - from ohbm2026.fetch import stage as fetch_stage_module + from abstractatlas.fetch import stage as fetch_stage_module with mock.patch.object(fetch_stage_module, "main", return_value=0) as fs_main: result = cli.main(["fetch-withdrawn", "--corpus-kind", "accepted"]) diff --git a/tests/test_cluster_palette.py b/tests/test_cluster_palette.py index 66ec1603..4e14276a 100644 --- a/tests/test_cluster_palette.py +++ b/tests/test_cluster_palette.py @@ -1,4 +1,4 @@ -"""Tests for ``ohbm2026.atlas_package.cluster_palette``. +"""Tests for ``abstractatlas.atlas_package.cluster_palette``. Spec: ``specs/015-neuroscape-context/`` — FR-010 + research R-003. @@ -17,7 +17,7 @@ import unittest -from ohbm2026.atlas_package import cluster_palette +from abstractatlas.atlas_package import cluster_palette class AssignPaletteShapeTests(unittest.TestCase): diff --git a/tests/test_compose_recipe.py b/tests/test_compose_recipe.py index d561ea57..0ea5a032 100644 --- a/tests/test_compose_recipe.py +++ b/tests/test_compose_recipe.py @@ -13,9 +13,9 @@ import numpy as np -from ohbm2026.embed import compose as embed_compose -from ohbm2026.embed import storage as embed_storage -from ohbm2026.exceptions import EmbeddingError +from abstractatlas.embed import compose as embed_compose +from abstractatlas.embed import storage as embed_storage +from abstractatlas.exceptions import EmbeddingError class _Tmp: diff --git a/tests/test_decimation.py b/tests/test_decimation.py index d25648ad..38dd5edc 100644 --- a/tests/test_decimation.py +++ b/tests/test_decimation.py @@ -1,4 +1,4 @@ -"""Tests for ``ohbm2026.atlas_package.decimation``. +"""Tests for ``abstractatlas.atlas_package.decimation``. Spec: ``specs/015-neuroscape-context/`` — research R-011 (decimation + mobile fallback strategy). @@ -20,7 +20,7 @@ import numpy as np -from ohbm2026.atlas_package import decimation +from abstractatlas.atlas_package import decimation class StratifiedSampleShapeTests(unittest.TestCase): diff --git a/tests/test_derive_neuroscape_centroids.py b/tests/test_derive_neuroscape_centroids.py index ddd4da75..a81873be 100644 --- a/tests/test_derive_neuroscape_centroids.py +++ b/tests/test_derive_neuroscape_centroids.py @@ -245,7 +245,7 @@ def test_version_derived_from_grouped_vectors(self) -> None: def test_load_centroid_table_round_trip(self) -> None: """Derived file + sidecar must be readable by load_centroid_table.""" - from ohbm2026.analyze.centroids import load_centroid_table + from abstractatlas.analyze.centroids import load_centroid_table with _isolated_cwd() as tmp: input_root = tmp / "ns" diff --git a/tests/test_docx_retirement.py b/tests/test_docx_retirement.py index a6b67757..65fcaecb 100644 --- a/tests/test_docx_retirement.py +++ b/tests/test_docx_retirement.py @@ -25,7 +25,7 @@ class TestDocxRejection(unittest.TestCase): def setUp(self) -> None: try: - from ohbm2026.book.cli import main # noqa: F401 + from abstractatlas.book.cli import main # noqa: F401 except ImportError as exc: self.skipTest(f"book CLI not importable: {exc}") self.tmp = tempfile.TemporaryDirectory() @@ -42,7 +42,7 @@ def tearDown(self) -> None: def _run(self, argv: list[str]) -> tuple[int, str, str]: """Invoke `book.cli.main` and capture (exit_code, stdout, stderr).""" - from ohbm2026.book import cli as book_cli + from abstractatlas.book import cli as book_cli out_buf = io.StringIO() err_buf = io.StringIO() diff --git a/tests/test_eco_vocabulary.py b/tests/test_eco_vocabulary.py index 3ef81514..05dda3cc 100644 --- a/tests/test_eco_vocabulary.py +++ b/tests/test_eco_vocabulary.py @@ -17,7 +17,7 @@ from pathlib import Path -_VOCAB_PATH = Path(__file__).resolve().parents[1] / "src" / "ohbm2026" / "data" / "eco_top_codes.json" +_VOCAB_PATH = Path(__file__).resolve().parents[1] / "src" / "abstractatlas" / "data" / "eco_top_codes.json" _SCHEMA_PATH = ( Path(__file__).resolve().parents[1] / "specs" diff --git a/tests/test_embed_components.py b/tests/test_embed_components.py index 9ae5f960..d2a98a18 100644 --- a/tests/test_embed_components.py +++ b/tests/test_embed_components.py @@ -10,7 +10,7 @@ import unittest -from ohbm2026.embed import components as embed_components +from abstractatlas.embed import components as embed_components def _record( diff --git a/tests/test_embed_provenance.py b/tests/test_embed_provenance.py index 043c52be..9b9b71d4 100644 --- a/tests/test_embed_provenance.py +++ b/tests/test_embed_provenance.py @@ -13,8 +13,8 @@ import unittest from pathlib import Path -from ohbm2026.embed import provenance as embed_provenance -from ohbm2026.exceptions import ProvenanceError +from abstractatlas.embed import provenance as embed_provenance +from abstractatlas.exceptions import ProvenanceError def _good_payload(**overrides) -> dict: diff --git a/tests/test_embed_stage.py b/tests/test_embed_stage.py index cc61da98..ae7118ce 100644 --- a/tests/test_embed_stage.py +++ b/tests/test_embed_stage.py @@ -18,9 +18,9 @@ import numpy as np -from ohbm2026.embed import stage as embed_stage -from ohbm2026.embed import storage as embed_storage -from ohbm2026.exceptions import ( +from abstractatlas.embed import stage as embed_stage +from abstractatlas.embed import storage as embed_storage +from abstractatlas.exceptions import ( EmbeddingBudgetError, EmbeddingContractError, EmbeddingError, diff --git a/tests/test_embed_storage.py b/tests/test_embed_storage.py index 11f31377..397da454 100644 --- a/tests/test_embed_storage.py +++ b/tests/test_embed_storage.py @@ -15,8 +15,8 @@ import numpy as np -from ohbm2026.embed import storage as embed_storage -from ohbm2026.exceptions import EmbeddingContractError +from abstractatlas.embed import storage as embed_storage +from abstractatlas.exceptions import EmbeddingContractError class _Tmp: diff --git a/tests/test_enrich_markdown_render.py b/tests/test_enrich_markdown_render.py index 3a0fc9fb..d8e8b701 100644 --- a/tests/test_enrich_markdown_render.py +++ b/tests/test_enrich_markdown_render.py @@ -1,4 +1,4 @@ -"""Tests for `ohbm2026.enrich.markdown_render` (Stage 5 / US1). +"""Tests for `abstractatlas.enrich.markdown_render` (Stage 5 / US1). Covers the manuscript/section markdown builders moved out of the legacy `enrichment.py`. These helpers are consumed by Stage 2.1's claim-extraction @@ -10,9 +10,9 @@ import unittest # Warmup import to break the exceptions ↔ fetch circular cycle. -from ohbm2026.analyze import stage as _stage_warmup # noqa: F401 +from abstractatlas.analyze import stage as _stage_warmup # noqa: F401 -from ohbm2026.enrich.markdown_render import ( +from abstractatlas.enrich.markdown_render import ( SECTION_ORDER, build_claim_manuscript_markdown, build_sections_markdown, diff --git a/tests/test_enrich_stage.py b/tests/test_enrich_stage.py index 64276e1a..56688d64 100644 --- a/tests/test_enrich_stage.py +++ b/tests/test_enrich_stage.py @@ -193,7 +193,7 @@ def _patch_components( backend_availability=None, ): """Patch the three Stage 2.1 per-abstract production seams + the - backend-availability classifier on `ohbm2026.enrich.stage`. Each + backend-availability classifier on `abstractatlas.enrich.stage`. Each runner is a callable that takes a per-abstract signature and returns a `(records_list, RunSummary)` tuple. @@ -204,7 +204,7 @@ def _patch_components( behavior even though the production path runs references corpus-wide before per-abstract dispatch. """ - from ohbm2026.enrich import stage as enrich_stage + from abstractatlas.enrich import stage as enrich_stage patches = [] if figure_runner is not None: @@ -310,7 +310,7 @@ def __exit__(self, *exc_info): def _default_backend_availability(): - from ohbm2026.enrich import stage as enrich_stage + from abstractatlas.enrich import stage as enrich_stage return enrich_stage.BackendAvailability( figures_backend="openai", @@ -333,9 +333,9 @@ def _ref_count(record: dict) -> int: def _default_runners(): """Standard per-abstract runners that return deterministic synthetic `(records_list, RunSummary)` tuples for the new Stage 2.1 seams.""" - from ohbm2026.enrich import figures as stage2_figures - from ohbm2026.enrich import claims as stage2_claims - from ohbm2026.enrich import references as stage2_references + from abstractatlas.enrich import figures as stage2_figures + from abstractatlas.enrich import claims as stage2_claims + from abstractatlas.enrich import references as stage2_references figure_calls = [] claims_calls = [] @@ -415,11 +415,11 @@ def _seed_assets(tmp: Path, abstracts: list[dict]) -> None: def _run_and_capture(tmp: Path, argv: list[str], env: dict[str, str] | None = None) -> int: - from ohbm2026.enrich import stage as enrich_stage + from abstractatlas.enrich import stage as enrich_stage env = env or {"OPENAI_API_KEY": "fake-key", "OPENALEX_API": "fake-alex"} with mock.patch.dict(os.environ, env, clear=False): - with mock.patch("ohbm2026.enrich.stage.Path.cwd", return_value=tmp): + with mock.patch("abstractatlas.enrich.stage.Path.cwd", return_value=tmp): return enrich_stage.main(argv) @@ -470,7 +470,7 @@ def test_ohbm2026_api_is_not_consulted_by_stage_2(self) -> None: def test_missing_required_api_key_exits_non_zero(self) -> None: """A configured backend that needs OPENAI_API_KEY MUST refuse to run with no key (Principle VI: fail loudly, no fallback).""" - from ohbm2026.enrich import stage as enrich_stage + from abstractatlas.enrich import stage as enrich_stage tmp = self.tmp_repo() _write_source_corpus(tmp, [_abstract(1)]) @@ -481,7 +481,7 @@ def test_missing_required_api_key_exits_non_zero(self) -> None: ) with _StackedPatches(_patch_components(backend_availability=unavailable)): with mock.patch.dict(os.environ, {}, clear=True): - with mock.patch("ohbm2026.enrich.stage.Path.cwd", return_value=tmp): + with mock.patch("abstractatlas.enrich.stage.Path.cwd", return_value=tmp): rc = enrich_stage.main([]) self.assertNotEqual(rc, 0) @@ -569,7 +569,7 @@ def test_corpus_metadata_table_reflects_run(self) -> None: rc = _run_and_capture(tmp, []) self.assertEqual(rc, 0) - from ohbm2026.enrich import storage as enrich_storage + from abstractatlas.enrich import storage as enrich_storage meta = enrich_storage.corpus_metadata(tmp / "data" / "primary" / "abstracts_enriched.sqlite") self.assertEqual(meta["storage_version"], enrich_storage.STORAGE_VERSION) self.assertEqual(meta["corpus_kind"], "accepted") @@ -663,7 +663,7 @@ def test_components_block_has_three_entries(self) -> None: class ErrorContractTests(_RepoFixture): def test_figure_failure_threshold_exceeded_exits_five(self) -> None: - from ohbm2026 import exceptions as exc_mod # noqa: F401 + from abstractatlas import exceptions as exc_mod # noqa: F401 tmp = self.tmp_repo() abstracts = [_abstract(i) for i in range(1, 11)] @@ -682,7 +682,7 @@ def failing_figure(*_a, **_kw): self.assertEqual(rc, 5) def test_claim_failure_threshold_exceeded_exits_five(self) -> None: - from ohbm2026 import exceptions as exc_mod + from abstractatlas import exceptions as exc_mod tmp = self.tmp_repo() abstracts = [_abstract(i) for i in range(1, 11)] @@ -702,7 +702,7 @@ def failing_claims(*_a, **_kw): def test_generic_enrichment_error_below_threshold_completes(self) -> None: """Per-record failures BELOW threshold are tolerated (FR-010).""" - from ohbm2026 import exceptions as exc_mod + from abstractatlas import exceptions as exc_mod tmp = self.tmp_repo() abstracts = [_abstract(i) for i in range(1, 21)] @@ -737,7 +737,7 @@ def test_absolute_export_parquet_path_raises_provenance_boundary(self) -> None: self.assertEqual(rc, 4) def test_cache_version_mismatch_exits_seven(self) -> None: - from ohbm2026.enrich import storage as enrich_storage + from abstractatlas.enrich import storage as enrich_storage tmp = self.tmp_repo() abstracts = [_abstract(1)] @@ -754,7 +754,7 @@ def test_cache_version_mismatch_exits_seven(self) -> None: # Approach: write a wildcard entry that the orchestrator # tries to load OR provide a config-level cache_version # override. We use the latter via a hidden test seam. - from ohbm2026.enrich import stage as enrich_stage + from abstractatlas.enrich import stage as enrich_stage fr, cr, rr, *_ = _default_runners() patches = _patch_components( @@ -783,7 +783,7 @@ def test_partial_run_aborts_without_writing_sqlite_then_resumes_using_cache(self """SC-009: start a run; raise mid-loop; no partial enriched corpus on disk. Re-invoke; the second run reuses every already-cached entry.""" - from ohbm2026 import exceptions as exc_mod + from abstractatlas import exceptions as exc_mod tmp = self.tmp_repo() abstracts = [_abstract(i) for i in range(1, 11)] @@ -850,7 +850,7 @@ def figure_runner_clean(abstract, **kw): class DiscoveryContractTests(_RepoFixture): def test_malformed_figure_response_raises_enrichment_error(self) -> None: """CA-007: LLM-response schema drift surfaces loudly.""" - from ohbm2026 import exceptions as exc_mod + from abstractatlas import exceptions as exc_mod tmp = self.tmp_repo() _write_source_corpus(tmp, [_abstract(1)]) @@ -871,7 +871,7 @@ def malformed_figure(*_a, **_kw): self.assertEqual(rc, 5) def test_classify_backend_availability_returns_typed_dataclass(self) -> None: - from ohbm2026.enrich import stage as enrich_stage + from abstractatlas.enrich import stage as enrich_stage result = enrich_stage._classify_backend_availability( env={"OPENAI_API_KEY": "k"}, dotenv_path=None, @@ -1246,7 +1246,7 @@ def figure_runner(abstract, **kw): self.assertGreater(max_observed["n"], 1) def test_default_concurrency_is_30(self) -> None: - from ohbm2026.enrich import stage as enrich_stage + from abstractatlas.enrich import stage as enrich_stage parser = enrich_stage._build_parser() args = parser.parse_args([]) self.assertEqual(args.concurrency_figures, 30) diff --git a/tests/test_enrich_storage.py b/tests/test_enrich_storage.py index cb9a74da..4f40d5b7 100644 --- a/tests/test_enrich_storage.py +++ b/tests/test_enrich_storage.py @@ -47,7 +47,7 @@ def _record(abstract_id: int, *, payload_size: int = 800) -> dict[str, object]: class TestRoundTrip(unittest.TestCase): def test_write_then_read_one_round_trips_record_bytes(self) -> None: - from ohbm2026.enrich import storage as enrich_storage + from abstractatlas.enrich import storage as enrich_storage with TemporaryDirectory() as tmp: db_path = Path(tmp) / "out.sqlite" @@ -65,7 +65,7 @@ def test_write_then_read_one_round_trips_record_bytes(self) -> None: def test_zlib_round_trip_at_raw_blob_level(self) -> None: """The on-disk payload column is `zlib(json(record))`.""" - from ohbm2026.enrich import storage as enrich_storage + from abstractatlas.enrich import storage as enrich_storage with TemporaryDirectory() as tmp: db_path = Path(tmp) / "out.sqlite" @@ -88,7 +88,7 @@ def test_zlib_round_trip_at_raw_blob_level(self) -> None: self.assertEqual(decoded["id"], 42) def test_duplicate_id_in_same_write_raises(self) -> None: - from ohbm2026.enrich import storage as enrich_storage + from abstractatlas.enrich import storage as enrich_storage with TemporaryDirectory() as tmp: db_path = Path(tmp) / "out.sqlite" @@ -104,7 +104,7 @@ def test_duplicate_id_in_same_write_raises(self) -> None: class TestRandomByID(unittest.TestCase): def test_random_lookup_returns_correct_payloads_for_100_ids(self) -> None: - from ohbm2026.enrich import storage as enrich_storage + from abstractatlas.enrich import storage as enrich_storage ids = list(range(1, 251)) with TemporaryDirectory() as tmp: @@ -126,7 +126,7 @@ def test_random_lookup_returns_correct_payloads_for_100_ids(self) -> None: def test_random_lookup_average_latency_is_under_ten_ms(self) -> None: """SC-006: under 10 ms random by ID.""" - from ohbm2026.enrich import storage as enrich_storage + from abstractatlas.enrich import storage as enrich_storage ids = list(range(1, 251)) with TemporaryDirectory() as tmp: @@ -151,7 +151,7 @@ def test_random_lookup_average_latency_is_under_ten_ms(self) -> None: self.assertLess(avg_ms, 10.0, f"random lookup avg {avg_ms:.3f} ms exceeds SC-006 budget") def test_missing_id_returns_none(self) -> None: - from ohbm2026.enrich import storage as enrich_storage + from abstractatlas.enrich import storage as enrich_storage with TemporaryDirectory() as tmp: db_path = Path(tmp) / "out.sqlite" @@ -167,7 +167,7 @@ def test_missing_id_returns_none(self) -> None: class TestSequentialIteration(unittest.TestCase): def test_iter_enriched_yields_all_records_in_id_order(self) -> None: - from ohbm2026.enrich import storage as enrich_storage + from abstractatlas.enrich import storage as enrich_storage ids = list(range(1, 51)) with TemporaryDirectory() as tmp: @@ -189,7 +189,7 @@ def test_canonical_path_is_only_populated_on_clean_close(self) -> None: """The writer writes to a temp path; the rename only happens on successful close. An exception inside the with-block MUST leave the canonical path absent (or unchanged).""" - from ohbm2026.enrich import storage as enrich_storage + from abstractatlas.enrich import storage as enrich_storage with TemporaryDirectory() as tmp: db_path = Path(tmp) / "out.sqlite" @@ -210,7 +210,7 @@ def test_canonical_path_is_only_populated_on_clean_close(self) -> None: def test_pre_existing_canonical_file_is_preserved_on_failure(self) -> None: """A pre-existing 'previous good' SQLite file at the canonical path MUST remain untouched if the new run aborts.""" - from ohbm2026.enrich import storage as enrich_storage + from abstractatlas.enrich import storage as enrich_storage with TemporaryDirectory() as tmp: db_path = Path(tmp) / "out.sqlite" @@ -240,7 +240,7 @@ def test_pre_existing_canonical_file_is_preserved_on_failure(self) -> None: ) def test_no_stray_temp_files_after_clean_run(self) -> None: - from ohbm2026.enrich import storage as enrich_storage + from abstractatlas.enrich import storage as enrich_storage with TemporaryDirectory() as tmp: db_path = Path(tmp) / "out.sqlite" @@ -257,7 +257,7 @@ def test_no_stray_temp_files_after_clean_run(self) -> None: class TestCorpusMetadataTable(unittest.TestCase): def test_corpus_metadata_seeded_with_version_and_state_key(self) -> None: - from ohbm2026.enrich import storage as enrich_storage + from abstractatlas.enrich import storage as enrich_storage with TemporaryDirectory() as tmp: db_path = Path(tmp) / "out.sqlite" @@ -316,7 +316,7 @@ def _check_type(self, value, expected, *, path: str) -> None: self.assertIsInstance(value, type_map[expected], f"{path} type mismatch") def test_round_tripped_record_satisfies_contract_schema(self) -> None: - from ohbm2026.enrich import storage as enrich_storage + from abstractatlas.enrich import storage as enrich_storage schema = self._load_contract_schema() defs = schema["$defs"] diff --git a/tests/test_enrich_text.py b/tests/test_enrich_text.py index 48bd813f..f58e0c78 100644 --- a/tests/test_enrich_text.py +++ b/tests/test_enrich_text.py @@ -1,4 +1,4 @@ -"""Tests for `ohbm2026.enrich.text` (Stage 5 / US1). +"""Tests for `abstractatlas.enrich.text` (Stage 5 / US1). Covers `html_to_markdown` + `HTMLToMarkdownParser` — the HTML→Markdown conversion helpers moved out of the legacy `enrichment.py`. @@ -10,9 +10,9 @@ # Warmup import to break the exceptions ↔ fetch circular cycle # (same pattern as tests/test_analyze_stage.py). -from ohbm2026.analyze import stage as _stage_warmup # noqa: F401 +from abstractatlas.analyze import stage as _stage_warmup # noqa: F401 -from ohbm2026.enrich.text import HTMLToMarkdownParser, html_to_markdown +from abstractatlas.enrich.text import HTMLToMarkdownParser, html_to_markdown class HtmlToMarkdownTests(unittest.TestCase): diff --git a/tests/test_fetch_stage.py b/tests/test_fetch_stage.py index b3d84237..9d89ca61 100644 --- a/tests/test_fetch_stage.py +++ b/tests/test_fetch_stage.py @@ -98,16 +98,16 @@ def _patch_upstream( withdrawn_submission_ids: list[int] | None = None, ): """Context-manager helper: patches every upstream call in - `ohbm2026.fetch.graphql_api` AND its name-imported reference inside - `ohbm2026.assets` so fetch_stage.main() can run hermetically. + `abstractatlas.fetch.graphql_api` AND its name-imported reference inside + `abstractatlas.assets` so fetch_stage.main() can run hermetically. Both targets are patched because `assets.py` imports `fetch_abstract_content` by name (a local reference); patching only the upstream module would leave the local binding active and the test would silently hit the live endpoint. """ - from ohbm2026 import assets as assets_module - from ohbm2026.fetch import graphql_api as graphql_api + from abstractatlas import assets as assets_module + from abstractatlas.fetch import graphql_api as graphql_api introspection = introspection or _fake_introspection() submission_ids = submission_ids if submission_ids is not None else [1, 2, 3] @@ -169,23 +169,23 @@ class InputContractTests(unittest.TestCase): missing → typed error, no upstream call attempted.""" def test_missing_api_key_exits_non_zero_with_typed_error_message(self) -> None: - from ohbm2026.fetch import stage as fetch_stage + from abstractatlas.fetch import stage as fetch_stage with _run_in_tmp_repo() as tmp_name, mock.patch.dict(os.environ, {}, clear=True): tmp = Path(tmp_name) - with mock.patch("ohbm2026.fetch.stage.Path.cwd", return_value=tmp): + with mock.patch("abstractatlas.fetch.stage.Path.cwd", return_value=tmp): exit_code = fetch_stage.main(["--env-file", str(tmp / ".env-missing")]) self.assertNotEqual(exit_code, 0) def test_env_var_value_never_appears_in_provenance(self) -> None: - from ohbm2026.fetch import stage as fetch_stage + from abstractatlas.fetch import stage as fetch_stage secret = "sk-" + "x" * 32 with _run_in_tmp_repo() as tmp_name, mock.patch.dict( os.environ, {"OHBM2026_API": secret}, clear=False ), _StackedPatches(_patch_upstream(submission_ids=[1])): tmp = Path(tmp_name) - with mock.patch("ohbm2026.fetch.stage.Path.cwd", return_value=tmp): + with mock.patch("abstractatlas.fetch.stage.Path.cwd", return_value=tmp): exit_code = fetch_stage.main([]) self.assertEqual(exit_code, 0) @@ -202,13 +202,13 @@ class OutputContractTests(unittest.TestCase): are written at the expected paths; checkpoint is deleted on success.""" def test_clean_run_writes_all_three_primary_outputs_and_deletes_checkpoint(self) -> None: - from ohbm2026.fetch import stage as fetch_stage + from abstractatlas.fetch import stage as fetch_stage with _run_in_tmp_repo() as tmp_name, mock.patch.dict( os.environ, {"OHBM2026_API": "fake"}, clear=False ), _StackedPatches(_patch_upstream(submission_ids=[1, 2])): tmp = Path(tmp_name) - with mock.patch("ohbm2026.fetch.stage.Path.cwd", return_value=tmp): + with mock.patch("abstractatlas.fetch.stage.Path.cwd", return_value=tmp): exit_code = fetch_stage.main([]) self.assertEqual(exit_code, 0) @@ -228,13 +228,13 @@ class ProvenanceContractTests(unittest.TestCase): absolute or ~-prefixed paths, env-var names only.""" def test_provenance_record_has_required_fields_and_no_absolute_paths(self) -> None: - from ohbm2026.fetch import stage as fetch_stage + from abstractatlas.fetch import stage as fetch_stage with _run_in_tmp_repo() as tmp_name, mock.patch.dict( os.environ, {"OHBM2026_API": "fake"}, clear=False ), _StackedPatches(_patch_upstream(submission_ids=[1])): tmp = Path(tmp_name) - with mock.patch("ohbm2026.fetch.stage.Path.cwd", return_value=tmp): + with mock.patch("abstractatlas.fetch.stage.Path.cwd", return_value=tmp): exit_code = fetch_stage.main([]) self.assertEqual(exit_code, 0) @@ -266,7 +266,7 @@ class ErrorContractTests(unittest.TestCase): figure-failure-rate over threshold exits 5.""" def test_hard_contract_drift_exits_two_without_overwriting_corpus(self) -> None: - from ohbm2026.fetch import stage as fetch_stage + from abstractatlas.fetch import stage as fetch_stage with _run_in_tmp_repo() as tmp_name, mock.patch.dict( os.environ, {"OHBM2026_API": "fake"}, clear=False @@ -280,7 +280,7 @@ def test_hard_contract_drift_exits_two_without_overwriting_corpus(self) -> None: sentinel = corpus_path.read_text(encoding="utf-8") # First run: write a schema artifact baseline. - with mock.patch("ohbm2026.fetch.stage.Path.cwd", return_value=tmp), _StackedPatches( + with mock.patch("abstractatlas.fetch.stage.Path.cwd", return_value=tmp), _StackedPatches( _patch_upstream(submission_ids=[1]) ): first = fetch_stage.main([]) @@ -293,7 +293,7 @@ def test_hard_contract_drift_exits_two_without_overwriting_corpus(self) -> None: # Second run: the SAME field set the live query asks for is # now missing from upstream — HARD-contract drift. broken_intro = {"queryType": {"name": "Query"}, "types": []} - with mock.patch("ohbm2026.fetch.stage.Path.cwd", return_value=tmp), _StackedPatches( + with mock.patch("abstractatlas.fetch.stage.Path.cwd", return_value=tmp), _StackedPatches( _patch_upstream(introspection=broken_intro, submission_ids=[1]) ): second = fetch_stage.main([]) @@ -305,24 +305,24 @@ def test_hard_contract_drift_exits_two_without_overwriting_corpus(self) -> None: ) def test_semantically_empty_corpus_exits_six_by_default(self) -> None: - from ohbm2026.fetch import stage as fetch_stage + from abstractatlas.fetch import stage as fetch_stage with _run_in_tmp_repo() as tmp_name, mock.patch.dict( os.environ, {"OHBM2026_API": "fake"}, clear=False ), _StackedPatches(_patch_upstream(submission_ids=[])): tmp = Path(tmp_name) - with mock.patch("ohbm2026.fetch.stage.Path.cwd", return_value=tmp): + with mock.patch("abstractatlas.fetch.stage.Path.cwd", return_value=tmp): exit_code = fetch_stage.main([]) self.assertEqual(exit_code, 6, "semantically empty corpus must exit code 6") def test_allow_empty_flag_permits_zero_abstract_corpus(self) -> None: - from ohbm2026.fetch import stage as fetch_stage + from abstractatlas.fetch import stage as fetch_stage with _run_in_tmp_repo() as tmp_name, mock.patch.dict( os.environ, {"OHBM2026_API": "fake"}, clear=False ), _StackedPatches(_patch_upstream(submission_ids=[])): tmp = Path(tmp_name) - with mock.patch("ohbm2026.fetch.stage.Path.cwd", return_value=tmp): + with mock.patch("abstractatlas.fetch.stage.Path.cwd", return_value=tmp): exit_code = fetch_stage.main(["--allow-empty"]) self.assertEqual(exit_code, 0) @@ -332,7 +332,7 @@ class ResumabilityContractTests(unittest.TestCase): fetches only pending records.""" def test_resume_after_mid_batch_interruption_completes_without_refetching_done_records(self) -> None: - from ohbm2026.fetch import stage as fetch_stage + from abstractatlas.fetch import stage as fetch_stage all_ids = [1, 2, 3, 4] seen_ids: list[int] = [] @@ -347,7 +347,7 @@ def recording_factory(sid: int) -> dict[str, object]: tmp = Path(tmp_name) # First run: complete normally so we have a schema baseline + # know the run's state_key. - with mock.patch("ohbm2026.fetch.stage.Path.cwd", return_value=tmp), _StackedPatches( + with mock.patch("abstractatlas.fetch.stage.Path.cwd", return_value=tmp), _StackedPatches( _patch_upstream(submission_ids=all_ids) ): first = fetch_stage.main([]) @@ -388,7 +388,7 @@ def recording_factory(sid: int) -> dict[str, object]: # Second run (resume): record what upstream IDs were requested # via the recording content factory. seen_ids.clear() - with mock.patch("ohbm2026.fetch.stage.Path.cwd", return_value=tmp), _StackedPatches( + with mock.patch("abstractatlas.fetch.stage.Path.cwd", return_value=tmp), _StackedPatches( _patch_upstream(submission_ids=all_ids, content_factory=recording_factory) ): second = fetch_stage.main([]) @@ -404,13 +404,13 @@ class AuthorIngestionTests(unittest.TestCase): for the withdrawn corpus). Email is dropped at normalize time.""" def test_accepted_run_writes_authors_to_data_primary_authors_json(self) -> None: - from ohbm2026.fetch import stage as fetch_stage + from abstractatlas.fetch import stage as fetch_stage with _run_in_tmp_repo() as tmp_name, mock.patch.dict( os.environ, {"OHBM2026_API": "fake"}, clear=False ), _StackedPatches(_patch_upstream(submission_ids=[1, 2])): tmp = Path(tmp_name) - with mock.patch("ohbm2026.fetch.stage.Path.cwd", return_value=tmp): + with mock.patch("abstractatlas.fetch.stage.Path.cwd", return_value=tmp): exit_code = fetch_stage.main([]) self.assertEqual(exit_code, 0) @@ -427,7 +427,7 @@ def test_accepted_run_writes_authors_to_data_primary_authors_json(self) -> None: self.assertNotIn("email", json.dumps(payload)) def test_withdrawn_run_writes_authors_to_separate_file(self) -> None: - from ohbm2026.fetch import stage as fetch_stage + from abstractatlas.fetch import stage as fetch_stage with _run_in_tmp_repo() as tmp_name, mock.patch.dict( os.environ, {"OHBM2026_API": "fake"}, clear=False @@ -435,7 +435,7 @@ def test_withdrawn_run_writes_authors_to_separate_file(self) -> None: _patch_upstream(submission_ids=[1], withdrawn_submission_ids=[99]) ): tmp = Path(tmp_name) - with mock.patch("ohbm2026.fetch.stage.Path.cwd", return_value=tmp): + with mock.patch("abstractatlas.fetch.stage.Path.cwd", return_value=tmp): exit_code = fetch_stage.main(["--corpus-kind", "withdrawn"]) self.assertEqual(exit_code, 0) @@ -449,13 +449,13 @@ def test_withdrawn_run_writes_authors_to_separate_file(self) -> None: ) def test_provenance_record_includes_authors_path_and_count(self) -> None: - from ohbm2026.fetch import stage as fetch_stage + from abstractatlas.fetch import stage as fetch_stage with _run_in_tmp_repo() as tmp_name, mock.patch.dict( os.environ, {"OHBM2026_API": "fake"}, clear=False ), _StackedPatches(_patch_upstream(submission_ids=[1])): tmp = Path(tmp_name) - with mock.patch("ohbm2026.fetch.stage.Path.cwd", return_value=tmp): + with mock.patch("abstractatlas.fetch.stage.Path.cwd", return_value=tmp): fetch_stage.main([]) provenance_path = list((tmp / "data" / "inputs").glob("abstracts_fetch_provenance__*.json"))[0] @@ -478,7 +478,7 @@ class WithdrawnCorpusKindTests(unittest.TestCase): artifacts too.""" def test_withdrawn_run_writes_separate_corpus_file(self) -> None: - from ohbm2026.fetch import stage as fetch_stage + from abstractatlas.fetch import stage as fetch_stage with _run_in_tmp_repo() as tmp_name, mock.patch.dict( os.environ, {"OHBM2026_API": "fake"}, clear=False @@ -486,7 +486,7 @@ def test_withdrawn_run_writes_separate_corpus_file(self) -> None: _patch_upstream(submission_ids=[10, 20], withdrawn_submission_ids=[99, 88]) ): tmp = Path(tmp_name) - with mock.patch("ohbm2026.fetch.stage.Path.cwd", return_value=tmp): + with mock.patch("abstractatlas.fetch.stage.Path.cwd", return_value=tmp): exit_code = fetch_stage.main(["--corpus-kind", "withdrawn"]) self.assertEqual(exit_code, 0) @@ -507,7 +507,7 @@ def test_withdrawn_run_writes_separate_corpus_file(self) -> None: self.assertEqual(ids, {99, 88}, "withdrawn corpus must contain the withdrawn IDs only") def test_accepted_and_withdrawn_have_different_state_keys(self) -> None: - from ohbm2026.fetch import stage as fetch_stage + from abstractatlas.fetch import stage as fetch_stage accepted_args = fetch_stage._build_parser().parse_args([]) withdrawn_args = fetch_stage._build_parser().parse_args(["--corpus-kind", "withdrawn"]) @@ -528,9 +528,9 @@ class DiscoveryContractTests(unittest.TestCase): mismatched bound_schema_hash refuses to resume silently.""" def test_introspection_call_happens_before_content_calls(self) -> None: - from ohbm2026 import assets as assets_module - from ohbm2026.fetch import stage as fetch_stage - from ohbm2026.fetch import graphql_api as graphql_api + from abstractatlas import assets as assets_module + from abstractatlas.fetch import stage as fetch_stage + from abstractatlas.fetch import graphql_api as graphql_api call_order: list[str] = [] @@ -558,7 +558,7 @@ def author_recorder(api_key, ids, **kw): mock.patch.object(assets_module, "fetch_abstract_content", side_effect=content_recorder), \ mock.patch.object(graphql_api, "fetch_author_details", side_effect=author_recorder): tmp = Path(tmp_name) - with mock.patch("ohbm2026.fetch.stage.Path.cwd", return_value=tmp): + with mock.patch("abstractatlas.fetch.stage.Path.cwd", return_value=tmp): exit_code = fetch_stage.main([]) self.assertEqual(exit_code, 0) self.assertEqual(call_order[0], "introspection") @@ -569,14 +569,14 @@ def author_recorder(api_key, ids, **kw): self.assertLess(call_order.index("content"), call_order.index("authors")) def test_checkpoint_with_mismatched_schema_hash_refuses_to_resume_silently(self) -> None: - from ohbm2026.fetch import stage as fetch_stage + from abstractatlas.fetch import stage as fetch_stage with _run_in_tmp_repo() as tmp_name, mock.patch.dict( os.environ, {"OHBM2026_API": "fake"}, clear=False ): tmp = Path(tmp_name) # Land a fresh schema artifact + state-key. - with mock.patch("ohbm2026.fetch.stage.Path.cwd", return_value=tmp), _StackedPatches( + with mock.patch("abstractatlas.fetch.stage.Path.cwd", return_value=tmp), _StackedPatches( _patch_upstream(submission_ids=[1]) ): first = fetch_stage.main([]) @@ -608,7 +608,7 @@ def test_checkpoint_with_mismatched_schema_hash_refuses_to_resume_silently(self) # Resume attempt without explicit allow flag: MUST fail. (tmp / "data" / "primary" / "abstracts.json").unlink(missing_ok=True) - with mock.patch("ohbm2026.fetch.stage.Path.cwd", return_value=tmp), _StackedPatches( + with mock.patch("abstractatlas.fetch.stage.Path.cwd", return_value=tmp), _StackedPatches( _patch_upstream(submission_ids=[1]) ): second = fetch_stage.main([]) diff --git a/tests/test_flex_tier.py b/tests/test_flex_tier.py index 3a883141..fd4ed0f4 100644 --- a/tests/test_flex_tier.py +++ b/tests/test_flex_tier.py @@ -13,8 +13,8 @@ import openai -from ohbm2026.enrich import flex_tier as flex_tier -from ohbm2026.exceptions import EnrichmentError +from abstractatlas.enrich import flex_tier as flex_tier +from abstractatlas.exceptions import EnrichmentError class _FakeResponse: @@ -110,7 +110,7 @@ def test_bad_request_error_does_not_retry_and_raises_typed(self) -> None: # tier will fail identically. The wrapper must surface it as # a typed EnrichmentError on the FIRST occurrence so the # orchestrator's per-abstract failure-threshold handles it. - from ohbm2026.exceptions import EnrichmentError + from abstractatlas.exceptions import EnrichmentError calls = [] diff --git a/tests/test_graphql_api.py b/tests/test_graphql_api.py index 024cce30..b6ebd7f2 100644 --- a/tests/test_graphql_api.py +++ b/tests/test_graphql_api.py @@ -4,7 +4,7 @@ from tempfile import TemporaryDirectory from unittest import mock -from ohbm2026.fetch.graphql_api import ( +from abstractatlas.fetch.graphql_api import ( chunked, extract_value_field, is_valid_external_url, @@ -72,7 +72,7 @@ class TestIntrospectionQuery(unittest.TestCase): """ def test_introspection_query_constant_is_a_full_schema_query(self) -> None: - from ohbm2026.fetch import graphql_api as graphql_api + from abstractatlas.fetch import graphql_api as graphql_api query = getattr(graphql_api, "INTROSPECTION_QUERY", None) self.assertIsNotNone(query, "graphql_api must define INTROSPECTION_QUERY") @@ -82,7 +82,7 @@ def test_introspection_query_constant_is_a_full_schema_query(self) -> None: self.assertIn("queryType", query) def test_fetch_schema_introspection_returns_data_schema_block(self) -> None: - from ohbm2026.fetch import graphql_api as graphql_api + from abstractatlas.fetch import graphql_api as graphql_api expected_schema = { "queryType": {"name": "Query"}, @@ -104,8 +104,8 @@ class TestIntrospectionRetry(unittest.TestCase): exhaustion raises GraphQLAPIError.""" def test_exhausted_retries_raise_graphqlapierror(self) -> None: - from ohbm2026.fetch import graphql_api as graphql_api - from ohbm2026.fetch.graphql_api import GraphQLAPIError + from abstractatlas.fetch import graphql_api as graphql_api + from abstractatlas.fetch.graphql_api import GraphQLAPIError # urlopen_with_retries already implements the retry loop; if it # gives up, it re-raises the underlying error. Simulate that by @@ -128,7 +128,7 @@ class TestPosterIdRequested(unittest.TestCase): """ def test_abstract_contents_query_requests_program_code(self) -> None: - from ohbm2026.fetch.graphql_api import ABSTRACT_CONTENTS_QUERY + from abstractatlas.fetch.graphql_api import ABSTRACT_CONTENTS_QUERY self.assertIn( "program_code", @@ -146,7 +146,7 @@ class TestWithdrawnIdsQuery(unittest.TestCase): don't appear.""" def test_withdrawn_ids_query_filters_on_decision_status_withdrawn(self) -> None: - from ohbm2026.fetch.graphql_api import WITHDRAWN_IDS_QUERY + from abstractatlas.fetch.graphql_api import WITHDRAWN_IDS_QUERY self.assertIn('decision_status: {_eq: "Withdrawn"}', WITHDRAWN_IDS_QUERY) self.assertIn("complete: {_eq: true}", WITHDRAWN_IDS_QUERY) @@ -154,12 +154,12 @@ def test_withdrawn_ids_query_filters_on_decision_status_withdrawn(self) -> None: self.assertNotIn("accepted_for", WITHDRAWN_IDS_QUERY) def test_withdrawn_ids_query_is_distinct_from_accepted_query(self) -> None: - from ohbm2026.fetch.graphql_api import ABSTRACT_IDS_QUERY, WITHDRAWN_IDS_QUERY + from abstractatlas.fetch.graphql_api import ABSTRACT_IDS_QUERY, WITHDRAWN_IDS_QUERY self.assertNotEqual(ABSTRACT_IDS_QUERY, WITHDRAWN_IDS_QUERY) def test_fetch_withdrawn_ids_returns_event_and_submission_ids(self) -> None: - from ohbm2026.fetch import graphql_api as graphql_api + from abstractatlas.fetch import graphql_api as graphql_api with mock.patch.object( graphql_api, @@ -189,13 +189,13 @@ class TestStandbyChainRequested(unittest.TestCase): once scheduling lands.""" def test_abstract_contents_query_requests_program_sessions_submissions(self) -> None: - from ohbm2026.fetch.graphql_api import ABSTRACT_CONTENTS_QUERY + from abstractatlas.fetch.graphql_api import ABSTRACT_CONTENTS_QUERY # The fetch query must traverse the junction table. self.assertIn("program_sessions_submissions", ABSTRACT_CONTENTS_QUERY) def test_abstract_contents_query_requests_session_metadata_chain(self) -> None: - from ohbm2026.fetch.graphql_api import ABSTRACT_CONTENTS_QUERY + from abstractatlas.fetch.graphql_api import ABSTRACT_CONTENTS_QUERY # Each linked program_session contributes day, location, type, # track, and the standby time window. @@ -221,13 +221,13 @@ class TestSchemaContractError(unittest.TestCase): names the affected field and the shape change.""" def test_schema_contract_error_is_a_runtime_error_and_is_named(self) -> None: - from ohbm2026.exceptions import SchemaContractError, Stage1Error + from abstractatlas.exceptions import SchemaContractError, Stage1Error self.assertTrue(issubclass(SchemaContractError, Stage1Error)) self.assertTrue(issubclass(SchemaContractError, RuntimeError)) def test_message_carries_field_and_shape_context(self) -> None: - from ohbm2026.exceptions import SchemaContractError + from abstractatlas.exceptions import SchemaContractError try: raise SchemaContractError( diff --git a/tests/test_image_quality.py b/tests/test_image_quality.py index df876928..c91eda8b 100644 --- a/tests/test_image_quality.py +++ b/tests/test_image_quality.py @@ -10,7 +10,7 @@ from PIL import Image, ImageFilter, ImageDraw -from ohbm2026.enrich import image_quality as image_quality +from abstractatlas.enrich import image_quality as image_quality def _synth_sharp(size: int = 200) -> Image.Image: diff --git a/tests/test_neighbour_index.py b/tests/test_neighbour_index.py index 89a5eab2..6804adf9 100644 --- a/tests/test_neighbour_index.py +++ b/tests/test_neighbour_index.py @@ -1,4 +1,4 @@ -"""Tests for ``ohbm2026.atlas_package.neighbour_index``. +"""Tests for ``abstractatlas.atlas_package.neighbour_index``. Spec: ``specs/015-neuroscape-context/`` — research R-008 (k-NN over NeuroScape Stage-2 vectors, k=20, cosine). @@ -19,8 +19,8 @@ import numpy as np -from ohbm2026 import exceptions -from ohbm2026.atlas_package import neighbour_index +from abstractatlas import exceptions +from abstractatlas.atlas_package import neighbour_index def _synthetic_vectors(n: int, dim: int = 64, seed: int = 0) -> np.ndarray: diff --git a/tests/test_neuroscape.py b/tests/test_neuroscape.py index fc612e83..b04bfebc 100644 --- a/tests/test_neuroscape.py +++ b/tests/test_neuroscape.py @@ -3,7 +3,7 @@ from tempfile import TemporaryDirectory from unittest import mock -from ohbm2026.analyze.clusters import ( +from abstractatlas.analyze.clusters import ( align_semantic_records, build_cluster_benchmark_parser, build_knn_graph, @@ -21,7 +21,7 @@ summarize_semantic_clusters, summarize_stage2_clusters, ) -from ohbm2026.analyze.projections import ( +from abstractatlas.analyze.projections import ( build_projection_compare_parser, build_projection_graph, build_projection_optimize_parser, @@ -36,7 +36,7 @@ umap_main, write_projection_comparison_outputs, ) -from ohbm2026.analyze.storage import ( +from abstractatlas.analyze.storage import ( DEFAULT_EMBEDDING_FIELDS, build_claim_embedding_text, build_distinct_color_map, @@ -54,7 +54,7 @@ normalize_embedding_fields, parse_string_list_value, ) -from ohbm2026.embed.neuroscape import ( +from abstractatlas.embed.neuroscape import ( apply_pretrained_stage2_main, build_apply_pretrained_stage2_parser, load_pretrained_stage2_model, @@ -870,7 +870,7 @@ def test_write_pretrained_stage2_bundle_uses_published_metadata(self) -> None: def test_projection_traces_share_legend_groups_across_methods(self) -> None: import numpy as np from plotly.subplots import make_subplots - import ohbm2026.analyze.projections as neuroscape_module + import abstractatlas.analyze.projections as neuroscape_module records = [ {"id": 1, "title": "One", "accepted_for": "Poster", "primary_topic": "MRI", "keywords": ["a"]}, @@ -1253,8 +1253,8 @@ def test_umap_main_writes_outputs(self) -> None: encoding="utf-8", ) - with mock.patch("ohbm2026.analyze.projections.compute_umap_projection", return_value=np.asarray([[0.1, 0.2], [0.3, 0.4]])), \ - mock.patch("ohbm2026.analyze.projections.write_umap_outputs") as write_umap_outputs_mock, \ + with mock.patch("abstractatlas.analyze.projections.compute_umap_projection", return_value=np.asarray([[0.1, 0.2], [0.3, 0.4]])), \ + mock.patch("abstractatlas.analyze.projections.write_umap_outputs") as write_umap_outputs_mock, \ mock.patch("builtins.print") as fake_print: result = umap_main( [ @@ -1327,13 +1327,13 @@ def test_projection_compare_main_writes_outputs(self) -> None: enriched_path.write_text(json.dumps({"abstracts": [{"id": 1}, {"id": 2}]}), encoding="utf-8") with mock.patch( - "ohbm2026.analyze.projections.compute_umap_projection", + "abstractatlas.analyze.projections.compute_umap_projection", return_value=np.asarray([[0.1, 0.2], [0.3, 0.4]], dtype=np.float32), ), mock.patch( - "ohbm2026.analyze.projections.compute_tsne_projection", + "abstractatlas.analyze.projections.compute_tsne_projection", return_value=np.asarray([[1.1, 1.2], [1.3, 1.4]], dtype=np.float32), ), mock.patch( - "ohbm2026.analyze.projections.write_projection_comparison_outputs" + "abstractatlas.analyze.projections.write_projection_comparison_outputs" ) as write_projection_comparison_outputs_mock, mock.patch("builtins.print") as fake_print: result = projection_compare_main( [ @@ -1385,7 +1385,7 @@ def test_projection_optimize_main_prints_ranked_results(self) -> None: "results": [{"method": "umap"}, {"method": "tsne"}], } with mock.patch( - "ohbm2026.analyze.projections.optimize_projection_parameters", + "abstractatlas.analyze.projections.optimize_projection_parameters", return_value=optimization_payload, ) as optimize_mock, mock.patch("builtins.print") as fake_print: result = projection_optimize_main( @@ -1432,10 +1432,10 @@ def test_apply_pretrained_stage2_main_writes_outputs(self) -> None: fake_model = object() projected = np.asarray([[0.1] * 64, [0.2] * 64], dtype=np.float32) with mock.patch( - "ohbm2026.embed.neuroscape.load_pretrained_stage2_model", + "abstractatlas.embed.neuroscape.load_pretrained_stage2_model", return_value=(fake_model, "cpu"), ) as load_model_mock, mock.patch( - "ohbm2026.embed.neuroscape.apply_stage2_model", + "abstractatlas.embed.neuroscape.apply_stage2_model", return_value=projected, ) as apply_mock, mock.patch( "builtins.print" diff --git a/tests/test_neuroscape_clusters_runner.py b/tests/test_neuroscape_clusters_runner.py index 774f4417..805905b0 100644 --- a/tests/test_neuroscape_clusters_runner.py +++ b/tests/test_neuroscape_clusters_runner.py @@ -22,16 +22,16 @@ import numpy as np # Ensure runners register on import. -import ohbm2026.analyze # noqa: F401 -from ohbm2026.analyze.centroids import STAGE2_DIM -from ohbm2026.analyze.runners import neuroscape_clusters_runner -from ohbm2026.analyze.stage import ( +import abstractatlas.analyze # noqa: F401 +from abstractatlas.analyze.centroids import STAGE2_DIM +from abstractatlas.analyze.runners import neuroscape_clusters_runner +from abstractatlas.analyze.stage import ( AnalysisConfig, KIND_RUNNERS, PlanEntry, run_matrix, ) -from ohbm2026.exceptions import AnalysisError +from abstractatlas.exceptions import AnalysisError @contextmanager @@ -181,7 +181,7 @@ def test_direct_runner_call_with_incompatible_model_raises(self) -> None: def test_checkpoint_sha_mismatch_raises(self) -> None: """Centroid metadata and Stage 3 neuroscape provenance must agree on the domain-model checkpoint SHA (FR-008 step (c)).""" - from ohbm2026.exceptions import CentroidTableVersionMismatch + from abstractatlas.exceptions import CentroidTableVersionMismatch with _isolated_cwd(): _seed_neuroscape_stage3_bundle("abc123def456", n_rows=3) diff --git a/tests/test_neuroscape_derivation.py b/tests/test_neuroscape_derivation.py index c6bc9bc6..6d9b5bc7 100644 --- a/tests/test_neuroscape_derivation.py +++ b/tests/test_neuroscape_derivation.py @@ -15,10 +15,10 @@ import numpy as np -from ohbm2026 import exceptions -from ohbm2026.embed import compose as embed_compose -from ohbm2026.embed import stage as embed_stage -from ohbm2026.embed import storage as embed_storage +from abstractatlas import exceptions +from abstractatlas.embed import compose as embed_compose +from abstractatlas.embed import stage as embed_stage +from abstractatlas.embed import storage as embed_storage class _Tmp: diff --git a/tests/test_neuroscape_loader.py b/tests/test_neuroscape_loader.py index 510b6a43..18c21dd0 100644 --- a/tests/test_neuroscape_loader.py +++ b/tests/test_neuroscape_loader.py @@ -1,4 +1,4 @@ -"""Tests for ``ohbm2026.atlas_package.neuroscape_loader``. +"""Tests for ``abstractatlas.atlas_package.neuroscape_loader``. Spec: ``specs/015-neuroscape-context/`` — research R-001 + data-model ``NeuroScapeArticle`` / ``NeuroScapeCluster`` + research R-009 @@ -20,8 +20,8 @@ import numpy as np -from ohbm2026 import exceptions -from ohbm2026.atlas_package import neuroscape_loader +from abstractatlas import exceptions +from abstractatlas.atlas_package import neuroscape_loader from tests._atlas_fixtures import ( FIXTURE_CLUSTERS_TOP_LEVEL, diff --git a/tests/test_nocd_experiments.py b/tests/test_nocd_experiments.py index f2b89542..71c3a89a 100644 --- a/tests/test_nocd_experiments.py +++ b/tests/test_nocd_experiments.py @@ -6,7 +6,7 @@ import numpy as np -from ohbm2026.layout import nocd_experiments +from abstractatlas.layout import nocd_experiments def _load_script_module(name: str): diff --git a/tests/test_ohbm2026_parquet_rename.py b/tests/test_ohbm2026_parquet_rename.py index 9b07f267..1765a079 100644 --- a/tests/test_ohbm2026_parquet_rename.py +++ b/tests/test_ohbm2026_parquet_rename.py @@ -8,7 +8,7 @@ This module enforces the byte-identity property at the Python level: -- The module-level :data:`ohbm2026.ui_data.formats.parquet_single.DEFAULT_OUTPUT_FILENAME` +- The module-level :data:`abstractatlas.ui_data.formats.parquet_single.DEFAULT_OUTPUT_FILENAME` constant equals the new name. - A real ``build_ui_data_package`` run with the parquet-single emitter produces a file at the new path (``<out>/ohbm2026.parquet``) and NOT @@ -31,8 +31,8 @@ from pathlib import Path from tempfile import TemporaryDirectory -from ohbm2026.ui_data.builder import build_ui_data_package -from ohbm2026.ui_data.formats import parquet_single +from abstractatlas.ui_data.builder import build_ui_data_package +from abstractatlas.ui_data.formats import parquet_single from tests._ui_data_fixtures import BUILD_INFO, write_fixtures diff --git a/tests/test_ohbm_projector.py b/tests/test_ohbm_projector.py index 0e6bc52c..86bfe601 100644 --- a/tests/test_ohbm_projector.py +++ b/tests/test_ohbm_projector.py @@ -1,4 +1,4 @@ -"""Tests for ``ohbm2026.atlas_package.ohbm_projector``. +"""Tests for ``abstractatlas.atlas_package.ohbm_projector``. Spec: ``specs/015-neuroscape-context/`` — research R-002 (OHBM 2026 projection into the UMAP space via ``umap.transform``) + R-009 @@ -6,7 +6,7 @@ The projector lands OHBM 2026 Stage-2 vectors into the same 2D/3D UMAP solution previously fitted on the NeuroScape corpus by -:func:`ohbm2026.atlas_package.umap_fit.fit`. Per R-002 it does NOT +:func:`abstractatlas.atlas_package.umap_fit.fit`. Per R-002 it does NOT re-fit the UMAP — it uses the fitted model's ``transform`` method. Per R-009 the projector aggregates failures across the entire OHBM @@ -22,8 +22,8 @@ import numpy as np -from ohbm2026 import exceptions -from ohbm2026.atlas_package import ohbm_projector, umap_fit +from abstractatlas import exceptions +from abstractatlas.atlas_package import ohbm_projector, umap_fit def _synthetic_vectors(n: int, dim: int = 64, seed: int = 0) -> np.ndarray: diff --git a/tests/test_openalex.py b/tests/test_openalex.py index 29cdfffa..1ac2d229 100644 --- a/tests/test_openalex.py +++ b/tests/test_openalex.py @@ -4,8 +4,8 @@ from tempfile import TemporaryDirectory from unittest.mock import patch -from ohbm2026 import artifacts -from ohbm2026.enrich.openalex import ( +from abstractatlas import artifacts +from abstractatlas.enrich.openalex import ( OPENAI_RESPONSES_API, OpenAlexError, add_query_parameter, @@ -60,7 +60,7 @@ def test_extract_reference_entries_uses_llm_split_for_concatenated_block(self) - raw = "Smith A. Interesting title. Journal. 2024. Jones B. Another title. Journal. 2023." with patch( - "ohbm2026.enrich.openalex.llm_reference_split_request", + "abstractatlas.enrich.openalex.llm_reference_split_request", return_value=[ { "reference": "Smith A. Interesting title. Journal. 2024.", @@ -88,7 +88,7 @@ def test_extract_reference_entries_falls_back_when_llm_split_is_not_lexical(self raw = "Smith A. Interesting title. Journal. 2024. Jones B. Another title. Journal. 2023." with patch( - "ohbm2026.enrich.openalex.llm_reference_split_request", + "abstractatlas.enrich.openalex.llm_reference_split_request", return_value=[ {"reference": "Invented reference one", "title": "Invented one", "doi": None}, {"reference": "Invented reference two", "title": "Invented two", "doi": None}, @@ -227,7 +227,7 @@ def test_promote_openalex_doi_lifts_doi_to_top_level(self) -> None: # (the DOI lived only in `reference["openalex"]["doi"]`). # Downstream consumers (SQLite, UI) read the top-level field, # so this is a real coverage hole — fix is to promote. - from ohbm2026.enrich.openalex import _promote_openalex_doi + from abstractatlas.enrich.openalex import _promote_openalex_doi reference = { "doi": None, "match_method": "title", @@ -242,7 +242,7 @@ def test_promote_openalex_doi_lifts_doi_to_top_level(self) -> None: def test_promote_openalex_doi_preserves_existing_top_level_doi(self) -> None: # Idempotent: never overwrite a DOI the resolver already set # (e.g., from a DOI-match path or LLM splitter). - from ohbm2026.enrich.openalex import _promote_openalex_doi + from abstractatlas.enrich.openalex import _promote_openalex_doi reference = { "doi": "10.1000/existing", "openalex": {"doi": "10.1000/openalex-says-otherwise"}, @@ -251,7 +251,7 @@ def test_promote_openalex_doi_preserves_existing_top_level_doi(self) -> None: self.assertEqual(reference["doi"], "10.1000/existing") def test_promote_openalex_doi_no_op_when_openalex_has_no_doi(self) -> None: - from ohbm2026.enrich.openalex import _promote_openalex_doi + from abstractatlas.enrich.openalex import _promote_openalex_doi reference = {"doi": None, "openalex": {"openalex_id": "https://openalex.org/W1"}} _promote_openalex_doi(reference) self.assertIsNone(reference["doi"]) @@ -261,7 +261,7 @@ def test_promote_openalex_doi_marks_doi_lookup_completed(self) -> None: # AND it has a DOI, the resolver has the authoritative # answer — a follow-up DOI batch-lookup would be redundant. # Mark the flag so subsequent phases skip the redundant call. - from ohbm2026.enrich.openalex import _promote_openalex_doi + from abstractatlas.enrich.openalex import _promote_openalex_doi reference = { "doi": None, "doi_lookup_completed": False, @@ -280,7 +280,7 @@ def test_promote_openalex_doi_marks_doi_lookup_completed(self) -> None: self.assertFalse(reference["pmid_lookup_completed"]) def test_promote_openalex_doi_lifts_pmid_and_marks_pmid_completed(self) -> None: - from ohbm2026.enrich.openalex import _promote_openalex_doi + from abstractatlas.enrich.openalex import _promote_openalex_doi reference = { "doi": None, "pmid": None, @@ -299,7 +299,7 @@ def test_promote_openalex_doi_leaves_flags_untouched_when_no_canonical_ids(self) # If OpenAlex returned neither a DOI nor a PMID, we DID NOT # get a canonical answer — the lookup flags stay False so # later phases can still attempt their own resolution. - from ohbm2026.enrich.openalex import _promote_openalex_doi + from abstractatlas.enrich.openalex import _promote_openalex_doi reference = { "doi": None, "pmid": None, @@ -358,7 +358,7 @@ def fake_urlopen(request): captured["url"] = request.full_url return DummyResponse() - with patch("ohbm2026.enrich.openalex.urlopen_with_retries", side_effect=fake_urlopen): + with patch("abstractatlas.enrich.openalex.urlopen_with_retries", side_effect=fake_urlopen): parsed = openalex_request("https://api.openalex.org/works?per-page=1") self.assertEqual(parsed, {"results": []}) @@ -438,7 +438,7 @@ def test_collect_reference_cache_uses_llm_split_for_concatenated_reference_text( request_counts = {"reference_split_requests": 0} with patch( - "ohbm2026.enrich.openalex.llm_reference_split_request", + "abstractatlas.enrich.openalex.llm_reference_split_request", return_value={ "estimated_reference_count": 2, "references": [ @@ -502,7 +502,7 @@ def test_collect_reference_cache_writes_collect_checkpoint_incrementally(self) - def test_split_reference_markdown_retries_before_fallback(self) -> None: with patch( - "ohbm2026.enrich.openalex.llm_reference_split_request", + "abstractatlas.enrich.openalex.llm_reference_split_request", side_effect=[ OpenAlexError("temporary timeout"), { @@ -534,7 +534,7 @@ def test_split_reference_markdown_retries_before_fallback(self) -> None: def test_split_reference_markdown_records_fallback_reason(self) -> None: with patch( - "ohbm2026.enrich.openalex.llm_reference_split_request", + "abstractatlas.enrich.openalex.llm_reference_split_request", side_effect=OpenAlexError("temporary timeout"), ): candidates, diagnostics = split_reference_markdown( @@ -586,8 +586,8 @@ def fake_urlopen(request): return DummyResponse() with ( - patch("ohbm2026.enrich.openalex.get_openai_api_key", return_value="test-key"), - patch("ohbm2026.enrich.openalex.urlopen_with_retries", side_effect=fake_urlopen), + patch("abstractatlas.enrich.openalex.get_openai_api_key", return_value="test-key"), + patch("abstractatlas.enrich.openalex.urlopen_with_retries", side_effect=fake_urlopen), ): references = openai_reference_split_request("1. Smith A. Interesting title. Journal. 2024.") @@ -640,7 +640,7 @@ def test_search_semantic_scholar_doi_by_title_returns_best_matching_doi(self) -> ] } - with patch("ohbm2026.enrich.openalex.semantic_scholar_request", return_value=payload): + with patch("abstractatlas.enrich.openalex.semantic_scholar_request", return_value=payload): doi, score = search_semantic_scholar_doi_by_title( "Migraine disease characterisation biomarkers and precision medicine", reference_year=2021, @@ -665,7 +665,7 @@ def test_search_semantic_scholar_doi_by_reference_returns_best_matching_doi(self ] } - with patch("ohbm2026.enrich.openalex.semantic_scholar_request", return_value=payload): + with patch("abstractatlas.enrich.openalex.semantic_scholar_request", return_value=payload): doi, score = search_semantic_scholar_doi_by_reference( "Ashina M, Terwindt GM. Migraine: disease characterisation, biomarkers, and precision medicine. The Lancet. 2021;397(10283):1496-1504.", reference_year=2021, @@ -688,7 +688,7 @@ def test_search_crossref_doi_by_title_returns_best_matching_doi(self) -> None: } } - with patch("ohbm2026.enrich.openalex.crossref_request", return_value=payload): + with patch("abstractatlas.enrich.openalex.crossref_request", return_value=payload): doi, score = search_crossref_doi_by_title( "Migraine disease characterisation biomarkers and precision medicine", reference_year=2021, @@ -731,8 +731,8 @@ def test_resolve_reference_cache_doi_discovery_finds_doi_and_matches_openalex(se with TemporaryDirectory() as temp_dir: output_path = Path(temp_dir) / "reference_metadata.json" with ( - patch("ohbm2026.enrich.openalex.search_semantic_scholar_doi_by_reference", return_value=("10.1/example", 0.93)), - patch("ohbm2026.enrich.openalex.fetch_openalex_work_by_doi", return_value=openalex_work), + patch("abstractatlas.enrich.openalex.search_semantic_scholar_doi_by_reference", return_value=("10.1/example", 0.93)), + patch("abstractatlas.enrich.openalex.fetch_openalex_work_by_doi", return_value=openalex_work), ): stats = resolve_reference_cache_doi_discovery( abstract_reference_records, @@ -777,8 +777,8 @@ def test_resolve_reference_cache_doi_discovery_does_not_fallback_to_crossref(sel with TemporaryDirectory() as temp_dir: output_path = Path(temp_dir) / "reference_metadata.json" with ( - patch("ohbm2026.enrich.openalex.search_semantic_scholar_doi_by_reference", return_value=(None, 0.42)), - patch("ohbm2026.enrich.openalex.search_crossref_doi_by_title", side_effect=AssertionError("Crossref should not be called")), + patch("abstractatlas.enrich.openalex.search_semantic_scholar_doi_by_reference", return_value=(None, 0.42)), + patch("abstractatlas.enrich.openalex.search_crossref_doi_by_title", side_effect=AssertionError("Crossref should not be called")), ): stats = resolve_reference_cache_doi_discovery( abstract_reference_records, @@ -822,7 +822,7 @@ def test_resolve_reference_cache_doi_discovery_skips_references_with_titles(self with TemporaryDirectory() as temp_dir: output_path = Path(temp_dir) / "reference_metadata.json" with patch( - "ohbm2026.enrich.openalex.search_semantic_scholar_doi_by_reference", + "abstractatlas.enrich.openalex.search_semantic_scholar_doi_by_reference", side_effect=AssertionError("Semantic Scholar should not be called when title exists"), ): stats = resolve_reference_cache_doi_discovery( @@ -1124,7 +1124,7 @@ def fake_build_reference_metadata_database(database, **kwargs): with TemporaryDirectory() as temp_dir: output_path = Path(temp_dir) / "reference_metadata.json" with patch( - "ohbm2026.enrich.openalex.build_reference_metadata_database", + "abstractatlas.enrich.openalex.build_reference_metadata_database", side_effect=fake_build_reference_metadata_database, ): merged = repair_failed_reference_splits( diff --git a/tests/test_poster_layout.py b/tests/test_poster_layout.py index 23f2beca..2ae2ca11 100644 --- a/tests/test_poster_layout.py +++ b/tests/test_poster_layout.py @@ -6,7 +6,7 @@ import csv import numpy as np -from ohbm2026.layout import poster_layout +from abstractatlas.layout import poster_layout def _response(parent: str, subcategory: str) -> dict[str, str]: diff --git a/tests/test_poster_sequencing.py b/tests/test_poster_sequencing.py index 4d5fb0d3..853d6342 100644 --- a/tests/test_poster_sequencing.py +++ b/tests/test_poster_sequencing.py @@ -5,7 +5,7 @@ import numpy as np -from ohbm2026.layout import poster_layout, poster_sequencing +from abstractatlas.layout import poster_layout, poster_sequencing def _response(parent: str, subcategory: str) -> dict[str, str]: diff --git a/tests/test_schema_diff.py b/tests/test_schema_diff.py index 6e817f61..0d77b2f8 100644 --- a/tests/test_schema_diff.py +++ b/tests/test_schema_diff.py @@ -63,7 +63,7 @@ def _list_of(inner: dict[str, object]) -> dict[str, object]: class TestFieldIndexEntry(unittest.TestCase): def test_dataclass_shape_round_trips_through_json(self) -> None: - from ohbm2026.fetch.schema_diff import FieldIndexEntry + from abstractatlas.fetch.schema_diff import FieldIndexEntry entry = FieldIndexEntry( type_name="Submission", @@ -84,7 +84,7 @@ def test_dataclass_shape_round_trips_through_json(self) -> None: self.assertEqual(restored, entry) def test_entries_are_orderable_by_type_then_field(self) -> None: - from ohbm2026.fetch.schema_diff import FieldIndexEntry + from abstractatlas.fetch.schema_diff import FieldIndexEntry unsorted = [ FieldIndexEntry("Z", "b", (), "Int", ""), @@ -98,7 +98,7 @@ def test_entries_are_orderable_by_type_then_field(self) -> None: class TestFlattenIntrospection(unittest.TestCase): def test_unwraps_non_null_and_list_into_wrapping_kinds(self) -> None: - from ohbm2026.fetch.schema_diff import flatten_introspection + from abstractatlas.fetch.schema_diff import flatten_introspection # Submission.id : NON_NULL Int # Submission.authors : NON_NULL LIST NON_NULL Author @@ -128,7 +128,7 @@ def test_unwraps_non_null_and_list_into_wrapping_kinds(self) -> None: self.assertEqual(as_dict[("Submission", "authors")].named_type, "Author") def test_output_is_sorted_by_type_then_field(self) -> None: - from ohbm2026.fetch.schema_diff import flatten_introspection + from abstractatlas.fetch.schema_diff import flatten_introspection raw = _introspection_with( { @@ -148,7 +148,7 @@ def test_output_is_sorted_by_type_then_field(self) -> None: def test_skips_introspection_meta_types(self) -> None: # Real introspection responses include __Schema, __Type, etc. # Those are GraphQL machinery, not corpus shape. - from ohbm2026.fetch.schema_diff import flatten_introspection + from abstractatlas.fetch.schema_diff import flatten_introspection raw = { "types": [ @@ -172,7 +172,7 @@ def test_skips_introspection_meta_types(self) -> None: class TestHashFieldIndex(unittest.TestCase): def test_is_deterministic_across_calls(self) -> None: - from ohbm2026.fetch.schema_diff import FieldIndexEntry, hash_field_index + from abstractatlas.fetch.schema_diff import FieldIndexEntry, hash_field_index entries = [ FieldIndexEntry("Submission", "id", ("NON_NULL",), "Int", ""), @@ -181,7 +181,7 @@ def test_is_deterministic_across_calls(self) -> None: self.assertEqual(hash_field_index(entries), hash_field_index(list(entries))) def test_is_invariant_to_input_order(self) -> None: - from ohbm2026.fetch.schema_diff import FieldIndexEntry, hash_field_index + from abstractatlas.fetch.schema_diff import FieldIndexEntry, hash_field_index forward = [ FieldIndexEntry("Submission", "id", ("NON_NULL",), "Int", ""), @@ -191,7 +191,7 @@ def test_is_invariant_to_input_order(self) -> None: self.assertEqual(hash_field_index(forward), hash_field_index(reverse)) def test_changes_when_a_field_changes_named_type(self) -> None: - from ohbm2026.fetch.schema_diff import FieldIndexEntry, hash_field_index + from abstractatlas.fetch.schema_diff import FieldIndexEntry, hash_field_index original = [FieldIndexEntry("Submission", "id", ("NON_NULL",), "Int", "")] renamed = [FieldIndexEntry("Submission", "id", ("NON_NULL",), "String", "")] @@ -200,7 +200,7 @@ def test_changes_when_a_field_changes_named_type(self) -> None: class TestParseHardSetFromQueries(unittest.TestCase): def test_picks_up_top_level_query_fields(self) -> None: - from ohbm2026.fetch.schema_diff import parse_hard_set_from_queries + from abstractatlas.fetch.schema_diff import parse_hard_set_from_queries query = textwrap.dedent( """ @@ -222,7 +222,7 @@ def test_picks_up_top_level_query_fields(self) -> None: self.assertIn(("Query", "submissions"), hard) def test_picks_up_nested_selection_fields(self) -> None: - from ohbm2026.fetch.schema_diff import parse_hard_set_from_queries + from abstractatlas.fetch.schema_diff import parse_hard_set_from_queries query = textwrap.dedent( """ @@ -250,7 +250,7 @@ def test_picks_up_nested_selection_fields(self) -> None: self.assertIn(("affiliations", "institution"), hard) def test_unions_across_multiple_query_texts(self) -> None: - from ohbm2026.fetch.schema_diff import parse_hard_set_from_queries + from abstractatlas.fetch.schema_diff import parse_hard_set_from_queries q1 = "query a { events { id } }" q2 = "query b { submissions { id } }" @@ -259,7 +259,7 @@ def test_unions_across_multiple_query_texts(self) -> None: self.assertIn(("Query", "submissions"), hard) def test_ignores_argument_blocks(self) -> None: - from ohbm2026.fetch.schema_diff import parse_hard_set_from_queries + from abstractatlas.fetch.schema_diff import parse_hard_set_from_queries # Arguments like `where: {complete: {_eq: true}}` MUST NOT be # parsed as selection set children. @@ -282,44 +282,44 @@ def test_ignores_argument_blocks(self) -> None: class TestCollectSoftContractFields(unittest.TestCase): def test_unions_consumed_abstract_fields_across_provided_modules(self) -> None: - from ohbm2026.fetch.schema_diff import collect_soft_contract_fields + from abstractatlas.fetch.schema_diff import collect_soft_contract_fields mod_a = types.SimpleNamespace( - __name__="ohbm2026.fake_a", + __name__="abstractatlas.fake_a", CONSUMED_ABSTRACT_FIELDS=frozenset({("Submission", "title"), ("Submission", "id")}), ) mod_b = types.SimpleNamespace( - __name__="ohbm2026.fake_b", + __name__="abstractatlas.fake_b", CONSUMED_ABSTRACT_FIELDS=frozenset({("Submission", "title"), ("Author", "id")}), ) soft = collect_soft_contract_fields([mod_a, mod_b]) - self.assertEqual(soft[("Submission", "title")], ["ohbm2026.fake_a", "ohbm2026.fake_b"]) - self.assertEqual(soft[("Submission", "id")], ["ohbm2026.fake_a"]) - self.assertEqual(soft[("Author", "id")], ["ohbm2026.fake_b"]) + self.assertEqual(soft[("Submission", "title")], ["abstractatlas.fake_a", "abstractatlas.fake_b"]) + self.assertEqual(soft[("Submission", "id")], ["abstractatlas.fake_a"]) + self.assertEqual(soft[("Author", "id")], ["abstractatlas.fake_b"]) def test_modules_without_consumed_abstract_fields_are_skipped(self) -> None: - from ohbm2026.fetch.schema_diff import collect_soft_contract_fields + from abstractatlas.fetch.schema_diff import collect_soft_contract_fields mod_a = types.SimpleNamespace( - __name__="ohbm2026.fake_a", + __name__="abstractatlas.fake_a", CONSUMED_ABSTRACT_FIELDS=frozenset({("Submission", "title")}), ) - mod_b = types.SimpleNamespace(__name__="ohbm2026.fake_b") # no declaration + mod_b = types.SimpleNamespace(__name__="abstractatlas.fake_b") # no declaration soft = collect_soft_contract_fields([mod_a, mod_b]) - self.assertEqual(soft[("Submission", "title")], ["ohbm2026.fake_a"]) + self.assertEqual(soft[("Submission", "title")], ["abstractatlas.fake_a"]) self.assertEqual(len(soft), 1) class TestCompare(unittest.TestCase): def _entry(self, type_name: str, field_name: str, named_type: str = "Int") -> object: - from ohbm2026.fetch.schema_diff import FieldIndexEntry + from abstractatlas.fetch.schema_diff import FieldIndexEntry return FieldIndexEntry(type_name, field_name, (), named_type, "") def test_removed_field_in_hard_set_classifies_as_hard(self) -> None: - from ohbm2026.fetch.schema_diff import compare + from abstractatlas.fetch.schema_diff import compare previous = [self._entry("Submission", "id"), self._entry("Submission", "title", "TitleResponse")] current = [self._entry("Submission", "id")] @@ -333,20 +333,20 @@ def test_removed_field_in_hard_set_classifies_as_hard(self) -> None: self.assertEqual(title_diff[0].change_kind, "removed") def test_removed_field_only_in_soft_set_classifies_as_soft(self) -> None: - from ohbm2026.fetch.schema_diff import compare + from abstractatlas.fetch.schema_diff import compare previous = [self._entry("Submission", "extras", "String")] current: list[object] = [] hard: set[tuple[str, str]] = set() - soft = {("Submission", "extras"): ["ohbm2026.future_consumer"]} + soft = {("Submission", "extras"): ["abstractatlas.future_consumer"]} diffs = compare(previous, current, hard_set=hard, soft_set=soft) self.assertEqual(len(diffs), 1) self.assertEqual(diffs[0].tier, "SOFT") - self.assertEqual(diffs[0].downstream_consumers, ["ohbm2026.future_consumer"]) + self.assertEqual(diffs[0].downstream_consumers, ["abstractatlas.future_consumer"]) def test_removed_field_in_neither_set_classifies_as_informational(self) -> None: - from ohbm2026.fetch.schema_diff import compare + from abstractatlas.fetch.schema_diff import compare previous = [self._entry("Submission", "unused", "String")] current: list[object] = [] @@ -359,7 +359,7 @@ def test_removed_field_in_neither_set_classifies_as_informational(self) -> None: self.assertEqual(diffs[0].downstream_consumers, []) def test_hard_takes_precedence_when_field_is_also_in_soft_set(self) -> None: - from ohbm2026.fetch.schema_diff import compare + from abstractatlas.fetch.schema_diff import compare # Field is in BOTH sets. Expected: HARD tier; downstream_consumers # still populated so operators see both signals (edge case in @@ -367,15 +367,15 @@ def test_hard_takes_precedence_when_field_is_also_in_soft_set(self) -> None: previous = [self._entry("Submission", "title", "TitleResponse")] current: list[object] = [] hard = {("Submission", "title")} - soft = {("Submission", "title"): ["ohbm2026.enrichment"]} + soft = {("Submission", "title"): ["abstractatlas.enrichment"]} diffs = compare(previous, current, hard_set=hard, soft_set=soft) self.assertEqual(len(diffs), 1) self.assertEqual(diffs[0].tier, "HARD") - self.assertEqual(diffs[0].downstream_consumers, ["ohbm2026.enrichment"]) + self.assertEqual(diffs[0].downstream_consumers, ["abstractatlas.enrichment"]) def test_type_change_on_hard_field_records_previous_and_current(self) -> None: - from ohbm2026.fetch.schema_diff import compare + from abstractatlas.fetch.schema_diff import compare previous = [self._entry("Submission", "id", "Int")] current = [self._entry("Submission", "id", "String")] @@ -394,7 +394,7 @@ def test_type_change_on_hard_field_records_previous_and_current(self) -> None: self.assertEqual(diffs[0].current["named_type"], "String") def test_added_field_classified_against_hard_then_soft(self) -> None: - from ohbm2026.fetch.schema_diff import compare + from abstractatlas.fetch.schema_diff import compare previous: list[object] = [] current = [ diff --git a/tests/test_stage20_exceptions.py b/tests/test_stage20_exceptions.py index f81536cb..aa2d4514 100644 --- a/tests/test_stage20_exceptions.py +++ b/tests/test_stage20_exceptions.py @@ -14,7 +14,7 @@ import unittest -from ohbm2026 import exceptions +from abstractatlas import exceptions class Stage20ExceptionTreeTests(unittest.TestCase): diff --git a/tests/test_stage2_claims.py b/tests/test_stage2_claims.py index 94927a2b..397b69f7 100644 --- a/tests/test_stage2_claims.py +++ b/tests/test_stage2_claims.py @@ -17,8 +17,8 @@ import unittest from unittest import mock -from ohbm2026.enrich import claims as stage2_claims -from ohbm2026.exceptions import EnrichmentError +from abstractatlas.enrich import claims as stage2_claims +from abstractatlas.exceptions import EnrichmentError # ----- Function-tool handler tests ------------------------------------- @@ -276,7 +276,7 @@ def test_empty_claims_list_is_legitimate(self) -> None: self.assertEqual(summary.claims_count, 0) def test_cache_key_includes_vocabulary_version(self) -> None: - from ohbm2026.enrich.claims import _hash_for_cache + from abstractatlas.enrich.claims import _hash_for_cache vocab = stage2_claims.load_eco_vocabulary() k1 = _hash_for_cache("manuscript", "gpt-5.4-mini", vocab["vocabulary_version"]) k2 = _hash_for_cache("manuscript", "gpt-5.4-mini", "eco.v2") diff --git a/tests/test_stage2_figures.py b/tests/test_stage2_figures.py index 8307aa50..fb1a4256 100644 --- a/tests/test_stage2_figures.py +++ b/tests/test_stage2_figures.py @@ -15,8 +15,8 @@ from pathlib import Path from PIL import Image, ImageDraw -from ohbm2026.enrich import figures as stage2_figures -from ohbm2026.exceptions import EnrichmentError +from abstractatlas.enrich import figures as stage2_figures +from abstractatlas.exceptions import EnrichmentError def _make_png(path: Path, size: tuple[int, int] = (2000, 1500), color: tuple = (200, 200, 200)) -> int: @@ -280,7 +280,7 @@ def test_context_length_exceeded_retries_with_fallback_and_marks_summary(self) - self.assertEqual(summary.fallback_model_used, "gpt-5.4") def test_no_fallback_when_unset_propagates_typed_error(self) -> None: - from ohbm2026.exceptions import ContextLengthExceededError + from abstractatlas.exceptions import ContextLengthExceededError abstract = _abstract_with_figures(1, cwd=self.fx.tmp) client = _FallbackClientFigures( [], primary_model_id="gpt-5.4-mini", fallback_model_id="gpt-5.4", diff --git a/tests/test_stage2_references.py b/tests/test_stage2_references.py index 9d9e8534..d9d2753b 100644 --- a/tests/test_stage2_references.py +++ b/tests/test_stage2_references.py @@ -7,8 +7,8 @@ import unittest -from ohbm2026.enrich import references as stage2_references -from ohbm2026.exceptions import EnrichmentError +from abstractatlas.enrich import references as stage2_references +from abstractatlas.exceptions import EnrichmentError def _abstract_with_refs(refs: list[str]) -> dict: @@ -57,7 +57,7 @@ def test_cache_key_includes_strategy_id(self) -> None: ) # Resolver returned empty; no records produced. Verify the # internal cache-key helper: - from ohbm2026.enrich.references import _cache_key + from abstractatlas.enrich.references import _cache_key self.assertNotEqual(_cache_key("ref-alpha", "v1"), _cache_key("ref-alpha", "v2")) def test_empty_references_block_returns_empty(self) -> None: diff --git a/tests/test_stage3_exceptions.py b/tests/test_stage3_exceptions.py index 79a726e4..ec44b583 100644 --- a/tests/test_stage3_exceptions.py +++ b/tests/test_stage3_exceptions.py @@ -8,7 +8,7 @@ import unittest -from ohbm2026 import exceptions +from abstractatlas import exceptions class Stage3ExceptionTreeTests(unittest.TestCase): diff --git a/tests/test_standby_schema.py b/tests/test_standby_schema.py index c47dd80c..e31bc761 100644 --- a/tests/test_standby_schema.py +++ b/tests/test_standby_schema.py @@ -30,13 +30,13 @@ class TestDeriveStandbySlots(unittest.TestCase): @classmethod def setUpClass(cls) -> None: try: - from ohbm2026.ui_data.standby_slots import ( + from abstractatlas.ui_data.standby_slots import ( build_poster_to_index_map, derive_standby_slots, ) except ImportError as exc: raise unittest.SkipTest( - f"ohbm2026.ui_data.standby_slots not yet implemented: {exc}" + f"abstractatlas.ui_data.standby_slots not yet implemented: {exc}" ) cls.derive = staticmethod(derive_standby_slots) cls.build_map = staticmethod(build_poster_to_index_map) @@ -132,7 +132,7 @@ class TestParquetSchemaVersionBump(unittest.TestCase): @classmethod def setUpClass(cls) -> None: try: - from ohbm2026.ui_data.formats import parquet_single # noqa: F401 + from abstractatlas.ui_data.formats import parquet_single # noqa: F401 except ImportError as exc: raise unittest.SkipTest(f"parquet_single module not importable: {exc}") cls.module = parquet_single diff --git a/tests/test_state_key_rename.py b/tests/test_state_key_rename.py index 9061cf42..61b3ffa2 100644 --- a/tests/test_state_key_rename.py +++ b/tests/test_state_key_rename.py @@ -24,7 +24,7 @@ class TestReadFetchStateKey(unittest.TestCase): @classmethod def setUpClass(cls) -> None: try: - from ohbm2026.artifacts import read_fetch_state_key + from abstractatlas.artifacts import read_fetch_state_key except ImportError as exc: raise unittest.SkipTest( f"read_fetch_state_key not yet implemented: {exc}" @@ -59,7 +59,7 @@ def test_fetch_stage_writes_fetch_state_key(self) -> None: import pathlib src = pathlib.Path(__file__).resolve().parents[1] - stage_py = (src / "src" / "ohbm2026" / "fetch" / "stage.py").read_text() + stage_py = (src / "src" / "abstractatlas" / "fetch" / "stage.py").read_text() # The new field name must appear in the provenance + checkpoint # emission paths. Legacy `"state_key": state_key` MUST be gone # from the EMISSION sites; the local Python variable still uses diff --git a/tests/test_titles.py b/tests/test_titles.py index 6137ab12..bc209861 100644 --- a/tests/test_titles.py +++ b/tests/test_titles.py @@ -1,7 +1,7 @@ import unittest -from ohbm2026 import artifacts -from ohbm2026.titles import build_title_modification_report, normalize_abstract_title +from abstractatlas import artifacts +from abstractatlas.titles import build_title_modification_report, normalize_abstract_title class TitleHelpersTest(unittest.TestCase): diff --git a/tests/test_ui_data_abstracts.py b/tests/test_ui_data_abstracts.py index eb153b99..22a4798a 100644 --- a/tests/test_ui_data_abstracts.py +++ b/tests/test_ui_data_abstracts.py @@ -6,7 +6,7 @@ from pathlib import Path from tempfile import TemporaryDirectory -from ohbm2026.ui_data.abstracts import build_abstracts, build_abstracts_records +from abstractatlas.ui_data.abstracts import build_abstracts, build_abstracts_records from tests._ui_data_fixtures import BUILD_INFO, write_fixtures @@ -74,7 +74,7 @@ def _records(self, tmp, research_dimensions): ) def test_dimensions_injected_into_facets(self) -> None: - from ohbm2026.ui_data.dimensions import load_research_dimensions + from abstractatlas.ui_data.dimensions import load_research_dimensions with TemporaryDirectory() as tmp: paths = write_fixtures(Path(tmp)) @@ -117,7 +117,7 @@ class TestHtmlToTextSupSub(unittest.TestCase): """ def test_sup_digits_become_unicode_superscript(self) -> None: - from ohbm2026.ui_data.abstracts import _html_to_text + from abstractatlas.ui_data.abstracts import _html_to_text out = _html_to_text("<p>reference<sup>1,2</sup>.</p>") self.assertIn("¹,²", out) @@ -125,17 +125,17 @@ def test_sup_digits_become_unicode_superscript(self) -> None: self.assertNotIn("reference1,2", out) def test_sup_single_digit(self) -> None: - from ohbm2026.ui_data.abstracts import _html_to_text + from abstractatlas.ui_data.abstracts import _html_to_text self.assertIn("mm³", _html_to_text("4 mm<sup>3</sup>")) def test_sub_digits(self) -> None: - from ohbm2026.ui_data.abstracts import _html_to_text + from abstractatlas.ui_data.abstracts import _html_to_text self.assertIn("H₂O", _html_to_text("H<sub>2</sub>O")) def test_math_delimiters_pass_through(self) -> None: - from ohbm2026.ui_data.abstracts import _html_to_text + from abstractatlas.ui_data.abstracts import _html_to_text # KaTeX picks these up client-side; the stripper leaves them # intact. @@ -146,7 +146,7 @@ def test_bare_math_autowrapped_for_katex(self) -> None: """Stage 12.2 — author-pasted raw LaTeX without `$...$` wrapping gets wrapped server-side so KaTeX can render it client-side. Poster 2094 in the real corpus was the canary.""" - from ohbm2026.ui_data.abstracts import _html_to_text + from abstractatlas.ui_data.abstracts import _html_to_text out = _html_to_text( r"<p>similarity was defined as \rho\left(s,j\right)=corr(z,j). next sentence</p>" @@ -156,7 +156,7 @@ def test_bare_math_autowrapped_for_katex(self) -> None: self.assertIn(r"$\rho\left(s,j\right)=corr(z,j)$", out) def test_bare_matrix_wrapped_for_katex(self) -> None: - from ohbm2026.ui_data.abstracts import _html_to_text + from abstractatlas.ui_data.abstracts import _html_to_text out = _html_to_text( r"<p>encoder inputs are \begin{matrix}a&b\\c&d\end{matrix} done.</p>" @@ -165,7 +165,7 @@ def test_bare_matrix_wrapped_for_katex(self) -> None: self.assertIn(r"\end{matrix}$$", out) def test_thinsp_alias_normalised(self) -> None: - from ohbm2026.ui_data.abstracts import _html_to_text + from abstractatlas.ui_data.abstracts import _html_to_text out = _html_to_text( r"<p>yields \mathrm{out}\mathrm{\thinsp} more text</p>" diff --git a/tests/test_ui_data_acknowledgments.py b/tests/test_ui_data_acknowledgments.py index 17f4e31a..0565659a 100644 --- a/tests/test_ui_data_acknowledgments.py +++ b/tests/test_ui_data_acknowledgments.py @@ -39,7 +39,7 @@ class TestAcknowledgmentsField(unittest.TestCase): @classmethod def setUpClass(cls) -> None: try: - from ohbm2026.ui_data.abstracts import iter_abstracts + from abstractatlas.ui_data.abstracts import iter_abstracts except ImportError as exc: raise unittest.SkipTest(f"abstracts module not importable: {exc}") cls.iter_abstracts = staticmethod(iter_abstracts) diff --git a/tests/test_ui_data_ai_provenance.py b/tests/test_ui_data_ai_provenance.py index ec333ac2..d25de202 100644 --- a/tests/test_ui_data_ai_provenance.py +++ b/tests/test_ui_data_ai_provenance.py @@ -25,7 +25,7 @@ import pyarrow.parquet as pq -from ohbm2026.ui_data.formats import parquet_single +from abstractatlas.ui_data.formats import parquet_single def _empty_envelope() -> dict: diff --git a/tests/test_ui_data_authors.py b/tests/test_ui_data_authors.py index 29aff836..53c909e1 100644 --- a/tests/test_ui_data_authors.py +++ b/tests/test_ui_data_authors.py @@ -6,8 +6,8 @@ from pathlib import Path from tempfile import TemporaryDirectory -from ohbm2026.ui_data.abstracts import build_abstracts_records -from ohbm2026.ui_data.authors import build_authors, build_authors_records +from abstractatlas.ui_data.abstracts import build_abstracts_records +from abstractatlas.ui_data.authors import build_authors, build_authors_records from tests._ui_data_fixtures import BUILD_INFO, write_fixtures diff --git a/tests/test_ui_data_builder.py b/tests/test_ui_data_builder.py index 1460851d..6d1589fa 100644 --- a/tests/test_ui_data_builder.py +++ b/tests/test_ui_data_builder.py @@ -8,7 +8,7 @@ from pathlib import Path from tempfile import TemporaryDirectory -from ohbm2026.ui_data.builder import build_ui_data_package +from abstractatlas.ui_data.builder import build_ui_data_package from tests._ui_data_fixtures import BUILD_INFO, write_fixtures diff --git a/tests/test_ui_data_cells.py b/tests/test_ui_data_cells.py index 76cf1c1d..0ea6031e 100644 --- a/tests/test_ui_data_cells.py +++ b/tests/test_ui_data_cells.py @@ -6,7 +6,7 @@ from pathlib import Path from tempfile import TemporaryDirectory -from ohbm2026.ui_data.cells import build_cells, build_cells_shards +from abstractatlas.ui_data.cells import build_cells, build_cells_shards from tests._ui_data_fixtures import BUILD_INFO, write_fixtures diff --git a/tests/test_ui_data_dimensions.py b/tests/test_ui_data_dimensions.py index 38804aa5..5306e405 100644 --- a/tests/test_ui_data_dimensions.py +++ b/tests/test_ui_data_dimensions.py @@ -49,7 +49,7 @@ class TestDistiller(unittest.TestCase): def test_slim_has_wrapper_and_only_four_lists(self) -> None: - from ohbm2026.ui_data.dimensions import distill_dimensions + from abstractatlas.ui_data.dimensions import distill_dimensions with TemporaryDirectory() as tmp: full = Path(tmp) / "abstracts.detail.json" @@ -74,7 +74,7 @@ def test_slim_has_wrapper_and_only_four_lists(self) -> None: self.assertEqual(dims["1003"]["theory_scope"], []) def test_distiller_is_deterministic(self) -> None: - from ohbm2026.ui_data.dimensions import distill_dimensions + from abstractatlas.ui_data.dimensions import distill_dimensions with TemporaryDirectory() as tmp: full = Path(tmp) / "abstracts.detail.json" @@ -86,7 +86,7 @@ def test_distiller_is_deterministic(self) -> None: self.assertEqual(a.read_bytes(), b.read_bytes()) def test_distiller_rejects_layout_mismatch(self) -> None: - from ohbm2026.ui_data.dimensions import DimensionInputError, distill_dimensions + from abstractatlas.ui_data.dimensions import DimensionInputError, distill_dimensions with TemporaryDirectory() as tmp: slim = Path(tmp) / "out.json" @@ -103,7 +103,7 @@ def test_distiller_rejects_layout_mismatch(self) -> None: distill_dimensions(bad2, slim) def test_distiller_raises_on_missing_file(self) -> None: - from ohbm2026.ui_data.dimensions import DimensionInputError, distill_dimensions + from abstractatlas.ui_data.dimensions import DimensionInputError, distill_dimensions with TemporaryDirectory() as tmp: with self.assertRaises(DimensionInputError): @@ -112,7 +112,7 @@ def test_distiller_raises_on_missing_file(self) -> None: class TestLoadSlim(unittest.TestCase): def test_load_returns_int_keyed_map(self) -> None: - from ohbm2026.ui_data.dimensions import distill_dimensions, load_research_dimensions + from abstractatlas.ui_data.dimensions import distill_dimensions, load_research_dimensions with TemporaryDirectory() as tmp: full = Path(tmp) / "abstracts.detail.json" @@ -127,7 +127,7 @@ def test_load_returns_int_keyed_map(self) -> None: self.assertEqual(set(dims[1001]), set(("focus", "research_modality", "theory_scope", "epistemic_basis"))) def test_load_rejects_wrong_shape(self) -> None: - from ohbm2026.ui_data.dimensions import DimensionInputError, load_research_dimensions + from abstractatlas.ui_data.dimensions import DimensionInputError, load_research_dimensions with TemporaryDirectory() as tmp: bad = Path(tmp) / "bad.json" @@ -140,7 +140,7 @@ class TestCoverage(unittest.TestCase): """Stage 23 — compute_dimension_coverage (data-model §3 / D1 / D3).""" def test_matched_plus_no_value_equals_corpus_count(self) -> None: - from ohbm2026.ui_data.dimensions import compute_dimension_coverage + from abstractatlas.ui_data.dimensions import compute_dimension_coverage dims = { 1001: {"focus": ["Clinical"], "research_modality": [], "theory_scope": ["X"], "epistemic_basis": ["Data-driven"]}, @@ -157,7 +157,7 @@ def test_matched_plus_no_value_equals_corpus_count(self) -> None: self.assertEqual(cov["source_file"], "dimensions.slim.json") def test_non_list_value_rejected_on_load(self) -> None: - from ohbm2026.ui_data.dimensions import DimensionInputError, load_research_dimensions + from abstractatlas.ui_data.dimensions import DimensionInputError, load_research_dimensions with TemporaryDirectory() as tmp: bad = Path(tmp) / "bad.json" @@ -183,7 +183,7 @@ def _facets(self, **overrides): return base def test_facets_to_arrow_includes_four_dimensions(self) -> None: - from ohbm2026.ui_data.formats.parquet_single import _facets_to_arrow + from abstractatlas.ui_data.formats.parquet_single import _facets_to_arrow out = _facets_to_arrow(self._facets(focus=["Clinical"], theory_scope=["Micro Theory"])) for key in ("focus", "research_modality", "theory_scope", "epistemic_basis"): @@ -193,7 +193,7 @@ def test_facets_to_arrow_includes_four_dimensions(self) -> None: self.assertEqual(out["research_modality"], []) def test_abstracts_table_round_trips_dimensions(self) -> None: - from ohbm2026.ui_data.formats.parquet_single import _abstracts_to_table + from abstractatlas.ui_data.formats.parquet_single import _abstracts_to_table envelope = { "abstracts": [ diff --git a/tests/test_ui_data_link_check.py b/tests/test_ui_data_link_check.py index 7772a5ce..48724f95 100644 --- a/tests/test_ui_data_link_check.py +++ b/tests/test_ui_data_link_check.py @@ -12,7 +12,7 @@ import requests.exceptions import responses -from ohbm2026.ui_data.link_check import link_check, head_url +from abstractatlas.ui_data.link_check import link_check, head_url GOOD_YAML = """ diff --git a/tests/test_ui_data_manifest.py b/tests/test_ui_data_manifest.py index b80897de..21166411 100644 --- a/tests/test_ui_data_manifest.py +++ b/tests/test_ui_data_manifest.py @@ -8,8 +8,8 @@ from pathlib import Path from tempfile import TemporaryDirectory -from ohbm2026.ui_data import manifest as manifest_module -from ohbm2026.ui_data.manifest import build_manifest, discover_cells, discover_topic_kinds +from abstractatlas.ui_data import manifest as manifest_module +from abstractatlas.ui_data.manifest import build_manifest, discover_cells, discover_topic_kinds from tests._ui_data_fixtures import BUILD_INFO, CORPUS_PAYLOAD, write_fixtures diff --git a/tests/test_ui_data_state_key.py b/tests/test_ui_data_state_key.py index 86f529ce..1805305c 100644 --- a/tests/test_ui_data_state_key.py +++ b/tests/test_ui_data_state_key.py @@ -8,7 +8,7 @@ from pathlib import Path from tempfile import TemporaryDirectory -from ohbm2026.ui_data.state_key import ( +from abstractatlas.ui_data.state_key import ( Stage6BuildError, discover_corpus_state_key, discover_minilm_bundle, From 25804e8c1bceeceeff68fa205ed49cce52a9fb6e Mon Sep 17 00:00:00 2001 From: Satrajit Ghosh <satra@mit.edu> Date: Fri, 3 Jul 2026 15:39:53 -0400 Subject: [PATCH 3/3] =?UTF-8?q?refactor(027)!:=20drop=20deprecation=20shim?= =?UTF-8?q?s=20=E2=80=94=20hard=20cutover=20to=20abstractatlas/aacli?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per requester: no deprecation shims. Removes: - src/ohbm2026/__init__.py (the import meta-path shim) — `import ohbm2026` now raises ModuleNotFoundError. - the `ohbmcli` [project.scripts] entry + `_ohbmcli_deprecated_main` in cli.py — `ohbmcli` is now `command not found`. Also renames the remaining `ohbmcli` string literals in src (+ tests): argparse `prog=` (aacli --help now shows `usage: aacli`), provenance `command_line` strings, help/error text; and fixes a dynamic `__import__("ohbm2026")` in test_enrich_stage that the module-ref passes missed. Reinstalled the venv (uninstall ohbm2026 dist → install -e .) so a prior installed `ohbm2026` can't mask the cutover. Spec/plan/tasks/contracts (FR-003, SC-007, R3, rename-map, cli-aacli) + CLAUDE pointer updated to describe the hard cutover. Verified: full suite `Ran 1034 — OK (skipped=1)`; `import ohbm2026` → ModuleNotFoundError; `ohbmcli` → command not found; `aacli` works. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --- CLAUDE.md | 8 ++- pyproject.toml | 14 ++--- .../contracts/cli-aacli.md | 8 +-- .../contracts/rename-map.md | 16 ++--- .../research.md | 30 ++++----- .../spec.md | 8 +-- .../tasks.md | 2 +- src/abstractatlas/analyze/__init__.py | 2 +- src/abstractatlas/analyze/stage.py | 10 +-- src/abstractatlas/atlas_hosting/cli.py | 8 +-- src/abstractatlas/atlas_package/__init__.py | 2 +- src/abstractatlas/atlas_package/cli.py | 4 +- .../atlas_package/orchestrator.py | 4 +- src/abstractatlas/atlas_package/provenance.py | 2 +- src/abstractatlas/book/cli.py | 6 +- src/abstractatlas/book/corpus.py | 2 +- src/abstractatlas/book/render_per_abstract.py | 2 +- src/abstractatlas/cli.py | 18 +----- .../data/__pycache__/__init__.cpython-314.pyc | Bin 361 -> 361 bytes src/abstractatlas/exceptions.py | 2 +- src/abstractatlas/fetch/stage.py | 2 +- src/abstractatlas/ui_data/questions.py | 2 +- src/abstractatlas/ui_data/state_key.py | 2 +- src/ohbm2026/__init__.py | 58 ------------------ tests/test_analyze_provenance.py | 4 +- tests/test_analyze_storage.py | 2 +- tests/test_atlas_hosting_manifest.py | 2 +- tests/test_atlas_hosting_uploader.py | 2 +- tests/test_atlas_parquet_writer.py | 2 +- tests/test_atlas_semantic_index.py | 2 +- tests/test_cli.py | 10 +-- tests/test_docx_retirement.py | 2 +- tests/test_embed_provenance.py | 2 +- tests/test_enrich_stage.py | 2 +- 34 files changed, 87 insertions(+), 155 deletions(-) delete mode 100644 src/ohbm2026/__init__.py diff --git a/CLAUDE.md b/CLAUDE.md index e4bb6b11..7c15433b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -175,8 +175,10 @@ things: (1) **renames** the component `ohbm2026`→`abstractatlas` (package `src/ohbm2026/`→`src/abstractatlas/` via `git mv` + import rewrite across ~86 files) and the CLI `ohbmcli`→**`aacli`** (module form `python -m abstractatlas.cli`; legacy `ohbm-*` scripts→`aa-*` or dropped), -with `ohbmcli`/`import ohbm2026` kept as **labeled deprecation shims** for -one cycle; and (2) **generalizes ingestion** into a pluggable architecture — +with `ohbmcli`/`import ohbm2026` **removed outright (hard cutover, no +shims)** — they fail loudly (`command not found` / `ModuleNotFoundError`); +the venv is reinstalled so a prior pip-installed `ohbm2026` dist doesn't +mask it; and (2) **generalizes ingestion** into a pluggable architecture — a NEW `src/abstractatlas/ingest/` subpackage with an `Ingestor` ABC (`pull`→`normalize`→`validate`), a runtime-discoverable `registry` (never a hardcoded source list — CA-007), and a standardized **LinkML @@ -199,7 +201,7 @@ fidelity. Companions: `research.md`, `data-model.md`, `contracts/{rename-map,ingestor-interface,ingest-schema.linkml.yaml,cli-aacli}.md`, `quickstart.md`. NOTE for future sessions: after this ships, the package is `abstractatlas` and the CLI is `aacli` — update muscle memory (`ohbmcli` -still works via a deprecation shim slated for removal). +and `import ohbm2026` are GONE; they error, no shim). The immediately-prior Stage 26 plan is at `specs/026-neuroscape-year-density/plan.md`. Stage 26 (Track A, diff --git a/pyproject.toml b/pyproject.toml index f7e29378..985f24cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,15 +39,13 @@ abstracts_book = [ ] [project.scripts] -# Canonical CLI (spec 027: ohbmcli → aacli). +# Canonical CLI (spec 027: ohbmcli → aacli). Hard cutover — no `ohbmcli` +# alias and no `ohbm2026` import shim: the old names are removed and fail +# loudly (command-not-found / ModuleNotFoundError). The legacy per-stage +# `ohbm-*` console scripts were also dropped (most pointed at modules moved +# in the Stage-5 reorg); their functions remain importable via +# `abstractatlas.*` and runnable as `aacli` subcommands. aacli = "abstractatlas.cli:main" -# Deprecated alias — warns then delegates to aacli. Kept one transition -# cycle; remove next release. (Spec 027 FR-003 / research R3.) -ohbmcli = "abstractatlas.cli:_ohbmcli_deprecated_main" -# The legacy per-stage `ohbm-*` console scripts were dropped in spec 027: -# most already pointed at modules moved in the Stage-5 reorg -# (ohbm2026.enrichment, ohbm2026.openalex). Their functions remain -# importable via `abstractatlas.*` and runnable as `aacli` subcommands. [build-system] requires = ["setuptools>=69"] diff --git a/specs/027-abstractatlas-rename-ingestors/contracts/cli-aacli.md b/specs/027-abstractatlas-rename-ingestors/contracts/cli-aacli.md index 115886bf..bf21a902 100644 --- a/specs/027-abstractatlas-rename-ingestors/contracts/cli-aacli.md +++ b/specs/027-abstractatlas-rename-ingestors/contracts/cli-aacli.md @@ -30,11 +30,11 @@ subcommand (SC-001). > surface that makes future sources first-class without new bespoke > subcommands. -## Deprecation +## Hard cutover (no deprecation) -- `ohbmcli <args>` → stderr: `ohbmcli is deprecated; use 'aacli'. Delegating…` - then runs `aacli <args>`. Removed next cycle. -- `python -m ohbm2026.cli` → shim module warns + delegates. +- `ohbmcli` → removed; `command not found`. +- `import ohbm2026` / `python -m ohbm2026.cli` → `ModuleNotFoundError: No module named 'ohbm2026'`. +- The sole interface is `aacli` / `python -m abstractatlas.cli`. ## Verification diff --git a/specs/027-abstractatlas-rename-ingestors/contracts/rename-map.md b/specs/027-abstractatlas-rename-ingestors/contracts/rename-map.md index 690debe0..97f98f2f 100644 --- a/specs/027-abstractatlas-rename-ingestors/contracts/rename-map.md +++ b/specs/027-abstractatlas-rename-ingestors/contracts/rename-map.md @@ -17,15 +17,17 @@ green) + SC-003 (byte-identical published data). | Docs | README, CLAUDE.md, constitution naming refs | updated to `abstractatlas`/`aacli` (FR-012) | | Site test strings | `ohbm2026` refs in `site/src/**` tests | updated where they name the package (NOT the `ohbm2026` data/site-mode/route which is the source identity) | -## Deprecated shims (one transition period) +## Hard cutover — no shims (per requester) -| Legacy | Behavior | -|--------|----------| -| `ohbmcli` entry point | prints "renamed to `aacli`" deprecation notice, then delegates to `aacli` | -| `import ohbm2026` | thin shim re-exporting `abstractatlas` with a `DeprecationWarning`; labeled + follow-up to remove | +| Legacy | Behavior after cutover | +|--------|------------------------| +| `ohbmcli` | removed from `[project.scripts]` → `command not found: ohbmcli` | +| `import ohbm2026` | package dir gone + venv reinstalled → `ModuleNotFoundError: No module named 'ohbm2026'` | -Shims MUST be labeled in code with root cause + removal follow-up -(Constitution VI). Neither may partially work in a confusing way (FR-003). +No deprecation shim is provided. Both fail loudly and immediately (FR-003 / +SC-007) — never a silent or partial success. The venv is reinstalled +(`uv pip uninstall ohbm2026 && uv pip install -e .`) so a previously +pip-installed `ohbm2026` dist can't mask the cutover. ## NOT renamed (data identity — FR-004) diff --git a/specs/027-abstractatlas-rename-ingestors/research.md b/specs/027-abstractatlas-rename-ingestors/research.md index aacf7121..49335c59 100644 --- a/specs/027-abstractatlas-rename-ingestors/research.md +++ b/specs/027-abstractatlas-rename-ingestors/research.md @@ -43,20 +43,22 @@ option) — rejected by the requester for continuity. ## R3 — Legacy name policy (`ohbmcli`, `import ohbm2026`) -**Decision**: `aacli` / `abstractatlas` are canonical. Provide a -**deprecated shim** for one transition period: `ohbmcli` remains as an entry -point that prints a clear "renamed to `aacli`" deprecation notice then -delegates; `import ohbm2026` resolves via a thin shim module that re-exports -`abstractatlas` with a `DeprecationWarning`. Both are labeled and slated for -removal. - -**Rationale**: Avoids silently breaking existing scripts/cron while making -the new name canonical (FR-003, SC-007, edge case). A hard break is also -acceptable per the spec, but a labeled shim is lower-friction and still -loud. The shim carries a root-cause comment + follow-up per Constitution VI. - -**Alternatives considered**: hard removal with an error — acceptable but -more disruptive; chose the shim as the default, removable next cycle. +**Decision (updated per requester): HARD CUTOVER — no deprecation shims.** +`aacli` / `abstractatlas` are the sole names. The `ohbmcli` entry point and +any `ohbm2026` import path are removed; using them fails loudly with the +standard not-found error (`command not found: ohbmcli` / +`ModuleNotFoundError: No module named 'ohbm2026'`). The venv is reinstalled +(`uv pip uninstall ohbm2026 && uv pip install -e .`) so a previously +pip-installed `ohbm2026` dist doesn't mask the cutover. + +**Rationale**: The requester opted for a clean break — no shim to maintain +or later remove. A bare not-found error is loud and unambiguous (satisfies +FR-003/SC-007's "never silent/partial"), and there is no external +consumer that needs a grace period. + +**Alternatives considered**: a labeled deprecation shim (`ohbmcli` warns + +delegates; `ohbm2026` meta-path redirect) — implemented first, then +**removed** at the requester's direction in favour of the hard cutover. ## R4 — Ingestor contract: wrap, don't rewrite diff --git a/specs/027-abstractatlas-rename-ingestors/spec.md b/specs/027-abstractatlas-rename-ingestors/spec.md index 915f58dc..f6c34d5a 100644 --- a/specs/027-abstractatlas-rename-ingestors/spec.md +++ b/specs/027-abstractatlas-rename-ingestors/spec.md @@ -102,7 +102,7 @@ error rather than silently passed downstream. ### Edge Cases - **Naming divergence for preserved data**: the package is `abstractatlas` but the published parquet keeps the name `ohbm2026.parquet` (that source's data name). The system must tolerate — and clearly document — this intentional divergence rather than "fix" the data name (which would force a re-publish). -- **Legacy imports / command**: existing scripts, cron jobs, or muscle-memory using `ohbmcli` / `import ohbm2026` must fail with a clear pointer to the new name (or work via a clearly-labeled deprecated alias for one transition period), never a confusing partial success. +- **Legacy imports / command**: existing scripts, cron jobs, or muscle-memory using `ohbmcli` / `import ohbm2026` fail loudly with the standard not-found error (hard cutover — the old names are removed), never a confusing partial success. - **State-keyed caches/checkpoints**: caches and checkpoints keyed by names that embed `ohbm2026` must remain readable so the rename does not silently invalidate expensive prior work. - **Source-type asymmetry**: a conference record has no DOI/journal; a literature record has no poster/session. The schema must make each set optional-by-source without letting a conference record masquerade as literature or vice versa. - **Two sources, one atlas**: an ingestor's records carry which source they came from, so downstream can attribute, filter, and de-duplicate across sources. @@ -114,7 +114,7 @@ error rather than silently passed downstream. - **FR-001**: The Python package MUST be renamed `ohbm2026` → `abstractatlas`, and all internal imports, references, and docs updated to the new name. - **FR-002**: The canonical CLI MUST be renamed `ohbmcli` → `aacli`; every existing subcommand MUST remain available with identical behavior and options. -- **FR-003**: Legacy entry points (`ohbmcli`, `import ohbm2026`) MUST either be removed with a precise "renamed to …" error or provided as an explicitly-deprecated alias for one transition period; they MUST NOT partially work in a confusing way. +- **FR-003**: Legacy entry points (`ohbmcli`, `import ohbm2026`) MUST be removed outright (hard cutover — no deprecation shims); using them MUST fail loudly with the standard not-found error (`command not found` / `ModuleNotFoundError: No module named 'ohbm2026'`), never partially work in a confusing way. - **FR-004**: Existing on-disk data artifact paths, artifact/state-key naming, cache/checkpoint keys, and published data-package names (including `ohbm2026.parquet`) MUST be preserved so no data is regenerated and the live site requires no re-publish (byte-identical data). - **FR-005**: Ingestion MUST be organized as a pluggable architecture: a common **ingestor contract** (pull records from a source → normalize) with a runtime-discoverable registry of named ingestors. - **FR-006**: The two existing sources — the OHBM conference (Oxford Abstracts) and the PubMed/NeuroScape corpus — MUST be ported to become the first two ingestor instances behind the common contract, each reproducing its former normalized output exactly. @@ -153,13 +153,13 @@ error rather than silently passed downstream. - **SC-004**: Both existing sources are expressed as named ingestors behind the common contract; porting them changed **zero** lines of downstream stage logic (measured by the diff touching only ingestor + rename code). - **SC-005**: 100% of records emitted by each ingestor validate against the LinkML ingest schema; a deliberately malformed record is rejected with a precise, source-attributed error (0 malformed records reach downstream). - **SC-006**: A maintainer can onboard a new source by implementing only the ingestor contract + registering it, with no edits to downstream stages — demonstrated by a documented "add an ingestor" guide and the two ports as worked examples. -- **SC-007**: Using a legacy name (`ohbmcli` / `import ohbm2026`) yields a clear rename pointer or a labeled-deprecated alias — never a silent or partial success. +- **SC-007**: Using a legacy name (`ohbmcli` / `import ohbm2026`) fails loudly and immediately with the standard not-found error — never a silent or partial success. (Hard cutover: the old names are gone.) ## Assumptions - "Foundation only": this change renames + establishes the ingestor architecture + LinkML ingest schema + ports the two existing sources. It does **not** add arXiv/bioRxiv/medRxiv or new-conference ingestors — those are follow-on specs enabled by this foundation. - Data continuity is paramount: on-disk artifact layout and published data-package names are preserved verbatim (including the historical `ohbm2026.parquet` name), so the rename is a code/CLI/package/docs change with no data regeneration or site re-publish. - The standardized LinkML ingest schema is designed to **capture the existing normalized shape** (so the ported OHBM output validates and downstream stays byte-identical), while being general enough to admit literature-index sources — it does not reshape already-published data. -- A deprecated `ohbmcli` alias (and/or an `ohbm2026` import shim) may be provided for one transition period; the canonical, documented interface is `aacli` / `abstractatlas`. +- Hard cutover: NO deprecation shims. `ohbmcli` and `import ohbm2026` are removed and fail loudly; the sole interface is `aacli` / `abstractatlas`. (The local venv must be reinstalled so a previously pip-installed `ohbm2026` dist is replaced by `abstractatlas`.) - Governance docs (constitution) contain naming references (`ohbmcli`, `src/ohbm2026/`, `data/abstracts.json`); updating those references is in scope for doc-sync, coordinated with the project's constitution-amendment process where required. - Downstream stages already treat the corpus generically enough that a standardized ingest contract can be introduced without changing their outputs for the existing sources. diff --git a/specs/027-abstractatlas-rename-ingestors/tasks.md b/specs/027-abstractatlas-rename-ingestors/tasks.md index 5b1573b4..c19b9891 100644 --- a/specs/027-abstractatlas-rename-ingestors/tasks.md +++ b/specs/027-abstractatlas-rename-ingestors/tasks.md @@ -57,7 +57,7 @@ renamed package and must follow it. Sequence: Setup → baseline → US1 → US2 - [x] T003 [US1] `git mv src/ohbm2026 src/abstractatlas`, then rewrite `ohbm2026`→`abstractatlas` across all imports/refs in `src/` (per `contracts/rename-map.md`). Do NOT touch data-identity strings in the rename-map "NOT renamed" table (data paths, state-keys, `ohbm2026.parquet`, `/ohbm2026/` route, `SITE_MODE='ohbm'`, `OHBM2026_*`). - [x] T004 [US1] Update `pyproject.toml`: `name` `ohbm2026`→`abstractatlas`; `[project.scripts]` `ohbmcli`→`aacli = "abstractatlas.cli:main"`; rename `ohbm-*` entry points to `aa-*` where still used or drop them (record disposition per rename-map). Keep `package-dir`/`packages.find` under `src`. -- [x] T005 [US1] Add labeled deprecation shims (FR-003, research R3): `ohbmcli` entry point that warns "renamed to aacli" then delegates; an `ohbm2026` import shim module re-exporting `abstractatlas` with a `DeprecationWarning`. Label both in code with root cause + removal follow-up (Constitution VI). +- [x] T005 [US1] HARD CUTOVER — no deprecation shims (per requester). Remove the `ohbmcli` entry point (pyproject keeps only `aacli`), the legacy `ohbm-*` scripts, and rename all `ohbmcli` string literals in `src/` (argparse `prog=`, provenance `command_line`, help/error text) → `aacli`. Reinstall the venv package so a previously pip-installed `ohbm2026` dist is replaced by `abstractatlas` (`uv pip uninstall ohbm2026 && uv pip install -e .`), so `import ohbm2026` fails with `ModuleNotFoundError` and `ohbmcli` is `command not found` (FR-003/SC-007). - [x] T006 [P] [US1] Rewrite `ohbm2026`→`abstractatlas` imports/refs across `tests/` and `scripts/`; update `PYTHONPATH=src -m ohbm2026.cli`→`-m abstractatlas.cli` in scripts + `.github/workflows/**` + `.githooks/`. - [x] T007 [P] [US1] Update the few `ohbm2026` string refs in `site/src/**` tests that name the PACKAGE (not the `ohbm2026` data/route/site-mode identity). — VERIFIED NO-OP: audited `site/src` — every `ohbm2026` ref is data/route/storage-key identity (`/ohbm2026/` routes, `ohbm2026.parquet`, `ohbm2026.ui.*`/`ohbm2026.analytics.*` localStorage keys, conference `kind`); the TypeScript site never imports the Python package, so nothing to rename (2 stale comment path-mentions left as historical, like the src ones). - [x] T008 [US1] Verify the rename: `PYTHONPATH=src .venv/bin/python -m unittest discover -s tests` fully green (SC-002); re-run the T002 fixture under `aacli` and diff artifacts — identical (SC-001); `ohbmcli`/`import ohbm2026` emit the labeled deprecation and still succeed (SC-007); confirm an existing `data/cache/**` (or checkpoint) entry created before the rename is still discovered/reused — no forced recompute from the name change (Constitution III / spec edge case); `git grep -n 'ohbm2026\|ohbmcli'` returns only intentional data-identity names, shims, and historical spec docs. diff --git a/src/abstractatlas/analyze/__init__.py b/src/abstractatlas/analyze/__init__.py index 402207c6..c3d7efb9 100644 --- a/src/abstractatlas/analyze/__init__.py +++ b/src/abstractatlas/analyze/__init__.py @@ -23,7 +23,7 @@ - `analyze.umap` — Stage 4 UMAP fit + `project_into_umap` (US2). - `analyze.rollup` — Canonical UI rollup writer (annotations.parquet + sqlite). -- `analyze.stage` — Stage 4 orchestrator (`ohbmcli analyze-matrix`). +- `analyze.stage` — Stage 4 orchestrator (`aacli analyze-matrix`). **No package-level re-exports.** Per spec clarification Session 2026-05-15 Q2, every caller imports from the explicit submodule that diff --git a/src/abstractatlas/analyze/stage.py b/src/abstractatlas/analyze/stage.py index c9df7d91..4435e385 100644 --- a/src/abstractatlas/analyze/stage.py +++ b/src/abstractatlas/analyze/stage.py @@ -1,4 +1,4 @@ -"""Stage 4 orchestrator — `ohbmcli analyze-matrix`. +"""Stage 4 orchestrator — `aacli analyze-matrix`. Iterates the `(model, input_source, analysis_kind)` matrix, dispatches to per-kind runners, writes per-bundle artifacts atomically, and emits @@ -7,7 +7,7 @@ Per spec FR-001 / FR-002 / FR-003 / FR-014 / FR-017: -- Single canonical CLI entrypoint (`ohbmcli analyze-matrix`). +- Single canonical CLI entrypoint (`aacli analyze-matrix`). - Default matrix is **34 bundles** (5 models × 2 inputs × 4 kinds, with `neuroscape_clusters` auto-skipped for minilm/openai/pubmedbert because the published Stage-2 lens is Voyage-dim-specific). @@ -365,7 +365,7 @@ def _run_entries_parallel( # internal thread pool should be 1. Setting these env vars BEFORE # the `Parallel` call ensures the loky-spawned subprocesses inherit # them. Existing user-set values win (the explicit env-var prefix - # `OMP_NUM_THREADS=N PYTHONPATH=src ohbmcli …` still applies). + # `OMP_NUM_THREADS=N PYTHONPATH=src aacli …` still applies). if config.n_jobs != 1: for var in ( "OMP_NUM_THREADS", @@ -594,7 +594,7 @@ def _git_rev_parse_head() -> str: def build_parser() -> argparse.ArgumentParser: p = argparse.ArgumentParser( - prog="ohbmcli analyze-matrix", + prog="aacli analyze-matrix", description="Stage 4 canonical analysis & annotation matrix.", ) p.add_argument("--env-file", type=Path, default=Path(".env")) @@ -692,7 +692,7 @@ def main(argv: list[str] | None = None) -> int: sqlite_path=args.rollup_path.with_suffix(".sqlite") if args.rollup_path else None, neuroscape_centroids_dir=args.neuroscape_centroids, code_revision=args.code_revision or _git_rev_parse_head(), - command_line=" ".join(sys.argv if argv is None else ["ohbmcli", "analyze-matrix"] + list(argv)), + command_line=" ".join(sys.argv if argv is None else ["aacli", "analyze-matrix"] + list(argv)), env_file=args.env_file, dry_run=args.dry_run, n_jobs=args.n_jobs, diff --git a/src/abstractatlas/atlas_hosting/cli.py b/src/abstractatlas/atlas_hosting/cli.py index 3eed7c4f..87544996 100644 --- a/src/abstractatlas/atlas_hosting/cli.py +++ b/src/abstractatlas/atlas_hosting/cli.py @@ -4,7 +4,7 @@ ``contracts/cli-upload-atlas-package.md`` + ``contracts/cli-compare-data-hosting.md``. -Each subcommand exposes a ``build_*_parser`` (the top-level ``ohbmcli`` +Each subcommand exposes a ``build_*_parser`` (the top-level ``aacli`` copies its actions in) and a ``*_main`` argv entry point returning the documented exit code. Typed Stage-20 failures map to distinct non-zero codes; secrets are never echoed. @@ -40,7 +40,7 @@ def build_upload_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( - prog="ohbmcli upload-atlas-package", + prog="aacli upload-atlas-package", description=( "Upload a built atlas-package's parquets to Cloudflare R2 under " "content-addressed, immutable keys; emit a registry channel entry " @@ -89,7 +89,7 @@ def upload_main(argv: list[str] | None = None) -> int: parser = build_upload_parser() args = parser.parse_args(argv) - command_line = " ".join(["ohbmcli", "upload-atlas-package", *(argv or sys.argv[1:])]) + command_line = " ".join(["aacli", "upload-atlas-package", *(argv or sys.argv[1:])]) try: settings = r2_client.load_settings(args.env_file) @@ -117,7 +117,7 @@ def upload_main(argv: list[str] | None = None) -> int: def build_compare_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( - prog="ohbmcli compare-data-hosting", + prog="aacli compare-data-hosting", description=( "Probe the Dropbox- and R2-served copies of each artifact for " "byte-parity, HTTP Range support, and CORS; write a pass/fail report." diff --git a/src/abstractatlas/atlas_package/__init__.py b/src/abstractatlas/atlas_package/__init__.py index 7f95ae67..0a21cdf6 100644 --- a/src/abstractatlas/atlas_package/__init__.py +++ b/src/abstractatlas/atlas_package/__init__.py @@ -1,6 +1,6 @@ """Stage 15 atlas package (spec ``015-neuroscape-context``). -Orchestrates the ``ohbmcli build-atlas-package`` pipeline: NeuroScape +Orchestrates the ``aacli build-atlas-package`` pipeline: NeuroScape release loader → deterministic 2D + 3D UMAP fit → OHBM 2026 projection via ``umap.transform`` → k=20 neighbour index → palette assignment → quadtree blue-noise LOD backdrop (progressive tiers) → diff --git a/src/abstractatlas/atlas_package/cli.py b/src/abstractatlas/atlas_package/cli.py index fef729d7..a6e815c0 100644 --- a/src/abstractatlas/atlas_package/cli.py +++ b/src/abstractatlas/atlas_package/cli.py @@ -6,7 +6,7 @@ Exposed entry points: - :func:`build_parser` — returns the argparse parser the - top-level ``ohbmcli`` dispatches into. Subcommand name in the + top-level ``aacli`` dispatches into. Subcommand name in the parent CLI is ``build-atlas-package``. - :func:`main` — argv-driven dispatch. Returns the documented exit code (0 success, 2..7 per typed exception). @@ -483,7 +483,7 @@ def main(argv: list[str] | None = None) -> int: ) return 7 - command_line = " ".join(["ohbmcli", "build-atlas-package", *(argv or sys.argv[1:])]) + command_line = " ".join(["aacli", "build-atlas-package", *(argv or sys.argv[1:])]) try: ohbm_records, ohbm2026_state_key = load_ohbm_corpus( diff --git a/src/abstractatlas/atlas_package/orchestrator.py b/src/abstractatlas/atlas_package/orchestrator.py index f6cb9457..dde5272b 100644 --- a/src/abstractatlas/atlas_package/orchestrator.py +++ b/src/abstractatlas/atlas_package/orchestrator.py @@ -5,7 +5,7 @@ provenance schema) + R-009 (typed-exception subtree). The orchestrator is the single function the -``ohbmcli build-atlas-package`` CLI wraps. It: +``aacli build-atlas-package`` CLI wraps. It: 1. Discovers + SHA-checks the NeuroScape v1.0.1 release inputs. 2. Loads the article + cluster tables. @@ -107,7 +107,7 @@ class AtlasBuildConfig: skip_link_check: bool = False link_check_rate: float = 3.0 code_revision: str = "unknown" - command_line: str = "ohbmcli build-atlas-package" + command_line: str = "aacli build-atlas-package" titles_index_bin: bytes = field(default=b"") titles_index_meta: Mapping[str, Any] = field(default_factory=dict) # When set, the orchestrator records this ISO-8601 UTC string in diff --git a/src/abstractatlas/atlas_package/provenance.py b/src/abstractatlas/atlas_package/provenance.py index 7fc7e362..1c122440 100644 --- a/src/abstractatlas/atlas_package/provenance.py +++ b/src/abstractatlas/atlas_package/provenance.py @@ -3,7 +3,7 @@ Spec: ``specs/015-neuroscape-context/`` — CA-008 + research R-009. The provenance file is the single audit record for an -``ohbmcli build-atlas-package`` run; it MUST contain only repo- +``aacli build-atlas-package`` run; it MUST contain only repo- relative paths so the bundle is portable to other machines (Principle VIII — no absolute or user-home paths anywhere in the record). diff --git a/src/abstractatlas/book/cli.py b/src/abstractatlas/book/cli.py index 4aa4b64d..fefc0fcd 100644 --- a/src/abstractatlas/book/cli.py +++ b/src/abstractatlas/book/cli.py @@ -1,4 +1,4 @@ -"""CLI entry point for `ohbmcli book`. +"""CLI entry point for `aacli book`. Orchestrates: corpus load → sort → author index → emit markdown bundle → (optional) pandoc PDF → write provenance. Runs the system- @@ -32,7 +32,7 @@ def _build_parser() -> argparse.ArgumentParser: p = argparse.ArgumentParser( - prog="ohbmcli book", + prog="aacli book", description="Compose a publication-quality book of all accepted abstracts.", ) p.add_argument( @@ -324,7 +324,7 @@ def main(argv: list[str] | None = None) -> int: ) # Provenance. - command_line = " ".join(["ohbmcli", "book", *(argv if argv is not None else sys.argv[1:])]) + command_line = " ".join(["aacli", "book", *(argv if argv is not None else sys.argv[1:])]) try: write_provenance( book, diff --git a/src/abstractatlas/book/corpus.py b/src/abstractatlas/book/corpus.py index 0ddc8e61..4aec10c9 100644 --- a/src/abstractatlas/book/corpus.py +++ b/src/abstractatlas/book/corpus.py @@ -40,7 +40,7 @@ def _load_json(path: pathlib.Path) -> dict | list: if not path.exists(): raise BookBuildError( f"required input not found at {path}; " - "run `ohbmcli fetch-abstracts` first" + "run `aacli fetch-abstracts` first" ) return json.loads(path.read_text(encoding="utf-8")) diff --git a/src/abstractatlas/book/render_per_abstract.py b/src/abstractatlas/book/render_per_abstract.py index ba124af3..222876b7 100644 --- a/src/abstractatlas/book/render_per_abstract.py +++ b/src/abstractatlas/book/render_per_abstract.py @@ -214,7 +214,7 @@ def _debug_main(argv: list[str] | None = None) -> int: description=( "Render a single abstract in isolation, populating the " "per-abstract PDF cache. Useful for diagnosing a chunk " - "that fails inside the full ohbmcli book pipeline." + "that fails inside the full aacli book pipeline." ), ) p.add_argument("--corpus", default="data/primary/abstracts.json") diff --git a/src/abstractatlas/cli.py b/src/abstractatlas/cli.py index 554f9bf5..470a8988 100644 --- a/src/abstractatlas/cli.py +++ b/src/abstractatlas/cli.py @@ -26,7 +26,7 @@ def _copy_actions(target: argparse.ArgumentParser, source: argparse.ArgumentPars def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( - prog="ohbmcli", + prog="aacli", description="Unified CLI for OHBM 2026 ingest and enrichment", epilog=( f"Local artifacts are organized under {artifacts.INPUTS_ROOT}, " @@ -196,7 +196,7 @@ def _run_analyze_umap_project(argv: list[str]) -> int: UnsupportedProjectionAlgorithm, ) - parser = _argparse.ArgumentParser(prog="ohbmcli analyze-umap-project") + parser = _argparse.ArgumentParser(prog="aacli analyze-umap-project") parser.add_argument("--fitted-bundle", type=str, required=True) parser.add_argument("--input-vectors", type=str, required=True) parser.add_argument( @@ -245,20 +245,6 @@ def _run_refresh_assets(argv: list[str]) -> int: return assets.main(argv) -def _ohbmcli_deprecated_main(argv: list[str] | None = None) -> int: - """Deprecated entry point: `ohbmcli` was renamed to `aacli` (spec 027). - - Warns loudly on stderr, then delegates to :func:`main` so existing - scripts keep working for one transition cycle. Remove next release. - FOLLOW-UP: drop the `ohbmcli` entry in pyproject.toml [project.scripts]. - """ - print( - "ohbmcli is deprecated and will be removed; use 'aacli'. Delegating…", - file=sys.stderr, - ) - return main(argv) - - def main(argv: list[str] | None = None) -> int: argv = list(sys.argv[1:] if argv is None else argv) parser = build_parser() diff --git a/src/abstractatlas/data/__pycache__/__init__.cpython-314.pyc b/src/abstractatlas/data/__pycache__/__init__.cpython-314.pyc index 86c3d8d6050a3b298e40a52e4bde2768fe13c887..bca2853e00dd82d59477fbe5560e82d8e5a1599a 100644 GIT binary patch delta 19 ZcmaFK^pc57n~#@^0SJ2dHgd%?0st)f1Lgn# delta 19 ZcmaFK^pc57n~#@^0SF>FHgd%?0st$j1FZl6 diff --git a/src/abstractatlas/exceptions.py b/src/abstractatlas/exceptions.py index 0d56dbd7..4ce2bc37 100644 --- a/src/abstractatlas/exceptions.py +++ b/src/abstractatlas/exceptions.py @@ -597,7 +597,7 @@ class Stage19SemanticError(Stage15Error): Subclasses `Stage15Error` so any existing Stage-15 catcher catches Stage-19 errors too — the semantic-index step is wired into the - `ohbmcli build-atlas-package` orchestrator that Stage 15 established. + `aacli build-atlas-package` orchestrator that Stage 15 established. """ diff --git a/src/abstractatlas/fetch/stage.py b/src/abstractatlas/fetch/stage.py index 44e21b81..486abad8 100644 --- a/src/abstractatlas/fetch/stage.py +++ b/src/abstractatlas/fetch/stage.py @@ -1,6 +1,6 @@ """Stage 1 orchestrator — fetch abstracts + persist GraphQL schema. -Replaces the legacy ``ohbmcli ingest`` invocation per FR-014. Calls +Replaces the legacy ``aacli ingest`` invocation per FR-014. Calls out to ``graphql_api`` for upstream I/O, ``schema_diff`` for tiered drift detection, ``assets`` for the per-record batched fetch with checkpoint hooks, and ``artifacts`` for path derivation + state-key. diff --git a/src/abstractatlas/ui_data/questions.py b/src/abstractatlas/ui_data/questions.py index 70be31bd..e595fee0 100644 --- a/src/abstractatlas/ui_data/questions.py +++ b/src/abstractatlas/ui_data/questions.py @@ -12,7 +12,7 @@ domain-facet axes that the UI's facet panel exposes. These helpers used to live in the legacy ``abstractatlas.ui.payload`` module -that drove the static UI bundle written by ``ohbmcli export-ui``. Now +that drove the static UI bundle written by ``aacli export-ui``. Now that the Stage 6 SvelteKit site has replaced that path, the helpers were extracted here and the legacy module retired. """ diff --git a/src/abstractatlas/ui_data/state_key.py b/src/abstractatlas/ui_data/state_key.py index 08e1f93d..af31663d 100644 --- a/src/abstractatlas/ui_data/state_key.py +++ b/src/abstractatlas/ui_data/state_key.py @@ -80,7 +80,7 @@ def discover_rollup_state_key(analysis_root: Path) -> str: if not candidates: raise Stage6BuildError( f"No annotations__*.sqlite rollups found under {analysis_root}; " - "run Stage 4 (`ohbmcli analyze-matrix`) first." + "run Stage 4 (`aacli analyze-matrix`) first." ) if len(candidates) > 1: names = ", ".join(sorted({c[1] for c in candidates})) diff --git a/src/ohbm2026/__init__.py b/src/ohbm2026/__init__.py deleted file mode 100644 index ead3c2f3..00000000 --- a/src/ohbm2026/__init__.py +++ /dev/null @@ -1,58 +0,0 @@ -"""Deprecated shim: the ``ohbm2026`` package was renamed to ``abstractatlas`` -(spec 027). Importing ``ohbm2026`` or any ``ohbm2026.*`` submodule warns with -a clear rename pointer and transparently redirects to the ``abstractatlas`` -equivalent, so existing external scripts keep working for one transition -cycle. - -FOLLOW-UP (root cause: package rename): delete ``src/ohbm2026/`` once no -external caller imports the old name. This shim exists only to satisfy the -"legacy name → clear pointer, never a silent/partial failure" guarantee -(spec 027 FR-003 / SC-007). The canonical package is ``abstractatlas``. -""" - -from __future__ import annotations - -import importlib -import sys -import warnings -from importlib.abc import Loader, MetaPathFinder -from importlib.machinery import ModuleSpec - -warnings.warn( - "The 'ohbm2026' package was renamed to 'abstractatlas'; update your imports " - "(e.g. 'from abstractatlas.X import Y'). This compatibility shim will be " - "removed in a future release.", - DeprecationWarning, - stacklevel=2, -) - -_OLD = "ohbm2026" -_NEW = "abstractatlas" - - -class _RedirectLoader(Loader): - def __init__(self, target: str) -> None: - self._target = target - - def create_module(self, spec: ModuleSpec): # noqa: D401 - module = importlib.import_module(self._target) - sys.modules[spec.name] = module - return module - - def exec_module(self, module) -> None: # already executed on import - return None - - -class _RedirectFinder(MetaPathFinder): - """Map ``ohbm2026`` / ``ohbm2026.*`` onto ``abstractatlas`` / ``abstractatlas.*``.""" - - def find_spec(self, fullname: str, path=None, target=None): # noqa: D401 - if fullname == _OLD or fullname.startswith(_OLD + "."): - redirected = _NEW + fullname[len(_OLD) :] - return ModuleSpec(fullname, _RedirectLoader(redirected)) - return None - - -sys.meta_path.insert(0, _RedirectFinder()) -# Alias the top-level package object itself to abstractatlas. -sys.modules[__name__] = importlib.import_module(_NEW) diff --git a/tests/test_analyze_provenance.py b/tests/test_analyze_provenance.py index 3de5503e..fbb6cf3b 100644 --- a/tests/test_analyze_provenance.py +++ b/tests/test_analyze_provenance.py @@ -62,7 +62,7 @@ def _payload(self) -> dict: "algorithm_config_canonical_json": "{}", "cache_key": "sha256:deadbeef", "code_revision": "0" * 40, - "command": "ohbmcli analyze-matrix", + "command": "aacli analyze-matrix", "seed": 42, "started_at": "2026-05-14T00:00:00Z", "completed_at": "2026-05-14T00:00:01Z", @@ -107,7 +107,7 @@ def _payload(self) -> dict: "seed": 42, "skip_llm_topics": False, "strict_matrix": False, - "command_line": "ohbmcli analyze-matrix --models voyage --inputs abstract --kinds projections", + "command_line": "aacli analyze-matrix --models voyage --inputs abstract --kinds projections", "code_revision": "0" * 40, "started_at": "2026-05-14T00:00:00Z", "completed_at": "2026-05-14T00:30:00Z", diff --git a/tests/test_analyze_storage.py b/tests/test_analyze_storage.py index 9252f533..ed36da8f 100644 --- a/tests/test_analyze_storage.py +++ b/tests/test_analyze_storage.py @@ -42,7 +42,7 @@ def test_atomic_write_creates_canonical_files(self) -> None: "algorithm_config_canonical_json": "{}", "cache_key": "sha256:deadbeef", "code_revision": "0" * 40, - "command": "ohbmcli analyze-matrix", + "command": "aacli analyze-matrix", "seed": 42, "started_at": "2026-05-14T00:00:00Z", "completed_at": "2026-05-14T00:00:01Z", diff --git a/tests/test_atlas_hosting_manifest.py b/tests/test_atlas_hosting_manifest.py index 5fe0f8e3..871d566d 100644 --- a/tests/test_atlas_hosting_manifest.py +++ b/tests/test_atlas_hosting_manifest.py @@ -52,7 +52,7 @@ def _manifest() -> UploadManifest: public_base_url="https://aadata.cirrusscience.org", key_prefix="", code_revision="deadbeef", - command_line="ohbmcli upload-atlas-package --package-dir data/outputs/atlas-package__x", + command_line="aacli upload-atlas-package --package-dir data/outputs/atlas-package__x", uploaded_utc="2026-05-31T12:00:00+00:00", source_package_dir="data/outputs/atlas-package__x", artifacts=objs, diff --git a/tests/test_atlas_hosting_uploader.py b/tests/test_atlas_hosting_uploader.py index 53673059..13a0f91f 100644 --- a/tests/test_atlas_hosting_uploader.py +++ b/tests/test_atlas_hosting_uploader.py @@ -102,7 +102,7 @@ def _all_keys(self) -> dict[str, int]: class HappyPathTests(_TempCwd): def test_uploads_all_four_writes_manifest_and_channel_entry(self) -> None: client = FakeR2Client(exists={}) - result = self._upload(client, command_line="ohbmcli upload-atlas-package") + result = self._upload(client, command_line="aacli upload-atlas-package") self.assertEqual(result.summary, {"uploaded": 4, "skipped": 0}) self.assertEqual(len(client.uploaded), 4) self.assertEqual(set(result.channel_entry), ALL_LOGICAL) diff --git a/tests/test_atlas_parquet_writer.py b/tests/test_atlas_parquet_writer.py index 249db1c2..609b4cca 100644 --- a/tests/test_atlas_parquet_writer.py +++ b/tests/test_atlas_parquet_writer.py @@ -92,7 +92,7 @@ def _build_info(state_key: str = "abcd12345678") -> dict: return { "state_key": state_key, "code_revision": "deadbeef", - "command_line": "ohbmcli build-atlas-package", + "command_line": "aacli build-atlas-package", "seed": 0, "umap_state_key": "111111111111", "centroid_table_version": "ec7a69d7cccd", diff --git a/tests/test_atlas_semantic_index.py b/tests/test_atlas_semantic_index.py index f388f52d..7582b669 100644 --- a/tests/test_atlas_semantic_index.py +++ b/tests/test_atlas_semantic_index.py @@ -44,7 +44,7 @@ def _manifest() -> dict: "corpus": "neuroscape", "state_key": "abc123def456", "code_revision": "test", - "command_line": "ohbmcli build-atlas-package --semantic-index", + "command_line": "aacli build-atlas-package --semantic-index", "seed": 0, "model_id": "Xenova/all-MiniLM-L6-v2", "model_sha256": "deadbeef" * 8, diff --git a/tests/test_cli.py b/tests/test_cli.py index fca29ae7..5a012d16 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -129,7 +129,7 @@ def test_write_manifest_subcommand_delegates_to_manifest_main(self) -> None: class TestAuthorsSubcommandRemoved(unittest.TestCase): - """FR-024 — `ohbmcli authors` is REMOVED (no backward-compat alias). + """FR-024 — `aacli authors` is REMOVED (no backward-compat alias). Authors are now fetched inline by Stage 1 (FR-023).""" def test_authors_subcommand_is_not_a_known_choice(self) -> None: @@ -138,7 +138,7 @@ def test_authors_subcommand_is_not_a_known_choice(self) -> None: class TestIngestSubcommandRemoved(unittest.TestCase): - """T011 — `ohbmcli ingest` is REMOVED (no backward-compat alias). + """T011 — `aacli ingest` is REMOVED (no backward-compat alias). Per spec FR-014 + Clarifications session 2026-05-12. Hermetic safety net: we mock `cli.assets.main` so that, in red @@ -154,7 +154,7 @@ def test_ingest_subcommand_is_not_a_known_choice(self) -> None: class TestFetchAbstractsSubcommand(unittest.TestCase): - """T011 — `ohbmcli fetch-abstracts` wires to fetch_stage.main. + """T011 — `aacli fetch-abstracts` wires to fetch_stage.main. Hermetic safety net: if `abstractatlas.fetch.stage` does not exist yet (red phase), the test fails on ImportError before any other @@ -174,7 +174,7 @@ def test_fetch_abstracts_delegates_to_fetch_stage_main(self) -> None: class TestLegacyEnrichmentSubcommandsRemoved(unittest.TestCase): """FR-014 — `enrich`, `analyze-figures`, `extract-claims`, and `reference-metadata` are REMOVED. Operators use - `ohbmcli enrich-abstracts` and `--invalidate <component>` for + `aacli enrich-abstracts` and `--invalidate <component>` for targeted refresh instead. No backward-compat alias. Hermetic safety net: we mock every legacy main so that in red @@ -205,7 +205,7 @@ def test_reference_metadata_subcommand_is_not_a_known_choice(self) -> None: class TestFetchWithdrawnSubcommand(unittest.TestCase): - """FR-022 — `ohbmcli fetch-withdrawn` is a distinct subcommand + """FR-022 — `aacli fetch-withdrawn` is a distinct subcommand that delegates to fetch_stage.main with --corpus-kind=withdrawn automatically appended.""" diff --git a/tests/test_docx_retirement.py b/tests/test_docx_retirement.py index 65fcaecb..796e646e 100644 --- a/tests/test_docx_retirement.py +++ b/tests/test_docx_retirement.py @@ -1,7 +1,7 @@ """T031 — DOCX export retired in Stage 11.1 US3. Contract: -- `ohbmcli book --format docx` exits non-zero with a stderr message +- `aacli book --format docx` exits non-zero with a stderr message naming the surviving formats (`--format md`, `--format pdf`). - No `book.docx` is written. - The `--format docx` choice is rejected at the argparse layer or diff --git a/tests/test_embed_provenance.py b/tests/test_embed_provenance.py index 9b9b71d4..29bfa63e 100644 --- a/tests/test_embed_provenance.py +++ b/tests/test_embed_provenance.py @@ -24,7 +24,7 @@ def _good_payload(**overrides) -> dict: "corpus_state_key": "f0c51e80dc0e", "corpus_source_path": "data/primary/abstracts_enriched.sqlite", "corpus_source_hash": "0" * 64, - "command_line": "ohbmcli embed-matrix --models minilm --components title", + "command_line": "aacli embed-matrix --models minilm --components title", "code_revision": "1234567", "seed": None, "started_at": "2026-05-14T00:00:00Z", diff --git a/tests/test_enrich_stage.py b/tests/test_enrich_stage.py index 56688d64..8bfe0407 100644 --- a/tests/test_enrich_stage.py +++ b/tests/test_enrich_stage.py @@ -767,7 +767,7 @@ def test_cache_version_mismatch_exits_seven(self) -> None: mock.patch.object( enrich_stage, "_load_cache_entry", - side_effect=__import__("ohbm2026").exceptions.CacheVersionError("stale cache"), + side_effect=__import__("abstractatlas").exceptions.CacheVersionError("stale cache"), ) ) with _StackedPatches(patches):