Implement first knowledge engine runtime slice

This commit is contained in:
2026-05-05 01:47:19 +02:00
parent 902ba7352d
commit cca9ebe172
17 changed files with 1445 additions and 26 deletions

75
tests/test_artifacts.py Normal file
View File

@@ -0,0 +1,75 @@
from kontextual_engine import (
Artifact,
ArtifactMetadata,
ArtifactReference,
ArtifactType,
Collection,
Relationship,
RelationshipGraph,
RelationshipType,
content_digest,
)
def test_artifact_create_updates_digest_and_roundtrips() -> None:
artifact = Artifact.create(
"collection-1",
"brief",
"first version",
artifact_type=ArtifactType.DOCUMENT,
metadata=ArtifactMetadata(tags=["example"], media_type="text/plain"),
)
assert artifact.content_digest == content_digest("first version")
assert artifact.content_size == len("first version".encode("utf-8"))
artifact.update_content("second version")
assert artifact.content_digest == content_digest("second version")
restored = Artifact.from_dict(artifact.to_dict())
assert restored.id == artifact.id
assert restored.metadata.tags == ["example"]
assert restored.artifact_type == ArtifactType.DOCUMENT
def test_artifact_reference_parse_formats() -> None:
assert ArtifactReference.parse("name") == ArtifactReference(name="name")
assert ArtifactReference.parse("space:name") == ArtifactReference(
name="name",
collection_id="space",
)
assert str(ArtifactReference.parse("space:name@v1")) == "space:name@v1"
def test_collection_and_relationship_roundtrip() -> None:
collection = Collection.create("project", domain="markitect")
restored_collection = Collection.from_dict(collection.to_dict())
assert restored_collection.name == "project"
assert restored_collection.domain == "markitect"
relationship = Relationship.create(
"a",
"b",
"depends on",
relationship_type=RelationshipType.DEPENDS_ON,
evidence="test evidence",
)
assert relationship.edge() == ("a", "b", "depends on")
restored_relationship = Relationship.from_dict(relationship.to_dict())
assert restored_relationship.relationship_type == RelationshipType.DEPENDS_ON
assert restored_relationship.evidence == "test evidence"
def test_relationship_graph_detects_cycles_and_orders_dependencies() -> None:
edge_ab = Relationship.create("A", "B", "depends on")
edge_bc = Relationship.create("B", "C", "depends on")
graph = RelationshipGraph.from_relationships([edge_ab, edge_bc])
assert graph.successors("A") == {"B"}
assert graph.predecessors("C") == {"B"}
assert graph.detect_cycles() == []
assert graph.topological_sort().index("C") < graph.topological_sort().index("A")
graph.add(Relationship.create("C", "A", "depends on"))
assert graph.detect_cycles()

View File

@@ -0,0 +1,100 @@
from kontextual_engine import (
ArtifactType,
Collection,
ContextAssembler,
Diagnostic,
InMemoryKnowledgeRepository,
IngestionRequest,
IngestionService,
InputBundle,
OperationRun,
OperationStage,
Relationship,
RunManifest,
RunStatus,
WorkflowStep,
)
def test_plain_text_ingestion_creates_normalized_artifact() -> None:
service = IngestionService()
result = service.ingest(
IngestionRequest(
collection_id="collection-1",
name="note",
content="hello",
media_type="text/plain",
artifact_type=ArtifactType.CONTENT,
)
)
assert result.adapter == "plain-text"
assert result.artifacts[0].name == "note"
assert result.artifacts[0].metadata.media_type == "text/plain"
assert result.normalized["text"] == "hello"
def test_input_bundle_hash_is_deterministic() -> None:
first = InputBundle(
operation="compose",
inputs={"b": "sha256:2", "a": "sha256:1"},
options={"heading_delta": 1},
)
second = InputBundle(
operation="compose",
inputs={"a": "sha256:1", "b": "sha256:2"},
options={"heading_delta": 1},
)
assert first.calculate_hash() == second.calculate_hash()
def test_operation_run_lifecycle_and_manifest() -> None:
bundle = InputBundle(operation="ingest", inputs={"source": "sha256:abc"})
run = OperationRun.create("ingest", bundle.calculate_hash(), depth=1)
run.advance(OperationStage.RUNNING)
assert run.status == RunStatus.RUNNING
run.mark_complete()
manifest = RunManifest(
run=run,
input_bundle=bundle,
steps=[WorkflowStep(id="step-1", kind="ingest", status=RunStatus.SUCCESS)],
produced_artifact_ids=["artifact-1"],
)
data = manifest.to_dict()
assert data["run"]["status"] == "success"
assert data["input_bundle"]["input_bundle_hash"] == bundle.calculate_hash()
assert data["steps"][0]["kind"] == "ingest"
def test_operation_run_failure_records_diagnostic() -> None:
bundle = InputBundle(operation="query", inputs={})
run = OperationRun.create("query", bundle.calculate_hash())
run.mark_failed(Diagnostic(severity="error", code="test", message="failed"))
assert run.status == RunStatus.FAILED
assert run.to_dict()["diagnostics"][0]["code"] == "test"
def test_context_assembler_includes_related_artifacts() -> None:
repo = InMemoryKnowledgeRepository()
collection = repo.save_collection(Collection.create("runtime"))
root = repo.save_artifact(repo_artifact(collection.id, "root", "Root summary\nDetails"))
related = repo.save_artifact(repo_artifact(collection.id, "related", "Related summary"))
repo.save_relationship(Relationship.create(root.id, related.id, "relates to"))
package = ContextAssembler(repo).for_artifact(root.id)
rendered = package.render_markdown()
assert len(package.items) == 2
assert "Root summary" in rendered
assert "Related summary" in rendered
def repo_artifact(collection_id: str, name: str, content: str):
from kontextual_engine import Artifact
return Artifact.create(collection_id, name, content)

