Skip to content

Commit 87024ee

Browse files
feat: v0.6.0 — fix freshness, add get_content/replace, persist adversarial+provenance (#3)
Fixes 7 gaps from Sitecast integration audit: - CRITICAL-3: Freshness decay now computed (was hardcoded 1.0) - CRITICAL-6: adversarial_status persisted in storage (was RAM-only) - CRITICAL-7: provenance records persisted in storage (was RAM-only) - CRITICAL-9: Layer search_boost applied in ranking - HIGH-1: vault.get_content(resource_id) retrieves full text - HIGH-2: vault.replace(resource_id, content) atomic replacement - HIGH-4: SearchResult includes updated_at, resource_type, data_classification - README badges corrected (removed undelivered claims) 448 tests passing. Lint clean. Build verified on Python 3.12.
1 parent efedfb8 commit 87024ee

File tree

10 files changed

+216
-21
lines changed

10 files changed

+216
-21
lines changed

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.6.0] - 2026-04-06
11+
12+
### Added
13+
- `vault.get_content(resource_id)` — retrieve full text content (reassembles chunks)
14+
- `vault.replace(resource_id, new_content)` — atomic content replacement with auto-supersession
15+
- `vault.get_provenance(resource_id)` — retrieve provenance records for a resource
16+
- `vault.set_adversarial_status(resource_id, status)` — persist adversarial verification status
17+
- `adversarial_status` column in storage schemas (persisted, was RAM-only)
18+
- `provenance` table in storage schemas (persisted, was RAM-only)
19+
- `updated_at`, `resource_type`, `data_classification` fields on `SearchResult` model
20+
- Layer `search_boost` applied in ranking (OPERATIONAL 1.5x, STRATEGIC 1.0x)
21+
22+
### Fixed
23+
- **Freshness decay**: was hardcoded to 1.0, now computed from `updated_at` with per-tier half-life
24+
- **Layer search_boost**: defined per layer but never applied in `apply_trust_weighting()`
25+
26+
### Changed
27+
- README badges corrected: removed undelivered encryption/FIPS claims, fixed test count
28+
- Encryption (`[encryption]`) and docling (`[docling]`) extras marked as "planned v0.8"
29+
1030
## [0.5.0] - 2026-04-06
1131

1232
### Added

README.md

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,12 @@
44

55
**The governed knowledge store for autonomous organizations.**
66

7-
Every document has a trust tier that weights search results. Every chunk has a SHA3-256 content ID. Every mutation is auditable. The entire vault is verifiable via Merkle tree. Air-gap native. Post-quantum ready.
7+
Every document has a trust tier that weights search results. Every chunk has a SHA3-256 content ID. Every mutation is auditable. The entire vault is verifiable via Merkle tree. Air-gap native.
88

