Initial implementation

This commit is contained in:
2026-05-14 11:32:25 +02:00
parent 6fd1ff7581
commit 916a895a85
31 changed files with 1461 additions and 21 deletions

59
tests/test_cli.py Normal file
View File

@@ -0,0 +1,59 @@
import json
import os
import subprocess
import sys
from pathlib import Path
def run_cli(*args: str) -> subprocess.CompletedProcess[str]:
env = os.environ.copy()
env["PYTHONPATH"] = "src"
return subprocess.run(
[sys.executable, "-m", "infospace_bench", *args],
check=False,
env=env,
text=True,
capture_output=True,
)
def test_cli_create_inspect_and_add_artifact(tmp_path: Path) -> None:
create = run_cli(
"create",
str(tmp_path),
"pilot",
"--name",
"Pilot Infospace",
"--topic-domain",
"Test Domain",
)
assert create.returncode == 0, create.stderr
assert json.loads(create.stdout)["slug"] == "pilot"
source = tmp_path / "source.md"
source.write_text("# Source\n", encoding="utf-8")
add = run_cli(
"add-artifact",
str(tmp_path / "infospaces" / "pilot"),
str(source),
"--kind",
"source",
"--title",
"Source",
)
assert add.returncode == 0, add.stderr
assert json.loads(add.stdout)["artifact"]["id"] == "source/source.md"
inspect = run_cli("inspect", str(tmp_path / "infospaces" / "pilot"))
assert inspect.returncode == 0, inspect.stderr
payload = json.loads(inspect.stdout)
assert payload["config"]["topic"]["domain"] == "Test Domain"
assert payload["artifacts"][0]["title"] == "Source"
def test_cli_returns_structured_error(tmp_path: Path) -> None:
result = run_cli("inspect", str(tmp_path / "missing"))
assert result.returncode == 2
payload = json.loads(result.stderr)
assert payload["error"]["code"] == "missing_infospace"

78
tests/test_evaluation.py Normal file
View File

@@ -0,0 +1,78 @@
from datetime import datetime, timezone
from infospace_bench.evaluation import (
EntityEvaluation,
EvaluationSnapshot,
MetricValue,
ScoreEntry,
diff_snapshots,
)
def test_entity_evaluation_round_trips_and_computes_overall_score() -> None:
evaluated_at = datetime(2026, 5, 14, tzinfo=timezone.utc)
evaluation = EntityEvaluation(
artifact_id="source/chapter.md",
evaluator="test",
scores=[
ScoreEntry("definition_precision", 4),
ScoreEntry("provenance_quality", 5),
],
evaluated_at=evaluated_at,
notes=["clear enough"],
)
payload = evaluation.to_dict()
loaded = EntityEvaluation.from_dict(payload)
assert payload["overall_score"] == 4.5
assert loaded == evaluation
def test_snapshot_diff_reports_added_removed_score_and_metric_changes() -> None:
now = datetime(2026, 5, 14, tzinfo=timezone.utc)
before = EvaluationSnapshot(
snapshot_id="before",
created_at=now,
schema_name="baseline",
artifact_count=1,
artifact_evaluations=[
EntityEvaluation(
artifact_id="source/a.md",
evaluator="test",
scores=[ScoreEntry("quality", 3)],
evaluated_at=now,
)
],
collection_metrics=[MetricValue("coverage_ratio", 0.5)],
)
after = EvaluationSnapshot(
snapshot_id="after",
created_at=now,
schema_name="baseline",
artifact_count=1,
artifact_evaluations=[
EntityEvaluation(
artifact_id="source/a.md",
evaluator="test",
scores=[ScoreEntry("quality", 4)],
evaluated_at=now,
),
EntityEvaluation(
artifact_id="source/b.md",
evaluator="test",
scores=[ScoreEntry("quality", 5)],
evaluated_at=now,
),
],
collection_metrics=[MetricValue("coverage_ratio", 0.75)],
)
diff = diff_snapshots(before, after)
assert diff.added_artifacts == ["source/b.md"]
assert diff.removed_artifacts == []
assert diff.score_changes[0].artifact_id == "source/a.md"
assert diff.score_changes[0].delta == 1
assert diff.metric_changes[0].name == "coverage_ratio"
assert diff.metric_changes[0].delta == 0.25

59
tests/test_inspection.py Normal file
View File

