generated from coulomb/repo-seed
engine and lifecycle
This commit is contained in:
@@ -22,6 +22,7 @@ Start with:
|
|||||||
- `docs/entity-relation-model.md`
|
- `docs/entity-relation-model.md`
|
||||||
- `docs/evaluation-history-and-metrics.md`
|
- `docs/evaluation-history-and-metrics.md`
|
||||||
- `docs/workflow-generation-pipeline.md`
|
- `docs/workflow-generation-pipeline.md`
|
||||||
|
- `docs/kontextual-engine-boundary.md`
|
||||||
- `docs/orthogonal-successor-roadmap.md`
|
- `docs/orthogonal-successor-roadmap.md`
|
||||||
- `docs/legacy-infospace-feature-inventory.md`
|
- `docs/legacy-infospace-feature-inventory.md`
|
||||||
- `docs/successor-boundary-interface-map.md`
|
- `docs/successor-boundary-interface-map.md`
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ artifacts/generated/
|
|||||||
workflows/templates/
|
workflows/templates/
|
||||||
output/evaluations/
|
output/evaluations/
|
||||||
output/metrics/
|
output/metrics/
|
||||||
|
output/engine/sync-runs/
|
||||||
output/workflows/runs/
|
output/workflows/runs/
|
||||||
reports/
|
reports/
|
||||||
exports/
|
exports/
|
||||||
|
|||||||
100
docs/kontextual-engine-boundary.md
Normal file
100
docs/kontextual-engine-boundary.md
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
# Kontextual Engine Boundary
|
||||||
|
|
||||||
|
`infospace-bench` can use `kontextual-engine` for durable knowledge operations,
|
||||||
|
but it must not become an engine itself. The default remains file-backed and
|
||||||
|
fully inspectable.
|
||||||
|
|
||||||
|
This boundary follows `kontextual-engine/INTENT.md`: the engine owns durable
|
||||||
|
asset identity, persistence, provenance, relationships, retrieval, workflow
|
||||||
|
runs, permissions, and audit. `infospace-bench` owns concrete infospaces,
|
||||||
|
manifests, evaluation methodology, workflow declarations, and reference pilots.
|
||||||
|
|
||||||
|
## Capability Contract
|
||||||
|
|
||||||
|
The current contract is exposed by:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from infospace_bench.engine import engine_capability_contract
|
||||||
|
```
|
||||||
|
|
||||||
|
File-backed application responsibilities:
|
||||||
|
|
||||||
|
- `infospace.yaml`
|
||||||
|
- `artifacts/index.yaml`
|
||||||
|
- Markdown artifacts
|
||||||
|
- evaluation history and metrics
|
||||||
|
- workflow run records
|
||||||
|
- dry-run sync plans
|
||||||
|
|
||||||
|
Engine-backed future responsibilities:
|
||||||
|
|
||||||
|
- durable asset identity and persistence
|
||||||
|
- provenance graph and relationship index
|
||||||
|
- retrieval
|
||||||
|
- workflow run persistence
|
||||||
|
- permissions and audit
|
||||||
|
|
||||||
|
Explicit non-goals for the engine boundary:
|
||||||
|
|
||||||
|
- Markdown syntax rules
|
||||||
|
- infospace-specific methodology
|
||||||
|
- repo identity
|
||||||
|
- provider-specific LLM calls
|
||||||
|
|
||||||
|
## Local Adapter
|
||||||
|
|
||||||
|
`LocalAssetRepository` is the default adapter. It writes asset records to:
|
||||||
|
|
||||||
|
```text
|
||||||
|
output/engine/assets.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
This file is a local evidence store, not a replacement for the infospace
|
||||||
|
manifest. The manifest remains authoritative for this repository.
|
||||||
|
|
||||||
|
Each asset record includes:
|
||||||
|
|
||||||
|
- `asset_id`
|
||||||
|
- `artifact_id`
|
||||||
|
- `infospace_slug`
|
||||||
|
- `kind`
|
||||||
|
- `path`
|
||||||
|
- `digest`
|
||||||
|
- `version`
|
||||||
|
- artifact provenance and relationships
|
||||||
|
|
||||||
|
## Sync Safety
|
||||||
|
|
||||||
|
Sync is dry-run by default:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m infospace_bench engine plan-sync infospaces/bootstrap-pilot
|
||||||
|
python3 -m infospace_bench engine sync infospaces/bootstrap-pilot
|
||||||
|
```
|
||||||
|
|
||||||
|
Mutation requires explicit apply:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m infospace_bench engine sync infospaces/bootstrap-pilot --apply
|
||||||
|
```
|
||||||
|
|
||||||
|
Apply writes the local adapter store and a sync run record under:
|
||||||
|
|
||||||
|
```text
|
||||||
|
output/engine/sync-runs/<run-id>.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Future `kontextual-engine` client configuration, permission changes, and apply
|
||||||
|
operations should be review-gated.
|
||||||
|
|
||||||
|
## First Safe Integration Scenario
|
||||||
|
|
||||||
|
The first safe scenario is one-way mirroring from file-backed infospace
|
||||||
|
artifacts into an engine-backed asset store:
|
||||||
|
|
||||||
|
1. Inspect the capability contract.
|
||||||
|
2. Run a dry-run sync plan.
|
||||||
|
3. Review creates, updates, unchanged assets, and digests.
|
||||||
|
4. Apply only after review.
|
||||||
|
5. Treat `artifacts/index.yaml` as the source of truth until a later workplan
|
||||||
|
explicitly changes that operational mode.
|
||||||
@@ -27,7 +27,7 @@ considered a replacement for each in-scope legacy infospace behavior from
|
|||||||
| Evaluate with LLM assistance | Provider-neutral assisted evaluation workflow | Dry-run plan, adapter contract, audited output | `IB-WP-0009` | boundary done |
|
| Evaluate with LLM assistance | Provider-neutral assisted evaluation workflow | Dry-run plan, adapter contract, audited output | `IB-WP-0009` | boundary done |
|
||||||
| Process source chapters | Explicit infospace workflow stages | Deterministic runner tests, generated artifact provenance | `IB-WP-0009` | deterministic baseline done |
|
| Process source chapters | Explicit infospace workflow stages | Deterministic runner tests, generated artifact provenance | `IB-WP-0009` | deterministic baseline done |
|
||||||
| Track stale outputs | Digest/provenance comparison | Tests after workflow provenance exists | `IB-WP-0009` | deferred |
|
| Track stale outputs | Digest/provenance comparison | Tests after workflow provenance exists | `IB-WP-0009` | deferred |
|
||||||
| Persist durable assets | Optional engine-backed repository adapter | Dry-run sync tests and integration design | `IB-WP-0010` | planned |
|
| Persist durable assets | Optional engine-backed repository adapter | Dry-run sync tests and integration design | `IB-WP-0010` | boundary done |
|
||||||
| Run a legacy-derived pilot | Pruned `infospace-with-history` migration | Pilot corpus, migration report, parity comparison | `IB-WP-0011` | planned |
|
| Run a legacy-derived pilot | Pruned `infospace-with-history` migration | Pilot corpus, migration report, parity comparison | `IB-WP-0011` | planned |
|
||||||
| Provide command migration path | Legacy command parity guide | Command table and examples | `IB-WP-0012` | planned |
|
| Provide command migration path | Legacy command parity guide | Command table and examples | `IB-WP-0012` | planned |
|
||||||
|
|
||||||
|
|||||||
@@ -101,15 +101,17 @@ Responsibilities:
|
|||||||
|
|
||||||
### Repository Adapter
|
### Repository Adapter
|
||||||
|
|
||||||
Planned module:
|
Current module:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
src/infospace_bench/repository.py
|
src/infospace_bench/engine.py
|
||||||
```
|
```
|
||||||
|
|
||||||
Responsibilities:
|
Responsibilities:
|
||||||
|
|
||||||
- provide local file-backed artifact reads/writes
|
- provide local file-backed artifact reads/writes
|
||||||
|
- expose an `AssetRepository` protocol
|
||||||
|
- provide `LocalAssetRepository` as the default adapter
|
||||||
- later provide `kontextual-engine` backed asset lookup/sync
|
- later provide `kontextual-engine` backed asset lookup/sync
|
||||||
- expose dry-run sync plans before mutation
|
- expose dry-run sync plans before mutation
|
||||||
- keep provenance and digest handling explicit
|
- keep provenance and digest handling explicit
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
from .errors import InfospaceError
|
from .errors import InfospaceError
|
||||||
from .evaluation import EntityEvaluation, EvaluationSnapshot, MetricValue, ScoreEntry
|
from .evaluation import EntityEvaluation, EvaluationSnapshot, MetricValue, ScoreEntry
|
||||||
|
from .engine import (
|
||||||
|
AssetRecord,
|
||||||
|
AssetSyncAction,
|
||||||
|
AssetSyncPlan,
|
||||||
|
EngineCapabilityContract,
|
||||||
|
LocalAssetRepository,
|
||||||
|
engine_capability_contract,
|
||||||
|
plan_asset_sync,
|
||||||
|
sync_assets,
|
||||||
|
)
|
||||||
from .evaluation_io import (
|
from .evaluation_io import (
|
||||||
append_to_history,
|
append_to_history,
|
||||||
read_entity_evaluation,
|
read_entity_evaluation,
|
||||||
@@ -39,13 +49,19 @@ __all__ = [
|
|||||||
"KnowledgeArtifact",
|
"KnowledgeArtifact",
|
||||||
"MetricValue",
|
"MetricValue",
|
||||||
"EntityRecord",
|
"EntityRecord",
|
||||||
|
"EngineCapabilityContract",
|
||||||
"RelationRecord",
|
"RelationRecord",
|
||||||
|
"AssetRecord",
|
||||||
|
"AssetSyncAction",
|
||||||
|
"AssetSyncPlan",
|
||||||
|
"LocalAssetRepository",
|
||||||
"ScoreEntry",
|
"ScoreEntry",
|
||||||
"TopicConfig",
|
"TopicConfig",
|
||||||
"ViabilityThreshold",
|
"ViabilityThreshold",
|
||||||
"add_artifact",
|
"add_artifact",
|
||||||
"append_to_history",
|
"append_to_history",
|
||||||
"create_infospace",
|
"create_infospace",
|
||||||
|
"engine_capability_contract",
|
||||||
"find_snapshot",
|
"find_snapshot",
|
||||||
"get_history",
|
"get_history",
|
||||||
"get_latest_snapshot",
|
"get_latest_snapshot",
|
||||||
@@ -62,6 +78,8 @@ __all__ = [
|
|||||||
"load_workflows",
|
"load_workflows",
|
||||||
"plan_workflow",
|
"plan_workflow",
|
||||||
"run_workflow",
|
"run_workflow",
|
||||||
|
"plan_asset_sync",
|
||||||
|
"sync_assets",
|
||||||
"write_entity_evaluation",
|
"write_entity_evaluation",
|
||||||
"write_metrics_file",
|
"write_metrics_file",
|
||||||
"write_snapshot",
|
"write_snapshot",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import sys
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from .checks import run_collection_checks
|
from .checks import run_collection_checks
|
||||||
|
from .engine import engine_capability_contract, plan_asset_sync, sync_assets
|
||||||
from .errors import InfospaceError
|
from .errors import InfospaceError
|
||||||
from .history import find_snapshot, get_history, metric_trend, record_check_results
|
from .history import find_snapshot, get_history, metric_trend, record_check_results
|
||||||
from .lifecycle import add_artifact, create_infospace, load_infospace
|
from .lifecycle import add_artifact, create_infospace, load_infospace
|
||||||
@@ -86,6 +87,28 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
workflow_run.add_argument("root")
|
workflow_run.add_argument("root")
|
||||||
workflow_run.add_argument("workflow_id")
|
workflow_run.add_argument("workflow_id")
|
||||||
|
|
||||||
|
engine = sub.add_parser("engine", help="Inspect and sync engine boundary state")
|
||||||
|
engine_sub = engine.add_subparsers(dest="engine_command", required=True)
|
||||||
|
|
||||||
|
engine_inspect = engine_sub.add_parser(
|
||||||
|
"inspect",
|
||||||
|
help="Inspect the optional engine capability contract",
|
||||||
|
)
|
||||||
|
engine_inspect.add_argument("root")
|
||||||
|
|
||||||
|
engine_plan = engine_sub.add_parser(
|
||||||
|
"plan-sync",
|
||||||
|
help="Plan artifact-to-asset sync without mutation",
|
||||||
|
)
|
||||||
|
engine_plan.add_argument("root")
|
||||||
|
|
||||||
|
engine_sync = engine_sub.add_parser(
|
||||||
|
"sync",
|
||||||
|
help="Dry-run artifact-to-asset sync unless --apply is passed",
|
||||||
|
)
|
||||||
|
engine_sync.add_argument("root")
|
||||||
|
engine_sync.add_argument("--apply", action="store_true")
|
||||||
|
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
@@ -194,6 +217,22 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
parser.error(f"Unhandled workflow command: {args.workflow_command}")
|
parser.error(f"Unhandled workflow command: {args.workflow_command}")
|
||||||
|
elif args.command == "engine":
|
||||||
|
if args.engine_command == "inspect":
|
||||||
|
_write_json(
|
||||||
|
{
|
||||||
|
"root": str(Path(args.root)),
|
||||||
|
"contract": engine_capability_contract().to_dict(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
elif args.engine_command == "plan-sync":
|
||||||
|
_write_json(plan_asset_sync(Path(args.root)).to_dict())
|
||||||
|
elif args.engine_command == "sync":
|
||||||
|
_write_json(
|
||||||
|
sync_assets(Path(args.root), dry_run=not args.apply).to_dict()
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
parser.error(f"Unhandled engine command: {args.engine_command}")
|
||||||
else:
|
else:
|
||||||
parser.error(f"Unhandled command: {args.command}")
|
parser.error(f"Unhandled command: {args.command}")
|
||||||
except InfospaceError as exc:
|
except InfospaceError as exc:
|
||||||
|
|||||||
365
src/infospace_bench/engine.py
Normal file
365
src/infospace_bench/engine.py
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import uuid
|
||||||
|
from dataclasses import asdict, dataclass, field
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Protocol
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from .errors import InfospaceError
|
||||||
|
from .lifecycle import load_infospace
|
||||||
|
from .models import Infospace, KnowledgeArtifact
|
||||||
|
|
||||||
|
ENGINE_DIR = Path("output/engine")
|
||||||
|
ASSET_STORE = ENGINE_DIR / "assets.yaml"
|
||||||
|
SYNC_RUN_DIR = ENGINE_DIR / "sync-runs"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class EngineCapabilityContract:
|
||||||
|
integration_posture: str
|
||||||
|
file_backed: list[str]
|
||||||
|
engine_backed: list[str]
|
||||||
|
explicit_non_goals: list[str]
|
||||||
|
review_required: list[str]
|
||||||
|
assumptions: dict[str, str]
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
return asdict(self)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class AssetRecord:
|
||||||
|
asset_id: str
|
||||||
|
artifact_id: str
|
||||||
|
infospace_slug: str
|
||||||
|
kind: str
|
||||||
|
title: str
|
||||||
|
path: str
|
||||||
|
digest: str
|
||||||
|
size_bytes: int
|
||||||
|
version: str
|
||||||
|
provenance: dict[str, Any] = field(default_factory=dict)
|
||||||
|
relationships: list[dict[str, Any]] = field(default_factory=list)
|
||||||
|
metadata: dict[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: dict[str, Any]) -> "AssetRecord":
|
||||||
|
return cls(
|
||||||
|
asset_id=str(data["asset_id"]),
|
||||||
|
artifact_id=str(data["artifact_id"]),
|
||||||
|
infospace_slug=str(data["infospace_slug"]),
|
||||||
|
kind=str(data["kind"]),
|
||||||
|
title=str(data.get("title") or ""),
|
||||||
|
path=str(data["path"]),
|
||||||
|
digest=str(data["digest"]),
|
||||||
|
size_bytes=int(data.get("size_bytes") or 0),
|
||||||
|
version=str(data.get("version") or data["digest"][:12]),
|
||||||
|
provenance=dict(data.get("provenance") or {}),
|
||||||
|
relationships=list(data.get("relationships") or []),
|
||||||
|
metadata=dict(data.get("metadata") or {}),
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
return asdict(self)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class AssetSyncAction:
|
||||||
|
action: str
|
||||||
|
asset_id: str
|
||||||
|
artifact_id: str
|
||||||
|
path: str
|
||||||
|
digest: str
|
||||||
|
previous_digest: str = ""
|
||||||
|
workflow_id: str = ""
|
||||||
|
reason: str = ""
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
key: value
|
||||||
|
for key, value in asdict(self).items()
|
||||||
|
if value not in ("", None)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class AssetSyncPlan:
|
||||||
|
infospace_slug: str
|
||||||
|
repository: str
|
||||||
|
dry_run: bool
|
||||||
|
status: str
|
||||||
|
actions: list[AssetSyncAction]
|
||||||
|
capabilities: EngineCapabilityContract
|
||||||
|
run_record_path: str = ""
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
data = {
|
||||||
|
"infospace_slug": self.infospace_slug,
|
||||||
|
"repository": self.repository,
|
||||||
|
"dry_run": self.dry_run,
|
||||||
|
"status": self.status,
|
||||||
|
"actions": [action.to_dict() for action in self.actions],
|
||||||
|
"contract": self.capabilities.to_dict(),
|
||||||
|
"run_record_path": self.run_record_path,
|
||||||
|
}
|
||||||
|
return {key: value for key, value in data.items() if value not in ("", [])}
|
||||||
|
|
||||||
|
|
||||||
|
class AssetRepository(Protocol):
|
||||||
|
repository_id: str
|
||||||
|
|
||||||
|
def get_asset(self, asset_id: str) -> AssetRecord | None:
|
||||||
|
"""Return an asset by durable ID if present."""
|
||||||
|
|
||||||
|
def list_assets(self) -> list[AssetRecord]:
|
||||||
|
"""Return all assets known to the repository."""
|
||||||
|
|
||||||
|
def upsert_asset(self, asset: AssetRecord) -> None:
|
||||||
|
"""Create or update a durable asset record."""
|
||||||
|
|
||||||
|
|
||||||
|
class LocalAssetRepository:
|
||||||
|
"""File-backed adapter used until a kontextual-engine client exists."""
|
||||||
|
|
||||||
|
def __init__(self, path: str | Path) -> None:
|
||||||
|
self.path = Path(path)
|
||||||
|
self.repository_id = f"local-file:{self.path}"
|
||||||
|
|
||||||
|
def get_asset(self, asset_id: str) -> AssetRecord | None:
|
||||||
|
return self._asset_map().get(asset_id)
|
||||||
|
|
||||||
|
def list_assets(self) -> list[AssetRecord]:
|
||||||
|
return list(self._asset_map().values())
|
||||||
|
|
||||||
|
def upsert_asset(self, asset: AssetRecord) -> None:
|
||||||
|
assets = self._asset_map()
|
||||||
|
assets[asset.asset_id] = asset
|
||||||
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
self.path.write_text(
|
||||||
|
yaml.safe_dump(
|
||||||
|
{
|
||||||
|
"assets": [
|
||||||
|
item.to_dict()
|
||||||
|
for item in sorted(
|
||||||
|
assets.values(),
|
||||||
|
key=lambda record: record.asset_id,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
},
|
||||||
|
sort_keys=False,
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _asset_map(self) -> dict[str, AssetRecord]:
|
||||||
|
if not self.path.is_file():
|
||||||
|
return {}
|
||||||
|
data = yaml.safe_load(self.path.read_text(encoding="utf-8")) or {}
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return {}
|
||||||
|
return {
|
||||||
|
item.asset_id: item
|
||||||
|
for item in [
|
||||||
|
AssetRecord.from_dict(raw)
|
||||||
|
for raw in data.get("assets", [])
|
||||||
|
if isinstance(raw, dict)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def engine_capability_contract() -> EngineCapabilityContract:
|
||||||
|
return EngineCapabilityContract(
|
||||||
|
integration_posture="optional",
|
||||||
|
file_backed=[
|
||||||
|
"artifact_manifest",
|
||||||
|
"infospace_config",
|
||||||
|
"markdown_artifacts",
|
||||||
|
"evaluation_history",
|
||||||
|
"workflow_run_records",
|
||||||
|
"dry_run_sync_plans",
|
||||||
|
],
|
||||||
|
engine_backed=[
|
||||||
|
"asset_identity",
|
||||||
|
"asset_persistence",
|
||||||
|
"provenance_graph",
|
||||||
|
"relationship_index",
|
||||||
|
"retrieval",
|
||||||
|
"workflow_runs",
|
||||||
|
"permissions",
|
||||||
|
"audit",
|
||||||
|
],
|
||||||
|
explicit_non_goals=[
|
||||||
|
"markdown_syntax_rules",
|
||||||
|
"infospace_domain_methodology",
|
||||||
|
"repository_identity",
|
||||||
|
"provider_specific_llm_calls",
|
||||||
|
],
|
||||||
|
review_required=[
|
||||||
|
"apply_sync",
|
||||||
|
"engine_client_configuration",
|
||||||
|
"permission_changes",
|
||||||
|
],
|
||||||
|
assumptions={
|
||||||
|
"source": "kontextual-engine/INTENT.md",
|
||||||
|
"engine_role": "durable knowledge operations engine",
|
||||||
|
"bench_role": "application workspace with file-backed default behavior",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def plan_asset_sync(
|
||||||
|
root: str | Path,
|
||||||
|
repository: AssetRepository | None = None,
|
||||||
|
) -> AssetSyncPlan:
|
||||||
|
infospace = load_infospace(root)
|
||||||
|
repo = repository or LocalAssetRepository(infospace.root / ASSET_STORE)
|
||||||
|
actions = [
|
||||||
|
_sync_action(record, repo.get_asset(record.asset_id))
|
||||||
|
for record in _asset_records(infospace)
|
||||||
|
]
|
||||||
|
return AssetSyncPlan(
|
||||||
|
infospace_slug=infospace.config.slug,
|
||||||
|
repository=repo.repository_id,
|
||||||
|
dry_run=True,
|
||||||
|
status="planned",
|
||||||
|
actions=actions,
|
||||||
|
capabilities=engine_capability_contract(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sync_assets(
|
||||||
|
root: str | Path,
|
||||||
|
*,
|
||||||
|
repository: AssetRepository | None = None,
|
||||||
|
dry_run: bool = True,
|
||||||
|
) -> AssetSyncPlan:
|
||||||
|
infospace = load_infospace(root)
|
||||||
|
repo = repository or LocalAssetRepository(infospace.root / ASSET_STORE)
|
||||||
|
plan = plan_asset_sync(infospace.root, repo)
|
||||||
|
if dry_run:
|
||||||
|
return plan
|
||||||
|
|
||||||
|
records = {record.asset_id: record for record in _asset_records(infospace)}
|
||||||
|
for action in plan.actions:
|
||||||
|
if action.action in {"create", "update"}:
|
||||||
|
repo.upsert_asset(records[action.asset_id])
|
||||||
|
|
||||||
|
applied = AssetSyncPlan(
|
||||||
|
infospace_slug=plan.infospace_slug,
|
||||||
|
repository=plan.repository,
|
||||||
|
dry_run=False,
|
||||||
|
status="completed",
|
||||||
|
actions=plan.actions,
|
||||||
|
capabilities=plan.capabilities,
|
||||||
|
)
|
||||||
|
run_record_path = _write_sync_run(infospace.root, applied)
|
||||||
|
return AssetSyncPlan(
|
||||||
|
infospace_slug=applied.infospace_slug,
|
||||||
|
repository=applied.repository,
|
||||||
|
dry_run=applied.dry_run,
|
||||||
|
status=applied.status,
|
||||||
|
actions=applied.actions,
|
||||||
|
capabilities=applied.capabilities,
|
||||||
|
run_record_path=run_record_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def asset_id_for(infospace_slug: str, artifact_id: str) -> str:
|
||||||
|
return f"infospace:{infospace_slug}:artifact:{artifact_id}"
|
||||||
|
|
||||||
|
|
||||||
|
def _asset_records(infospace: Infospace) -> list[AssetRecord]:
|
||||||
|
return [
|
||||||
|
_asset_record_for(infospace, artifact)
|
||||||
|
for artifact in infospace.artifacts
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _asset_record_for(
|
||||||
|
infospace: Infospace,
|
||||||
|
artifact: KnowledgeArtifact,
|
||||||
|
) -> AssetRecord:
|
||||||
|
artifact_path = infospace.root / artifact.path
|
||||||
|
if not artifact_path.is_file():
|
||||||
|
raise InfospaceError(
|
||||||
|
"missing_artifact_file",
|
||||||
|
f"Manifest artifact file does not exist: {artifact.path}",
|
||||||
|
{"artifact_id": artifact.id, "path": artifact.path},
|
||||||
|
)
|
||||||
|
data = artifact_path.read_bytes()
|
||||||
|
digest = hashlib.sha256(data).hexdigest()
|
||||||
|
workflow_id = str(artifact.provenance.get("workflow_id") or "")
|
||||||
|
return AssetRecord(
|
||||||
|
asset_id=asset_id_for(infospace.config.slug, artifact.id),
|
||||||
|
artifact_id=artifact.id,
|
||||||
|
infospace_slug=infospace.config.slug,
|
||||||
|
kind=artifact.kind,
|
||||||
|
title=artifact.title,
|
||||||
|
path=artifact.path,
|
||||||
|
digest=digest,
|
||||||
|
size_bytes=len(data),
|
||||||
|
version=digest[:12],
|
||||||
|
provenance={
|
||||||
|
**artifact.provenance,
|
||||||
|
"artifact_id": artifact.id,
|
||||||
|
"artifact_path": artifact.path,
|
||||||
|
"infospace_slug": infospace.config.slug,
|
||||||
|
},
|
||||||
|
relationships=artifact.relationships,
|
||||||
|
metadata={
|
||||||
|
"workflow_id": workflow_id,
|
||||||
|
"source_path": artifact.provenance.get("source_path", ""),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _sync_action(
|
||||||
|
desired: AssetRecord,
|
||||||
|
existing: AssetRecord | None,
|
||||||
|
) -> AssetSyncAction:
|
||||||
|
workflow_id = str(desired.provenance.get("workflow_id") or "")
|
||||||
|
if existing is None:
|
||||||
|
return AssetSyncAction(
|
||||||
|
action="create",
|
||||||
|
asset_id=desired.asset_id,
|
||||||
|
artifact_id=desired.artifact_id,
|
||||||
|
path=desired.path,
|
||||||
|
digest=desired.digest,
|
||||||
|
workflow_id=workflow_id,
|
||||||
|
reason="asset_not_present",
|
||||||
|
)
|
||||||
|
if existing.digest != desired.digest:
|
||||||
|
return AssetSyncAction(
|
||||||
|
action="update",
|
||||||
|
asset_id=desired.asset_id,
|
||||||
|
artifact_id=desired.artifact_id,
|
||||||
|
path=desired.path,
|
||||||
|
digest=desired.digest,
|
||||||
|
previous_digest=existing.digest,
|
||||||
|
workflow_id=workflow_id,
|
||||||
|
reason="digest_changed",
|
||||||
|
)
|
||||||
|
return AssetSyncAction(
|
||||||
|
action="unchanged",
|
||||||
|
asset_id=desired.asset_id,
|
||||||
|
artifact_id=desired.artifact_id,
|
||||||
|
path=desired.path,
|
||||||
|
digest=desired.digest,
|
||||||
|
previous_digest=existing.digest,
|
||||||
|
workflow_id=workflow_id,
|
||||||
|
reason="digest_matches",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _write_sync_run(root: Path, plan: AssetSyncPlan) -> str:
|
||||||
|
run_id = uuid.uuid4().hex[:12]
|
||||||
|
path = root / SYNC_RUN_DIR / f"{run_id}.yaml"
|
||||||
|
payload = plan.to_dict()
|
||||||
|
payload["recorded_at"] = datetime.now(timezone.utc).isoformat()
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(yaml.safe_dump(payload, sort_keys=False), encoding="utf-8")
|
||||||
|
return str(path)
|
||||||
@@ -21,6 +21,7 @@ LAYOUT_DIRS = (
|
|||||||
"workflows/templates",
|
"workflows/templates",
|
||||||
"output/evaluations",
|
"output/evaluations",
|
||||||
"output/metrics",
|
"output/metrics",
|
||||||
|
"output/engine/sync-runs",
|
||||||
"output/workflows/runs",
|
"output/workflows/runs",
|
||||||
"reports",
|
"reports",
|
||||||
"exports",
|
"exports",
|
||||||
|
|||||||
167
tests/test_engine_boundary.py
Normal file
167
tests/test_engine_boundary.py
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from infospace_bench import add_artifact, create_infospace, load_infospace, register_artifact
|
||||||
|
from infospace_bench.engine import (
|
||||||
|
LocalAssetRepository,
|
||||||
|
engine_capability_contract,
|
||||||
|
plan_asset_sync,
|
||||||
|
sync_assets,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def cli_env() -> dict[str, str]:
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["PYTHONPATH"] = "src:/home/worsch/markitect-tool/src"
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
def make_engine_infospace(tmp_path: Path) -> Path:
|
||||||
|
infospace = create_infospace(tmp_path, "pilot", name="Pilot")
|
||||||
|
source = tmp_path / "chapter.md"
|
||||||
|
source.write_text("# Chapter\n\nSource text.\n", encoding="utf-8")
|
||||||
|
add_artifact(infospace.root, source, kind="source", title="Chapter")
|
||||||
|
|
||||||
|
generated = infospace.root / "artifacts" / "generated" / "chapter-summary.md"
|
||||||
|
generated.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
generated.write_text("# Chapter Summary\n\nGenerated text.\n", encoding="utf-8")
|
||||||
|
register_artifact(
|
||||||
|
infospace.root,
|
||||||
|
artifact_id="generated/chapter-summary.md",
|
||||||
|
path="artifacts/generated/chapter-summary.md",
|
||||||
|
kind="generated",
|
||||||
|
title="Chapter Summary",
|
||||||
|
provenance={
|
||||||
|
"workflow_id": "source-summary",
|
||||||
|
"stage_id": "render-summary",
|
||||||
|
"input_artifact_id": "source/chapter.md",
|
||||||
|
},
|
||||||
|
relationships=[
|
||||||
|
{"type": "generated_from", "target": "source/chapter.md"},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
return infospace.root
|
||||||
|
|
||||||
|
|
||||||
|
def test_engine_capability_contract_keeps_application_boundary_explicit() -> None:
|
||||||
|
contract = engine_capability_contract()
|
||||||
|
|
||||||
|
assert "asset_identity" in contract.engine_backed
|
||||||
|
assert "artifact_manifest" in contract.file_backed
|
||||||
|
assert "markdown_parsing" not in contract.engine_backed
|
||||||
|
assert contract.assumptions["source"] == "kontextual-engine/INTENT.md"
|
||||||
|
assert contract.review_required == [
|
||||||
|
"apply_sync",
|
||||||
|
"engine_client_configuration",
|
||||||
|
"permission_changes",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_plan_asset_sync_is_dry_run_and_maps_artifacts_to_assets(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
root = make_engine_infospace(tmp_path)
|
||||||
|
repository_path = root / "output" / "engine" / "assets.yaml"
|
||||||
|
|
||||||
|
plan = plan_asset_sync(root)
|
||||||
|
|
||||||
|
assert plan.dry_run is True
|
||||||
|
assert repository_path.exists() is False
|
||||||
|
assert [action.action for action in plan.actions] == ["create", "create"]
|
||||||
|
assert plan.actions[0].artifact_id == "source/chapter.md"
|
||||||
|
assert plan.actions[0].asset_id == "infospace:pilot:artifact:source/chapter.md"
|
||||||
|
assert len(plan.actions[0].digest) == 64
|
||||||
|
assert plan.actions[1].workflow_id == "source-summary"
|
||||||
|
|
||||||
|
|
||||||
|
def test_sync_assets_apply_persists_to_local_repository_and_detects_updates(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
root = make_engine_infospace(tmp_path)
|
||||||
|
|
||||||
|
applied = sync_assets(root, dry_run=False)
|
||||||
|
repository = LocalAssetRepository(root / "output" / "engine" / "assets.yaml")
|
||||||
|
assets = repository.list_assets()
|
||||||
|
unchanged = plan_asset_sync(root)
|
||||||
|
|
||||||
|
source_path = root / "artifacts" / "sources" / "chapter.md"
|
||||||
|
source_path.write_text("# Chapter\n\nChanged text.\n", encoding="utf-8")
|
||||||
|
updated = plan_asset_sync(root)
|
||||||
|
|
||||||
|
assert applied.status == "completed"
|
||||||
|
assert len(assets) == 2
|
||||||
|
assert repository.get_asset("infospace:pilot:artifact:source/chapter.md") is not None
|
||||||
|
assert [action.action for action in unchanged.actions] == [
|
||||||
|
"unchanged",
|
||||||
|
"unchanged",
|
||||||
|
]
|
||||||
|
assert updated.actions[0].action == "update"
|
||||||
|
assert updated.actions[0].previous_digest != updated.actions[0].digest
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_engine_inspect_plan_sync_and_apply(tmp_path: Path) -> None:
|
||||||
|
root = make_engine_infospace(tmp_path)
|
||||||
|
|
||||||
|
inspected = subprocess.run(
|
||||||
|
[sys.executable, "-m", "infospace_bench", "engine", "inspect", str(root)],
|
||||||
|
check=False,
|
||||||
|
env=cli_env(),
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
planned = subprocess.run(
|
||||||
|
[sys.executable, "-m", "infospace_bench", "engine", "plan-sync", str(root)],
|
||||||
|
check=False,
|
||||||
|
env=cli_env(),
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
dry_run_sync = subprocess.run(
|
||||||
|
[sys.executable, "-m", "infospace_bench", "engine", "sync", str(root)],
|
||||||
|
check=False,
|
||||||
|
env=cli_env(),
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
applied = subprocess.run(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
"-m",
|
||||||
|
"infospace_bench",
|
||||||
|
"engine",
|
||||||
|
"sync",
|
||||||
|
str(root),
|
||||||
|
"--apply",
|
||||||
|
],
|
||||||
|
check=False,
|
||||||
|
env=cli_env(),
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert inspected.returncode == 0, inspected.stderr
|
||||||
|
assert planned.returncode == 0, planned.stderr
|
||||||
|
assert dry_run_sync.returncode == 0, dry_run_sync.stderr
|
||||||
|
assert applied.returncode == 0, applied.stderr
|
||||||
|
assert json.loads(inspected.stdout)["contract"]["integration_posture"] == "optional"
|
||||||
|
assert json.loads(planned.stdout)["dry_run"] is True
|
||||||
|
assert json.loads(dry_run_sync.stdout)["dry_run"] is True
|
||||||
|
assert json.loads(applied.stdout)["status"] == "completed"
|
||||||
|
assert (root / "output" / "engine" / "assets.yaml").is_file()
|
||||||
|
|
||||||
|
|
||||||
|
def test_file_backed_infospace_remains_authoritative_after_engine_sync(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
root = make_engine_infospace(tmp_path)
|
||||||
|
|
||||||
|
sync_assets(root, dry_run=False)
|
||||||
|
loaded = load_infospace(root)
|
||||||
|
|
||||||
|
assert [artifact.id for artifact in loaded.artifacts] == [
|
||||||
|
"source/chapter.md",
|
||||||
|
"generated/chapter-summary.md",
|
||||||
|
]
|
||||||
@@ -4,7 +4,7 @@ type: workplan
|
|||||||
title: "Kontextual Engine Integration Boundary"
|
title: "Kontextual Engine Integration Boundary"
|
||||||
domain: markitect
|
domain: markitect
|
||||||
repo: infospace-bench
|
repo: infospace-bench
|
||||||
status: planned
|
status: completed
|
||||||
owner: markitect
|
owner: markitect
|
||||||
topic_slug: markitect
|
topic_slug: markitect
|
||||||
created: "2026-05-14"
|
created: "2026-05-14"
|
||||||
@@ -27,7 +27,7 @@ Define and implement the boundary where concrete infospaces can use
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: IB-WP-0010-T01
|
id: IB-WP-0010-T01
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "f77c10cf-6ea2-44a6-af0b-f9c70860b999"
|
state_hub_task_id: "f77c10cf-6ea2-44a6-af0b-f9c70860b999"
|
||||||
```
|
```
|
||||||
@@ -41,7 +41,7 @@ state_hub_task_id: "f77c10cf-6ea2-44a6-af0b-f9c70860b999"
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: IB-WP-0010-T02
|
id: IB-WP-0010-T02
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "bd20ddaa-c37d-4ec0-af4e-45198172e6f8"
|
state_hub_task_id: "bd20ddaa-c37d-4ec0-af4e-45198172e6f8"
|
||||||
```
|
```
|
||||||
@@ -54,7 +54,7 @@ state_hub_task_id: "bd20ddaa-c37d-4ec0-af4e-45198172e6f8"
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: IB-WP-0010-T03
|
id: IB-WP-0010-T03
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "12ffd95b-6db2-4881-bc3c-6bc4f9f3cc21"
|
state_hub_task_id: "12ffd95b-6db2-4881-bc3c-6bc4f9f3cc21"
|
||||||
```
|
```
|
||||||
@@ -68,7 +68,7 @@ state_hub_task_id: "12ffd95b-6db2-4881-bc3c-6bc4f9f3cc21"
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: IB-WP-0010-T04
|
id: IB-WP-0010-T04
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "fe1031d6-0b48-4fa1-9edd-3b1908a62003"
|
state_hub_task_id: "fe1031d6-0b48-4fa1-9edd-3b1908a62003"
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user