continuity-plane 0.1.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. continuity_plane/__init__.py +21 -0
  2. continuity_plane/artifact_store.py +318 -0
  3. continuity_plane/assertion_provenance.py +418 -0
  4. continuity_plane/authorization_audit.py +807 -0
  5. continuity_plane/checkpoint.py +532 -0
  6. continuity_plane/claim_evidence_gate.py +419 -0
  7. continuity_plane/cli.py +281 -0
  8. continuity_plane/codegraph_verification.py +700 -0
  9. continuity_plane/collaboration_notifications.py +1262 -0
  10. continuity_plane/compiled_skill_packet.py +318 -0
  11. continuity_plane/database/migrations/001_postgres_state.up.sql +62 -0
  12. continuity_plane/decision_evidence_projection.py +1003 -0
  13. continuity_plane/durable_continuation.py +625 -0
  14. continuity_plane/durable_operation.py +651 -0
  15. continuity_plane/durable_state_migration.py +309 -0
  16. continuity_plane/effect_scope_gate.py +377 -0
  17. continuity_plane/execution_packet.py +586 -0
  18. continuity_plane/experiment_lifecycle.py +157 -0
  19. continuity_plane/external_state_provider.py +532 -0
  20. continuity_plane/human_governance.py +790 -0
  21. continuity_plane/idea_continuity.py +110 -0
  22. continuity_plane/idea_review.py +794 -0
  23. continuity_plane/obsidian_vault.py +490 -0
  24. continuity_plane/postgres_state_store.py +365 -0
  25. continuity_plane/project_graph_projection.py +728 -0
  26. continuity_plane/relationship_impact_projection.py +633 -0
  27. continuity_plane/retrieval_routing.py +769 -0
  28. continuity_plane/sanitizer.py +136 -0
  29. continuity_plane/schema_governance.py +238 -0
  30. continuity_plane/schemas/m4-01/spdx-license-ids-3.28.0.json +737 -0
  31. continuity_plane/shared_state_mcp.py +1092 -0
  32. continuity_plane/shared_state_migration.py +511 -0
  33. continuity_plane/shared_work_ledger.py +1068 -0
  34. continuity_plane/skill_catalog.py +360 -0
  35. continuity_plane/skill_compatibility.py +703 -0
  36. continuity_plane/skill_manifest_set.py +549 -0
  37. continuity_plane/skill_resolver.py +721 -0
  38. continuity_plane/source_registry.py +359 -0
  39. continuity_plane/sqlite_state_store.py +1645 -0
  40. continuity_plane/sqlite_work_ledger.py +456 -0
  41. continuity_plane/state_events.py +1311 -0
  42. continuity_plane/state_mcp.py +3576 -0
  43. continuity_plane/state_store.py +266 -0
  44. continuity_plane/templates/MASTER.en.md +31 -0
  45. continuity_plane/templates/MASTER.md +28 -0
  46. continuity_plane/templates/STATUS.en.md +19 -0
  47. continuity_plane/templates/STATUS.md +17 -0
  48. continuity_plane/templates/__init__.py +1 -0
  49. continuity_plane/templates/project.yaml +16 -0
  50. continuity_plane/typed_state.py +1523 -0
  51. continuity_plane/verification_profile.py +1042 -0
  52. continuity_plane-0.1.0a1.dist-info/METADATA +156 -0
  53. continuity_plane-0.1.0a1.dist-info/RECORD +59 -0
  54. continuity_plane-0.1.0a1.dist-info/WHEEL +5 -0
  55. continuity_plane-0.1.0a1.dist-info/entry_points.txt +2 -0
  56. continuity_plane-0.1.0a1.dist-info/licenses/LICENSE +202 -0
  57. continuity_plane-0.1.0a1.dist-info/licenses/NOTICE +5 -0
  58. continuity_plane-0.1.0a1.dist-info/licenses/THIRD_PARTY_NOTICES.md +30 -0
  59. continuity_plane-0.1.0a1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,21 @@
