diff --git a/tests/test_arelle_cache.py b/tests/test_arelle_cache.py
index 395c8b3..37a8cd2 100644
--- a/tests/test_arelle_cache.py
+++ b/tests/test_arelle_cache.py
@@ -355,6 +355,102 @@ def test_schema_hosts_are_fetched_over_https() -> None:
"""
+ACME_URL = "http://acme.example/tax/acme.xsd"
+
+ACME_SCHEMA = """
+
+
+
+
+"""
+
+ACME_REPORT = """
+
+
+
+
+ 00000000000000000000
+
+ 2025-12-31
+
+ iso4217:EUR
+ 100
+
+"""
+
+
+def _acme_package(root: Path, name: str, concept: str) -> tuple[Path, Path]:
+ """A taxonomy package whose catalog maps ``ACME_URL`` to its own schema,
+ which declares one ``concept``, and a report tagged with it. Returns the
+ report and the manifest that registers the package."""
+ package = root / name
+ for folder in ("META-INF", "tax", "reports"):
+ (package / folder).mkdir(parents=True)
+ manifest = package / "META-INF" / "taxonomyPackage.xml"
+ manifest.write_text(
+ ""
+ f"http://acme.example/{name}"
+ f"{name}{name}"
+ "1"
+ )
+ (package / "META-INF" / "catalog.xml").write_text(
+ ""
+ ""
+ ""
+ )
+ (package / "tax" / "acme.xsd").write_text(ACME_SCHEMA.format(concept=concept))
+ report = package / "reports" / "report.xml"
+ report.write_text(ACME_REPORT.format(concept=concept))
+ return report, manifest
+
+
+def _defined_facts(report: Path, cache: Path, packages: list[Path]) -> list[str]:
+ """The facts of ``report`` that found their concept, loaded offline."""
+ mx = load_model(report, cache, offline=True, packages=packages)
+ try:
+ return [f.qname.localName for f in mx.facts if f.concept is not None]
+ finally:
+ arelle_load.close(mx.modelManager.cntlr)
+
+
+class TestPackagesAreRegisteredPerLoad:
+ """Arelle keeps registered packages for the life of the process; a load
+ must resolve through its own and no earlier load's."""
+
+ def test_a_report_without_a_package_does_not_read_an_earlier_one(
+ self, tmp_path: Path
+ ) -> None:
+ report, manifest = _acme_package(tmp_path, "first", "Revenue")
+ assert _defined_facts(report, tmp_path / "cache", [manifest]) == ["Revenue"]
+
+ alone = tmp_path / "alone.xml"
+ alone.write_text(ACME_REPORT.format(concept="Revenue"))
+ with pytest.raises(DtsResolutionError) as excinfo:
+ load_model(alone, tmp_path / "cache", offline=True)
+ assert excinfo.value.unresolved == [ACME_URL]
+
+ def test_a_package_read_again_is_not_read_through_a_later_one(
+ self, tmp_path: Path
+ ) -> None:
+ """Two packages that map the same URL to different schemas."""
+ first, first_manifest = _acme_package(tmp_path, "first", "Revenue")
+ second, second_manifest = _acme_package(tmp_path, "second", "Assets")
+ cache = tmp_path / "cache"
+
+ assert _defined_facts(first, cache, [first_manifest]) == ["Revenue"]
+ assert _defined_facts(second, cache, [second_manifest]) == ["Assets"]
+ assert _defined_facts(first, cache, [first_manifest]) == ["Revenue"]
+
+
class TestLoadModel:
def test_offline_with_a_cold_cache_refuses_the_partial_dts(
self, tmp_path: Path
diff --git a/xbrlkit/parse/README.md b/xbrlkit/parse/README.md
index 5d664aa..c02d3da 100644
--- a/xbrlkit/parse/README.md
+++ b/xbrlkit/parse/README.md
@@ -88,3 +88,7 @@ it references the filer's extension taxonomy at their own domain. Point at the
those URLs resolve to the schema travelling beside the report. Register the
*archive*, not its manifest: one Dutch package's manifest raises inside Arelle
where the same package as a zip registers cleanly.
+
+A package is registered for the one load. Arelle keeps its registrations for
+the life of the process, so `load_model` clears them first: a later filing
+never resolves through an earlier filing's catalog.
diff --git a/xbrlkit/parse/arelle_load.py b/xbrlkit/parse/arelle_load.py
index 08e168d..09c01ea 100644
--- a/xbrlkit/parse/arelle_load.py
+++ b/xbrlkit/parse/arelle_load.py
@@ -201,11 +201,17 @@ def _is_gone(url: str) -> bool:
def _register_packages(cntlr: Any, packages: Sequence[str | Path]) -> None:
- """Register taxonomy packages so their catalogs remap the URLs they own."""
- if not packages:
- return
+ """Register taxonomy packages so their catalogs remap the URLs they own.
+
+ Arelle keeps what it has registered for the life of the process, so it is
+ cleared first: a load resolves through its own packages and no earlier
+ load's.
+ """
from arelle import PackageManager
+ PackageManager.reset()
+ if not packages:
+ return
PackageManager.init(cntlr, loadPackagesConfig=False)
added = False
for package in packages: