diff --git a/README.md b/README.md index 6b1a038..9e92843 100644 --- a/README.md +++ b/README.md @@ -14,5 +14,15 @@ Start with: - `wiki/ProductRequirementsDocument.md` - `wiki/FunctionalRequirementsSpecification.md` - `SCOPE.md` +- `docs/infospace-layout.md` +- `docs/evaluation-and-inspection.md` +- `docs/reference-pilot-decision.md` - `docs/markitect-main-scope-assessment.md` +- `infospaces/bootstrap-pilot/` - `workplans/` + +Current development command: + +```bash +python3 -m pytest +``` diff --git a/docs/evaluation-and-inspection.md b/docs/evaluation-and-inspection.md new file mode 100644 index 0000000..3a84490 --- /dev/null +++ b/docs/evaluation-and-inspection.md @@ -0,0 +1,42 @@ +# Evaluation And Inspection + +`infospace-bench` now has a deterministic baseline for evaluation and +inspection. It is intentionally small: the repo can produce structured quality +objects and relationship summaries before any LLM or engine integration is +introduced. + +## Evaluation Objects + +- `ScoreEntry` +- `EntityEvaluation` +- `MetricValue` +- `EvaluationSnapshot` +- `SnapshotDiff` + +Snapshots are serializable through `to_dict()` / `from_dict()` and can be +compared with `diff_snapshots()`. + +## Collection Checks + +`run_collection_checks()` produces five baseline metrics: + +- `redundancy_ratio` +- `coverage_ratio` +- `coherence_components` +- `consistency_cycles` +- `granularity_entropy` + +These metrics are deliberately deterministic and file-backed. Later work can +replace or extend their internals with embeddings, richer graph analysis, or +agent-assisted evaluation without changing the result contract. + +## Viability + +`evaluate_viability()` compares metric values against declared +`ViabilityThreshold` values. Missing metrics fail visibly. + +## Relationship Inspection + +`relationship_summary()` extracts nodes, edges, and relationship type counts +from artifact manifests. `export_mermaid()` provides the first graph-friendly +representation. diff --git a/docs/infospace-layout.md b/docs/infospace-layout.md new file mode 100644 index 0000000..c25e056 --- /dev/null +++ b/docs/infospace-layout.md @@ -0,0 +1,55 @@ +# Infospace Layout + +An infospace is a file-backed project rooted at: + +```text +infospaces// +``` + +## Required Files + +```text +infospace.yaml +artifacts/index.yaml +``` + +`infospace.yaml` declares identity, topic, schema references, workflow +references, discipline bindings, and viability thresholds. `artifacts/index.yaml` +is the deterministic manifest of artifacts that have been added to the +infospace. + +## Required Directories + +```text +artifacts/sources/ +artifacts/generated/ +output/evaluations/ +output/metrics/ +reports/ +exports/ +``` + +## Artifact Manifest + +Artifacts are represented by stable IDs such as `source/chapter-01.md`. + +Each manifest entry records: + +- `id` +- `path` +- `kind` +- `title` +- `provenance` +- `relationships` + +The manifest is intentionally plain YAML so it can be inspected, diffed, and +regenerated by tools or agents. + +## Commands + +```bash +python3 -m infospace_bench create . pilot --name "Pilot Infospace" +python3 -m infospace_bench add-artifact infospaces/pilot ./source.md --kind source +python3 -m infospace_bench inspect infospaces/pilot +python3 -m infospace_bench export infospaces/pilot +``` diff --git a/docs/reference-pilot-decision.md b/docs/reference-pilot-decision.md new file mode 100644 index 0000000..ee9a3eb --- /dev/null +++ b/docs/reference-pilot-decision.md @@ -0,0 +1,24 @@ +# Reference Pilot Decision + +Date: 2026-05-14 + +## Decision + +Use a small purpose-built corpus as the first maintained reference infospace. + +## Rationale + +`markitect-main/examples/infospace-with-history/` remains the primary migration +candidate for a larger pilot, but it contains a large public-domain book corpus +and substantial generated output. Pulling it in before the lifecycle and +evaluation baseline exists would make the new repo noisy before it is useful. + +The bootstrap pilot uses this repo's own PRD/FRS intent as a compact corpus. It +proves the expected file layout, artifact manifest, relationship inspection, +collection metrics, and viability thresholding with minimal bulk. + +## Follow-up + +After the baseline is stable, migrate a pruned Wealth of Nations/VSM fixture or +a similarly representative slice from `markitect-main` under a separate +workplan. diff --git a/infospaces/bootstrap-pilot/artifacts/generated/evaluation-baseline.md b/infospaces/bootstrap-pilot/artifacts/generated/evaluation-baseline.md new file mode 100644 index 0000000..e07245a --- /dev/null +++ b/infospaces/bootstrap-pilot/artifacts/generated/evaluation-baseline.md @@ -0,0 +1,5 @@ +# Evaluation Baseline + +The evaluation baseline implements serializable score objects, evaluation +snapshots, snapshot diffs, deterministic collection checks, viability +thresholding, and relationship inspection with Mermaid export. diff --git a/infospaces/bootstrap-pilot/artifacts/generated/lifecycle-baseline.md b/infospaces/bootstrap-pilot/artifacts/generated/lifecycle-baseline.md new file mode 100644 index 0000000..4c9e73d --- /dev/null +++ b/infospaces/bootstrap-pilot/artifacts/generated/lifecycle-baseline.md @@ -0,0 +1,5 @@ +# Lifecycle Baseline + +The lifecycle baseline implements a file-backed infospace layout with +`infospace.yaml`, an artifact manifest, deterministic artifact loading, and a +small JSON CLI for create, inspect, add-artifact, and export operations. diff --git a/infospaces/bootstrap-pilot/artifacts/index.yaml b/infospaces/bootstrap-pilot/artifacts/index.yaml new file mode 100644 index 0000000..1ea9fe9 --- /dev/null +++ b/infospaces/bootstrap-pilot/artifacts/index.yaml @@ -0,0 +1,41 @@ +artifacts: + - id: source/prd-scope.md + path: artifacts/sources/prd-scope.md + kind: source + title: PRD Scope + provenance: + source_path: wiki/ProductRequirementsDocument.md + source_section: Scope Definition + relationships: + - type: supports + target: generated/lifecycle-baseline.md + - type: supports + target: generated/evaluation-baseline.md + - id: source/frs-requirements.md + path: artifacts/sources/frs-requirements.md + kind: source + title: FRS Requirements + provenance: + source_path: wiki/FunctionalRequirementsSpecification.md + source_section: Functional Requirements + relationships: + - type: supports + target: generated/lifecycle-baseline.md + - type: supports + target: generated/evaluation-baseline.md + - id: generated/lifecycle-baseline.md + path: artifacts/generated/lifecycle-baseline.md + kind: generated + title: Lifecycle Baseline + provenance: + source_path: workplans/IB-WP-0002-infospace-lifecycle-baseline.md + relationships: + - type: precedes + target: generated/evaluation-baseline.md + - id: generated/evaluation-baseline.md + path: artifacts/generated/evaluation-baseline.md + kind: generated + title: Evaluation Baseline + provenance: + source_path: workplans/IB-WP-0003-evaluation-and-inspection.md + relationships: [] diff --git a/infospaces/bootstrap-pilot/artifacts/sources/frs-requirements.md b/infospaces/bootstrap-pilot/artifacts/sources/frs-requirements.md new file mode 100644 index 0000000..b4ccf66 --- /dev/null +++ b/infospaces/bootstrap-pilot/artifacts/sources/frs-requirements.md @@ -0,0 +1,6 @@ +# FRS Requirements + +The functional requirements describe externally observable behavior for +infospace lifecycle management, knowledge population, structure and +relationships, evaluation, inspection, workflow execution, export, AI +assistance, and explicit error handling. diff --git a/infospaces/bootstrap-pilot/artifacts/sources/prd-scope.md b/infospaces/bootstrap-pilot/artifacts/sources/prd-scope.md new file mode 100644 index 0000000..5c16a7b --- /dev/null +++ b/infospaces/bootstrap-pilot/artifacts/sources/prd-scope.md @@ -0,0 +1,7 @@ +# PRD Scope + +The product requirements define `infospace-bench` as the application layer for +concrete structured knowledge spaces. The repo should support creation, +population, evaluation, inspection, transformation, generation, and export of +infospaces while avoiding low-level markdown tooling and runtime platform +ownership. diff --git a/infospaces/bootstrap-pilot/infospace.yaml b/infospaces/bootstrap-pilot/infospace.yaml new file mode 100644 index 0000000..cf04f70 --- /dev/null +++ b/infospaces/bootstrap-pilot/infospace.yaml @@ -0,0 +1,24 @@ +slug: bootstrap-pilot +name: Infospace Bench Bootstrap Pilot +topic: + name: Infospace Bench Bootstrap Pilot + domain: Knowledge Engineering Tooling + sources: artifacts/sources +disciplines: + - name: Infospace Lifecycle + path: artifacts/generated/lifecycle-baseline.md +schemas: {} +workflows: + - name: baseline-inspection + report: reports/baseline-inspection.md +viability: + redundancy_ratio: + max: 0 + coverage_ratio: + min: 1 + coherence_components: + max: 1 + consistency_cycles: + max: 0 + granularity_entropy: + min: 1 diff --git a/infospaces/bootstrap-pilot/output/metrics/baseline.yaml b/infospaces/bootstrap-pilot/output/metrics/baseline.yaml new file mode 100644 index 0000000..889106c --- /dev/null +++ b/infospaces/bootstrap-pilot/output/metrics/baseline.yaml @@ -0,0 +1,9 @@ +metrics: + redundancy_ratio: 0 + coverage_ratio: 1 + coherence_components: 1 + consistency_cycles: 0 + granularity_entropy: 1 +details: + artifact_count: 4 + relationship_count: 5 diff --git a/infospaces/bootstrap-pilot/reports/baseline-inspection.md b/infospaces/bootstrap-pilot/reports/baseline-inspection.md new file mode 100644 index 0000000..75a4991 --- /dev/null +++ b/infospaces/bootstrap-pilot/reports/baseline-inspection.md @@ -0,0 +1,24 @@ +# Bootstrap Pilot Baseline Inspection + +## Scope + +This pilot exercises the new file-backed lifecycle and evaluation baseline +against a compact corpus derived from the `infospace-bench` PRD, FRS, and +initial workplans. + +## Result + +The pilot is viable under its declared thresholds: + +- `redundancy_ratio`: 0 +- `coverage_ratio`: 1 +- `coherence_components`: 1 +- `consistency_cycles`: 0 +- `granularity_entropy`: 1 + +## Migration from markitect-main + +Migration from markitect-main is intentionally staged. The large +`examples/infospace-with-history/` corpus remains the main reference candidate, +but this bootstrap pilot proves the successor repo's baseline behavior before +bulk corpus migration. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5196690 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,13 @@ +[project] +name = "infospace-bench" +version = "0.1.0" +description = "Application-layer workspace for concrete structured knowledge spaces." +requires-python = ">=3.11" +dependencies = ["PyYAML>=6"] + +[project.scripts] +infospace-bench = "infospace_bench.cli:main" + +[tool.pytest.ini_options] +pythonpath = ["src"] +testpaths = ["tests"] diff --git a/src/infospace_bench/__init__.py b/src/infospace_bench/__init__.py new file mode 100644 index 0000000..67ae208 --- /dev/null +++ b/src/infospace_bench/__init__.py @@ -0,0 +1,28 @@ +from .errors import InfospaceError +from .evaluation import EntityEvaluation, EvaluationSnapshot, MetricValue, ScoreEntry +from .lifecycle import add_artifact, create_infospace, load_infospace +from .models import ( + DisciplineBinding, + Infospace, + InfospaceConfig, + KnowledgeArtifact, + TopicConfig, + ViabilityThreshold, +) + +__all__ = [ + "DisciplineBinding", + "EntityEvaluation", + "EvaluationSnapshot", + "Infospace", + "InfospaceConfig", + "InfospaceError", + "KnowledgeArtifact", + "MetricValue", + "ScoreEntry", + "TopicConfig", + "ViabilityThreshold", + "add_artifact", + "create_infospace", + "load_infospace", +] diff --git a/src/infospace_bench/__main__.py b/src/infospace_bench/__main__.py new file mode 100644 index 0000000..a049ad7 --- /dev/null +++ b/src/infospace_bench/__main__.py @@ -0,0 +1,5 @@ +from .cli import main + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/infospace_bench/checks.py b/src/infospace_bench/checks.py new file mode 100644 index 0000000..e4f3422 --- /dev/null +++ b/src/infospace_bench/checks.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +from dataclasses import dataclass +from math import log2 + +from .models import KnowledgeArtifact + + +@dataclass(frozen=True) +class CollectionCheckReport: + metrics: dict[str, float] + details: dict[str, object] + + +def run_collection_checks(artifacts: list[KnowledgeArtifact]) -> CollectionCheckReport: + graph = _directed_graph(artifacts) + metrics = { + "redundancy_ratio": _redundancy_ratio(artifacts), + "coverage_ratio": _coverage_ratio(artifacts), + "coherence_components": float(_component_count(graph)), + "consistency_cycles": float(_cycle_count(graph)), + "granularity_entropy": _kind_entropy(artifacts), + } + return CollectionCheckReport( + metrics=metrics, + details={ + "artifact_count": len(artifacts), + "relationship_count": sum(len(item.relationships) for item in artifacts), + }, + ) + + +def _redundancy_ratio(artifacts: list[KnowledgeArtifact]) -> float: + if not artifacts: + return 0.0 + labels = [item.title or item.id for item in artifacts] + duplicate_count = len(labels) - len(set(labels)) + return duplicate_count / len(artifacts) + + +def _coverage_ratio(artifacts: list[KnowledgeArtifact]) -> float: + if not artifacts: + return 0.0 + covered = sum(1 for item in artifacts if item.title and item.path) + return covered / len(artifacts) + + +def _kind_entropy(artifacts: list[KnowledgeArtifact]) -> float: + if not artifacts: + return 0.0 + counts: dict[str, int] = {} + for artifact in artifacts: + counts[artifact.kind] = counts.get(artifact.kind, 0) + 1 + total = len(artifacts) + return -sum((count / total) * log2(count / total) for count in counts.values()) + + +def _directed_graph(artifacts: list[KnowledgeArtifact]) -> dict[str, set[str]]: + ids = {item.id for item in artifacts} + graph = {item.id: set() for item in artifacts} + for item in artifacts: + for relationship in item.relationships: + target = relationship.get("target") + if isinstance(target, str) and target in ids: + graph[item.id].add(target) + return graph + + +def _component_count(graph: dict[str, set[str]]) -> int: + if not graph: + return 0 + undirected = {node: set(edges) for node, edges in graph.items()} + for node, edges in graph.items(): + for target in edges: + undirected.setdefault(target, set()).add(node) + + seen: set[str] = set() + count = 0 + for node in undirected: + if node in seen: + continue + count += 1 + stack = [node] + while stack: + current = stack.pop() + if current in seen: + continue + seen.add(current) + stack.extend(undirected[current] - seen) + return count + + +def _cycle_count(graph: dict[str, set[str]]) -> int: + cycles = 0 + visited: set[str] = set() + active: set[str] = set() + + def visit(node: str) -> None: + nonlocal cycles + visited.add(node) + active.add(node) + for target in graph[node]: + if target not in visited: + visit(target) + elif target in active: + cycles += 1 + active.remove(node) + + for node in graph: + if node not in visited: + visit(node) + return cycles diff --git a/src/infospace_bench/cli.py b/src/infospace_bench/cli.py new file mode 100644 index 0000000..0b1be0b --- /dev/null +++ b/src/infospace_bench/cli.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path + +from .errors import InfospaceError +from .lifecycle import add_artifact, create_infospace, load_infospace + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(prog="infospace-bench") + sub = parser.add_subparsers(dest="command", required=True) + + create = sub.add_parser("create", help="Create an infospace") + create.add_argument("workspace") + create.add_argument("slug") + create.add_argument("--name", required=True) + create.add_argument("--topic-domain", default="") + + inspect = sub.add_parser("inspect", help="Inspect an infospace") + inspect.add_argument("root") + + add = sub.add_parser("add-artifact", help="Add an artifact to an infospace") + add.add_argument("root") + add.add_argument("source") + add.add_argument("--kind", required=True) + add.add_argument("--title", default="") + + export = sub.add_parser("export", help="Print the infospace representation") + export.add_argument("root") + + return parser + + +def main(argv: list[str] | None = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + try: + if args.command == "create": + infospace = create_infospace( + Path(args.workspace), + args.slug, + name=args.name, + topic_domain=args.topic_domain, + ) + _write_json({"slug": infospace.config.slug, "root": str(infospace.root)}) + elif args.command == "inspect": + _write_json(load_infospace(Path(args.root)).to_dict()) + elif args.command == "add-artifact": + artifact = add_artifact( + Path(args.root), + Path(args.source), + kind=args.kind, + title=args.title, + ) + _write_json({"artifact": artifact.to_dict()}) + elif args.command == "export": + _write_json(load_infospace(Path(args.root)).to_dict()) + else: + parser.error(f"Unhandled command: {args.command}") + except InfospaceError as exc: + print(json.dumps(exc.to_dict(), indent=2), file=sys.stderr) + return 2 + return 0 + + +def _write_json(payload: dict) -> None: + print(json.dumps(payload, indent=2)) diff --git a/src/infospace_bench/errors.py b/src/infospace_bench/errors.py new file mode 100644 index 0000000..833cdf7 --- /dev/null +++ b/src/infospace_bench/errors.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any + + +@dataclass +class InfospaceError(Exception): + """Structured application error suitable for CLI and API surfaces.""" + + code: str + message: str + detail: dict[str, Any] = field(default_factory=dict) + + def __post_init__(self) -> None: + super().__init__(self.message) + + def to_dict(self) -> dict[str, Any]: + return { + "error": { + "code": self.code, + "message": self.message, + "detail": self.detail, + } + } diff --git a/src/infospace_bench/evaluation.py b/src/infospace_bench/evaluation.py new file mode 100644 index 0000000..11ed2d7 --- /dev/null +++ b/src/infospace_bench/evaluation.py @@ -0,0 +1,210 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from datetime import datetime +from typing import Any + + +@dataclass(frozen=True) +class ScoreEntry: + name: str + value: float + max_value: float = 5.0 + rationale: str = "" + + def to_dict(self) -> dict[str, Any]: + data: dict[str, Any] = { + "name": self.name, + "value": self.value, + "max_value": self.max_value, + } + if self.rationale: + data["rationale"] = self.rationale + return data + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> "ScoreEntry": + return cls( + name=str(data["name"]), + value=float(data["value"]), + max_value=float(data.get("max_value", 5.0)), + rationale=str(data.get("rationale") or ""), + ) + + +@dataclass(frozen=True) +class EntityEvaluation: + artifact_id: str + evaluator: str + scores: list[ScoreEntry] + evaluated_at: datetime + notes: list[str] = field(default_factory=list) + + @property + def overall_score(self) -> float: + if not self.scores: + return 0.0 + return sum(score.value for score in self.scores) / len(self.scores) + + def to_dict(self) -> dict[str, Any]: + return { + "artifact_id": self.artifact_id, + "evaluator": self.evaluator, + "evaluated_at": self.evaluated_at.isoformat(), + "overall_score": round(self.overall_score, 4), + "scores": [score.to_dict() for score in self.scores], + "notes": self.notes, + } + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> "EntityEvaluation": + return cls( + artifact_id=str(data["artifact_id"]), + evaluator=str(data["evaluator"]), + scores=[ScoreEntry.from_dict(item) for item in data.get("scores", [])], + evaluated_at=datetime.fromisoformat(str(data["evaluated_at"])), + notes=list(data.get("notes") or []), + ) + + +@dataclass(frozen=True) +class MetricValue: + name: str + value: float + concern: str = "" + details: dict[str, Any] = field(default_factory=dict) + + def to_dict(self) -> dict[str, Any]: + data: dict[str, Any] = {"name": self.name, "value": self.value} + if self.concern: + data["concern"] = self.concern + if self.details: + data["details"] = self.details + return data + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> "MetricValue": + return cls( + name=str(data["name"]), + value=float(data["value"]), + concern=str(data.get("concern") or ""), + details=dict(data.get("details") or {}), + ) + + +@dataclass(frozen=True) +class EvaluationSnapshot: + snapshot_id: str + created_at: datetime + schema_name: str + artifact_count: int + artifact_evaluations: list[EntityEvaluation] = field(default_factory=list) + collection_metrics: list[MetricValue] = field(default_factory=list) + metadata: dict[str, Any] = field(default_factory=dict) + + def to_dict(self) -> dict[str, Any]: + return { + "snapshot_id": self.snapshot_id, + "created_at": self.created_at.isoformat(), + "schema_name": self.schema_name, + "artifact_count": self.artifact_count, + "artifact_evaluations": [ + evaluation.to_dict() for evaluation in self.artifact_evaluations + ], + "collection_metrics": [ + metric.to_dict() for metric in self.collection_metrics + ], + "metadata": self.metadata, + } + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> "EvaluationSnapshot": + return cls( + snapshot_id=str(data["snapshot_id"]), + created_at=datetime.fromisoformat(str(data["created_at"])), + schema_name=str(data["schema_name"]), + artifact_count=int(data["artifact_count"]), + artifact_evaluations=[ + EntityEvaluation.from_dict(item) + for item in data.get("artifact_evaluations", []) + ], + collection_metrics=[ + MetricValue.from_dict(item) for item in data.get("collection_metrics", []) + ], + metadata=dict(data.get("metadata") or {}), + ) + + +@dataclass(frozen=True) +class ScoreChange: + artifact_id: str + dimension: str + before: float + after: float + + @property + def delta(self) -> float: + return self.after - self.before + + +@dataclass(frozen=True) +class MetricChange: + name: str + before: float + after: float + + @property + def delta(self) -> float: + return self.after - self.before + + +@dataclass(frozen=True) +class SnapshotDiff: + before_id: str + after_id: str + added_artifacts: list[str] = field(default_factory=list) + removed_artifacts: list[str] = field(default_factory=list) + score_changes: list[ScoreChange] = field(default_factory=list) + metric_changes: list[MetricChange] = field(default_factory=list) + + +def diff_snapshots( + before: EvaluationSnapshot, + after: EvaluationSnapshot, +) -> SnapshotDiff: + before_scores = _score_index(before) + after_scores = _score_index(after) + before_artifacts = {artifact_id for artifact_id, _ in before_scores} + after_artifacts = {artifact_id for artifact_id, _ in after_scores} + + score_changes = [ + ScoreChange(artifact_id, dimension, before_scores[key], after_scores[key]) + for key in sorted(before_scores.keys() & after_scores.keys()) + for artifact_id, dimension in [key] + if before_scores[key] != after_scores[key] + ] + + before_metrics = {metric.name: metric.value for metric in before.collection_metrics} + after_metrics = {metric.name: metric.value for metric in after.collection_metrics} + metric_changes = [ + MetricChange(name, before_metrics[name], after_metrics[name]) + for name in sorted(before_metrics.keys() & after_metrics.keys()) + if before_metrics[name] != after_metrics[name] + ] + + return SnapshotDiff( + before_id=before.snapshot_id, + after_id=after.snapshot_id, + added_artifacts=sorted(after_artifacts - before_artifacts), + removed_artifacts=sorted(before_artifacts - after_artifacts), + score_changes=score_changes, + metric_changes=metric_changes, + ) + + +def _score_index(snapshot: EvaluationSnapshot) -> dict[tuple[str, str], float]: + return { + (evaluation.artifact_id, score.name): score.value + for evaluation in snapshot.artifact_evaluations + for score in evaluation.scores + } diff --git a/src/infospace_bench/inspection.py b/src/infospace_bench/inspection.py new file mode 100644 index 0000000..8e26a69 --- /dev/null +++ b/src/infospace_bench/inspection.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +from dataclasses import dataclass, field + +from .models import KnowledgeArtifact + + +@dataclass(frozen=True) +class RelationshipEdge: + source: str + target: str + type: str + + +@dataclass(frozen=True) +class RelationshipSummary: + nodes: list[str] + edges: list[RelationshipEdge] + relationship_types: dict[str, int] = field(default_factory=dict) + + @property + def node_count(self) -> int: + return len(self.nodes) + + @property + def edge_count(self) -> int: + return len(self.edges) + + +def relationship_summary(artifacts: list[KnowledgeArtifact]) -> RelationshipSummary: + ids = {artifact.id for artifact in artifacts} + edges: list[RelationshipEdge] = [] + type_counts: dict[str, int] = {} + for artifact in artifacts: + for relationship in artifact.relationships: + target = relationship.get("target") + relation_type = str(relationship.get("type") or "related") + if isinstance(target, str) and target in ids: + edges.append(RelationshipEdge(artifact.id, target, relation_type)) + type_counts[relation_type] = type_counts.get(relation_type, 0) + 1 + return RelationshipSummary( + nodes=sorted(ids), + edges=edges, + relationship_types=dict(sorted(type_counts.items())), + ) + + +def export_mermaid(summary: RelationshipSummary) -> str: + lines = ["graph TD"] + for node in summary.nodes: + lines.append(f" {node}") + for edge in summary.edges: + lines.append(f" {edge.source} -->|{edge.type}| {edge.target}") + return "\n".join(lines) + "\n" diff --git a/src/infospace_bench/lifecycle.py b/src/infospace_bench/lifecycle.py new file mode 100644 index 0000000..733c6e2 --- /dev/null +++ b/src/infospace_bench/lifecycle.py @@ -0,0 +1,170 @@ +from __future__ import annotations + +import re +import shutil +from pathlib import Path +from typing import Any + +import yaml + +from .errors import InfospaceError +from .models import Infospace, InfospaceConfig, KnowledgeArtifact, TopicConfig + +SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$") +CONFIG_FILE = "infospace.yaml" +ARTIFACT_INDEX = "artifacts/index.yaml" +LAYOUT_DIRS = ( + "artifacts/sources", + "artifacts/generated", + "output/evaluations", + "output/metrics", + "reports", + "exports", +) +KIND_DIRS = {"source": "sources", "generated": "generated"} + + +def create_infospace( + workspace: Path | str, + slug: str, + *, + name: str, + topic_domain: str = "", +) -> Infospace: + _validate_slug(slug) + workspace_path = Path(workspace) + root = workspace_path / "infospaces" / slug + if root.exists(): + raise InfospaceError( + "infospace_exists", + f"Infospace already exists: {root}", + {"root": str(root)}, + ) + + for relative in LAYOUT_DIRS: + (root / relative).mkdir(parents=True, exist_ok=True) + + config = InfospaceConfig( + slug=slug, + name=name, + topic=TopicConfig(name=name, domain=topic_domain), + ) + _write_yaml(root / CONFIG_FILE, config.to_dict()) + _write_yaml(root / ARTIFACT_INDEX, {"artifacts": []}) + return Infospace(root=root, config=config, artifacts=[]) + + +def load_infospace(root: Path | str) -> Infospace: + root_path = Path(root) + if not root_path.exists(): + raise InfospaceError( + "missing_infospace", + f"Infospace path does not exist: {root_path}", + {"root": str(root_path)}, + ) + config_path = root_path / CONFIG_FILE + if not config_path.is_file(): + raise InfospaceError( + "missing_config", + f"Missing infospace.yaml at {config_path}", + {"config_path": str(config_path)}, + ) + + raw_config = _read_yaml(config_path) + try: + config = InfospaceConfig.from_dict(raw_config) + except KeyError as exc: + raise InfospaceError( + "invalid_config", + f"Missing required config field: {exc.args[0]}", + {"config_path": str(config_path), "field": exc.args[0]}, + ) from exc + + return Infospace(root=root_path, config=config, artifacts=_read_artifacts(root_path)) + + +def add_artifact( + root: Path | str, + source: Path | str, + *, + kind: str, + title: str = "", + relationships: list[dict[str, Any]] | None = None, +) -> KnowledgeArtifact: + infospace = load_infospace(root) + if kind not in KIND_DIRS: + raise InfospaceError( + "invalid_artifact_kind", + f"Unsupported artifact kind: {kind}", + {"kind": kind, "valid_kinds": sorted(KIND_DIRS)}, + ) + + source_path = Path(source) + if not source_path.is_file(): + raise InfospaceError( + "missing_artifact_source", + f"Artifact source does not exist: {source_path}", + {"source": str(source_path)}, + ) + + artifact_id = f"{kind}/{source_path.name}" + if any(item.id == artifact_id for item in infospace.artifacts): + raise InfospaceError( + "duplicate_artifact", + f"Artifact already exists: {artifact_id}", + {"artifact_id": artifact_id}, + ) + + target = infospace.root / "artifacts" / KIND_DIRS[kind] / source_path.name + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(source_path, target) + + artifact = KnowledgeArtifact( + id=artifact_id, + path=str(target.relative_to(infospace.root)), + kind=kind, + title=title, + provenance={"source_path": str(source_path)}, + relationships=relationships or [], + ) + artifacts = [*infospace.artifacts, artifact] + _write_yaml( + infospace.root / ARTIFACT_INDEX, + {"artifacts": [item.to_dict() for item in artifacts]}, + ) + return artifact + + +def _validate_slug(slug: str) -> None: + if not SLUG_RE.match(slug): + raise InfospaceError( + "invalid_slug", + "Slug must contain only lowercase letters, numbers, and hyphens", + {"slug": slug}, + ) + + +def _read_artifacts(root: Path) -> list[KnowledgeArtifact]: + path = root / ARTIFACT_INDEX + if not path.exists(): + return [] + data = _read_yaml(path) + return [KnowledgeArtifact.from_dict(item) for item in data.get("artifacts", [])] + + +def _read_yaml(path: Path) -> dict[str, Any]: + with path.open("r", encoding="utf-8") as handle: + data = yaml.safe_load(handle) or {} + if not isinstance(data, dict): + raise InfospaceError( + "invalid_yaml", + f"Expected mapping in YAML file: {path}", + {"path": str(path)}, + ) + return data + + +def _write_yaml(path: Path, data: dict[str, Any]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("w", encoding="utf-8") as handle: + yaml.safe_dump(data, handle, sort_keys=False) diff --git a/src/infospace_bench/models.py b/src/infospace_bench/models.py new file mode 100644 index 0000000..4b21fce --- /dev/null +++ b/src/infospace_bench/models.py @@ -0,0 +1,143 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any + + +@dataclass(frozen=True) +class TopicConfig: + name: str + domain: str = "" + sources: str = "artifacts/sources" + + @classmethod + def from_dict(cls, data: dict[str, Any] | None) -> "TopicConfig": + data = data or {} + return cls( + name=str(data.get("name") or ""), + domain=str(data.get("domain") or ""), + sources=str(data.get("sources") or "artifacts/sources"), + ) + + def to_dict(self) -> dict[str, Any]: + return {"name": self.name, "domain": self.domain, "sources": self.sources} + + +@dataclass(frozen=True) +class DisciplineBinding: + name: str + path: str + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> "DisciplineBinding": + return cls(name=str(data["name"]), path=str(data["path"])) + + def to_dict(self) -> dict[str, Any]: + return {"name": self.name, "path": self.path} + + +@dataclass(frozen=True) +class ViabilityThreshold: + min: float | None = None + max: float | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> "ViabilityThreshold": + return cls( + min=float(data["min"]) if data.get("min") is not None else None, + max=float(data["max"]) if data.get("max") is not None else None, + ) + + def to_dict(self) -> dict[str, float]: + result: dict[str, float] = {} + if self.min is not None: + result["min"] = self.min + if self.max is not None: + result["max"] = self.max + return result + + +@dataclass(frozen=True) +class InfospaceConfig: + slug: str + name: str + topic: TopicConfig + disciplines: list[DisciplineBinding] = field(default_factory=list) + schemas: dict[str, str] = field(default_factory=dict) + workflows: list[dict[str, Any]] = field(default_factory=list) + viability: dict[str, ViabilityThreshold] = field(default_factory=dict) + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> "InfospaceConfig": + return cls( + slug=str(data["slug"]), + name=str(data.get("name") or data["slug"]), + topic=TopicConfig.from_dict(data.get("topic")), + disciplines=[ + DisciplineBinding.from_dict(item) + for item in data.get("disciplines", []) + ], + schemas={str(k): str(v) for k, v in (data.get("schemas") or {}).items()}, + workflows=list(data.get("workflows") or []), + viability={ + str(k): ViabilityThreshold.from_dict(v) + for k, v in (data.get("viability") or {}).items() + }, + ) + + def to_dict(self) -> dict[str, Any]: + return { + "slug": self.slug, + "name": self.name, + "topic": self.topic.to_dict(), + "disciplines": [item.to_dict() for item in self.disciplines], + "schemas": self.schemas, + "workflows": self.workflows, + "viability": {k: v.to_dict() for k, v in self.viability.items()}, + } + + +@dataclass(frozen=True) +class KnowledgeArtifact: + id: str + path: str + kind: str + title: str = "" + provenance: dict[str, Any] = field(default_factory=dict) + relationships: list[dict[str, Any]] = field(default_factory=list) + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> "KnowledgeArtifact": + return cls( + id=str(data["id"]), + path=str(data["path"]), + kind=str(data["kind"]), + title=str(data.get("title") or ""), + provenance=dict(data.get("provenance") or {}), + relationships=list(data.get("relationships") or []), + ) + + def to_dict(self) -> dict[str, Any]: + return { + "id": self.id, + "path": self.path, + "kind": self.kind, + "title": self.title, + "provenance": self.provenance, + "relationships": self.relationships, + } + + +@dataclass(frozen=True) +class Infospace: + root: Path + config: InfospaceConfig + artifacts: list[KnowledgeArtifact] = field(default_factory=list) + + def to_dict(self) -> dict[str, Any]: + return { + "root": str(self.root), + "config": self.config.to_dict(), + "artifacts": [item.to_dict() for item in self.artifacts], + } diff --git a/src/infospace_bench/viability.py b/src/infospace_bench/viability.py new file mode 100644 index 0000000..fe50a6e --- /dev/null +++ b/src/infospace_bench/viability.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +from dataclasses import dataclass + +from .models import ViabilityThreshold + + +@dataclass(frozen=True) +class ViabilityResult: + metric: str + value: float | None + threshold: ViabilityThreshold + passed: bool + + +@dataclass(frozen=True) +class ViabilityReport: + passed: bool + results: dict[str, ViabilityResult] + + +def evaluate_viability( + metrics: dict[str, float], + thresholds: dict[str, ViabilityThreshold], +) -> ViabilityReport: + results: dict[str, ViabilityResult] = {} + for name, threshold in thresholds.items(): + value = metrics.get(name) + passed = value is not None + if value is not None and threshold.min is not None: + passed = passed and value >= threshold.min + if value is not None and threshold.max is not None: + passed = passed and value <= threshold.max + results[name] = ViabilityResult( + metric=name, + value=value, + threshold=threshold, + passed=passed, + ) + return ViabilityReport( + passed=all(result.passed for result in results.values()), + results=results, + ) diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..9585ae2 --- /dev/null +++ b/tests/test_cli.py @@ -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" diff --git a/tests/test_evaluation.py b/tests/test_evaluation.py new file mode 100644 index 0000000..44112cc --- /dev/null +++ b/tests/test_evaluation.py @@ -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 diff --git a/tests/test_inspection.py b/tests/test_inspection.py new file mode 100644 index 0000000..9e2588a --- /dev/null +++ b/tests/test_inspection.py @@ -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 diff --git a/tests/test_lifecycle.py b/tests/test_lifecycle.py new file mode 100644 index 0000000..d11303e --- /dev/null +++ b/tests/test_lifecycle.py @@ -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" diff --git a/tests/test_reference_pilot.py b/tests/test_reference_pilot.py new file mode 100644 index 0000000..6550159 --- /dev/null +++ b/tests/test_reference_pilot.py @@ -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") diff --git a/workplans/IB-WP-0002-infospace-lifecycle-baseline.md b/workplans/IB-WP-0002-infospace-lifecycle-baseline.md index d9566bd..f0333e1 100644 --- a/workplans/IB-WP-0002-infospace-lifecycle-baseline.md +++ b/workplans/IB-WP-0002-infospace-lifecycle-baseline.md @@ -4,11 +4,11 @@ type: workplan title: "Infospace Lifecycle Baseline" domain: markitect repo: infospace-bench -status: planned +status: done owner: markitect topic_slug: markitect created: "2026-05-03" -updated: "2026-05-03" +updated: "2026-05-14" state_hub_workstream_slug: "ib-wp-0002-infospace-lifecycle-baseline" state_hub_workstream_id: "b5972baf-1fb4-4375-a8e3-6e6d6d6fb97c" --- @@ -34,7 +34,7 @@ inspecting, and exporting infospaces. ```task id: IB-WP-0002-T01 -status: todo +status: done priority: high state_hub_task_id: "c8aaa52e-db89-42e3-9ebc-cb89be3f4d30" ``` @@ -47,7 +47,7 @@ state_hub_task_id: "c8aaa52e-db89-42e3-9ebc-cb89be3f4d30" ```task id: IB-WP-0002-T02 -status: todo +status: done priority: high state_hub_task_id: "a96c3439-89e4-40d0-8731-fe6b39a8f451" ``` @@ -61,7 +61,7 @@ state_hub_task_id: "a96c3439-89e4-40d0-8731-fe6b39a8f451" ```task id: IB-WP-0002-T03 -status: todo +status: done priority: high state_hub_task_id: "36ad8a84-f4fd-48ca-bc6f-94827ac03481" ``` @@ -74,7 +74,7 @@ state_hub_task_id: "36ad8a84-f4fd-48ca-bc6f-94827ac03481" ```task id: IB-WP-0002-T04 -status: todo +status: done priority: medium state_hub_task_id: "bfa70d92-c896-4133-8b93-0ece3e53c649" ``` diff --git a/workplans/IB-WP-0003-evaluation-and-inspection.md b/workplans/IB-WP-0003-evaluation-and-inspection.md index bab1100..b0313b4 100644 --- a/workplans/IB-WP-0003-evaluation-and-inspection.md +++ b/workplans/IB-WP-0003-evaluation-and-inspection.md @@ -4,11 +4,11 @@ type: workplan title: "Evaluation And Inspection Framework" domain: markitect repo: infospace-bench -status: planned +status: done owner: markitect topic_slug: markitect created: "2026-05-03" -updated: "2026-05-03" +updated: "2026-05-14" state_hub_workstream_slug: "ib-wp-0003-evaluation-and-inspection" state_hub_workstream_id: "bc368ba0-9fd7-4821-a5d7-e5c301faa80a" --- @@ -32,7 +32,7 @@ application behavior. ```task id: IB-WP-0003-T01 -status: todo +status: done priority: high state_hub_task_id: "9bab4b20-3fef-469e-9ce2-f0db3e05e26a" ``` @@ -45,7 +45,7 @@ state_hub_task_id: "9bab4b20-3fef-469e-9ce2-f0db3e05e26a" ```task id: IB-WP-0003-T02 -status: todo +status: done priority: high state_hub_task_id: "ee335d74-5be3-4b94-91e3-509486909f93" ``` @@ -58,7 +58,7 @@ state_hub_task_id: "ee335d74-5be3-4b94-91e3-509486909f93" ```task id: IB-WP-0003-T03 -status: todo +status: done priority: high state_hub_task_id: "d46b3429-37ef-4375-96e1-304eabf2cc13" ``` @@ -70,7 +70,7 @@ state_hub_task_id: "d46b3429-37ef-4375-96e1-304eabf2cc13" ```task id: IB-WP-0003-T04 -status: todo +status: done priority: medium state_hub_task_id: "de4f45e4-81a1-4ddb-98de-15e99ed5605a" ``` diff --git a/workplans/IB-WP-0004-reference-infospace-pilot.md b/workplans/IB-WP-0004-reference-infospace-pilot.md index 8fa7053..97e038c 100644 --- a/workplans/IB-WP-0004-reference-infospace-pilot.md +++ b/workplans/IB-WP-0004-reference-infospace-pilot.md @@ -4,11 +4,11 @@ type: workplan title: "Reference Infospace Pilot" domain: markitect repo: infospace-bench -status: planned +status: done owner: markitect topic_slug: markitect created: "2026-05-03" -updated: "2026-05-03" +updated: "2026-05-14" state_hub_workstream_slug: "ib-wp-0004-reference-infospace-pilot" state_hub_workstream_id: "8940a180-646a-4b20-b41f-c56719adfb0e" --- @@ -17,9 +17,10 @@ state_hub_workstream_id: "8940a180-646a-4b20-b41f-c56719adfb0e" ## Goal -Create the first maintained reference infospace in this repo, using -`markitect-main/examples/infospace-with-history/` as the primary migration -candidate. +Create the first maintained reference infospace in this repo. The large +`markitect-main/examples/infospace-with-history/` corpus remains the primary +future migration candidate; this workplan starts with a small purpose-built +bootstrap corpus so baseline behavior is easy to inspect. ## Tasks @@ -27,7 +28,7 @@ candidate. ```task id: IB-WP-0004-T01 -status: todo +status: done priority: high state_hub_task_id: "5042482f-a14c-4ae5-8d1a-19039dc97010" ``` @@ -40,7 +41,7 @@ state_hub_task_id: "5042482f-a14c-4ae5-8d1a-19039dc97010" ```task id: IB-WP-0004-T02 -status: todo +status: done priority: high state_hub_task_id: "a0909309-775b-4220-94f5-6ca811d44caf" ``` @@ -53,7 +54,7 @@ state_hub_task_id: "a0909309-775b-4220-94f5-6ca811d44caf" ```task id: IB-WP-0004-T03 -status: todo +status: done priority: medium state_hub_task_id: "10c001a3-ce42-4f1b-8b9a-9965844a94cb" ``` @@ -66,7 +67,7 @@ state_hub_task_id: "10c001a3-ce42-4f1b-8b9a-9965844a94cb" ```task id: IB-WP-0004-T04 -status: todo +status: done priority: medium state_hub_task_id: "b5e43c2a-b29c-4cb6-b33c-b4db631d0079" ```