1
+ """Provider-neutral context control-plane primitives."""
2
+
3
+ from .source_registry import (
4
+ ProvenanceError,
5
+ SourceRegistry,
6
+ build_provenance,
7
+ validate_provenance,
8
+ )
9
+ from .sanitizer import SanitizationError, SanitizationFinding, SanitizationResult, sanitize_text, validate_admission
10
+
11
+ __all__ = [
12
+ "ProvenanceError",
13
+ "SourceRegistry",
14
+ "build_provenance",
15
+ "validate_provenance",
16
+ "SanitizationError",
17
+ "SanitizationFinding",
18
+ "SanitizationResult",
19
+ "sanitize_text",
20
+ "validate_admission",
21
+ ]
@@ -0,0 +1,318 @@
1
+ """Local content-addressed artifact storage for bounded evidence access."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import os
8
+ import re
9
+ import stat
10
+ import tempfile
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+ from typing import Any, BinaryIO
14
+
15
+
16
+ ARTIFACT_REF_SCHEMA_VERSION = "context.artifact-ref/v1alpha1"
17
+ ARTIFACT_DIGEST_ALGORITHM = "sha-256"
18
+ _DIGEST_RE = re.compile(r"^[0-9a-f]{64}$")
19
+ _URI_RE = re.compile(r"^artifact://sha256/([0-9a-f]{64})$")
20
+ _REF_FIELDS = frozenset(
21
+ {"schema_version", "digest_algorithm", "digest", "size_bytes", "artifact_uri"}
22
+ )
23
+
24
+
25
+ class ArtifactStoreError(RuntimeError):
26
+ """Base error for local artifact operations."""
27
+
28
+
29
+ class ArtifactInputError(ArtifactStoreError):
30
+ """Raised when an artifact source cannot be read as bytes."""
31
+
32
+
33
+ class ArtifactIntegrityError(ArtifactStoreError):
34
+ """Raised when a durable object is missing, changed, or malformed."""
35
+
36
+
37
+ class ArtifactRangeError(ArtifactStoreError):
38
+ """Raised when a requested byte range is outside the artifact or bound."""
39
+
40
+
41
+ @dataclass(frozen=True, slots=True)
42
+ class ArtifactRef:
43
+ """Stable identity and size of one raw byte artifact."""
44
+
45
+ digest: str
46
+ size_bytes: int
47
+ schema_version: str = ARTIFACT_REF_SCHEMA_VERSION
48
+ digest_algorithm: str = ARTIFACT_DIGEST_ALGORITHM
49
+
50
+ def __post_init__(self) -> None:
51
+ if self.schema_version != ARTIFACT_REF_SCHEMA_VERSION:
52
+ raise ValueError("unsupported artifact ref schema_version")
53
+ if self.digest_algorithm != ARTIFACT_DIGEST_ALGORITHM:
54
+ raise ValueError("unsupported artifact digest_algorithm")
55
+ if not isinstance(self.digest, str) or not _DIGEST_RE.fullmatch(self.digest):
56
+ raise ValueError("artifact digest must be lowercase SHA-256")
57
+ if type(self.size_bytes) is not int or self.size_bytes < 0:
58
+ raise ValueError("artifact size_bytes must be a non-negative integer")
59
+
60
+ @property
61
+ def uri(self) -> str:
62
+ return f"artifact://sha256/{self.digest}"
63
+
64
+ def to_document(self) -> dict[str, Any]:
65
+ return {
66
+ "schema_version": self.schema_version,
67
+ "digest_algorithm": self.digest_algorithm,
68
+ "digest": self.digest,
69
+ "size_bytes": self.size_bytes,
70
+ "artifact_uri": self.uri,
71
+ }
72
+
73
+ @classmethod
74
+ def from_document(cls, document: dict[str, Any]) -> "ArtifactRef":
75
+ if not isinstance(document, dict) or frozenset(document) != _REF_FIELDS:
76
+ raise ValueError("artifact ref document fields are invalid")
77
+ ref = cls(
78
+ digest=document["digest"],
79
+ size_bytes=document["size_bytes"],
80
+ schema_version=document["schema_version"],
81
+ digest_algorithm=document["digest_algorithm"],
82
+ )
83
+ if document["artifact_uri"] != ref.uri:
84
+ raise ValueError("artifact ref URI does not match digest")
85
+ return ref
86
+
87
+ @classmethod
88
+ def from_uri(cls, uri: str, size_bytes: int) -> "ArtifactRef":
89
+ if not isinstance(uri, str):
90
+ raise ValueError("artifact URI must be a string")
91
+ match = _URI_RE.fullmatch(uri)
92
+ if match is None:
93
+ raise ValueError("artifact URI is invalid")
94
+ return cls(digest=match.group(1), size_bytes=size_bytes)
95
+
96
+
97
+ def _canonical_json(value: dict[str, Any]) -> str:
98
+ return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
99
+
100
+
101
+ def _is_regular(path: Path) -> bool:
102
+ try:
103
+ return stat.S_ISREG(path.stat(follow_symlinks=False).st_mode)
104
+ except FileNotFoundError:
105
+ return False
106
+
107
+
108
+ def _fsync_directory(path: Path) -> None:
109
+ """Persist directory entry updates where the host supports directory fsync."""
110
+ flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)
111
+ try:
112
+ descriptor = os.open(path, flags)
113
+ except OSError:
114
+ return
115
+ try:
116
+ os.fsync(descriptor)
117
+ except OSError:
118
+ pass
119
+ finally:
120
+ os.close(descriptor)
121
+
122
+
123
+ class LocalArtifactStore:
124
+ """Zero-service filesystem CAS with full-object integrity verification."""
125
+
126
+ def __init__(
127
+ self,
128
+ root: str | Path,
129
+ *,
130
+ chunk_size: int = 1024 * 1024,
131
+ max_range_bytes: int = 1024 * 1024,
132
+ ):
133
+ if type(chunk_size) is not int or chunk_size <= 0:
134
+ raise ValueError("chunk_size must be a positive integer")
135
+ if type(max_range_bytes) is not int or max_range_bytes <= 0:
136
+ raise ValueError("max_range_bytes must be a positive integer")
137
+ self.root = Path(root)
138
+ self.chunk_size = chunk_size
139
+ self.max_range_bytes = max_range_bytes
140
+
141
+ def initialize(self) -> None:
142
+ if self.root.is_symlink():
143
+ raise ArtifactIntegrityError("artifact root must not be a symlink")
144
+ if self.root.exists() and not self.root.is_dir():
145
+ raise ArtifactIntegrityError("artifact root is not a directory")
146
+ self.root.mkdir(parents=True, exist_ok=True, mode=0o700)
147
+ objects = self.root / "objects"
148
+ algorithm = objects / "sha256"
149
+ temporary = self.root / "tmp"
150
+ for directory in (self.root, objects, algorithm, temporary):
151
+ directory.mkdir(parents=True, exist_ok=True, mode=0o700)
152
+ if directory.is_symlink() or not directory.is_dir():
153
+ raise ArtifactIntegrityError("artifact store directory is invalid")
154
+ try:
155
+ directory.chmod(0o700)
156
+ except OSError:
157
+ pass
158
+
159
+ def _ensure_initialized(self) -> None:
160
+ expected = (self.root, self.root / "objects", self.root / "objects" / "sha256", self.root / "tmp")
161
+ if any(not path.is_dir() or path.is_symlink() for path in expected):
162
+ raise ArtifactStoreError("artifact store is not initialized")
163
+
164
+ def object_path(self, ref: ArtifactRef) -> Path:
165
+ return self._object_path(ref, create_shard=True)
166
+
167
+ def _object_path(self, ref: ArtifactRef, *, create_shard: bool) -> Path:
168
+ if not isinstance(ref, ArtifactRef):
169
+ raise TypeError("object_path requires ArtifactRef")
170
+ self._ensure_initialized()
171
+ shard = self.root / "objects" / "sha256" / ref.digest[:2]
172
+ if shard.is_symlink() or (shard.exists() and not shard.is_dir()):
173
+ raise ArtifactIntegrityError("artifact shard directory is invalid")
174
+ if create_shard:
175
+ shard.mkdir(mode=0o700, exist_ok=True)
176
+ return shard / ref.digest[2:]
177
+
178
+ def put_bytes(self, payload: bytes | bytearray | memoryview) -> ArtifactRef:
179
+ if not isinstance(payload, (bytes, bytearray, memoryview)):
180
+ raise ArtifactInputError("artifact payload must be bytes")
181
+ return self.put_stream(BinaryIOBytes(bytes(payload)))
182
+
183
+ def put_stream(self, source: BinaryIO) -> ArtifactRef:
184
+ self._ensure_initialized()
185
+ if not hasattr(source, "read"):
186
+ raise ArtifactInputError("artifact source must provide read()")
187
+ descriptor, temporary_name = tempfile.mkstemp(
188
+ prefix=".artifact-", suffix=".tmp", dir=self.root / "tmp"
189
+ )
190
+ temporary = Path(temporary_name)
191
+ digest = hashlib.sha256()
192
+ size = 0
193
+ try:
194
+ with os.fdopen(descriptor, "wb") as destination:
195
+ while True:
196
+ try:
197
+ chunk = source.read(self.chunk_size)
198
+ except Exception as exc:
199
+ raise ArtifactInputError("artifact source read failed") from exc
200
+ if not isinstance(chunk, (bytes, bytearray, memoryview)):
201
+ raise ArtifactInputError("artifact source must return bytes")
202
+ chunk = bytes(chunk)
203
+ if not chunk:
204
+ break
205
+ digest.update(chunk)
206
+ size += len(chunk)
207
+ destination.write(chunk)
208
+ destination.flush()
209
+ os.fsync(destination.fileno())
210
+
211
+ ref = ArtifactRef(digest=digest.hexdigest(), size_bytes=size)
212
+ target = self.object_path(ref)
213
+ if target.exists() or target.is_symlink():
214
+ self._verify_path(target, ref)
215
+ return ref
216
+ try:
217
+ os.link(temporary, target)
218
+ temporary.unlink(missing_ok=True)
219
+ except FileExistsError:
220
+ self._verify_path(target, ref)
221
+ temporary.unlink(missing_ok=True)
222
+ except (NotImplementedError, OSError):
223
+ if target.exists() or target.is_symlink():
224
+ self._verify_path(target, ref)
225
+ temporary.unlink(missing_ok=True)
226
+ else:
227
+ os.replace(temporary, target)
228
+ try:
229
+ target.chmod(0o600)
230
+ except OSError:
231
+ pass
232
+ _fsync_directory(target.parent)
233
+ return ref
234
+ except ArtifactStoreError:
235
+ temporary.unlink(missing_ok=True)
236
+ raise
237
+ except (OSError, ValueError) as exc:
238
+ temporary.unlink(missing_ok=True)
239
+ raise ArtifactStoreError("artifact publication failed") from exc
240
+ finally:
241
+ temporary.unlink(missing_ok=True)
242
+
243
+ def _verify_path(self, path: Path, ref: ArtifactRef) -> None:
244
+ if not path.exists() and not path.is_symlink():
245
+ raise ArtifactIntegrityError("artifact object is missing")
246
+ if path.is_symlink() or not _is_regular(path):
247
+ raise ArtifactIntegrityError("artifact object is not a regular file")
248
+ digest = hashlib.sha256()
249
+ size = 0
250
+ try:
251
+ with path.open("rb") as source:
252
+ while chunk := source.read(self.chunk_size):
253
+ digest.update(chunk)
254
+ size += len(chunk)
255
+ except OSError as exc:
256
+ raise ArtifactIntegrityError("artifact object cannot be read") from exc
257
+ if size != ref.size_bytes or digest.hexdigest() != ref.digest:
258
+ raise ArtifactIntegrityError("artifact object checksum or size mismatch")
259
+
260
+ def verify(self, ref: ArtifactRef) -> ArtifactRef:
261
+ self._verify_path(self._object_path(ref, create_shard=False), ref)
262
+ return ref
263
+
264
+ def read(self, ref: ArtifactRef) -> bytes:
265
+ return self._read_verified(ref, offset=0, length=ref.size_bytes)
266
+
267
+ def read_range(self, ref: ArtifactRef, *, offset: int, length: int) -> bytes:
268
+ if type(offset) is not int or offset < 0:
269
+ raise ArtifactRangeError("artifact range offset must be non-negative")
270
+ if type(length) is not int or length < 0:
271
+ raise ArtifactRangeError("artifact range length must be non-negative")
272
+ if length > self.max_range_bytes:
273
+ raise ArtifactRangeError("artifact range exceeds configured bound")
274
+ if offset > ref.size_bytes or offset + length > ref.size_bytes:
275
+ raise ArtifactRangeError("artifact range exceeds artifact bounds")
276
+ return self._read_verified(ref, offset=offset, length=length)
277
+
278
+ def _read_verified(self, ref: ArtifactRef, *, offset: int, length: int) -> bytes:
279
+ path = self._object_path(ref, create_shard=False)
280
+ if path.is_symlink() or not path.exists():
281
+ raise ArtifactIntegrityError("artifact object is missing or is a symlink")
282
+ if not _is_regular(path):
283
+ raise ArtifactIntegrityError("artifact object is not a regular file")
284
+ digest = hashlib.sha256()
285
+ size = 0
286
+ output = bytearray()
287
+ end = offset + length
288
+ try:
289
+ with path.open("rb") as source:
290
+ while chunk := source.read(self.chunk_size):
291
+ chunk_start = size
292
+ size += len(chunk)
293
+ digest.update(chunk)
294
+ chunk_end = size
295
+ overlap_start = max(offset, chunk_start)
296
+ overlap_end = min(end, chunk_end)
297
+ if overlap_start < overlap_end:
298
+ output.extend(chunk[overlap_start - chunk_start : overlap_end - chunk_start])
299
+ except OSError as exc:
300
+ raise ArtifactIntegrityError("artifact object cannot be read") from exc
301
+ if size != ref.size_bytes or digest.hexdigest() != ref.digest:
302
+ raise ArtifactIntegrityError("artifact object checksum or size mismatch")
303
+ return bytes(output)
304
+
305
+
306
+ class BinaryIOBytes:
307
+ """Small file-like adapter that keeps put_bytes on the streaming path."""
308
+
309
+ def __init__(self, payload: bytes):
310
+ self._payload = payload
311
+ self._offset = 0
312
+
313
+ def read(self, size: int = -1) -> bytes:
314
+ if size < 0:
315
+ size = len(self._payload) - self._offset
316
+ start = self._offset
317
+ self._offset = min(len(self._payload), self._offset + size)
318
+ return self._payload[start : self._offset]