1
0
Fork 0
Code Issues Pull requests Projects Releases 2 Packages Wiki Activity Actions Pages

Add deterministic incremental adapter assembly

This commit is contained in:
Andraxion 2026-07-27 16:01:40 -04:00
parent 09c09300b1
commit cd54cae71d
6 changed files with 224 additions and 38 deletions

View file

@ -1,15 +1,15 @@
# Active slice # Active slice
```text ```text
Slice: DFG-21 language-neutral project onboarding Slice: DFG-22 deterministic incremental adapter assembly
Goal: Let an unfamiliar codebase assess DocForge readiness and create a valid generic manual without implying that detected source languages already have semantic extraction. Goal: Let language frontends cache repeated raw source evidence while publishing one deterministic graph without maintaining a second project-owned extraction cache.
In scope: Read-only repository assessment; deterministic language and build-evidence detection; explicit multi-language selection; safe generic manual scaffolding; immediate index and render; a detailed language-neutral onboarding and adapter checklist; focused and complete quality proof. In scope: Optional generic assembly contract; cached contribution assembly; manifest-bound identity checks; final Logic ownership checks; overlapping-evidence tests; incremental documentation and onboarding guidance.
Out of scope: Bundled C++, Rust, Java, or other source frontends; dependency installation; project builds; Git mutation through DocForge; MCP self-installation; automatic canonical-document import; inferred source-to-manual relationships; deployment or publication. Out of scope: Language-specific merge rules; bundled C++, Rust, Java, or other frontends; changes to the default unique-ownership path; hidden source reads during assembly; project-owned cache formats; source mutation; deployment or publication.
Done when: Assessment writes nothing, excluded trees and symlinks are ignored, mixed-language evidence is reported deterministically, scaffolding refuses conflicts and unsafe paths, a new project validates, indexes, and renders immediately, source graph status remains adapter_required, documentation explains the complete frontend and proof path, and the full DocForge quality gate passes. Done when: An adapter can cache overlapping source contributions, deterministically assemble one valid node, reuse the warm cache, reparse only one changed source, preserve the selected published fact, pass full/incremental equivalence, and the complete DocForge quality gate passes.
Owners: DocForge owns generic assessment, scaffolding, graph contracts, incremental compilation, rendering, visualization, and MCP boundaries. Each project or reusable frontend owns language-specific source discovery and semantic extraction. Canonical project files retain authority. Owners: DocForge owns cached contribution delivery, the optional assembly boundary, identity validation, and final graph validation. The adapter owns deterministic evidence selection and merge semantics. Canonical source files remain authoritative.
Proof: The focused onboarding, CLI, and core suite passed 19 tests and 2 subtests. Strict Pyright passed with no errors or warnings. Ruff lint and formatting, Python compilation, and the HTML/CSS/JavaScript quality gate passed. The complete warning-strict suite passed 81 tests and 2 subtests. The scaffold test creates a Rust project, writes a confined generic configuration without replacement, builds one authoritative node, renders the manual, reopens the project, and retains source_graph_status=adapter_required. Proof: The focused adapter and onboarding suite passed 14 tests. Strict Pyright passed with no errors or warnings. Ruff lint and formatting, Python compilation, and the HTML/CSS/JavaScript quality gate passed. The complete warning-strict suite passed 82 tests and 2 subtests. The overlap fixture cached two source contributions, published one deterministic node, reused both warm entries, reparsed only one changed source, preserved the selected node, and passed full/incremental equivalence.
``` ```
**Next gate:** Prove Worldforge's C++ integration against the generic incremental contract. **Next gate:** Prove Worldforge's C++ integration against the generic incremental and assembly
Extract a reusable language frontend only after a second consumer demonstrates which behavior is contracts. Extract a reusable language frontend only after a second consumer demonstrates which
genuinely shared. behavior is genuinely shared.

View file

@ -1,5 +1,28 @@
# Completed slices # Completed slices
## DFG-22 deterministic incremental adapter assembly
### Changed
- Added an optional language-neutral assembly contract after incremental source extraction.
- Kept raw source contributions inside DocForge's existing fingerprint, dependency invalidation,
cache, and atomic publication path.
- Required assembled projections to retain the manifest-bound identity, revision, and source hash.
- Validated the assembled primary graph and function Logic owners before publication.
- Preserved the stricter unique-source ownership path for adapters that do not need assembly.
- Documented assembly for compilers and language tools that repeat shared declarations across
extraction units.
### Verification
- The overlap fixture caches two repeated source contributions, publishes one deterministic node,
reuses both warm cache entries, reparses one changed source, preserves the selected fact, and
passes full/incremental equivalence.
- Focused adapter and onboarding tests passed 14 tests.
- Strict Pyright, Ruff lint and formatting, Python compilation, and the HTML/CSS/JavaScript quality
gate passed.
- The complete warning-strict suite passed 82 tests and 2 subtests.
## DFG-21 language-neutral project onboarding ## DFG-21 language-neutral project onboarding
### Changed ### Changed

