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.
- provalume/__init__.py +133 -0
- provalume/_ids.py +112 -0
- provalume/_time.py +91 -0
- provalume/cli/__init__.py +1 -0
- provalume/cli/main.py +817 -0
- provalume/cli/theme.py +108 -0
- provalume/demo/__init__.py +1 -0
- provalume/demo/scenario.py +385 -0
- provalume/errors.py +119 -0
- provalume/evals/__init__.py +1 -0
- provalume/evals/metrics.py +220 -0
- provalume/evals/replay.py +752 -0
- provalume/integrations/__init__.py +1 -0
- provalume/integrations/generic.py +220 -0
- provalume/integrations/orkestra.py +411 -0
- provalume/interchange/__init__.py +1 -0
- provalume/interchange/hashing.py +179 -0
- provalume/interchange/jsonl.py +641 -0
- provalume/interchange/signatures.py +224 -0
- provalume/mcp/__init__.py +1 -0
- provalume/mcp/permissions.py +224 -0
- provalume/mcp/server.py +728 -0
- provalume/policy/__init__.py +1 -0
- provalume/policy/admission.py +218 -0
- provalume/policy/invalidation.py +207 -0
- provalume/policy/poisoning.py +274 -0
- provalume/policy/promotion.py +540 -0
- provalume/policy/scope.py +131 -0
- provalume/py.typed +0 -0
- provalume/redact.py +299 -0
- provalume/retrieval/__init__.py +1 -0
- provalume/retrieval/digest.py +267 -0
- provalume/retrieval/lexical.py +467 -0
- provalume/retrieval/preflight.py +341 -0
- provalume/retrieval/ranking.py +275 -0
- provalume/retrieval/vectors.py +359 -0
- provalume/schemas/__init__.py +109 -0
- provalume/schemas/events.py +288 -0
- provalume/schemas/memories.py +327 -0
- provalume/schemas/provenance.py +197 -0
- provalume/schemas/retrieval.py +336 -0
- provalume/schemas/scope.py +177 -0
- provalume/schemas/trust.py +226 -0
- provalume/sdk/__init__.py +1 -0
- provalume/sdk/client.py +706 -0
- provalume/store/__init__.py +1 -0
- provalume/store/db.py +284 -0
- provalume/store/fts.py +148 -0
- provalume/store/gitinfo.py +261 -0
- provalume/store/integrity.py +380 -0
- provalume/store/journal.py +414 -0
- provalume/store/migrations.py +313 -0
- provalume/store/projections.py +987 -0
- provalume/store/repository.py +596 -0
- provalume/writers/__init__.py +1 -0
- provalume/writers/decisions.py +112 -0
- provalume/writers/failures.py +424 -0
- provalume/writers/reviews.py +184 -0
- provalume/writers/runs.py +256 -0
- provalume/writers/verification.py +234 -0
- provalume-0.1.0.dist-info/METADATA +273 -0
- provalume-0.1.0.dist-info/RECORD +66 -0
- provalume-0.1.0.dist-info/WHEEL +4 -0
- provalume-0.1.0.dist-info/entry_points.txt +2 -0
- provalume-0.1.0.dist-info/licenses/LICENSE +202 -0
- 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)."""
|