provalume 0.1.0__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 (66) hide show
  1. provalume/__init__.py +133 -0
  2. provalume/_ids.py +112 -0
  3. provalume/_time.py +91 -0
  4. provalume/cli/__init__.py +1 -0
  5. provalume/cli/main.py +817 -0
  6. provalume/cli/theme.py +108 -0
  7. provalume/demo/__init__.py +1 -0
  8. provalume/demo/scenario.py +385 -0
  9. provalume/errors.py +119 -0
  10. provalume/evals/__init__.py +1 -0
  11. provalume/evals/metrics.py +220 -0
  12. provalume/evals/replay.py +752 -0
  13. provalume/integrations/__init__.py +1 -0
  14. provalume/integrations/generic.py +220 -0
  15. provalume/integrations/orkestra.py +411 -0
  16. provalume/interchange/__init__.py +1 -0
  17. provalume/interchange/hashing.py +179 -0
  18. provalume/interchange/jsonl.py +641 -0
  19. provalume/interchange/signatures.py +224 -0
  20. provalume/mcp/__init__.py +1 -0
  21. provalume/mcp/permissions.py +224 -0
  22. provalume/mcp/server.py +728 -0
  23. provalume/policy/__init__.py +1 -0
  24. provalume/policy/admission.py +218 -0
  25. provalume/policy/invalidation.py +207 -0
  26. provalume/policy/poisoning.py +274 -0
  27. provalume/policy/promotion.py +540 -0
  28. provalume/policy/scope.py +131 -0
  29. provalume/py.typed +0 -0
  30. provalume/redact.py +299 -0
  31. provalume/retrieval/__init__.py +1 -0
  32. provalume/retrieval/digest.py +267 -0
  33. provalume/retrieval/lexical.py +467 -0
  34. provalume/retrieval/preflight.py +341 -0
  35. provalume/retrieval/ranking.py +275 -0
  36. provalume/retrieval/vectors.py +359 -0
  37. provalume/schemas/__init__.py +109 -0
  38. provalume/schemas/events.py +288 -0
  39. provalume/schemas/memories.py +327 -0
  40. provalume/schemas/provenance.py +197 -0
  41. provalume/schemas/retrieval.py +336 -0
  42. provalume/schemas/scope.py +177 -0
  43. provalume/schemas/trust.py +226 -0
  44. provalume/sdk/__init__.py +1 -0
  45. provalume/sdk/client.py +706 -0
  46. provalume/store/__init__.py +1 -0
  47. provalume/store/db.py +284 -0
  48. provalume/store/fts.py +148 -0
  49. provalume/store/gitinfo.py +261 -0
  50. provalume/store/integrity.py +380 -0
  51. provalume/store/journal.py +414 -0
  52. provalume/store/migrations.py +313 -0
  53. provalume/store/projections.py +987 -0
  54. provalume/store/repository.py +596 -0
  55. provalume/writers/__init__.py +1 -0
  56. provalume/writers/decisions.py +112 -0
  57. provalume/writers/failures.py +424 -0
  58. provalume/writers/reviews.py +184 -0
  59. provalume/writers/runs.py +256 -0
  60. provalume/writers/verification.py +234 -0
  61. provalume-0.1.0.dist-info/METADATA +273 -0
  62. provalume-0.1.0.dist-info/RECORD +66 -0
  63. provalume-0.1.0.dist-info/WHEEL +4 -0
  64. provalume-0.1.0.dist-info/entry_points.txt +2 -0
  65. provalume-0.1.0.dist-info/licenses/LICENSE +202 -0
  66. provalume-0.1.0.dist-info/licenses/NOTICE +70 -0