View file

@ -64,6 +64,25 @@ Each `AdapterSourceProjection` owns:
Ownership must be deterministic. Two sources may not produce the same primary node or the same Ownership must be deterministic. Two sources may not produce the same primary node or the same
function logic projection. function logic projection.
Some language tools emit overlapping raw evidence before ownership can be resolved. An incremental
loader may additionally implement:
```python
def assemble_projection(
manifest: AdapterManifest,
contributions: tuple[AdapterSourceProjection, ...],
) -> AdapterAssembly: ...
```
DocForge caches and invalidates the source contributions normally, then passes the complete current
contribution set to this pure assembly step. The assembler must deterministically select or merge
overlapping evidence and return one valid final graph and Logic set. It may not read hidden source
state or create a second extraction cache. The final identity, revision, and source hash must match
the manifest exactly.
Without an assembler, the stricter default remains in force: two contributions may not publish the
same primary node or Logic owner.
## Invalidation ## Invalidation
DocForge invalidates a source when: DocForge invalidates a source when:

View file

@ -123,6 +123,7 @@ All frontends emit the same DocForge contracts:
- `AdapterManifest` inventories fingerprinted extraction units and dependencies. - `AdapterManifest` inventories fingerprinted extraction units and dependencies.
- `AdapterSourceProjection` owns nodes, relationships, and optional function Logic for one unit. - `AdapterSourceProjection` owns nodes, relationships, and optional function Logic for one unit.
- `AdapterProjection` provides the deterministic complete rebuild. - `AdapterProjection` provides the deterministic complete rebuild.
- `AdapterAssembly` optionally resolves overlapping raw evidence into the single published graph.
Language metadata may differ. Graph publication, indexing, querying, visualization, and MCP Language metadata may differ. Graph publication, indexing, querying, visualization, and MCP
behavior do not. behavior do not.
@ -130,6 +131,11 @@ behavior do not.
Done when repeated extraction produces the same stable identities without inferred or guessed Done when repeated extraction produces the same stable identities without inferred or guessed
facts. facts.
When compiler or language tooling repeats shared declarations across extraction units, use the
optional assembly contract. Cache the raw source contributions through DocForge, then
deterministically select or merge ownership from the complete contribution set. Do not hide a
second extraction cache inside the project adapter.
### 5. Build-system evidence ### 5. Build-system evidence
#### C and C++ #### C and C++

View file