99
[![Python](https://img.shields.io/badge/Python-3.12+-3776AB.svg)](https://www.python.org/)
1010
[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
11-
[![Tests](https://img.shields.io/badge/Tests-375_passing-brightgreen.svg)](tests/)
12-
[![Security](https://img.shields.io/badge/Security-100%2F100-brightgreen.svg)](docs/security.md)
13-
[![FIPS](https://img.shields.io/badge/Crypto-SHA3--256%20%C2%B7%20Ed25519%20%C2%B7%20ML--KEM--768-purple.svg)](#security)
11+
[![Tests](https://img.shields.io/badge/Tests-448_passing-brightgreen.svg)](tests/)
12+
[![Crypto](https://img.shields.io/badge/Crypto-SHA3--256%20%C2%B7%20Ed25519-purple.svg)](#security)
1413

1514
</div>
1615

@@ -138,8 +137,8 @@ pip install qp-vault
138137
| `pip install qp-vault` | SQLite, trust search, CAS, Merkle, lifecycle | **1** (pydantic) |
139138
| `pip install qp-vault[postgres]` | + PostgreSQL + pgvector hybrid search | + sqlalchemy, asyncpg, pgvector |
140139
| `pip install qp-vault[capsule]` | + Cryptographic audit trail | + [qp-capsule](https://github.com/quantumpipes/capsule) |
141-
| `pip install qp-vault[docling]` | + 25+ format document processing | + docling |
142-
| `pip install qp-vault[encryption]` | + AES-256-GCM + ML-KEM-768 at rest | + cryptography, pynacl |
140+
| `pip install qp-vault[docling]` | + 25+ format document processing (planned v0.8) | + docling |
141+
| `pip install qp-vault[encryption]` | + AES-256-GCM encryption at rest (planned v0.8) | + cryptography, pynacl |
143142
| `pip install qp-vault[fastapi]` | + REST API (15+ endpoints) | + fastapi |
144143
| `pip install qp-vault[cli]` | + `vault` command-line tool | + typer, rich |
145144
| `pip install qp-vault[all]` | Everything | All of the above |
@@ -247,7 +246,7 @@ app.include_router(router, prefix="/v1/vault")
247246
|---|---|---|---|
248247
| Content integrity | SHA3-256 | FIPS 202 | Tamper-evident CIDs and Merkle roots |
249248
| Audit signatures | Ed25519 + ML-DSA-65 | FIPS 186-5, FIPS 204 | Via [qp-capsule](https://github.com/quantumpipes/capsule) (optional) |
250-
| Encryption at rest | AES-256-GCM + ML-KEM-768 | FIPS 197, FIPS 203 | Post-quantum key exchange (optional) |
249+
| Encryption at rest | AES-256-GCM (planned v0.8) | FIPS 197 | Post-quantum key exchange (planned) |
251250
| Search integrity | Parameterized SQL | -- | No string interpolation, FTS5 sanitized |
252251
| Input validation | Pydantic + custom | -- | Enum checks, name/tag/metadata limits |
253252

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "qp-vault"
7-
version = "0.5.0"
7+
version = "0.6.0"
88
description = "Governed knowledge store for autonomous organizations. Trust tiers, cryptographic audit trails, content-addressed storage, air-gap native."
99
readme = "README.md"
1010
license = "Apache-2.0"

src/qp_vault/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
Docs: https://github.com/quantumpipes/vault
2727
"""
2828

29-
__version__ = "0.5.0"
29+
__version__ = "0.6.0"
3030
__author__ = "Quantum Pipes Technologies, LLC"
3131
__license__ = "Apache-2.0"
3232

src/qp_vault/core/layer_manager.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ async def search(
182182
query,
183183
top_k=top_k,
184184
layer=self._layer,
185+
_layer_boost=self._layer_config.search_boost,
185186
**kwargs,
186187
)
187188

src/qp_vault/core/search_engine.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -157,10 +157,12 @@ def compute_freshness(
157157
def apply_trust_weighting(
158158
results: list[SearchResult],
159159
config: VaultConfig | None = None,
160+
*,
161+
layer_boost: float = 1.0,
160162
) -> list[SearchResult]:
161-
"""Apply 2D trust weights and freshness decay to search results.
163+
"""Apply 2D trust weights, freshness decay, and layer boost to search results.
162164
163-
Computes composite relevance = raw * organizational_trust * adversarial_multiplier * freshness.
165+
Computes composite relevance = raw * organizational_trust * adversarial_multiplier * freshness * layer_boost.
164166
Re-sorts results by composite score (highest first).
165167
166168
Args:
@@ -181,12 +183,13 @@ def apply_trust_weighting(
181183
adv_str = adv_status.value if hasattr(adv_status, "value") else str(adv_status or "unverified")
182184
adv_mult = compute_adversarial_multiplier(adv_str)
183185

184-
# Freshness: we don't have updated_at on SearchResult, use 1.0 for now
185-
freshness = 1.0
186+
# Freshness: compute from resource updated_at timestamp
187+
result_updated = getattr(result, "updated_at", None)
188+
freshness = compute_freshness(result_updated, tier, config) if result_updated else 1.0
186189

187-
# Composite score: raw * organizational_trust * adversarial_verification * freshness
190+
# Composite score: raw * organizational_trust * adversarial_verification * freshness * layer_boost
188191
raw = result.relevance
189-
composite = raw * tw * adv_mult * freshness
192+
composite = raw * tw * adv_mult * freshness * layer_boost
190193

191194
weighted.append(
192195
result.model_copy(

src/qp_vault/models.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,12 @@ class SearchResult(BaseModel):
117117
freshness: float = 1.0
118118
relevance: float = 0.0
119119

120+
# Resource metadata (for ranking and display)
121+
updated_at: str | None = None
122+
created_at: str | None = None
123+
resource_type: str | None = None
124+
data_classification: str | None = None
125+
120126
# Provenance
121127
trust_tier: TrustTier = TrustTier.WORKING
122128
adversarial_status: AdversarialStatus = AdversarialStatus.UNVERIFIED

src/qp_vault/protocols.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class ResourceUpdate:
4141
tags: list[str] | None = None
4242
metadata: dict[str, Any] | None = None
4343
lifecycle: str | None = None
44+
adversarial_status: str | None = None
4445
valid_from: str | None = None
4546
valid_until: str | None = None
4647
supersedes: str | None = None

src/qp_vault/storage/sqlite.py

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
resource_type TEXT NOT NULL DEFAULT 'document',
3434
status TEXT NOT NULL DEFAULT 'pending',
3535
lifecycle TEXT NOT NULL DEFAULT 'active',
36+
adversarial_status TEXT NOT NULL DEFAULT 'unverified',
3637
valid_from TEXT,
3738
valid_until TEXT,
3839
supersedes TEXT,
@@ -93,8 +94,23 @@
9394
CREATE INDEX IF NOT EXISTS idx_resources_collection ON resources(collection_id);
9495
CREATE INDEX IF NOT EXISTS idx_resources_layer ON resources(layer);
9596
CREATE INDEX IF NOT EXISTS idx_resources_hash ON resources(content_hash);
97+
CREATE TABLE IF NOT EXISTS provenance (
98+
id TEXT PRIMARY KEY,
99+
resource_id TEXT NOT NULL,
100+
uploader_id TEXT,
101+
upload_method TEXT,
102+
source_description TEXT DEFAULT '',
103+
original_hash TEXT NOT NULL,
104+
provenance_signature TEXT,
105+
signature_verified INTEGER DEFAULT 0,
106+
created_at TEXT NOT NULL,
107+
FOREIGN KEY (resource_id) REFERENCES resources(id) ON DELETE CASCADE
108+
);
109+
96110
CREATE INDEX IF NOT EXISTS idx_chunks_resource ON chunks(resource_id);
97111
CREATE INDEX IF NOT EXISTS idx_chunks_cid ON chunks(cid);
112+
CREATE INDEX IF NOT EXISTS idx_provenance_resource ON provenance(resource_id);
113+
CREATE INDEX IF NOT EXISTS idx_resources_adversarial ON resources(adversarial_status);
98114
"""
99115

100116
_FTS_SCHEMA = """
@@ -145,6 +161,11 @@ def _cosine_similarity(a: list[float], b: list[float]) -> float:
145161
return dot / (norm_a * norm_b)
146162

147163

164+
def _enum_val(v: Any) -> str:
165+
"""Extract .value from enum, or return str directly."""
166+
return v.value if hasattr(v, "value") else str(v)
167+
168+
148169
def _resource_from_row(row: dict[str, Any]) -> Resource:
149170
"""Convert a SQLite row dict to a Resource model."""
150171
data = dict(row)
@@ -190,14 +211,14 @@ async def store_resource(self, resource: Resource) -> str:
190211
"""INSERT INTO resources (
191212
id, name, content_hash, cid, merkle_root,
192213
trust_tier, data_classification, resource_type,
193-
status, lifecycle, valid_from, valid_until,
214+
status, lifecycle, adversarial_status, valid_from, valid_until,
194215
supersedes, superseded_by, collection_id, layer,
195216
tags, metadata, mime_type, size_bytes, chunk_count,
196217
created_at, updated_at, indexed_at, deleted_at
197218
) VALUES (
198219
?, ?, ?, ?, ?,
199220
?, ?, ?,
200-
?, ?, ?, ?,
221+
?, ?, ?, ?, ?,
201222
?, ?, ?, ?,
202223
?, ?, ?, ?, ?,
203224
?, ?, ?, ?
@@ -213,6 +234,7 @@ async def store_resource(self, resource: Resource) -> str:
213234
resource.resource_type.value if hasattr(resource.resource_type, "value") else resource.resource_type,
214235
resource.status.value if hasattr(resource.status, "value") else resource.status,
215236
resource.lifecycle.value if hasattr(resource.lifecycle, "value") else resource.lifecycle,
237+
_enum_val(getattr(resource, "adversarial_status", "unverified")),
216238
str(resource.valid_from) if resource.valid_from else None,
217239
str(resource.valid_until) if resource.valid_until else None,
218240
resource.supersedes,
@@ -289,7 +311,7 @@ async def update_resource(self, resource_id: str, updates: ResourceUpdate) -> Re
289311

290312
for field_name in (
291313
"name", "trust_tier", "data_classification", "lifecycle",
292-
"valid_from", "valid_until", "supersedes", "superseded_by",
314+
"adversarial_status", "valid_from", "valid_until", "supersedes", "superseded_by",
293315
):
294316
val = getattr(updates, field_name, None)
295317
if val is not None:
@@ -398,7 +420,8 @@ async def search(self, query: SearchQuery) -> list[SearchResult]:
398420
f"SELECT c.rowid as chunk_rowid, c.id as chunk_id, c.resource_id," # nosec B608
399421
f" c.content, c.cid as chunk_cid, c.embedding,"
400422
f" c.page_number, c.section_title, c.chunk_index,"
401-
f" r.name as resource_name, r.trust_tier, r.lifecycle"
423+
f" r.name as resource_name, r.trust_tier, r.lifecycle,"
424+
f" r.updated_at as resource_updated_at, r.resource_type, r.data_classification"
402425
f" FROM chunks c JOIN resources r ON c.resource_id = r.id"
403426
f" WHERE {where_clause} ORDER BY c.chunk_index"
404427
)
@@ -457,6 +480,9 @@ async def search(self, query: SearchQuery) -> list[SearchResult]:
457480
trust_tier=TrustTier(row_dict["trust_tier"]),
458481
cid=row_dict["chunk_cid"],
459482
lifecycle=row_dict["lifecycle"],
483+
updated_at=row_dict.get("resource_updated_at"),
484+
resource_type=row_dict.get("resource_type"),
485+
data_classification=row_dict.get("data_classification"),
460486
relevance=raw_score,
461487
)
462488
)
@@ -500,6 +526,43 @@ async def get_chunks_for_resource(self, resource_id: str) -> list[Chunk]:
500526
result.append(Chunk(**d))
501527
return result
502528

529+
async def store_provenance(
530+
self,
531+
provenance_id: str,
532+
resource_id: str,
533+
uploader_id: str | None,
534+
upload_method: str | None,
535+
source_description: str,
536+
original_hash: str,
537+
signature: str | None,
538+
verified: bool,
539+
created_at: str,
540+
) -> None:
541+
"""Store a provenance record."""
542+
conn = self._get_conn()
543+
conn.execute(
544+
"""INSERT INTO provenance (
545+
id, resource_id, uploader_id, upload_method,
546+
source_description, original_hash, provenance_signature,
547+
signature_verified, created_at
548+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
549+
(
550+
provenance_id, resource_id, uploader_id, upload_method,
551+
source_description, original_hash, signature,
552+
1 if verified else 0, created_at,
553+
),
554+
)
555+
conn.commit()
556+
557+
async def get_provenance(self, resource_id: str) -> list[dict[str, Any]]:
558+
"""Get all provenance records for a resource."""
559+
conn = self._get_conn()
560+
rows = conn.execute(
561+
"SELECT * FROM provenance WHERE resource_id = ? ORDER BY created_at",
562+
(resource_id,),
563+
).fetchall()
564+
return [dict(r) for r in rows]
565+
503566
async def close(self) -> None:
504567
"""Close the database connection."""
505568
if self._conn:

0 commit comments

Comments
 (0)