@@ -0,0 +1,59 @@
from infospace_bench.checks import run_collection_checks
from infospace_bench.inspection import export_mermaid, relationship_summary
from infospace_bench.models import KnowledgeArtifact, ViabilityThreshold
from infospace_bench.viability import evaluate_viability
def artifacts() -> list[KnowledgeArtifact]:
return [
KnowledgeArtifact(
id="source/a.md",
path="artifacts/sources/a.md",
kind="source",
title="A",
relationships=[{"type": "supports", "target": "generated/b.md"}],
),
KnowledgeArtifact(
id="generated/b.md",
path="artifacts/generated/b.md",
kind="generated",
title="B",
relationships=[{"type": "refines", "target": "source/a.md"}],
),
]
def test_collection_checks_produce_viability_metrics() -> None:
report = run_collection_checks(artifacts())
assert report.metrics["redundancy_ratio"] == 0
assert report.metrics["coverage_ratio"] == 1
assert report.metrics["coherence_components"] == 1
assert report.metrics["consistency_cycles"] == 1
assert report.metrics["granularity_entropy"] == 1
def test_viability_reports_per_threshold_and_overall_result() -> None:
report = evaluate_viability(
{"coverage_ratio": 0.75, "consistency_cycles": 1},
{
"coverage_ratio": ViabilityThreshold(min=0.5),
"consistency_cycles": ViabilityThreshold(max=0),
},
)
assert report.passed is False
assert report.results["coverage_ratio"].passed is True
assert report.results["consistency_cycles"].passed is False
def test_relationship_summary_and_mermaid_export() -> None:
summary = relationship_summary(artifacts())
assert summary.node_count == 2
assert summary.edge_count == 2
assert summary.relationship_types == {"refines": 1, "supports": 1}
mermaid = export_mermaid(summary)
assert "source/a.md -->|supports| generated/b.md" in mermaid
assert "generated/b.md -->|refines| source/a.md" in mermaid

91
tests/test_lifecycle.py Normal file
View File

@@ -0,0 +1,91 @@
from pathlib import Path
import pytest
from infospace_bench import (
InfospaceError,
add_artifact,
create_infospace,
load_infospace,
)
def test_create_infospace_writes_layout_and_loadable_config(tmp_path: Path) -> None:
infospace = create_infospace(
tmp_path,
"wealth-vsm",
name="Wealth of Nations through VSM",
topic_domain="Classical Economics",
)
root = tmp_path / "infospaces" / "wealth-vsm"
assert infospace.root == root
assert (root / "infospace.yaml").is_file()
assert (root / "artifacts" / "sources").is_dir()
assert (root / "artifacts" / "generated").is_dir()
assert (root / "output" / "evaluations").is_dir()
assert (root / "output" / "metrics").is_dir()
assert (root / "reports").is_dir()
assert (root / "exports").is_dir()
loaded = load_infospace(root)
assert loaded.config.slug == "wealth-vsm"
assert loaded.config.name == "Wealth of Nations through VSM"
assert loaded.config.topic.domain == "Classical Economics"
assert loaded.config.topic.sources == "artifacts/sources"
assert loaded.artifacts == []
def test_create_infospace_rejects_unsafe_slug_with_structured_error(tmp_path: Path) -> None:
with pytest.raises(InfospaceError) as raised:
create_infospace(tmp_path, "../outside", name="Nope")
assert raised.value.code == "invalid_slug"
assert raised.value.detail["slug"] == "../outside"
def test_load_infospace_reports_missing_config(tmp_path: Path) -> None:
root = tmp_path / "infospaces" / "empty"
root.mkdir(parents=True)
with pytest.raises(InfospaceError) as raised:
load_infospace(root)
assert raised.value.code == "missing_config"
assert "infospace.yaml" in raised.value.message
def test_add_artifact_copies_file_and_updates_manifest(tmp_path: Path) -> None:
create_infospace(tmp_path, "pilot", name="Pilot Infospace")
source = tmp_path / "chapter.md"
source.write_text("# Chapter\n\nSource text.\n", encoding="utf-8")
artifact = add_artifact(
tmp_path / "infospaces" / "pilot",
source,
kind="source",
title="Chapter 1",
)
assert artifact.id == "source/chapter.md"
assert artifact.path == "artifacts/sources/chapter.md"
assert (tmp_path / "infospaces" / "pilot" / artifact.path).read_text(
encoding="utf-8"
).startswith("# Chapter")
loaded = load_infospace(tmp_path / "infospaces" / "pilot")
assert [item.id for item in loaded.artifacts] == ["source/chapter.md"]
assert loaded.artifacts[0].title == "Chapter 1"
def test_add_artifact_rejects_duplicate_manifest_entry(tmp_path: Path) -> None:
create_infospace(tmp_path, "pilot", name="Pilot Infospace")
source = tmp_path / "chapter.md"
source.write_text("# Chapter\n", encoding="utf-8")
add_artifact(tmp_path / "infospaces" / "pilot", source, kind="source")
with pytest.raises(InfospaceError) as raised:
add_artifact(tmp_path / "infospaces" / "pilot", source, kind="source")
assert raised.value.code == "duplicate_artifact"

View File

@@ -0,0 +1,27 @@
from pathlib import Path
from infospace_bench import load_infospace
from infospace_bench.checks import run_collection_checks
from infospace_bench.viability import evaluate_viability
def test_reference_pilot_is_loadable_and_viable() -> None:
root = Path("infospaces/bootstrap-pilot")
infospace = load_infospace(root)
metrics = run_collection_checks(infospace.artifacts).metrics
viability = evaluate_viability(metrics, infospace.config.viability)
assert infospace.config.slug == "bootstrap-pilot"
assert len(infospace.artifacts) == 4
assert metrics["coverage_ratio"] == 1
assert metrics["coherence_components"] == 1
assert viability.passed is True
def test_reference_pilot_has_traceable_decision_and_report() -> None:
decision = Path("docs/reference-pilot-decision.md")
report = Path("infospaces/bootstrap-pilot/reports/baseline-inspection.md")
assert "small purpose-built corpus" in decision.read_text(encoding="utf-8")
assert "Migration from markitect-main" in report.read_text(encoding="utf-8")