diff --git a/docs/operations.md b/docs/operations.md index 45f1e677..de284a5b 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -39,6 +39,7 @@ Effect classes used below: | Read exact material/resources | CLI `read` / `resource`; Python `KnowledgeReader.read` / `KnowledgeReader.resource` | Read-only | Returns the requested revision or an explicit unavailable/stale error; never substitutes newer bytes | | Export legacy cited material | CLI `context` | Read-only | Cited package export for manual use with a coding agent; not a conversation store and not a full Collect equivalent | | Create/update a store candidate | Python `KnowledgeStore.create` / `KnowledgeStore.update` | Candidate-writing | One explicitly authorized local store; expected-revision updates with reason and evidence; retained history; no promotion and no edits to package/checkout content | +| Inspect a store candidate's history | Python `open_knowledge_store(...).history` | Read-only | Opens an existing store and reads the revision lineage for one store ID | | Amend repository-owned knowledge | Scoped repository PR | — | Repository review and integration authority apply; this is not a runtime store Update | | Inspect/prepare experiments | CLI `doctor`; `plan` or `--dry-run` | Read-only inspection / preview | `doctor` may invoke bounded probes and does not certify target compatibility; `plan` previews a specification without persisting a campaign or authorizing execution | | Execute/inspect experiments | CLI `run`; `status`; `report` | Experiment-executing / read-only / report-generating | `run` executes caller-supplied commands and records evidence; `status` reads recorded progress; `report` generates HTML from recorded evidence | @@ -194,6 +195,11 @@ for scoring, provenance and accounting. For example: ## Store candidates (Python) +Open the store with `inferrsi.apps.knowledge.open_knowledge_store`. Pass the +authorized `store_root` and the store's reserved `skill_root`, if configured. +The default opens an existing store read-only for history. Pass +`writable=True` explicitly to create or update candidates. + ### Create / Update — `KnowledgeStore.create` / `.update` - **Inputs:** a complete candidate `Bundle`; Update additionally requires the expected current revision, a reason and an explicit evidence list. diff --git a/src/inferrsi/apps/knowledge.py b/src/inferrsi/apps/knowledge.py index 3c7d240d..881a70cb 100644 --- a/src/inferrsi/apps/knowledge.py +++ b/src/inferrsi/apps/knowledge.py @@ -6,12 +6,21 @@ from inferrsi.knowledge.knowledge_store import KnowledgeStore +def open_knowledge_store( + *, store_root: Path, skill_root: Path | None = None, writable: bool = False, +) -> KnowledgeStore: + """Open one caller-authorized local store for history or an explicit write.""" + if type(writable) is not bool: + raise ValueError("writable must be a boolean") + return KnowledgeStore(store_root, skill_root=skill_root, read_only=not writable) + + def knowledge_reader( *, skill_root: Path | None = None, store_root: Path | None = None, ) -> KnowledgeReader: """Select a Knowledge reader without enabling store writes.""" store = ( - KnowledgeStore(store_root, skill_root=skill_root, read_only=True) + open_knowledge_store(store_root=store_root, skill_root=skill_root) if store_root is not None else None ) diff --git a/tests/test_apps_knowledge.py b/tests/test_apps_knowledge.py index eabfda07..5aceaf69 100644 --- a/tests/test_apps_knowledge.py +++ b/tests/test_apps_knowledge.py @@ -4,8 +4,8 @@ import pytest -from inferrsi.apps.knowledge import knowledge_reader, read -from inferrsi.knowledge.knowledge_operations import RevisionConflict +from inferrsi.apps.knowledge import knowledge_reader, open_knowledge_store, read +from inferrsi.knowledge.knowledge_operations import Bundle, RevisionConflict from inferrsi.knowledge.knowledge_store import KnowledgeStore _IDENTITY = "inferrsi.common.skills.audit_h3_runtime_residency" @@ -51,3 +51,43 @@ def count_owners(): assert revision == view["revision"] assert view["content"].endswith("Example\n") assert len(scans) == 1 + + +def test_apps_store_requires_explicit_write_and_retains_fixed_history(tmp_path): + root = tmp_path / "candidate-store" + root.mkdir() + with pytest.raises(ValueError, match="requires an existing database"): + open_knowledge_store(store_root=root) + with pytest.raises(ValueError, match="boolean"): + open_knowledge_store(store_root=root, writable="yes") + assert not list(root.iterdir()) + + source = knowledge_reader().read(_IDENTITY) + source_ref = f"package:{_IDENTITY}" + identity = "apps-example-candidate" + metadata = { + **source.metadata, + "id": identity, + "state": "candidate", + "derived_from": [{"ref": source_ref, "revision": source.revision}], + } + bundle = Bundle(metadata, source.content, source.dependencies) + store = open_knowledge_store(store_root=root, writable=True) + first = store.create(bundle) + updated = Bundle( + {**metadata, "title": "Revised candidate"}, bundle.content, bundle.dependencies, + ) + second = store.update(updated, expected=first, reason="clarify title", evidence=[]) + with pytest.raises(RevisionConflict, match="Stale"): + store.update(bundle, expected=first, reason="stale write", evidence=[]) + + viewer = open_knowledge_store(store_root=root) + assert viewer.history(identity) == [ + {"revision": first, "parent": None, "reason": "create", "evidence": []}, + {"revision": second, "parent": first, "reason": "clarify title", "evidence": []}, + ] + with pytest.raises(ValueError, match="read-only"): + viewer.update(bundle, expected=second, reason="read-only write", evidence=[]) + reader = knowledge_reader(store_root=root) + assert read(reader, f"store:{identity}")[1] == second + assert read(reader, f"store:{identity}", revision=first)[1] == first