provalume/__init__.py ADDED
@@ -0,0 +1,133 @@
1
+ """Provalume — verified, git-aware memory for autonomous software agents.
2
+
3
+ Facts your agents proved, not things they said.
4
+
5
+ Provalume records what agents did and which evidence proved it, then re-injects a
6
+ bounded, labelled digest of that record into future prompts. Trust is granted by
7
+ deterministic evidence — a command that returned, a reviewer who was not the
8
+ author, a commit that landed — never by assertion.
9
+
10
+ from provalume import Provalume
11
+
12
+ pv = Provalume.open(project_id="my-project")
13
+
14
+ pv.record_verification(
15
+ command="pytest -n auto tests/integration",
16
+ passed=False,
17
+ excerpt="deadlock in db fixture teardown",
18
+ )
19
+
20
+ warning = pv.preflight(command="pytest -n auto tests/integration")
21
+ if warning.matched:
22
+ print(warning.summary)
23
+
24
+ digest = pv.recall("integration tests").digest(char_budget=2000)
25
+ print(digest.text) # always banner-first, always within budget
26
+
27
+ What this module re-exports is the public API, stable within a minor release
28
+ series. Everything else — ``provalume.store``, ``provalume.policy``,
29
+ ``provalume.writers``, ``provalume.retrieval``, ``provalume.interchange``, and
30
+ the ``provalume.mcp`` internals — is internal and may change without notice
31
+ (ADR-0017).
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ from provalume.errors import (
37
+ BudgetExceeded,
38
+ EmbedderUnavailable,
39
+ IntegrityError,
40
+ InterchangeError,
41
+ PathConfinementError,
42
+ PolicyViolation,
43
+ ProvalumeError,
44
+ SchemaVersionError,
45
+ ScopeError,
46
+ SignatureError,
47
+ StoreError,
48
+ TrustError,
49
+ UnknownRecordVersion,
50
+ ValidationError,
51
+ )
52
+ from provalume.schemas import (
53
+ DIGEST_BANNER,
54
+ Applicability,
55
+ Digest,
56
+ Event,
57
+ EventFilter,
58
+ EventType,
59
+ Explanation,
60
+ IntegrationState,
61
+ Memory,
62
+ MemoryFilter,
63
+ MemoryType,
64
+ PreflightMatch,
65
+ PreflightResult,
66
+ Provenance,
67
+ RankingPolicy,
68
+ RecallQuery,
69
+ RecallResult,
70
+ ResolutionStatus,
71
+ ReviewState,
72
+ Scope,
73
+ ScopeLevel,
74
+ ScoreBreakdown,
75
+ Source,
76
+ Transition,
77
+ TrustState,
78
+ VerificationState,
79
+ )
80
+ from provalume.sdk.client import Provalume, RecallResponse
81
+
82
+ try: # pragma: no cover - depends on install state, not on logic
83
+ from importlib.metadata import version
84
+
85
+ __version__ = version("provalume")
86
+ except Exception: # pragma: no cover
87
+ __version__ = "0.0.0+unknown"
88
+
89
+ __all__ = [
90
+ "DIGEST_BANNER",
91
+ "Applicability",
92
+ "BudgetExceeded",
93
+ "Digest",
94
+ "EmbedderUnavailable",
95
+ "Event",
96
+ "EventFilter",
97
+ "EventType",
98
+ "Explanation",
99
+ "IntegrationState",
100
+ "IntegrityError",
101
+ "InterchangeError",
102
+ "Memory",
103
+ "MemoryFilter",
104
+ "MemoryType",
105
+ "PathConfinementError",
106
+ "PolicyViolation",
107
+ "PreflightMatch",
108
+ "PreflightResult",
109
+ "Provalume",
110
+ "ProvalumeError",
111
+ "Provenance",
112
+ "RankingPolicy",
113
+ "RecallQuery",
114
+ "RecallResponse",
115
+ "RecallResult",
116
+ "ResolutionStatus",
117
+ "ReviewState",
118
+ "SchemaVersionError",
119
+ "Scope",
120
+ "ScopeError",
121
+ "ScopeLevel",
122
+ "ScoreBreakdown",
123
+ "SignatureError",
124
+ "Source",
125
+ "StoreError",
126
+ "Transition",
127
+ "TrustError",
128
+ "TrustState",
129
+ "UnknownRecordVersion",
130
+ "ValidationError",
131
+ "VerificationState",
132
+ "__version__",
133
+ ]
provalume/_ids.py ADDED
@@ -0,0 +1,112 @@
1
+ """Collision-resistant, time-sortable identifiers.
2
+
3
+ Provalume needs identifiers that are:
4
+
5
+ * **time-sortable**, so JSONL exports append near the end of the file and Git
6
+ diffs stay append-mostly rather than churning (ADR-0011);
7
+ * **collision-resistant across machines**, because two people can record events
8
+ offline and later merge without coordination (ADR-0002);
9
+ * **URL- and filename-safe**, and readable enough to paste into a bug report.
10
+
11
+ This is a ULID: 48 bits of millisecond timestamp followed by 80 bits of
12
+ randomness, Crockford base32 encoded to 26 characters. The implementation is
13
+ about forty lines, so it is here rather than in a dependency.
14
+
15
+ The ULID is the *identity* of a record. It is deliberately not derived from
16
+ content: two genuinely distinct events with identical payloads (the same command
17
+ failing twice in the same millisecond) must not collide. Content-based dedup is a
18
+ separate concern handled by ``payload_hash`` in :mod:`provalume.interchange.hashing`.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import os
24
+ import time
25
+
26
+ # Crockford base32: no I, L, O, or U, so a transcribed identifier is hard to
27
+ # garble and impossible to turn into an accidental word.
28
+ _ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
29
+ _DECODE = {c: i for i, c in enumerate(_ALPHABET)}
30
+
31
+ _TIME_CHARS = 10
32
+ _RANDOM_CHARS = 16
33
+ ULID_LENGTH = _TIME_CHARS + _RANDOM_CHARS
34
+
35
+ _MAX_TIME_MS = (1 << 48) - 1
36
+
37
+
38
+ def _encode(value: int, length: int) -> str:
39
+ chars = []
40
+ for _ in range(length):
41
+ chars.append(_ALPHABET[value & 0x1F])
42
+ value >>= 5
43
+ return "".join(reversed(chars))
44
+
45
+
46
+ def new_id(*, timestamp_ms: int | None = None) -> str:
47
+ """Return a new 26-character ULID.
48
+
49
+ ``timestamp_ms`` is for tests and deterministic replay only; production
50
+ callers leave it unset.
51
+ """
52
+ ts = int(time.time() * 1000) if timestamp_ms is None else timestamp_ms
53
+ if not 0 <= ts <= _MAX_TIME_MS:
54
+ msg = f"timestamp out of ULID range: {ts}"
55
+ raise ValueError(msg)
56
+ randomness = int.from_bytes(os.urandom(10), "big")
57
+ return _encode(ts, _TIME_CHARS) + _encode(randomness, _RANDOM_CHARS)
58
+
59
+
60
+ def is_valid(value: str) -> bool:
61
+ """Return whether ``value`` is a well-formed ULID."""
62
+ if len(value) != ULID_LENGTH:
63
+ return False
64
+ return all(c in _DECODE for c in value)
65
+
66
+
67
+ def timestamp_ms(value: str) -> int:
68
+ """Extract the embedded millisecond timestamp from a ULID."""
69
+ if not is_valid(value):
70
+ msg = f"not a valid identifier: {value!r}"
71
+ raise ValueError(msg)
72
+ result = 0
73
+ for c in value[:_TIME_CHARS]:
74
+ result = (result << 5) | _DECODE[c]
75
+ return result
76
+
77
+
78
+ class MonotonicIdFactory:
79
+ """Generates ULIDs that strictly increase, even within one millisecond.
80
+
81
+ Plain :func:`new_id` sorts correctly across milliseconds but two IDs minted
82
+ in the same millisecond have an arbitrary relative order. Where insertion
83
+ order must be recoverable from the ID alone — journal append being the case
84
+ that matters — this factory increments the random component instead of
85
+ redrawing it, so ordering holds at sub-millisecond resolution.
86
+
87
+ Not thread-safe by design: Provalume is single-writer (ADR-0003).
88
+ """
89
+
90
+ def __init__(self) -> None:
91
+ self._last_ms = -1
92
+ self._last_random = 0
93
+
94
+ def new_id(self, *, timestamp_ms: int | None = None) -> str:
95
+ ts = int(time.time() * 1000) if timestamp_ms is None else timestamp_ms
96
+ if ts == self._last_ms:
97
+ self._last_random += 1
98
+ if self._last_random >= (1 << 80):
99
+ # Overflowing 80 bits inside one millisecond is not reachable in
100
+ # practice; step the clock rather than emit a duplicate.
101
+ ts += 1
102
+ self._last_ms = ts
103
+ self._last_random = int.from_bytes(os.urandom(10), "big")
104
+ else:
105
+ # A clock that moved backwards must not produce descending IDs.
106
+ ts = max(ts, self._last_ms)
107
+ if ts == self._last_ms:
108
+ self._last_random += 1
109
+ else:
110
+ self._last_ms = ts
111
+ self._last_random = int.from_bytes(os.urandom(10), "big")
112
+ return _encode(ts, _TIME_CHARS) + _encode(self._last_random, _RANDOM_CHARS)
provalume/_time.py ADDED
@@ -0,0 +1,91 @@
1
+ """Timestamp handling.
2
+
3
+ One format, everywhere: RFC 3339 in UTC with millisecond precision and a literal
4
+ ``Z`` suffix.
5
+
6
+ 2026-07-25T14:32:01.482Z
7
+
8
+ Fixed precision matters more than it looks. Timestamps are part of the canonical
9
+ JSON that gets hashed (:mod:`provalume.interchange.hashing`), so a value that
10
+ serialises as ``…:01.482Z`` on one machine and ``…:01.482000Z`` on another would
11
+ hash differently for identical data and break cross-machine duplicate detection.
12
+
13
+ Millisecond precision is chosen to match the ULID timestamp component, so an
14
+ event's identifier and its ``recorded_at`` cannot disagree about which
15
+ millisecond it belongs to.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import re
21
+ from datetime import UTC, datetime, timedelta, timezone
22
+
23
+ _RFC3339 = re.compile(
24
+ r"^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))?"
25
+ r"(Z|[+-]\d{2}:\d{2})$"
26
+ )
27
+
28
+
29
+ def now() -> str:
30
+ """Current time as a canonical RFC 3339 UTC string."""
31
+ return to_rfc3339(datetime.now(UTC))
32
+
33
+
34
+ def to_rfc3339(value: datetime) -> str:
35
+ """Format a datetime canonically. Naive input is assumed to be UTC."""
36
+ if value.tzinfo is None:
37
+ value = value.replace(tzinfo=UTC)
38
+ value = value.astimezone(UTC)
39
+ return f"{value.strftime('%Y-%m-%dT%H:%M:%S')}.{value.microsecond // 1000:03d}Z"
40
+
41
+
42
+ def parse(value: str) -> datetime:
43
+ """Parse an RFC 3339 timestamp into an aware UTC datetime.
44
+
45
+ Accepts any valid RFC 3339 offset and any sub-second precision, so records
46
+ produced by other tooling still import; :func:`to_rfc3339` is what normalises
47
+ them on the way to storage.
48
+ """
49
+ match = _RFC3339.match(value)
50
+ if match is None:
51
+ msg = f"not an RFC 3339 timestamp: {value!r}"
52
+ raise ValueError(msg)
53
+ year, month, day, hour, minute, second = (int(match.group(i)) for i in range(1, 7))
54
+ fraction = match.group(7) or ""
55
+ microsecond = int(fraction.ljust(6, "0")[:6]) if fraction else 0
56
+ offset = match.group(8)
57
+ tz: timezone
58
+ if offset == "Z":
59
+ tz = UTC
60
+ else:
61
+ sign = 1 if offset[0] == "+" else -1
62
+ tz_hours, tz_minutes = int(offset[1:3]), int(offset[4:6])
63
+ tz = timezone(timedelta(minutes=sign * (tz_hours * 60 + tz_minutes)))
64
+ return datetime(year, month, day, hour, minute, second, microsecond, tz).astimezone(UTC)
65
+
66
+
67
+ def normalize(value: str) -> str:
68
+ """Round-trip a timestamp into canonical form.
69
+
70
+ Used at admission so that everything stored is byte-identical in format
71
+ regardless of how a caller expressed it.
72
+ """
73
+ return to_rfc3339(parse(value))
74
+
75
+
76
+ def age_days(recorded_at: str, *, reference: str | None = None) -> float:
77
+ """Age in fractional days, used by the recency component of ranking.
78
+
79
+ Never negative: a record whose timestamp is in the future — clock skew, or a
80
+ caller passing a bad ``occurred_at`` — is treated as brand new rather than
81
+ given a recency score above 1.0, which would let a forged future timestamp
82
+ win every ranking.
83
+ """
84
+ then = parse(recorded_at)
85
+ ref = parse(reference) if reference else datetime.now(UTC)
86
+ return max(0.0, (ref - then).total_seconds() / 86400.0)
87
+
88
+
89
+ def is_before(left: str, right: str) -> bool:
90
+ """Whether ``left`` is strictly earlier than ``right``."""
91
+ return parse(left) < parse(right)
@@ -0,0 +1 @@
1
+ """Internal package. Not public API (ADR-0017)."""