View File

@@ -1,6 +1,12 @@
from kontextual_engine import __version__
from kontextual_engine import Artifact, Collection, InMemoryKnowledgeRepository, __version__
def test_package_exports_version() -> None:
assert __version__ == "0.1.0"
def test_package_exports_core_runtime_contracts() -> None:
repo = InMemoryKnowledgeRepository()
collection = repo.save_collection(Collection.create("export-check"))
artifact = repo.save_artifact(Artifact.create(collection.id, "note", "content"))
assert repo.get_artifact(artifact.id).name == "note"

View File

@@ -0,0 +1,61 @@
from kontextual_engine import (
Artifact,
ArtifactMetadata,
Collection,
DuplicateResourceError,
InMemoryKnowledgeRepository,
QueryEngine,
Relationship,
)
def test_in_memory_repository_stores_collections_artifacts_and_relationships() -> None:
repo = InMemoryKnowledgeRepository()
collection = repo.save_collection(Collection.create("runtime"))
source = repo.save_artifact(Artifact.create(collection.id, "source", "source text"))
derived = repo.save_artifact(Artifact.create(collection.id, "derived", "derived text"))
relationship = repo.save_relationship(Relationship.create(derived.id, source.id, "derived from"))
assert repo.get_collection(collection.id).name == "runtime"
assert repo.get_artifact_by_name(collection.id, "source").id == source.id
assert repo.list_relationships(artifact_id=source.id) == [relationship]
def test_repository_rejects_duplicate_artifact_names_in_collection() -> None:
repo = InMemoryKnowledgeRepository()
collection = repo.save_collection(Collection.create("runtime"))
repo.save_artifact(Artifact.create(collection.id, "same", "one"))
try:
repo.save_artifact(Artifact.create(collection.id, "same", "two"))
except DuplicateResourceError as exc:
assert exc.details["name"] == "same"
else:
raise AssertionError("Expected duplicate artifact name to fail")
def test_query_engine_filters_artifacts_and_related_artifacts() -> None:
repo = InMemoryKnowledgeRepository()
collection = repo.save_collection(Collection.create("runtime"))
source = repo.save_artifact(
Artifact.create(
collection.id,
"source",
"alpha content",
metadata=ArtifactMetadata(tags=["seed"], custom={"kind": "source"}),
)
)
derived = repo.save_artifact(Artifact.create(collection.id, "derived", "beta content"))
repo.save_relationship(Relationship.create(derived.id, source.id, "derived from"))
query = QueryEngine(repo)
by_text = query.artifacts(text_contains="alpha")
by_metadata = query.artifacts(metadata={"custom.kind": "source"})
related = query.related_artifacts(source.id)
assert by_text.result_count == 1
assert by_text.results[0]["id"] == source.id
assert by_metadata.result_count == 1
assert related.result_count == 1
assert related.results[0]["id"] == derived.id