@ -12,6 +12,7 @@ from typing import Protocol, runtime_checkable
from .adapter_validation import ( from .adapter_validation import (
source_payload, source_payload,
source_projection, source_projection,
validate_logic_projection,
validate_manifest, validate_manifest,
validate_projection, validate_projection,
validate_source_projection, validate_source_projection,
@ -136,6 +137,14 @@ class AdapterSourceProjection:
logic: tuple[LogicProjection, ...] = () logic: tuple[LogicProjection, ...] = ()
@dataclass(frozen=True)
class AdapterAssembly:
"""One finalized graph and Logic set assembled from cached source contributions."""
projection: AdapterProjection
logic: tuple[LogicProjection, ...] = ()
class AdapterLoader(Protocol): class AdapterLoader(Protocol):
"""Load one current, deterministic, project-confined adapter projection.""" """Load one current, deterministic, project-confined adapter projection."""
@ -151,6 +160,17 @@ class IncrementalAdapterLoader(AdapterLoader, Protocol):
def extract_source(self, source: AdapterSource) -> AdapterSourceProjection: ... def extract_source(self, source: AdapterSource) -> AdapterSourceProjection: ...
@runtime_checkable
class IncrementalAdapterAssembler(Protocol):
"""Optionally normalize overlapping source evidence into one final projection."""
def assemble_projection(
self,
manifest: AdapterManifest,
contributions: tuple[AdapterSourceProjection, ...],
) -> AdapterAssembly: ...
ProposalValidator = Callable[ ProposalValidator = Callable[
[ [
ProjectSnapshot, ProjectSnapshot,
@ -503,6 +523,16 @@ class AdapterProject:
validate_source_projection(source, contribution) validate_source_projection(source, contribution)
contributions.append(contribution) contributions.append(contribution)
cache_records.append(cache_record) cache_records.append(cache_record)
if isinstance(loader, IncrementalAdapterAssembler):
assembly = loader.assemble_projection(manifest, tuple(contributions))
projection = assembly.projection
logic_projections = tuple(
sorted(
assembly.logic,
key=lambda projection: projection.owner_node_id,
)
)
else:
projection = AdapterProjection( projection = AdapterProjection(
project_id=manifest.project_id, project_id=manifest.project_id,
title=manifest.title, title=manifest.title,
@ -528,18 +558,43 @@ class AdapterProject:
) )
), ),
) )
validate_projection(projection)
logic_projections = tuple( logic_projections = tuple(
sorted( sorted(
(logic for contribution in contributions for logic in contribution.logic), (logic for contribution in contributions for logic in contribution.logic),
key=lambda projection: projection.owner_node_id, key=lambda projection: projection.owner_node_id,
) )
) )
identity = (
projection.project_id,
projection.adapter_id,
projection.adapter_version,
projection.root,
)
if (
identity != manifest.identity()
or projection.title != manifest.title
or projection.revision != manifest.revision
or projection.source_hash != manifest.source_hash
):
raise DocForgeError(
"invalid_adapter",
"Incremental assembly changed the manifest-bound project identity",
)
validate_projection(projection)
owners = [projection.owner_node_id for projection in logic_projections] owners = [projection.owner_node_id for projection in logic_projections]
if len(owners) != len(set(owners)): if len(owners) != len(set(owners)):
raise DocForgeError( raise DocForgeError(
"invalid_adapter", "A primary graph node may own only one logic projection" "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,
)
stable = loader.load_manifest() stable = loader.load_manifest()
validate_manifest(stable) validate_manifest(stable)
if stable != manifest: if stable != manifest:

View file

@ -12,6 +12,7 @@ from pathlib import Path
from mcp.shared.memory import create_connected_server_and_client_session from mcp.shared.memory import create_connected_server_and_client_session
from docforge.adapter_contract import ( from docforge.adapter_contract import (
AdapterAssembly,
AdapterEdge, AdapterEdge,
AdapterManifest, AdapterManifest,
AdapterNode, AdapterNode,
@ -182,6 +183,56 @@ class IncrementalLoader:
) )
class OverlappingIncrementalLoader(IncrementalLoader):
def extract_source(self, source: AdapterSource) -> AdapterSourceProjection:
self.extract_calls.append(source.source_id)
content = self.sources[source.source_id]
shared = Node(
node_id="guide.shared",
title="Shared",
family="guide",
authority="derived",
status="active",
tags=("guide",),
summary=f"Evidence selected from {source.source_id}.",
content=content,
source_path=source.source_path,
source_anchor=None,
content_hash=self._hash(content),
)
return AdapterSourceProjection(
source_id=source.source_id,
fingerprint=source.fingerprint,
nodes=(AdapterNode(shared),),
edges=(),
)
def assemble_projection(
self,
manifest: AdapterManifest,
contributions: tuple[AdapterSourceProjection, ...],
) -> AdapterAssembly:
selected = min(contributions, key=lambda item: item.source_id)
return AdapterAssembly(
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=selected.nodes,
edges=(),
)
)
def load_projection(self) -> AdapterProjection:
manifest = self.load_manifest()
contributions = tuple(self.extract_source(source) for source in manifest.sources)
return self.assemble_projection(manifest, contributions).projection
class AdapterContractTests(unittest.TestCase): class AdapterContractTests(unittest.TestCase):
def projection(self, root: Path) -> AdapterProjection: def projection(self, root: Path) -> AdapterProjection:
foundation = Node( foundation = Node(
@ -426,6 +477,38 @@ class AdapterContractTests(unittest.TestCase):
self.assertEqual("ok", equivalent["status"]) self.assertEqual("ok", equivalent["status"])
self.assertEqual(1, equivalent["node_count"]) self.assertEqual(1, equivalent["node_count"])
def test_incremental_adapter_can_assemble_overlapping_source_evidence(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory).resolve()
loader = OverlappingIncrementalLoader(root)
project = AdapterProject(loader, cache_root=root / ".cache" / "overlap")
index = ProjectIndex(project)
first = index.build()
self.assertEqual(2, first["build"]["reparsed_sources"])
self.assertEqual(1, first["node_count"])
self.assertEqual(
"Foundation content.",
index.get_node("guide.shared")["node"]["content"],
)
loader.extract_calls.clear()
warm = index.build()
self.assertEqual(2, warm["build"]["cache_hits"])
self.assertEqual([], loader.extract_calls)
loader.sources["guide.workflow"] = "Changed overlapping evidence."
loader.extract_calls.clear()
changed = index.build()
self.assertEqual(["guide.workflow"], loader.extract_calls)
self.assertEqual(1, changed["build"]["reparsed_sources"])
self.assertEqual(1, changed["node_count"])
self.assertEqual(
"Foundation content.",
index.get_node("guide.shared")["node"]["content"],
)
self.assertEqual("ok", project.verify_incremental_equivalence()["status"])
def test_artifact_comparison_is_complete_and_byte_exact(self) -> None: def test_artifact_comparison_is_complete_and_byte_exact(self) -> None:
reference = ( reference = (
ShadowArtifact("manual", b"same"), ShadowArtifact("manual", b"same"),