From e6523c0c00e5d213b5c4df52e00035a2b88a03ba Mon Sep 17 00:00:00 2001 From: Andraxion Date: Wed, 29 Jul 2026 14:23:29 -0400 Subject: [PATCH] Add adapter SDK conformance proofs --- src/docforge/adapter_contract.py | 251 +++++++++++++++++++++++++---- src/docforge/adapter_sdk.py | 69 ++++++++ tests/test_adapter_contract.py | 70 +++++++++ tests/test_adapter_sdk.py | 261 +++++++++++++++++++++++++++++++ tests/test_public_contract.py | 17 ++ 5 files changed, 634 insertions(+), 34 deletions(-) create mode 100644 src/docforge/adapter_sdk.py create mode 100644 tests/test_adapter_sdk.py diff --git a/src/docforge/adapter_contract.py b/src/docforge/adapter_contract.py index 7dd1445..1b396b8 100644 --- a/src/docforge/adapter_contract.py +++ b/src/docforge/adapter_contract.py @@ -147,6 +147,13 @@ class AdapterAssembly: logic: tuple[LogicProjection, ...] = () +@runtime_checkable +class CompleteAdapterAssemblyLoader(Protocol): + """Load one cache-independent complete graph and Logic equivalence oracle.""" + + def load_complete_assembly(self) -> AdapterAssembly: ... + + class AdapterLoader(Protocol): """Load one current, deterministic, project-confined adapter projection.""" @@ -201,6 +208,14 @@ def _extract_adapter_source( return loader.extract_source(source) +def _load_complete_adapter_assembly( + loader: CompleteAdapterAssemblyLoader, +) -> AdapterAssembly: + increment("adapter_projection_loads") + with stage("adapter.projection"): + return loader.load_complete_assembly() + + @dataclass(frozen=True) class AdapterImplementation: """One confined implementation boundary that must remain stable for a process.""" @@ -232,6 +247,38 @@ class _ImplementationSnapshot: files: tuple[tuple[str, str], ...] +@dataclass(frozen=True) +class AdapterConformanceReport: + """Stable evidence that one adapter passed its complete and incremental contracts.""" + + schema_version: int + project_id: str + adapter_id: str + adapter_version: str + revision: str + source_hash: str + assembly_hash: str + node_count: int + edge_count: int + logic_projection_count: int + incremental: bool + + def as_dict(self) -> dict[str, object]: + return { + "schema_version": self.schema_version, + "project_id": self.project_id, + "adapter_id": self.adapter_id, + "adapter_version": self.adapter_version, + "revision": self.revision, + "source_hash": self.source_hash, + "assembly_hash": self.assembly_hash, + "node_count": self.node_count, + "edge_count": self.edge_count, + "logic_projection_count": self.logic_projection_count, + "incremental": self.incremental, + } + + class AdapterProject: """Expose a validated adapter projection through the standard index boundary.""" @@ -523,43 +570,57 @@ class AdapterProject: return self._last_logic def verify_incremental_equivalence(self) -> dict[str, object]: - """Prove the incremental and full loader contracts produce the same graph.""" + """Prove incremental graph and Logic output matches an independent complete oracle.""" self.validate_runtime() - if self._incremental_loader is None: + loader = self._incremental_loader + if loader is None: raise DocForgeError( "incremental_disabled", "Adapter does not implement incremental extraction" ) - incremental = self._load_incremental() + incremental = self._load_incremental_assembly() full = _load_adapter_projection(self.loader) validate_projection(full) - fields = { - "project_id": incremental.project_id == full.project_id, - "adapter_id": incremental.adapter_id == full.adapter_id, - "adapter_version": incremental.adapter_version == full.adapter_version, - "revision": incremental.revision == full.revision, - "source_hash": incremental.source_hash == full.source_hash, - "nodes": incremental.nodes == full.nodes, - "edges": incremental.edges == full.edges, - } + complete = AdapterAssembly(full) + if isinstance(loader, CompleteAdapterAssemblyLoader): + complete = _load_complete_adapter_assembly(loader) + _validate_adapter_assembly(complete) + oracle_fields = _projection_equivalence_fields(complete.projection, full) + oracle_mismatches = [field for field, matches in oracle_fields.items() if not matches] + if oracle_mismatches: + raise DocForgeError( + "complete_oracle_mismatch", + "Complete adapter assembly does not match load_projection()", + fields=oracle_mismatches, + ) + elif incremental.logic: + raise DocForgeError( + "complete_logic_oracle_required", + "Incremental Logic publication requires load_complete_assembly()", + ) + fields = _assembly_equivalence_fields(incremental, complete) mismatches = [field for field, matches in fields.items() if not matches] if mismatches: raise DocForgeError( "incremental_mismatch", - "Incremental extraction does not match a full adapter projection", + "Incremental extraction does not match a complete adapter assembly", fields=mismatches, ) self.validate_runtime() return { "status": "ok", - "project_id": incremental.project_id, - "revision": incremental.revision, - "source_hash": incremental.source_hash, - "node_count": len(incremental.nodes), - "edge_count": len(incremental.edges), + "project_id": incremental.projection.project_id, + "revision": incremental.projection.revision, + "source_hash": incremental.projection.source_hash, + "node_count": len(incremental.projection.nodes), + "edge_count": len(incremental.projection.edges), + "logic_projection_count": len(incremental.logic), } def _load_incremental(self) -> AdapterProjection: + return self._load_incremental_assembly().projection + + def _load_incremental_assembly(self) -> AdapterAssembly: loader = self._incremental_loader if loader is None: raise DocForgeError("incremental_disabled", "Incremental adapter is not configured") @@ -658,6 +719,7 @@ class AdapterProject: key=lambda projection: projection.owner_node_id, ) ) + assembly = AdapterAssembly(projection=projection, logic=logic_projections) identity = ( projection.project_id, projection.adapter_id, @@ -674,21 +736,7 @@ class AdapterProject: "invalid_adapter", "Incremental assembly changed the manifest-bound project identity", ) - validate_projection(projection) - owners = [projection.owner_node_id for projection in logic_projections] - if len(owners) != len(set(owners)): - raise DocForgeError( - "invalid_adapter", "A primary graph node may own only one logic projection" - ) - node_ids = {item.node.node_id for item in projection.nodes} - for logic_projection in logic_projections: - validate_logic_projection(logic_projection) - if logic_projection.owner_node_id not in node_ids: - raise DocForgeError( - "invalid_adapter", - "A Logic projection owner must exist in the assembled primary graph", - owner_node_id=logic_projection.owner_node_id, - ) + _validate_adapter_assembly(assembly) stable = loader.load_manifest() validate_manifest(stable) if stable != manifest: @@ -716,7 +764,7 @@ class AdapterProject: "cache_hit_ids": hits, "reparsed_source_ids": reparsed, } - return projection + return assembly def canonical_source_paths(self) -> tuple[Path, ...]: """Adapters validate their own source sets before producing a projection.""" @@ -1032,6 +1080,141 @@ class AdapterProject: outputs.add(output) +def _validate_adapter_assembly(assembly: AdapterAssembly) -> None: + validate_projection(assembly.projection) + ordered_logic = tuple(sorted(assembly.logic, key=lambda projection: projection.owner_node_id)) + if assembly.logic != ordered_logic: + raise DocForgeError( + "invalid_adapter", + "Complete Logic projections must be deterministically ordered", + ) + owners = [projection.owner_node_id for projection in assembly.logic] + if len(owners) != len(set(owners)): + raise DocForgeError( + "invalid_adapter", "A primary graph node may own only one logic projection" + ) + node_ids = {item.node.node_id for item in assembly.projection.nodes} + for logic_projection in assembly.logic: + validate_logic_projection(logic_projection) + if logic_projection.owner_node_id not in node_ids: + raise DocForgeError( + "invalid_adapter", + "A Logic projection owner must exist in the assembled primary graph", + owner_node_id=logic_projection.owner_node_id, + ) + + +def _projection_equivalence_fields( + candidate: AdapterProjection, + reference: AdapterProjection, +) -> dict[str, bool]: + return { + "project_id": candidate.project_id == reference.project_id, + "title": candidate.title == reference.title, + "adapter_id": candidate.adapter_id == reference.adapter_id, + "adapter_version": candidate.adapter_version == reference.adapter_version, + "root": candidate.root == reference.root, + "revision": candidate.revision == reference.revision, + "source_hash": candidate.source_hash == reference.source_hash, + "nodes": candidate.nodes == reference.nodes, + "edges": candidate.edges == reference.edges, + } + + +def _assembly_equivalence_fields( + candidate: AdapterAssembly, + reference: AdapterAssembly, +) -> dict[str, bool]: + return { + **_projection_equivalence_fields(candidate.projection, reference.projection), + "logic": candidate.logic == reference.logic, + } + + +def _assembly_hash(assembly: AdapterAssembly) -> str: + projection = assembly.projection + payload = { + "projection": { + "project_id": projection.project_id, + "title": projection.title, + "adapter_id": projection.adapter_id, + "adapter_version": projection.adapter_version, + "root": str(projection.root), + "revision": projection.revision, + "source_hash": projection.source_hash, + "nodes": [item.as_dict() for item in projection.nodes], + "edges": [item.as_dict() for item in projection.edges], + }, + "logic": [logic_projection.as_dict() for logic_projection in assembly.logic], + } + return hashlib.sha256( + json.dumps(payload, sort_keys=True, separators=(",", ":")).encode() + ).hexdigest() + + +def _complete_reference_assembly(loader: AdapterLoader) -> AdapterAssembly: + if isinstance(loader, CompleteAdapterAssemblyLoader): + assembly = _load_complete_adapter_assembly(loader) + else: + assembly = AdapterAssembly(_load_adapter_projection(loader)) + _validate_adapter_assembly(assembly) + return assembly + + +def verify_adapter_conformance( + loader: AdapterLoader, + *, + cache_root: Path, + settings: AdapterProjectSettings | None = None, +) -> AdapterConformanceReport: + """Prove deterministic complete output and, when supported, incremental parity.""" + + project = AdapterProject(loader, cache_root=cache_root, settings=settings) + project.validate_runtime() + first = _complete_reference_assembly(loader) + second = _complete_reference_assembly(loader) + deterministic_fields = _assembly_equivalence_fields(first, second) + nondeterministic = [field for field, matches in deterministic_fields.items() if not matches] + if nondeterministic: + raise DocForgeError( + "nondeterministic_adapter", + "Repeated complete adapter assemblies do not match", + fields=nondeterministic, + ) + + if isinstance(loader, CompleteAdapterAssemblyLoader): + graph_oracle = _load_adapter_projection(loader) + validate_projection(graph_oracle) + oracle_fields = _projection_equivalence_fields(first.projection, graph_oracle) + oracle_mismatches = [field for field, matches in oracle_fields.items() if not matches] + if oracle_mismatches: + raise DocForgeError( + "complete_oracle_mismatch", + "Complete adapter assembly does not match load_projection()", + fields=oracle_mismatches, + ) + + incremental = isinstance(loader, IncrementalAdapterLoader) + if incremental: + project.verify_incremental_equivalence() + + project.validate_runtime() + projection = first.projection + return AdapterConformanceReport( + schema_version=1, + project_id=projection.project_id, + adapter_id=projection.adapter_id, + adapter_version=projection.adapter_version, + revision=projection.revision, + source_hash=projection.source_hash, + assembly_hash=_assembly_hash(first), + node_count=len(projection.nodes), + edge_count=len(projection.edges), + logic_projection_count=len(first.logic), + incremental=incremental, + ) + + @dataclass(frozen=True) class ShadowArtifact: """One named deterministic byte artifact used by a shadow comparison.""" diff --git a/src/docforge/adapter_sdk.py b/src/docforge/adapter_sdk.py new file mode 100644 index 0000000..888f7f2 --- /dev/null +++ b/src/docforge/adapter_sdk.py @@ -0,0 +1,69 @@ +"""Public typed surface for project adapter implementations and conformance checks.""" + +from .adapter_contract import ( + AdapterAssembly, + AdapterConformanceReport, + AdapterEdge, + AdapterImplementation, + AdapterLoader, + AdapterManifest, + AdapterNode, + AdapterProject, + AdapterProjection, + AdapterProjectSettings, + AdapterSource, + AdapterSourceProjection, + CompleteAdapterAssemblyLoader, + IncrementalAdapterAssembler, + IncrementalAdapterLoader, + verify_adapter_conformance, +) +from .adapter_validation import ( + validate_logic_projection, + validate_manifest, + validate_projection, + validate_source_projection, +) +from .models import ( + Edge, + Limits, + LogicEdge, + LogicNode, + LogicProjection, + Node, + ProposalWriter, + RenderConfig, + RenderView, +) + +__all__ = [ + "AdapterAssembly", + "AdapterConformanceReport", + "AdapterEdge", + "AdapterImplementation", + "AdapterLoader", + "AdapterManifest", + "AdapterNode", + "AdapterProject", + "AdapterProjection", + "AdapterProjectSettings", + "AdapterSource", + "AdapterSourceProjection", + "CompleteAdapterAssemblyLoader", + "Edge", + "IncrementalAdapterAssembler", + "IncrementalAdapterLoader", + "Limits", + "LogicEdge", + "LogicNode", + "LogicProjection", + "Node", + "ProposalWriter", + "RenderConfig", + "RenderView", + "validate_logic_projection", + "validate_manifest", + "validate_projection", + "validate_source_projection", + "verify_adapter_conformance", +] diff --git a/tests/test_adapter_contract.py b/tests/test_adapter_contract.py index e4bc3f5..2d5bc3f 100644 --- a/tests/test_adapter_contract.py +++ b/tests/test_adapter_contract.py @@ -248,6 +248,36 @@ class NonLogicIncrementalLoader(IncrementalLoader): return replace(super().extract_source(source), logic=()) +class CompleteLogicIncrementalLoader(IncrementalLoader): + """Supply an independent complete Logic oracle alongside the legacy graph oracle.""" + + def __init__(self, root: Path, *, mismatched_logic: bool = False) -> None: + super().__init__(root) + self.mismatched_logic = mismatched_logic + + def load_complete_assembly(self) -> AdapterAssembly: + projection = super().load_projection() + if "guide.workflow" not in self.sources: + return AdapterAssembly(projection) + manifest = self.load_manifest() + workflow = next( + source for source in manifest.sources if source.source_id == "guide.workflow" + ) + logic = self.extract_source(workflow).logic + if self.mismatched_logic: + current = logic[0] + logic = ( + replace( + current, + nodes=( + current.nodes[0], + replace(current.nodes[1], label="Different complete return"), + ), + ), + ) + return AdapterAssembly(projection, logic) + + class AdapterContractTests(unittest.TestCase): def projection(self, root: Path) -> AdapterProjection: foundation = Node( @@ -782,6 +812,46 @@ class AdapterContractTests(unittest.TestCase): self.assertEqual("ok", equivalent["status"]) self.assertEqual(1, equivalent["node_count"]) + def test_logic_equivalence_requires_an_independent_complete_oracle(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory).resolve() + project = AdapterProject( + IncrementalLoader(root), + cache_root=root / ".cache" / "logic-without-oracle", + ) + + with self.assertRaises(DocForgeError) as captured: + project.verify_incremental_equivalence() + + self.assertEqual("complete_logic_oracle_required", captured.exception.code) + + def test_logic_equivalence_uses_the_complete_assembly_oracle(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory).resolve() + project = AdapterProject( + CompleteLogicIncrementalLoader(root), + cache_root=root / ".cache" / "logic-oracle", + ) + + result = project.verify_incremental_equivalence() + + self.assertEqual("ok", result["status"]) + self.assertEqual(1, result["logic_projection_count"]) + + def test_logic_equivalence_reports_exact_mismatch_field(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory).resolve() + project = AdapterProject( + CompleteLogicIncrementalLoader(root, mismatched_logic=True), + cache_root=root / ".cache" / "logic-mismatch", + ) + + with self.assertRaises(DocForgeError) as captured: + project.verify_incremental_equivalence() + + self.assertEqual("incremental_mismatch", captured.exception.code) + self.assertEqual(["logic"], captured.exception.details["fields"]) + def test_incremental_adapter_can_assemble_overlapping_source_evidence(self) -> None: with tempfile.TemporaryDirectory() as directory: root = Path(directory).resolve() diff --git a/tests/test_adapter_sdk.py b/tests/test_adapter_sdk.py new file mode 100644 index 0000000..059d095 --- /dev/null +++ b/tests/test_adapter_sdk.py @@ -0,0 +1,261 @@ +from __future__ import annotations + +import hashlib +import tempfile +import unittest +from dataclasses import replace +from pathlib import Path + +import docforge.adapter_sdk as sdk +from docforge.errors import DocForgeError + + +class ReferenceIncrementalAdapter: + """Small public-SDK-only adapter used to prove the conformance entry point.""" + + def __init__(self, root: Path) -> None: + self.root = root + + @staticmethod + def _hash(value: str) -> str: + return hashlib.sha256(value.encode()).hexdigest() + + def _node(self) -> sdk.AdapterNode: + content = "A deterministic reference function." + return sdk.AdapterNode( + sdk.Node( + node_id="reference.run", + title="Reference run", + family="source", + authority="derived", + status="current", + tags=("python",), + summary=content, + content=content, + source_path="src/reference.py", + source_anchor="L1", + content_hash=self._hash(content), + ) + ) + + @staticmethod + def _logic() -> sdk.LogicProjection: + return sdk.LogicProjection( + owner_node_id="reference.run", + source_id="source.reference", + nodes=( + sdk.LogicNode("entry", "entry", "Enter run", "L1"), + sdk.LogicNode("return", "return", "Return", "L2"), + ), + edges=(sdk.LogicEdge("entry", "return", "return", "RETURN", 0),), + ) + + def load_manifest(self) -> sdk.AdapterManifest: + fingerprint = self._hash("def run(): return True\n") + return sdk.AdapterManifest( + project_id="sdk-reference", + title="SDK reference", + adapter_id="python-reference", + adapter_version="1", + root=self.root, + revision=fingerprint[:12], + source_hash=fingerprint, + families=("source",), + allowed_relations=(), + sources=( + sdk.AdapterSource( + source_id="source.reference", + source_path="src/reference.py", + fingerprint=fingerprint, + extractor_version="python-reference@1", + ), + ), + estimated_nodes=1, + ) + + def extract_source(self, source: sdk.AdapterSource) -> sdk.AdapterSourceProjection: + return sdk.AdapterSourceProjection( + source_id=source.source_id, + fingerprint=source.fingerprint, + nodes=(self._node(),), + edges=(), + logic=(self._logic(),), + ) + + def load_projection(self) -> sdk.AdapterProjection: + manifest = self.load_manifest() + return sdk.AdapterProjection( + project_id=manifest.project_id, + title=manifest.title, + adapter_id=manifest.adapter_id, + adapter_version=manifest.adapter_version, + root=manifest.root, + revision=manifest.revision, + source_hash=manifest.source_hash, + nodes=(self._node(),), + edges=(), + ) + + def load_complete_assembly(self) -> sdk.AdapterAssembly: + return sdk.AdapterAssembly( + projection=self.load_projection(), + logic=(self._logic(),), + ) + + +class LegacyAdapter: + def __init__(self, projection: sdk.AdapterProjection) -> None: + self.projection = projection + + def load_projection(self) -> sdk.AdapterProjection: + return self.projection + + +class LogicWithoutCompleteAdapter: + def __init__(self, root: Path) -> None: + self.reference = ReferenceIncrementalAdapter(root) + + def load_manifest(self) -> sdk.AdapterManifest: + return self.reference.load_manifest() + + def extract_source(self, source: sdk.AdapterSource) -> sdk.AdapterSourceProjection: + return self.reference.extract_source(source) + + def load_projection(self) -> sdk.AdapterProjection: + return self.reference.load_projection() + + +class NondeterministicCompleteAdapter(ReferenceIncrementalAdapter): + def __init__(self, root: Path) -> None: + super().__init__(root) + self.complete_calls = 0 + + def load_complete_assembly(self) -> sdk.AdapterAssembly: + self.complete_calls += 1 + assembly = super().load_complete_assembly() + if self.complete_calls % 2 == 0: + logic = assembly.logic[0] + return replace( + assembly, + logic=( + replace( + logic, + nodes=( + logic.nodes[0], + replace(logic.nodes[1], label="Unstable return"), + ), + ), + ), + ) + return assembly + + +class AdapterSdkTests(unittest.TestCase): + def test_public_facade_contains_the_complete_authoring_surface(self) -> None: + expected = { + "AdapterAssembly", + "AdapterConformanceReport", + "AdapterManifest", + "AdapterNode", + "AdapterProjection", + "AdapterSource", + "AdapterSourceProjection", + "CompleteAdapterAssemblyLoader", + "Edge", + "LogicEdge", + "LogicNode", + "LogicProjection", + "Node", + "verify_adapter_conformance", + } + + self.assertLessEqual(expected, set(sdk.__all__)) + for name in expected: + with self.subTest(name=name): + self.assertTrue(hasattr(sdk, name)) + + def test_conformance_proves_deterministic_complete_graph_and_logic_parity(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory).resolve() + report = sdk.verify_adapter_conformance( + ReferenceIncrementalAdapter(root), + cache_root=root / ".cache" / "conformance", + ) + + self.assertEqual(1, report.schema_version) + self.assertTrue(report.incremental) + self.assertEqual(1, report.node_count) + self.assertEqual(0, report.edge_count) + self.assertEqual(1, report.logic_projection_count) + self.assertEqual(64, len(report.assembly_hash)) + self.assertEqual(report.assembly_hash, report.as_dict()["assembly_hash"]) + + def test_conformance_keeps_load_projection_only_adapters_first_class(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory).resolve() + content = "Legacy adapter content." + projection = sdk.AdapterProjection( + project_id="legacy-sdk", + title="Legacy SDK", + adapter_id="legacy", + adapter_version="1", + root=root, + revision="legacy", + source_hash=hashlib.sha256(content.encode()).hexdigest(), + nodes=( + sdk.AdapterNode( + sdk.Node( + node_id="legacy.node", + title="Legacy node", + family="guide", + authority="authoritative", + status="current", + tags=("legacy",), + summary=content, + content=content, + source_path="docs/legacy.md", + source_anchor=None, + content_hash=hashlib.sha256(content.encode()).hexdigest(), + ) + ), + ), + edges=(), + ) + + report = sdk.verify_adapter_conformance( + LegacyAdapter(projection), + cache_root=root / ".cache" / "legacy-conformance", + ) + + self.assertFalse(report.incremental) + self.assertEqual(0, report.logic_projection_count) + self.assertEqual(projection.identity(), projection.identity()) + + def test_conformance_requires_a_complete_oracle_for_incremental_logic(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory).resolve() + + with self.assertRaises(DocForgeError) as captured: + sdk.verify_adapter_conformance( + LogicWithoutCompleteAdapter(root), + cache_root=root / ".cache" / "missing-logic-oracle", + ) + + self.assertEqual("complete_logic_oracle_required", captured.exception.code) + + def test_conformance_rejects_nondeterministic_complete_logic(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory).resolve() + + with self.assertRaises(DocForgeError) as captured: + sdk.verify_adapter_conformance( + NondeterministicCompleteAdapter(root), + cache_root=root / ".cache" / "nondeterministic", + ) + + self.assertEqual("nondeterministic_adapter", captured.exception.code) + self.assertEqual(["logic"], captured.exception.details["fields"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_public_contract.py b/tests/test_public_contract.py index e32af11..f60fc83 100644 --- a/tests/test_public_contract.py +++ b/tests/test_public_contract.py @@ -43,6 +43,7 @@ PUBLIC_IMPORTS = { ), "docforge.adapter_contract": ( "AdapterAssembly", + "AdapterConformanceReport", "AdapterEdge", "AdapterImplementation", "AdapterLoader", @@ -53,8 +54,24 @@ PUBLIC_IMPORTS = { "AdapterProjectSettings", "AdapterSource", "AdapterSourceProjection", + "CompleteAdapterAssemblyLoader", "IncrementalAdapterAssembler", "IncrementalAdapterLoader", + "verify_adapter_conformance", + ), + "docforge.adapter_sdk": ( + "AdapterAssembly", + "AdapterConformanceReport", + "AdapterLoader", + "AdapterManifest", + "AdapterProject", + "AdapterProjection", + "AdapterSourceProjection", + "CompleteAdapterAssemblyLoader", + "IncrementalAdapterLoader", + "LogicProjection", + "Node", + "verify_adapter_conformance", ), "docforge.application": ( "CanonicalApplier",