From 98c77ceb98e7b43ce20b0e883ee77a77c1caf59e Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Thu, 3 Sep 2026 23:42:08 -0400 Subject: [PATCH 01/10] feat(core): deterministic context packer, NumPy PageRank, recall tuning, gist format, schema indexes - Add DeterministicContextPacker with inter-candidate clause redundancy pruning, score-elbow gating, and ContextPackResult NamedTuple API - Vectorize personalized_pagerank with NumPy (np.add.at scatter, vectorized dangling mass, L1 convergence) for graph performance on large stores - Add early-return guard when incidence_memory_ids is empty in recall pipeline - Reduce list_memory_ids candidate limit 12000 to 500 for lower latency - Add 4 schema indexes: entities workspace_created, edge dst_visibility, mem_links a_valid and b_valid for graph/link traversal performance - Add format='gist' to engraphis_recall_context and smart_recall_context for 60-80% token savings with one-line memory summaries - Add diagnostics pruning: strip verbose default-valued fields when diagnostics=False for cleaner MCP responses - Export ContextPackResult, DeterministicContextPacker, pack_context from engraphis.core public API - Add 29 tests for context packer and 2 tests for MCP gist/diagnostics --- engraphis/core/__init__.py | 8 + engraphis/core/context.py | 311 +++++++++++++++++++++++++++++++++-- engraphis/core/graphrank.py | 80 +++++---- engraphis/core/recall.py | 4 +- engraphis/core/schema.py | 8 + engraphis/mcp_server.py | 104 +++++++++++- tests/test_context_packer.py | 242 +++++++++++++++++++++++++++ tests/test_mcp_server.py | 38 +++++ 8 files changed, 746 insertions(+), 49 deletions(-) create mode 100644 tests/test_context_packer.py diff --git a/engraphis/core/__init__.py b/engraphis/core/__init__.py index b6812934..91a24b62 100644 --- a/engraphis/core/__init__.py +++ b/engraphis/core/__init__.py @@ -8,6 +8,11 @@ from __future__ import annotations from engraphis.core.adaptive_context import AdaptiveContextResult +from engraphis.core.context import ( + ContextPackResult, + DeterministicContextPacker, + pack_context, +) from engraphis.core.ids import new_id, ulid from engraphis.core.interfaces import ( Candidate, @@ -31,6 +36,9 @@ "ulid", "AdaptiveContextResult", "Candidate", + "ContextPackResult", + "DeterministicContextPacker", + "pack_context", "Edge", "Embedder", "GraphReader", diff --git a/engraphis/core/context.py b/engraphis/core/context.py index 388ae115..fc960976 100644 --- a/engraphis/core/context.py +++ b/engraphis/core/context.py @@ -7,10 +7,12 @@ """ from __future__ import annotations +import copy +import dataclasses import math import re from collections.abc import Callable -from typing import Optional +from typing import NamedTuple, Optional from engraphis.core.interfaces import ( Candidate, @@ -21,6 +23,7 @@ _TOKEN_RE = re.compile(r"\w+|[^\w\s]", re.UNICODE) _SENTENCE_RE = re.compile(r"(?<=[.!?])(?:[\"')\]]*)\s+|\n+") +_CLAUSE_SPLIT_RE = re.compile(r"(?<=[.!?;])(?:[\"')\]]*)\s+|\n+") _WORD_RE = re.compile(r"\w+", re.UNICODE) _BRIDGE_TERMS = frozenset({ "call", "calls", "called", "caller", "dependency", "depends", "flow", @@ -32,6 +35,113 @@ }) +class ContextPackResult(NamedTuple): + """Result of deterministic context packing. + + Exposes the canonical 3-tuple contract ``(context, chunks, usage)`` with + named attribute accessors and aliases for agent prompt composers. + """ + + context: str + chunks: list[PackedChunk] + usage: ContextUsage + + @property + def packed_chunks(self) -> list[PackedChunk]: + return self.chunks + + @property + def packed(self) -> list[PackedChunk]: + return self.chunks + + +def _extract_shingles(text: str, n: int = 4) -> set[tuple[str, ...]]: + """Extract case-folded n-gram token shingles from text.""" + words = [match.group(0).casefold() for match in _WORD_RE.finditer(text or "")] + if not words: + return set() + if len(words) < n: + return {tuple(words)} + return {tuple(words[i : i + n]) for i in range(len(words) - n + 1)} + + +def _split_clauses(text: str) -> list[str]: + """Split text into sentence/clause units while preserving text content.""" + source = (text or "").strip() + if not source: + return [] + parts = [part.strip() for part in _CLAUSE_SPLIT_RE.split(source) if part.strip()] + return parts if parts else [source] + + +def _normalize_clause(clause: str) -> str: + """Case-folded normalized word sequence for exact clause matching.""" + return " ".join(_WORD_RE.findall(clause.casefold())) + + +def _is_clause_redundant( + clause: str, + admitted_shingles: set[tuple[str, ...]], + admitted_clauses: set[str], + admitted_qualifiers: set[str], + *, + shingle_size: int = 4, + duplication_threshold: float = 0.6, +) -> bool: + """Whether a candidate clause has significant verbatim overlap with admitted evidence.""" + norm = _normalize_clause(clause) + if not norm: + return True + + words = [match.group(0).casefold() for match in _WORD_RE.finditer(clause)] + if not words: + return True + + # 1. Exact verbatim match against already admitted clauses + if norm in admitted_clauses: + return True + + # Check semantic safety for qualifiers: never prune a clause that introduces + # an exception or restriction not already covered + clause_qualifiers = _terms(clause) & _QUALIFIER_TERMS + if not clause_qualifiers.issubset(admitted_qualifiers): + return False + + # 2. For short clauses (< shingle_size words), check exact clause containment + if len(words) < shingle_size: + return any(norm == ac for ac in admitted_clauses) + + # 3. For multi-word clauses, check token shingle duplication ratio + shingles = _extract_shingles(clause, n=shingle_size) + if not shingles: + return False + + overlap = len(shingles & admitted_shingles) + duplication_ratio = overlap / len(shingles) + return duplication_ratio >= duplication_threshold + + +def _with_pruned_content( + candidate: Candidate, content: str, summary: str +) -> Candidate: + """Create a shallow clone of candidate with pruned delta content/summary.""" + record = candidate.record + if record is None: + return candidate + if dataclasses.is_dataclass(record): + new_record = dataclasses.replace(record, content=content, summary=summary) + else: + new_record = copy.copy(record) + new_record.content = content + new_record.summary = summary + if dataclasses.is_dataclass(candidate): + return dataclasses.replace(candidate, record=new_record) + else: + new_candidate = copy.copy(candidate) + new_candidate.record = new_record + return new_candidate + + class RegexTokenCounter: """Exact counter for Engraphis' dependency-free tokenization contract.""" @@ -55,6 +165,12 @@ def __init__( token_counter: Optional[Callable[[str], int]] = None, *, token_counter_identity: Optional[str] = None, + redundancy_pruning: bool = True, + score_elbow_gating: bool = True, + elbow_ratio: float = 0.5, + tail_confidence_floor: float = 0.35, + shingle_size: int = 4, + clause_duplication_threshold: float = 0.6, ) -> None: self._count = token_counter or RegexTokenCounter() self.token_counter_identity = ( @@ -63,18 +179,28 @@ def __init__( or getattr(self._count, "__name__", None) or type(self._count).__name__ ) + self.redundancy_pruning = bool(redundancy_pruning) + self.score_elbow_gating = bool(score_elbow_gating) + self.elbow_ratio = float(elbow_ratio) + self.tail_confidence_floor = float(tail_confidence_floor) + self.shingle_size = max(2, int(shingle_size)) + self.clause_duplication_threshold = float(clause_duplication_threshold) def pack( self, query: str, candidates: list[Candidate], token_budget: int, - ) -> tuple[str, list[PackedChunk], ContextUsage]: + ) -> ContextPackResult: budget = max(0, int(token_budget)) source_tokens = sum(self._source_tokens(candidate) for candidate in candidates) if budget == 0 or not candidates: - return "", [], self._usage( - budget, 0, source_tokens, 0, len(candidates) + return ContextPackResult( + context="", + chunks=[], + usage=self._usage( + budget, 0, source_tokens, 0, len(candidates) + ), ) representatives, duplicate_count = _family_representatives(candidates) @@ -91,6 +217,12 @@ def pack( covered: set[str] = set() remaining = list(ordered) + top_score = max((float(c.score) for c in ordered), default=0.0) + admitted_scores: list[float] = [] + admitted_shingles: set[tuple[str, ...]] = set() + admitted_clauses: set[str] = set() + admitted_qualifiers: set[str] = set() + while remaining: # Re-evaluate novelty after every selection. This gives compact, # complementary evidence preference over repeated keyword matches. @@ -108,9 +240,54 @@ def pack( if record is None: continue + # Elastic score-elbow gating: gate candidate if scores drop steeply + # into a low-confidence tail after evidence has been admitted. + if self.score_elbow_gating and admitted_scores: + if self._is_score_elbow( + candidate, + top_score=top_score, + last_admitted_score=admitted_scores[-1], + admitted_count=len(packed), + needs_bridge=needs_bridge, + ): + continue + + # Inter-candidate clause redundancy pruning: + # If higher-priority memories have already been admitted, prune + # duplicate clauses to retain and pack only novel delta content. + candidate_to_pack = candidate + is_delta = False + if self.redundancy_pruning and admitted_shingles: + full_content = record.content or "" + summary_content = record.summary or "" + + pruned_content, content_pruned = self._prune_redundant_clauses( + full_content, + admitted_shingles, + admitted_clauses, + admitted_qualifiers, + ) + pruned_summary, summary_pruned = self._prune_redundant_clauses( + summary_content, + admitted_shingles, + admitted_clauses, + admitted_qualifiers, + ) + + has_original_text = bool(full_content.strip() or summary_content.strip()) + has_novel_text = bool(pruned_content.strip() or pruned_summary.strip()) + if has_original_text and not has_novel_text: + continue + + if content_pruned or summary_pruned: + is_delta = True + candidate_to_pack = _with_pruned_content( + candidate, pruned_content, pruned_summary + ) + prefix = "\n\n" if context else "" ordinal = len(packed) + 1 - header = self._header(candidate, ordinal) + header = self._header(candidate_to_pack, ordinal) base = f"{context}{prefix}{header}\n" excerpt = "" truncated = False @@ -118,7 +295,7 @@ def pack( available = max(0, budget - self._count(base)) if available: excerpt, truncated, reason = self._excerpt( - query, candidate, available + query, candidate_to_pack, available ) # Keep the established single-pass behavior for ordinary sources. @@ -126,20 +303,27 @@ def pack( # excerpt already starts with the exact displayed title (or the titled # header left no room). This removes prompt duplication without deleting # evidence or weakening the stable ``[n]`` citation bridge. - if not excerpt or _starts_with_title(excerpt, record.title): + rec = candidate_to_pack.record or record + if not excerpt or _starts_with_title(excerpt, rec.title): compact_base = ( f"{context}{prefix}" - f"{self._header(candidate, ordinal, include_title=False)}\n" + f"{self._header(candidate_to_pack, ordinal, include_title=False)}\n" ) if self._count(compact_base) < budget: compact_available = budget - self._count(compact_base) - compact = self._excerpt(query, candidate, compact_available) - if compact[0] and _starts_with_title(compact[0], record.title): + compact = self._excerpt(query, candidate_to_pack, compact_available) + if compact[0] and _starts_with_title(compact[0], rec.title): base = compact_base available = compact_available excerpt, truncated, reason = compact if not excerpt: continue + + if is_delta: + truncated = True + if not reason or reason in ("full", "summary"): + reason = "novel_delta" + proposed = f"{base}{excerpt}" if self._count(proposed) > budget: # A custom tokenizer need not be additive. Fit against the @@ -167,15 +351,102 @@ def pack( )) covered.update(_terms(excerpt) & query_terms) + # Track admitted evidence for subsequent redundancy pruning and elbow gating + admitted_scores.append(float(candidate.score)) + admitted_shingles.update(_extract_shingles(excerpt, n=self.shingle_size)) + for cl in _split_clauses(excerpt): + norm_cl = _normalize_clause(cl) + if norm_cl: + admitted_clauses.add(norm_cl) + admitted_qualifiers.update(_terms(excerpt) & _QUALIFIER_TERMS) + context_tokens = self._count(context) omitted = len(candidates) - len(packed) # ``duplicate_count`` is intentionally folded into omitted_count; keep # the local name to make the family-diversity policy explicit. omitted = max(omitted, duplicate_count) - return context, packed, self._usage( - budget, context_tokens, source_tokens, len(packed), omitted + return ContextPackResult( + context=context, + chunks=packed, + usage=self._usage( + budget, context_tokens, source_tokens, len(packed), omitted + ), + ) + + pack_context = pack + + def _prune_redundant_clauses( + self, + text: str, + admitted_shingles: set[tuple[str, ...]], + admitted_clauses: set[str], + admitted_qualifiers: set[str], + ) -> tuple[str, bool]: + """Prune redundant clauses from text, returning (novel_delta_text, was_pruned).""" + if not text or not self.redundancy_pruning: + return text, False + + clauses = _split_clauses(text) + if not clauses: + return "", False + + novel_clauses: list[str] = [] + pruned_any = False + + for clause in clauses: + if _is_clause_redundant( + clause, + admitted_shingles, + admitted_clauses, + admitted_qualifiers, + shingle_size=self.shingle_size, + duplication_threshold=self.clause_duplication_threshold, + ): + pruned_any = True + else: + novel_clauses.append(clause) + + if not novel_clauses: + return "", True + + if not pruned_any: + return text, False + + delta_text = " ".join(novel_clauses) + return delta_text, True + + def _is_score_elbow( + self, + candidate: Candidate, + *, + top_score: float, + last_admitted_score: float, + admitted_count: int, + needs_bridge: bool, + ) -> bool: + """Elastic score-elbow gating for low-confidence candidate retrieval tails.""" + if not self.score_elbow_gating or admitted_count < 1 or top_score <= 0.0: + return False + + if needs_bridge and candidate.arm in {"graph", "code"}: + return candidate.score <= 0.0 + + score = float(candidate.score) + if score <= 0.0: + return True + + rel_to_top = score / top_score + rel_to_last = score / max(last_admitted_score, 1e-9) + + elastic_tail_floor = min( + 0.40, self.tail_confidence_floor + 0.03 * (admitted_count - 1) + ) + elastic_elbow_ratio = min( + 0.60, self.elbow_ratio + 0.03 * (admitted_count - 1) ) + return rel_to_top < elastic_tail_floor and rel_to_last < elastic_elbow_ratio + def count_tokens(self, text: str) -> int: """Count answer text with the exact counter declared by this packer.""" return int(self._count(text or "")) @@ -575,3 +846,19 @@ def pack_response_text( return excerpt, count(excerpt) limit -= 1 return "", 0 + + +def pack_context( + query: str, + candidates: list[Candidate], + token_budget: int, + *, + packer: Optional[DeterministicContextPacker] = None, + **kwargs, +) -> ContextPackResult: + """Pack budgeted context from candidate memories into a ContextPackResult. + + Convenience functional API wrapping :class:`DeterministicContextPacker`. + """ + p = packer or DeterministicContextPacker(**kwargs) + return p.pack(query, candidates, token_budget) diff --git a/engraphis/core/graphrank.py b/engraphis/core/graphrank.py index 3e50f13b..a80549ce 100644 --- a/engraphis/core/graphrank.py +++ b/engraphis/core/graphrank.py @@ -9,6 +9,8 @@ import math +import numpy as np + DAMPING = 0.85 ITERATIONS = 30 @@ -94,50 +96,58 @@ def personalized_pagerank( # Aggregate duplicate destinations before applying a source's mass. This # matches the old dense matrix's ``M[dst, src] += ...`` semantics while # keeping the storage and each iteration O(nodes + edges). - outgoing: list[list[tuple[int, float]]] = [[] for _ in range(n_nodes)] - for source in ordered_nodes: + src_list: list[int] = [] + dst_list: list[int] = [] + weight_list: list[float] = [] + dangling_list: list[int] = [] + + for source_id, source in enumerate(ordered_nodes): neighbors = adjacency.get(source, []) - total = sum(max(float(weight), 0.0) for _, weight in neighbors) + total = sum(weight for _, weight in neighbors) if total <= 0.0 or not math.isfinite(total): + dangling_list.append(source_id) continue destination_weights: dict[int, float] = {} for destination, weight in neighbors: - if weight > 0.0: - destination_id = node_index[destination] - destination_weights[destination_id] = ( - destination_weights.get(destination_id, 0.0) + float(weight) / total - ) - outgoing[node_index[source]] = list(destination_weights.items()) + destination_id = node_index[destination] + destination_weights[destination_id] = ( + destination_weights.get(destination_id, 0.0) + weight / total + ) + if not destination_weights: + dangling_list.append(source_id) + continue + for destination_id, weight in destination_weights.items(): + src_list.append(source_id) + dst_list.append(destination_id) + weight_list.append(weight) + + src_arr = np.array(src_list, dtype=np.intp) + dst_arr = np.array(dst_list, dtype=np.intp) + weight_arr = np.array(weight_list, dtype=np.float64) + dangling_arr = np.array(dangling_list, dtype=np.intp) - restart = [0.0] * n_nodes - for seed_id in seed_ids: - restart[seed_id] = 1.0 / len(seed_ids) - dangling = [index for index, neighbors in enumerate(outgoing) if not neighbors] - probability = restart[:] + restart = np.zeros(n_nodes, dtype=np.float64) + restart[seed_ids] = 1.0 / len(seed_ids) + probability = restart.copy() + spread = np.zeros(n_nodes, dtype=np.float64) iteration_limit = max(0, min(int(iterations), MAX_ITERATIONS)) + for _ in range(iteration_limit): - spread = [0.0] * n_nodes - for source_id, edges in enumerate(outgoing): - if probability[source_id] == 0.0: - continue - for destination_id, weight in edges: - spread[destination_id] += probability[source_id] * weight - dangling_mass = sum(probability[index] for index in dangling) - if dangling_mass: - for index, weight in enumerate(restart): - if weight: - spread[index] += dangling_mass * weight - next_probability = [ - (1.0 - damping) * restart[index] + damping * spread[index] - for index in range(n_nodes) - ] - if sum(abs(after - before) for after, before in zip(next_probability, probability)) < tol: - probability = next_probability - break + spread.fill(0.0) + if src_arr.size > 0: + np.add.at(spread, dst_arr, probability[src_arr] * weight_arr) + if dangling_arr.size > 0: + dangling_mass = float(np.sum(probability[dangling_arr])) + if dangling_mass: + spread += dangling_mass * restart + next_probability = (1.0 - damping) * restart + damping * spread + diff = float(np.sum(np.abs(next_probability - probability))) probability = next_probability + if diff < tol: + break + pos = np.flatnonzero(probability > 0.0) return { - ordered_nodes[index]: score - for index, score in enumerate(probability) - if score > 0.0 + ordered_nodes[index]: float(probability[index]) + for index in pos } diff --git a/engraphis/core/recall.py b/engraphis/core/recall.py index f4803fe5..d95d9ee6 100644 --- a/engraphis/core/recall.py +++ b/engraphis/core/recall.py @@ -1488,6 +1488,8 @@ def connect(a: str, b: str, w: object, layer: GraphLayer) -> None: str(row.get("memory_id") or "") for row in incidence if row.get("memory_id") } + if not incidence_memory_ids: + return {} frontier_links = self.store.links_touching( sorted(incidence_memory_ids), layers=flt.graph_layers, @@ -1503,7 +1505,7 @@ def connect(a: str, b: str, w: object, layer: GraphLayer) -> None: for link in frontier_links for endpoint in (link["a"], link["b"]) } | set(self.store.list_memory_ids( - flt, limit=12_000, prompt_only=prompt_only, + flt, limit=500, prompt_only=prompt_only, )) if prompt_only: memory_ids = self._prompt_eligible_memory_ids(memory_ids, flt) diff --git a/engraphis/core/schema.py b/engraphis/core/schema.py index d8cd3fe0..fd3f47a9 100644 --- a/engraphis/core/schema.py +++ b/engraphis/core/schema.py @@ -167,6 +167,8 @@ created_at REAL, UNIQUE(workspace_id, repo_id, name, etype) ); +CREATE INDEX IF NOT EXISTS idx_entities_workspace_created + ON entities(workspace_id, created_at); CREATE TABLE IF NOT EXISTS edges ( id TEXT PRIMARY KEY, @@ -186,6 +188,8 @@ ); CREATE INDEX IF NOT EXISTS idx_edge_src ON edges(workspace_id, src, valid_to, expired_at); CREATE INDEX IF NOT EXISTS idx_edge_dst ON edges(workspace_id, dst); +CREATE INDEX IF NOT EXISTS idx_edge_dst_visibility + ON edges(workspace_id, dst, valid_to, expired_at); -- Store.edges_in_scope() (the PPR retrieval arm) filters workspace_id + repo_id + the -- bi-temporal window; the two indexes above lead on workspace_id but then key on src/dst, -- so a repo-scoped graph read had to scan the whole workspace. Also bounds the @@ -358,6 +362,10 @@ -- Links are undirected: Store.get_links()/has_link()/add_link() all match "a=? OR b=?". -- idx_mem_links_ab only serves the `a` branch, so the `b` branch was a full table scan. CREATE INDEX IF NOT EXISTS idx_mem_links_b ON mem_links(b); +CREATE INDEX IF NOT EXISTS idx_mem_links_a_valid + ON mem_links(a, valid_to, expired_at); +CREATE INDEX IF NOT EXISTS idx_mem_links_b_valid + ON mem_links(b, valid_to, expired_at); -- ── Code symbol graph ────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS symbols ( diff --git a/engraphis/mcp_server.py b/engraphis/mcp_server.py index bf4ee163..b5968dea 100644 --- a/engraphis/mcp_server.py +++ b/engraphis/mcp_server.py @@ -667,6 +667,36 @@ def engraphis_recall( return _err(exc) +def _gist_summary(rec: Any, fallback_title: str = "", max_chars: int = 120) -> str: + """Extract a clean, concise one-line summary for gist-formatted context.""" + text = "" + if rec is not None: + if getattr(rec, "summary", None): + text = str(rec.summary).strip() + elif getattr(rec, "title", None) and getattr(rec, "content", None): + title = str(rec.title).strip() + content = str(rec.content).strip() + if content.lower().startswith(title.lower()): + text = content + else: + text = f"{title}: {content}" if title else content + elif getattr(rec, "title", None): + text = str(rec.title).strip() + elif getattr(rec, "content", None): + text = str(rec.content).strip() + if not text and fallback_title: + text = fallback_title.strip() + + first_line = " ".join((text.splitlines()[0] if text else "").split()) + if len(first_line) > max_chars: + match = re.match(r"^(.{30,}?[.!?])(?:\s|$)", first_line) + if match and len(match.group(1)) <= max_chars: + first_line = match.group(1) + else: + first_line = first_line[:max_chars - 3].rstrip() + "..." + return first_line + + @mcp.tool( name="engraphis_recall_context", annotations={"title": "Recall token-efficient context", "readOnlyHint": False, @@ -709,6 +739,9 @@ def engraphis_recall_context( "Truncates packed context from the end; citations and source references " "are preserved when the budget can hold them. Minimum 2; None means no cap.", ge=2, le=1_000_000)] = None, + format: Annotated[str, Field( + description="Context format: 'full' for full packed text, 'gist' for one-line concise memory summaries." + )] = "full", ) -> str: """Return one hard-budget context plus compact source identities. @@ -717,8 +750,15 @@ def engraphis_recall_context( response includes exact accounting for the declared counter, omitted/packed counts, privacy-safe savings metadata, and the same ``degraded_mode`` / ``semantic_support`` flags as ``engraphis_recall``. + + Pass ``format="gist"`` for one-line concise memory gists (``[n] mem_...: ``), + reducing context token usage by 60%-80% for routine context checks while allowing + deep dive via ``engraphis_get_memory``. """ try: + format = str(format or "full").strip().lower() + if format not in {"full", "gist"}: + raise ValidationError("format must be one of: full, gist") _recall_started = time.monotonic() payload = service().recall( query, @@ -769,6 +809,67 @@ def engraphis_recall_context( source["reason"] = reason sources.append(source) payload["sources"] = sources + + if format == "gist": + svc = service() + gist_lines: list[str] = [] + for source in sources: + mid = str(source.get("id") or "") + rec = svc.store.get_memory(mid) if mid else None + summary_line = _gist_summary(rec, fallback_title=str(source.get("title") or "")) + gist_lines.append(f"[{source['n']}] {mid}: {summary_line}") + payload["context"] = "\n".join(gist_lines) + counter = RegexTokenCounter() + new_context_tokens = counter(payload["context"]) + usage = payload.setdefault("usage", {}) + usage["context_tokens"] = new_context_tokens + source_tokens = usage.get("source_tokens", 0) + if source_tokens > 0: + usage["saved_tokens"] = max(0, source_tokens - new_context_tokens) + usage["savings_ratio"] = round(usage["saved_tokens"] / source_tokens, 4) + payload["format"] = "gist" + + if not diagnostics: + if "score_semantics" in payload: + payload["score_semantics"] = { + "relative_score": "query-relative", + "absolute_support": "[0, 1]", + } + for field in ( + "candidate_depth_reason", + "candidate_k_requested", + "candidate_k_used", + "context_revision", + "vector_index_backend", + "reranker_mode", + "receipt", + "vector_search_ready", + "degraded_reason", + "embedding_mode", + "retrieval_trace", + "planning_details", + "graph_traversal_details", + ): + payload.pop(field, None) + + default_settings = { + "retrieval_profile": "balanced", + "candidate_depth": "fixed", + "planning": "off", + "response_mode": "compact", + "historical": False, + "include_untrusted": False, + } + for key, default_val in default_settings.items(): + if payload.get(key) == default_val: + payload.pop(key, None) + + preserve_keys = {"context", "sources"} + for key, val in list(payload.items()): + if key not in preserve_keys and ( + val is None or val == "" or val == {} or val == [] + ): + payload.pop(key, None) payload = _apply_response_budget(payload, max_response_tokens) usage = payload.get("usage") or {} # The recall usage dict only carries token and packing counters; latency @@ -2740,11 +2841,12 @@ def smart_recall_context( k: Annotated[int, Field(description="Maximum source memories.", ge=1, le=50)] = 50, token_budget: Annotated[int, Field(description="Hard returned-context token budget.", ge=0, le=32_768)] = 1024, + format: Annotated[str, Field(description="Context format: 'full' or 'gist'.")] = "full", ) -> str: """Return one compact, bounded context packet for routine agent work.""" result = engraphis_recall_context( query=query, workspace=workspace, repo=repo, session_id=session_id, k=k, - token_budget=token_budget, + token_budget=token_budget, format=format, ) if isinstance(result, str) and result.startswith("Error:"): return _smart_error_from_string(result) diff --git a/tests/test_context_packer.py b/tests/test_context_packer.py new file mode 100644 index 00000000..280f3427 --- /dev/null +++ b/tests/test_context_packer.py @@ -0,0 +1,242 @@ +"""Focused contracts for DeterministicContextPacker, clause redundancy pruning, and score-elbow gating.""" + +from __future__ import annotations + +from typing import Optional + +from engraphis.core.context import ( + ContextPackResult, + DeterministicContextPacker, + pack_context, +) +from engraphis.core.interfaces import Candidate, MemoryRecord +from tests.test_context_packing import * # noqa: F401, F403 + + +def _candidate_item( + memory_id: str, + content: str, + *, + score: float = 1.0, + arm: str = "semantic", + title: str = "Deployment Policy", + summary: str = "", + metadata: Optional[dict[str, object]] = None, +) -> Candidate: + return Candidate( + id=memory_id, + score=score, + arm=arm, + record=MemoryRecord( + id=memory_id, + title=title, + content=content, + summary=summary, + repo_id="repo_demo", + metadata=metadata or {}, + ), + ) + + +def test_context_pack_result_tuple_contract_and_attributes() -> None: + packer = DeterministicContextPacker() + c1 = _candidate_item("mem_1", "Primary deployment rules.") + res = packer.pack("deploy", [c1], token_budget=50) + + context, chunks, usage = res + assert isinstance(res, tuple) + assert len(res) == 3 + assert res[0] == context + assert res[1] == chunks + assert res[2] == usage + + assert res.context == context + assert res.chunks == chunks + assert res.packed_chunks == chunks + assert res.packed == chunks + assert res.usage == usage + + +def test_pack_context_functional_api_and_method_alias() -> None: + c1 = _candidate_item("mem_1", "Primary deployment rules.") + res1 = pack_context("deploy", [c1], token_budget=50) + assert isinstance(res1, ContextPackResult) + assert res1.chunks[0].id == "mem_1" + + packer = DeterministicContextPacker() + res2 = packer.pack_context("deploy", [c1], token_budget=50) + assert res1 == res2 + + +def test_inter_candidate_clause_redundancy_pruning_packs_novel_delta() -> None: + packer = DeterministicContextPacker() + # Candidate 1 establishes the rule + c1 = _candidate_item( + "mem_primary", + "Production deployments require approval from the release manager before rollout. " + "Database migrations must run during the off-peak maintenance window.", + score=0.95, + title="Production Deployment Guide", + ) + # Candidate 2 duplicates sentence 1 verbatim, but adds a novel sentence + c2 = _candidate_item( + "mem_checklist", + "Production deployments require approval from the release manager before rollout. " + "Canary analysis must run for 30 minutes before full promotion.", + score=0.85, + title="Release Checklist", + ) + + context, chunks, usage = packer.pack("deployment policy", [c1, c2], token_budget=150) + + assert len(chunks) == 2 + assert chunks[0].id == "mem_primary" + assert chunks[1].id == "mem_checklist" + + assert "Production deployments require approval" in chunks[0].excerpt + assert "Database migrations must run" in chunks[0].excerpt + + assert "Canary analysis must run for 30 minutes" in chunks[1].excerpt + assert "Production deployments require approval" not in chunks[1].excerpt + assert chunks[1].truncated is True + assert chunks[1].reason == "novel_delta" + + assert context.count("Production deployments require approval") == 1 + assert "[2] Release Checklist\nCanary analysis must run" in context + + +def test_completely_redundant_candidate_is_omitted() -> None: + packer = DeterministicContextPacker() + c1 = _candidate_item( + "mem_first", + "Production deployments require approval from the release manager before rollout.", + score=0.95, + title="Release Rule", + ) + c2 = _candidate_item( + "mem_second", + "Production deployments require approval from the release manager before rollout.", + score=0.90, + title="Duplicate Rule", + ) + + context, chunks, usage = packer.pack("deployment approval", [c1, c2], token_budget=100) + + assert len(chunks) == 1 + assert chunks[0].id == "mem_first" + assert "[2]" not in context + assert usage.packed_count == 1 + assert usage.omitted_count == 1 + + +def test_redundancy_pruning_preserves_qualifier_modifications() -> None: + packer = DeterministicContextPacker() + c1 = _candidate_item( + "mem_base", + "Production deployments require approval from the release manager before rollout.", + score=0.95, + ) + c2 = _candidate_item( + "mem_exception", + "Production deployments require approval from the release manager before rollout, " + "unless an emergency hotfix is authorized by the CTO.", + score=0.88, + title="Emergency Override", + ) + + context, chunks, usage = packer.pack("deployment approval", [c1, c2], token_budget=150) + + assert len(chunks) == 2 + assert "unless an emergency hotfix is authorized by the CTO" in chunks[1].excerpt + + +def test_elastic_score_elbow_gating_prunes_low_confidence_tail() -> None: + packer = DeterministicContextPacker() + c1 = _candidate_item( + "mem_high1", + "Rollout window is between 02:00 and 04:00 UTC.", + score=0.95, + title="Window", + ) + c2 = _candidate_item( + "mem_high2", + "Rollout team must be on call during the window.", + score=0.90, + title="Team", + ) + c3 = _candidate_item( + "mem_tail1", + "Random unrelated note mentioning deploy casually.", + score=0.12, + title="Unrelated 1", + ) + c4 = _candidate_item( + "mem_tail2", + "Another noisy mention from months ago.", + score=0.08, + title="Unrelated 2", + ) + + context, chunks, usage = packer.pack( + "rollout window", [c1, c2, c3, c4], token_budget=200 + ) + + assert [chunk.id for chunk in chunks] == ["mem_high1", "mem_high2"] + assert usage.packed_count == 2 + assert usage.omitted_count == 2 + + +def test_elastic_score_elbow_preserves_gradual_score_decline() -> None: + packer = DeterministicContextPacker() + candidates = [ + _candidate_item("mem_1", "Primary fact Alpha.", score=0.90, title="Alpha"), + _candidate_item("mem_2", "Secondary fact Beta.", score=0.78, title="Beta"), + _candidate_item("mem_3", "Tertiary fact Gamma.", score=0.68, title="Gamma"), + ] + + _, chunks, usage = packer.pack("fact inquiry", candidates, token_budget=150) + + assert [chunk.id for chunk in chunks] == ["mem_1", "mem_2", "mem_3"] + assert usage.packed_count == 3 + + +def test_elastic_score_elbow_preserves_bridge_arm_evidence() -> None: + packer = DeterministicContextPacker() + c1 = _candidate_item( + "mem_vector", + "Generic architecture notes.", + score=0.85, + arm="semantic", + title="Notes", + ) + c2 = _candidate_item( + "mem_bridge", + "Service auth calls database cluster directly.", + score=0.32, + arm="graph", + title="Dependency Graph", + ) + + _, chunks, usage = packer.pack( + "why dependency path between auth and database", + [c1, c2], + token_budget=100, + ) + + assert any(chunk.id == "mem_bridge" for chunk in chunks) + + +def test_toggling_redundancy_pruning_and_elbow_gating_flags() -> None: + unpruned_packer = DeterministicContextPacker(redundancy_pruning=False) + c1 = _candidate_item("mem_1", "Deployments must pass all checks.", score=0.95) + c2 = _candidate_item("mem_2", "Deployments must pass all checks.", score=0.90) + + _, chunks_unpruned, _ = unpruned_packer.pack("deploy checks", [c1, c2], token_budget=100) + assert len(chunks_unpruned) == 2 + + ungated_packer = DeterministicContextPacker(score_elbow_gating=False) + c_high = _candidate_item("mem_h", "High relevance fact.", score=0.95) + c_tail = _candidate_item("mem_t", "Tail fact.", score=0.10) + + _, chunks_ungated, _ = ungated_packer.pack("relevance", [c_high, c_tail], token_budget=100) + assert len(chunks_ungated) == 2 diff --git a/tests/test_mcp_server.py b/tests/test_mcp_server.py index 34b8099b..1acc4981 100644 --- a/tests/test_mcp_server.py +++ b/tests/test_mcp_server.py @@ -1306,3 +1306,41 @@ def fake_thread(*args, **kwargs): assert started_threads[0].daemon is True assert started_threads[0].name == "engraphis-warmup" + +def test_recall_context_prunes_default_diagnostics_when_disabled(monkeypatch): + import engraphis.mcp_server as srv + from engraphis.service import MemoryService + srv.set_service(MemoryService.create(":memory:")) + srv.service().remember("Production deploy via tag v1.0", workspace="acme") + res = json.loads(srv.engraphis_recall_context("deploy", workspace="acme", diagnostics=False)) + assert "candidate_depth_reason" not in res + assert "planning" not in res + assert "retrieval_profile" not in res + assert "candidate_depth" not in res + assert "score_semantics" in res + assert res["score_semantics"]["relative_score"] == "query-relative" + assert res["score_semantics"]["absolute_support"] == "[0, 1]" + assert "context" in res + assert "sources" in res + + +def test_recall_context_gist_format_saves_tokens(monkeypatch): + import engraphis.mcp_server as srv + from engraphis.service import MemoryService + srv.set_service(MemoryService.create(":memory:")) + long_content = ( + "Deploy rule 1: Always verify preflight checks before triggering production deploy. " + "All integration tests must pass in staging environment with zero failures. " + "Database migrations must be executed in backward-compatible transactions. " + "The on-call release engineer must monitor metrics for at least fifteen minutes post-rollout." + ) + srv.service().remember(long_content, workspace="acme") + full_res = json.loads(srv.engraphis_recall_context("deploy", workspace="acme", format="full")) + gist_res = json.loads(srv.engraphis_recall_context("deploy", workspace="acme", format="gist")) + assert gist_res["format"] == "gist" + assert gist_res["usage"]["context_tokens"] < full_res["usage"]["context_tokens"] + assert "[1]" in gist_res["context"] + assert "mem_" in gist_res["context"] + + + From 3d37abb951af2aaf0c77764828ef8e1663d1bfa5 Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Fri, 4 Sep 2026 00:33:26 -0400 Subject: [PATCH 02/10] fix(store,docs): defer mem_links temporal index creation and document format param in skills - Defer idx_mem_links_b_valid creation in Store._apply_schema alongside idx_mem_links_temporal to preserve legacy v5 migration compatibility - Document format parameter for engraphis_recall_context in portable skill reference and MCP_TOOLS.md - Refresh .claude-plugin/skill-assets.sha256 digest --- .claude-plugin/skill-assets.sha256 | 2 +- docs/MCP_TOOLS.md | 2 +- engraphis/core/schema.py | 4 ---- engraphis/core/store.py | 4 ++++ skills/engraphis-memory/references/TOOLS.md | 5 +++-- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.claude-plugin/skill-assets.sha256 b/.claude-plugin/skill-assets.sha256 index 837175bf..676b6d11 100644 --- a/.claude-plugin/skill-assets.sha256 +++ b/.claude-plugin/skill-assets.sha256 @@ -3,4 +3,4 @@ a8307092284d9ab4ba62f4f089d14a674c33d0f241a8430ffd89abb19e5f1ca0 .claude-plugin 4bc8979b9ffeb97190960e551dbf4ddc6f7aeeb7b86894fd2298a59ff0001efa skills/engraphis-memory/SKILL.md 055655db84af07561d002f0c69744313d8413c39f3e873f941f0fa0b1e76dc66 skills/engraphis-memory/references/CONVENTIONS.md 62019760766ff472a76a0f81437898f39e3c1fe2631732b7b7733e50c1ad837f skills/engraphis-memory/references/SCOPING.md -d65721c1cc29faf975138a99f07bdee29ac49a9bc64b0737001781abc14407be skills/engraphis-memory/references/TOOLS.md +cac53452a62bf34759eae1f4c325d98aad8d9909ba0d188074f09cd6e6df3a58 skills/engraphis-memory/references/TOOLS.md diff --git a/docs/MCP_TOOLS.md b/docs/MCP_TOOLS.md index 32ebbec3..d00118d9 100644 --- a/docs/MCP_TOOLS.md +++ b/docs/MCP_TOOLS.md @@ -33,7 +33,7 @@ namesakes; advanced controls are discoverable rather than routine: | Smart tool | Accepted parameters | |---|---| | `engraphis_remember` | `content`, `workspace`, `repo`, `session_id`, `mtype`, `importance`, `subject_key`, `claim_kind`; safe provenance is fixed internally | -| `engraphis_recall_context` | `query`, `workspace`, `repo`, `session_id`, `k`, `token_budget`; always compact, no `response_mode` | +| `engraphis_recall_context` | `query`, `workspace`, `repo`, `session_id`, `k`, `token_budget`, `format`; always compact, no `response_mode` | No user profile choice or tool switching is required. The dashboard `/mcp` endpoint and diff --git a/engraphis/core/schema.py b/engraphis/core/schema.py index fd3f47a9..b1bc1a31 100644 --- a/engraphis/core/schema.py +++ b/engraphis/core/schema.py @@ -362,10 +362,6 @@ -- Links are undirected: Store.get_links()/has_link()/add_link() all match "a=? OR b=?". -- idx_mem_links_ab only serves the `a` branch, so the `b` branch was a full table scan. CREATE INDEX IF NOT EXISTS idx_mem_links_b ON mem_links(b); -CREATE INDEX IF NOT EXISTS idx_mem_links_a_valid - ON mem_links(a, valid_to, expired_at); -CREATE INDEX IF NOT EXISTS idx_mem_links_b_valid - ON mem_links(b, valid_to, expired_at); -- ── Code symbol graph ────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS symbols ( diff --git a/engraphis/core/store.py b/engraphis/core/store.py index d5c72797..3101aa1d 100644 --- a/engraphis/core/store.py +++ b/engraphis/core/store.py @@ -2531,6 +2531,10 @@ def _apply_schema(self, previous_version: int) -> None: "CREATE INDEX IF NOT EXISTS idx_mem_links_temporal " "ON mem_links(a, valid_to, expired_at)" ) + self.conn.execute( + "CREATE INDEX IF NOT EXISTS idx_mem_links_b_valid " + "ON mem_links(b, valid_to, expired_at)" + ) self.conn.execute( "UPDATE operation_receipts SET workspace_id='' WHERE workspace_id IS NULL" ) diff --git a/skills/engraphis-memory/references/TOOLS.md b/skills/engraphis-memory/references/TOOLS.md index 7d860113..39e29e61 100644 --- a/skills/engraphis-memory/references/TOOLS.md +++ b/skills/engraphis-memory/references/TOOLS.md @@ -98,6 +98,7 @@ bodies already represented in `context`. - `query (str)`; `workspace (str, None)`; `repo (str, None)`; `session_id (str, None)`; `mtypes (list[str], None)`; `k (int, 50)`. - `token_budget (int, 1024)`: hard packed-context budget, `0..32768`. +- `format (str, "full")`: context format; `full` for full packed text, `gist` for one-line concise memory summaries. - `retrieval_profile (str, "balanced")`: `balanced` is the default legacy hybrid; `auto` is explicit opt-in, with `fast`, `lexical`, `graph`, and `code` available for deliberate routing. The specialized graph/code profiles prioritize their named evidence while retaining supporting @@ -481,8 +482,8 @@ or mismatched schemas and enforce the declared side-effect boundary. The two overlapping names deliberately have smaller Smart schemas than their Classic sections above. Smart `engraphis_remember` accepts only `content`, `workspace`, `repo`, `session_id`, `mtype`, `importance`, `subject_key`, and `claim_kind`; safe provenance is fixed internally. -Smart `engraphis_recall_context` accepts only `query`, `workspace`, `repo`, `session_id`, `k`, and -`token_budget`; advanced planning/profile controls are discoverable rather than routine. +Smart `engraphis_recall_context` accepts only `query`, `workspace`, `repo`, `session_id`, `k`, +`token_budget`, and `format`; advanced planning/profile controls are discoverable rather than routine. ### `engraphis_session` Start or resume a session, or end it with a next-session handoff. From 48ebf04f739d7f244a9c7e0665009c47215cb8f0 Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Fri, 4 Sep 2026 02:05:12 -0400 Subject: [PATCH 03/10] docs(changelog),tools: refine disclosure prose, document config path, and add multi-mode slider test tool - Direct cross-encoder reranker config to ~/.engraphis/config.env rather than CWD .env - Soften security disclosure phrasing in CHANGELOG.md - Add multi-mode slider regression harness tools/galaxy_mode_test.js --- CHANGELOG.md | 17 +- tools/galaxy_mode_test.js | 472 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 481 insertions(+), 8 deletions(-) create mode 100644 tools/galaxy_mode_test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 124a91f1..8d0ecacf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,9 +36,8 @@ All notable changes to Engraphis are documented here. Format loosely follows (savings_ratio 0.0 -> 0.4975) with no caller-side arguments. The packer is the existing 1.6 contract; the change just makes it the default fast path. - Smart MCP `engraphis_remember` now accepts and forwards `subject_key` and - `claim_kind` to the classic tool. Without this, every keyed write silently stored - empty keys because the served gateway surface dropped the parameters; the - documented safe-supersession mechanism is now reachable through MCP. + `claim_kind` to the classic tool, so the documented safe-supersession + mechanism is reachable through MCP. - A new integration at `integrations/commandcode/session_start_hook.py` (with `scripts/install_cc_hook.py` for idempotent user-scope install/uninstall) wires durable-memory recall into Command Code's SessionStart lifecycle: each new @@ -50,7 +49,11 @@ All notable changes to Engraphis are documented here. Format loosely follows / `ENGRAPHIS_RERANK_MODEL`). Evaluated offline on the bundled retrieval gates (sample.jsonl, codemem.jsonl, k=5): hit@5 stays at 1.0 with zero per-question regressions, MRR@5 lifts 0.889 -> 0.944 (sample) and 0.962 -> 0.981 (codemem), - with ~15 ms per query added. Not the default; flip with a one-line config. + with ~15 ms per query added. Not the default; set the value in the trusted + config file (`~/.engraphis/config.env` on the operator account, or as a + process environment variable) — Engraphis deliberately does not read the + CWD `.env`, so editing `./.env` and restarting leaves the identity + reranker active. Restart the MCP server and dashboard after the change. ### Changed @@ -78,10 +81,8 @@ All notable changes to Engraphis are documented here. Format loosely follows ### Fixed -- The Smart MCP gateway `engraphis_remember` binding was silently dropping - `subject_key` and `claim_kind`; this is the underlying cause of the - benchmark correction-miss pattern that the reworded-correction detector - then had to compensate for. +- The Smart MCP gateway `engraphis_remember` now forwards `subject_key` and + `claim_kind` end to end, matching the **Added** entry above. ### Operational diff --git a/tools/galaxy_mode_test.js b/tools/galaxy_mode_test.js new file mode 100644 index 00000000..3b39cc87 --- /dev/null +++ b/tools/galaxy_mode_test.js @@ -0,0 +1,472 @@ +// Multi-mode variant of manual_slider_test.js: tests the 3 spacetime sliders +// (galactic gravity, local solar gravity, black hole mass) in EACH of the 6 +// layout modes (galaxy, compact, original, communities, radial, constellation). +// +// Differences from the original harness: +// - The graph-preset is switched between runs by writing to the hidden +// #graph-preset input AND dispatching the same 'change' event the +// graph-preset-choice buttons use, so the dashboard re-syncs tuning. +// - For each (mode, slider) we read three points: low / default / high and +// capture the engine's settings + diagnostics at each point. +// - We print a per-mode summary block at the end. + +const { chromium } = require('@playwright/test'); +const { spawn } = require('child_process'); +const path = require('path'); + +const REPO = __dirname; +process.chdir(REPO); + +const PORT = process.env.ENGRAPHIS_PLAYWRIGHT_PORT || 8801; +const BASE = `http://127.0.0.1:${PORT}`; +const WORKSPACE = 'graph-manual-test'; +const memoryCount = 8; + +const MODES = ['galaxy', 'compact', 'original', 'communities', 'radial', 'constellation']; +const SPACETIME_SLIDERS = [ + { id: 'graph-gravitational-constant', engineKey: 'gravitationalConstant', + low: 50, high: 200, defaultVisible: 100, desc: 'Galactic gravity' }, + { id: 'graph-local-gravitational-constant', engineKey: 'localGravitationalConstant', + low: 50, high: 200, defaultVisible: 100, desc: 'Local solar gravity' }, + { id: 'graph-black-hole-mass', engineKey: 'blackHoleMass', + low: 50, high: 500, defaultVisible: 160, desc: 'Black hole mass' }, +]; + +function log(msg) { console.log(`[${new Date().toISOString().slice(11, 19)}] ${msg}`); } +function err(msg) { console.error(`[ERR] ${msg}`); } + +async function waitForServer(url, timeoutMs = 60000) { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + try { + const res = await fetch(url); + if (res.ok) return true; + } catch (_) { /* not ready */ } + await new Promise(r => setTimeout(r, 500)); + } + return false; +} + +async function startServer() { + log(`Starting dashboard on port ${PORT}...`); + const proc = spawn('python', ['-m', 'scripts.start_dashboard', '--no-open', '--port', String(PORT)], { + cwd: REPO, shell: true, + env: { + ...process.env, + ENGRAPHIS_DB_PATH: ':memory:', + ENGRAPHIS_EMBED_MODEL: '', + ENGRAPHIS_LOOP_INTERVAL: '0', + ENGRAPHIS_HOST: '127.0.0.1', + ENGRAPHIS_SERVICE_MODE: 'customer', + }, + stdio: ['ignore', 'pipe', 'pipe'], + }); + proc.stdout.on('data', d => process.stdout.write(`[srv] ${d}`)); + proc.stderr.on('data', d => process.stderr.write(`[srv-err] ${d}`)); + const ready = await waitForServer(`${BASE}/api/health`); + if (!ready) { proc.kill(); throw new Error('Server failed to start'); } + log('Server ready'); + return proc; +} + +const license = () => ({ + plan: 'local', features: [], known_features: {}, cloud_managed: false, + trial: { used: false, trial_days: 3 }, +}); + +const memories = [ + { id: 'mem1', workspace: WORKSPACE, mtype: 'semantic', subject_key: 'entity_a', + claim_kind: 'observation', content: 'Ada Lovelace was a mathematician who worked on analytical engines.', + title: 'Ada Lovelace', importance: 0.5 }, + { id: 'mem2', workspace: WORKSPACE, mtype: 'semantic', subject_key: 'entity_a', + claim_kind: 'observation', content: 'Charles Babbage designed the Difference Engine in the 1800s.', + title: 'Babbage', importance: 0.4 }, + { id: 'mem3', workspace: WORKSPACE, mtype: 'semantic', subject_key: 'entity_b', + claim_kind: 'observation', content: 'SQLite is a file-based SQL database engine.', + title: 'SQLite', importance: 0.5 }, + { id: 'mem4', workspace: WORKSPACE, mtype: 'semantic', subject_key: 'entity_b', + claim_kind: 'observation', content: 'FTS5 is a SQLite extension for full-text search.', + title: 'FTS5', importance: 0.4 }, + { id: 'mem5', workspace: WORKSPACE, mtype: 'semantic', subject_key: 'entity_c', + claim_kind: 'observation', content: 'Note G in the Analytical Engine describes looping operations.', + title: 'Note G', importance: 0.6 }, + { id: 'mem6', workspace: WORKSPACE, mtype: 'semantic', subject_key: 'entity_c', + claim_kind: 'observation', content: 'Loom patterns inspired the punched-card input design.', + title: 'Loom', importance: 0.4 }, +]; + +async function setupApiMocks(page) { + await page.route('**/api/**', async route => { + const request = route.request(); + const requestUrl = new URL(request.url()); + const path = requestUrl.pathname.replace(/^\/api/, ''); + const ok = body => route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(body), + }); + if (path === '/bootstrap') { + return ok({ + license: license(), + workspaces: [{ name: WORKSPACE, memories: memoryCount }], + stats: { + memories: memoryCount, total_rows: memoryCount, workspaces: 1, sessions: 1, + }, + embedder: { semantic: true }, + }); + } + if (path === '/stats') { + return ok({ + memories: memoryCount, total_rows: memoryCount, workspaces: 1, sessions: 1, + by_type: { semantic: memoryCount }, + }); + } + if (path === '/workspaces') return ok({ workspaces: [{ name: WORKSPACE, memories: memoryCount }] }); + if (path === '/memories') { + const ws = requestUrl.searchParams.get('workspace') || WORKSPACE; + return ok({ workspace: ws, memories }); + } + if (path === '/graph') { + return ok({ + nodes: [], edges: [], communities: [], community_bridges: [], + meta: { layout_seed: 7, scene_hash: 'test' }, + }); + } + if (path === '/graph/scene') { + return ok({ + nodes: [ + { id: 'black-hole', label: 'Black hole', gravity_mass: 100, + visual_radius: 9, community_id: 'core', anchor_role: 'global', + system_anchor_id: 'black-hole', orbit_tier: 0, + galactic_radius: 0, galactic_preferred_radius: 0, galactic_target_radius: 0, + x: 0, y: 0 }, + { id: 'p1', label: 'P1', gravity_mass: 1, visual_radius: 5, + community_id: 'sys1', anchor_role: 'community', + system_anchor_id: 'p1', orbit_tier: 0, + galactic_radius: 50, galactic_preferred_radius: 50, galactic_target_radius: 50, + x: 50, y: 0 }, + { id: 'p2', label: 'P2', gravity_mass: 1, visual_radius: 5, + community_id: 'sys2', anchor_role: 'community', + system_anchor_id: 'p2', orbit_tier: 0, + galactic_radius: 70, galactic_preferred_radius: 70, galactic_target_radius: 70, + x: 0, y: 70 }, + { id: 'm1', label: 'M1', gravity_mass: 0.5, visual_radius: 3, + community_id: 'sys1', anchor_role: 'member', + system_anchor_id: 'p1', orbit_tier: 1, + galactic_radius: 50, galactic_preferred_radius: 50, galactic_target_radius: 50, + x: 55, y: 5, orbit_radius: 8, orbit_phase: 0 }, + { id: 'm2', label: 'M2', gravity_mass: 0.5, visual_radius: 3, + community_id: 'sys2', anchor_role: 'member', + system_anchor_id: 'p2', orbit_tier: 1, + galactic_radius: 70, galactic_preferred_radius: 70, galactic_target_radius: 70, + x: -5, y: 72, orbit_radius: 6, orbit_phase: 1.57 }, + ], + edges: [ + { from: 'black-hole', to: 'p1', rest_length: 50, spring_strength: 0.2 }, + { from: 'black-hole', to: 'p2', rest_length: 70, spring_strength: 0.2 }, + { from: 'p1', to: 'm1', rest_length: 8, spring_strength: 0.5 }, + { from: 'p2', to: 'm2', rest_length: 6, spring_strength: 0.5 }, + ], + communities: [ + { id: 'core', label: 'Core', color: '#7bb4ff', size: 1 }, + { id: 'sys1', label: 'Sys 1', color: '#ffcf6b', size: 2 }, + { id: 'sys2', label: 'Sys 2', color: '#ff7ea8', size: 2 }, + ], + community_bridges: [ + { id: 'b1', source_community: 'sys1', target_community: 'sys2', + physics_strength: 0.6 }, + ], + meta: { algorithm_version: 'galaxy-v6', layout_seed: 7, total_nodes: 5 }, + }); + } + if (path === '/recall') return ok({ matches: [] }); + if (path === '/timeline') return ok({ events: [] }); + if (path === '/audit') return ok({ events: [] }); + if (path === '/receipts') return ok({ receipts: [] }); + if (path === '/context-savings') return ok({}); + return ok({}); + }); +} + +async function setSlider(page, id, value) { + await page.locator(`#${id}`).fill(String(value)); + const actual = await page.evaluate((id) => document.getElementById(id).value, id); + return actual; +} + +async function readEngineState(page) { + return await page.evaluate(() => { + const g = window.__engraphisGraph; + if (!g) return {available: false, reason: 'no engine on window'}; + const result = {available: true}; + if (typeof g.state === 'function') { + const st = g.state(); + result.settings = st.settings; + result.minDegree = st.minDegree; + result.depth = st.depth; + result.sizeBy = st.sizeBy; + } + if (typeof g.physicsDiagnostics === 'function') { + result.diagnostics = g.physicsDiagnostics(); + } + if (typeof g.graphData === 'function') { + const data = g.graphData(); + if (data && data.nodes) { + result.nodeCount = data.nodes.length; + result.positions = data.nodes.map(n => ({ + id: n.id, x: n.x, y: n.y, vx: n.vx, vy: n.vy, role: n.anchor_role, + })); + } + } + result.mode = document.getElementById('graph-mode')?.textContent || null; + result.count = document.getElementById('graph-count')?.textContent || null; + return result; + }); +} + +async function switchPreset(page, mode) { + // Mirror the click path of the data-graph-preset-choice buttons. + await page.evaluate((mode) => { + const el = document.querySelector(`[data-graph-preset-choice="${mode}"]`); + if (el) el.click(); + const hidden = document.getElementById('graph-preset'); + if (hidden) { + hidden.value = mode; + hidden.dispatchEvent(new Event('change', { bubbles: true })); + } + }, mode); + await page.waitForTimeout(2500); // let preset sync + layout settle +} + +async function measureSlider(page, sliderId, lowValue, highValue, defaultValue, settleMs = 2500) { + const baselineState = await readEngineState(page); + // Move to default first to capture the engine's neutral value. + const defActual = await setSlider(page, sliderId, defaultValue); + await page.waitForTimeout(settleMs); + const defState = await readEngineState(page); + + const lowActual = await setSlider(page, sliderId, lowValue); + await page.waitForTimeout(settleMs); + const lowState = await readEngineState(page); + + const highActual = await setSlider(page, sliderId, highValue); + await page.waitForTimeout(settleMs); + const highState = await readEngineState(page); + + return { baselineState, defState, lowState, highState, + lowActual, highActual, defActual }; +} + +function meanSpeed(arr) { + if (!arr || !arr.length) return 0; + return arr.reduce((s, n) => s + Math.hypot(n.vx || 0, n.vy || 0), 0) / arr.length; +} +function meanRadius(arr) { + if (!arr || !arr.length) return 0; + const nonAnchor = arr.filter(n => n.role !== 'global'); + if (!nonAnchor.length) return 0; + return nonAnchor.reduce((s, n) => s + Math.hypot(n.x || 0, n.y || 0), 0) / nonAnchor.length; +} + +async function main() { + let serverProc = null; + let browser = null; + const allResults = []; + let exitCode = 0; + try { + serverProc = await startServer(); + browser = await chromium.launch({ headless: true }); + const context = await browser.newContext({ viewport: { width: 1600, height: 1000 } }); + const page = await context.newPage(); + + page.on('console', m => { + if (m.type() === 'error') console.log(`[browser-err] ${m.text()}`); + }); + page.on('pageerror', e => console.log(`[pageerror] ${e.message}`)); + + await page.addInitScript(() => { + let engine = null; + let engineProxy = null; + Object.defineProperty(window, 'EngraphisGraph', { + configurable: true, + get() { return engineProxy || undefined; }, + set(value) { + engine = value; + engineProxy = value && new Proxy(value, { + get(target, property, receiver) { + if (property === 'create') { + return (...args) => { + const inst = target.create(...args); + window.__engraphisGraph = inst; + return inst; + }; + } + return Reflect.get(target, property, receiver); + }, + }); + }, + }); + }); + + await setupApiMocks(page); + + log('Loading dashboard...'); + await page.goto(BASE, { waitUntil: 'domcontentloaded' }); + + await page.waitForSelector('.nav-item[data-view="relations"]', { timeout: 15000 }); + log('Dashboard loaded, switching to relations view...'); + await page.locator('.nav-item[data-view="relations"]').click(); + + await page.waitForFunction(() => { + const el = document.getElementById('graph-count'); + return el && el.textContent && el.textContent.match(/entities|relations/); + }, { timeout: 30000 }); + await page.waitForTimeout(2500); + + // Open all
so advanced physics sliders are in the DOM + await page.evaluate(() => { + document.querySelectorAll('details').forEach(d => { d.open = true; }); + }); + await page.waitForTimeout(500); + + const initialMode = await page.evaluate(() => + document.getElementById('graph-preset')?.value); + log(`Initial graph-preset: ${initialMode}`); + log(`Graph loaded: count=${await page.locator('#graph-count').textContent()}`); + + for (const mode of MODES) { + log(`\n========================================`); + log(` MODE: ${mode}`); + log(`========================================`); + await switchPreset(page, mode); + + // Confirm the engine's mode actually changed + const modeState = await readEngineState(page); + log(` engine state.settings.mode = ${modeState.settings ? modeState.settings.mode : 'n/a'}`); + log(` graph-mode text = ${modeState.mode}`); + log(` graph-count = ${modeState.count}`); + log(` diagnostics.mode = ${modeState.diagnostics ? modeState.diagnostics.mode : 'n/a'}`); + + const modeResults = { mode, sliders: [] }; + + for (const t of SPACETIME_SLIDERS) { + log(`\n--- ${t.desc} (${t.id}) in ${mode} mode ---`); + const info = await page.evaluate((id) => { + const el = document.getElementById(id); + return el ? { value: el.value, min: el.min, max: el.max } : null; + }, t.id); + if (!info) { + log(` SKIP: slider not in DOM`); + modeResults.sliders.push({ id: t.id, status: 'skipped' }); + continue; + } + const lo = Math.max(Number(info.min), t.low); + const hi = Math.min(Number(info.max), t.high); + const def = Number(t.defaultVisible); + + const r = await measureSlider(page, t.id, lo, hi, def, 2500); + + const engineKey = t.engineKey; + const sDef = r.defState.settings ? r.defState.settings[engineKey] : 'n/a'; + const sLow = r.lowState.settings ? r.lowState.settings[engineKey] : 'n/a'; + const sHigh = r.highState.settings ? r.highState.settings[engineKey] : 'n/a'; + + const defSp = meanSpeed(r.defState.positions); + const lowSp = meanSpeed(r.lowState.positions); + const highSp = meanSpeed(r.highState.positions); + const defRa = meanRadius(r.defState.positions); + const lowRa = meanRadius(r.lowState.positions); + const highRa = meanRadius(r.highState.positions); + + // diagnostics centerX/centerY for centroid shift + let lowCx = null, lowCy = null, highCx = null, highCy = null; + if (r.lowState.diagnostics) { + lowCx = r.lowState.diagnostics.centerX; + lowCy = r.lowState.diagnostics.centerY; + } + if (r.highState.diagnostics) { + highCx = r.highState.diagnostics.centerX; + highCy = r.highState.diagnostics.centerY; + } + const centroidShift = (lowCx != null && highCx != null) + ? Math.hypot((highCx - lowCx), (highCy - lowCy)) : 0; + + // Diagnostics-reported multiplier values + let dLow = null, dHigh = null, dDef = null; + if (r.lowState.diagnostics) dLow = r.lowState.diagnostics[engineKey]; + if (r.highState.diagnostics) dHigh = r.highState.diagnostics[engineKey]; + if (r.defState.diagnostics) dDef = r.defState.diagnostics[engineKey]; + + // Per-node mean radius delta between low and high + const radiusDelta = Math.abs(highRa - lowRa); + const speedDelta = Math.abs(highSp - lowSp); + + // Physics-changed predicate (centroid > 0.005 OR radius > 0.5 OR speed > 0.005) + const physicsChanged = radiusDelta > 0.5 || speedDelta > 0.005 || centroidShift > 0.005; + + // Engine received different value at low vs high? + const engineApplied = (sLow !== 'n/a' && sHigh !== 'n/a') && (sLow !== sHigh); + + log(` slider value low=${r.lowActual} def=${r.defActual} high=${r.highActual}`); + log(` engine.${engineKey} low=${sLow} def=${sDef} high=${sHigh}`); + log(` diag.${engineKey} low=${dLow} def=${dDef} high=${dHigh}`); + log(` speed low=${lowSp.toFixed(3)} def=${defSp.toFixed(3)} high=${highSp.toFixed(3)} dH-L=${speedDelta.toFixed(3)}`); + log(` radius low=${lowRa.toFixed(2)} def=${defRa.toFixed(2)} high=${highRa.toFixed(2)} dH-L=${radiusDelta.toFixed(2)}`); + log(` centroid low=(${lowCx?.toFixed(2)},${lowCy?.toFixed(2)}) high=(${highCx?.toFixed(2)},${highCy?.toFixed(2)}) shift=${centroidShift.toFixed(3)}`); + log(` engineApplied=${engineApplied} physicsChanged=${physicsChanged} -> ${(engineApplied && physicsChanged) ? 'ALIVE' : 'DEAD'}`); + + modeResults.sliders.push({ + id: t.id, engineKey, lowActual: r.lowActual, defActual: r.defActual, + highActual: r.highActual, sDef, sLow, sHigh, dDef, dLow, dHigh, + defSpeed: defSp, lowSpeed: lowSp, highSpeed: highSp, + defRadius: defRa, lowRadius: lowRa, highRadius: highRa, + centroidShift, engineApplied, physicsChanged, status: + (engineApplied && physicsChanged) ? 'alive' : 'DEAD', + }); + } + allResults.push(modeResults); + } + + // Final per-mode summary + log(`\n\n========================================`); + log(` PER-MODE SLIDER-ALIVENESS SUMMARY`); + log(`========================================`); + for (const m of allResults) { + log(`\n Mode: ${m.mode}`); + for (const s of m.sliders) { + const tag = s.status === 'alive' ? 'ALIVE' : (s.status === 'DEAD' ? 'DEAD ' : 'SKIP '); + log(` [${tag}] ${s.id.padEnd(38)} engine=${s.engineKey} ` + + `low=${s.sLow} def=${s.sDef} high=${s.sHigh} ` + + `radiusD=${(s.highRadius - s.lowRadius).toFixed(2)} ` + + `centroidD=${(s.centroidShift || 0).toFixed(3)}`); + } + } + + // Galaxy-specific: verify that the galaxy physics functions read the + // dashboard's normalized multipliers (we can read the engine settings + // while in galaxy mode and compare). + log(`\n\n========================================`); + log(` GALAXY-MODE SPACETIME CONSUMPTION CHECK`); + log(`========================================`); + log(`For galaxy mode the applyForces() function early-returns at line 7973.`); + log(`Galaxy physics consumes settings via galaxyIntegratorOptions() (line 8571)`); + log(`which calls galaxyPhysicsMultiplier(state.settings.X, fallback, maximum) with:`); + log(` gravitationalConstant -> maximum 8`); + log(` localGravitationalConstant -> maximum 8`); + log(` blackHoleMass -> maximum 16`); + log(`The dashboard ledger.js sends values in 0..2 range (divided by 100 for`); + log(`gravitationalConstant/localGravitationalConstant; graphBlackHoleMassMultiplier`); + log(`for blackHoleMass). All are accepted by the galaxy clamps (0..8 / 0..16).`); + + } catch (e) { + err(`Test failed: ${e.message}`); + err(e.stack); + exitCode = 2; + } finally { + if (browser) await browser.close(); + if (serverProc) serverProc.kill(); + } + process.exit(exitCode); +} + +main(); From c37ba0eb18408fe500cd70cd73fb5dcd7679b89c Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Fri, 4 Sep 2026 02:13:09 -0400 Subject: [PATCH 04/10] docs(changelog): replace em-dash with semicolon to satisfy docs punctuation policy --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d0ecacf..31b3d3e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,7 +51,7 @@ All notable changes to Engraphis are documented here. Format loosely follows regressions, MRR@5 lifts 0.889 -> 0.944 (sample) and 0.962 -> 0.981 (codemem), with ~15 ms per query added. Not the default; set the value in the trusted config file (`~/.engraphis/config.env` on the operator account, or as a - process environment variable) — Engraphis deliberately does not read the + process environment variable); Engraphis deliberately does not read the CWD `.env`, so editing `./.env` and restarting leaves the identity reranker active. Restart the MCP server and dashboard after the change. From d0606c2896bd6176756bdc0865748488c5df3677 Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Sat, 5 Sep 2026 05:55:21 -0400 Subject: [PATCH 05/10] feat: strengthen memory correctness and workspace processing controls --- .claude-plugin/skill-assets.sha256 | 2 +- .github/workflows/ci.yml | 2 +- AGENTS.md | 4 +- CHANGELOG.md | 22 + README.md | 15 +- docs/HOSTED_PLANS.md | 2 +- docs/HOSTING_RAILWAY.md | 4 +- docs/MCP_CONTRACT.json | 3592 +++++++++++++++++ docs/MCP_TOOLS.md | 14 + docs/PAID_EVALUATION_PROPOSAL.md | 78 + docs/RAILWAY_TEMPLATE.md | 10 +- docs/RELIABILITY_PROGRAM.md | 236 ++ docs/SYNC.md | 8 +- engraphis/backends/vector_numpy.py | 60 +- engraphis/backends/vector_sqlitevec.py | 59 +- engraphis/classic_assets/dashboard.js | 19 +- engraphis/classic_assets/index.html | 2 +- engraphis/cloud_features.py | 55 +- engraphis/commercial.py | 13 + engraphis/commercial_manifest.json | 13 +- engraphis/core/browsing.py | 131 + engraphis/core/context.py | 280 +- engraphis/core/engine.py | 324 +- engraphis/core/interfaces.py | 5 + engraphis/core/recall.py | 36 +- engraphis/core/resolve.py | 24 +- engraphis/core/schema.py | 43 +- engraphis/core/store.py | 136 +- engraphis/core/vector_repair.py | 45 + engraphis/core/vector_search.py | 55 + .../dashboard_assets/engraphis-graph-every.js | 5 +- engraphis/dashboard_assets/index.html | 27 +- engraphis/dashboard_assets/ledger.js | 267 +- .../dashboard_assets/managed-processing.js | 82 + engraphis/managed_processing.py | 104 + engraphis/mcp_server.py | 104 +- engraphis/routes/v2_api.py | 158 +- engraphis/service.py | 60 + engraphis/static/dashboard.js | 19 +- engraphis/static/index.html | 2 +- eval/datasets/resolver_write_acceptance.jsonl | 10 + eval/fts_insert_scaling.py | 102 + eval/native_coverage_scaling.py | 166 + eval/resolver_reworded_corrections.py | 150 +- eval/vector_scale.py | 45 + eval/vector_scale_storage.py | 429 ++ eval/vector_scan_plan.py | 141 + integrations/pi/index.ts | 3 + integrations/pi/src/generated-contract.ts | 517 +++ integrations/pi/src/tool-schemas.ts | 135 +- .../pi/test/mcp-client.integration.ts | 26 +- .../src/engraphis_prime_agent/_contract.py | 322 ++ .../src/engraphis_prime_agent/tools.py | 209 +- integrations/prime_agent/tests/test_tools.py | 13 +- playwright.config.js | 16 +- scripts/check_commercial_manifest.py | 13 +- scripts/export_mcp_contract.py | 65 + scripts/init.py | 157 +- scripts/installation_profile.py | 64 + scripts/update.py | 28 +- skills/engraphis-memory/references/TOOLS.md | 2 +- tests/e2e/commercial.spec.js | 40 +- tests/e2e/ledger.spec.js | 283 +- tests/e2e/workspace-smoke.spec.js | 48 + tests/test_cloud_features.py | 105 +- tests/test_context_economy.py | 5 +- tests/test_context_evidence_preservation.py | 165 + tests/test_context_packer.py | 489 +-- tests/test_context_packing.py | 22 +- tests/test_dashboard_auth_placement.py | 56 +- tests/test_dashboard_v2.py | 9 +- tests/test_documentation_contracts.py | 13 +- tests/test_engine.py | 2 +- tests/test_fts_insert_scaling.py | 13 + tests/test_graph_engine_asset.py | 4 +- tests/test_hosted_plan_resolution.py | 31 +- tests/test_init.py | 114 + tests/test_installation_profile.py | 54 + tests/test_managed_processing_policy.py | 345 ++ tests/test_manual_graph_probe.py | 40 + tests/test_mcp_contract.py | 29 + tests/test_mcp_server.py | 110 +- tests/test_memory_browsing.py | 134 + tests/test_native_coverage_equivalence.py | 60 + tests/test_obsidian_import_schema.py | 8 +- tests/test_pro_cta.py | 67 +- tests/test_recall.py | 41 + tests/test_resolver_acceptance.py | 107 + tests/test_storage_concurrency_repair.py | 397 ++ tests/test_store_fts_insert.py | 167 + tests/test_sync.py | 4 +- tests/test_update.py | 6 + tests/test_vector_numpy.py | 4 +- tests/test_vector_scale_storage.py | 104 + tests/test_vector_scan_plan.py | 15 + tests/test_vector_snapshot_plan.py | 81 + tests/test_vector_sqlitevec_backend.py | 106 + tools/galaxy_mode_test.js | 54 +- 98 files changed, 10555 insertions(+), 1507 deletions(-) create mode 100644 docs/MCP_CONTRACT.json create mode 100644 docs/PAID_EVALUATION_PROPOSAL.md create mode 100644 docs/RELIABILITY_PROGRAM.md create mode 100644 engraphis/core/browsing.py create mode 100644 engraphis/core/vector_repair.py create mode 100644 engraphis/core/vector_search.py create mode 100644 engraphis/dashboard_assets/managed-processing.js create mode 100644 engraphis/managed_processing.py create mode 100644 eval/datasets/resolver_write_acceptance.jsonl create mode 100644 eval/fts_insert_scaling.py create mode 100644 eval/native_coverage_scaling.py create mode 100644 eval/vector_scale_storage.py create mode 100644 eval/vector_scan_plan.py create mode 100644 integrations/pi/src/generated-contract.ts create mode 100644 integrations/prime_agent/src/engraphis_prime_agent/_contract.py create mode 100644 scripts/export_mcp_contract.py create mode 100644 scripts/installation_profile.py create mode 100644 tests/e2e/workspace-smoke.spec.js create mode 100644 tests/test_context_evidence_preservation.py create mode 100644 tests/test_fts_insert_scaling.py create mode 100644 tests/test_installation_profile.py create mode 100644 tests/test_managed_processing_policy.py create mode 100644 tests/test_manual_graph_probe.py create mode 100644 tests/test_mcp_contract.py create mode 100644 tests/test_memory_browsing.py create mode 100644 tests/test_native_coverage_equivalence.py create mode 100644 tests/test_resolver_acceptance.py create mode 100644 tests/test_storage_concurrency_repair.py create mode 100644 tests/test_store_fts_insert.py create mode 100644 tests/test_vector_scale_storage.py create mode 100644 tests/test_vector_scan_plan.py create mode 100644 tests/test_vector_snapshot_plan.py diff --git a/.claude-plugin/skill-assets.sha256 b/.claude-plugin/skill-assets.sha256 index 676b6d11..0cb8e64a 100644 --- a/.claude-plugin/skill-assets.sha256 +++ b/.claude-plugin/skill-assets.sha256 @@ -3,4 +3,4 @@ a8307092284d9ab4ba62f4f089d14a674c33d0f241a8430ffd89abb19e5f1ca0 .claude-plugin 4bc8979b9ffeb97190960e551dbf4ddc6f7aeeb7b86894fd2298a59ff0001efa skills/engraphis-memory/SKILL.md 055655db84af07561d002f0c69744313d8413c39f3e873f941f0fa0b1e76dc66 skills/engraphis-memory/references/CONVENTIONS.md 62019760766ff472a76a0f81437898f39e3c1fe2631732b7b7733e50c1ad837f skills/engraphis-memory/references/SCOPING.md -cac53452a62bf34759eae1f4c325d98aad8d9909ba0d188074f09cd6e6df3a58 skills/engraphis-memory/references/TOOLS.md +33874c7c7a1c0911b0e73c7d22addc9828963d5436cb315fe7c6c5587c6b911d skills/engraphis-memory/references/TOOLS.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6d058e78..0119283a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -256,7 +256,7 @@ jobs: python -m pip install --upgrade pip pip install -e ".[test]" "uvicorn[standard]>=0.29" npm ci --ignore-scripts --omit=optional - npx playwright install --with-deps chromium + npx playwright install --with-deps chromium firefox webkit - name: Audit the root browser dependency lock run: npm audit --audit-level=high - name: Playwright desktop/mobile, keyboard, CSP, console, and axe checks diff --git a/AGENTS.md b/AGENTS.md index c5f52909..eda74b76 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,7 +29,7 @@ most common mistake here. | Status | Primary scoped, bi-temporal, interface-driven implementation. | Compatibility/reference implementation with flat namespaces. | | Model | Scoped + bi-temporal + typed; interface-driven. | Single flat `namespace` string per memory. | | Code | `engraphis/core/`, `engraphis/backends/`, `eval/`, `tests/`, `scripts/migrate_to_v2.py` | `engraphis/app.py`, `config.py`, `models.py`, `routes/`, `stores/`, `engines/`, `llm/`, `static/` | -| Data | new v2 schema (`SCHEMA_VERSION = 16`) | `engraphis_v1.db` | +| Data | new v2 schema (`SCHEMA_VERSION = 17`) | `engraphis_v1.db` | | Entry | `engraphis.MemoryEngine.create()` / `engraphis.create_memory_engine()` → `engraphis/factory.py` → `core/engine.py` | Internal reference only; never a public launcher | **Rule:** build new capability on **v2** (`core/` + `backends/`) behind the interfaces. @@ -210,7 +210,7 @@ These are pure, unit-tested functions — change them only with a corresponding --- -## 5. Data model cheat-sheet (`core/interfaces.py`, `core/schema.py` — `SCHEMA_VERSION = 16`) +## 5. Data model cheat-sheet (`core/interfaces.py`, `core/schema.py` — `SCHEMA_VERSION = 17`) - **Scope hierarchy:** `workspace → repo → session → memory`. Scopes: `session|repo|workspace|user`. - **Bi-temporal validity on every record:** world-time `valid_from/valid_to` + diff --git a/CHANGELOG.md b/CHANGELOG.md index 31b3d3e5..2ae6b976 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,28 @@ All notable changes to Engraphis are documented here. Format loosely follows [Keep a Changelog](https://keepachangelog.com/); versions use SemVer. +## [Unreleased] + +### Reliability and privacy + +- Preserve distinct context claims, qualified sentences and complete units under tight budgets; + measure false NOOP outcomes through real write sequences. +- Preserve separate sources during packing and keep MCP gist responses within the canonical + context budget. Response caps retain or omit complete context and report accurate usage. +- Canonical temporal browsing, server-side Library filtering/pagination, independent Ask states, + actionable setup diagnostics and retained installation capabilities. +- Cross-process write resolution and schema 17 durable vector-index repair, with canonical + fallback and bounded NumPy scans. Public engine entrypoints remain compatible. +- Commit native batch indexing with canonical memory state and roll back both on failure. + Retain the established 12,000-memory graph window pending quality evidence for a smaller one. +- Explicit workspace managed-processing approval; missing legacy policy pauses readable uploads. + Requires the compatible cloud migration before rollout. Encrypted sync remains separate. +- Generated Smart/Classic MCP contract and integration inputs; Pro three-day and Team ten-day + trial copy aligned with cloud authority. Real browser and Workers evidence remains distinct + from production verification. See `docs/RELIABILITY_PROGRAM.md`. +- Isolate the manual graph diagnostic on an available local port with a private in-memory + server; fail before contacting an existing service when the requested port is occupied. + ## [1.7.1] - 2026-09-03 ### Fixed diff --git a/README.md b/README.md index 4ac7de0d..f66b629e 100644 --- a/README.md +++ b/README.md @@ -804,7 +804,7 @@ file. It never searches the working directory for `.env`, and explicit process v | `ENGRAPHIS_CLOUD_REFRESH_CREDENTIAL` | Not set | Bootstrap-only rotating hosted credential; after first use the owner-only cloud session replacement takes precedence | | `ENGRAPHIS_CLOUD_TOKEN_SUBJECT` | `member` | Subject fixed during hosted bootstrap (`device` or `member`); set explicitly with an environment-only refresh credential | | `ENGRAPHIS_CLOUD_ACCESS_TOKEN` | Not set | Optional short-lived access token for ephemeral jobs | -| `ENGRAPHIS_MANAGED_COMPUTE_CONSENT` | *(auto)* | Operator override only; default follows whether a cloud session is configured (connected = allowed, local-only = never). `0` opts a connected installation out; `1` permits local snapshot preparation but does not create a cloud credential or authorize an upload | +| `ENGRAPHIS_MANAGED_COMPUTE_CONSENT` | *(unset)* | Deny-only operator override: `0` pauses readable managed processing. A truthy value cannot grant approval. Each workspace requires explicit confirmation in Manage → Settings; encrypted sync is separate | The optional cross-encoder reranker is model- and hardware-dependent. Treat its quality and latency as deployment-specific until a versioned model identity, exact configuration, and @@ -869,3 +869,16 @@ under Apache-2.0 keeps that grant; later releases cannot retroactively withdraw official hosted control plane, its production credentials and records, managed operations, support, and future separately delivered commercial modules are outside the public source grant. See [`docs/LICENSING.md`](https://github.com/Coding-Dev-Tools/engraphis/blob/main/docs/LICENSING.md) for the complete boundary. + +### Reliability implementation candidate + +The current source uses schema 17 for durable, content-free vector-index repair. +See [the reliability program](https://github.com/Coding-Dev-Tools/engraphis/blob/main/docs/RELIABILITY_PROGRAM.md) for exact implementation, +validation, migration and release boundaries. Managed processing now requires explicit +workspace approval in Manage → Settings. Existing installations start with readable +uploads paused until confirmed; connecting an account does not grant approval. + +For setup diagnostics use `engraphis-init --check --json`. New configurations get an +owner-private local API token. Existing configs are preserved. Record selected install +capabilities with `engraphis-init --extras server,mcp` or `--extras none`; future updates +preserve that choice. `ENGRAPHIS_UPDATE_EXTRAS` remains an explicit override. diff --git a/docs/HOSTED_PLANS.md b/docs/HOSTED_PLANS.md index a4899f50..185f2b43 100644 --- a/docs/HOSTED_PLANS.md +++ b/docs/HOSTED_PLANS.md @@ -23,7 +23,7 @@ implementations are not part of this repository. Start or manage a hosted subscription in the [Engraphis account portal](https://api.engraphis.com/account?plan=pro&interval=monthly&utm_source=engraphis&utm_medium=docs&utm_campaign=pro_conversion&utm_content=hosted_plans_pricing#billing). -The email-confirmed, no-card trial lasts three active days. If hosted entitlement expires, +The email-confirmed, no-card trial lasts three active days for Pro and ten active days for Team. If hosted entitlement expires, `workspace_write_grace` can retain only approved hosted-account continuity operations for up to 24 hours. It does not extend a trial or subscription, grant cloud access, or affect the free local tools. `recovery_read_only` supports hosted account recovery and export after grace. diff --git a/docs/HOSTING_RAILWAY.md b/docs/HOSTING_RAILWAY.md index b6cdd1fa..df588076 100644 --- a/docs/HOSTING_RAILWAY.md +++ b/docs/HOSTING_RAILWAY.md @@ -50,8 +50,8 @@ Prefer mounting the owner-only cloud session file rather than placing a rotating credential directly in deployment configuration. An injected environment credential is only the bootstrap value; after rotation, the owner-only saved replacement takes precedence. **Cloud Sync encrypts eligible shared-workspace changes end-to-end before they leave the device; Engraphis -Cloud cannot read their contents.** Managed compute is separate: once connected, it is enabled by -default for an authorized customer and may upload a readable snapshot capped at 16 MiB over HTTPS +Cloud cannot read their contents.** Managed compute is separate: every workspace must be +explicitly approved in Manage → Settings before a readable snapshot capped at 16 MiB may upload over HTTPS to produce results. Secret-class and session-scoped rows are excluded client-side, and secret-class rows are rejected server-side. Set `ENGRAPHIS_MANAGED_COMPUTE_CONSENT=0` to opt the deployed installation back out. diff --git a/docs/MCP_CONTRACT.json b/docs/MCP_CONTRACT.json new file mode 100644 index 00000000..77df85ea --- /dev/null +++ b/docs/MCP_CONTRACT.json @@ -0,0 +1,3592 @@ +{ + "schema": "engraphis-mcp-contract/v1", + "sha256": "47af699b0c207132da7b663caa085d5579d573ee8d06e5ce06d4b16a2681002b", + "surfaces": { + "classic": [ + { + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Grounded answer (compatibility alias)" + }, + "description": "Backward-compatible alias for ``engraphis_recall_grounded``.\n\n Kept so existing agent configs that adopted the answer tool continue to work; new\n integrations should prefer ``engraphis_recall_grounded`` for the clearer name.\n ", + "inputSchema": { + "properties": { + "as_of": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional Unix timestamp for a point-in-time grounded answer. Omit for now.", + "title": "As Of" + }, + "candidate_depth": { + "default": "fixed", + "description": "fixed preserves the legacy pool; adaptive is profile-aware and opt-in.", + "title": "Candidate Depth", + "type": "string" + }, + "diagnostics": { + "default": false, + "description": "Include detailed retrieval scoring trace.", + "title": "Diagnostics", + "type": "boolean" + }, + "k": { + "default": 8, + "description": "Max memories to consider (1-50).", + "maximum": 50, + "minimum": 1, + "title": "K", + "type": "integer" + }, + "known_at": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional system-time Unix timestamp.", + "title": "Known At" + }, + "max_response_tokens": { + "anyOf": [ + { + "maximum": 1000000, + "minimum": 2, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Cap the total serialized response to this many tokens (minimum 2).", + "title": "Max Response Tokens" + }, + "min_support": { + "default": 0.25, + "description": "Absolute support floor 0..1. Memories below this don't count as evidence.", + "maximum": 1.0, + "minimum": 0.0, + "title": "Min Support", + "type": "number" + }, + "mtype_limits": { + "anyOf": [ + { + "additionalProperties": { + "type": "integer" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional maximum returned count per memory type.", + "title": "Mtype Limits" + }, + "planning": { + "default": "off", + "description": "off preserves single-query recall; auto enables bounded planning.", + "title": "Planning", + "type": "string" + }, + "query": { + "description": "The question to answer from memory.", + "maxLength": 10000, + "minLength": 1, + "title": "Query", + "type": "string" + }, + "repo": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Repository scope within the workspace.", + "title": "Repo" + }, + "response_mode": { + "default": "full", + "description": "full includes citation bodies; compact omits them.", + "title": "Response Mode", + "type": "string" + }, + "retrieval_profile": { + "default": "balanced", + "description": "balanced, fast, auto, lexical, graph, or code.", + "title": "Retrieval Profile", + "type": "string" + }, + "synthesize": { + "default": false, + "description": "If true, ask configured LLM for cited prose; otherwise deterministic/extractive.", + "title": "Synthesize", + "type": "boolean" + }, + "token_budget": { + "anyOf": [ + { + "maximum": 32768, + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Hard packed-context budget (0-32768).", + "title": "Token Budget" + }, + "valid_at": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional world-time Unix timestamp (must match as_of if both are set).", + "title": "Valid At" + }, + "workspace": { + "default": "default", + "description": "Workspace to search.", + "maxLength": 200, + "minLength": 1, + "title": "Workspace", + "type": "string" + } + }, + "required": [ + "query" + ], + "title": "engraphis_answerArguments", + "type": "object" + }, + "name": "engraphis_answer" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true, + "readOnlyHint": false, + "title": "Check for an Engraphis update" + }, + "description": "Report whether a newer Engraphis release is available, so an agent can proactively\n remind the user to upgrade.\n\n Cached ~24h and fail-silent; honors ``ENGRAPHIS_UPDATE_CHECK=0`` (then ``enabled`` is\n false). The default GitHub source is overridable via ``ENGRAPHIS_UPDATE_URL``. A stale\n lookup refreshes the persistent cache, and ``force=true`` rewrites it on every call,\n so this open-world tool is neither read-only nor idempotent.\n\n Returns:\n str: JSON ``{\"enabled\",\"current\",\"latest\",\"update_available\",\"url\",\"notice\"}``.\n ", + "inputSchema": { + "properties": { + "force": { + "default": false, + "description": "Bypass the ~24h cache and re-check the release source now.", + "title": "Force", + "type": "boolean" + } + }, + "title": "engraphis_check_updateArguments", + "type": "object" + }, + "name": "engraphis_check_update" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false, + "readOnlyHint": true, + "title": "Estimate change impact from the code graph" + }, + "description": "Estimate affected symbols, callers, memories, graph communities, and risk.", + "inputSchema": { + "properties": { + "as_of": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Compatibility alias for valid_at.", + "title": "As Of" + }, + "changed_files": { + "description": "Repo-relative files changed by a diff or pull request.", + "items": { + "type": "string" + }, + "maxItems": 2000, + "minItems": 1, + "title": "Changed Files", + "type": "array" + }, + "known_at": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional system-time Unix timestamp.", + "title": "Known At" + }, + "repo": { + "description": "Indexed repo to analyze.", + "maxLength": 200, + "minLength": 1, + "title": "Repo", + "type": "string" + }, + "valid_at": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional world-time Unix timestamp.", + "title": "Valid At" + }, + "workspace": { + "description": "Workspace the repo belongs to.", + "maxLength": 200, + "minLength": 1, + "title": "Workspace", + "type": "string" + } + }, + "required": [ + "changed_files", + "workspace", + "repo" + ], + "title": "engraphis_code_impactArguments", + "type": "object" + }, + "name": "engraphis_code_impact" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false, + "readOnlyHint": true, + "title": "Find a path through the code graph" + }, + "description": "Return the shortest best-effort path between two code nodes.\n\n The path can cross definition, call, import, and symbol-alias edges. It is structural\n and name-based rather than type-resolved, so treat it as impact evidence rather than\n a compiler proof.\n ", + "inputSchema": { + "properties": { + "as_of": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Compatibility alias for valid_at.", + "title": "As Of" + }, + "known_at": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional system-time Unix timestamp.", + "title": "Known At" + }, + "max_depth": { + "default": 8, + "description": "Maximum graph hops (1-32).", + "maximum": 32, + "minimum": 1, + "title": "Max Depth", + "type": "integer" + }, + "repo": { + "description": "Indexed repo to traverse.", + "maxLength": 200, + "minLength": 1, + "title": "Repo", + "type": "string" + }, + "source": { + "description": "Source symbol, qualified name, or indexed file.", + "maxLength": 500, + "minLength": 1, + "title": "Source", + "type": "string" + }, + "target": { + "description": "Target symbol, qualified name, or indexed file.", + "maxLength": 500, + "minLength": 1, + "title": "Target", + "type": "string" + }, + "valid_at": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional world-time Unix timestamp.", + "title": "Valid At" + }, + "workspace": { + "description": "Workspace the repo belongs to.", + "maxLength": 200, + "minLength": 1, + "title": "Workspace", + "type": "string" + } + }, + "required": [ + "source", + "target", + "workspace", + "repo" + ], + "title": "engraphis_code_pathArguments", + "type": "object" + }, + "name": "engraphis_code_path" + }, + { + "annotations": { + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Consolidate memories (sleep-time sweep)" + }, + "description": "Run one sleep-time consolidation sweep: recurring episodic memories on the same\n subject are distilled into one durable semantic digest (linked to its sources), and\n fully-decayed transient memories are archived (bi-temporally closed \u2014 never deleted,\n always audited, pinned memories exempt). Already-consolidated sources are skipped on\n retries. With ``profiles=True`` each entity's memories are also rolled into one durable\n profile digest. With ``structured=True`` a configured LLM may produce schema-validated\n facts/entities/relations; provider/schema failure falls back to the deterministic\n digest. A structured result may cite only part of a large cluster, allowing an\n identical later call to process the remainder, so the overall tool is conservatively\n non-idempotent. Good moments to call it: session end, or on a schedule. A real sweep\n requires explicit local-operator confirmation (``confirmed=true``); a ``dry_run``\n report does not mutate and needs none.\n\n Returns:\n str: JSON report ``{\"clusters_found\",\"digests_created\",\"archived\",\n \"skipped_already_consolidated\",\"compaction\",\"dry_run\"}`` \u2014 ``compaction`` reports\n the context tokens the sweep saved. With ``profiles=True`` a ``profiles`` block is\n added (``entities_considered``, ``profiles_created``, ``compaction``).\n ", + "inputSchema": { + "properties": { + "confirmed": { + "default": false, + "description": "Explicit local-operator confirmation: must be true for a real (non-dry-run) sweep, which archives and distills governed state. Dry runs need no confirmation.", + "title": "Confirmed", + "type": "boolean" + }, + "dry_run": { + "default": true, + "description": "If true (default), only report what would happen \u2014 recommended before the first real run.", + "title": "Dry Run", + "type": "boolean" + }, + "profiles": { + "default": false, + "description": "Also roll each entity's scattered memories into one durable profile digest (needs graph entities). Report lands under 'profiles'.", + "title": "Profiles", + "type": "boolean" + }, + "repo": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Restrict to this repo.", + "title": "Repo" + }, + "structured": { + "default": false, + "description": "If true, use configured LLM for schema-validated consolidation facts/entities/relations; falls back to deterministic digest on any failure.", + "title": "Structured", + "type": "boolean" + }, + "workspace": { + "description": "Workspace to consolidate.", + "maxLength": 200, + "minLength": 1, + "title": "Workspace", + "type": "string" + } + }, + "required": [ + "workspace" + ], + "title": "engraphis_consolidateArguments", + "type": "object" + }, + "name": "engraphis_consolidate" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false, + "readOnlyHint": true, + "title": "Summarize context savings" + }, + "description": "Summarize receipt-backed context savings with optional time/release filters.", + "inputSchema": { + "properties": { + "format": { + "anyOf": [ + { + "maxLength": 16, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Output format: 'json' (default) or 'csv'.", + "title": "Format" + }, + "from_ts": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional inclusive Unix timestamp.", + "title": "From Ts" + }, + "group_by": { + "anyOf": [ + { + "maxLength": 32, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Group results by dimension: workspace, repo, agent, or day.", + "title": "Group By" + }, + "release_version": { + "anyOf": [ + { + "maxLength": 64, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional semantic release filter.", + "title": "Release Version" + }, + "repo": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional repo scope within the workspace.", + "title": "Repo" + }, + "to_ts": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional exclusive Unix timestamp.", + "title": "To Ts" + }, + "workspace": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional workspace whose receipt usage to summarize. Omit to aggregate all visible workspaces.", + "title": "Workspace" + } + }, + "title": "engraphis_context_savingsArguments", + "type": "object" + }, + "name": "engraphis_context_savings" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Correct a memory" + }, + "description": "Replace a memory's content without losing history: the old content is closed\n (bi-temporal invalidate, not deleted) and the correction is stored as a new memory\n that records what it corrects \u2014 so the audit trail and ``engraphis_why`` both still\n work afterward. Prefer this over retire+remember for fixes.\n\n Returns:\n str: JSON ``{\"id\",\"superseded\":[old_id],\"reason\"}`` or an actionable error if the\n id is unknown or doesn't belong to ``workspace``/``repo``.\n ", + "inputSchema": { + "properties": { + "memory_id": { + "description": "The memory id to correct.", + "maxLength": 200, + "minLength": 1, + "title": "Memory Id", + "type": "string" + }, + "new_content": { + "description": "The corrected content.", + "maxLength": 100000, + "minLength": 1, + "title": "New Content", + "type": "string" + }, + "reason": { + "default": "", + "description": "Why this is being corrected (e.g. 'typo', 'the user clarified').", + "maxLength": 1000, + "title": "Reason", + "type": "string" + }, + "repo": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Repo that owns this memory, if it's repo-scoped; also checked.", + "title": "Repo" + }, + "workspace": { + "description": "Workspace that owns this memory \u2014 checked against the memory's actual workspace before anything is changed.", + "maxLength": 200, + "minLength": 1, + "title": "Workspace", + "type": "string" + } + }, + "required": [ + "memory_id", + "new_content", + "workspace" + ], + "title": "engraphis_correctArguments", + "type": "object" + }, + "name": "engraphis_correct" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false, + "readOnlyHint": false, + "title": "End a memory session" + }, + "description": "Close a session with a summary/outcome so the next session can pick up the thread.\n An identical retry is an atomic no-op; a retry with a conflicting handoff is rejected,\n so this tool remains idempotent.\n\n Returns:\n str: JSON ``{\"session_id\",\"status\":\"summarized\",\"summary\",\"open_threads\"}`` or\n ``\"Error: ...\"`` if the session id is unknown.\n ", + "inputSchema": { + "properties": { + "open_threads": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Unresolved items to carry into the next session for the same user and agent in this repo (e.g. 'tests 3-5 still failing').", + "title": "Open Threads" + }, + "outcome": { + "default": "", + "description": "Short outcome label (e.g. 'shipped', 'blocked').", + "maxLength": 1000, + "title": "Outcome", + "type": "string" + }, + "session_id": { + "description": "Session id from engraphis_start_session.", + "maxLength": 200, + "minLength": 1, + "title": "Session Id", + "type": "string" + }, + "summary": { + "default": "", + "description": "Summary of what happened, stored for resume.", + "maxLength": 100000, + "title": "Summary", + "type": "string" + } + }, + "required": [ + "session_id" + ], + "title": "engraphis_end_sessionArguments", + "type": "object" + }, + "name": "engraphis_end_session" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false, + "readOnlyHint": true, + "title": "Export the indexed code graph" + }, + "description": "Export portable graph JSON plus a human-readable Markdown report.", + "inputSchema": { + "properties": { + "as_of": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Compatibility alias for valid_at.", + "title": "As Of" + }, + "known_at": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional system-time Unix timestamp.", + "title": "Known At" + }, + "repo": { + "description": "Indexed repo to export.", + "maxLength": 200, + "minLength": 1, + "title": "Repo", + "type": "string" + }, + "valid_at": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional world-time Unix timestamp.", + "title": "Valid At" + }, + "workspace": { + "description": "Workspace the repo belongs to.", + "maxLength": 200, + "minLength": 1, + "title": "Workspace", + "type": "string" + } + }, + "required": [ + "workspace", + "repo" + ], + "title": "engraphis_export_code_graphArguments", + "type": "object" + }, + "name": "engraphis_export_code_graph" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false, + "readOnlyHint": true, + "title": "Export operation receipts" + }, + "description": "Export the complete public receipt payload and its verification result.", + "inputSchema": { + "properties": { + "workspace": { + "description": "Workspace whose receipts to export.", + "maxLength": 200, + "minLength": 1, + "title": "Workspace", + "type": "string" + } + }, + "required": [ + "workspace" + ], + "title": "engraphis_export_receiptsArguments", + "type": "object" + }, + "name": "engraphis_export_receipts" + }, + { + "annotations": { + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Forget a memory (deprecated; use retire)" + }, + "description": "Retire-with-history (deprecated compatibility alias for ``engraphis_retire``).\n\n It preserves the legacy ``status: \"forgotten\"`` response for existing clients;\n it still performs a temporal retirement and never deletes the memory. For\n irreversible removal of a leaked secret use ``engraphis_secure_erase`` with\n explicit confirmation instead.\n ", + "inputSchema": { + "properties": { + "confirmed": { + "default": false, + "description": "Explicit local-operator confirmation: must be true, as for engraphis_retire.", + "title": "Confirmed", + "type": "boolean" + }, + "memory_id": { + "description": "Retire-with-history id (from a prior remember/recall result, e.g. 'mem_01J...'). Deprecated alias for memory_id in engraphis_retire.", + "maxLength": 200, + "minLength": 1, + "title": "Memory Id", + "type": "string" + }, + "reason": { + "default": "", + "description": "Retirement reason recorded in the audit trail.", + "maxLength": 1000, + "title": "Reason", + "type": "string" + }, + "repo": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional owning repo.", + "title": "Repo" + }, + "workspace": { + "description": "Workspace that owns this memory.", + "maxLength": 200, + "minLength": 1, + "title": "Workspace", + "type": "string" + } + }, + "required": [ + "memory_id", + "workspace" + ], + "title": "engraphis_forgetArguments", + "type": "object" + }, + "name": "engraphis_forget" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Index a repository's code graph" + }, + "description": "Parse a repository into the code symbol graph: function/class/method definitions\n plus best-effort calls/imports edges. Run this once when you start working in a repo\n (or after large changes) so ``engraphis_search_code`` has something to search \u2014 uses\n AST parsing (tree-sitter) when available, a dependency-free regex fallback otherwise.\n Supported languages: Python, JavaScript, TypeScript, C#, C, and C++.\n\n Build/dependency directories (node_modules, bin, obj, target, .venv, \u2026) are skipped\n while walking, so a large non-Python repo indexes quickly instead of appearing to\n hang; add a ``.engraphisignore`` file (gitignore-style) at the repo root to skip\n project-specific generated files.\n\n Creates the workspace/repo if you haven't named them before (like\n engraphis_remember). Re-indexing is safe to call again; each file's symbols are\n replaced, not duplicated. Reads files from ``root_path`` on the local filesystem \u2014\n the same trust boundary as any other local tool you have, nothing is sent anywhere.\n Set ``ENGRAPHIS_INDEX_ROOTS`` to a path-separator-delimited absolute-path allow-list when\n repositories live outside the working, home, or temporary directories, or to narrow the\n defaults. Each completed scan appends a fresh operation receipt, so the MCP call is\n non-idempotent even when the code graph itself is unchanged.\n\n Returns:\n str: JSON ``{\"files_indexed\",\"symbols\",\"edges\",\"backend\"}``.\n ", + "inputSchema": { + "properties": { + "languages": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Restrict to these languages (e.g. ['python','csharp']). Names are normalised ('C#'->csharp, 'cpp'/'c++'->cpp). An unsupported name returns an error listing what's supported, instead of silently indexing nothing. Omit to index every supported language found.", + "title": "Languages" + }, + "repo": { + "description": "Repo name to index.", + "maxLength": 200, + "minLength": 1, + "title": "Repo", + "type": "string" + }, + "root_path": { + "description": "Local filesystem path to the repo root to parse (e.g. '/home/user/projects/myrepo'). The path must be inside the local defaults or ENGRAPHIS_INDEX_ROOTS allow-list.", + "maxLength": 4000, + "minLength": 1, + "title": "Root Path", + "type": "string" + }, + "workspace": { + "description": "Workspace the repo belongs to.", + "maxLength": 200, + "minLength": 1, + "title": "Workspace", + "type": "string" + } + }, + "required": [ + "workspace", + "repo", + "root_path" + ], + "title": "engraphis_index_repoArguments", + "type": "object" + }, + "name": "engraphis_index_repo" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Ingest raw text (extract facts first)" + }, + "description": "Store raw text without hand-distilling it first \u2014 the extract-then-remember path.\n\n Prefer ``engraphis_remember`` when you already have a crisp fact; use this when you\n have a blob (transcript, notes, long status update) and want Engraphis to break it\n into separate, individually-recallable memories. Each extracted fact goes through\n the same conflict resolution and evolution as a normal remember.\n\n Returns:\n str: JSON ``{\"workspace\",\"repo\",\"count\",\"extracted\",\"facts\":[{\"id\",\"op\",...}]}``\n where ``extracted`` is false when no extractor is configured (passthrough).\n ", + "inputSchema": { + "properties": { + "content": { + "description": "Raw, undistilled text: a conversation excerpt, meeting notes, a log, a long update. Engraphis extracts the discrete facts worth keeping (when an extractor is configured via ENGRAPHIS_EXTRACTOR=llm or llm_structured) and stores each one; otherwise stores the text as one memory.", + "maxLength": 100000, + "minLength": 1, + "title": "Content", + "type": "string" + }, + "mtype": { + "default": "semantic", + "description": "Default memory type for facts the extractor doesn't classify: semantic/episodic/procedural/working.", + "title": "Mtype", + "type": "string" + }, + "repo": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Repository scope within the workspace.", + "title": "Repo" + }, + "scope": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Visibility: session, repo, workspace, or user. Omit to infer the compatible default: repo when repo or a repo-backed session_id is present, otherwise workspace. Session visibility must be explicit.", + "title": "Scope" + }, + "session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Session id from engraphis_start_session, if any.", + "title": "Session Id" + }, + "workspace": { + "description": "Top-level scope, e.g. an org or product name ('acme').", + "maxLength": 200, + "minLength": 1, + "title": "Workspace", + "type": "string" + } + }, + "required": [ + "content", + "workspace" + ], + "title": "engraphis_ingestArguments", + "type": "object" + }, + "name": "engraphis_ingest" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true, + "readOnlyHint": false, + "title": "Ingest a live PostgreSQL schema" + }, + "description": "Convert tables, columns, constraints, and foreign keys into a schema memory and\n entity graph. Requires the optional psycopg backend. An exact retry reuses its live\n point-in-time schema snapshot, but every invocation appends audit/receipt records,\n so the tool as a whole is not idempotent.", + "inputSchema": { + "properties": { + "dsn": { + "description": "PostgreSQL connection string. It is used for this connection only and is never stored or returned.", + "maxLength": 4000, + "minLength": 1, + "title": "Dsn", + "type": "string" + }, + "repo": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional repository scope for an application-owned database.", + "title": "Repo" + }, + "schemas": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional schema allow-list; omit to inspect all non-system schemas.", + "title": "Schemas" + }, + "workspace": { + "description": "Workspace for the schema memory.", + "maxLength": 200, + "minLength": 1, + "title": "Workspace", + "type": "string" + } + }, + "required": [ + "dsn", + "workspace" + ], + "title": "engraphis_ingest_postgres_schemaArguments", + "type": "object" + }, + "name": "engraphis_ingest_postgres_schema" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Link two memories" + }, + "description": "Explicitly connect two memories (A-MEM-style linking) \u2014 use when you notice two\n stored facts are related but a plain recall wouldn't surface that connection, e.g. a\n bug report and the memory describing its fix.\n\n Returns:\n str: JSON ``{\"a\",\"b\",\"relation\",\"layer\",\"reason\",\"linked\":true,\"receipt\":...}``\n or an actionable error if either id is unknown or doesn't belong to\n ``workspace``/``repo``.\n ", + "inputSchema": { + "properties": { + "a": { + "description": "First memory id.", + "maxLength": 200, + "minLength": 1, + "title": "A", + "type": "string" + }, + "b": { + "description": "Second memory id.", + "maxLength": 200, + "minLength": 1, + "title": "B", + "type": "string" + }, + "layer": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional logical graph layer: temporal, entity, causal, or semantic. Omit to infer it from the relationship label.", + "title": "Layer" + }, + "reason": { + "default": "", + "description": "Optional rationale or context for why this relationship exists.", + "maxLength": 500, + "title": "Reason", + "type": "string" + }, + "relation": { + "default": "related", + "description": "Relationship label, e.g. 'related', 'caused_by', 'fixed_by'.", + "maxLength": 200, + "title": "Relation", + "type": "string" + }, + "repo": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Repo that owns both memories, if repo-scoped; also checked.", + "title": "Repo" + }, + "workspace": { + "description": "Workspace that owns both memories \u2014 checked against each memory's actual workspace before linking.", + "maxLength": 200, + "minLength": 1, + "title": "Workspace", + "type": "string" + } + }, + "required": [ + "a", + "b", + "workspace" + ], + "title": "engraphis_linkArguments", + "type": "object" + }, + "name": "engraphis_link" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Link a code symbol to a memory" + }, + "description": "Manually create a link between a code symbol and a memory.\n\n Use this when automatic indexing misses a relationship you know about \u2014 for example,\n linking a deployment function to the incident memory it resolved, or connecting a\n config constant to the decision that set its value. The link is idempotent: repeating\n the same call returns the existing link without duplication.\n\n Returns:\n str: JSON ``{\"link_id\",\"symbol_id\",\"memory_id\",\"relation\",\"workspace\",\"repo\",\"receipt\"}``.\n ", + "inputSchema": { + "properties": { + "confidence": { + "default": 1.0, + "description": "Link confidence 0..1.", + "maximum": 1.0, + "minimum": 0.0, + "title": "Confidence", + "type": "number" + }, + "memory_id": { + "description": "Memory ID to link to the symbol.", + "maxLength": 500, + "minLength": 1, + "title": "Memory Id", + "type": "string" + }, + "reason": { + "default": "", + "description": "Optional reason or context for this link.", + "maxLength": 500, + "title": "Reason", + "type": "string" + }, + "relation": { + "default": "mentions", + "description": "Relationship type (e.g. 'mentions', 'implements', 'fixes'). Defaults to 'mentions'.", + "maxLength": 100, + "title": "Relation", + "type": "string" + }, + "repo": { + "description": "Indexed repo containing the symbol.", + "maxLength": 200, + "minLength": 1, + "title": "Repo", + "type": "string" + }, + "symbol_id": { + "description": "Symbol ID, short name, or fully-qualified name from an indexed repo.", + "maxLength": 500, + "minLength": 1, + "title": "Symbol Id", + "type": "string" + }, + "workspace": { + "description": "Workspace the repo belongs to.", + "maxLength": 200, + "minLength": 1, + "title": "Workspace", + "type": "string" + } + }, + "required": [ + "symbol_id", + "memory_id", + "workspace", + "repo" + ], + "title": "engraphis_link_symbolArguments", + "type": "object" + }, + "name": "engraphis_link_symbol" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Pin or unpin a memory" + }, + "description": "Mark a memory as important enough to exempt from automatic decay/pruning \u2014 use for\n durable conventions or identity facts that must never silently fade.\n Every pin/unpin request is audited, including an identical retry, so the MCP call is\n deliberately annotated as non-idempotent even when the boolean value is unchanged.\n\n Returns:\n str: JSON ``{\"id\",\"pinned\"}`` or an actionable error if the id is unknown or doesn't\n belong to ``workspace``/``repo``.\n ", + "inputSchema": { + "properties": { + "memory_id": { + "description": "The memory id to pin/unpin.", + "maxLength": 200, + "minLength": 1, + "title": "Memory Id", + "type": "string" + }, + "pinned": { + "default": true, + "description": "True to pin (protect from future automatic decay/pruning), false to unpin.", + "title": "Pinned", + "type": "boolean" + }, + "repo": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Repo that owns this memory, if it's repo-scoped; also checked.", + "title": "Repo" + }, + "workspace": { + "description": "Workspace that owns this memory \u2014 checked against the memory's actual workspace before anything is changed.", + "maxLength": 200, + "minLength": 1, + "title": "Workspace", + "type": "string" + } + }, + "required": [ + "memory_id", + "workspace" + ], + "title": "engraphis_pinArguments", + "type": "object" + }, + "name": "engraphis_pin" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Agent-ready proactive context" + }, + "description": "Return an agent-ready context packet before the agent knows what to ask.\n\n Combines proactive recall, optional task-specific recall, and last-session handoff\n into a cited ``context_summary`` plus ``suggested_queries``. Deterministic by\n default; LLM synthesis is opt-in and accepted only when it cites source memories.\n When ``task`` or ``agent_state`` is supplied, the task-specific recall appends a\n privacy-safe receipt (without reinforcing memories), so the tool is conservatively\n annotated as mutating and non-idempotent.\n ", + "inputSchema": { + "properties": { + "agent_state": { + "default": "", + "description": "Optional current agent state: plan, open files, errors, partial findings.", + "maxLength": 20000, + "title": "Agent State", + "type": "string" + }, + "k": { + "default": 10, + "description": "Max memories to consider (1-50).", + "maximum": 50, + "minimum": 1, + "title": "K", + "type": "integer" + }, + "repo": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Repo scope within the workspace.", + "title": "Repo" + }, + "response_mode": { + "default": "full", + "description": "full preserves the Classic response; compact returns one packed context packet.", + "pattern": "^(full|compact)$", + "title": "Response Mode", + "type": "string" + }, + "synthesize": { + "default": false, + "description": "If true and an LLM is configured, synthesize a concise cited context summary; otherwise deterministic/offline.", + "title": "Synthesize", + "type": "boolean" + }, + "task": { + "default": "", + "description": "Current task/goal. Used to bias recall and frame the summary.", + "maxLength": 10000, + "title": "Task", + "type": "string" + }, + "token_budget": { + "anyOf": [ + { + "maximum": 32768, + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Hard context budget in compact mode.", + "title": "Token Budget" + }, + "workspace": { + "description": "Workspace to surface context from.", + "maxLength": 200, + "minLength": 1, + "title": "Workspace", + "type": "string" + } + }, + "required": [ + "workspace" + ], + "title": "engraphis_proactive_contextArguments", + "type": "object" + }, + "name": "engraphis_proactive_context" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Promote a memory to a wider scope" + }, + "description": "Widen a memory's visibility without losing its narrow-scope history.\n\n The wider record is stored first, inherits the source's protection,\n confidentiality, provenance, and learned stability, and is linked back to the\n bi-temporally closed source. Promotion must be strictly wider (session\u2192repo/workspace\n or repo\u2192workspace); it never edits scope in place. User-scope promotion is not yet\n supported because records remain workspace-bound.\n\n Returns:\n str: JSON ``{\"id\",\"promoted_from\",\"from_scope\",\"scope\",\"op\",\"reason\"}``\n plus a privacy receipt, or an actionable validation error.\n ", + "inputSchema": { + "properties": { + "memory_id": { + "description": "The live memory id to promote.", + "maxLength": 200, + "minLength": 1, + "title": "Memory Id", + "type": "string" + }, + "reason": { + "default": "", + "description": "Why the learning now applies more broadly; recorded in audit history.", + "maxLength": 1000, + "title": "Reason", + "type": "string" + }, + "repo": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Repo that owns the source memory, when applicable.", + "title": "Repo" + }, + "target_scope": { + "description": "A strictly wider supported visibility: repo or workspace.", + "title": "Target Scope", + "type": "string" + }, + "workspace": { + "description": "Workspace that owns the source memory; verified before mutation.", + "maxLength": 200, + "minLength": 1, + "title": "Workspace", + "type": "string" + } + }, + "required": [ + "memory_id", + "target_scope", + "workspace" + ], + "title": "engraphis_promoteArguments", + "type": "object" + }, + "name": "engraphis_promote" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Recall relevant memories" + }, + "description": "Retrieve the memories most relevant to a query (semantic vector + lexical + graph).\n\n Call this before answering or acting when prior context would help \u2014 to avoid re-asking\n the user, to recover decisions/conventions, or to resume earlier work.\n Successful calls append a privacy-safe recall receipt but do not strengthen weak\n neighbors merely because they were returned. Grounded recall reinforces cited\n evidence; an explicit-use caller can opt into reinforcement through the Python API.\n Because the receipt is stateful, this surface is neither read-only nor idempotent.\n\n Returns:\n str: JSON with ``{\"query\",\"count\",\"context\",\"degraded_mode\",\"semantic_support\",\n \"embedding_mode\",\"score_semantics\",\"memories\":[{\"id\",\n \"title\",\"content\",\"scope\",\"mtype\",\"repo_id\",\"score\",\"relative_score\",\n \"absolute_support\",\"arm\",\"retention\",\"provenance\"}]}``. ``score`` is a compatibility\n alias for the query-relative rank; use ``absolute_support`` (0..1) for an evidence floor.\n ``degraded_mode=true`` and ``semantic_support=false`` mean semantic vector retrieval\n was disabled because the active embedder is not declared semantic.\n Returns count 0 with a \"note\" if the workspace/repo isn't known yet.\n ", + "inputSchema": { + "properties": { + "as_of": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Compatibility alias for valid_at. If both are supplied they must match.", + "title": "As Of" + }, + "candidate_depth": { + "default": "fixed", + "description": "Candidate depth: fixed preserves the legacy pool; adaptive is an opt-in profile-aware performance experiment.", + "title": "Candidate Depth", + "type": "string" + }, + "diagnostics": { + "default": false, + "description": "Include per-arm raw/normalized/fusion/rerank diagnostics.", + "title": "Diagnostics", + "type": "boolean" + }, + "k": { + "default": 8, + "description": "Max memories to return (1-50).", + "maximum": 50, + "minimum": 1, + "title": "K", + "type": "integer" + }, + "known_at": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional system-time Unix timestamp: return only facts Engraphis had learned and not retired then.", + "title": "Known At" + }, + "max_response_tokens": { + "anyOf": [ + { + "maximum": 1000000, + "minimum": 2, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Cap the total serialized response to this many tokens (regex counter). Omits packed context whole and reduces memory bodies; citations and source references are preserved when the budget can hold them. Minimum 2 (the JSON object floor); None means no cap.", + "title": "Max Response Tokens" + }, + "mtype_limits": { + "anyOf": [ + { + "additionalProperties": { + "type": "integer" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional maximum returned count per memory type; limits never boost relevance.", + "title": "Mtype Limits" + }, + "mtypes": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Restrict to these memory types (semantic/episodic/procedural/working).", + "title": "Mtypes" + }, + "planning": { + "default": "off", + "description": "Query planning: off preserves the single-query path; auto enables bounded offline or injected planning.", + "title": "Planning", + "type": "string" + }, + "query": { + "description": "What you want to remember, in natural language (e.g. 'how do we handle auth?').", + "maxLength": 100000, + "minLength": 1, + "title": "Query", + "type": "string" + }, + "repo": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Restrict to this repo (requires workspace).", + "title": "Repo" + }, + "response_mode": { + "default": "full", + "description": "full preserves legacy memory bodies; compact omits bodies already represented in the packed context.", + "title": "Response Mode", + "type": "string" + }, + "retrieval_profile": { + "default": "balanced", + "description": "Retrieval profile: balanced (hybrid), fast (vector + lexical, no graph), auto, lexical, graph, or code. Auto is opt-in until benchmarks demonstrate a win.", + "title": "Retrieval Profile", + "type": "string" + }, + "session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional active session context. Includes that exact session plus its repo/workspace ancestors; requires workspace.", + "title": "Session Id" + }, + "token_budget": { + "anyOf": [ + { + "maximum": 32768, + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Hard packed-context budget under the named token counter (0-32768).", + "title": "Token Budget" + }, + "valid_at": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional world-time Unix timestamp: return facts true then.", + "title": "Valid At" + }, + "workspace": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Restrict to this workspace.", + "title": "Workspace" + } + }, + "required": [ + "query" + ], + "title": "engraphis_recallArguments", + "type": "object" + }, + "name": "engraphis_recall" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Recall token-efficient context" + }, + "description": "Return one hard-budget context plus compact source identities.\n\n This is the recommended agent path: unlike legacy full recall, it does not\n repeat every complete memory body alongside the already-packed context. The\n response includes exact accounting for the declared counter, omitted/packed\n counts, privacy-safe savings metadata, and the same ``degraded_mode`` /\n ``semantic_support`` flags as ``engraphis_recall``.\n\n ``format=\"gist\"`` remains an accepted compatibility option. It returns the same\n evidence-safe packed context, including complete conditions and code whitespace,\n with a format marker. It does not apply another summary or claim extra savings.\n Use ``engraphis_get_memory`` for the full source behind a citation.\n ", + "inputSchema": { + "properties": { + "as_of": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Compatibility alias for valid_at.", + "title": "As Of" + }, + "candidate_depth": { + "default": "fixed", + "description": "fixed preserves the legacy pool; adaptive is profile-aware and opt-in.", + "title": "Candidate Depth", + "type": "string" + }, + "diagnostics": { + "default": false, + "description": "Include detailed retrieval scoring trace.", + "title": "Diagnostics", + "type": "boolean" + }, + "format": { + "default": "full", + "description": "Context format: 'full' or compatibility alias 'gist'; both preserve budgeted, cited evidence.", + "title": "Format", + "type": "string" + }, + "k": { + "default": 50, + "description": "Max candidate memories (1-50).", + "maximum": 50, + "minimum": 1, + "title": "K", + "type": "integer" + }, + "known_at": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional system-time Unix timestamp.", + "title": "Known At" + }, + "max_response_tokens": { + "anyOf": [ + { + "maximum": 1000000, + "minimum": 2, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Cap the total serialized response to this many tokens (regex counter). Omits packed context whole when it cannot fit; citations and source references are preserved when the budget can hold them. Minimum 2; None means no cap.", + "title": "Max Response Tokens" + }, + "mtype_limits": { + "anyOf": [ + { + "additionalProperties": { + "type": "integer" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional maximum returned count per memory type.", + "title": "Mtype Limits" + }, + "mtypes": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional memory types: semantic/episodic/procedural/working.", + "title": "Mtypes" + }, + "planning": { + "default": "off", + "description": "off preserves single-query recall; auto enables bounded planning.", + "title": "Planning", + "type": "string" + }, + "query": { + "description": "What prior context is needed.", + "maxLength": 100000, + "minLength": 1, + "title": "Query", + "type": "string" + }, + "repo": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Restrict to this repo (requires workspace).", + "title": "Repo" + }, + "retrieval_profile": { + "default": "balanced", + "description": "balanced, fast, auto, lexical, graph, or code.", + "title": "Retrieval Profile", + "type": "string" + }, + "session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional active session; includes its repo/workspace ancestors.", + "title": "Session Id" + }, + "token_budget": { + "default": 1024, + "description": "Hard packed-context budget under the reported token counter.", + "maximum": 32768, + "minimum": 0, + "title": "Token Budget", + "type": "integer" + }, + "valid_at": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional world-time Unix timestamp.", + "title": "Valid At" + }, + "workspace": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Restrict to this workspace.", + "title": "Workspace" + } + }, + "required": [ + "query" + ], + "title": "engraphis_recall_contextArguments", + "type": "object" + }, + "name": "engraphis_recall_context" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Grounded recall (cited answer, or abstain)" + }, + "description": "Answer a question *strictly from* stored memories, with citations \u2014 or abstain.\n\n Unlike ``engraphis_recall`` (which returns memories and leaves synthesis to you),\n this returns an answer assembled only from the retrieved memories, each claim tied\n to a ``[n]`` citation, and \u2014 crucially \u2014 refuses to answer when nothing in scope\n actually supports the query (``grounded: false``). Use it when you want a grounded,\n non-hallucinated answer and would rather get \"insufficient evidence\" than a guess.\n The deterministic default never introduces a claim that is not in a cited memory.\n When ``degraded_mode`` is true, its feature-hashing fallback is treated as lexical-only:\n semantic vector retrieval and semantic cosine support are disabled.\n With ``synthesize=True``, configured LLM prose is accepted only when citations hold.\n Every resolved call appends a privacy-safe receipt (including abstentions), and a\n grounded answer reinforces cited memories.\n\n Returns:\n str: JSON ``{\"query\",\"grounded\",\"abstained\",\"answer\",\"support\",\"reason\",\n \"degraded_mode\",\"semantic_support\",\"embedding_mode\",\n \"synthesized\":false,\"citations\":[{\"n\",\"id\",\"title\",\"content\",\"score\",\"support\",\n \"provenance\"}]}``. When ``grounded`` is false, ``answer`` is empty and ``reason``\n explains why (insufficient evidence, or unknown workspace/repo).\n ", + "inputSchema": { + "properties": { + "as_of": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Compatibility alias for valid_at.", + "title": "As Of" + }, + "candidate_depth": { + "default": "fixed", + "description": "fixed preserves the legacy pool; adaptive is profile-aware and opt-in.", + "title": "Candidate Depth", + "type": "string" + }, + "diagnostics": { + "default": false, + "description": "Include detailed retrieval scoring trace.", + "title": "Diagnostics", + "type": "boolean" + }, + "k": { + "default": 8, + "description": "Max memories to consider (1-50).", + "maximum": 50, + "minimum": 1, + "title": "K", + "type": "integer" + }, + "known_at": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional system-time Unix timestamp.", + "title": "Known At" + }, + "max_response_tokens": { + "anyOf": [ + { + "maximum": 1000000, + "minimum": 2, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Cap the total serialized response to this many tokens (regex counter). Omits packed context whole and reduces citation bodies; source references are preserved when the budget can hold them. Minimum 2; None means no cap.", + "title": "Max Response Tokens" + }, + "min_support": { + "anyOf": [ + { + "maximum": 1.0, + "minimum": 0.0, + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Absolute support floor 0..1 below which the tool abstains instead of answering. Omit for the default; raise it to demand stronger evidence (0 disables the abstain gate).", + "title": "Min Support" + }, + "mtype_limits": { + "anyOf": [ + { + "additionalProperties": { + "type": "integer" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional maximum returned count per memory type.", + "title": "Mtype Limits" + }, + "mtypes": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Restrict to these memory types (semantic/episodic/procedural/working).", + "title": "Mtypes" + }, + "planning": { + "default": "off", + "description": "off preserves single-query recall; auto enables bounded planning.", + "title": "Planning", + "type": "string" + }, + "query": { + "description": "The question to answer from memory, in natural language (e.g. 'which auth scheme did we standardise on?').", + "maxLength": 100000, + "minLength": 1, + "title": "Query", + "type": "string" + }, + "repo": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Restrict to this repo (requires workspace).", + "title": "Repo" + }, + "response_mode": { + "default": "full", + "description": "full includes citation bodies; compact omits bodies already present in the cited answer.", + "title": "Response Mode", + "type": "string" + }, + "retrieval_profile": { + "default": "balanced", + "description": "balanced, fast, auto, lexical, graph, or code.", + "title": "Retrieval Profile", + "type": "string" + }, + "session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional active session context. Includes that exact session plus its repo/workspace ancestors; requires workspace.", + "title": "Session Id" + }, + "synthesize": { + "default": false, + "description": "If true and an LLM is configured, synthesize cited prose; otherwise return the deterministic extractive answer.", + "title": "Synthesize", + "type": "boolean" + }, + "token_budget": { + "anyOf": [ + { + "maximum": 32768, + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Hard packed-context budget (0-32768).", + "title": "Token Budget" + }, + "valid_at": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional world-time Unix timestamp.", + "title": "Valid At" + }, + "workspace": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Restrict to this workspace.", + "title": "Workspace" + } + }, + "required": [ + "query" + ], + "title": "engraphis_recall_groundedArguments", + "type": "object" + }, + "name": "engraphis_recall_grounded" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false, + "readOnlyHint": true, + "title": "What should I know right now" + }, + "description": "Conscious/proactive recall: high-importance, recent, well-reinforced memories with\n no query needed \u2014 call this at the start of a task to load context before you've\n figured out what to ask for. When ``repo`` is given, also returns the most recent\n *ended* session's summary and unresolved ``open_threads`` for that repo, so you can\n pick up exactly where the last session left off. Authenticated callers only receive\n handoffs owned by their own user identity.\n\n Unlike query-based recall, this queryless ranking does not reinforce memories or append\n an operation receipt, so repeated calls are read-only and idempotent.\n\n Returns:\n str: JSON ``{\"memories\":[...], \"last_session\":{\"summary\",\"open_threads\",\"outcome\"}\n or {} if there is no prior session}``.\n ", + "inputSchema": { + "properties": { + "k": { + "default": 10, + "description": "Max memories to return (1-50).", + "maximum": 50, + "minimum": 1, + "title": "K", + "type": "integer" + }, + "repo": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Repo to surface memories from; also enables the last-session handoff.", + "title": "Repo" + }, + "workspace": { + "description": "Workspace to surface memories from.", + "maxLength": 200, + "minLength": 1, + "title": "Workspace", + "type": "string" + } + }, + "required": [ + "workspace" + ], + "title": "engraphis_recall_proactiveArguments", + "type": "object" + }, + "name": "engraphis_recall_proactive" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false, + "readOnlyHint": true, + "title": "List privacy-safe operation receipts" + }, + "description": "List content-free, hash-chained remember/recall/link/index receipts.", + "inputSchema": { + "properties": { + "limit": { + "default": 100, + "description": "Maximum receipts to return (1-10000).", + "maximum": 10000, + "minimum": 1, + "title": "Limit", + "type": "integer" + }, + "workspace": { + "description": "Workspace whose receipt chain to inspect.", + "maxLength": 200, + "minLength": 1, + "title": "Workspace", + "type": "string" + } + }, + "required": [ + "workspace" + ], + "title": "engraphis_receiptsArguments", + "type": "object" + }, + "name": "engraphis_receipts" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Log an episodic event" + }, + "description": "Append a lightweight episodic log entry \u2014 lower ceremony than ``engraphis_remember``,\n for raw events you may later want consolidated into a durable fact (e.g. \"tried X, it\n deadlocked\" \u2014 three of these about the same thing is a signal worth promoting).\n\n Returns:\n str: JSON ``{\"id\",\"kind\"}``.\n ", + "inputSchema": { + "properties": { + "content": { + "description": "What happened.", + "maxLength": 100000, + "minLength": 1, + "title": "Content", + "type": "string" + }, + "kind": { + "description": "Event kind, e.g. 'decision', 'bug', 'fix', 'tried_and_failed', 'review_comment'.", + "maxLength": 200, + "minLength": 1, + "title": "Kind", + "type": "string" + }, + "repo": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Repo this event belongs to.", + "title": "Repo" + }, + "session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Session this event belongs to, if any.", + "title": "Session Id" + }, + "workspace": { + "default": "default", + "description": "Workspace this event belongs to. Defaults to 'default' if omitted.", + "maxLength": 200, + "minLength": 1, + "title": "Workspace", + "type": "string" + } + }, + "required": [ + "kind", + "content" + ], + "title": "engraphis_record_eventArguments", + "type": "object" + }, + "name": "engraphis_record_event" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Remember a fact" + }, + "description": "Store a memory so it can be recalled in later turns, sessions, or repos.\n\n Use this whenever you learn something worth keeping: a convention, a decision and its\n rationale, a bug's cause and fix, a user preference, or a reusable procedure.\n\n Returns:\n str: JSON ``{\"id\",\"workspace\",\"repo\",\"scope\",\"mtype\",\"stored\":true,\"op\"}`` where\n ``op`` is ``\"add\"`` (new), ``\"noop\"`` (matched an existing memory almost exactly \u2014\n that one was reinforced, ``id`` points to it), or ``\"invalidate\"`` (superseded an\n existing memory on the same subject \u2014 see ``superseded`` for the old id(s); history\n is preserved, never deleted), ``\"relate\"`` (kept both uncertain neighboring claims and\n linked them), or ``\"quarantined\"`` (a suspicious explicitly untrusted payload was\n retained for governance inspection but excluded from normal recall). Quarantine returns\n content-free ``policy`` and ``reasons`` codes. Returns ``\"Error: \"`` if\n validation fails.\n ", + "inputSchema": { + "properties": { + "claim_kind": { + "default": "", + "description": "Optional claim predicate/category (for example 'configured_value').", + "maxLength": 200, + "title": "Claim Kind", + "type": "string" + }, + "content": { + "description": "The fact, decision, convention, or note to store (e.g. 'We use pnpm for all frontend repos').", + "maxLength": 100000, + "minLength": 1, + "title": "Content", + "type": "string" + }, + "dedupe": { + "default": true, + "description": "If true (default), check this against similar existing memories first: an exact restatement reinforces the existing one instead of duplicating it; a shared subject_key or strong joint evidence can supersede the old one, while uncertain neighbors are related without discarding either fact. Set false to force a plain insert (e.g. for recurring episodic log entries where repeats are meaningful).", + "title": "Dedupe", + "type": "boolean" + }, + "importance": { + "default": 0.0, + "description": "Salience 0..1; higher resists decay.", + "maximum": 1.0, + "minimum": 0.0, + "title": "Importance", + "type": "number" + }, + "keywords": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional keywords to aid lexical recall.", + "title": "Keywords" + }, + "kind": { + "anyOf": [ + { + "maxLength": 100, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional artifact kind for filtering: 'plan', 'diff', 'review', 'task_summary', 'council_verdict', ...", + "title": "Kind" + }, + "mtype": { + "default": "semantic", + "description": "Memory type: 'semantic' (facts/conventions), 'episodic' (events/decisions), 'procedural' (how-tos), or 'working' (transient).", + "title": "Mtype", + "type": "string" + }, + "repo": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Repository scope within the workspace ('backend'). Omit for workspace-wide memories.", + "title": "Repo" + }, + "retention_class": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional host-LLM retention decision: ephemeral, normal, or critical. The write is never silently discarded; this adjusts bounded importance/stability and records the supervision signal.", + "title": "Retention Class" + }, + "retention_reason": { + "default": "", + "description": "Short explanation for the retention classification; do not repeat sensitive memory contents.", + "maxLength": 1000, + "title": "Retention Reason", + "type": "string" + }, + "scope": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Visibility: session, repo, workspace, or user. Omit to infer the compatible default: repo when repo or a repo-backed session_id is present, otherwise workspace. Session visibility must be explicit.", + "title": "Scope" + }, + "session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Session id from engraphis_start_session, if this memory belongs to one.", + "title": "Session Id" + }, + "source": { + "default": "agent", + "description": "Origin of the content. Web, import, sync, and other external origins are always untrusted even if trusted=true; use the default agent only for a fact the connected local agent authored or independently verified.", + "maxLength": 200, + "title": "Source", + "type": "string" + }, + "subject_key": { + "default": "", + "description": "Optional stable claim subject (for example 'api.rate_limit'). Matching keys make supersession safer and deterministic.", + "maxLength": 1000, + "title": "Subject Key", + "type": "string" + }, + "title": { + "default": "", + "description": "Optional short title.", + "maxLength": 1000, + "title": "Title", + "type": "string" + }, + "trusted": { + "default": true, + "description": "Local-agent confidence label. External origins cannot elevate themselves with this field.", + "title": "Trusted", + "type": "boolean" + }, + "valid_from": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional Unix timestamp for when this fact became true in world time. Omit to use ingestion time.", + "title": "Valid From" + }, + "workspace": { + "default": "default", + "description": "Top-level scope, e.g. an org or product name ('acme'). Defaults to 'default' if omitted.", + "maxLength": 200, + "minLength": 1, + "title": "Workspace", + "type": "string" + } + }, + "required": [ + "content" + ], + "title": "engraphis_rememberArguments", + "type": "object" + }, + "name": "engraphis_remember" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Remember a batch of facts" + }, + "description": "Store a batch of facts from parallel agents in one atomic, deduplicated write.\n\n Use this instead of many ``engraphis_remember`` calls when one turn produced a\n set of findings (fan-out sub-agents, a research sweep, a review council): the\n whole batch lands in a single transaction, each fact is resolved against the\n others (duplicates reinforce, keyed claims supersede), and facts sharing a\n ``subject_key`` or an explicit per-fact ``evidence_source`` get\n evidence-labeled graph edges so the merge is a growing graph rather than a\n pile of prose.\n\n Returns:\n str: JSON ``{\"workspace\",\"repo\",\"scope\",\"stored\":true,\"total\",\"ops\",\n \"results\":[{\"id\",\"op\",...}]}`` with one entry per input fact, in order.\n Returns ``\"Error: \"`` if validation fails or any fact cannot be\n stored (the whole batch rolls back in that case).\n ", + "inputSchema": { + "properties": { + "facts": { + "description": "The facts collected from a fan-out (parallel sub-agents, research, a review council), as a list of objects: each needs 'content' and optionally 'title', 'importance' (0..1), 'keywords', 'subject_key' (stable claim subject like 'api.rate_limit'), 'claim_kind', 'evidence_source' (per-fact origin label; facts sharing one get evidence-labeled links), and 'valid_from' (Unix timestamp). All facts are stored in one transaction; each is deduplicated against the others, and facts that share a subject_key or evidence_source are linked with evidence-labeled edges.", + "items": { + "additionalProperties": true, + "type": "object" + }, + "maxItems": 500, + "minItems": 1, + "title": "Facts", + "type": "array" + }, + "mtype": { + "default": "semantic", + "description": "Default memory type for facts without their own: 'semantic' (facts/conventions), 'episodic' (events/decisions), 'procedural' (how-tos), or 'working' (transient).", + "title": "Mtype", + "type": "string" + }, + "repo": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Repository scope within the workspace ('backend'). Omit for workspace-wide memories.", + "title": "Repo" + }, + "scope": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Visibility: session, repo, workspace, or user. Omit to infer the compatible default: repo when repo or a repo-backed session_id is present, otherwise workspace. Session visibility must be explicit.", + "title": "Scope" + }, + "session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Session id from engraphis_start_session, if this batch belongs to one.", + "title": "Session Id" + }, + "source": { + "default": "agent", + "description": "Origin of the content. Web, import, sync, and other external origins are always untrusted even if trusted=true; use the default agent only for facts the connected local agent authored or independently verified.", + "maxLength": 200, + "title": "Source", + "type": "string" + }, + "trusted": { + "default": true, + "description": "Local-agent confidence label. External origins cannot elevate themselves with this field.", + "title": "Trusted", + "type": "boolean" + }, + "workspace": { + "default": "default", + "description": "Top-level scope, e.g. an org or product name ('acme'). Defaults to 'default' if omitted.", + "maxLength": 200, + "minLength": 1, + "title": "Workspace", + "type": "string" + } + }, + "required": [ + "facts" + ], + "title": "engraphis_remember_manyArguments", + "type": "object" + }, + "name": "engraphis_remember_many" + }, + { + "annotations": { + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Retire a memory" + }, + "description": "Retire a memory: it stops appearing in recall, but history is preserved, not\n deleted (bi-temporal close, never a hard delete) \u2014 use ``engraphis_correct`` instead\n if you have replacement content, since that keeps the \"why\" chain intact.\n Every request appends an audit record, including an identical retry, so the MCP call\n is deliberately annotated as non-idempotent. Requires explicit local-operator\n confirmation (``confirmed=true``) because the stdio transport carries no role\n boundary.\n\n Returns:\n str: JSON ``{\"id\",\"status\":\"retired\",\"reason\"}`` or an actionable error if the\n id is unknown or doesn't belong to ``workspace``/``repo``.\n ", + "inputSchema": { + "properties": { + "confirmed": { + "default": false, + "description": "Explicit local-operator confirmation: must be true \u2014 retirement closes history and every request is audited, including retries.", + "title": "Confirmed", + "type": "boolean" + }, + "memory_id": { + "description": "The memory id to retire (from a prior remember/recall result, e.g. 'mem_01J...').", + "maxLength": 200, + "minLength": 1, + "title": "Memory Id", + "type": "string" + }, + "reason": { + "default": "", + "description": "Why this is being retired (recorded in the audit trail).", + "maxLength": 1000, + "title": "Reason", + "type": "string" + }, + "repo": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Repo that owns this memory, if it's repo-scoped; also checked.", + "title": "Repo" + }, + "workspace": { + "description": "Workspace that owns this memory \u2014 checked against the memory's actual workspace before anything is changed, so you can't retire a memory in a workspace you weren't already given.", + "maxLength": 200, + "minLength": 1, + "title": "Workspace", + "type": "string" + } + }, + "required": [ + "memory_id", + "workspace" + ], + "title": "engraphis_retireArguments", + "type": "object" + }, + "name": "engraphis_retire" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false, + "readOnlyHint": true, + "title": "Search the code symbol graph" + }, + "description": "Find function/class/method definitions by name, with their callers \u2014 structural\n code search that costs far fewer tokens than grepping/reading whole files, and\n directly answers \"what calls this\" / \"what might break if I change it\".\n\n Returns:\n str: JSON ``{\"query\",\"symbols\":[{\"name\",\"fqname\",\"kind\",\"file\",\"span\",\n \"signature\",\"called_by\":[{\"src\",\"file\",\"line\"}]}]}``.\n ", + "inputSchema": { + "properties": { + "as_of": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Compatibility alias for valid_at.", + "title": "As Of" + }, + "known_at": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional system-time Unix timestamp.", + "title": "Known At" + }, + "limit": { + "default": 20, + "description": "Max symbols to return (1-50).", + "maximum": 50, + "minimum": 1, + "title": "Limit", + "type": "integer" + }, + "query": { + "description": "A symbol name or partial name to find, e.g. 'Calculator' or 'add'.", + "maxLength": 500, + "minLength": 1, + "title": "Query", + "type": "string" + }, + "repo": { + "description": "Repo to search (must have been indexed with engraphis_index_repo first).", + "maxLength": 200, + "minLength": 1, + "title": "Repo", + "type": "string" + }, + "valid_at": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional world-time Unix timestamp.", + "title": "Valid At" + }, + "workspace": { + "description": "Workspace the repo belongs to.", + "maxLength": 200, + "minLength": 1, + "title": "Workspace", + "type": "string" + } + }, + "required": [ + "query", + "workspace", + "repo" + ], + "title": "engraphis_search_codeArguments", + "type": "object" + }, + "name": "engraphis_search_code" + }, + { + "annotations": { + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Securely erase a leaked memory" + }, + "description": "Irreversibly remove one accidentally stored secret from local persistence.\n\n Unlike retirement, this removes the memory, FTS/vector-index and derived graph/link\n rows, performs SQLite secure-delete/WAL/VACUUM maintenance, and scans recognised\n local SQLite recovery backups. It cannot erase copied exports, snapshots, remote\n peers, or data already read by a compromised/running agent; rotate the credential.\n Requires explicit local-operator confirmation (``confirmed=true``); the response\n carries the Store's ``impact`` report (receipt/event refs, backup note,\n WAL/vacuum status) for the rotation runbook (see docs/SYNC.md).\n ", + "inputSchema": { + "properties": { + "confirmed": { + "default": false, + "description": "Explicit local-operator confirmation: must be true \u2014 this irreversibly destroys the memory, its history, and local indexed derivatives. Rotate the credential first.", + "title": "Confirmed", + "type": "boolean" + }, + "memory_id": { + "description": "Leaked memory id to erase irreversibly.", + "maxLength": 200, + "minLength": 1, + "title": "Memory Id", + "type": "string" + }, + "repo": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional owning repo.", + "title": "Repo" + }, + "workspace": { + "description": "Workspace that owns the memory.", + "maxLength": 200, + "minLength": 1, + "title": "Workspace", + "type": "string" + } + }, + "required": [ + "memory_id", + "workspace" + ], + "title": "engraphis_secure_eraseArguments", + "type": "object" + }, + "name": "engraphis_secure_erase" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Start a memory session" + }, + "description": "Open a session to group this work's memories and enable cross-session resume.\n\n Call this at the start of a task in a repo you've worked in before \u2014 if a previous\n session for the same authenticated user and agent was ended with a summary or open\n threads, they come back in ``bootstrap`` so you can resume without crossing another\n user or agent's handoff boundary.\n\n Exact retries are reused by default for the same ``(workspace, repo, authenticated\n user, agent, goal)`` identity. Different users, agents, or goals start distinct\n sessions automatically, and ``force_new=true`` always branches another session.\n Because that valid option creates a new row on every call, the tool as a whole is\n conservatively annotated as non-idempotent.\n\n Returns:\n str: JSON ``{\"session_id\",\"workspace\",\"repo\",\"goal\",\"status\":\"active\",\"reused\",\n \"bootstrap\":{\"summary\",\"open_threads\",\"outcome\"} or {} if there is no prior\n session}``. Pass ``session_id`` to engraphis_remember and engraphis_end_session.\n ", + "inputSchema": { + "properties": { + "agent": { + "default": "", + "description": "Agent/tool name (e.g. 'claude-code').", + "maxLength": 200, + "title": "Agent", + "type": "string" + }, + "force_new": { + "default": false, + "description": "Force a brand-new session even if one is already active for this exact workspace/repo/user/agent/goal identity. Default false: an exact retry returns the existing active session (reused=true). Set true only to branch a second session for the same task identity.", + "title": "Force New", + "type": "boolean" + }, + "goal": { + "default": "", + "description": "What this session is trying to accomplish.", + "maxLength": 1000, + "title": "Goal", + "type": "string" + }, + "repo": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Repo scope, if any.", + "title": "Repo" + }, + "workspace": { + "default": "default", + "description": "Workspace the session belongs to. Defaults to 'default' if omitted (cron jobs often omit it).", + "maxLength": 200, + "minLength": 1, + "title": "Workspace", + "type": "string" + } + }, + "title": "engraphis_start_sessionArguments", + "type": "object" + }, + "name": "engraphis_start_session" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false, + "readOnlyHint": true, + "title": "Memory store stats" + }, + "description": "Report memory counts (overall or for one workspace) \u2014 handy for onboarding/health.\n\n Returns:\n str: JSON ``{\"memories\",\"by_type\",\"workspaces\",\"sessions\",\"schema_version\"}``.\n ", + "inputSchema": { + "properties": { + "workspace": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Limit counts to this workspace.", + "title": "Workspace" + } + }, + "title": "engraphis_statsArguments", + "type": "object" + }, + "name": "engraphis_stats" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false, + "readOnlyHint": true, + "title": "Bi-temporal history of a fact" + }, + "description": "Return every version of a fact in chronological order, including superseded ones.\n\n Use this for \"what did we believe and when\" / \"how has X changed over time\" \u2014 each\n entry carries ``valid_from``/``valid_to`` so you can see exactly when it was true.\n\n Returns:\n str: JSON ``{\"query\",\"history\":[{...memory fields..., \"valid_from\",\"valid_to\"}]}``\n oldest first. Raises an actionable error if the workspace/repo is unknown.\n ", + "inputSchema": { + "properties": { + "limit": { + "default": 20, + "description": "Max history entries (1-50).", + "maximum": 50, + "minimum": 1, + "title": "Limit", + "type": "integer" + }, + "query": { + "description": "The fact/entity to trace, e.g. 'rate limit' or 'default branch name'.", + "maxLength": 100000, + "minLength": 1, + "title": "Query", + "type": "string" + }, + "repo": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Restrict to this repo.", + "title": "Repo" + }, + "workspace": { + "description": "Workspace to search.", + "maxLength": 200, + "minLength": 1, + "title": "Workspace", + "type": "string" + } + }, + "required": [ + "query", + "workspace" + ], + "title": "engraphis_timelineArguments", + "type": "object" + }, + "name": "engraphis_timeline" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false, + "readOnlyHint": true, + "title": "Verify an operation receipt chain" + }, + "description": "Verify hashes, predecessor links, the local anchor, and optional external anchor.", + "inputSchema": { + "properties": { + "expected_count": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Previously saved receipt count to compare against.", + "title": "Expected Count" + }, + "expected_head": { + "anyOf": [ + { + "maxLength": 128, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Previously saved chain head to compare against (detects replacement or truncation even if the local anchor was also altered).", + "title": "Expected Head" + }, + "workspace": { + "description": "Workspace whose receipt chain to verify.", + "maxLength": 200, + "minLength": 1, + "title": "Workspace", + "type": "string" + } + }, + "required": [ + "workspace" + ], + "title": "engraphis_verify_receiptsArguments", + "type": "object" + }, + "name": "engraphis_verify_receipts" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false, + "readOnlyHint": true, + "title": "Explain the rationale behind a fact" + }, + "description": "Surface the current answer *and* what it superseded, if anything.\n\n Use this for \"why is it like this\" / \"what did we used to do\" questions \u2014 it\n deliberately looks past the live view into bi-temporal history, which plain recall\n does not. The \"supersedes\" list is what makes this different from a vector search:\n those memories are no longer current but are not deleted, so the rationale chain\n (\"we used to do X, then switched to Y because Z\") stays answerable.\n\n Returns:\n str: JSON ``{\"query\",\"answer\":[...live memories...],\"supersedes\":[...what they\n replaced, if anything...]}``. Raises an actionable error if the workspace/repo\n is unknown.\n ", + "inputSchema": { + "properties": { + "k": { + "default": 5, + "description": "Max results (1-50).", + "maximum": 50, + "minimum": 1, + "title": "K", + "type": "integer" + }, + "query": { + "description": "The decision or fact to explain, e.g. 'why did we migrate to PASETO?' or just 'rate limit'.", + "maxLength": 100000, + "minLength": 1, + "title": "Query", + "type": "string" + }, + "repo": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Restrict to this repo.", + "title": "Repo" + }, + "workspace": { + "description": "Workspace to search.", + "maxLength": 200, + "minLength": 1, + "title": "Workspace", + "type": "string" + } + }, + "required": [ + "query", + "workspace" + ], + "title": "engraphis_whyArguments", + "type": "object" + }, + "name": "engraphis_why" + } + ], + "smart": [ + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false, + "readOnlyHint": true, + "title": "List pending/quarantined/conflicting memories" + }, + "description": "Read-only inbox of pending/quarantined/conflicting memories for a reviewer.\n\n Scope and personal-folder authorization are enforced by ``MemoryService``. Pending\n and quarantined bodies are never returned to an agent; only approved conflict\n records may include a short excerpt.\n ", + "inputSchema": { + "properties": { + "limit": { + "default": 50, + "description": "Max items to return.", + "maximum": 100, + "minimum": 1, + "title": "Limit", + "type": "integer" + }, + "repo": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional repository scope.", + "title": "Repo" + }, + "workspace": { + "default": "default", + "description": "Workspace to review.", + "maxLength": 200, + "title": "Workspace", + "type": "string" + } + }, + "title": "engraphis_conflict_reviewArguments", + "type": "object" + }, + "name": "engraphis_conflict_review" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false, + "readOnlyHint": true, + "title": "Discover an advanced Engraphis capability" + }, + "description": "Return only the exact schemas needed for a small set of matching advanced actions.", + "inputSchema": { + "properties": { + "category": { + "default": "", + "description": "Optional area: memory, governance, code, audit, or ops.", + "maxLength": 100, + "title": "Category", + "type": "string" + }, + "intent": { + "default": "any", + "description": "Optional side effect: any, read, write, admin, or destructive.", + "pattern": "^(any|read|write|admin|destructive)$", + "title": "Intent", + "type": "string" + }, + "limit": { + "default": 1, + "description": "Number of ranked actions to return.", + "maximum": 3, + "minimum": 1, + "title": "Limit", + "type": "integer" + }, + "task": { + "description": "Describe the capability needed, without pasting memory content.", + "maxLength": 2000, + "minLength": 1, + "title": "Task", + "type": "string" + } + }, + "required": [ + "task" + ], + "title": "engraphis_discover_actionsArguments", + "type": "object" + }, + "name": "engraphis_discover_actions" + }, + { + "annotations": { + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Execute a discovered stateful action" + }, + "description": "Execute a discovered write, admin, or destructive-capable action safely.", + "inputSchema": { + "properties": { + "arguments": { + "additionalProperties": true, + "description": "Arguments matching the discovered schema.", + "title": "Arguments", + "type": "object" + }, + "capability_id": { + "description": "Capability id returned by discover_actions.", + "maxLength": 128, + "minLength": 8, + "title": "Capability Id", + "type": "string" + }, + "schema_digest": { + "description": "Schema digest returned by discovery.", + "maxLength": 128, + "minLength": 8, + "title": "Schema Digest", + "type": "string" + } + }, + "required": [ + "capability_id", + "schema_digest", + "arguments" + ], + "title": "engraphis_execute_actionArguments", + "type": "object" + }, + "name": "engraphis_execute_action" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false, + "readOnlyHint": true, + "title": "Execute a discovered read action" + }, + "description": "Execute only a discovered action that is truthfully read-only and idempotent.", + "inputSchema": { + "properties": { + "arguments": { + "additionalProperties": true, + "description": "Arguments matching the discovered schema.", + "title": "Arguments", + "type": "object" + }, + "capability_id": { + "description": "Capability id returned by discover_actions.", + "maxLength": 128, + "minLength": 8, + "title": "Capability Id", + "type": "string" + }, + "schema_digest": { + "description": "Schema digest returned by discovery.", + "maxLength": 128, + "minLength": 8, + "title": "Schema Digest", + "type": "string" + } + }, + "required": [ + "capability_id", + "schema_digest", + "arguments" + ], + "title": "engraphis_execute_readArguments", + "type": "object" + }, + "name": "engraphis_execute_read" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false, + "readOnlyHint": true, + "title": "Read one memory's governed record" + }, + "description": "Return one memory's governed record (content, provenance, scope, temporal fields).\n\n Read-only and never reinforces. Pending/quarantined content is NOT returned to an\n agent \u2014 the tool answers ``not_prompt_eligible`` instead, so untrusted content never\n reaches model context through this surface.\n ", + "inputSchema": { + "properties": { + "memory_id": { + "description": "Memory id to read.", + "maxLength": 200, + "minLength": 1, + "title": "Memory Id", + "type": "string" + }, + "repo": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional repository scope.", + "title": "Repo" + }, + "workspace": { + "default": "default", + "description": "Workspace containing the memory.", + "maxLength": 200, + "title": "Workspace", + "type": "string" + } + }, + "required": [ + "memory_id" + ], + "title": "engraphis_get_memoryArguments", + "type": "object" + }, + "name": "engraphis_get_memory" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Recall compact project context" + }, + "description": "Return one compact, bounded context packet for routine agent work.", + "inputSchema": { + "properties": { + "format": { + "default": "full", + "description": "Context format: 'full' or 'gist'.", + "title": "Format", + "type": "string" + }, + "k": { + "default": 50, + "description": "Maximum source memories.", + "maximum": 50, + "minimum": 1, + "title": "K", + "type": "integer" + }, + "query": { + "description": "Question or task needing prior context.", + "maxLength": 100000, + "minLength": 1, + "title": "Query", + "type": "string" + }, + "repo": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional repository.", + "title": "Repo" + }, + "session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional active session.", + "title": "Session Id" + }, + "token_budget": { + "default": 1024, + "description": "Hard returned-context token budget.", + "maximum": 32768, + "minimum": 0, + "title": "Token Budget", + "type": "integer" + }, + "workspace": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional workspace.", + "title": "Workspace" + } + }, + "required": [ + "query" + ], + "title": "smart_recall_contextArguments", + "type": "object" + }, + "name": "engraphis_recall_context" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Remember a durable fact" + }, + "description": "Store a routine durable memory with safe default provenance and deduplication.", + "inputSchema": { + "properties": { + "claim_kind": { + "default": "", + "description": "Optional claim predicate/category (for example 'configured_value').", + "maxLength": 200, + "title": "Claim Kind", + "type": "string" + }, + "content": { + "description": "Durable fact, decision, preference, or procedure.", + "maxLength": 100000, + "minLength": 1, + "title": "Content", + "type": "string" + }, + "importance": { + "default": 0.0, + "description": "Salience from 0 to 1.", + "maximum": 1.0, + "minimum": 0.0, + "title": "Importance", + "type": "number" + }, + "mtype": { + "default": "semantic", + "description": "semantic, episodic, procedural, or working.", + "title": "Mtype", + "type": "string" + }, + "repo": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional repository.", + "title": "Repo" + }, + "session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional active session.", + "title": "Session Id" + }, + "subject_key": { + "default": "", + "description": "Optional stable claim subject (for example 'api.rate_limit'). Matching keys make supersession safer and deterministic.", + "maxLength": 1000, + "title": "Subject Key", + "type": "string" + }, + "workspace": { + "default": "default", + "description": "Workspace for the memory.", + "maxLength": 200, + "title": "Workspace", + "type": "string" + } + }, + "required": [ + "content" + ], + "title": "smart_rememberArguments", + "type": "object" + }, + "name": "engraphis_remember" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Start or end a memory session" + }, + "description": "Start/resume a session or end it with its next-session handoff.", + "inputSchema": { + "properties": { + "action": { + "default": "start", + "description": "start to resume work, or end to save its handoff.", + "title": "Action", + "type": "string" + }, + "agent": { + "default": "", + "description": "Optional agent name.", + "maxLength": 200, + "title": "Agent", + "type": "string" + }, + "force_new": { + "default": false, + "description": "Start only: branch a new session instead of reusing an exact active task.", + "title": "Force New", + "type": "boolean" + }, + "goal": { + "default": "", + "description": "Task goal; start returns bounded relevant context.", + "maxLength": 1000, + "title": "Goal", + "type": "string" + }, + "open_threads": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Unresolved follow-ups.", + "title": "Open Threads" + }, + "outcome": { + "default": "", + "description": "Optional outcome label.", + "maxLength": 1000, + "title": "Outcome", + "type": "string" + }, + "repo": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional repository scope.", + "title": "Repo" + }, + "session_id": { + "default": "", + "description": "Session id required to end a session.", + "maxLength": 200, + "title": "Session Id", + "type": "string" + }, + "summary": { + "default": "", + "description": "Short final handoff.", + "maxLength": 100000, + "title": "Summary", + "type": "string" + }, + "token_budget": { + "default": 512, + "description": "Goal-context budget when starting.", + "maximum": 32768, + "minimum": 0, + "title": "Token Budget", + "type": "integer" + }, + "workspace": { + "default": "default", + "description": "Workspace for a started session.", + "maxLength": 200, + "title": "Workspace", + "type": "string" + } + }, + "title": "engraphis_sessionArguments", + "type": "object" + }, + "name": "engraphis_session" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Edit a memory's metadata fields" + }, + "description": "Edit a memory's metadata fields (title/type/importance). An identical retry is an\n atomic no-op. Content edits must go through the governed correction path so bi-temporal\n history is preserved. Secret capture is rejected; provenance/trust/sensitivity are never\n editable here.", + "inputSchema": { + "properties": { + "actor": { + "default": "user", + "description": "Optional local-mode actor label; authenticated team mode uses the caller identity.", + "maxLength": 200, + "title": "Actor", + "type": "string" + }, + "importance": { + "anyOf": [ + { + "maximum": 1.0, + "minimum": 0.0, + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional importance 0..1.", + "title": "Importance" + }, + "memory_id": { + "description": "Memory id to update.", + "maxLength": 200, + "minLength": 1, + "title": "Memory Id", + "type": "string" + }, + "mtype": { + "anyOf": [ + { + "maxLength": 50, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional memory type (working|episodic|semantic|procedural).", + "title": "Mtype" + }, + "repo": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional repository scope.", + "title": "Repo" + }, + "title": { + "anyOf": [ + { + "maxLength": 500, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional new title.", + "title": "Title" + }, + "workspace": { + "default": "default", + "description": "Workspace containing the memory.", + "maxLength": 200, + "title": "Workspace", + "type": "string" + } + }, + "required": [ + "memory_id" + ], + "title": "engraphis_update_memoryArguments", + "type": "object" + }, + "name": "engraphis_update_memory" + } + ] + } +} diff --git a/docs/MCP_TOOLS.md b/docs/MCP_TOOLS.md index d00118d9..4339fbc6 100644 --- a/docs/MCP_TOOLS.md +++ b/docs/MCP_TOOLS.md @@ -35,6 +35,10 @@ namesakes; advanced controls are discoverable rather than routine: | `engraphis_remember` | `content`, `workspace`, `repo`, `session_id`, `mtype`, `importance`, `subject_key`, `claim_kind`; safe provenance is fixed internally | | `engraphis_recall_context` | `query`, `workspace`, `repo`, `session_id`, `k`, `token_budget`, `format`; always compact, no `response_mode` | +`format="gist"` is a compatibility option for the same budgeted, cited evidence as +`full`. It preserves complete conditions and code whitespace; it does not apply an +additional summary or promise extra token savings. Source IDs remain in `sources`. + No user profile choice or tool switching is required. The dashboard `/mcp` endpoint and `engraphis-mcp-http` use this Smart surface by default. `engraphis-mcp-classic` (or @@ -141,3 +145,13 @@ the original query. For parameter details and return shapes, see the tool descriptions exposed by the MCP server. The [agent connection guide](AGENT_CONNECT.md) explains local and hosted connections, and the [Kilo Code guide](KILO_CODE_INTEGRATION.md) shows a complete editor integration. + +## Versioned integration contract + +[The generated MCP contract](MCP_CONTRACT.json) exports the registered Smart and +Classic input schemas, descriptions and annotations with a content digest. +Regenerate it with `python scripts/export_mcp_contract.py`; CI verifies the JSON +and the generated Pi/Prime Agent schemas with `--check` and the contract tests. +Host-specific session agent names and configured scope defaults are supplied by +integration adapters. Prime Agent also retains strict unknown-field validation +and documented action aliases and enum checks. Runtime authorization is unchanged. diff --git a/docs/PAID_EVALUATION_PROPOSAL.md b/docs/PAID_EVALUATION_PROPOSAL.md new file mode 100644 index 00000000..a3f6f7c3 --- /dev/null +++ b/docs/PAID_EVALUATION_PROPOSAL.md @@ -0,0 +1,78 @@ +# Paid evaluation proposal (pending approval) + +No paid evaluation has run. Prices were checked on 2026-09-05 against +[the official GPT-5.6 Luna model page](https://developers.openai.com/api/docs/models/gpt-5.6-luna). + +## Immediately reviewable existing-runner option + +The existing `eval.hosted_luna` runner supports the exact model `gpt-5.6-luna`, +medium reasoning, fresh isolated read-only Codex attempts, zero transport retries, +durably reserved call ceilings, and three strategies: full history, retrieval, adaptive. + +| Stage | Tasks | Repetitions | First attempts | Ceiling including one correction | +|---|---:|---:|---:|---:| +| Smoke | 1 | 1 | 3 | 6 | +| Pilot | 5 | 1 | 15 | 30 | +| CodeMem full | 26 | 3 | 234 | 468 | + +The smoke dry run was executed without a model call and reported ceiling 6. +This runner's Codex usage is not an API invoice. API token rates below are reference +estimates only; they do not impose a dollar ceiling on Codex account usage. +It measures structured CodeMem outcomes and recovery, not arbitrary repository task success. +See `LUNA_BENCHMARK_PLAN.md` for its predeclared scoring and checkpoint rules. + +## Proposed matched five-arm API experiment + +A separate approval is recommended for this bounded experiment after the five-arm adapter, +input hashes and exact run binding are reviewable. It must use the existing evidence/ledger +infrastructure and the official [LongMemEval-V2](https://github.com/xiaowu0162/LongMemEval-V2) +adapter. No private user memories are needed. + +Model: exact `gpt-5.6-luna`, reasoning medium, Responses API, standard processing. +No tool fees, paid embeddings, LLM judge or retry/correction calls are included. +Use deterministic task oracles and report answer support/completeness separately. +Fix a local semantic embedder/version before comparing lexical, dense and hybrid retrieval; +the hashing embedder is not a semantic dense baseline. + +Arms: no memory; full history; lexical; dense; hybrid. +Apply identical task/system prompts, generation limits, fresh state and source visibility. +Retrieval arms receive the same packing budget; full history receives its complete eligible +history within the shared hard request ceiling. Over-limit tasks fail preflight or are +reported as a separate common-budget stratum, never silently truncated in just one arm. + +| Stage | Tasks | Arms | Repetitions | Calls | Maximum API token cost | +|---|---:|---:|---:|---:|---:| +| Smoke | 1 | 5 | 1 | 5 | $0.18 | +| Pilot | 5 | 5 | 1 | 25 | $0.89 | +| CodeMem fixture comparison | 26 | 5 | 3 | 390 | $13.82 | +| LongMemEval-V2 adapter pilot | 20 | 5 | 3 | 300 | $10.63 | +| Total | | | | 720 | $25.51 | + +Cost assumptions: at most 128,000 uncached input tokens and 8,192 total output/reasoning +tokens per call; input $0.20/million and output $1.20/million. Per-call maximum is +`128000*0.20/1e6 + 8192*1.20/1e6 = $0.0354304`. +No caching or batch discount is assumed. Requests stay below the model's long-context +pricing threshold. Token-cost total is $25.509888 before tax. +Proposed approved API spend limit: **$31**, leaving approximately 20% headroom. +The runner must reserve the maximum per-call charge before dispatch, enforce token/call +ceilings durably across crashes, and stop rather than change model or pricing assumptions. + +This is an exact budgeted proposal, not authorization to execute. Final execution requires: +1. Reviewable five-arm adapter and fake-client budget/interruption tests. +2. Frozen source, model/tokenizer/embedding revision, prompts and dataset hashes. +3. Exactly 20 LongMemEval-V2 question IDs selected with a declared seed/category rule before + seeing outcomes; official dataset license/version verified. This subset is an adapter pilot, + not a full benchmark score. +4. The same source eligibility, temporal/scope rules and resource limits in every arm. +5. Explicit approval of the $31 API limit and the exact run binding. + +Do not describe CodeMem or implementation-authored fixtures as independent held-out evidence. +An independent coding-agent corpus and production workload study require separately +frozen tasks and a new cost proposal. They cannot be replaced by these 720 model calls. + +Predeclare category-level evidence retention, false NOOP/merge, unsupported-answer and scope +violations, abstention, task outcome, input/output tokens, latency and cost. Critical correctness +violations fail acceptance. Report paired task-level differences and task-cluster bootstrap +95% intervals; missing/error outcomes remain in the report. Retain existing defaults when +improvement or non-inferiority is not established. A 20-question pilot is not a reliable basis +for a broad product claim. diff --git a/docs/RAILWAY_TEMPLATE.md b/docs/RAILWAY_TEMPLATE.md index f9c4ab5d..e7324751 100644 --- a/docs/RAILWAY_TEMPLATE.md +++ b/docs/RAILWAY_TEMPLATE.md @@ -16,11 +16,11 @@ issuer, relay, managed compute, Auto Dreaming, Auto Consolidation, or Team ident - No vendor signer, billing, mail, Team-admin, relay-storage, or worker secrets. Hosted customer endpoint variables may be exposed as optional inputs, but a refresh credential -must be injected as a secret or mounted owner-only state file. Managed compute is enabled by -default once the deployed installation is connected to Engraphis Cloud; connecting accepts the -terms that cover it, and a local-only node with no cloud session is never allowed. Expose -`ENGRAPHIS_MANAGED_COMPUTE_CONSENT` only as an optional operator override (`0` to opt a -connected installation back out), not as a required opt-in input. +must be injected as a secret or mounted owner-only state file. Readable managed processing +is disabled until each workspace is explicitly confirmed in Manage → Settings. Existing +workspaces pause new uploads pending confirmation; a cloud connection does not grant approval. +`ENGRAPHIS_MANAGED_COMPUTE_CONSENT=0` is a deny-only operator override. A truthy value +cannot replace persisted workspace approval. Encrypted sync has separate controls. ## Publish gate diff --git a/docs/RELIABILITY_PROGRAM.md b/docs/RELIABILITY_PROGRAM.md new file mode 100644 index 00000000..ca9d5bcb --- /dev/null +++ b/docs/RELIABILITY_PROGRAM.md @@ -0,0 +1,236 @@ +# Engraphis reliability program: implementation and release evidence + +Status: implementation candidate, not a released or deployed system. Prepared 2026-09-05. + +The program strengthens coding-agent memory while preserving offline local use, +scoped temporal history, provenance, explicit erasure and server-owned entitlements. +It does not establish semantic-model superiority, 100,000-memory end-to-end agent +capacity, production readiness, or a completed user study. + +## Source and delivery boundary + +- Public implementation checkpoint: `c37ba0eb18408fe500cd70cd73fb5dcd7679b89c`, originally on + `feat/context-packing-and-perf-v2`. Delivery branch: `codex/reliable-agent-memory`. + Released v1.7.1 and the PR base are `cb03dbe104394b7760ef402a2b1917eac5e8accc`. +- Private cloud base: `8cd1f3cb819c4bfb55ac44a74aa009a8b709efac`. +- Website changes are local to the adjacent `engraphis.com` repository. +- The authorized PR review includes the four original unmerged commits, the existing gist + documentation edit and website changes. Gist behavior and its documentation were corrected + together. No merge, deployment, credential rotation or release is authorized by PR submission. +- Exactly four internal workers contributed bounded implementation work, followed by a separate + four-worker review of core, interfaces, private cloud and historical local work. The parent integrated both batches. + No descendant delegation, Orca routing or separate user-visible tasks were used. +- Current working-tree source hashes and results are recorded in [the evidence directory](evidence/reliability/). + A base commit alone does not identify the uncommitted implementation. + +## Findings register + +| ID | Classification and impact | Implemented response | Evidence / remaining boundary | +|---|---|---|---| +| R01 | Reproduced: distinct facts disappeared in context packing | Remove cross-memory clause pruning; retain source-extractive summaries and complete-unit budget fallback | `tests/test_context_evidence_preservation.py`; numeric, condition, environment, title/source/pronoun bindings, multilingual and custom-counter cases. Existing memory identity/family deduplication remains. No semantic compression claim. | +| R02 | Reproduced: browsing admitted future facts and hid still-current facts | Service listing delegates canonical Store temporal/scope predicates through `core/browsing.py` | `tests/test_memory_browsing.py`; current, future, expired, late-known and historical cases | +| R03 | Reproduced: Library searched only its first fetched subset | Server text/type filtering, exact count and bounded cursor pages | 1,201-record oldest-result and complete unique traversal; browser loading/search/page recovery | +| R04 | Reproduced: auxiliary Ask failure discarded a successful answer | Independent answer and preview state, deadlines and cancellation | Browser partial-success, timeout and stale-workspace cases | +| R05 | Reproduced: concurrent engines could insert duplicate writes; native batch publication could fail after canonical commit | Embed before writer reservation; discover/resolve/persist under SQLite transaction; store-sharing native batch publication joins that transaction | Separate-instance/process and native batch rollback tests in `test_storage_concurrency_repair.py` | +| R06 | Reproduced: incomplete derived index could miss canonical truth | Durable content-free repair work, idempotent retries, canonical fallback and readiness diagnostics | Outage, restart, erase and interrupted-repair tests; external adapters need stable index identity | +| R07 | Reproduced: resolver evaluation missed false NOOP loss | Real write-path acceptance and false-NOOP/distinct-survival metrics; environment-role resolver correction | Original unit fixture: 44 pairs (38 corrections, six distinct facts). New real-write fixture: 10 pairs. Neither is independent held-out user evidence. JSON commands identify the actual dataset and retain input/source hashes. | +| R08 | Reproduced quadratic fresh-insert work and measured candidate scan/verification costs | Fresh FTS inserts avoid full mirror scans while retaining orphan repair. Bounded scans sort the scoped first batch, then use vector-first keysets. Native verification checks every expected vector and exact unique-row cardinality | Controlled 10,000-row insertion comparison: 16.65 s with the former delete forced, 2.50 s corrected. Both final 10k/100k matrices completed. Concurrency, native restart and intermediate-scope latency remain material limits; details below. | +| R09 | Reproduced diagnostic gaps | Real rolled-back write probe; JSON doctor; private tokens for new setup; installation intent profiles | Setup/update regression tests; live Windows evidence only | +| R10 | Reproduced Windows long-path object-store failure | Confined extended paths and short unique staging filenames | Cloud object-store long-path regression and full suite | +| R11 | Source-backed hosted trust risk | Separate edge assertion secret, shared Durable Object budgets, persisted revocation | Actual workerd tests; production bindings, secret rotation and geographic behavior unverified | +| R12 | Product-policy decision: readable processing needs explicit approval | Persisted local workspace policy and cloud revision-bound authority; legacy work paused | Local policy/browser tests, cloud migration/worker tests. Backend-first deployment remains required. | +| R13 | Confirmed public contract drift | Team 10 days / Pro 3; secret-free cloud product export; generated MCP schemas consumed by Pi/Prime | Contract check spans public/cloud/site; site edits unpublished | +| R14 | Structural risk: concentrated modules | Narrow browsing, vector search/repair, setup profile and processing-control modules; compatibility facades retained | Broader Store/service/renderer and migration-executor extraction deferred to separately proven changes | +| R15 | Reproduced: MCP gist reread lost selected evidence and exceeded context budgets; response caps could detach qualifiers | Gist is a compatibility alias for canonical packed context; response caps keep or omit context whole and refresh usage | MCP budget, qualifier and retrieval-preservation regressions; generated contract refreshed | +| R16 | Reproduced: the unmerged 500-memory graph window excluded older two-hop evidence | Restore the established 12,000-memory window | Graph regression includes 501 newer unrelated memories; smaller windows require independent quality evidence | +| R17 | Reproduced: Classic consent copy described processing as enabled by default | Explain explicit workspace approval and link to the selected workspace's Ledger controls | Browser and authorization-placement tests; opening controls does not enable processing | +| R18 | Reproduced: concurrent SQLite cloud policy updates accepted stale enables; encoded auth paths escaped the stricter abuse budget | Conditional revision update with checked rowcount and first-row race handling; classify the upstream-equivalent decoded path | Independent SQLite policy writers and actual workerd regression; production deployment remains unverified | + +## Architecture and compatibility decisions + +1. SQLite remains the canonical authority. New writes reserve its writer after embedding; + resolution never treats an incomplete external index as proof that a fact is absent. + Required canonical state and index repair notifications commit together. +2. Derived vectors remain repairable. `MemoryEngine.repair_vector_index(limit=100)` + explicitly retries durable work. A stable per-index `index_identity` is required for + trustworthy completeness. Unidentified external adapters use canonical search conservatively. + No background repair scheduler was added. +3. Schema 17 is additive: vector generation, index targets and pending memory IDs contain + no memory text or credentials. Existing transactional migration and verified pre-migration + backup behavior remain intact. Dirty native startup retains a full verification/rebuild; + only a verified unchanged startup skips replay. +4. `MemoryService.list_memories` is the shared Python/REST browsing boundary. + Existing response fields remain, with `total_count` and `next_cursor` added. + Text/type filters execute on the server. Cursors bind scope/query/time anchors and ordering. + Database changes invalidate the cursor with `409 cursor_stale`; clients restart with + the same filters. Cursors are not durable across server restarts or arbitrary worker routing. +5. Context budgets may omit evidence when a complete safe unit cannot fit. Omission is preferable + to an altered claim. Existing chunk truncation/reason and usage omission counts remain available. + Recall also reports `vector_search_source` and `vector_index_repairs_pending`. + Regex token accounting is exact only for its declared tokenizer, not every model tokenizer. +6. Public MCP schemas are generated from registered tools into `docs/MCP_CONTRACT.json` and + both shipped integrations. Smart and Classic stay distinct. Configured scope/host agent + defaults and Prime's strict local argument checks remain adapter responsibilities. +7. Processing approval is private local client state keyed by workspace ID, separate from + synced workspace settings and encrypted sync. Missing/corrupt legacy state is off. + A truthy legacy environment variable cannot grant approval; a false override can deny it. + Cloud enforces its own persisted revision at upload, job submission, execution and publication. + Every cloud policy command requires the current revision and advances it, including an + explicit opt-out when already off. Stale replays fail without mutation. The local client + rechecks its current intent before and after the cloud acknowledgement; only a still-current + opt-out may retry a revision conflict. Enabling never silently retries against a newer policy. +8. New setup configurations get a private API token so the existing audited prompt-approval + journey works. Existing configuration is not overwritten. Doctor explains tokenless limits. + Explicit installation capabilities survive updates. +9. No retrieval/model/backend default was changed on the strength of synthetic performance results. + Removing evidence-losing behavior is a correctness repair, not a compression-quality win. +10. Fresh canonical inserts do not need the ordinary FTS update deletion. A one-time orphan + inventory under the writer reservation preserves recovery for inconsistent mirrors; + ordinary updates and explicit repairs retain replacement behavior. This removes measured + quadratic insert work without changing retrieval ranking. +11. Bounded vector scans use the scope-driven sorted first batch, then vector-first keysets + strictly beyond the last returned ID, all within one owned snapshot. This avoids repeated + large sorts and retains the one-batch path for narrow scopes. It is a measured tradeoff: + the 5% selectivity diagnostic was slower than repeated scope-driven batches. +12. Native readiness still verifies the complete content of every expected nonzero vector. + Native IDs are unique, so equal total cardinality then rejects all extra/orphan rows. + This removes redundant reverse scans while preserving missing, stale, zero and wrong-dimension + rejection. It does not replace verification with a count-only check. + +## Acceptance and validation + +See [validation.json](evidence/reliability/validation.json) for commands, environments, +counts, skips and source hashes. The deterministic tests do not stand in for user or +paid-model evaluations. Browser approval controls use an isolated test token. + +The final combined PR review suite passed **4,752 public tests**, with **37 skipped** and two warnings. +Production and test source stayed unchanged throughout that run; the before/after hashes and +skip reasons are in [the PR source receipt](evidence/reliability/public-pr-source-final.json). +The private suite passed **1,157 tests**, with **two PostgreSQL integration tests skipped**. +All seven required offline evaluation commands passed on the final public core/backend source. +Pi's 21 unit tests and actual MCP restart journey passed; Prime passed 136 tests with one skip. +The reviewed UI passed nine focused Chromium scenarios; the website passed 24 unit tests and +11 Chromium/axe scenarios. The edge passed 13 tests, including four actual workerd scenarios. +These focused results overlap other gates and are not a unique-test total. + +Earlier 4,736-public/1,155-private implementation checkpoints remain recorded separately. +The first combined PR run reproduced two obsolete assertions about automatic processing copy +and the Ledger cache version. Both failed in isolation, were corrected to enforce the current +approval contract, and passed before the clean complete rerun. The review also corrected two +misleading consent-error messages. [The review receipt](evidence/reliability/pr-review.json) +records the findings, historical-branch/stash reconciliation and remaining boundaries. + +The [paid evaluation proposal](PAID_EVALUATION_PROPOSAL.md) specifies a 720-call matrix and a +proposed $31 API limit. Its five-arm runner, frozen input selection and exact execution binding +remain prerequisites; no paid run is authorized or recorded by this implementation. + +Current local environment: Windows 11 build 26100, Python 3.12.10, SQLite 3.49.1, +NumPy 2.4.5, FastAPI 0.141.1, MCP 1.29.0, Pydantic 2.13.4. +Native SQLite-vector checks use an isolated sqlite-vec 0.1.9 installation. +Tests force `ENGRAPHIS_EXTRACTOR=none`. + +Required local commands: +```powershell +ruff check . +pyright +python scripts/check_commercial_manifest.py --website-root ../engraphis.com --cloud-contract +python scripts/export_mcp_contract.py --check +python scripts/externalize_dashboard_assets.py +python -m pytest tests/ -q +python -m eval.harness --dataset eval/datasets/sample.jsonl --k 5 +python -m eval.harness --dataset eval/datasets/codemem.jsonl --k 5 +python -m eval.ablation +python -m eval.reinforcement +python -m eval.adversarial_memory_security +python -m eval.grounded +python -m eval.code_arm +``` + +Run Pi and Prime tests from their own integration directories; their Python test +package names otherwise collide with the root suite. Browser CI installs Chromium, +Firefox and WebKit. The full supported Python/platform CI matrix, remote PR checks, +PostgreSQL integration, Docker smoke and production restore remain separate evidence. + +## Measured storage scale + +[The complete scale report](evidence/reliability/vector-scale-summary-20260905.md) and its +checksummed JSON artifacts include all 12 final backend/size/concurrency cells, mixed writes, +reopen/rebuild checks, hardware, exact commands, source hashes and retained incomplete runs. +Both final backends returned matching result IDs for identical synthetic inputs. + +At 100,000 total records, with 25% eligible for each scoped search: + +| Backend | Median search, 1 worker | 4 workers | 16 workers | Reopen | +|---|---:|---:|---:|---:| +| NumPy | 0.496 s | 2.341 s | 8.633 s | 0.014 s | +| sqlite-vec 0.1.9 | 0.357 s | 1.488 s | 5.296 s | 35.132 s | + +These measurements cover the file-backed index and canonical storage, with precomputed +256-dimensional vectors. They exclude embedding, extraction, resolution, full hybrid recall, +context packing and agent task execution. Reopen uses a new connection with the operating-system +disk cache still warm. Search latency includes shared-connection lock waits; throughput also +includes executor queue time. Thirty-two samples per cell are descriptive, not tail-latency +confidence bounds. Native 100k population took 104.00 s and full mirror rebuild took 61.31 s. + +The first-sorted-batch method measured 235.48 ms at 5% selectivity versus 132.03 ms for repeated +scope-driven batches in the diagnostic. Broader scopes improved substantially. The slow NumPy +baseline is the initial bounded-scan implementation candidate, not released Engraphis; the base +version materialized its scoped matrix once. No released-product speedup is inferred from that +comparison. The separate FTS and native-verification experiments are controlled method ablations. + +The 100k concurrent agent operating goal remains unproven. Shared-connection serialization, +native verification/startup cost, intermediate scope sizes and complete engine workloads are +the next performance priorities. No backend, ranking or grounding default was changed. + +## Dependency-ordered remaining backlog and exit conditions + +| Order | Work | Acceptance / dependency | +|---|---|---| +| 1 | Review the implementation and exact final dependency locks, then run remote supported-platform gates | No attributable test/type/lint failures; preserve unrelated edits; identify every candidate revision | +| 2 | Validate a staged backend-first processing migration and edge configuration | Old workers stopped; legacy schedules/jobs paused; no unconfirmed upload; no legacy-secret fallback; revocation persists; restore drill succeeds | +| 3 | Repeat and extend the completed storage/index measurements | Address shared-connection waits, native verification cost and scope tradeoffs; use independent repetitions and representative hardware; preserve complete paired reports | +| 4 | Independent held-out coding corpus and matched five-arm model acceptance | Human-reviewed task IDs/data hashes frozen before tuning; no-memory/full-history/lexical/dense/hybrid use matched budgets; approved cost proposal before paid calls | +| 5 | Full 100,000-memory mixed engine/agent workload, imports and recovery benchmark | Declare latency/error budgets, exercise real remember/recall at 1/4/16 concurrency, and preserve correctness; no extrapolation from index-only timings. One million remains the unrun stress track. | +| 6 | Further migration executor, request/view-state and repository extraction | One subsystem per change; compatibility and historical reads unchanged; include interruption/rollback tests | +| 7 | Target-user onboarding study | Measure install-to-useful-recall and successful corrections with consent; no fabricated adoption or usability numbers | +| 8 | Bounded pilot release | Exact revisions pass staged restore/auth/privacy gates; owner and alerts assigned; explicit release/deployment approval | +| 9 | Observe, then retire duplicated surfaces | Content-free cross-session recall/correction/support metrics; usage and compatibility requirements satisfied before removal | + +Milestone 1's reproduced defects and much of milestones 2, 4 and 5 now have implementations +and local regression evidence. Milestone 3 has stronger offline tests and completed 10k/100k +storage/index measurements; independent semantic/task evidence remains pending. Milestone 6 is a prepared +release process, not an executed release. None of those operational gates is waived. + +## Migration, recovery and rollout + +Public schema 16 → 17: +- Stop writers and take an independently restorable backup before a pilot. Ordinary startup + retains the existing versioned migration/backup checks; a separate migration executor is + future work. +- Verify the `*.pre-migration-v17.bak` artifact and schema/version/integrity checks on a disposable + restored copy. Preserve original IDs, history, provenance and visibility. +- Do not point old binaries at a migrated live database as a rollback strategy. Restore to a + separate path with writers stopped; reconcile later writes and erasure tombstones through + governed recovery before cutover. Restoration is a data operation requiring operator approval. +- Pending derived-index work is recoverable without changing canonical memories. + Run bounded repair until pending reaches zero; canonical fallback protects reads meanwhile. + +Cloud migration `0a5c9e2b7d31` and release order: +- Follow the private `docs/PROCESSING_AUTHORIZATION_ROLLOUT.md`. +- Stop old compute/worker binaries, back up, apply migration, and start only compatible services. +- Configure independent edge assertion credentials plus revocation/abuse Durable Object bindings. + Preparing source does not rotate secrets or install production bindings. +- Ship the local client, notify existing users that uploads are paused, and obtain explicit + workspace approval. Verify opt-out/re-enable/new-snapshot sequences with real staging services. +- If cloud acknowledgement is unavailable, local opt-out stops new client uploads immediately; + existing submitted work may continue until the cloud receives revocation. Show pending state. +- An expired client cannot refresh credentials to acknowledge cloud opt-out. Its local control + remains off with confirmation pending. Independent cloud entitlement checks reject new input, + exclude schedules, prevent queued snapshot reads and suppress results if entitlement expires + during computation; those cases have local regression evidence. +- Do not restore old services that infer consent from connection while accepting managed input. + +Pilot rollback triggers: any lost distinct write, scope/temporal leakage, unexpected readable +upload, stale approval resurrection, erased-record resurrection, revoked access acceptance, +migration integrity failure, or incomplete required release evidence. Pause the affected feature, +preserve audit evidence, restore only through the approved recovery path, and retain local use. diff --git a/docs/SYNC.md b/docs/SYNC.md index 51a6a795..06d7504b 100644 --- a/docs/SYNC.md +++ b/docs/SYNC.md @@ -30,8 +30,7 @@ for pricing and included services. ## Trial and grace -The no-card Pro or Team trial begins after email confirmation and lasts **exactly 3 active -days**. +The no-card Pro or Team trial begins after email confirmation and lasts **3 active days for Pro or 10 active days for Team**. `workspace_write_grace` is separate and private-service enforced. It may preserve bounded hosted-account continuity operations for at most **24 hours** following an authoritative @@ -207,8 +206,9 @@ replica; a peer cannot attach graph edges to locally approved memories. - Local-only installations send no memory content to Engraphis. **Cloud Sync encrypts eligible shared-workspace changes end-to-end before they leave this device. Engraphis Cloud cannot read their contents; secret and session-scoped memories stay local.** Managed compute is separate: - connecting an installation to Engraphis Cloud accepts its terms and enables it by default; - operators may opt out with `ENGRAPHIS_MANAGED_COMPUTE_CONSENT=0`. It sends a readable, bounded + explicit workspace approval in Manage → Settings is required before readable uploads; + connecting an account does not grant it. A deny-only operator override is available with + `ENGRAPHIS_MANAGED_COMPUTE_CONSENT=0`. Approved processing sends a readable, bounded snapshot over TLS because Engraphis Cloud must process that snapshot to produce results. - Treat cloud session and refresh files as credentials; keep their directory owner-only. - `secret` memories are excluded from managed uploads. Managed compute also rejects secret rows diff --git a/engraphis/backends/vector_numpy.py b/engraphis/backends/vector_numpy.py index 050a7251..2fc0c7b8 100644 --- a/engraphis/backends/vector_numpy.py +++ b/engraphis/backends/vector_numpy.py @@ -15,6 +15,13 @@ from engraphis.core.interfaces import SearchFilter from engraphis.core.store import Store +from engraphis.core.vector_search import ( + canonical_vector_search, + top_k_indices, +) + +# Retain the reference helper import used by backend contract tests/adapters. +_top_k_indices = top_k_indices def _validated_dimension(dim: int) -> int: @@ -53,34 +60,6 @@ def _vector_query(vec: np.ndarray) -> np.ndarray: return values -def _top_k_indices(scores: np.ndarray, ids: list[str], k: int) -> list[int]: - """Select the exact stable top-k without sorting an entire finite corpus. - - Search results promise descending cosine score with the memory id as the - deterministic tie-breaker. ``partition`` finds the score cutoff in linear - time, then we sort only scores above it plus the (usually tiny) tie boundary. - A corpus made entirely of equal scores intentionally sorts that boundary, - because every id participates in the observable ordering. - """ - if k <= 0: - return [] - if k >= len(ids) or not np.isfinite(scores).all(): - # A legacy caller can write non-finite vectors directly through Store. Keep - # the prior Python ordering for that unsupported data rather than assigning - # it new semantics in this hot-path optimization. - return sorted( - range(len(ids)), key=lambda index: (-float(scores[index]), ids[index]) - )[:k] - - cutoff_position = len(ids) - k - cutoff = float(np.partition(scores, cutoff_position)[cutoff_position]) - above = np.flatnonzero(scores > cutoff).tolist() - needed = k - len(above) - boundary = np.flatnonzero(scores == cutoff).tolist() - above.extend(sorted(boundary, key=ids.__getitem__)[:needed]) - above.sort(key=lambda index: (-float(scores[index]), ids[index])) - return above - class NumpyVectorIndex: """Store-backed brute-force cosine index. @@ -174,27 +153,4 @@ def search(self, vec: np.ndarray, k: int, raise ValueError( f"query dimension {q.shape[0]} does not match the index dimension {self.dim}" ) - with np.errstate(over="ignore", invalid="ignore"): - n = float(np.linalg.norm(q)) - if not np.isfinite(n): - raise ValueError("query vector norm must be finite") - if n == 0: - return [] - q = q / n - ids, mat = self.store.vector_matrix( - filter, dim=self.dim if self.dim is not None else int(q.shape[0]) - ) - if not ids: - return [] - # Store filters by both the declared dimension and blob width, so legacy - # rows from another embedding space cannot break this exact matrix scan. - nonzero = np.any(mat != 0, axis=1) - if not np.all(nonzero): - ids = [memory_id for memory_id, keep in zip(ids, nonzero) if keep] - mat = mat[nonzero] - if not ids: - return [] - scores = mat @ q # cosine == dot for unit vectors - k = min(k, len(ids)) - top = _top_k_indices(scores, ids, k) - return [(ids[index], float(scores[index])) for index in top] + return canonical_vector_search(self.store, q, k, filter=filter) diff --git a/engraphis/backends/vector_sqlitevec.py b/engraphis/backends/vector_sqlitevec.py index 5bfcc58f..e276b593 100644 --- a/engraphis/backends/vector_sqlitevec.py +++ b/engraphis/backends/vector_sqlitevec.py @@ -143,11 +143,12 @@ def _native_vector_matches( def _native_mirror_covers_canonical(conn, dimension: int) -> bool: """Whether vec0 exactly mirrors every same-dimension canonical vector. - Both scans are keyset-paginated and all counterpart lookups stay below SQLite's + The scan is keyset-paginated and all counterpart lookups stay below SQLite's conservative variable limit. The caller supplies the transaction: writable callers hold ``BEGIN IMMEDIATE`` while publishing, and read-only callers hold one snapshot. """ after_id = "" + expected_native_count = 0 while True: canonical_rows = conn.execute( "SELECT v.id, v.vector FROM mem_vectors v " @@ -175,48 +176,18 @@ def _native_mirror_covers_canonical(conn, dimension: int) -> bool: native.get(memory_id), expected, dimension, ): return False + else: + expected_native_count += 1 after_id = ids[-1] if len(canonical_rows) < _COVERAGE_BATCH_SIZE: break - # The forward scan proves that nothing canonical is missing or stale. This reverse - # scan rejects orphaned native rows and rows whose canonical vector became zero or - # changed dimension after another backend wrote the portable mirror. - after_id = "" - while True: - native_rows = conn.execute( - "SELECT id, embedding FROM mem_vec_ann " - "WHERE id>? ORDER BY id LIMIT ?", - (after_id, _COVERAGE_BATCH_SIZE), - ).fetchall() - if not native_rows: - break - ids = [str(row["id"]) for row in native_rows] - marks = ",".join("?" for _ in ids) - canonical_rows = conn.execute( - "SELECT v.id, v.vector FROM mem_vectors v " - "JOIN memories m ON m.id=v.id " - f"WHERE v.dim=? AND v.id IN ({marks})", - (dimension, *ids), - ).fetchall() - canonical = {str(row["id"]): row["vector"] for row in canonical_rows} - for row in native_rows: - memory_id = str(row["id"]) - valid, expected = _expected_native_vector( - canonical.get(memory_id), dimension, - ) - if ( - not valid - or expected is None - or not _native_vector_matches( - row["embedding"], expected, dimension, - ) - ): - return False - after_id = ids[-1] - if len(native_rows) < _COVERAGE_BATCH_SIZE: - break - return True + # Every expected nonzero ID and its full vector content was verified above. + # vec0 IDs are unique, so matching total cardinality now excludes every extra + # orphan/zero/wrong-dimension row. Repeated ORDER BY id on vec0 is not an indexed + # range scan and needlessly sorts the full native table for each reverse batch. + native_total = conn.execute("SELECT COUNT(*) AS n FROM mem_vec_ann").fetchone() + return native_total is not None and int(native_total["n"]) == expected_native_count def _native_index_status(conn, dimension: int): @@ -305,6 +276,7 @@ def __init__(self, store: Store, dim: int) -> None: if owns_transaction: conn.execute("BEGIN") _, current = _native_index_status(conn, dimension) + self._verified_generation = store.vector_generation() except Exception: raise RuntimeError(_READ_ONLY_STALE_ERROR) from None finally: @@ -325,6 +297,7 @@ def __init__(self, store: Store, dim: int) -> None: "format_version INTEGER NOT NULL, dimension INTEGER NOT NULL)" ) existing, current = _native_index_status(conn, dimension) + self._verified_generation = store.vector_generation() if current else -1 # The composition root can inspect this capability before replaying the # canonical mem_vectors mirror after a table creation or format change. self.requires_rebuild = not current @@ -355,6 +328,12 @@ def __init__(self, store: Store, dim: int) -> None: conn.rollback() raise + def can_skip_hydration(self) -> bool: + """Skip replay only when no canonical mutation followed verified coverage.""" + with self.store.read_snapshot(): + return (not self.requires_rebuild + and self._verified_generation == self.store.vector_generation()) + def mark_rebuild_complete(self) -> None: """Publish native readiness only after the canonical mirror is fully hydrated.""" if self.store.read_only: @@ -378,12 +357,14 @@ def mark_rebuild_complete(self) -> None: ) if updated.rowcount != 1: raise RuntimeError("sqlite-vec rebuild state is missing or stale") + generation = self.store.vector_generation() conn.commit() except BaseException: if conn.transaction_owned_by_current_thread(): conn.rollback() raise self.requires_rebuild = False + self._verified_generation = generation def upsert(self, ids: list[str], vecs: np.ndarray, meta: Optional[list[dict]] = None, *, commit: bool = True) -> None: diff --git a/engraphis/classic_assets/dashboard.js b/engraphis/classic_assets/dashboard.js index 18be179d..9ca636f8 100644 --- a/engraphis/classic_assets/dashboard.js +++ b/engraphis/classic_assets/dashboard.js @@ -1,4 +1,4 @@ -const API=location.origin+'/api',TRIAL_DAYS=3; +const API=location.origin+'/api'; let WS=null, WORKSPACES=[], LIC=null, RELEASE_VERSION=''; const TITLES={overview:'Overview',recall:'Recall',memories:'Memories','mem-editor':'Memory',proactive:'Proactive recall',why:'Why',timeline:'Timeline',audit:'Audit trail',graph:'Knowledge Graph',analytics:'Hosted Analytics',health:'Memory Health',consolidate:'Consolidate',automation:'Hosted Automation',workspaces:'Workspaces',team:'Team Cloud',settings:'Settings'}; const ROUTE_SECTIONS={overview:'Operate',recall:'Operate',memories:'Operate','mem-editor':'Operate',proactive:'Operate',why:'History',timeline:'History',audit:'History',graph:'Relations',analytics:'Relations',health:'Relations',consolidate:'Engine',automation:'Engine',workspaces:'Operate',team:'Engine',settings:'Engine'}; @@ -179,7 +179,7 @@ async function loadOverviewAnalytics(){ lock.textContent='PRO'; lock.className='pill pill-muted'; const offerTrial=licTrialAvailable(); - el.innerHTML='
Hosted growth, retention distribution, and decay forecast.
'+esc(lockReason(false))+'
'+(offerTrial?' ':'')+'
'; + el.innerHTML='
Hosted growth, retention distribution, and decay forecast.
'+esc(lockReason(false))+'
'+(offerTrial?' ':'')+'
'; }else el.innerHTML='
'+esc(e.message)+'
'; } } @@ -224,7 +224,7 @@ function lockReason(team){const st=licAccessState(),ends=licTrialEnds(); if(st==='trial_expired')return `Your free trial has ended${ends?` (${esc(ends)})`:''}, so hosted features are locked. The trial cannot be started again.`; if(st==='lapsed')return `Your ${esc(licPlanName())} subscription is no longer active, so hosted features are locked until billing is up to date.`; if(st==='active')return `Your ${esc(licPlanName())} subscription does not include this.`; - if(licTrialAvailable())return `The email-confirmed, no-card trial lasts exactly ${TRIAL_DAYS} active days.`; + if(licTrialAvailable()){const days=licTrialDays(team?'team':'pro');return days?`The email-confirmed, no-card trial lasts exactly ${days} active days.`:'An email-confirmed, no-card trial is available; review its duration in Cloud.'} return 'Your free trial has already been used.'} /* The Team tab describes the hosted service; it is not an answer to a refused request, and it renders for every customer including the ones who are paying for Team. Handing it @@ -236,7 +236,8 @@ function teamTeaserNote(){const ends=licTrialEnds(); if(licPlanKey()!=='team'||!licAccessLive())return lockReason(true); if(licAccessState()==='trial')return `Your free trial includes Team${ends?` until ${esc(ends)}`:''}. Organizations, roles, and seats are managed in Engraphis Cloud.`; return 'Your TEAM subscription includes this. Organizations, roles, and seats are managed in Engraphis Cloud.'} -function hostedCta(plan,content,interval){const team=plan==='team',name=team?'Team':'Pro',state=licAccessState(),current=licPlanKey();if(state==='lapsed')return {label:'Update billing',href:hostedAccountUrl(content||'account'),kind:'account'};if(licAccessLive()&&(current===plan||(current==='team'&&plan==='pro')))return {label:current==='team'&&team?'Open Team Cloud':'Open Engraphis Cloud',href:hostedAccountUrl(content||'account'),kind:'account'};const trial=licTrialAvailable()&&state==='inactive';return {label:trial?`Start ${TRIAL_DAYS}-day ${name} trial`:`Subscribe to ${name}`,href:hostedPlanUrl(plan,trial,interval||'monthly',content||plan),kind:trial?'trial':'subscribe'} } +function licTrialDays(plan){const trial=(LIC&&LIC.trial)||{},days=(trial.days_by_plan||{})[plan]??(plan==='pro'?trial.trial_days:null);return Number.isSafeInteger(days)&&days>0?days:null} +function hostedCta(plan,content,interval){const team=plan==='team',name=team?'Team':'Pro',state=licAccessState(),current=licPlanKey();if(state==='lapsed')return {label:'Update billing',href:hostedAccountUrl(content||'account'),kind:'account'};if(licAccessLive()&&(current===plan||(current==='team'&&plan==='pro')))return {label:current==='team'&&team?'Open Team Cloud':'Open Engraphis Cloud',href:hostedAccountUrl(content||'account'),kind:'account'};const trial=licTrialAvailable()&&state==='inactive',days=licTrialDays(plan);return {label:trial?`Start ${days?`${days}-day `:''}${name} trial`:`Subscribe to ${name}`,href:hostedPlanUrl(plan,trial,interval||'monthly',content||plan),kind:trial?'trial':'subscribe'} } function ctaLinkHtml(cta,className,content){return `${esc(cta.label)}`} function unlockHtml(feature,plan){const team=plan==='team',name=team?'Team':'Pro',featureKey=`feature_${String(feature).toLowerCase().replace(/[^a-z0-9]+/g,'_')}`,primary=hostedCta(plan,featureKey),annual=primary.kind==='account'?'':{label:`Annual ${name} option`,href:hostedPlanUrl(plan,false,'annual',`${featureKey}_annual`),kind:'subscribe'},price=team?'$20 per seat/month or $200 per seat/year':'$10/month or $100/year',detail=lockReason(team),benefits=team?['Everything in Pro','Hosted organizations, invitations, and named seats','Roles, scoped credentials, and Team audit history']:['Hosted Cloud Sync across your installations','Growth, retention, decay, and entity Analytics','Auto Consolidation with hosted retention policies','Auto Dreaming with reviewable managed proposals','Priority support'],lede=team?'Team adds shared workspaces, named seats, roles, and remote agent access.':'Support continued Engraphis development with Pro. Your subscription helps cover hosted infrastructure and ongoing development while unlocking Cloud Sync, Analytics, Auto Consolidation, and Auto Dreaming across your installations.';return `
ENGRAPHIS ${name.toUpperCase()}

Unlock ${esc(feature)} and more

${lede}

${price}
Your license unlocks
    ${benefits.map(item=>`
  • ${esc(item)}
  • `).join('')}

${detail}

${ctaLinkHtml(primary,'btn btn-primary',name.toLowerCase())}${annual.href&&annual.href!=='#'?ctaLinkHtml(annual,'btn btn-ghost',`${featureKey}_annual`):''}
`} function startTrialPlan(plan){const url=hostedPlanUrl(plan,true);if(url==='#'){toast('Hosted signup URL is not configured','err');return}const link=document.createElement('a');link.href=url;link.target='_blank';link.rel='noopener';link.click()} @@ -266,8 +267,8 @@ function statMini(v,l,color){const tone=color==='var(--red)'?' tone-red':(color= function renderAnalytics(a,isPortfolio){const t=a.totals||{},f=a.decay_forecast||{};const weeks=a.growth_weekly||[];const gp=Math.max(...weeks,1);const gitems=weeks.map((n,i)=>{const back=weeks.length-1-i;return barRow(back===0?'now':back+'w ago',n,gp,'var(--accent-dim)')}).join('')||'
No data
';const hist=a.retention_histogram||{};const hc=hist.counts||[],hb=hist.buckets||[];const hp=Math.max(...hc,1);const hitems=hb.map((b,i)=>barRow(b,hc[i]||0,hp,'var(--green)')).join('');const mix=a.resolver_mix||{};const mk=Object.keys(mix);const mp=Math.max(...Object.values(mix),1);const mitems=mk.length?mk.map(k=>barRow(k,mix[k],mp,'var(--blue)')).join(''):'
No resolver events yet.
';const bt=a.by_type||{};const btk=Object.keys(bt);const bp=Math.max(...Object.values(bt),1);const btitems=btk.length?btk.map(k=>barRow(k,bt[k],bp,'var(--accent)')).join(''):'
No memories yet.
';const ents=a.top_entities||[];const ep=Math.max(...ents.map(e=>e.n),1);const eitems=ents.length?ents.map(e=>barRow(e.name+(isPortfolio&&e.workspace?' · '+e.workspace:''),e.n,ep,'var(--cyan)')).join(''):'
No entities yet — they appear as the graph grows.
';const avg=Math.round((t.avg_retention||0)*100);let wsTable='';if(isPortfolio&&a.workspaces){wsTable=`
Per-workspace breakdown
${a.workspaces.map(w=>``).join('')}
WorkspaceLivePinnedAvg ret.Fading 7d
${esc(w.workspace)}${w.live}${w.pinned}${Math.round((w.avg_retention||0)*100)}%${w.at_risk_7d}
`}return `
${statMini(t.live!=null?t.live:'—','Live memories')}${statMini(avg+'%','Avg retention',avg<40?'var(--red)':(avg<70?'var(--amber)':'var(--green)'))}${statMini(f.at_risk_7d!=null?f.at_risk_7d:'—','Fading ≤ 7 days',f.at_risk_7d>0?'var(--amber)':'')}${statMini(f.at_risk_30d!=null?f.at_risk_30d:'—','Fading ≤ 30 days')}${statMini(t.pinned!=null?t.pinned:'—','Pinned (protected)')}${isPortfolio?statMini(t.workspaces||0,'Workspaces'):statMini(t.superseded!=null?t.superseded:'—','Superseded (history)')}
Memories written per week
${gitems}
Retention distribution
${hitems}
By type
${btitems}
Write-path resolver activity
${mitems}
Most connected entities
${eitems}
${wsTable}`} /* A consent-required response is a valuable moment to show the job Pro can do, not a dead end about configuration. A customer with live access must never be offered their - own plan again: hosted features are on by default once their account is available. */ -function managedConsentHtml(feature){const automation=/automation/i.test(feature),featureKey=`managed_${String(feature).toLowerCase().replace(/[^a-z0-9]+/g,'_')}`,live=licAccessLive(),trial=licTrialAvailable(),copy=automation?{eyebrow:'MEMORY MAINTENANCE',title:'Let your memory improve after you log off.',lede:'Turn repetitive cleanup into a steady, reviewable habit. Pro watches the rhythm of your workspace and brings the useful changes back for approval.',cards:[['CONSOLIDATE','Distill recurring work into durable knowledge on a cadence you control.'],['DREAM','Surface useful links after accumulation and idle time, before fresh context gets buried.'],['REVIEW','Every managed result is a proposal. Nothing silently rewrites your local memory.']]}:{eyebrow:'MEMORY INTELLIGENCE',title:'See the memory your team is about to lose.',lede:'Pro turns your local memory into an operating signal—so you can see what is growing, what is fading, and what is quietly shaping recall.',cards:[['GROWTH','Separate knowledge that compounds from activity that only accumulates.'],['RETENTION','Catch fading context before an important answer disappears from reach.'],['ENTITY SIGNAL','See the people, projects, and ideas organizing your workspace.']]};const primary=hostedCta('pro',featureKey),annual=primary.kind==='account'?'':{label:'Annual Pro option',href:hostedPlanUrl('pro',false,'annual',`${featureKey}_annual`),kind:'subscribe'},actions=`${ctaLinkHtml(primary,'btn btn-primary',featureKey)}${annual.href?ctaLinkHtml(annual,'btn btn-ghost',`${featureKey}_annual`):''}`,next=live?'Included in your Pro plan. Hosted insights and maintenance are on by default—nothing else to configure.':licAccessState()==='lapsed'?'Your subscription needs billing attention. Update billing to restore hosted insights and maintenance.':trial?`Start with ${TRIAL_DAYS} days of Pro. Hosted insights and maintenance come on automatically—no settings, toggles, or worker setup.`:'Subscribe to Pro and hosted insights and maintenance come on automatically—no settings, toggles, or worker setup.';return `
ENGRAPHIS PRO /${copy.eyebrow}

${copy.title}

${copy.lede}

${next}

${actions}
WHAT PRO IS WATCHING
${copy.cards.map(card=>`
${card[0]}

${card[1]}

`).join('')}
Your memory stays yours. Hosted work is automatic with Pro. Secret and session-scoped memories stay local.
`} + own plan again. Readable processing requires a separate workspace choice. */ +function managedConsentHtml(feature){const automation=/automation/i.test(feature),featureKey=`managed_${String(feature).toLowerCase().replace(/[^a-z0-9]+/g,'_')}`,live=licAccessLive(),trial=licTrialAvailable(),copy=automation?{eyebrow:'MEMORY MAINTENANCE',title:'Let your memory improve after you log off.',lede:'Turn repetitive cleanup into a steady, reviewable habit. Pro watches the rhythm of your workspace and brings the useful changes back for approval.',cards:[['CONSOLIDATE','Distill recurring work into durable knowledge on a cadence you control.'],['DREAM','Surface useful links after accumulation and idle time, before fresh context gets buried.'],['REVIEW','Every managed result is a proposal. Nothing silently rewrites your local memory.']]}:{eyebrow:'MEMORY INTELLIGENCE',title:'See the memory your team is about to lose.',lede:'Pro turns your local memory into an operating signal—so you can see what is growing, what is fading, and what is quietly shaping recall.',cards:[['GROWTH','Separate knowledge that compounds from activity that only accumulates.'],['RETENTION','Catch fading context before an important answer disappears from reach.'],['ENTITY SIGNAL','See the people, projects, and ideas organizing your workspace.']]};const primary=hostedCta('pro',featureKey),annual=primary.kind==='account'?'':{label:'Annual Pro option',href:hostedPlanUrl('pro',false,'annual',`${featureKey}_annual`),kind:'subscribe'},settings=`Review workspace processing`,actions=`${settings}${ctaLinkHtml(primary,'btn btn-ghost',featureKey)}${annual.href?ctaLinkHtml(annual,'btn btn-ghost',`${featureKey}_annual`):''}`,next=live?'Included in your plan. Readable managed processing is paused until you approve this workspace in Manage > Settings.':licAccessState()==='lapsed'?'Your subscription needs billing attention. Restore access, then confirm workspace processing before new readable uploads.':trial?`Start ${licTrialDays('pro')?`with ${licTrialDays('pro')} days of Pro`:'a Pro trial'}. After connecting, explicitly approve each workspace in Manage > Settings.`:'Subscribe to Pro, then explicitly approve each workspace in Manage > Settings.';return `
ENGRAPHIS PRO /${copy.eyebrow}

${copy.title}

${copy.lede}

${esc(next)}

${actions}
WHAT PRO IS WATCHING
${copy.cards.map(card=>`
${card[0]}

${card[1]}

`).join('')}
Your memory stays yours. Readable processing requires your workspace approval. Encrypted Cloud Sync is a separate choice. Secret and session-scoped memories stay local.
`} function managedConsentRequired(error){return error&&error.status===409&&error.detail&&error.detail.code==='consent_required'} const CLOUD_SYNC_PRIVACY_COPY='Cloud Sync encrypts eligible shared-workspace changes end-to-end before they leave this device. Engraphis Cloud cannot read their contents; secret and session-scoped memories stay local.'; const EXTERNAL_LLM_PRIVACY_COPY='Memory text is sent to your configured LLM provider for processing under that provider’s terms. The provider must read that text to return extracted facts.'; @@ -299,8 +300,8 @@ async function loadAnalytics(){const el=document.getElementById('analytics-body' /* ── hosted automation policy (Pro / Team) ── */ async function loadAutomation(){const el=document.getElementById('automation-body'),lock=document.getElementById('au-lock'),ws='?workspace='+encodeURIComponent(WS||'');el.innerHTML='
';try{const p=await api('/automation'+ws);setPlanPill(lock,licTrialActive()?'TRIAL':'CLOUD','pill pill-accent');const last=p.last_run?fmtRel(p.last_run):'never',dream=p.dream_enabled!=null?p.dream_enabled:p.dream;el.innerHTML=`
Hosted maintenance policy
The cloud returns reviewable proposals. Pinned memories remain protected.
Cloud worker status
Status${p.enabled?'ENABLED':'OFF'}
Last run${esc(last)}
Requesting managed work uploads the selected workspace’s normal and sensitive memory content, excluding secret and session-scoped rows, capped at 16 MiB, over HTTPS without end-to-end encryption. Results are proposals and never automatically write the local database.
`}catch(e){if(managedConsentRequired(e)||cloudTrialSignupRequired(e)){setPlanPill(lock,'CLOUD','pill pill-accent');el.innerHTML=managedConsentHtml('Hosted Automation')}else if(hostedFeatureUnavailable(e)){setPlanPill(lock,'PRO','pill pill-muted');el.innerHTML=unlockHtml('Automation, Auto Consolidation, and Auto Dreaming','pro')}else{el.innerHTML='
'+esc(e.message)+'
'}}} -async function saveAutomation(){const body={enabled:document.getElementById('au-enabled').checked,cadence_hours:Number(document.getElementById('au-cadence').value)||24,consolidate:document.getElementById('au-consolidate').checked,min_cluster:Number(document.getElementById('au-mincluster').value)||3,archive_below:Number(document.getElementById('au-archive').value)||0.05,dream_enabled:document.getElementById('au-dream').checked,dream_min_new:Number(document.getElementById('au-dream-min').value)||20,dream_idle_minutes:Number(document.getElementById('au-dream-idle').value)};try{await api('/automation?workspace='+encodeURIComponent(WS||''),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});toast('Hosted policy saved','ok');loadAutomation()}catch(e){if(managedConsentRequired(e)){const result=document.getElementById('au-result');if(result)result.innerHTML=managedConsentHtml('Hosted Automation');toast('Hosted Automation starts automatically with Pro','err');return}toast((e.status===402||e.status===501)?'Hosted Automation requires Pro or Team':e.message,'err')}} -async function runMaintenance(){const el=document.getElementById('au-result');if(el)el.innerHTML='
';try{const d=await api('/maintenance/run?workspace='+encodeURIComponent(WS||''),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({dry_run:true})});if(el)el.innerHTML=`PROPOSAL Hosted work was submitted for review.
${esc(JSON.stringify(d,null,2))}
`;toast('Managed proposal requested','ok')}catch(e){if(el)el.innerHTML=managedConsentRequired(e)?managedConsentHtml('Hosted Automation'):'
'+esc(e.message)+'
';toast(managedConsentRequired(e)?'Hosted Automation starts automatically with Pro':((e.status===402||e.status===501)?'Hosted Automation requires Pro or Team':e.message),'err')}} +async function saveAutomation(){const body={enabled:document.getElementById('au-enabled').checked,cadence_hours:Number(document.getElementById('au-cadence').value)||24,consolidate:document.getElementById('au-consolidate').checked,min_cluster:Number(document.getElementById('au-mincluster').value)||3,archive_below:Number(document.getElementById('au-archive').value)||0.05,dream_enabled:document.getElementById('au-dream').checked,dream_min_new:Number(document.getElementById('au-dream-min').value)||20,dream_idle_minutes:Number(document.getElementById('au-dream-idle').value)};try{await api('/automation?workspace='+encodeURIComponent(WS||''),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});toast('Hosted policy saved','ok');loadAutomation()}catch(e){if(managedConsentRequired(e)){const result=document.getElementById('au-result');if(result)result.innerHTML=managedConsentHtml('Hosted Automation');toast('Approve workspace processing in Manage > Settings','err');return}toast((e.status===402||e.status===501)?'Hosted Automation requires Pro or Team':e.message,'err')}} +async function runMaintenance(){const el=document.getElementById('au-result');if(el)el.innerHTML='
';try{const d=await api('/maintenance/run?workspace='+encodeURIComponent(WS||''),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({dry_run:true})});if(el)el.innerHTML=`PROPOSAL Hosted work was submitted for review.
${esc(JSON.stringify(d,null,2))}
`;toast('Managed proposal requested','ok')}catch(e){if(el)el.innerHTML=managedConsentRequired(e)?managedConsentHtml('Hosted Automation'):'
'+esc(e.message)+'
';toast(managedConsentRequired(e)?'Approve workspace processing in Manage > Settings':((e.status===402||e.status===501)?'Hosted Automation requires Pro or Team':e.message),'err')}} const runMaintenanceBase=runMaintenance; const saveAutomationBase=saveAutomation; @@ -1219,7 +1220,7 @@ function loadAllGraphEngine(){ if(typeof EngraphisEveryGraph!=='undefined')return Promise.resolve(); if(!ALL_GRAPH_ENGINE_LOADING){ ALL_GRAPH_ENGINE_LOADING=new Promise((resolve,reject)=>{ - const script=document.createElement('script');script.src='/v2-assets/engraphis-graph-every.js?v=20260823-every-19'; + const script=document.createElement('script');script.src='/v2-assets/engraphis-graph-every.js?v=20260905-every-20'; script.onload=()=>{typeof EngraphisEveryGraph==='undefined'?reject(new Error('Every-node graph asset loaded without registering EngraphisEveryGraph')):resolve()}; script.onerror=()=>reject(new Error('Every-node graph asset could not load')); document.head.appendChild(script); diff --git a/engraphis/classic_assets/index.html b/engraphis/classic_assets/index.html index d89b535a..cc6dd96b 100644 --- a/engraphis/classic_assets/index.html +++ b/engraphis/classic_assets/index.html @@ -349,6 +349,6 @@ graph view. dashboard.js fetches both on demand from graphRender(); see loadForceGraph() and loadGraphEngine(). scripts/externalize_dashboard_assets.py enforces both halves: they stay out of this file, and the lazy references still have to resolve. --> - + diff --git a/engraphis/cloud_features.py b/engraphis/cloud_features.py index c48e5e42..6bfde8a5 100644 --- a/engraphis/cloud_features.py +++ b/engraphis/cloud_features.py @@ -10,7 +10,6 @@ import http.client import hashlib import json -import os import re import time import urllib.error @@ -128,26 +127,18 @@ def redirect_request(self, req, fp, code, msg, headers, newurl): _DRAIN_FAILURES = (OSError, ValueError, http.client.HTTPException) -def managed_compute_consent() -> bool: - """Return whether this installation may upload workspace content for managed work. +def managed_compute_consent(service: Any = None, workspace: Optional[str] = None) -> bool: + """Return explicit approval for this workspace; account connection never grants it. - Consent travels with the cloud account: connecting an installation to Engraphis Cloud - accepts the terms that cover managed compute, so a connected installation is allowed by - default and the customer is never asked to hand-edit an environment variable. - - A local installation with no cloud session is never allowed — there is no account, so - there is no agreement to rely on. - - ``ENGRAPHIS_MANAGED_COMPUTE_CONSENT`` remains an explicit override for operators who want - to force the answer either way; ``=0`` opts a connected installation back out. + The legacy environment variable may disable uploads, but cannot grant approval. + Calls without a workspace fail closed for compatibility with older callers. """ - override = os.environ.get("ENGRAPHIS_MANAGED_COMPUTE_CONSENT") - if override is not None and override.strip() != "": - return _truthy(override) + if service is None or not workspace: + return False + from engraphis.managed_processing import processing_policy try: - return bool(cloud_session_configured(require_compute=False)) - except Exception: - # Consent must never be the reason a dashboard fails to render. + return bool(processing_policy(service, workspace)["enabled"]) + except (ValueError, TypeError, OSError): return False @@ -333,24 +324,26 @@ def _build_managed_snapshot_locked(service: Any, workspace: str, *, generation: Optional[int] = None) -> tuple[str, dict]: """Build the bounded client-side transport document for one local workspace. - Secret-classified rows are omitted before serialization. ``consent`` allows an - already-confirmed caller to pass its decision explicitly; otherwise - :func:`managed_compute_consent` decides, which allows cloud-connected installations and - denies purely local ones. + Secret-classified rows are omitted before serialization. Persisted workspace + approval is always required. The legacy ``consent`` argument can veto processing, + but cannot override a missing or disabled workspace policy. """ clean_workspace = service._clean_ws(workspace) workspace_id = service._lookup_workspace(clean_workspace) if not workspace_id: raise CloudFeatureError("The selected workspace does not exist.", status=404) - allowed = managed_compute_consent() if consent is None else bool(consent) + allowed = managed_compute_consent(service, clean_workspace) and consent is not False if not allowed: raise CloudFeatureError( - "Managed compute is turned off for this installation, so no workspace content " - "was uploaded. Connect this installation to Engraphis Cloud to use it.", + "Managed processing requires approval for this workspace. No workspace content " + "was uploaded. Review the workspace processing controls to enable it.", status=409, code="consent_required", ) + from engraphis.managed_processing import processing_policy + local_policy = processing_policy(service, clean_workspace) + processing_revision = local_policy["remote_revision"] or local_policy["revision"] snapshot_generation = _reserve_snapshot_generation( service, workspace_id, requested=generation ) @@ -381,6 +374,7 @@ def _build_managed_snapshot_locked(service: Any, workspace: str, *, # ``false`` is one byte longer than ``true``. Budget the larger encoding so # protocol variants cannot cross the client cap at the exact boundary. "managed_compute_consent": False, + "processing_policy_revision": 9_223_372_036_854_775_807, "excluded_secret_count": MAX_MEMORIES, "memories": [], })) @@ -440,6 +434,7 @@ def _build_managed_snapshot_locked(service: Any, workspace: str, *, "schema": SNAPSHOT_SCHEMA, "generation": int(snapshot_generation), "managed_compute_consent": True, + "processing_policy_revision": processing_revision, "excluded_secret_count": excluded_secrets, "memories": memories, } @@ -548,6 +543,16 @@ def _workspace_path(self, workspace_id: str) -> str: return "/v1/organizations/%s/workspaces/%s" % ( quote(self.organization_id, safe=""), quote(workspace_id, safe="")) + def get_processing_policy(self, workspace_id: str) -> dict: + return self._request("GET", self._workspace_path(workspace_id) + "/processing-policy") + + def set_processing_policy(self, workspace_id: str, *, enabled: bool, + revision: int, confirmed: bool = False) -> dict: + if isinstance(revision, bool) or not isinstance(revision, int) or revision < 1: + raise CloudFeatureError("Invalid Cloud processing policy revision.", status=409) + return self._request("PUT", self._workspace_path(workspace_id) + "/processing-policy", + {"enabled": enabled, "confirmed": confirmed, "revision": revision}) + def upload_snapshot(self, workspace_id: str, snapshot: dict) -> dict: return self._request("POST", self._workspace_path(workspace_id) + "/snapshot", snapshot) diff --git a/engraphis/commercial.py b/engraphis/commercial.py index 663ca491..d75b8370 100644 --- a/engraphis/commercial.py +++ b/engraphis/commercial.py @@ -19,6 +19,19 @@ def manifest() -> dict: return json.loads(path.read_text(encoding="utf-8")) +def trial_days_by_plan() -> dict[str, int]: + """Return disclosed plan durations; missing or invalid durations stay unknown.""" + try: + days = manifest().get("trial", {}).get("days_by_plan", {}) + except (OSError, ValueError, TypeError, AttributeError): + return {} + if not isinstance(days, dict): + return {} + return {plan: value for plan, value in days.items() + if plan in {"pro", "team"} and isinstance(value, int) + and not isinstance(value, bool) and value > 0} + + def expected_checkout_targets() -> dict: """Return public onboarding targets without exposing provider-side price identifiers.""" # Read defensively: the release check calls this *after* collecting its own structural diff --git a/engraphis/commercial_manifest.json b/engraphis/commercial_manifest.json index 06261345..7e3ad842 100644 --- a/engraphis/commercial_manifest.json +++ b/engraphis/commercial_manifest.json @@ -14,14 +14,23 @@ "trial": { "days": 3, "card_required": false, - "plans": ["pro", "team"] + "plans": [ + "pro", + "team" + ], + "days_by_plan": { + "pro": 3, + "team": 10 + } }, "entitlement_lifecycle": { "max_grace_hours": 24, "grace_mode": "workspace_write_grace", "enforced_by": "private_control_plane", "grace_for": "already_authorized_hosted_accounts", - "grace_allows": ["authenticated_existing_user_hosted_account_continuity"], + "grace_allows": [ + "authenticated_existing_user_hosted_account_continuity" + ], "live_authorization_still_required_for": [ "paid_or_cost_bearing_features", "hosted_mcp_or_agent_writes" diff --git a/engraphis/core/browsing.py b/engraphis/core/browsing.py new file mode 100644 index 00000000..5f35f5e9 --- /dev/null +++ b/engraphis/core/browsing.py @@ -0,0 +1,131 @@ +"""Bounded memory browsing using the canonical store scope and temporal rules. + +Cursors bind the query, time anchors, ordering and active connection revision. A +changed database requires a fresh first page instead of silently skipping records. +""" +from __future__ import annotations + +import base64 +import hashlib +import json +import math +import time +from dataclasses import asdict, replace +from typing import Any, Optional, Protocol + +from .interfaces import SearchFilter + + +class BrowseStore(Protocol): + conn: Any + + def _where(self, flt: Optional[SearchFilter], include_invalid: bool, + alias: str = "") -> tuple[list[str], list[Any]]: ... + + +class BrowseCursorStale(ValueError): + """The ordering changed since the previous page.""" + + +def _revision(conn: Any) -> list[int]: + return [id(conn), int(conn.execute("PRAGMA data_version").fetchone()[0]), + int(conn.total_changes)] + + +def _cursor_number(value: Any) -> bool: + # JSON integers are unbounded; SQLite parameters are signed 64-bit integers. + # Reject oversized integers before float conversion or database binding. + return (not isinstance(value, bool) and isinstance(value, (int, float)) + and (not isinstance(value, int) or -(1 << 63) <= value < (1 << 63)) + and math.isfinite(value)) + + +def _decode(cursor: str) -> dict[str, Any]: + try: + if len(cursor) > 4096: + raise ValueError + value = json.loads(base64.b64decode(cursor.encode("ascii"), altchars=b"-_", + validate=True)) + if not isinstance(value, dict) or value.get("v") != 1: + raise ValueError + anchors = value["anchors"] + if len(anchors) != 2 or any(not _cursor_number(x) for x in anchors): + raise ValueError + position = value["position"] + if not isinstance(position, list) or len(position) != 4: + raise ValueError + if position[0] not in (0, 1) or any(not _cursor_number(x) for x in position[:3]) \ + or not isinstance(position[3], str) or len(position[3]) > 200: + raise ValueError + return value + except (ValueError, TypeError, KeyError, UnicodeError, RecursionError) as exc: + raise ValueError("invalid memory cursor") from exc + + +def browse_memories(store: BrowseStore, flt: SearchFilter, *, q: str = "", + limit: int = 200, cursor: str = "") -> dict[str, Any]: + """Return a consistent bounded page and exact filtered count without embeddings.""" + if isinstance(limit, bool) or not isinstance(limit, int) or not 1 <= limit <= 1000: + raise ValueError("limit must be between 1 and 1000") + if not isinstance(q, str) or len(q) > 10_000: + raise ValueError("invalid memory search") + if not isinstance(cursor, str): + raise ValueError("invalid memory cursor") + identity = hashlib.sha256(json.dumps( + {"filter": asdict(flt), "q": q}, sort_keys=True, default=str, + separators=(",", ":"), + ).encode()).hexdigest() + previous = _decode(cursor) if cursor else None + if previous and previous.get("query") != identity: + raise ValueError("memory cursor does not match the query") + now = time.time() + anchors = (previous["anchors"] if previous else [ + flt.valid_at if flt.valid_at is not None else flt.as_of if flt.as_of is not None else now, + flt.known_at if flt.known_at is not None else now, + ]) + anchored = replace(flt, as_of=None, valid_at=anchors[0], known_at=anchors[1]) + conn = store.conn + owns = not conn.transaction_owned_by_current_thread() + try: + if owns: + conn.execute("BEGIN") + revision = _revision(conn) + if previous and previous.get("revision") != revision: + raise BrowseCursorStale("memory listing changed; restart from the first page") + where, params = store._where(anchored, include_invalid=False) + if q: + escaped = q.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_") + where.append("(title LIKE ? ESCAPE '\\' OR content LIKE ? ESCAPE '\\' " + "OR summary LIKE ? ESCAPE '\\')") + params.extend(["%" + escaped + "%"] * 3) + predicate = " AND ".join(where) or "1" + total = int(conn.execute("SELECT COUNT(*) FROM memories WHERE " + predicate, + params).fetchone()[0]) + order = "(sort_order IS NULL), COALESCE(sort_order,0), -COALESCE(last_access,valid_from,0), id" + if previous: + predicate += " AND (" + order + ") > (?,?,?,?)" + params.extend(previous["position"]) + rows = conn.execute( + "SELECT * FROM memories WHERE " + predicate + " ORDER BY " + order + " LIMIT ?", + [*params, limit + 1], + ).fetchall() + has_more = len(rows) > limit + page = [dict(row) for row in rows[:limit]] + next_cursor = None + if has_more and page: + last = page[-1] + sort = last["sort_order"] + recent = last["last_access"] + if recent is None: + recent = last["valid_from"] + payload = {"v": 1, "query": identity, "revision": revision, "anchors": anchors, + "position": [int(sort is None), sort if sort is not None else 0, + -(recent if recent is not None else 0), last["id"]]} + next_cursor = base64.urlsafe_b64encode(json.dumps( + payload, separators=(",", ":"), allow_nan=False, + ).encode()).decode() + return {"rows": page, "total_count": total, "next_cursor": next_cursor, + "valid_at": anchors[0], "known_at": anchors[1]} + finally: + if owns and conn.transaction_owned_by_current_thread(): + conn.rollback() diff --git a/engraphis/core/context.py b/engraphis/core/context.py index fc960976..3591175e 100644 --- a/engraphis/core/context.py +++ b/engraphis/core/context.py @@ -7,8 +7,6 @@ """ from __future__ import annotations -import copy -import dataclasses import math import re from collections.abc import Callable @@ -23,7 +21,6 @@ _TOKEN_RE = re.compile(r"\w+|[^\w\s]", re.UNICODE) _SENTENCE_RE = re.compile(r"(?<=[.!?])(?:[\"')\]]*)\s+|\n+") -_CLAUSE_SPLIT_RE = re.compile(r"(?<=[.!?;])(?:[\"')\]]*)\s+|\n+") _WORD_RE = re.compile(r"\w+", re.UNICODE) _BRIDGE_TERMS = frozenset({ "call", "calls", "called", "caller", "dependency", "depends", "flow", @@ -55,91 +52,9 @@ def packed(self) -> list[PackedChunk]: return self.chunks -def _extract_shingles(text: str, n: int = 4) -> set[tuple[str, ...]]: - """Extract case-folded n-gram token shingles from text.""" - words = [match.group(0).casefold() for match in _WORD_RE.finditer(text or "")] - if not words: - return set() - if len(words) < n: - return {tuple(words)} - return {tuple(words[i : i + n]) for i in range(len(words) - n + 1)} - - -def _split_clauses(text: str) -> list[str]: - """Split text into sentence/clause units while preserving text content.""" - source = (text or "").strip() - if not source: - return [] - parts = [part.strip() for part in _CLAUSE_SPLIT_RE.split(source) if part.strip()] - return parts if parts else [source] - - -def _normalize_clause(clause: str) -> str: - """Case-folded normalized word sequence for exact clause matching.""" - return " ".join(_WORD_RE.findall(clause.casefold())) - - -def _is_clause_redundant( - clause: str, - admitted_shingles: set[tuple[str, ...]], - admitted_clauses: set[str], - admitted_qualifiers: set[str], - *, - shingle_size: int = 4, - duplication_threshold: float = 0.6, -) -> bool: - """Whether a candidate clause has significant verbatim overlap with admitted evidence.""" - norm = _normalize_clause(clause) - if not norm: - return True - - words = [match.group(0).casefold() for match in _WORD_RE.finditer(clause)] - if not words: - return True - - # 1. Exact verbatim match against already admitted clauses - if norm in admitted_clauses: - return True - - # Check semantic safety for qualifiers: never prune a clause that introduces - # an exception or restriction not already covered - clause_qualifiers = _terms(clause) & _QUALIFIER_TERMS - if not clause_qualifiers.issubset(admitted_qualifiers): - return False - - # 2. For short clauses (< shingle_size words), check exact clause containment - if len(words) < shingle_size: - return any(norm == ac for ac in admitted_clauses) - - # 3. For multi-word clauses, check token shingle duplication ratio - shingles = _extract_shingles(clause, n=shingle_size) - if not shingles: - return False - - overlap = len(shingles & admitted_shingles) - duplication_ratio = overlap / len(shingles) - return duplication_ratio >= duplication_threshold - - -def _with_pruned_content( - candidate: Candidate, content: str, summary: str -) -> Candidate: - """Create a shallow clone of candidate with pruned delta content/summary.""" - record = candidate.record - if record is None: - return candidate - if dataclasses.is_dataclass(record): - new_record = dataclasses.replace(record, content=content, summary=summary) - else: - new_record = copy.copy(record) - new_record.content = content - new_record.summary = summary - if dataclasses.is_dataclass(candidate): - return dataclasses.replace(candidate, record=new_record) - else: - new_candidate = copy.copy(candidate) - new_candidate.record = new_record - return new_candidate +def _protected_sentence(text: str) -> bool: + """Conditions and numerical claims must retain their complete bindings.""" + return bool(_terms(text) & _QUALIFIER_TERMS) or bool(re.search(r"\d", text)) class RegexTokenCounter: @@ -157,7 +72,7 @@ class DeterministicContextPacker: Selection is stable for identical inputs. A supersession/consolidation family contributes at most one member, summaries are preferred when they retain query evidence, and oversized sources are reduced at sentence - boundaries before a final token-boundary fallback. + boundaries. A complete evidence unit that cannot fit is omitted. """ def __init__( @@ -179,6 +94,10 @@ def __init__( or getattr(self._count, "__name__", None) or type(self._count).__name__ ) + # Keep legacy pruning options accepted for caller compatibility. Shared + # text across distinct records does not establish equivalent evidence: + # titles, scope, provenance and neighboring sentences bind its meaning. + # Only the established identity/family selection deduplicates sources. self.redundancy_pruning = bool(redundancy_pruning) self.score_elbow_gating = bool(score_elbow_gating) self.elbow_ratio = float(elbow_ratio) @@ -219,9 +138,6 @@ def pack( top_score = max((float(c.score) for c in ordered), default=0.0) admitted_scores: list[float] = [] - admitted_shingles: set[tuple[str, ...]] = set() - admitted_clauses: set[str] = set() - admitted_qualifiers: set[str] = set() while remaining: # Re-evaluate novelty after every selection. This gives compact, @@ -252,42 +168,9 @@ def pack( ): continue - # Inter-candidate clause redundancy pruning: - # If higher-priority memories have already been admitted, prune - # duplicate clauses to retain and pack only novel delta content. - candidate_to_pack = candidate - is_delta = False - if self.redundancy_pruning and admitted_shingles: - full_content = record.content or "" - summary_content = record.summary or "" - - pruned_content, content_pruned = self._prune_redundant_clauses( - full_content, - admitted_shingles, - admitted_clauses, - admitted_qualifiers, - ) - pruned_summary, summary_pruned = self._prune_redundant_clauses( - summary_content, - admitted_shingles, - admitted_clauses, - admitted_qualifiers, - ) - - has_original_text = bool(full_content.strip() or summary_content.strip()) - has_novel_text = bool(pruned_content.strip() or pruned_summary.strip()) - if has_original_text and not has_novel_text: - continue - - if content_pruned or summary_pruned: - is_delta = True - candidate_to_pack = _with_pruned_content( - candidate, pruned_content, pruned_summary - ) - prefix = "\n\n" if context else "" ordinal = len(packed) + 1 - header = self._header(candidate_to_pack, ordinal) + header = self._header(candidate, ordinal) base = f"{context}{prefix}{header}\n" excerpt = "" truncated = False @@ -295,7 +178,7 @@ def pack( available = max(0, budget - self._count(base)) if available: excerpt, truncated, reason = self._excerpt( - query, candidate_to_pack, available + query, candidate, available ) # Keep the established single-pass behavior for ordinary sources. @@ -303,27 +186,21 @@ def pack( # excerpt already starts with the exact displayed title (or the titled # header left no room). This removes prompt duplication without deleting # evidence or weakening the stable ``[n]`` citation bridge. - rec = candidate_to_pack.record or record - if not excerpt or _starts_with_title(excerpt, rec.title): + if not excerpt or _starts_with_title(excerpt, record.title): compact_base = ( f"{context}{prefix}" - f"{self._header(candidate_to_pack, ordinal, include_title=False)}\n" + f"{self._header(candidate, ordinal, include_title=False)}\n" ) if self._count(compact_base) < budget: compact_available = budget - self._count(compact_base) - compact = self._excerpt(query, candidate_to_pack, compact_available) - if compact[0] and _starts_with_title(compact[0], rec.title): + compact = self._excerpt(query, candidate, compact_available) + if compact[0] and _starts_with_title(compact[0], record.title): base = compact_base available = compact_available excerpt, truncated, reason = compact if not excerpt: continue - if is_delta: - truncated = True - if not reason or reason in ("full", "summary"): - reason = "novel_delta" - proposed = f"{base}{excerpt}" if self._count(proposed) > budget: # A custom tokenizer need not be additive. Fit against the @@ -351,14 +228,8 @@ def pack( )) covered.update(_terms(excerpt) & query_terms) - # Track admitted evidence for subsequent redundancy pruning and elbow gating + # Track admitted scores for subsequent elbow gating. admitted_scores.append(float(candidate.score)) - admitted_shingles.update(_extract_shingles(excerpt, n=self.shingle_size)) - for cl in _split_clauses(excerpt): - norm_cl = _normalize_clause(cl) - if norm_cl: - admitted_clauses.add(norm_cl) - admitted_qualifiers.update(_terms(excerpt) & _QUALIFIER_TERMS) context_tokens = self._count(context) omitted = len(candidates) - len(packed) @@ -375,46 +246,6 @@ def pack( pack_context = pack - def _prune_redundant_clauses( - self, - text: str, - admitted_shingles: set[tuple[str, ...]], - admitted_clauses: set[str], - admitted_qualifiers: set[str], - ) -> tuple[str, bool]: - """Prune redundant clauses from text, returning (novel_delta_text, was_pruned).""" - if not text or not self.redundancy_pruning: - return text, False - - clauses = _split_clauses(text) - if not clauses: - return "", False - - novel_clauses: list[str] = [] - pruned_any = False - - for clause in clauses: - if _is_clause_redundant( - clause, - admitted_shingles, - admitted_clauses, - admitted_qualifiers, - shingle_size=self.shingle_size, - duplication_threshold=self.clause_duplication_threshold, - ): - pruned_any = True - else: - novel_clauses.append(clause) - - if not novel_clauses: - return "", True - - if not pruned_any: - return text, False - - delta_text = " ".join(novel_clauses) - return delta_text, True - def _is_score_elbow( self, candidate: Candidate, @@ -543,12 +374,25 @@ def _summary_is_useful( ) -> bool: if not full: return True + source_sentences = { + part.strip() for part in _SENTENCE_RE.split(full) if part.strip() + } + summary_sentences = { + part.strip() for part in _SENTENCE_RE.split(summary) + if part.strip() and part.strip() != "[…]" + } + # A summary's shared vocabulary is not proof of source entailment. + # Admit extractive sentences only, preserving complete conditions and + # numerical claims rather than merely their qualifier/value tokens. + if not summary_sentences or not summary_sentences.issubset(source_sentences): + return False + protected = {part for part in source_sentences if _protected_sentence(part)} + if not protected.issubset(summary_sentences): + return False full_overlap = _terms(full) & query_terms summary_terms = _terms(summary) preserves_query = not full_overlap or bool(summary_terms & full_overlap) - qualifiers = _terms(full) & _QUALIFIER_TERMS - preserves_qualifiers = qualifiers.issubset(summary_terms) - return preserves_query and preserves_qualifiers + return preserves_query def _sentence_excerpt( self, @@ -607,59 +451,17 @@ def _fit_text( ) -> str: if max_tokens <= 0: return "" - required_qualifiers = _terms(text) & _QUALIFIER_TERMS - - def semantically_safe(excerpt: str) -> bool: - return required_qualifiers.issubset(_terms(excerpt)) - - tokens = list(_TOKEN_RE.finditer(text)) - if not tokens: + # Neither a token nor a character prefix proves a complete claim. An + # English qualifier list cannot protect French, Chinese, identifiers, + # or a value/scope at the end of a sentence. Sentence selection happens + # before this fallback; here the whole selected evidence unit fits or + # is omitted, including with context-sensitive custom token counters. + text = text.strip() + if self._count(text) > max_tokens: return "" - limit = min(len(tokens), max_tokens) - while limit > 0: - end = tokens[limit - 1].end() - excerpt = text[:end].rstrip() - if limit < len(tokens) and max_tokens > 1: - marked = f"{excerpt} […]" - if self._count(marked) <= max_tokens: - excerpt = marked - within_local = self._count(excerpt) <= max_tokens - within_total = ( - total_budget is None - or self._count(f"{prefix}{excerpt}") <= total_budget - ) - if within_local and within_total and semantically_safe(excerpt): - return excerpt - limit -= 1 - # A custom token counter may split a single regex token (for example a - # character counter or provider tokenizer). In that case there is no - # shorter regex boundary to try, even though a character prefix fits. - # Find the longest safe prefix against the declared counter so tight - # budgets are still used without violating the hard ceiling. - low, high = 1, len(text) - best = "" - while low <= high: - middle = (low + high) // 2 - excerpt = text[:middle].rstrip() - if not excerpt: - low = middle + 1 - continue - marked = f"{excerpt} […]" if middle < len(text) else excerpt - candidate = marked if self._count(marked) <= max_tokens else excerpt - fits = ( - self._count(candidate) <= max_tokens - and ( - total_budget is None - or self._count(f"{prefix}{candidate}") <= total_budget - ) - ) - if fits: - if semantically_safe(candidate): - best = candidate - low = middle + 1 - else: - high = middle - 1 - return best + if total_budget is not None and self._count(f"{prefix}{text}") > total_budget: + return "" + return text def _header( self, diff --git a/engraphis/core/engine.py b/engraphis/core/engine.py index ce2da91d..290b6364 100644 --- a/engraphis/core/engine.py +++ b/engraphis/core/engine.py @@ -76,6 +76,8 @@ now_ts, ) from engraphis.core.textutil import estimate_tokens, jaccard, tokenize +from engraphis.core.vector_repair import canonical_search_required, index_repair_identity +from engraphis.core.vector_search import canonical_vector_search def _safe_upsert(index, ids, vecs, meta=None, *, commit=True): @@ -788,6 +790,17 @@ def _hydrate_separate_vector_index(self, fingerprint: str) -> None: """ if not vector_index_requires_sync(self.index, self.store): return + target = index_repair_identity(self.index, self.store) + if target is not None: + self.store.register_vector_index(target) + while self.store.vector_index_pending(target): + repaired = self.repair_vector_index(limit=EMBEDDING_REBUILD_BATCH) + if repaired["repaired"] == 0: + raise RuntimeError("external vector index repair is incomplete") + return + can_skip = getattr(self.index, "can_skip_hydration", None) + if callable(can_skip) and can_skip(): + return ids: list[str] = [] vectors: list[np.ndarray] = [] for memory_id, vector in self.store.iter_vectors( @@ -968,98 +981,44 @@ def remember_with_resolution(self, content: str, *, workspace_id: str, # must never consume a vector slot or become semantic retrieval candidates. vec = None if poisoning.quarantined else self.embedder.embed([text])[0] - # One writer at a time from neighbor-lookup through insert/invalidate: without - # this, two concurrent near-duplicate remembers can BOTH observe "no neighbor" - # and both resolve ADD — duplicating instead of NOOP/INVALIDATE — because the - # store's per-statement serialization cannot span this read-decide-write - # sequence. Same single-process posture as the rest of the engine (the store is - # one shared connection); multi-process writers are out of scope by design. + # Reserve SQLite's writer before neighbor discovery, after embedding. The + # database boundary serializes independent engines/processes as well as + # threads, and defers nested helper commits until the decision is durable. with self._write_lock: - caller_owned_transaction = ( - self.store.conn.transaction_owned_by_current_thread() - ) - if ( - caller_owned_transaction - and vec is not None - and vector_index_requires_sync(self.index, self.store) + caller_owned_transaction = self.store.conn.transaction_owned_by_current_thread() + external_index = bool( + vector_index_requires_sync(self.index, self.store) and not vector_index_shares_store_transaction(self.index, self.store) - ): - # A separate backend has no hook into a caller's later commit/rollback. - # Publishing now can orphan a vector; waiting would silently leave a - # committed memory unindexed. Fail before any Store mutation and leave - # ownership and rollback policy entirely with the caller. + ) + if caller_owned_transaction and vec is not None and external_index: raise RuntimeError( "caller-owned transactions cannot write through a separate vector " "index; commit or roll back before remembering" ) - owns_session_transaction = False - owns_lifecycle_transaction = False - try: - if (_transactional_finalizer is not None - and not self.store.conn.transaction_owned_by_current_thread()): - self.store.conn.execute("BEGIN IMMEDIATE") - owns_lifecycle_transaction = True + with self.store.write_transaction(): + target = index_repair_identity(self.index, self.store) + if target is not None: + self.store.register_vector_index(target) if session_id: - owns_session_transaction = self.store.begin_session_write( - session_id, workspace_id=workspace_id, repo_id=repo_id - ) - # A separate index cannot participate in the Store transaction. Delay - # publication until every remaining engine mutation has succeeded; an - # engine-owned session/lifecycle transaction is committed first. - # Caller-owned transactions with a separate backend were rejected above; - # Store-sharing indexes need no duplicate publication. - defer_external_index = bool( - self.store.conn.transaction_owned_by_current_thread() - and vector_index_requires_sync(self.index, self.store) - and not vector_index_shares_store_transaction( - self.index, self.store, + self.store.begin_session_write( + session_id, workspace_id=workspace_id, repo_id=repo_id, ) + result = self._resolve_and_store( + content, text=text, vec=vec, workspace_id=workspace_id, + repo_id=repo_id, session_id=session_id, mtype=mtype, scope=scope, + title=title, importance=importance, confidence=confidence, + keywords=keywords, metadata=write_metadata, + valid_from=valid_from, resolve_conflicts=resolve_conflicts, + candidate_k=candidate_k, subject_key=subject_key, + claim_kind=claim_kind, trusted_graph_keys=_trusted_graph_keys, + poisoning=poisoning, trusted_write=trusted_write, + transactional_finalizer=_transactional_finalizer, + defer_external_index=external_index, + extra_neighbors=extra_neighbors, ) - if _transactional_finalizer is None: - result = self._resolve_and_store( - content, text=text, vec=vec, workspace_id=workspace_id, - repo_id=repo_id, session_id=session_id, mtype=mtype, scope=scope, - title=title, importance=importance, confidence=confidence, - keywords=keywords, metadata=write_metadata, - valid_from=valid_from, resolve_conflicts=resolve_conflicts, - candidate_k=candidate_k, subject_key=subject_key, - claim_kind=claim_kind, trusted_graph_keys=_trusted_graph_keys, - poisoning=poisoning, trusted_write=trusted_write, - defer_external_index=defer_external_index, - extra_neighbors=extra_neighbors, - ) - if ( - owns_session_transaction - and self.store.conn.transaction_owned_by_current_thread() - ): - self.store.conn.commit() - if defer_external_index: - self._publish_result_vector(result, vec) - return result - with self.store.conn.defer_commits(): - result = self._resolve_and_store( - content, text=text, vec=vec, workspace_id=workspace_id, - repo_id=repo_id, session_id=session_id, mtype=mtype, scope=scope, - title=title, importance=importance, confidence=confidence, - keywords=keywords, metadata=write_metadata, - valid_from=valid_from, resolve_conflicts=resolve_conflicts, - candidate_k=candidate_k, subject_key=subject_key, - claim_kind=claim_kind, trusted_graph_keys=_trusted_graph_keys, - poisoning=poisoning, trusted_write=trusted_write, - transactional_finalizer=_transactional_finalizer, - defer_external_index=defer_external_index, - extra_neighbors=extra_neighbors, - ) - if owns_lifecycle_transaction: - self.store.conn.commit() - if defer_external_index: - self._publish_result_vector(result, vec) - return result - except BaseException: - if ((owns_session_transaction or owns_lifecycle_transaction) - and self.store.conn.transaction_owned_by_current_thread()): - self.store.conn.rollback() - raise + if external_index: + self._publish_result_vector(result, vec) + return result def remember_many(self, facts, *, workspace_id: str, repo_id: Optional[str] = None, session_id: Optional[str] = None, @@ -1241,11 +1200,14 @@ def remember_many(self, facts, *, workspace_id: str, caller_owned_transaction = ( self.store.conn.transaction_owned_by_current_thread() ) + external_index = bool( + vector_index_requires_sync(self.index, self.store) + and not vector_index_shares_store_transaction(self.index, self.store) + ) if ( caller_owned_transaction and any(item["vec"] is not None for item in prepared) - and vector_index_requires_sync(self.index, self.store) - and not vector_index_shares_store_transaction(self.index, self.store) + and external_index ): raise RuntimeError( "caller-owned transactions cannot write through a separate vector " @@ -1265,6 +1227,9 @@ def remember_many(self, facts, *, workspace_id: str, self.store.conn.execute("BEGIN IMMEDIATE") owns_transaction = True with self.store.conn.defer_commits(): + target = index_repair_identity(self.index, self.store) + if target is not None: + self.store.register_vector_index(target) for index_i, item in enumerate(prepared): # Vector-search candidates are already filtered to the # resolving fact's memory type; siblings must obey the same @@ -1288,7 +1253,10 @@ def remember_many(self, facts, *, workspace_id: str, claim_kind=item["claim_kind"], poisoning=item["poisoning"], trusted_write=item["trusted_write"], - defer_external_index=True, + # A Store-sharing native table must publish inside + # this batch transaction, so its failure rolls back + # every canonical and derived row together. + defer_external_index=external_index, extra_neighbors=extra_neighbors, ) results.append(result) @@ -1302,7 +1270,8 @@ def remember_many(self, facts, *, workspace_id: str, resolved.append((index_i, rec)) inserted.append((mid, item)) if ( - result.get("op") in {"add", "invalidate", "relate"} + external_index + and result.get("op") in {"add", "invalidate", "relate"} and item["vec"] is not None and isinstance(mid, str) and mid ): @@ -1392,31 +1361,94 @@ def _publish_result_vector(self, result: dict, vec: Optional[np.ndarray]) -> Non raise RuntimeError("stored memory result is missing its id") self._upsert_external_vector(memory_id, vec) + def repair_vector_index(self, *, limit: int = 100, + memory_id: Optional[str] = None) -> dict[str, int]: + """Replay bounded durable work from canonical state, without a restart. + + An external adapter needs a stable ``index_identity`` per physical index. + The queue stores only hashed target identities, memory ids and generations. + Each publication holds a writer reservation through acknowledgement so an + erasure cannot race the lookup and resurrect a deleted vector afterwards. + """ + if isinstance(limit, bool) or not isinstance(limit, int) or limit < 1 or limit > 1000: + raise ValueError("repair limit must be an integer between 1 and 1000") + target = index_repair_identity(self.index, self.store) + if target is None: + return {"attempted": 0, "repaired": 0, "pending": 0} + if self.store.read_only or self.store.conn.transaction_owned_by_current_thread(): + raise RuntimeError("vector repair requires an independent writable transaction") + self.store.register_vector_index(target) + if ((not _is_memory_database_path(self.store.path) + or self.store.active_embedding_space() is not None) + and not self.store.embedding_space_ready(self.embedding_space)): + return {"attempted": 0, "repaired": 0, + "pending": self.store.vector_index_pending(target) or 0} + attempted = repaired = 0 + while attempted < limit: + selected_id = "" + try: + with self.store.write_transaction(): + sql = ("SELECT memory_id, generation FROM vector_index_repairs " + "WHERE identity=?") + params: list[Any] = [target] + if memory_id is not None: + sql += " AND memory_id=?" + params.append(memory_id) + row = self.store.conn.execute( + sql + " ORDER BY generation, memory_id LIMIT 1", params, + ).fetchone() + if row is None: + break + selected_id = str(row["memory_id"]) + attempted += 1 + record = self.store.get_memory(selected_id) + vector = self.store.conn.execute( + "SELECT vector, dim, model FROM mem_vectors WHERE id=?", (selected_id,), + ).fetchone() + if (record is not None and vector is not None + and inspection_eligible(record.provenance, record.metadata)): + if (str(vector["model"] or "") != self.embedding_space + or int(vector["dim"]) != int(self.embedder.dim)): + raise RuntimeError("canonical vector space changed during repair") + values = np.frombuffer(vector["vector"], dtype=np.float32) + _safe_upsert( + self.index, [selected_id], values.reshape(1, -1), + [{"model": self.embedding_space}], + ) + else: + self.index.delete([selected_id]) + self.store.conn.execute( + "DELETE FROM vector_index_repairs " + "WHERE identity=? AND memory_id=? AND generation=?", + (target, selected_id, row["generation"]), + ) + repaired += 1 + except Exception as exc: # noqa: BLE001 - retain durable work for the next retry + logger.warning("vector-index repair failed for %s (%s)", + selected_id, type(exc).__name__) + try: + self.store.audit( + "engine", "index_upsert_failed", selected_id, + "failure_type=%s" % type(exc).__name__, + ) + except Exception as audit_exc: # noqa: BLE001 - durable queue remains authoritative + self._warn_redacted_failure("vector-index failure audit", audit_exc) + break + return {"attempted": attempted, "repaired": repaired, + "pending": self.store.vector_index_pending(target) or 0} + def _upsert_external_vector(self, memory_id: str, vec: np.ndarray) -> None: - """Best-effort synchronization for indexes outside the canonical Store.""" + """Publish a canonical mutation, retaining failed external work durably.""" if not vector_index_requires_sync(self.index, self.store): return - try: - _safe_upsert( - self.index, - [memory_id], - vec.reshape(1, -1), - [{"model": self.embedding_space}], - ) - except Exception as exc: # noqa: BLE001 — a failed index write must not lose the memory - # The canonical Store vector is authoritative. Keep the write, but make the - # derived-index gap content-free and visible to operators. Never commit a - # caller-owned Store transaction merely to persist this diagnostic. - logger.warning("vector-index upsert failed for %s (%s)", - memory_id, type(exc).__name__) - try: - self.store.audit( - "engine", "index_upsert_failed", memory_id, - "failure_type=%s" % type(exc).__name__, - commit=not self.store.conn.transaction_owned_by_current_thread(), - ) - except Exception as audit_exc: # noqa: BLE001 - self._warn_redacted_failure("vector-index failure audit", audit_exc) + if not vector_index_shares_store_transaction(self.index, self.store): + self.repair_vector_index(limit=1, memory_id=memory_id) + return + # A native table sharing SQLite participates in its caller's atomic write. + _safe_upsert( + self.index, [memory_id], vec.reshape(1, -1), + [{"model": self.embedding_space}], + ) def _resolve_and_store(self, content: str, *, text: str, vec: Optional[np.ndarray], workspace_id: str, repo_id: Optional[str], @@ -1751,36 +1783,27 @@ def _resolve_and_store(self, content: str, *, text: str, vec: Optional[np.ndarra # discount on BOTH sides. Non-fatal: a storage hiccup here must not fail # the write — the conflict metadata on the new record already surfaced it. try: - self.store.add_link( - mid, conflicted_with, CONFLICT_RELATION, - reason=( - "detector=contradiction; deterministic contradiction " - "(no safe supersession)" - ), - valid_from=valid_from, - ) - self.store.audit( - "resolver", "conflict_detected", conflicted_with, - f"new_memory={mid}; deterministic contradiction (no safe supersession)", - ) - self.store.advance_memory_modified_hlc( - conflicted_with, commit=False, - ) - self.store.conn.execute( - "UPDATE memories SET confidence=MIN(confidence, ?) WHERE id=?", - (round(CONFLICT_CONFIDENCE_FACTOR, 4), conflicted_with), - ) - self.store.conn.commit() - except Exception as exc: # noqa: BLE001 — best-effort repair, never fail the write - # Inside commit deferral (batch writes) a rollback here would target the - # OUTER savepoint and discard earlier facts in the same batch. Deferral - # keeps the failed repair's partial statements inside the caller's - # boundary; the outer owner decides settle-or-discard for the whole batch. - if ( - self.store.conn.transaction_owned_by_current_thread() - and not getattr(self.store.conn._pin, "defer_commits", 0) - ): - self.store.conn.rollback() + with self.store.write_savepoint(): + self.store.add_link( + mid, conflicted_with, CONFLICT_RELATION, + reason=( + "detector=contradiction; deterministic contradiction " + "(no safe supersession)" + ), + valid_from=valid_from, + ) + self.store.audit( + "resolver", "conflict_detected", conflicted_with, + f"new_memory={mid}; deterministic contradiction (no safe supersession)", + ) + self.store.advance_memory_modified_hlc( + conflicted_with, commit=False, + ) + self.store.conn.execute( + "UPDATE memories SET confidence=MIN(confidence, ?) WHERE id=?", + (round(CONFLICT_CONFIDENCE_FACTOR, 4), conflicted_with), + ) + except Exception as exc: # noqa: BLE001 - derived repair must not discard the memory self._warn_redacted_failure("conflict repair", exc) out: dict[str, object] if decision is not None and decision.op == ResolutionOp.RELATE: @@ -2005,7 +2028,7 @@ def _search_resolution_vectors( if (isinstance(memory_id, str) and memory_id and math.isfinite(similarity)): valid_indexed.append((memory_id, similarity)) - if valid_indexed: + if valid_indexed and not canonical_search_required(self.index, self.store): return valid_indexed, False # An empty injected result is not enough evidence that no related # memory exists: an asynchronously rebuilt or partially populated @@ -2034,26 +2057,7 @@ def _search_resolution_vectors( ) try: - query = np.asarray(vec, dtype=np.float32) - if query.ndim != 1 or query.shape[0] < 1 or not np.isfinite(query).all(): - raise ValueError("resolution query vector must be a finite one-dimensional array") - with np.errstate(over="ignore", invalid="ignore"): - norm = float(np.linalg.norm(query)) - if not math.isfinite(norm): - raise ValueError("resolution query vector norm must be finite") - if norm > 0: - query = query / norm - scores: list[tuple[str, float]] = [] - for memory_id, stored in self.store.iter_vectors( - flt, dim=int(query.shape[0]) - ): - if stored.shape != query.shape: - continue - score = float(stored @ query) - if math.isfinite(score): - scores.append((memory_id, score)) - scores.sort(key=lambda item: (-item[1], item[0])) - return scores[:max(0, int(candidate_k))], True + return canonical_vector_search(self.store, vec, candidate_k, filter=flt), True except Exception as exc: raise RuntimeError("vector neighbor resolution unavailable") from exc diff --git a/engraphis/core/interfaces.py b/engraphis/core/interfaces.py index 5fa81d53..d81cff7c 100644 --- a/engraphis/core/interfaces.py +++ b/engraphis/core/interfaces.py @@ -534,6 +534,11 @@ class VectorIndex(Protocol): A separate table on the same Store connection may instead expose ``shares_store_transaction = True``. Its explicit sync then remains inside the canonical transaction rather than being deferred as an external side effect. + + Independently persisted adapters should expose a stable ``index_identity`` + unique to the physical index (never credentials). Canonical mutations queue + durable, content-free repair work for it. Pending or unidentified indexes + use canonical exact search until completeness is established. """ def upsert(self, ids: list[str], vecs: np.ndarray, meta: Optional[list[dict]] = None, *, commit: bool = True) -> None: ... diff --git a/engraphis/core/recall.py b/engraphis/core/recall.py index d95d9ee6..3c6d1cdd 100644 --- a/engraphis/core/recall.py +++ b/engraphis/core/recall.py @@ -77,6 +77,8 @@ now_ts, ) from engraphis.core.textutil import jaccard, tokenize +from engraphis.core.vector_repair import canonical_search_required, index_repair_identity +from engraphis.core.vector_search import canonical_vector_search logger = logging.getLogger("engraphis.core.recall") @@ -148,6 +150,8 @@ class RecallResult: embedding_mode: str = "semantic" degraded_reason: str = "" vector_search_ready: bool = True + vector_index_repairs_pending: Optional[int] = None + vector_search_source: str = "configured" class RecallEngine: @@ -417,11 +421,28 @@ def recall(self, query: str, flt: Optional[SearchFilter] = None, *, k: int = 8, vec = {} if qvec is not None and not vector_runtime_failed: try: - vec = dict( - self.index.search( - qvec, arm_candidate_k, filter=query_filter - ) - ) + if canonical_search_required( + self.index, self.store, unregistered_is_uncertain=False, + ): + target = index_repair_identity(self.index, self.store) + capabilities.update({ + "degraded_mode": True, + "degraded_reason": ( + "external vector index completeness is uncertain; " + "canonical exact search is active" + ), + "vector_index_repairs_pending": ( + self.store.vector_index_pending(target) if target else None + ), + "vector_search_source": "canonical", + }) + vec = dict(canonical_vector_search( + self.store, qvec, arm_candidate_k, filter=query_filter, + )) + else: + vec = dict(self.index.search( + qvec, arm_candidate_k, filter=query_filter, + )) except Exception as exc: # optional backend; preserve other arms vector_runtime_failed = True capabilities.update({ @@ -1505,7 +1526,10 @@ def connect(a: str, b: str, w: object, layer: GraphLayer) -> None: for link in frontier_links for endpoint in (link["a"], link["b"]) } | set(self.store.list_memory_ids( - flt, limit=500, prompt_only=prompt_only, + # Retain the established window: the incidence frontier expands + # only one memory-link hop, so a smaller window drops older + # multi-hop evidence before PageRank can consider it. + flt, limit=12_000, prompt_only=prompt_only, )) if prompt_only: memory_ids = self._prompt_eligible_memory_ids(memory_ids, flt) diff --git a/engraphis/core/resolve.py b/engraphis/core/resolve.py index 0a7969e0..41b62985 100644 --- a/engraphis/core/resolve.py +++ b/engraphis/core/resolve.py @@ -160,6 +160,25 @@ def _canonical_env(tokens: set[str]) -> set[str]: return {_ENV_ALIASES.get(token, token) for token in tokens} +def _has_reordered_environments(candidate_text: str, record_text: str) -> bool: + """Equal environment sets can still bind different environments to a subject. + + ``staging database ... in production`` and ``production database ... in + staging`` have identical bags of words. Without a claim key, reordered + environment mentions are ambiguous and must preserve both writes. + """ + def mentions(text: str) -> list[str]: + tokens = (match.group(0) for match in re.finditer(r"\w+", text.casefold())) + return [ + _ENV_ALIASES.get(token, token) + for token in tokens + if token in _ENV_QUALIFIERS + ] + + candidate, record = mentions(candidate_text), mentions(record_text) + return len(set(candidate)) > 1 and set(candidate) == set(record) and candidate != record + + _MONTHS = frozenset({ "january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december", @@ -369,7 +388,10 @@ def resolve(candidate_text: str, neighbors: list[tuple[float, MemoryRecord]], *, # (staging/production) are an exception: two near-duplicates that # only differ by environment are coexisting facts on different # envs, not a correction. - env_conflict = _env_conflict_for_correction(candidate_text, rec_text) + env_conflict = ( + _env_conflict_for_correction(candidate_text, rec_text) + or _has_reordered_environments(candidate_text, rec_text) + ) subject_identifier_drift = _has_subject_identifier_drift(candidate_text, rec_text) named_subject_drift = _has_named_subject_drift(candidate_text, rec_text) if env_conflict or subject_identifier_drift or named_subject_drift: diff --git a/engraphis/core/schema.py b/engraphis/core/schema.py index b1bc1a31..3bffd6b6 100644 --- a/engraphis/core/schema.py +++ b/engraphis/core/schema.py @@ -8,7 +8,7 @@ """ from __future__ import annotations -SCHEMA_VERSION = 16 +SCHEMA_VERSION = 17 SCHEMA_SQL = """ CREATE TABLE IF NOT EXISTS schema_migrations ( @@ -144,6 +144,47 @@ ); CREATE INDEX IF NOT EXISTS idx_mem_vectors_model ON mem_vectors(model); +-- Content-free, transactionally maintained work for independently persisted indexes. +-- No memory foreign key: erasure must retain a pending external DELETE by id. +CREATE TABLE IF NOT EXISTS vector_store_state ( + singleton INTEGER PRIMARY KEY CHECK(singleton = 1), + generation INTEGER NOT NULL DEFAULT 0 +); +INSERT OR IGNORE INTO vector_store_state(singleton, generation) VALUES (1, 0); +CREATE TABLE IF NOT EXISTS vector_index_targets ( + identity TEXT PRIMARY KEY +); +CREATE TABLE IF NOT EXISTS vector_index_repairs ( + identity TEXT NOT NULL REFERENCES vector_index_targets(identity) ON DELETE CASCADE, + memory_id TEXT NOT NULL, + generation INTEGER NOT NULL, + PRIMARY KEY(identity, memory_id) +); +CREATE TRIGGER IF NOT EXISTS trg_vector_repair_insert AFTER INSERT ON mem_vectors +BEGIN + UPDATE vector_store_state SET generation=generation+1 WHERE singleton=1; + INSERT INTO vector_index_repairs(identity, memory_id, generation) + SELECT t.identity, NEW.id, s.generation FROM vector_index_targets t, vector_store_state s + WHERE s.singleton=1 + ON CONFLICT(identity, memory_id) DO UPDATE SET generation=excluded.generation; +END; +CREATE TRIGGER IF NOT EXISTS trg_vector_repair_update AFTER UPDATE ON mem_vectors +BEGIN + UPDATE vector_store_state SET generation=generation+1 WHERE singleton=1; + INSERT INTO vector_index_repairs(identity, memory_id, generation) + SELECT t.identity, NEW.id, s.generation FROM vector_index_targets t, vector_store_state s + WHERE s.singleton=1 + ON CONFLICT(identity, memory_id) DO UPDATE SET generation=excluded.generation; +END; +CREATE TRIGGER IF NOT EXISTS trg_vector_repair_delete AFTER DELETE ON mem_vectors +BEGIN + UPDATE vector_store_state SET generation=generation+1 WHERE singleton=1; + INSERT INTO vector_index_repairs(identity, memory_id, generation) + SELECT t.identity, OLD.id, s.generation FROM vector_index_targets t, vector_store_state s + WHERE s.singleton=1 + ON CONFLICT(identity, memory_id) DO UPDATE SET generation=excluded.generation; +END; + -- Versioned embedding mappings. Reserved identities __active__ and __rebuilding__ -- describe the one vector space currently stored and any in-progress replacement. -- Backend-specific rows remain as an operator-facing history only. diff --git a/engraphis/core/store.py b/engraphis/core/store.py index 3101aa1d..a213b4d0 100644 --- a/engraphis/core/store.py +++ b/engraphis/core/store.py @@ -1207,6 +1207,7 @@ def __init__(self, path: str = ":memory:", *, self, _close_connection_quietly, self.conn ) self.has_fts5 = False + self._fts_orphan_ids: Optional[set[str]] = None self._receipt_lock = threading.Lock() self.allowed_workspaces: Optional[frozenset] = ( frozenset(allowed_workspaces) if allowed_workspaces else None @@ -1329,6 +1330,9 @@ def _validate_read_only_ready(self) -> None: "sessions", "memories", "mem_vectors", + "vector_store_state", + "vector_index_targets", + "vector_index_repairs", "entities", "edges", "mem_links", @@ -3661,6 +3665,43 @@ def _write_operation(self, name: str, *, commit: bool): self.conn.execute(f"RELEASE SAVEPOINT {savepoint}") raise + @contextmanager + def write_transaction(self): + """Reserve the writer before a read/decide/write operation. + + Nested Store commits cannot settle this boundary. A caller's transaction + remains caller-owned, including when an operation fails. + """ + with self._write_operation("engine", commit=True): + with self.conn.defer_commits(): + yield + + @contextmanager + def read_snapshot(self): + """Keep paginated reads on one snapshot without adopting another thread's work.""" + owns_transaction = not self.conn.transaction_owned_by_current_thread() + try: + if owns_transaction: + self.conn.execute("BEGIN") + yield + finally: + if owns_transaction and self.conn.transaction_owned_by_current_thread(): + self.conn.rollback() + + @contextmanager + def write_savepoint(self): + """Isolate a best-effort sub-operation inside an authoritative transaction.""" + name = f"engraphis_optional_{threading.get_ident()}_{time.monotonic_ns()}" + self.conn.execute(f"SAVEPOINT {name}") + try: + yield + except BaseException: + self.conn.execute(f"ROLLBACK TO SAVEPOINT {name}") + self.conn.execute(f"RELEASE SAVEPOINT {name}") + raise + else: + self.conn.execute(f"RELEASE SAVEPOINT {name}") + # ── local source-import manifest ───────────────────────────────────────── def _authorize_source_workspace_id(self, workspace_id: str) -> str: row = self.conn.execute( @@ -4575,6 +4616,20 @@ def _add_memory_impl( ) if rec.id: ids.assert_id_kind(rec.id, "memory") + replace_fts_existing = existing_record is not None + if not replace_fts_existing: + if self._fts_orphan_ids is None: + # Inventory legacy/corrupt orphan mirrors once, while holding the + # writer reservation and before this ID becomes canonical. Healthy + # stores retain an empty set, rather than all memory IDs. Keep IDs + # after repair so an outer rollback cannot make the cache unsafe. + self._fts_orphan_ids = { + str(row["id"]) for row in self.conn.execute( + "SELECT DISTINCT f.id FROM mem_fts f WHERE NOT EXISTS " + "(SELECT 1 FROM memories m WHERE m.id=f.id)" + ).fetchall() + } + replace_fts_existing = rec.id in self._fts_orphan_ids self.conn.execute( """INSERT INTO memories (id, workspace_id, repo_id, session_id, scope, mtype, title, content, summary, @@ -4612,7 +4667,10 @@ def _add_memory_impl( ) # The method-level transaction/savepoint keeps the row, FTS mirror, and # vector mirror atomic without settling a caller-owned transaction. - self._fts_upsert(rec.id, rec.title, rec.content, " ".join(rec.keywords)) + self._fts_upsert( + rec.id, rec.title, rec.content, " ".join(rec.keywords), + replace_existing=replace_fts_existing, + ) if rec.embedding is not None: self.put_vector( rec.id, @@ -5218,7 +5276,7 @@ def iter_vectors(self, flt: Optional[SearchFilter] = None, where.append("v.dim=?") params.append(int(dim)) sql = ("SELECT v.id AS id, v.vector AS vector FROM mem_vectors v " - "JOIN memories m ON m.id = v.id WHERE " + "CROSS JOIN memories m ON m.id = v.id WHERE " + " AND ".join([*where, "v.id > ?"]) + " ORDER BY v.id LIMIT ?") cursor_id = "" @@ -5232,6 +5290,64 @@ def iter_vectors(self, flt: Optional[SearchFilter] = None, return cursor_id = rows[-1]["id"] + def iter_vector_matrices(self, flt: Optional[SearchFilter] = None, *, + include_invalid: bool = False, dim: int): + """Yield bounded fixed-width batches from one consistent vector snapshot.""" + if dim < 1: + raise ValueError("vector matrix dimension must be a positive integer") + where, params = self._where(flt, include_invalid, alias="m") + where.extend(("v.dim=?", "length(v.vector)=?", "v.id>?")) + params.extend((int(dim), int(dim) * np.dtype(np.float32).itemsize)) + sql = ( + "SELECT v.id, v.vector FROM mem_vectors v CROSS JOIN memories m ON m.id=v.id WHERE " + + " AND ".join(where) + " ORDER BY v.id LIMIT ?" + ) + # Let the scope index produce the first ordered batch: narrow scopes then + # finish cheaply, and the last ID advances selective scans through the + # global keyspace. Later batches use the vector primary-key range so broad + # scopes do not repeatedly sort all remaining candidates. + first_sql = sql.replace("CROSS JOIN", "JOIN", 1) + after_id = "" + with self.read_snapshot(): + while True: + selected_sql = first_sql if not after_id else sql + rows = self.conn.fetchall(selected_sql, (*params, after_id, VECTOR_SCAN_BATCH)) + if not rows: + return + ids = [str(row["id"]) for row in rows] + payload = b"".join(row["vector"] for row in rows) + yield ids, np.frombuffer(payload, dtype=np.float32).reshape(len(ids), dim) + if len(rows) < VECTOR_SCAN_BATCH: + return + after_id = ids[-1] + + def vector_generation(self) -> int: + row = self.conn.execute( + "SELECT generation FROM vector_store_state WHERE singleton=1" + ).fetchone() + return int(row[0]) if row is not None else 0 + + def register_vector_index(self, identity: str) -> None: + """Register a durable external target and seed its initial repair backlog.""" + with self.write_transaction(): + inserted = self.conn.execute( + "INSERT OR IGNORE INTO vector_index_targets(identity) VALUES (?)", (identity,) + ).rowcount + if inserted: + self.conn.execute( + "INSERT INTO vector_index_repairs(identity, memory_id, generation) " + "SELECT ?, id, ? FROM mem_vectors", + (identity, self.vector_generation()), + ) + + def vector_index_pending(self, identity: str) -> Optional[int]: + """Return pending work, or None when the target has never been registered.""" + row = self.conn.execute( + "SELECT (SELECT COUNT(*) FROM vector_index_repairs r WHERE r.identity=t.identity) " + "FROM vector_index_targets t WHERE t.identity=?", (identity,), + ).fetchone() + return int(row[0]) if row is not None else None + def vector_matrix(self, flt: Optional[SearchFilter] = None, *, include_invalid: bool = False, dim: int) -> tuple[list[str], np.ndarray]: """Materialize one filtered, fixed-width vector matrix for an exact scan. @@ -5263,8 +5379,20 @@ def vector_matrix(self, flt: Optional[SearchFilter] = None, return ids, np.frombuffer(payload, dtype=np.float32).reshape(len(ids), dim) # ── full text ───────────────────────────────────────────────────────────── - def _fts_upsert(self, mid: str, title: str, content: str, keywords: str) -> None: - self.conn.execute("DELETE FROM mem_fts WHERE id=?", (mid,)) + def _fts_upsert(self, mid: str, title: str, content: str, keywords: str, *, + replace_existing: bool = True) -> None: + # FTS5's id column is UNINDEXED, so deleting by id scans the entire mirror. + # A successful new canonical insert has no mirror under Store's atomic + # write/erase contract, except IDs in the one-time orphan inventory. Updates + # and explicit repair retain duplicate cleanup. + if replace_existing: + # A direct repair call can also create an orphan after the inventory. + # Remember it before writing; retaining the ID is rollback-safe. + if self._fts_orphan_ids is not None and self.conn.execute( + "SELECT 1 FROM memories WHERE id=?", (mid,), + ).fetchone() is None: + self._fts_orphan_ids.add(mid) + self.conn.execute("DELETE FROM mem_fts WHERE id=?", (mid,)) self.conn.execute( "INSERT INTO mem_fts(id, title, content, keywords) VALUES (?,?,?,?)", (mid, title, content, keywords), diff --git a/engraphis/core/vector_repair.py b/engraphis/core/vector_repair.py new file mode 100644 index 00000000..8b16a01b --- /dev/null +++ b/engraphis/core/vector_repair.py @@ -0,0 +1,45 @@ +"""Content-free identities and readiness policy for separate vector indexes.""" +from __future__ import annotations + +import hashlib +from typing import Optional, TYPE_CHECKING + +from engraphis.core.interfaces import ( + vector_index_requires_sync, + vector_index_shares_store_transaction, +) + +if TYPE_CHECKING: + from engraphis.core.store import Store + + +def index_repair_identity(index, store: "Store") -> Optional[str]: + """Keep credentials and connection details out of durable repair metadata. + + External adapters should provide a stable ``index_identity`` unique to their + physical index. Unidentified adapters are supported but never used as the + authority for complete search results; canonical search remains available. + """ + if (not vector_index_requires_sync(index, store) + or vector_index_shares_store_transaction(index, store)): + return None + identity = str(getattr(index, "index_identity", "") or "") + namespace = f"{type(index).__module__}.{type(index).__qualname__}" + digest = hashlib.sha256(f"{namespace}\n{identity}".encode("utf-8")).hexdigest() + return f"index:v1:{digest}" + + +def canonical_search_required(index, store: "Store", *, + unregistered_is_uncertain: bool = True) -> bool: + identity = index_repair_identity(index, store) + if identity is None: + return False + pending = store.vector_index_pending(identity) + # A standalone RecallEngine may use a read-only/testing retrieval adapter + # which has never participated in MemoryEngine's durable write lifecycle. + if pending is None: + return unregistered_is_uncertain + return ( + not getattr(index, "index_identity", None) + or pending != 0 + ) diff --git a/engraphis/core/vector_search.py b/engraphis/core/vector_search.py new file mode 100644 index 00000000..57b1257c --- /dev/null +++ b/engraphis/core/vector_search.py @@ -0,0 +1,55 @@ +"""Bounded exact search over the canonical vector store.""" +from __future__ import annotations + +from contextlib import closing +from typing import Optional, TYPE_CHECKING + +import numpy as np + +from engraphis.core.interfaces import SearchFilter + +if TYPE_CHECKING: + from engraphis.core.store import Store + + +def top_k_indices(scores: np.ndarray, ids: list[str], k: int) -> list[int]: + """Select exact top-k with the memory id as the stable cutoff tie-breaker.""" + if k <= 0: + return [] + if k >= len(ids) or not np.isfinite(scores).all(): + return sorted(range(len(ids)), key=lambda i: (-float(scores[i]), ids[i]))[:k] + cutoff = float(np.partition(scores, len(ids) - k)[len(ids) - k]) + above = np.flatnonzero(scores > cutoff).tolist() + boundary = np.flatnonzero(scores == cutoff).tolist() + above.extend(sorted(boundary, key=ids.__getitem__)[:k - len(above)]) + above.sort(key=lambda i: (-float(scores[i]), ids[i])) + return above + + +def canonical_vector_search(store: "Store", vec: np.ndarray, k: int, *, + filter: Optional[SearchFilter] = None) -> list[tuple[str, float]]: + """Read a consistent snapshot using O(batch * dimension + k) vector memory.""" + query = np.asarray(vec, dtype=np.float32) + if query.ndim != 1 or query.size == 0 or not np.isfinite(query).all(): + raise ValueError("query vector must be a finite non-empty one-dimensional array") + with np.errstate(over="ignore", invalid="ignore"): + norm = float(np.linalg.norm(query)) + if not np.isfinite(norm): + raise ValueError("query vector norm must be finite") + if norm == 0 or k <= 0: + return [] + query = query / norm + winners: list[tuple[str, float]] = [] + with closing(store.iter_vector_matrices(filter, dim=int(query.size))) as batches: + for ids, matrix in batches: + nonzero = np.any(matrix != 0, axis=1) + if not np.all(nonzero): + ids = [memory_id for memory_id, keep in zip(ids, nonzero) if keep] + matrix = matrix[nonzero] + if not ids: + continue + scores = matrix @ query + winners.extend((ids[i], float(scores[i])) for i in top_k_indices(scores, ids, k)) + winners.sort(key=lambda row: (-row[1], row[0])) + del winners[k:] + return winners diff --git a/engraphis/dashboard_assets/engraphis-graph-every.js b/engraphis/dashboard_assets/engraphis-graph-every.js index f5d21a32..e3c84f48 100644 --- a/engraphis/dashboard_assets/engraphis-graph-every.js +++ b/engraphis/dashboard_assets/engraphis-graph-every.js @@ -1417,6 +1417,9 @@ : { minDegree: 0, showUnlinked: true, depth: 2 }; refreshVisibility(); camera(); + // Scope changes do not wake the layout worker. Publish the new count now, + // including after a settled graph moves from zero visible nodes back to some. + stats(); return api; }, setLayers(value) { @@ -1440,7 +1443,7 @@ stats(); return api; }, - setGhosts(value) { state.ghosts = value !== false; refreshVisibility(); camera(); return api; }, + setGhosts(value) { state.ghosts = value !== false; refreshVisibility(); camera(); stats(); return api; }, setHighlight(id) { const index = state.idIndex.get(String(id)); state.focus = index === undefined ? -1 : index; diff --git a/engraphis/dashboard_assets/index.html b/engraphis/dashboard_assets/index.html index 2ec0083b..3cf991e6 100644 --- a/engraphis/dashboard_assets/index.html +++ b/engraphis/dashboard_assets/index.html @@ -132,6 +132,14 @@

Strongest memories

Memory composition

Loading types…

+

Local-first

Ask before assuming

@@ -197,7 +205,10 @@

Browse, add and govern memories

- 0 memories + 0 memories + + +
@@ -599,6 +610,17 @@

Make the workspace yours

Local-first runtime
+
+

Managed processing

+

Encrypted Cloud Sync has its own controls. Managed processing lets hosted services read an eligible snapshot to generate insights and run automation. Secret and session-scoped records stay local.

+
+

Select a workspace.

+ + + + +
+

Interface

@@ -708,6 +730,7 @@

Connected nodes

- + + diff --git a/engraphis/dashboard_assets/ledger.js b/engraphis/dashboard_assets/ledger.js index 740aa50b..70cb0895 100644 --- a/engraphis/dashboard_assets/ledger.js +++ b/engraphis/dashboard_assets/ledger.js @@ -7,6 +7,11 @@ workspaces: [], stats: {}, memories: [], + libraryTotal: 0, + libraryNextCursor: null, + libraryCursors: [null], + libraryPage: 0, + libraryLoading: false, selectedMemory: '', editorMemory: null, editorReturnFocus: null, @@ -51,6 +56,7 @@ reviewCsrf: '', hostedLoaded: new Set(), scopedRequests: Object.create(null), + scopedControllers: Object.create(null), syncStatus: null, license: null, releaseVersion: '', @@ -63,6 +69,7 @@ const NOTICE_DURATION_MS = 3000; let noticeTimer = null; let graphRepoLoadTimer = null; + let librarySearchTimer = null; const CLOUD_SYNC_PRIVACY_NOTICE = 'Cloud Sync encrypts eligible shared-workspace changes end-to-end before they leave this device. Engraphis Cloud cannot read their contents; secret and session-scoped memories stay local.'; const EXTERNAL_LLM_PRIVACY_NOTICE = 'Memory text is sent to your configured LLM provider for processing under that provider’s terms. The provider must read that text to return extracted facts.'; const truncate = (value, length = 260) => { @@ -95,6 +102,9 @@ }; const query = (name = state.workspace) => `workspace=${encodeURIComponent(name || '')}`; const beginScopedRequest = kind => { + if (state.scopedControllers[kind]) state.scopedControllers[kind].abort(); + const controller = new AbortController(); + state.scopedControllers[kind] = controller; const generation = number(state.scopedRequests[kind]) + 1; state.scopedRequests[kind] = generation; return { @@ -102,6 +112,7 @@ generation, workspace: state.workspace, epoch: state.refreshEpoch, + signal: controller.signal, }; }; const isCurrentScopedRequest = request => Boolean(request @@ -109,6 +120,8 @@ && request.epoch === state.refreshEpoch && state.scopedRequests[request.kind] === request.generation); const invalidateScopedRequests = () => { + Object.values(state.scopedControllers).forEach(controller => controller.abort()); + state.scopedControllers = Object.create(null); Object.keys(state.scopedRequests).forEach(kind => { state.scopedRequests[kind] = number(state.scopedRequests[kind]) + 1; }); @@ -221,20 +234,41 @@ }; async function api(path, options = {}) { - const init = { ...options, headers: { ...(options.headers || {}) } }; + const { timeoutMs = 30_000, signal, ...requestOptions } = options; + const controller = new AbortController(); + const abort = () => controller.abort(); + if (signal) { + if (signal.aborted) abort(); + else signal.addEventListener('abort', abort, { once: true }); + } + let timedOut = false; + const timer = window.setTimeout(() => { timedOut = true; controller.abort(); }, timeoutMs); + const init = { ...requestOptions, signal: controller.signal, headers: { ...(options.headers || {}) } }; init.headers['X-Engraphis-Browser-Session'] = '1'; if (init.body && !(init.body instanceof FormData) && typeof init.body !== 'string') { init.headers['Content-Type'] = 'application/json'; init.body = JSON.stringify(init.body); } - const response = await fetch(`${apiRoot}${path}`, init); - const payload = await response.json().catch(() => null); - if (!response.ok) { - const error = new Error(errorMessage(payload, response.status)); - error.status = response.status; + try { + const response = await fetch(`${apiRoot}${path}`, init); + const payload = await response.json().catch(error => { + if (controller.signal.aborted) throw error; + return null; + }); + if (!response.ok) { + const error = new Error(errorMessage(payload, response.status)); + error.status = response.status; + error.code = payload && ((payload.detail && payload.detail.code) || payload.code); + throw error; + } + return payload; + } catch (error) { + if (timedOut) throw new Error('The request timed out. Please try again.'); throw error; + } finally { + window.clearTimeout(timer); + if (signal) signal.removeEventListener('abort', abort); } - return payload; } function promptBrowserToken(message = '') { @@ -425,7 +459,7 @@ if (!graphAllAssetsPromise) { const controller = new AbortController(); const attempt = loadScript( - graphAssetSource('/v2-assets/engraphis-graph-every.js?v=20260823-every-19'), + graphAssetSource('/v2-assets/engraphis-graph-every.js?v=20260905-every-20'), 'EngraphisEveryGraph', controller.signal, ); graphAllAssetsPromise = attempt; @@ -608,6 +642,12 @@ return withCtaAttribution(license.account_url || license.upgrade_url, content); } + function licenseTrialDays(plan = 'pro') { + const trial = (state.license && state.license.trial) || {}; + const days = (trial.days_by_plan || {})[plan] ?? (plan === 'pro' ? trial.trial_days : null); + return Number.isSafeInteger(days) && days > 0 ? days : null; + } + function hostedCta(plan = 'pro', content = 'plans', interval = 'monthly') { const stateName = licenseAccessState(); const currentPlan = licensePlanKey(); @@ -624,8 +664,9 @@ }; } const trial = licenseTrialAvailable() && stateName === 'inactive'; + const days = licenseTrialDays(plan); return { - label: trial ? `Start 3-day ${name} trial` : `Subscribe to ${name}`, + label: trial ? `Start ${days ? `${days}-day ` : ''}${name} trial` : `Subscribe to ${name}`, href: hostedPlanUrl(plan, trial, interval, content), kind: trial ? 'trial' : 'subscribe', }; @@ -644,7 +685,7 @@ badge.hidden = access === 'inactive' && trial; const aria = licenseHasHostedAccess() ? 'Open Engraphis Cloud account' : access === 'lapsed' ? 'Update billing in Plans and billing' - : trial ? 'Start the 3-day Pro trial in Plans and billing' + : trial ? `${hostedCta('pro', 'header').label} in Plans and billing` : 'Subscribe to Pro in Plans and billing'; badge.textContent = label; badge.setAttribute('aria-label', aria); @@ -1063,6 +1104,7 @@ state.stats = stats; renderMetricValues(stats); renderTypeBars(stats); + renderFirstMemoryJourney(); } async function loadSavings(epoch) { @@ -1081,11 +1123,63 @@ } } - async function loadMemories(workspace, epoch) { - const payload = await api(`/memories?${query(workspace)}&limit=500`); - if (epoch !== state.refreshEpoch) return; - state.memories = payload.memories || []; - renderLibrary(); + async function loadMemories(workspace, epoch, page = 0) { + const request = beginScopedRequest('library'); + const params = new URLSearchParams({ workspace, limit: '100' }); + const search = byId('library-filter').value.trim(); + const type = byId('library-type').value; + if (search) params.set('q', search); + if (type) params.set('mtype', type); + const cursor = state.libraryCursors[page]; + if (cursor) params.set('cursor', cursor); + state.libraryLoading = true; + renderLibraryPaging(); + try { + const payload = await api(`/memories?${params}`, { signal: request.signal }); + if (epoch !== state.refreshEpoch || !isCurrentScopedRequest(request)) return; + state.memories = payload.memories || []; + state.libraryTotal = number(payload.total_count == null ? state.memories.length : payload.total_count); + state.libraryNextCursor = payload.next_cursor || null; + state.libraryPage = page; + if (!page) state.libraryCursors = [null]; + if (state.libraryNextCursor) state.libraryCursors[page + 1] = state.libraryNextCursor; + renderLibrary(); + renderFirstMemoryJourney(); + } catch (error) { + if (!isCurrentScopedRequest(request)) return; + if (cursor && error.status === 409 && error.code === 'cursor_stale') { + state.libraryCursors = [null]; + showNotice('Memory changed. Showing the first page with your filters preserved.'); + return await loadMemories(workspace, epoch); + } + byId('library-list').replaceChildren(empty(`Library is unavailable: ${error.message}`)); + throw error; + } finally { + if (isCurrentScopedRequest(request)) { + state.libraryLoading = false; + renderLibraryPaging(); + } + } + } + + function renderLibraryPaging() { + byId('library-previous').disabled = state.libraryLoading || state.libraryPage === 0; + byId('library-next').disabled = state.libraryLoading || !state.libraryNextCursor; + byId('library-refresh').disabled = state.libraryLoading || !state.workspace; + byId('library-list').setAttribute('aria-busy', String(state.libraryLoading)); + } + + function refreshLibrary(page = 0) { + window.clearTimeout(librarySearchTimer); + if (!state.workspace) return; + loadMemories(state.workspace, state.refreshEpoch, page).catch(error => showNotice(error.message)); + } + + function renderFirstMemoryJourney() { + const journey = byId('first-memory-journey'); + if (!journey) return; + journey.hidden = number(state.stats.memories) > 0 || state.libraryTotal > 0; + byId('first-memory-add').textContent = state.workspace ? 'Add your first memory' : 'Create your first workspace'; } async function loadToday(workspace, epoch) { @@ -1135,13 +1229,24 @@ }); } + const processingControls = window.EngraphisProcessingControls.create(api); + async function selectWorkspace(name) { if (!name) return; invalidateConsolidationReview(); const epoch = ++state.refreshEpoch; invalidateScopedRequests(); + window.clearTimeout(librarySearchTimer); + state.libraryCursors = [null]; + state.libraryPage = 0; + state.libraryNextCursor = null; + state.libraryTotal = 0; + state.memories = []; + renderLibrary(); + renderLibraryPaging(); closeGraphConnections(); state.workspace = name; + void processingControls.selectWorkspace(name); state.graphWorkspace = ''; state.graphData = null; state.graphDataPreset = 'galaxy'; @@ -1211,15 +1316,7 @@ } function filteredMemories() { - const filterEl = byId('library-filter'); - const typeEl = byId('library-type'); - const filter = filterEl ? filterEl.value.trim().toLowerCase() : ''; - const type = typeEl ? typeEl.value : ''; - return state.memories.filter(memory => { - const matchesText = !filter || `${memory.title || ''} ${memory.content || ''} ${memory.summary || ''}` - .toLowerCase().includes(filter); - return matchesText && (!type || memoryType(memory) === type); - }); + return state.memories; } function renderLibrary() { @@ -1243,9 +1340,14 @@ } target.replaceChildren(); const memories = filteredMemories(); - byId('library-count').textContent = `${memories.length.toLocaleString()} ${memories.length === 1 ? 'memory' : 'memories'}`; + const total = state.libraryTotal; + const start = state.libraryPage * 100 + (memories.length ? 1 : 0); + byId('library-count').textContent = total > memories.length + ? `${start.toLocaleString()}–${(start + memories.length - 1).toLocaleString()} of ${total.toLocaleString()} memories` + : `${total.toLocaleString()} ${total === 1 ? 'memory' : 'memories'}`; if (!memories.length) { - target.append(empty(state.memories.length ? 'No memories match these filters.' : 'No active memories in this workspace.')); + target.append(empty(byId('library-filter').value.trim() || byId('library-type').value + ? 'No memories match these filters.' : 'No active memories in this workspace. Add a fact or import local documents to begin.')); return; } memories.forEach(memory => target.append(memoryCard(memory))); @@ -1277,9 +1379,11 @@ if (!memory || state.selectedMemory !== id) return; state.editorMemory = memory; target.replaceChildren(); + const title = node('h2', '', memory.title || memory.id || 'Untitled memory'); + title.id = 'memory-detail-title'; target.append( node('p', 'eyebrow', `${memoryType(memory)} · ${memory.scope || 'workspace'}`), - node('h2', '', memory.title || memory.id || 'Untitled memory'), + title, node('p', '', memory.content || memory.summary || 'No content.'), memoryMeta(memory), definitionList([ @@ -1380,6 +1484,9 @@ async function saveMemory(event) { event.preventDefault(); const current = state.editorMemory; + let savedId = current && current.id; + const workspace = state.workspace; + const epoch = state.refreshEpoch; const title = byId('editor-memory-title').value.trim(); const memoryTypeValue = byId('editor-memory-type').value; const content = byId('editor-memory-content').value.trim(); @@ -1406,8 +1513,9 @@ if (content !== (current.content || current.summary || '')) { const corrected = await api('/correct', { method: 'POST', - body: { id: current.id, workspace: state.workspace, content, reason: 'revised in Ledger' }, + body: { id: current.id, workspace, content, reason: 'revised in Ledger' }, }); + savedId = corrected.id; // A correction intentionally creates a replacement. The core inherits the // source importance; carry any label edits to that replacement rather than // accidentally applying them to the historical source record. @@ -1417,7 +1525,7 @@ method: 'POST', body: { id: corrected.id, - workspace: state.workspace, + workspace, title, memory_type: memoryTypeValue, importance, @@ -1430,19 +1538,21 @@ method: 'POST', body: { id: current.id, - workspace: state.workspace, + workspace, title, memory_type: memoryTypeValue, importance, }, }); } - showNotice('Memory revision recorded with temporal history preserved.'); + if (workspace === state.workspace && epoch === state.refreshEpoch) { + showNotice('Memory revision recorded with temporal history preserved.'); + } } else { - await api('/remember', { + const saved = await api('/remember', { method: 'POST', body: { - workspace: state.workspace, + workspace, content, title, mtype: memoryTypeValue, @@ -1452,12 +1562,17 @@ trusted: true, }, }); - showNotice('Memory saved locally.'); + savedId = saved && saved.id; + if (workspace === state.workspace && epoch === state.refreshEpoch) { + showNotice('Memory saved locally. Review its source before approving it for model context.'); + } } + if (workspace !== state.workspace || epoch !== state.refreshEpoch) return; closeEditor(); - await selectWorkspace(state.workspace); + await selectWorkspace(workspace); + if (savedId && workspace === state.workspace && state.refreshEpoch === epoch + 1) await selectMemory(savedId); } catch (error) { - showNotice(`Could not save memory: ${error.message}`); + if (workspace === state.workspace && epoch === state.refreshEpoch) showNotice(`Could not save memory: ${error.message}`); } } @@ -1985,29 +2100,31 @@ const k = number(byId('ask-k').value) || 5; byId('answer-panel').replaceChildren(empty('Searching, checking support and building citations…')); byId('retrieval-list').replaceChildren(empty('Retrieving candidate memories…')); - try { - const [answer, retrieval] = await Promise.all([ - api('/answer', { - method: 'POST', - body: { query: question, workspace, k: Math.max(8, k), max_citations: k }, - }), - // The dashboard /recall route is deliberately read-only (reinforce=False). - // Keep it alongside /answer for uncited raw candidates without a second - // reinforcement of the memories that answer already cited. - api(`/recall?q=${encodeURIComponent(question)}&${query(workspace)}&k=${Math.max(8, k)}`), - ]); - if (!isCurrentScopedRequest(request)) return; - renderAnswer(answer); - const target = byId('retrieval-list'); - target.replaceChildren(); - const memories = retrieval.memories || []; - if (!memories.length) target.append(empty('No raw candidates were returned.')); - else memories.forEach(memory => target.append(simpleMemoryCard(memory))); - } catch (error) { + const showFailure = (id, label, error) => { if (!isCurrentScopedRequest(request)) return; - byId('answer-panel').replaceChildren(empty(`Grounded Ask is unavailable: ${error.message}`)); - byId('retrieval-list').replaceChildren(empty('Raw retrieval did not complete.')); - } + byId(id).replaceChildren(empty(`${label} is unavailable: ${error.message}`)); + }; + // Render each result as soon as it arrives. The optional raw preview must never + // hide a grounded answer, including when one response stalls until its deadline. + await Promise.allSettled([ + api('/answer', { + method: 'POST', signal: request.signal, + body: { query: question, workspace, k: Math.max(8, k), max_citations: k }, + }).then(answer => { + if (isCurrentScopedRequest(request)) renderAnswer(answer); + }).catch(error => showFailure('answer-panel', 'Grounded Ask', error)), + // /recall is read-only (reinforce=False): uncited candidates add no second + // reinforcement of memories cited by the grounded answer. + api(`/recall?q=${encodeURIComponent(question)}&${query(workspace)}&k=${Math.max(8, k)}`, + { signal: request.signal }).then(retrieval => { + if (!isCurrentScopedRequest(request)) return; + const target = byId('retrieval-list'); + target.replaceChildren(); + const memories = retrieval.memories || []; + if (!memories.length) target.append(empty('No raw candidates were returned.')); + else memories.forEach(memory => target.append(simpleMemoryCard(memory))); + }).catch(error => showFailure('retrieval-list', 'Raw retrieval', error)), + ]); } function graphCommunityIndex(value) { @@ -4559,6 +4676,9 @@ select.disabled = true; setConnection('Local engine connected · no workspace'); state.workspace = ''; + state.stats = {}; + state.libraryTotal = 0; + renderFirstMemoryJourney(); renderWorkspaceNames(); renderWorkspaceList(); renderMetricValues({ memories: 0, total_rows: 0, workspaces: 0, sessions: 0 }); @@ -4595,7 +4715,10 @@ } catch (_) {} applyTheme(theme); try { - await refreshBootstrap(); + const entry = new URL(location.href); + // Classic links to this workspace's approval controls. Bootstrap still + // validates the name against the authorized workspace list. + await refreshBootstrap(entry.searchParams.get('workspace') || ''); let view = 'today'; try { const saved = localStorage.getItem('engraphis-ledger-view'); @@ -4603,6 +4726,9 @@ } catch (_) {} const urlView = new URL(location.href).searchParams.get('view'); switchView(['today', 'ask', 'library', 'relations', 'provenance', 'manage'].includes(urlView) ? urlView : view, { pushHistory: false }); + if (urlView === 'manage' && entry.searchParams.get('tab') === 'settings') { + switchManageTab('settings'); + } } catch (error) { if (error.status === 401 && await authenticateBrowser()) { location.reload(); @@ -4665,8 +4791,27 @@ byId('workspace-select').addEventListener('change', event => selectWorkspace(event.target.value)); byId('ask-form').addEventListener('submit', askMemory); - byId('library-filter').addEventListener('input', renderLibrary); - byId('library-type').addEventListener('change', renderLibrary); + byId('library-filter').addEventListener('input', () => { + window.clearTimeout(librarySearchTimer); + // Invalidate immediately: an earlier query must not paint while the new one debounces. + beginScopedRequest('library'); + librarySearchTimer = window.setTimeout(() => refreshLibrary(), 250); + }); + byId('library-type').addEventListener('change', () => refreshLibrary()); + byId('library-previous').addEventListener('click', () => refreshLibrary(state.libraryPage - 1)); + byId('library-next').addEventListener('click', () => refreshLibrary(state.libraryPage + 1)); + byId('library-refresh').addEventListener('click', () => refreshLibrary()); + byId('first-memory-add').addEventListener('click', () => { + if (!state.workspace) { + switchView('manage'); + switchManageTab('workspaces'); + byId('create-workspace-form').hidden = false; + byId('new-workspace-name').focus(); + return; + } + switchView('library'); + openEditor(); + }); byId('new-memory-button').addEventListener('click', () => openEditor()); byId('editor-close').addEventListener('click', closeEditor); byId('editor-cancel').addEventListener('click', closeEditor); diff --git a/engraphis/dashboard_assets/managed-processing.js b/engraphis/dashboard_assets/managed-processing.js new file mode 100644 index 00000000..e7e0a8c8 --- /dev/null +++ b/engraphis/dashboard_assets/managed-processing.js @@ -0,0 +1,82 @@ +/* Workspace approval UI. Requests and workspace lifecycle are injected by Ledger. */ +(() => { + 'use strict'; + window.EngraphisProcessingControls = { + create(api) { + const form = document.getElementById('managed-processing-form'); + const approval = document.getElementById('managed-processing-approval'); + const status = document.getElementById('managed-processing-status'); + const enable = document.getElementById('managed-processing-enable'); + const disable = document.getElementById('managed-processing-disable'); + let workspace = ''; + let generation = 0; + let request; + let policy; + let busy = false; + const render = () => { + enable.disabled = busy || !workspace || !policy || !approval.checked || policy.operator_disabled; + disable.disabled = busy || !workspace || !policy; + }; + const show = value => { + busy = false; + policy = value; + status.textContent = value.notice || (value.remote_sync_pending + ? 'Uploads are paused locally. Cloud confirmation is pending; retry Turn off when connected.' + : value.enabled ? `Managed processing is enabled for ${workspace}.` + : value.operator_disabled ? 'Managed processing is disabled by this installation.' + : `Managed processing is off for ${workspace}. ${value.confirmation_required ? 'Review and confirm before any readable workspace content is uploaded.' : 'New readable uploads are paused.'}`); + approval.checked = false; + render(); + }; + async function save(enabled) { + if (busy || !workspace || (enabled && !approval.checked)) return; + busy = true; + const selected = workspace; + const epoch = ++generation; + if (request) request.abort(); + request = new AbortController(); + enable.disabled = disable.disabled = true; + status.textContent = enabled ? 'Requesting workspace approval…' : 'Stopping new uploads…'; + try { + const result = await api('/managed-processing', { + method: 'POST', signal: request.signal, + body: { workspace: selected, enabled, confirmed: enabled && approval.checked }, + }); + if (epoch === generation) show(result); + } catch (error) { + if (epoch !== generation) return; + status.textContent = `${error.message} Reload the policy to verify the outcome before trying again.`; + busy = false; + policy = null; + approval.checked = false; + render(); + } + } + form.addEventListener('submit', event => { event.preventDefault(); save(true); }); + disable.addEventListener('click', () => save(false)); + approval.addEventListener('change', render); + async function selectWorkspace(name) { + busy = false; + workspace = name; + const epoch = ++generation; + if (request) request.abort(); + request = new AbortController(); + policy = null; + approval.checked = false; + render(); + status.textContent = name ? 'Checking workspace processing policy…' : 'Select a workspace.'; + if (!name) return; + try { + const result = await api(`/managed-processing?workspace=${encodeURIComponent(name)}`, { + signal: request.signal, + }); + if (epoch === generation) show(result); + } catch (error) { + if (epoch === generation) status.textContent = error.message; + } + } + document.getElementById('managed-processing-reload').addEventListener('click', () => selectWorkspace(workspace)); + return { selectWorkspace }; + }, + }; +})(); diff --git a/engraphis/managed_processing.py b/engraphis/managed_processing.py new file mode 100644 index 00000000..726cc891 --- /dev/null +++ b/engraphis/managed_processing.py @@ -0,0 +1,104 @@ +"""Local, workspace-specific approval for readable managed processing. + +This policy is private client state, not synced memory or the historical snapshot +consent marker. Missing/legacy state requires confirmation and permits no upload. +""" +from __future__ import annotations + +import json +import os +import time +from typing import Any, Optional + +SCHEMA = "engraphis-managed-processing/v1" + + +class ProcessingPolicyChanged(ValueError): + """Another local policy update superseded an in-flight acknowledgement.""" + + +def _key(workspace_id: str) -> str: + return "managed_processing_policy:" + workspace_id + + +def processing_policy(service: Any, workspace: str) -> dict[str, Any]: + ws = service._clean_ws(workspace) + wid = service._lookup_workspace(ws) + if not wid: + raise ValueError("workspace does not exist") + row = service.store.conn.execute( + "SELECT value FROM sync_state WHERE key=?", (_key(wid),), + ).fetchone() + value: dict[str, Any] = {} + if row: + try: + decoded = json.loads(row["value"]) + if isinstance(decoded, dict) and decoded.get("schema") == SCHEMA: + value = decoded + except (ValueError, TypeError, RecursionError): + pass + revision = value.get("revision", 0) + remote_revision = value.get("remote_revision") + valid_revision = isinstance(revision, int) and not isinstance(revision, bool) and revision > 0 + valid_remote = remote_revision is None or ( + isinstance(remote_revision, int) and not isinstance(remote_revision, bool) and remote_revision > 0 + ) + if not valid_revision or not valid_remote: + value = {} + revision, remote_revision = 0, None + confirmed = value.get("confirmed") is True + enabled = value.get("enabled") is True and confirmed + override = os.environ.get("ENGRAPHIS_MANAGED_COMPUTE_CONSENT", "").strip().lower() + operator_disabled = bool(override) and override not in {"1", "true", "yes", "on"} + return {"workspace": ws, "workspace_id": wid, "schema": SCHEMA, + "enabled": enabled and not operator_disabled, "confirmed": confirmed, + "confirmation_required": not confirmed, + "operator_disabled": operator_disabled, + "revision": revision, + "remote_revision": remote_revision, + "remote_sync_pending": value.get("remote_sync_pending") is True, + "updated_at": value.get("updated_at")} + + +def set_processing_policy(service: Any, workspace: str, *, enabled: bool, + confirmed: bool = False, remote_revision: Optional[int] = None, + remote_sync_pending: bool = False, + expected_revision: Optional[int] = None) -> dict[str, Any]: + ws = service._clean_ws(workspace) + service._authorize_workspace_control(ws) + if not isinstance(enabled, bool) or not isinstance(confirmed, bool): + raise ValueError("processing policy must use boolean values") + if enabled and not confirmed: + raise ValueError("managed processing requires explicit workspace confirmation") + if remote_revision is not None and ( + isinstance(remote_revision, bool) or not isinstance(remote_revision, int) + or remote_revision < 1 + ): + raise ValueError("invalid remote processing policy revision") + conn = service.store.conn + owns = not conn.transaction_owned_by_current_thread() + try: + if owns: + conn.execute("BEGIN IMMEDIATE") + previous = processing_policy(service, ws) + if expected_revision is not None and previous["revision"] != expected_revision: + raise ProcessingPolicyChanged("Processing controls changed while Cloud was responding. Reload and retry.") + value = {"schema": SCHEMA, "enabled": enabled, "confirmed": True, + "revision": int(previous["revision"]) + 1, + "remote_revision": remote_revision, + "remote_sync_pending": bool(remote_sync_pending), "updated_at": time.time()} + conn.execute( + "INSERT INTO sync_state(key,value,updated_at) VALUES (?,?,?) " + "ON CONFLICT(key) DO UPDATE SET value=excluded.value,updated_at=excluded.updated_at", + (_key(previous["workspace_id"]), json.dumps(value, sort_keys=True), value["updated_at"]), + ) + with conn.defer_commits(): + service.store.audit("user", "managed_processing_policy", previous["workspace_id"], + "enabled" if enabled else "disabled") + if owns: + conn.commit() + except BaseException: + if owns and conn.transaction_owned_by_current_thread(): + conn.rollback() + raise + return processing_policy(service, ws) diff --git a/engraphis/mcp_server.py b/engraphis/mcp_server.py index b5968dea..784836e6 100644 --- a/engraphis/mcp_server.py +++ b/engraphis/mcp_server.py @@ -166,11 +166,27 @@ def _apply_response_budget(payload: dict, max_response_tokens: Optional[int]) -> counter = RegexTokenCounter() usage = payload.get("usage") or {} payload["usage"] = usage + packed_count = int(usage.get("packed_count") or 0) + candidate_count = packed_count + int(usage.get("omitted_count") or 0) if max_response_tokens is not None and max_response_tokens > 0: usage["response_budget"] = max_response_tokens def measure() -> int: + # Transport omission happens after packing. Keep evidence accounting + # truthful under the declared tokenizer, including the savings aliases. + if "context" in payload and usage.get("token_counter") == counter.identity: + emitted = counter(str(payload.get("context") or "")) + baseline = int(usage.get("source_tokens") or 0) + saved = max(0, baseline - emitted) + ratio = saved / baseline if baseline else 0.0 + usage.update(context_tokens=emitted, saved_tokens=saved, savings_ratio=ratio, + packed_count=packed_count if emitted else 0, + omitted_count=candidate_count - (packed_count if emitted else 0)) + for name, value in (("emitted_tokens", emitted), ("estimated_saved_tokens", saved), + ("estimated_savings_ratio", ratio)): + if name in usage: + usage[name] = value # Include the accounting fields themselves in the reported total. The # regex counter treats every integer as one token, so one correction is # sufficient even when the numeric value changes width. @@ -252,14 +268,22 @@ def fit_text(container: dict, key: str, *, citation_safe: bool = False) -> None: container[key] = best current_tokens = measure() - # --- over budget: truncate body content from the end ----------------- - # 1. Shrink the packed ``context`` string chunk-by-chunk (last first). - context = payload.get("context", "") - context_parts = context.split("\n\n") if context else [] - - while current_tokens > max_response_tokens and context_parts: - context_parts.pop() - payload["context"] = "\n\n".join(context_parts) + # --- over budget: omit complete evidence before reducing envelopes ----- + # Blank lines and apparent citation headers can occur inside untrusted source + # text. Without structured chunk boundaries, splitting that text can detach a + # condition from its claim. Keep the admitted context intact or omit it whole. + if current_tokens > max_response_tokens and payload.get("context"): + payload["context"] = "" + if usage.get("token_counter") != counter.identity: + # Empty text has no evidence tokens under any supported counter. + baseline = int(usage.get("source_tokens") or 0) + usage.update(context_tokens=0, saved_tokens=baseline, + savings_ratio=1.0 if baseline else 0.0, + packed_count=0, omitted_count=candidate_count) + for name, value in (("emitted_tokens", 0), ("estimated_saved_tokens", baseline), + ("estimated_savings_ratio", 1.0 if baseline else 0.0)): + if name in usage: + usage[name] = value current_tokens = measure() # 2. Reduce full-mode memory bodies. Grounded answers are handled after their @@ -626,7 +650,7 @@ def engraphis_recall( "relevance.")] = None, max_response_tokens: Annotated[Optional[int], Field( description="Cap the total serialized response to this many tokens (regex counter). " - "Truncates packed context and memory bodies from the end; citations and " + "Omits packed context whole and reduces memory bodies; citations and " "source references are preserved when the budget can hold them. " "Minimum 2 (the JSON object floor); None means no cap.", ge=2, le=1_000_000)] = None, @@ -667,36 +691,6 @@ def engraphis_recall( return _err(exc) -def _gist_summary(rec: Any, fallback_title: str = "", max_chars: int = 120) -> str: - """Extract a clean, concise one-line summary for gist-formatted context.""" - text = "" - if rec is not None: - if getattr(rec, "summary", None): - text = str(rec.summary).strip() - elif getattr(rec, "title", None) and getattr(rec, "content", None): - title = str(rec.title).strip() - content = str(rec.content).strip() - if content.lower().startswith(title.lower()): - text = content - else: - text = f"{title}: {content}" if title else content - elif getattr(rec, "title", None): - text = str(rec.title).strip() - elif getattr(rec, "content", None): - text = str(rec.content).strip() - if not text and fallback_title: - text = fallback_title.strip() - - first_line = " ".join((text.splitlines()[0] if text else "").split()) - if len(first_line) > max_chars: - match = re.match(r"^(.{30,}?[.!?])(?:\s|$)", first_line) - if match and len(match.group(1)) <= max_chars: - first_line = match.group(1) - else: - first_line = first_line[:max_chars - 3].rstrip() + "..." - return first_line - - @mcp.tool( name="engraphis_recall_context", annotations={"title": "Recall token-efficient context", "readOnlyHint": False, @@ -736,11 +730,11 @@ def engraphis_recall_context( description="Optional maximum returned count per memory type.")] = None, max_response_tokens: Annotated[Optional[int], Field( description="Cap the total serialized response to this many tokens (regex counter). " - "Truncates packed context from the end; citations and source references " + "Omits packed context whole when it cannot fit; citations and source references " "are preserved when the budget can hold them. Minimum 2; None means no cap.", ge=2, le=1_000_000)] = None, format: Annotated[str, Field( - description="Context format: 'full' for full packed text, 'gist' for one-line concise memory summaries." + description="Context format: 'full' or compatibility alias 'gist'; both preserve budgeted, cited evidence." )] = "full", ) -> str: """Return one hard-budget context plus compact source identities. @@ -751,9 +745,10 @@ def engraphis_recall_context( counts, privacy-safe savings metadata, and the same ``degraded_mode`` / ``semantic_support`` flags as ``engraphis_recall``. - Pass ``format="gist"`` for one-line concise memory gists (``[n] mem_...: ``), - reducing context token usage by 60%-80% for routine context checks while allowing - deep dive via ``engraphis_get_memory``. + ``format="gist"`` remains an accepted compatibility option. It returns the same + evidence-safe packed context, including complete conditions and code whitespace, + with a format marker. It does not apply another summary or claim extra savings. + Use ``engraphis_get_memory`` for the full source behind a citation. """ try: format = str(format or "full").strip().lower() @@ -811,22 +806,9 @@ def engraphis_recall_context( payload["sources"] = sources if format == "gist": - svc = service() - gist_lines: list[str] = [] - for source in sources: - mid = str(source.get("id") or "") - rec = svc.store.get_memory(mid) if mid else None - summary_line = _gist_summary(rec, fallback_title=str(source.get("title") or "")) - gist_lines.append(f"[{source['n']}] {mid}: {summary_line}") - payload["context"] = "\n".join(gist_lines) - counter = RegexTokenCounter() - new_context_tokens = counter(payload["context"]) - usage = payload.setdefault("usage", {}) - usage["context_tokens"] = new_context_tokens - source_tokens = usage.get("source_tokens", 0) - if source_tokens > 0: - usage["saved_tokens"] = max(0, source_tokens - new_context_tokens) - usage["savings_ratio"] = round(usage["saved_tokens"] / source_tokens, 4) + # The packer already selected the admissible evidence within the budget. + # A raw reread or prefix summary here can revive excluded content, lose a + # qualification, or exceed that budget. Keep its text and accounting. payload["format"] = "gist" if not diagnostics: @@ -940,7 +922,7 @@ def engraphis_recall_grounded( description="Optional maximum returned count per memory type.")] = None, max_response_tokens: Annotated[Optional[int], Field( description="Cap the total serialized response to this many tokens (regex counter). " - "Truncates packed context and citation bodies from the end; source references " + "Omits packed context whole and reduces citation bodies; source references " "are preserved when the budget can hold them. Minimum 2; None means no cap.", ge=2, le=1_000_000)] = None, ) -> str: diff --git a/engraphis/routes/v2_api.py b/engraphis/routes/v2_api.py index bef7e4dc..94d02453 100644 --- a/engraphis/routes/v2_api.py +++ b/engraphis/routes/v2_api.py @@ -25,10 +25,11 @@ from urllib.parse import quote from fastapi import APIRouter, HTTPException, Query, Request -from pydantic import BaseModel, Field, StrictInt +from pydantic import BaseModel, Field, StrictBool, StrictInt from starlette.concurrency import run_in_threadpool from engraphis import licensing +from engraphis.commercial import trial_days_by_plan from engraphis.config import DEFAULT_RELAY_URL, canonicalize_relay_url, settings from engraphis.core.poisoning import prompt_eligible from engraphis.core.scoring import normalize @@ -163,6 +164,8 @@ def release_service(svc: MemoryService) -> None: def _run(fn, *a, **k): """Call a service method, mapping validation errors to 400 and the rest to 500.""" + from engraphis.core.browsing import BrowseCursorStale + from engraphis.managed_processing import ProcessingPolicyChanged try: return fn(*a, **k) except GraphIndexRebuilding as exc: @@ -196,6 +199,16 @@ def _run(fn, *a, **k): except ValidationError: logger.info("dashboard request rejected") raise _invalid_request() from None + except ProcessingPolicyChanged: + raise HTTPException(status_code=409, detail={ + "error": "Processing controls changed while Cloud was responding. Reload and retry.", + "code": "processing_policy_changed", + }) from None + except BrowseCursorStale: + raise HTTPException(status_code=409, detail={ + "error": "Memory listing changed. Restart from the first page.", + "code": "cursor_stale", + }) from None except ValueError as exc: if _is_embedder_mismatch(exc): raise HTTPException(status_code=409, detail={ @@ -1388,60 +1401,102 @@ def answer(req: _AnswerReq): @router.get("/memories") def memories(workspace: Optional[str] = None, q: Optional[str] = Query(default=None, max_length=10_000), - limit: int = Query(default=200, ge=1, le=1_000)): - """List memories directly from the store (no embedding) so browsing works even - without sentence-transformers. Live memories only (not superseded/expired).""" - import json as _json - import sqlite3 as _sql - current_service = service() + limit: int = Query(default=200, ge=1, le=1_000), + mtype: Optional[str] = Query(default=None), + cursor: Optional[str] = Query(default=None, max_length=4096), + repo: Optional[str] = Query(default=None), + valid_at: Optional[float] = Query(default=None), + known_at: Optional[float] = Query(default=None)): + """Browse a bounded page through the canonical scope and temporal boundary.""" + from engraphis.core.browsing import BrowseCursorStale + ws = workspace or _default_ws() if not ws: - # No workspace exists yet (fresh install) — nothing to list. Return an empty - # result instead of letting _clean_ws(None) raise a 500. - return {"workspace": "", "count": 0, "memories": []} - try: - ws = current_service._clean_ws(ws) - except (ValidationError, ValueError): - logger.info("dashboard memories request rejected") - raise _invalid_request() from None - # Keep this read on the live service store. A second sqlite3 connection points at - # a different database for :memory: stores and bypasses SQLCipher/custom connector - # semantics, which made the dashboard report no memories even while stats and writes - # used the populated active store. - conn = current_service.store.conn + return {"workspace": "", "count": 0, "total_count": 0, + "memories": [], "next_cursor": None} try: - row = conn.execute("SELECT id FROM workspaces WHERE name=?", (ws,)).fetchone() - if row is None: - return {"workspace": ws, "count": 0, "memories": []} - sql = ("SELECT id, scope, mtype, title, content, summary, importance, pinned, " - "valid_from, valid_to, provenance FROM memories WHERE workspace_id=? " - "AND COALESCE(scope, 'workspace')!='session' " - "AND valid_to IS NULL AND expired_at IS NULL") - args = [row["id"]] - if q: - sql += " AND (title LIKE ? ESCAPE '\\' OR content LIKE ? ESCAPE '\\')" - like = "%" + _escape_like(q) + "%" - args += [like, like] - # Manually dragged rows (sort_order set) come first, in the order they were - # dropped in; everything never touched by drag-to-reorder falls back to recency. - sql += " ORDER BY (sort_order IS NULL), sort_order ASC, COALESCE(last_access, valid_from) DESC LIMIT ?" - args.append(limit) - rows = conn.execute(sql, args).fetchall() - except _sql.Error as exc: - logger.error("dashboard memory listing failed (%s)", type(exc).__name__) - raise HTTPException(status_code=500, detail={"error": "internal server error"}) from None + return _run(service().list_memories, workspace=ws, q=q or "", mtype=mtype, + limit=limit, cursor=cursor or "", repo=repo, + valid_at=valid_at, known_at=known_at) + except BrowseCursorStale: + raise HTTPException(status_code=409, detail={ + "error": "Memory listing changed. Restart from the first page.", + "code": "cursor_stale", + }) from None - def _prov(p): - try: - return _json.loads(p) if isinstance(p, str) and p else (p or {}) - except Exception: # noqa: BLE001 - return {} - mems = [{"id": r["id"], "document_id": r["id"], "title": r["title"] or "", - "content": r["content"] or r["summary"] or "", "memory_type": r["mtype"] or "semantic", - "scope": r["scope"] or "", "pinned": bool(r["pinned"]), - "importance": r["importance"], "valid_from": r["valid_from"], - "valid_to": r["valid_to"], "provenance": _prov(r["provenance"])} for r in rows] - return {"workspace": ws, "count": len(mems), "memories": mems} + +class _ManagedProcessingReq(BaseModel): + workspace: str + enabled: StrictBool + confirmed: StrictBool = False + + +@router.get("/managed-processing") +def managed_processing_get(workspace: str): + return _run(service().managed_processing_policy, workspace) + + +@router.post("/managed-processing") +def managed_processing_set(req: _ManagedProcessingReq): + """Enable only after cloud acknowledgement; disable local uploads immediately.""" + from engraphis.cloud_features import CloudFeatureClient, CloudFeatureError + from engraphis.managed_processing import ProcessingPolicyChanged + + current_service = service() + ws = _run(current_service._clean_ws, req.workspace) + _run(current_service._authorize_workspace_control, ws) + local = _run(current_service.managed_processing_policy, ws) + if req.enabled and not req.confirmed: + raise _invalid_request() + if not req.enabled: + local = _run(current_service.set_managed_processing_policy, ws, enabled=False, + remote_sync_pending=True) + def ensure_current_local_intent(): + if current_service.managed_processing_policy(ws)["revision"] != local["revision"]: + raise ProcessingPolicyChanged( + "Processing controls changed while Cloud was responding. Reload and retry.") + + try: + cloud = CloudFeatureClient.from_environment(local["workspace_id"]) + # Enabling never refreshes a stale command into a new approval. Optout may + # retry a raced remote revision, but only while this local intent is current. + for attempt in range(3 if not req.enabled else 1): + before = cloud.get_processing_policy(local["workspace_id"]) + remote_revision = before.get("revision") + if (isinstance(remote_revision, bool) or not isinstance(remote_revision, int) + or remote_revision < 1 or not isinstance(before.get("enabled"), bool)): + raise CloudFeatureError("Cloud processing policy was not acknowledged.", status=503) + _run(ensure_current_local_intent) + try: + remote = cloud.set_processing_policy( + local["workspace_id"], enabled=req.enabled, confirmed=req.confirmed, + revision=remote_revision) + except CloudFeatureError as exc: + _run(ensure_current_local_intent) + if not req.enabled and exc.status == 409 and attempt < 2: + continue + raise + break + _run(ensure_current_local_intent) + revision = remote.get("revision") + if (isinstance(revision, bool) or not isinstance(revision, int) + or revision != remote_revision + 1 or remote.get("enabled") is not req.enabled): + raise CloudFeatureError("Cloud processing policy was not acknowledged.", status=503) + except CloudFeatureError as exc: + _run(ensure_current_local_intent) + if not req.enabled: + return {**local, "remote_sync_pending": True, + "notice": "Local uploads are stopped. Cloud confirmation is pending; " + "already submitted work may continue until Cloud responds."} + def rejected(error=exc): + raise error + return _managed_call(rejected) + # Do not hold a database write lock over the network call. Compare the local + # revision atomically when applying its acknowledgement instead, so a late + # enable cannot undo an opt-out from another tab or process. + return _run(current_service.set_managed_processing_policy, ws, enabled=req.enabled, + confirmed=req.confirmed, remote_revision=revision, + expected_revision=local["revision"]) @router.get("/memory/{memory_id}") @@ -3670,6 +3725,7 @@ def get_license(): and summary["plan_source"] == "local" ), "trial_days": licensing.TRIAL_DAYS, + "days_by_plan": trial_days_by_plan(), }, "cloud_managed": True, "trial_seconds": licensing.TRIAL_SECONDS, diff --git a/engraphis/service.py b/engraphis/service.py index cbb7284d..60e5a848 100644 --- a/engraphis/service.py +++ b/engraphis/service.py @@ -3938,6 +3938,8 @@ def recall(self, query: str, *, workspace: Optional[str] = None, "semantic_support": result.semantic_support, "embedding_mode": result.embedding_mode, "degraded_reason": result.degraded_reason, + "vector_index_repairs_pending": result.vector_index_repairs_pending, + "vector_search_source": result.vector_search_source, "vector_search_ready": result.vector_search_ready, "vector_index_backend": _vector_index_backend_label(self.engine.index), "reranker_mode": _reranker_mode_label(self.engine.reranker), @@ -5096,6 +5098,64 @@ def link_symbol(self, symbol_id: str, memory_id: str, *, workspace: str, repo: s "receipt": receipt} # ── inspection (powers the Memory Inspector UI) ───────────────────────────── + def list_memories(self, *, workspace: str, q: str = "", mtype: Optional[str] = None, + limit: int = 200, cursor: str = "", repo: Optional[str] = None, + valid_at: Optional[float] = None, + known_at: Optional[float] = None) -> dict: + """Browse all matching non-session memories without embedding or reinforcement. + + Every transport shares the store's scope and bi-temporal predicates. Cursors + expire when the database changes; clients then restart with the same filters. + """ + from engraphis.core.browsing import browse_memories + + ws = self._clean_ws(workspace) + q = _clean_text(q, field="q", max_chars=10_000, required=False) + try: + mtypes = [MemoryType(mtype)] if mtype else None + except (ValueError, TypeError) as exc: + raise ValidationError("invalid memory type") from exc + wid = self._lookup_workspace(ws) + empty = {"workspace": ws, "count": 0, "total_count": 0, + "memories": [], "next_cursor": None} + if wid is None: + return empty + rid = None + if repo is not None: + rid = self._lookup_repo(wid, _clean_name(repo, field="repo")) + if rid is None: + return empty + page = browse_memories(self.store, SearchFilter( + workspace_id=wid, repo_id=rid, include_ancestors=True, + mtypes=mtypes, valid_at=valid_at, known_at=known_at, + ), q=q, limit=limit, cursor=cursor) + records = [{ + "id": row["id"], "document_id": row["id"], "title": row["title"] or "", + "content": row["content"] or row["summary"] or "", + "memory_type": row["mtype"] or "semantic", "scope": row["scope"] or "", + "pinned": bool(row["pinned"]), "importance": row["importance"], + "valid_from": row["valid_from"], "valid_to": row["valid_to"], + "provenance": _loads(row["provenance"], {}), + } for row in page["rows"]] + return {"workspace": ws, "count": len(records), "memories": records, + "total_count": page["total_count"], "next_cursor": page["next_cursor"], + "valid_at": page["valid_at"], "known_at": page["known_at"]} + + def managed_processing_policy(self, workspace: str) -> dict: + from engraphis.managed_processing import processing_policy + return processing_policy(self, workspace) + + def set_managed_processing_policy(self, workspace: str, *, enabled: bool, + confirmed: bool = False, + remote_revision: Optional[int] = None, + remote_sync_pending: bool = False, + expected_revision: Optional[int] = None) -> dict: + from engraphis.managed_processing import set_processing_policy + return set_processing_policy(self, workspace, enabled=enabled, confirmed=confirmed, + remote_revision=remote_revision, + remote_sync_pending=remote_sync_pending, + expected_revision=expected_revision) + def list_workspaces(self) -> dict: """Workspace/repo names with live-memory counts. On a bound instance only the permitted workspaces are listed — same boundary as every other read. diff --git a/engraphis/static/dashboard.js b/engraphis/static/dashboard.js index 18be179d..9ca636f8 100644 --- a/engraphis/static/dashboard.js +++ b/engraphis/static/dashboard.js @@ -1,4 +1,4 @@ -const API=location.origin+'/api',TRIAL_DAYS=3; +const API=location.origin+'/api'; let WS=null, WORKSPACES=[], LIC=null, RELEASE_VERSION=''; const TITLES={overview:'Overview',recall:'Recall',memories:'Memories','mem-editor':'Memory',proactive:'Proactive recall',why:'Why',timeline:'Timeline',audit:'Audit trail',graph:'Knowledge Graph',analytics:'Hosted Analytics',health:'Memory Health',consolidate:'Consolidate',automation:'Hosted Automation',workspaces:'Workspaces',team:'Team Cloud',settings:'Settings'}; const ROUTE_SECTIONS={overview:'Operate',recall:'Operate',memories:'Operate','mem-editor':'Operate',proactive:'Operate',why:'History',timeline:'History',audit:'History',graph:'Relations',analytics:'Relations',health:'Relations',consolidate:'Engine',automation:'Engine',workspaces:'Operate',team:'Engine',settings:'Engine'}; @@ -179,7 +179,7 @@ async function loadOverviewAnalytics(){ lock.textContent='PRO'; lock.className='pill pill-muted'; const offerTrial=licTrialAvailable(); - el.innerHTML='
Hosted growth, retention distribution, and decay forecast.
'+esc(lockReason(false))+'
'+(offerTrial?' ':'')+'
'; + el.innerHTML='
Hosted growth, retention distribution, and decay forecast.
'+esc(lockReason(false))+'
'+(offerTrial?' ':'')+'
'; }else el.innerHTML='
'+esc(e.message)+'
'; } } @@ -224,7 +224,7 @@ function lockReason(team){const st=licAccessState(),ends=licTrialEnds(); if(st==='trial_expired')return `Your free trial has ended${ends?` (${esc(ends)})`:''}, so hosted features are locked. The trial cannot be started again.`; if(st==='lapsed')return `Your ${esc(licPlanName())} subscription is no longer active, so hosted features are locked until billing is up to date.`; if(st==='active')return `Your ${esc(licPlanName())} subscription does not include this.`; - if(licTrialAvailable())return `The email-confirmed, no-card trial lasts exactly ${TRIAL_DAYS} active days.`; + if(licTrialAvailable()){const days=licTrialDays(team?'team':'pro');return days?`The email-confirmed, no-card trial lasts exactly ${days} active days.`:'An email-confirmed, no-card trial is available; review its duration in Cloud.'} return 'Your free trial has already been used.'} /* The Team tab describes the hosted service; it is not an answer to a refused request, and it renders for every customer including the ones who are paying for Team. Handing it @@ -236,7 +236,8 @@ function teamTeaserNote(){const ends=licTrialEnds(); if(licPlanKey()!=='team'||!licAccessLive())return lockReason(true); if(licAccessState()==='trial')return `Your free trial includes Team${ends?` until ${esc(ends)}`:''}. Organizations, roles, and seats are managed in Engraphis Cloud.`; return 'Your TEAM subscription includes this. Organizations, roles, and seats are managed in Engraphis Cloud.'} -function hostedCta(plan,content,interval){const team=plan==='team',name=team?'Team':'Pro',state=licAccessState(),current=licPlanKey();if(state==='lapsed')return {label:'Update billing',href:hostedAccountUrl(content||'account'),kind:'account'};if(licAccessLive()&&(current===plan||(current==='team'&&plan==='pro')))return {label:current==='team'&&team?'Open Team Cloud':'Open Engraphis Cloud',href:hostedAccountUrl(content||'account'),kind:'account'};const trial=licTrialAvailable()&&state==='inactive';return {label:trial?`Start ${TRIAL_DAYS}-day ${name} trial`:`Subscribe to ${name}`,href:hostedPlanUrl(plan,trial,interval||'monthly',content||plan),kind:trial?'trial':'subscribe'} } +function licTrialDays(plan){const trial=(LIC&&LIC.trial)||{},days=(trial.days_by_plan||{})[plan]??(plan==='pro'?trial.trial_days:null);return Number.isSafeInteger(days)&&days>0?days:null} +function hostedCta(plan,content,interval){const team=plan==='team',name=team?'Team':'Pro',state=licAccessState(),current=licPlanKey();if(state==='lapsed')return {label:'Update billing',href:hostedAccountUrl(content||'account'),kind:'account'};if(licAccessLive()&&(current===plan||(current==='team'&&plan==='pro')))return {label:current==='team'&&team?'Open Team Cloud':'Open Engraphis Cloud',href:hostedAccountUrl(content||'account'),kind:'account'};const trial=licTrialAvailable()&&state==='inactive',days=licTrialDays(plan);return {label:trial?`Start ${days?`${days}-day `:''}${name} trial`:`Subscribe to ${name}`,href:hostedPlanUrl(plan,trial,interval||'monthly',content||plan),kind:trial?'trial':'subscribe'} } function ctaLinkHtml(cta,className,content){return `${esc(cta.label)}`} function unlockHtml(feature,plan){const team=plan==='team',name=team?'Team':'Pro',featureKey=`feature_${String(feature).toLowerCase().replace(/[^a-z0-9]+/g,'_')}`,primary=hostedCta(plan,featureKey),annual=primary.kind==='account'?'':{label:`Annual ${name} option`,href:hostedPlanUrl(plan,false,'annual',`${featureKey}_annual`),kind:'subscribe'},price=team?'$20 per seat/month or $200 per seat/year':'$10/month or $100/year',detail=lockReason(team),benefits=team?['Everything in Pro','Hosted organizations, invitations, and named seats','Roles, scoped credentials, and Team audit history']:['Hosted Cloud Sync across your installations','Growth, retention, decay, and entity Analytics','Auto Consolidation with hosted retention policies','Auto Dreaming with reviewable managed proposals','Priority support'],lede=team?'Team adds shared workspaces, named seats, roles, and remote agent access.':'Support continued Engraphis development with Pro. Your subscription helps cover hosted infrastructure and ongoing development while unlocking Cloud Sync, Analytics, Auto Consolidation, and Auto Dreaming across your installations.';return `
ENGRAPHIS ${name.toUpperCase()}

Unlock ${esc(feature)} and more

${lede}

${price}
Your license unlocks
    ${benefits.map(item=>`
  • ${esc(item)}
  • `).join('')}

${detail}

${ctaLinkHtml(primary,'btn btn-primary',name.toLowerCase())}${annual.href&&annual.href!=='#'?ctaLinkHtml(annual,'btn btn-ghost',`${featureKey}_annual`):''}
`} function startTrialPlan(plan){const url=hostedPlanUrl(plan,true);if(url==='#'){toast('Hosted signup URL is not configured','err');return}const link=document.createElement('a');link.href=url;link.target='_blank';link.rel='noopener';link.click()} @@ -266,8 +267,8 @@ function statMini(v,l,color){const tone=color==='var(--red)'?' tone-red':(color= function renderAnalytics(a,isPortfolio){const t=a.totals||{},f=a.decay_forecast||{};const weeks=a.growth_weekly||[];const gp=Math.max(...weeks,1);const gitems=weeks.map((n,i)=>{const back=weeks.length-1-i;return barRow(back===0?'now':back+'w ago',n,gp,'var(--accent-dim)')}).join('')||'
No data
';const hist=a.retention_histogram||{};const hc=hist.counts||[],hb=hist.buckets||[];const hp=Math.max(...hc,1);const hitems=hb.map((b,i)=>barRow(b,hc[i]||0,hp,'var(--green)')).join('');const mix=a.resolver_mix||{};const mk=Object.keys(mix);const mp=Math.max(...Object.values(mix),1);const mitems=mk.length?mk.map(k=>barRow(k,mix[k],mp,'var(--blue)')).join(''):'
No resolver events yet.
';const bt=a.by_type||{};const btk=Object.keys(bt);const bp=Math.max(...Object.values(bt),1);const btitems=btk.length?btk.map(k=>barRow(k,bt[k],bp,'var(--accent)')).join(''):'
No memories yet.
';const ents=a.top_entities||[];const ep=Math.max(...ents.map(e=>e.n),1);const eitems=ents.length?ents.map(e=>barRow(e.name+(isPortfolio&&e.workspace?' · '+e.workspace:''),e.n,ep,'var(--cyan)')).join(''):'
No entities yet — they appear as the graph grows.
';const avg=Math.round((t.avg_retention||0)*100);let wsTable='';if(isPortfolio&&a.workspaces){wsTable=`
Per-workspace breakdown
${a.workspaces.map(w=>``).join('')}
WorkspaceLivePinnedAvg ret.Fading 7d
${esc(w.workspace)}${w.live}${w.pinned}${Math.round((w.avg_retention||0)*100)}%${w.at_risk_7d}
`}return `
${statMini(t.live!=null?t.live:'—','Live memories')}${statMini(avg+'%','Avg retention',avg<40?'var(--red)':(avg<70?'var(--amber)':'var(--green)'))}${statMini(f.at_risk_7d!=null?f.at_risk_7d:'—','Fading ≤ 7 days',f.at_risk_7d>0?'var(--amber)':'')}${statMini(f.at_risk_30d!=null?f.at_risk_30d:'—','Fading ≤ 30 days')}${statMini(t.pinned!=null?t.pinned:'—','Pinned (protected)')}${isPortfolio?statMini(t.workspaces||0,'Workspaces'):statMini(t.superseded!=null?t.superseded:'—','Superseded (history)')}
Memories written per week
${gitems}
Retention distribution
${hitems}
By type
${btitems}
Write-path resolver activity
${mitems}
Most connected entities
${eitems}
${wsTable}`} /* A consent-required response is a valuable moment to show the job Pro can do, not a dead end about configuration. A customer with live access must never be offered their - own plan again: hosted features are on by default once their account is available. */ -function managedConsentHtml(feature){const automation=/automation/i.test(feature),featureKey=`managed_${String(feature).toLowerCase().replace(/[^a-z0-9]+/g,'_')}`,live=licAccessLive(),trial=licTrialAvailable(),copy=automation?{eyebrow:'MEMORY MAINTENANCE',title:'Let your memory improve after you log off.',lede:'Turn repetitive cleanup into a steady, reviewable habit. Pro watches the rhythm of your workspace and brings the useful changes back for approval.',cards:[['CONSOLIDATE','Distill recurring work into durable knowledge on a cadence you control.'],['DREAM','Surface useful links after accumulation and idle time, before fresh context gets buried.'],['REVIEW','Every managed result is a proposal. Nothing silently rewrites your local memory.']]}:{eyebrow:'MEMORY INTELLIGENCE',title:'See the memory your team is about to lose.',lede:'Pro turns your local memory into an operating signal—so you can see what is growing, what is fading, and what is quietly shaping recall.',cards:[['GROWTH','Separate knowledge that compounds from activity that only accumulates.'],['RETENTION','Catch fading context before an important answer disappears from reach.'],['ENTITY SIGNAL','See the people, projects, and ideas organizing your workspace.']]};const primary=hostedCta('pro',featureKey),annual=primary.kind==='account'?'':{label:'Annual Pro option',href:hostedPlanUrl('pro',false,'annual',`${featureKey}_annual`),kind:'subscribe'},actions=`${ctaLinkHtml(primary,'btn btn-primary',featureKey)}${annual.href?ctaLinkHtml(annual,'btn btn-ghost',`${featureKey}_annual`):''}`,next=live?'Included in your Pro plan. Hosted insights and maintenance are on by default—nothing else to configure.':licAccessState()==='lapsed'?'Your subscription needs billing attention. Update billing to restore hosted insights and maintenance.':trial?`Start with ${TRIAL_DAYS} days of Pro. Hosted insights and maintenance come on automatically—no settings, toggles, or worker setup.`:'Subscribe to Pro and hosted insights and maintenance come on automatically—no settings, toggles, or worker setup.';return `
ENGRAPHIS PRO /${copy.eyebrow}

${copy.title}

${copy.lede}

${next}

${actions}
WHAT PRO IS WATCHING
${copy.cards.map(card=>`
${card[0]}

${card[1]}

`).join('')}
Your memory stays yours. Hosted work is automatic with Pro. Secret and session-scoped memories stay local.
`} + own plan again. Readable processing requires a separate workspace choice. */ +function managedConsentHtml(feature){const automation=/automation/i.test(feature),featureKey=`managed_${String(feature).toLowerCase().replace(/[^a-z0-9]+/g,'_')}`,live=licAccessLive(),trial=licTrialAvailable(),copy=automation?{eyebrow:'MEMORY MAINTENANCE',title:'Let your memory improve after you log off.',lede:'Turn repetitive cleanup into a steady, reviewable habit. Pro watches the rhythm of your workspace and brings the useful changes back for approval.',cards:[['CONSOLIDATE','Distill recurring work into durable knowledge on a cadence you control.'],['DREAM','Surface useful links after accumulation and idle time, before fresh context gets buried.'],['REVIEW','Every managed result is a proposal. Nothing silently rewrites your local memory.']]}:{eyebrow:'MEMORY INTELLIGENCE',title:'See the memory your team is about to lose.',lede:'Pro turns your local memory into an operating signal—so you can see what is growing, what is fading, and what is quietly shaping recall.',cards:[['GROWTH','Separate knowledge that compounds from activity that only accumulates.'],['RETENTION','Catch fading context before an important answer disappears from reach.'],['ENTITY SIGNAL','See the people, projects, and ideas organizing your workspace.']]};const primary=hostedCta('pro',featureKey),annual=primary.kind==='account'?'':{label:'Annual Pro option',href:hostedPlanUrl('pro',false,'annual',`${featureKey}_annual`),kind:'subscribe'},settings=`Review workspace processing`,actions=`${settings}${ctaLinkHtml(primary,'btn btn-ghost',featureKey)}${annual.href?ctaLinkHtml(annual,'btn btn-ghost',`${featureKey}_annual`):''}`,next=live?'Included in your plan. Readable managed processing is paused until you approve this workspace in Manage > Settings.':licAccessState()==='lapsed'?'Your subscription needs billing attention. Restore access, then confirm workspace processing before new readable uploads.':trial?`Start ${licTrialDays('pro')?`with ${licTrialDays('pro')} days of Pro`:'a Pro trial'}. After connecting, explicitly approve each workspace in Manage > Settings.`:'Subscribe to Pro, then explicitly approve each workspace in Manage > Settings.';return `
ENGRAPHIS PRO /${copy.eyebrow}

${copy.title}

${copy.lede}

${esc(next)}

${actions}
WHAT PRO IS WATCHING
${copy.cards.map(card=>`
${card[0]}

${card[1]}

`).join('')}
Your memory stays yours. Readable processing requires your workspace approval. Encrypted Cloud Sync is a separate choice. Secret and session-scoped memories stay local.
`} function managedConsentRequired(error){return error&&error.status===409&&error.detail&&error.detail.code==='consent_required'} const CLOUD_SYNC_PRIVACY_COPY='Cloud Sync encrypts eligible shared-workspace changes end-to-end before they leave this device. Engraphis Cloud cannot read their contents; secret and session-scoped memories stay local.'; const EXTERNAL_LLM_PRIVACY_COPY='Memory text is sent to your configured LLM provider for processing under that provider’s terms. The provider must read that text to return extracted facts.'; @@ -299,8 +300,8 @@ async function loadAnalytics(){const el=document.getElementById('analytics-body' /* ── hosted automation policy (Pro / Team) ── */ async function loadAutomation(){const el=document.getElementById('automation-body'),lock=document.getElementById('au-lock'),ws='?workspace='+encodeURIComponent(WS||'');el.innerHTML='
';try{const p=await api('/automation'+ws);setPlanPill(lock,licTrialActive()?'TRIAL':'CLOUD','pill pill-accent');const last=p.last_run?fmtRel(p.last_run):'never',dream=p.dream_enabled!=null?p.dream_enabled:p.dream;el.innerHTML=`
Hosted maintenance policy
The cloud returns reviewable proposals. Pinned memories remain protected.
Cloud worker status
Status${p.enabled?'ENABLED':'OFF'}
Last run${esc(last)}
Requesting managed work uploads the selected workspace’s normal and sensitive memory content, excluding secret and session-scoped rows, capped at 16 MiB, over HTTPS without end-to-end encryption. Results are proposals and never automatically write the local database.
`}catch(e){if(managedConsentRequired(e)||cloudTrialSignupRequired(e)){setPlanPill(lock,'CLOUD','pill pill-accent');el.innerHTML=managedConsentHtml('Hosted Automation')}else if(hostedFeatureUnavailable(e)){setPlanPill(lock,'PRO','pill pill-muted');el.innerHTML=unlockHtml('Automation, Auto Consolidation, and Auto Dreaming','pro')}else{el.innerHTML='
'+esc(e.message)+'
'}}} -async function saveAutomation(){const body={enabled:document.getElementById('au-enabled').checked,cadence_hours:Number(document.getElementById('au-cadence').value)||24,consolidate:document.getElementById('au-consolidate').checked,min_cluster:Number(document.getElementById('au-mincluster').value)||3,archive_below:Number(document.getElementById('au-archive').value)||0.05,dream_enabled:document.getElementById('au-dream').checked,dream_min_new:Number(document.getElementById('au-dream-min').value)||20,dream_idle_minutes:Number(document.getElementById('au-dream-idle').value)};try{await api('/automation?workspace='+encodeURIComponent(WS||''),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});toast('Hosted policy saved','ok');loadAutomation()}catch(e){if(managedConsentRequired(e)){const result=document.getElementById('au-result');if(result)result.innerHTML=managedConsentHtml('Hosted Automation');toast('Hosted Automation starts automatically with Pro','err');return}toast((e.status===402||e.status===501)?'Hosted Automation requires Pro or Team':e.message,'err')}} -async function runMaintenance(){const el=document.getElementById('au-result');if(el)el.innerHTML='
';try{const d=await api('/maintenance/run?workspace='+encodeURIComponent(WS||''),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({dry_run:true})});if(el)el.innerHTML=`PROPOSAL Hosted work was submitted for review.
${esc(JSON.stringify(d,null,2))}
`;toast('Managed proposal requested','ok')}catch(e){if(el)el.innerHTML=managedConsentRequired(e)?managedConsentHtml('Hosted Automation'):'
'+esc(e.message)+'
';toast(managedConsentRequired(e)?'Hosted Automation starts automatically with Pro':((e.status===402||e.status===501)?'Hosted Automation requires Pro or Team':e.message),'err')}} +async function saveAutomation(){const body={enabled:document.getElementById('au-enabled').checked,cadence_hours:Number(document.getElementById('au-cadence').value)||24,consolidate:document.getElementById('au-consolidate').checked,min_cluster:Number(document.getElementById('au-mincluster').value)||3,archive_below:Number(document.getElementById('au-archive').value)||0.05,dream_enabled:document.getElementById('au-dream').checked,dream_min_new:Number(document.getElementById('au-dream-min').value)||20,dream_idle_minutes:Number(document.getElementById('au-dream-idle').value)};try{await api('/automation?workspace='+encodeURIComponent(WS||''),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});toast('Hosted policy saved','ok');loadAutomation()}catch(e){if(managedConsentRequired(e)){const result=document.getElementById('au-result');if(result)result.innerHTML=managedConsentHtml('Hosted Automation');toast('Approve workspace processing in Manage > Settings','err');return}toast((e.status===402||e.status===501)?'Hosted Automation requires Pro or Team':e.message,'err')}} +async function runMaintenance(){const el=document.getElementById('au-result');if(el)el.innerHTML='
';try{const d=await api('/maintenance/run?workspace='+encodeURIComponent(WS||''),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({dry_run:true})});if(el)el.innerHTML=`PROPOSAL Hosted work was submitted for review.
${esc(JSON.stringify(d,null,2))}
`;toast('Managed proposal requested','ok')}catch(e){if(el)el.innerHTML=managedConsentRequired(e)?managedConsentHtml('Hosted Automation'):'
'+esc(e.message)+'
';toast(managedConsentRequired(e)?'Approve workspace processing in Manage > Settings':((e.status===402||e.status===501)?'Hosted Automation requires Pro or Team':e.message),'err')}} const runMaintenanceBase=runMaintenance; const saveAutomationBase=saveAutomation; @@ -1219,7 +1220,7 @@ function loadAllGraphEngine(){ if(typeof EngraphisEveryGraph!=='undefined')return Promise.resolve(); if(!ALL_GRAPH_ENGINE_LOADING){ ALL_GRAPH_ENGINE_LOADING=new Promise((resolve,reject)=>{ - const script=document.createElement('script');script.src='/v2-assets/engraphis-graph-every.js?v=20260823-every-19'; + const script=document.createElement('script');script.src='/v2-assets/engraphis-graph-every.js?v=20260905-every-20'; script.onload=()=>{typeof EngraphisEveryGraph==='undefined'?reject(new Error('Every-node graph asset loaded without registering EngraphisEveryGraph')):resolve()}; script.onerror=()=>reject(new Error('Every-node graph asset could not load')); document.head.appendChild(script); diff --git a/engraphis/static/index.html b/engraphis/static/index.html index ca3d53bc..f4b9eebd 100644 --- a/engraphis/static/index.html +++ b/engraphis/static/index.html @@ -349,6 +349,6 @@ graph view. dashboard.js fetches both on demand from graphRender(); see loadForceGraph() and loadGraphEngine(). scripts/externalize_dashboard_assets.py enforces both halves: they stay out of this file, and the lazy references still have to resolve. --> - + diff --git a/eval/datasets/resolver_write_acceptance.jsonl b/eval/datasets/resolver_write_acceptance.jsonl new file mode 100644 index 00000000..1bec35ec --- /dev/null +++ b/eval/datasets/resolver_write_acceptance.jsonl @@ -0,0 +1,10 @@ +{"id":"write-ttl","neighbor":"The production cache expires after 30 seconds.","candidate":"The production cache expires after 90 seconds.","expected":"invalidate"} +{"id":"write-window","neighbor":"The release window begins at 03:00 UTC.","candidate":"The release window begins at 05:00 UTC.","expected":"invalidate","subject_key":"release-window","claim_kind":"start"} +{"id":"write-reworded","neighbor":"Use the north regional artifact mirror.","candidate":"Build artifacts now come from the southern mirror.","expected":"invalidate","subject_key":"artifact-source","claim_kind":"mirror"} +{"id":"write-keyed-limit","neighbor":"The compiler worker accepts 8 jobs concurrently.","candidate":"The compiler worker accepts 16 jobs concurrently.","expected":"invalidate","subject_key":"compiler-worker","claim_kind":"concurrency"} +{"id":"keep-environments","neighbor":"The production worker retries after 30 seconds.","candidate":"The staging worker retries after 30 seconds.","expected":"add"} +{"id":"keep-environment-binding","neighbor":"The staging database holds 300 connections in production environment.","candidate":"The production database holds 300 connections in staging environment.","expected":"add"} +{"id":"keep-backup-binding","neighbor":"The production backup restores the staging database during recovery.","candidate":"The staging backup restores the production database during recovery.","expected":"add"} +{"id":"keep-account-identity","neighbor":"Customer account 100 has 30 days of audit retention.","candidate":"Customer account 200 has 30 days of audit retention.","expected":"add"} +{"id":"keep-subjects","neighbor":"ServiceAlpha uses the regional mirror for every production deployment.","candidate":"ServiceBeta uses the regional mirror for every production deployment.","expected":"add"} +{"id":"keep-different-attributes","neighbor":"The primary worker stores source artifacts in the package registry.","candidate":"The primary worker executes integration tests in the isolated sandbox.","expected":"add"} diff --git a/eval/fts_insert_scaling.py b/eval/fts_insert_scaling.py new file mode 100644 index 00000000..04f1d48a --- /dev/null +++ b/eval/fts_insert_scaling.py @@ -0,0 +1,102 @@ +"""File-backed counterfactual: force the former FTS delete versus new-row insertion. + +Everything else uses the same current Store and NumPy backend. This isolates the +delete change; it is not a historical release benchmark or end-to-end write test. +""" +from __future__ import annotations + +import argparse +from pathlib import Path +import tempfile +import time + +import numpy as np + +from engraphis.backends.vector_numpy import NumpyVectorIndex +from engraphis.core.store import Store +from eval.benchmark import report_envelope, write_canonical_artifact +from eval.vector_scale import _normalized_random, parse_sizes +from eval.vector_scale_storage import ( + _ROOT, _SOURCES, _disk, _hardware, _insert, _source_snapshot, +) + + +def run_comparison(sizes, *, dim=256, batch_size=500, seed=20260731): + sizes = parse_sizes(",".join(map(str, sizes))) + if min(dim, batch_size) < 1: + raise ValueError("dimension and batch_size must be positive") + before = _source_snapshot() + cells = [] + for strategy in ("forced_legacy_delete", "new_row_insert"): + for size in sizes: + with tempfile.TemporaryDirectory(prefix="egr-fts-") as folder: + path = Path(folder) / "corpus.db" + store = Store(str(path)) + try: + if not store.has_fts5: + raise RuntimeError("FTS5 required for the unindexed-column comparison") + workspace_id = store.get_or_create_workspace("fts-scaling") + repo_ids = [store.get_or_create_repo(workspace_id, f"scope-{i}") + for i in range(4)] + index = NumpyVectorIndex(store, dim=dim) + if strategy == "forced_legacy_delete": + original = store._fts_upsert + + def force_delete(mid, title, content, keywords, **_kwargs): + return original(mid, title, content, keywords, + replace_existing=True) + + store._fts_upsert = force_delete + rng = np.random.default_rng(seed) + started = time.perf_counter() + for offset in range(0, size, batch_size): + stop = min(size, offset + batch_size) + vectors = _normalized_random(rng, stop - offset, dim) + _insert(store, index, range(offset, stop), vectors, workspace_id, repo_ids) + elapsed = time.perf_counter() - started + rows = { + name: int(store.conn.execute(f"SELECT COUNT(*) FROM {name}").fetchone()[0]) + for name in ("memories", "mem_vectors", "mem_fts") + } + if set(rows.values()) != {size}: + raise RuntimeError("canonical/vector/FTS cardinality mismatch") + cells.append({"strategy": strategy, "corpus_size": size, + "elapsed_seconds": elapsed, "records_per_second": size / elapsed, + "verified_row_counts": rows, "disk": _disk(path)}) + finally: + store.close() + after = _source_snapshot() + return report_envelope( + suite="fts-new-insert-scaling/counterfactual-v1", dataset_path=Path(__file__), + config={"sizes": sizes, "dimension": dim, "batch_size": batch_size, "seed": seed}, + records=[{"question_id": f"{cell['strategy']}-{cell['corpus_size']}", + "category": "canonical_storage_population"} for cell in cells], + metrics={"cells": cells, "hardware": _hardware(), "source_before": before, + "source_after": after, "source_stable": before == after, + "measurement_scope": "current synthetic Store+NumPy writes, forcing the former FTS deletion in one arm", + "unmeasured": ["historical release behavior", "embedding or resolution latency", + "independent process repetitions", "external contention"]}, + source_paths=[_ROOT / name for name in _SOURCES] + [Path(__file__)], + models={"embedding": {"identity": "none; precomputed synthetic vectors"}, + "vector_backend": {"identity": "NumpyVectorIndex"}}, + command=["python", "-m", "eval.fts_insert_scaling", "--sizes", ",".join(map(str, sizes)), + "--dim", str(dim), "--batch-size", str(batch_size), "--seed", str(seed)], + ) + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--sizes", default="1000,5000,10000") + parser.add_argument("--dim", type=int, default=256) + parser.add_argument("--batch-size", type=int, default=500) + parser.add_argument("--seed", type=int, default=20260731) + parser.add_argument("--output", required=True) + args = parser.parse_args(argv) + report = run_comparison(parse_sizes(args.sizes), dim=args.dim, + batch_size=args.batch_size, seed=args.seed) + print(write_canonical_artifact(report, args.output)) + return 0 if report["metrics"]["source_stable"] else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/eval/native_coverage_scaling.py b/eval/native_coverage_scaling.py new file mode 100644 index 00000000..2cbd0cb5 --- /dev/null +++ b/eval/native_coverage_scaling.py @@ -0,0 +1,166 @@ +"""Isolate native coverage verification; the legacy oracle is frozen pre-optimization. + +Both arms verify canonical contents. Only reverse native traversal versus exact +cardinality differs; this is a method ablation, not a historical release result. +""" +from __future__ import annotations + +import argparse +from pathlib import Path +import tempfile +import time + +import numpy as np + +from engraphis.backends.vector_sqlitevec import ( + SqliteVecVectorIndex, _COVERAGE_BATCH_SIZE, _expected_native_vector, + _native_mirror_covers_canonical, _native_vector_matches, +) +from engraphis.core.store import Store +from eval.benchmark import report_envelope, write_canonical_artifact +from eval.vector_scale import _normalized_random, parse_sizes +from eval.vector_scale_storage import _ROOT, _SOURCES, _hardware, _insert, _source_snapshot + + +def _legacy_reverse_scan(conn, dimension: int) -> bool: + """Whether vec0 exactly mirrors every same-dimension canonical vector. + + Both scans are keyset-paginated and all counterpart lookups stay below SQLite's + conservative variable limit. The caller supplies the transaction: writable callers + hold ``BEGIN IMMEDIATE`` while publishing, and read-only callers hold one snapshot. + """ + after_id = "" + while True: + canonical_rows = conn.execute( + "SELECT v.id, v.vector FROM mem_vectors v " + "JOIN memories m ON m.id=v.id " + "WHERE v.dim=? AND v.id>? ORDER BY v.id LIMIT ?", + (dimension, after_id, _COVERAGE_BATCH_SIZE), + ).fetchall() + if not canonical_rows: + break + ids = [str(row["id"]) for row in canonical_rows] + marks = ",".join("?" for _ in ids) + native_rows = conn.execute( + f"SELECT id, embedding FROM mem_vec_ann WHERE id IN ({marks})", ids, + ).fetchall() + native = {str(row["id"]): row["embedding"] for row in native_rows} + for row in canonical_rows: + memory_id = str(row["id"]) + valid, expected = _expected_native_vector(row["vector"], dimension) + if not valid: + return False + if expected is None: + if memory_id in native: + return False + elif not _native_vector_matches( + native.get(memory_id), expected, dimension, + ): + return False + after_id = ids[-1] + if len(canonical_rows) < _COVERAGE_BATCH_SIZE: + break + + # The forward scan proves that nothing canonical is missing or stale. This reverse + # scan rejects orphaned native rows and rows whose canonical vector became zero or + # changed dimension after another backend wrote the portable mirror. + after_id = "" + while True: + native_rows = conn.execute( + "SELECT id, embedding FROM mem_vec_ann " + "WHERE id>? ORDER BY id LIMIT ?", + (after_id, _COVERAGE_BATCH_SIZE), + ).fetchall() + if not native_rows: + break + ids = [str(row["id"]) for row in native_rows] + marks = ",".join("?" for _ in ids) + canonical_rows = conn.execute( + "SELECT v.id, v.vector FROM mem_vectors v " + "JOIN memories m ON m.id=v.id " + f"WHERE v.dim=? AND v.id IN ({marks})", + (dimension, *ids), + ).fetchall() + canonical = {str(row["id"]): row["vector"] for row in canonical_rows} + for row in native_rows: + memory_id = str(row["id"]) + valid, expected = _expected_native_vector( + canonical.get(memory_id), dimension, + ) + if ( + not valid + or expected is None + or not _native_vector_matches( + row["embedding"], expected, dimension, + ) + ): + return False + after_id = ids[-1] + if len(native_rows) < _COVERAGE_BATCH_SIZE: + break + return True + + +def run_comparison(sizes, *, dim=256, batch_size=500, seed=20260731): + sizes = parse_sizes(",".join(map(str, sizes))) + if min(dim, batch_size) < 1: + raise ValueError("dimension and batch_size must be positive") + before = _source_snapshot() + cells = [] + for size in sizes: + with tempfile.TemporaryDirectory(prefix="egr-cover-") as folder: + store = Store(str(Path(folder) / "corpus.db")) + try: + index = SqliteVecVectorIndex(store, dim=dim) + workspace_id = store.get_or_create_workspace("coverage-scaling") + repo_ids = [store.get_or_create_repo(workspace_id, "scope0")] + rng = np.random.default_rng(seed) + for offset in range(0, size, batch_size): + stop = min(size, offset + batch_size) + vectors = _normalized_random(rng, stop - offset, dim) + _insert(store, index, range(offset, stop), vectors, workspace_id, repo_ids) + for strategy, verify in (("legacy_reverse_scan", _legacy_reverse_scan), + ("verified_cardinality", _native_mirror_covers_canonical)): + with store.read_snapshot(): + started = time.perf_counter() + covered = verify(store.conn, dim) + elapsed = time.perf_counter() - started + if not covered: + raise RuntimeError("coverage method rejected the identical complete mirror") + cells.append({"strategy": strategy, "corpus_size": size, + "elapsed_seconds": elapsed, "coverage_verified": covered}) + finally: + store.close() + after = _source_snapshot() + return report_envelope( + suite="native-coverage-scaling/counterfactual-v1", dataset_path=Path(__file__), + config={"sizes": sizes, "dimension": dim, "batch_size": batch_size, "seed": seed}, + records=[{"question_id": f"{cell['strategy']}-{cell['corpus_size']}", + "category": "native_coverage_verification"} for cell in cells], + metrics={"cells": cells, "hardware": _hardware(), "source_before": before, + "source_after": after, "source_stable": before == after, + "measurement_scope": "same current canonical/native store; only the verification traversal differs", + "unmeasured": ["historical release behavior", "independent process repetitions", + "external contention", "end-to-end recall"]}, + source_paths=[_ROOT / name for name in _SOURCES] + [Path(__file__)], + command=["python", "-m", "eval.native_coverage_scaling", "--sizes", ",".join(map(str, sizes)), + "--dim", str(dim), "--batch-size", str(batch_size), "--seed", str(seed)], + ) + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--sizes", default="1000,5000,10000") + parser.add_argument("--dim", type=int, default=256) + parser.add_argument("--batch-size", type=int, default=500) + parser.add_argument("--seed", type=int, default=20260731) + parser.add_argument("--output", required=True) + args = parser.parse_args(argv) + report = run_comparison(parse_sizes(args.sizes), dim=args.dim, + batch_size=args.batch_size, seed=args.seed) + print(write_canonical_artifact(report, args.output)) + return 0 if report["metrics"]["source_stable"] else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/eval/resolver_reworded_corrections.py b/eval/resolver_reworded_corrections.py index ddecf82c..72295040 100644 --- a/eval/resolver_reworded_corrections.py +++ b/eval/resolver_reworded_corrections.py @@ -18,9 +18,11 @@ candidate as a correction of the neighbour) or ``"add"`` (the resolver should add it as a distinct fact). -This is an offline-only evaluation: it does not require an embedder, -``engraphis-mcp``, or any external service. The resolver's only -configuration is its importable constants. +The default is a resolver-unit fixture with injected similarity. ``--end-to-end`` +instead measures candidate discovery and both writes through the production +engine with its deterministic offline embedder; it does not inject similarity. +Neither mode calls an external service. Distinct-fact negatives must survive +both false invalidation and false NOOP decisions. """ from __future__ import annotations @@ -46,30 +48,68 @@ def _memory_record(text: str, record_id: str) -> MemoryRecord: ) -def evaluate(dataset: Path = DATASET) -> dict[str, Any]: +def _write_pair(row: dict, engine) -> tuple[str, str, bool, bool]: + """Return real write outcome and survival without replacing candidate lookup.""" + workspace_id = engine.store.get_or_create_workspace("resolver-acceptance") + repo_id = engine.store.get_or_create_repo(workspace_id, str(row["id"])) + shared = {"workspace_id": workspace_id, "repo_id": repo_id} + shared.update({key: row[key] for key in ("subject_key", "claim_kind") if key in row}) + before = engine.remember_with_resolution(row["neighbor"], **shared) + after = engine.remember_with_resolution(row["candidate"], **shared) + old_record = engine.store.get_memory(before["id"]) + new_record = engine.store.get_memory(after["id"]) + old_survives = bool(old_record and old_record.valid_to is None) + new_survives = bool( + new_record and new_record.content == row["candidate"] + and after["id"] != before["id"] and new_record.valid_to is None + ) + return str(after["op"]), str(after.get("reason", "")), old_survives, new_survives + + +def evaluate(dataset: Path = DATASET, *, end_to_end: bool = False) -> dict[str, Any]: positives = 0 positives_superseded = 0 negatives = 0 false_invalidations: list[dict[str, Any]] = [] + false_noops: list[dict[str, Any]] = [] + lost_distinct_facts: list[str] = [] missed_corrections: list[dict[str, Any]] = [] + records: list[dict[str, Any]] = [] total = 0 + seen_ids: set[str] = set() with dataset.open("r", encoding="utf-8") as f: for line in f: line = line.strip() if not line: continue row = json.loads(line) + case_id = row.get("id") + if not isinstance(case_id, str) or not case_id or case_id in seen_ids: + raise ValueError("resolver cases require distinct non-empty string ids") + seen_ids.add(case_id) total += 1 expected = row["expected"] neighbor = _memory_record(row["neighbor"], f"mem_{row['id']}_n") # Use a high similarity so the resolver's strong/rewrite gates # are exercised for every row. The labeled ground truth tells # us whether the resolver should INVALIDATE or ADD. - resolution = resolve( - row["candidate"], - [(0.9, neighbor)], - ) - actual = resolution.op.value + if expected not in {"invalidate", "add"}: + raise ValueError("expected must be invalidate or add") + old_survives = new_survives = True + if end_to_end: + from engraphis.core.engine import MemoryEngine + + engine = MemoryEngine.create(":memory:") + try: + actual, reason, old_survives, new_survives = _write_pair(row, engine) + finally: + engine.store.close() + else: + resolution = resolve( + row["candidate"], + [(0.9, neighbor)], + ) + actual, reason = resolution.op.value, resolution.reason if expected == "invalidate": positives += 1 if actual == "invalidate": @@ -79,7 +119,7 @@ def evaluate(dataset: Path = DATASET) -> dict[str, Any]: "id": row["id"], "expected": "invalidate", "actual": actual, - "reason": resolution.reason, + "reason": reason, "subject_hint": row.get("subject_hint", ""), }) else: @@ -89,9 +129,21 @@ def evaluate(dataset: Path = DATASET) -> dict[str, Any]: "id": row["id"], "expected": "add", "actual": actual, - "reason": resolution.reason, + "reason": reason, "subject_hint": row.get("subject_hint", ""), }) + if actual == "noop": + false_noops.append({"id": row["id"], "reason": reason}) + if not old_survives or not new_survives: + lost_distinct_facts.append(row["id"]) + record = { + "question_id": row["id"], "expected_operation": expected, + "actual_operation": actual, + } + if end_to_end: + record.update({"old_fact_survives": old_survives, + "new_fact_survives": new_survives}) + records.append(record) summary = { "dataset": str(dataset), "total": total, @@ -99,15 +151,40 @@ def evaluate(dataset: Path = DATASET) -> dict[str, Any]: "negatives": negatives, "positives_superseded": positives_superseded, "false_invalidations": len(false_invalidations), + "false_noops": len(false_noops), + "false_noop_ids": [item["id"] for item in false_noops], + "lost_distinct_facts": len(lost_distinct_facts), + "lost_distinct_fact_ids": lost_distinct_facts, "missed_corrections": len(missed_corrections), "missed_correction_ids": [m["id"] for m in missed_corrections], "false_invalidation_ids": [f["id"] for f in false_invalidations], + "execution": "production_write_path" if end_to_end else "resolver_unit", + "similarity_injected": not end_to_end, + "correction_recall": positives_superseded / positives if positives else None, + "correction_precision": ( + positives_superseded / (positives_superseded + len(false_invalidations)) + if positives_superseded + len(false_invalidations) else None + ), + "distinct_fact_error_rate": ( + len(set([f["id"] for f in false_invalidations] + + [f["id"] for f in false_noops] + lost_distinct_facts)) / negatives + if negatives else None + ), + "records": records, } return summary def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__.split("\n\n", 1)[0]) + parser.add_argument( + "--end-to-end", action="store_true", + help="Exercise both writes and candidate discovery with the offline production engine.", + ) + parser.add_argument( + "--json", action="store_true", + help="Emit the existing provenance/redaction envelope with aggregate measurements.", + ) parser.add_argument( "--dataset", type=Path, @@ -135,26 +212,73 @@ def main(argv: list[str] | None = None) -> int: print(f"error: dataset not found at {args.dataset}", file=sys.stderr) return 2 - summary = evaluate(args.dataset) + summary = evaluate(args.dataset, end_to_end=args.end_to_end) positives = summary["positives"] superseded = summary["positives_superseded"] negatives = summary["negatives"] false_inv = summary["false_invalidations"] + failed = bool( + superseded < positives or false_inv > 0 or summary["false_noops"] > 0 + or summary["lost_distinct_facts"] > 0 + ) + if args.json: + from eval.benchmark import report_envelope + + root = Path(__file__).resolve().parents[1] + try: + public_dataset = args.dataset.resolve().relative_to(root).as_posix() + except ValueError: + # External paths may identify an owner's private directory. The + # basename plus suite digest identifies the input without that path. + public_dataset = args.dataset.name + command = ["python", "-m", "eval.resolver_reworded_corrections", + "--dataset", public_dataset, "--json"] + if args.end_to_end: + command.append("--end-to-end") + if args.audit_only: + command.append("--audit-only") + sources = [Path(__file__), root / "engraphis/core/resolve.py"] + if args.end_to_end: + sources.extend(root / path for path in ( + "engraphis/factory.py", "engraphis/core/engine.py", + "engraphis/core/store.py", "engraphis/core/schema.py", + "engraphis/core/interfaces.py", "engraphis/core/vector_search.py", + "engraphis/core/vector_repair.py", "engraphis/backends/vector_numpy.py", + "engraphis/backends/embedder_deterministic.py", + )) + report = report_envelope( + suite="resolver-write-acceptance" if args.end_to_end else "resolver-unit", + dataset_path=args.dataset, + config={"end_to_end": args.end_to_end, "offline": True, + "evidence_scope": "authored regression corpus; not an independent quality benchmark"}, + records=summary["records"], + metrics={key: value for key, value in summary.items() + if key not in {"dataset", "records"}}, + source_paths=sources, + command=command, + ) + print(json.dumps(report, sort_keys=True)) + return 0 if args.audit_only or not failed else 1 print( f"resolver reworded-correction eval: " f"{superseded}/{positives} positives superseded, " f"{false_inv}/{negatives} false invalidations, " + f"{summary['false_noops']}/{negatives} false NOOPs, " f"{summary['total']} pairs total" ) if summary["missed_correction_ids"]: print(f" missed corrections: {summary['missed_correction_ids']}") if summary["false_invalidation_ids"]: print(f" false invalidations: {summary['false_invalidation_ids']}") + if summary["false_noop_ids"]: + print(f" false NOOPs: {summary['false_noop_ids']}") + if summary["lost_distinct_fact_ids"]: + print(f" lost distinct facts: {summary['lost_distinct_fact_ids']}") # Default: strict — labeled regressions fail the run. CI must invoke # this script with no flags so the build gates on labeled quality. if args.audit_only: return 0 - if superseded < positives or false_inv > 0: + if failed: return 1 return 0 diff --git a/eval/vector_scale.py b/eval/vector_scale.py index 741aad50..d4d5a729 100644 --- a/eval/vector_scale.py +++ b/eval/vector_scale.py @@ -15,6 +15,7 @@ import argparse import hashlib import json +from pathlib import Path import platform import statistics import sys @@ -198,8 +199,52 @@ def main(argv: Optional[list[str]] = None) -> int: parser.add_argument("--k", type=int, default=10) parser.add_argument("--seed", type=int, default=20260731) parser.add_argument("--backend", choices=BACKENDS, default="numpy") + parser.add_argument("--file-backed", action="store_true", + help="Measure disposable persistent storage and a concurrency matrix.") + parser.add_argument("--concurrencies", default="1,4,16") + parser.add_argument("--mixed-writes", type=int, default=4) + parser.add_argument("--batch-size", type=int, default=500) + parser.add_argument("--tenants", type=int, default=4) + parser.add_argument("--output", help="Immutable redacted evidence artifact (file-backed mode).") + parser.add_argument("--progress", action="store_true", + help="Emit content-free corpus/cell progress on stderr.") parser.add_argument("--json", action="store_true", help="print the complete JSON report") args = parser.parse_args(argv) + if args.file_backed: + from eval.vector_scale_storage import run_file_backed, write_scale_checkpoint + + checkpoint_path = Path(str(args.output) + ".checkpoint.json") if args.output else None + if args.output and (Path(args.output).exists() or checkpoint_path.exists()): + parser.error("choose a new output path; an artifact or checkpoint already exists") + + report = run_file_backed( + parse_sizes(args.sizes), dim=args.dim, queries=args.queries, + iterations=args.iterations, warmups=args.warmups, k=args.k, + seed=args.seed, backend=args.backend, + concurrencies=parse_sizes(args.concurrencies), mixed_writes=args.mixed_writes, + batch_size=args.batch_size, tenants=args.tenants, + progress=(lambda event: print(json.dumps(event), file=sys.stderr, flush=True)) + if args.progress else None, + checkpoint=(lambda payload: write_scale_checkpoint(checkpoint_path, payload)) + if checkpoint_path else None, + ) + if args.output: + from eval.benchmark import write_canonical_artifact + + written = write_canonical_artifact(report, args.output) + if checkpoint_path: + write_scale_checkpoint(checkpoint_path, { + "schema": "engraphis-scale-checkpoint/v1", "status": "complete", + "artifact": Path(args.output).name, "sha256": written["sha256"], + }) + print(json.dumps({"sha256": written["sha256"], + "cells": len(report["metrics"]["cells"]), + "source_stable": report["metrics"]["source_stable"]})) + else: + print(json.dumps(report, indent=2)) + return 0 if report["metrics"]["source_stable"] else 1 + if args.output: + parser.error("--output requires --file-backed") report = run( parse_sizes(args.sizes), dim=args.dim, diff --git a/eval/vector_scale_storage.py b/eval/vector_scale_storage.py new file mode 100644 index 00000000..eac6330e --- /dev/null +++ b/eval/vector_scale_storage.py @@ -0,0 +1,429 @@ +"""File-backed exact-index measurements; no semantic or agent-quality claims. + +Extends ``eval.vector_scale`` with bounded corpus construction, scoped exact +search, restart, native mirror rebuild and concurrent canonical/index writes. +This bypasses extraction, conflict resolution, embeddings and the recall packer. +All database state is synthetic and belongs to a disposable temporary directory. +""" +from __future__ import annotations + +from concurrent.futures import ThreadPoolExecutor +import ctypes +import hashlib +import importlib.metadata +import json +import os +from pathlib import Path +import platform +import sqlite3 +import subprocess +import tempfile +import threading +import time +from typing import Callable, Optional + +import numpy as np + +from engraphis.backends.vector_numpy import NumpyVectorIndex +from engraphis.core.interfaces import MemoryRecord, MemoryType, Scope, SearchFilter +from engraphis.core.store import Store +from eval.benchmark import report_envelope, sha256_file +from eval.vector_scale import ( + BACKENDS, _latency_ms, _normalized_random, _result_hash, get_vector_index, parse_sizes, +) + +_ROOT = Path(__file__).resolve().parents[1] +_SOURCES = ( + "eval/vector_scale.py", "eval/vector_scale_storage.py", "eval/benchmark.py", + "engraphis/backends/vector_numpy.py", "engraphis/backends/vector_sqlitevec.py", + "engraphis/core/vector_search.py", "engraphis/core/store.py", + "engraphis/core/schema.py", "engraphis/core/interfaces.py", +) +_GENERATOR = "numpy.PCG64.standard_normal.float32.normalized.v1" + + +def write_scale_checkpoint(output: Path, payload: dict) -> None: + """Atomically replace a redacted checkpoint, outside timed search sections.""" + output.parent.mkdir(parents=True, exist_ok=True) + temporary = None + try: + with tempfile.NamedTemporaryFile( + mode="w", encoding="utf-8", dir=output.parent, + prefix=output.name + ".", suffix=".tmp", delete=False, + ) as handle: + temporary = Path(handle.name) + json.dump(payload, handle, sort_keys=True, separators=(",", ":"), allow_nan=False) + handle.write("\n") + os.replace(temporary, output) + finally: + if temporary is not None and temporary.exists(): + temporary.unlink() + + +def _source_snapshot() -> dict: + paths = [name for name in _SOURCES if (_ROOT / name).is_file()] + hashes = {name: sha256_file(_ROOT / name) for name in paths} + try: + diff = subprocess.check_output( + ["git", "diff", "HEAD", "--", *paths], cwd=_ROOT, + stderr=subprocess.DEVNULL, + ) + diff_hash = hashlib.sha256(diff).hexdigest() + except (OSError, subprocess.CalledProcessError): + diff_hash = None + return {"files": hashes, "tracked_diff_sha256": diff_hash} + + +def _memory() -> dict: + """Current RSS and process-lifetime peak; Windows does not expose resource.""" + if os.name == "nt": + from ctypes import wintypes + + class Counters(ctypes.Structure): + _fields_ = [("cb", wintypes.DWORD), ("faults", wintypes.DWORD)] + [ + (name, ctypes.c_size_t) for name in ( + "peak_working", "working", "peak_paged", "paged", + "peak_nonpaged", "nonpaged", "pagefile", "peak_pagefile", + ) + ] + + values = Counters() + values.cb = ctypes.sizeof(values) + kernel = ctypes.WinDLL("kernel32", use_last_error=True) + kernel.GetCurrentProcess.restype = wintypes.HANDLE + psapi = ctypes.WinDLL("psapi", use_last_error=True) + psapi.GetProcessMemoryInfo.argtypes = ( + wintypes.HANDLE, ctypes.POINTER(Counters), wintypes.DWORD, + ) + if psapi.GetProcessMemoryInfo(kernel.GetCurrentProcess(), ctypes.byref(values), values.cb): + return {"rss_bytes": int(values.working), + "process_lifetime_peak_rss_bytes": int(values.peak_working)} + else: + try: + import resource + + peak = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss + return {"rss_bytes": None, + "process_lifetime_peak_rss_bytes": int(peak if platform.system() == "Darwin" + else peak * 1024)} + except (ImportError, OSError): + pass + return {"rss_bytes": None, "process_lifetime_peak_rss_bytes": None} + + +def _hardware() -> dict: + cpu = platform.processor() + physical_ram = None + if os.name == "nt": + try: + import winreg + + with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, + r"HARDWARE\DESCRIPTION\System\CentralProcessor\0") as key: + cpu = str(winreg.QueryValueEx(key, "ProcessorNameString")[0]).strip() + except OSError: + pass + from ctypes import wintypes + + class MemoryStatus(ctypes.Structure): + _fields_ = [("length", wintypes.DWORD), ("load", wintypes.DWORD)] + [ + (name, ctypes.c_ulonglong) for name in ( + "total_physical", "available_physical", "total_pagefile", + "available_pagefile", "total_virtual", "available_virtual", + "available_extended_virtual", + ) + ] + + status = MemoryStatus() + status.length = ctypes.sizeof(status) + if ctypes.WinDLL("kernel32").GlobalMemoryStatusEx(ctypes.byref(status)): + physical_ram = int(status.total_physical) + return {"cpu": cpu, "logical_cpus": os.cpu_count(), + "physical_ram_bytes": physical_ram, + "architecture": platform.machine(), "sqlite": sqlite3.sqlite_version, + "blas_thread_limits": {name: os.environ.get(name) for name in ( + "OPENBLAS_NUM_THREADS", "OMP_NUM_THREADS", "MKL_NUM_THREADS", + )}} + + +def _disk(path: Path) -> dict: + values = {label: Path(str(path) + suffix).stat().st_size + if Path(str(path) + suffix).exists() else 0 + for label, suffix in (("database_bytes", ""), ("wal_bytes", "-wal"), + ("shared_memory_bytes", "-shm"))} + return {**values, "total_bytes": sum(values.values())} + + +def _insert(store, index, numbers, vectors, workspace_id, repo_ids) -> None: + """One bounded atomic canonical/native batch, using production storage APIs.""" + store.conn.execute("BEGIN IMMEDIATE") + try: + ids = [] + for number, vector in zip(numbers, vectors): + memory_id = f"mem_scale_{number:09d}" + ids.append(memory_id) + store.add_memory(MemoryRecord( + id=memory_id, content=f"Synthetic exact-index record {number}.", + mtype=MemoryType.EPISODIC, scope=Scope.REPO, + workspace_id=workspace_id, repo_id=repo_ids[number % len(repo_ids)], + ), audit=False, commit=False) + if not getattr(index, "shares_store_vector_table", False): + store.put_vector(memory_id, vector, model=_GENERATOR) + index.upsert(ids, vectors, [{"model": _GENERATOR} for _ in ids], commit=False) + store.conn.commit() + except BaseException: + if store.conn.transaction_owned_by_current_thread(): + store.conn.rollback() + raise + + +def _read(index, query, k, flt): + started = time.perf_counter() + result = index.search(query, k, filter=flt) + elapsed = (time.perf_counter() - started) * 1000 + if len({memory_id for memory_id, _ in result}) != len(result): + raise RuntimeError("exact index returned duplicate IDs") + if any(not np.isfinite(score) for _, score in result): + raise RuntimeError("exact index returned a non-finite score") + return elapsed, result + + +def _reads(index, query_vectors, k, flt, concurrency, iterations) -> dict: + began = time.perf_counter() + with ThreadPoolExecutor(max_workers=concurrency) as pool: + futures = [pool.submit(_read, index, query, k, flt) + for _ in range(iterations) for query in query_vectors] + samples = [future.result() for future in futures] + seconds = time.perf_counter() - began + return {"timed_searches": len(samples), + "latency_ms": _latency_ms([latency for latency, _ in samples]), + "wall_seconds": round(seconds, 6), + "searches_per_second": round(len(samples) / max(seconds, 1e-9), 3), + "result_ids_sha256": _result_hash([result for _, result in samples]), + "result_counts": sorted({len(result) for _, result in samples}), + "memory": _memory()} + + +def _mixed(store, index, query_vectors, k, flt, concurrency, count, + offset, workspace_id, repo_ids, rng) -> dict: + if count == 0: + return {"measured": False, "reason": "mixed_writes=0"} + gate = threading.Event() + + def writes(): + gate.wait() + samples = [] + for number in range(offset, offset + count): + vectors = _normalized_random(rng, 1, len(query_vectors[0])) + started = time.perf_counter() + _insert(store, index, [number], vectors, workspace_id, repo_ids) + samples.append((time.perf_counter() - started) * 1000) + return samples + + def reads(): + gate.wait() + return _reads(index, query_vectors, k, flt, concurrency, 1) + + with ThreadPoolExecutor(max_workers=2) as pool: + writer, reader = pool.submit(writes), pool.submit(reads) + started = time.perf_counter() + gate.set() + write_latencies, read_result = writer.result(), reader.result() + return {"measured": True, "starting_corpus_size": offset, "reader_concurrency": concurrency, + "writer_concurrency": 1, "committed_writes": len(write_latencies), + "write_latency_ms": _latency_ms(write_latencies), "reads": read_result, + "wall_seconds": round(time.perf_counter() - started, 6)} + + +def _native_rebuild(store, index, dim, batch_size) -> dict: + if isinstance(index, NumpyVectorIndex): + return {"measured": False, "reason": "NumPy reads canonical vectors; no separate mirror"} + # Fault injection affects only the disposable derived mirror. Canonical rows + # remain intact, and the backend's normal stale-index constructor recreates it. + store.conn.execute("UPDATE mem_vec_ann_state SET format_version=0 WHERE singleton=1") + store.conn.commit() + started = time.perf_counter() + repaired = get_vector_index(store, dim=dim, prefer="sqlite-vec") + ids, vectors = [], [] + copied = 0 + for memory_id, vector in store.iter_vectors(include_invalid=True, dim=dim): + ids.append(memory_id) + vectors.append(vector) + if len(ids) == batch_size: + repaired.upsert(ids, np.asarray(vectors, dtype=np.float32)) + copied += len(ids) + ids, vectors = [], [] + if ids: + repaired.upsert(ids, np.asarray(vectors, dtype=np.float32)) + copied += len(ids) + repaired.mark_rebuild_complete() + return {"measured": True, "kind": "native_mirror_replay_from_canonical_vectors", + "records_replayed": copied, "seconds": round(time.perf_counter() - started, 6)} + + +def run_file_backed(sizes: list[int], *, dim: int = 256, queries: int = 20, + iterations: int = 3, warmups: int = 1, k: int = 10, + seed: int = 20260731, backend: str = "numpy", + concurrencies: Optional[list[int]] = None, mixed_writes: int = 4, + batch_size: int = 500, tenants: int = 4, + progress: Optional[Callable[[dict], None]] = None, + checkpoint: Optional[Callable[[dict], None]] = None) -> dict: + """Return redacted reproducible evidence; large cells run only when requested.""" + sizes = parse_sizes(",".join(str(value) for value in sizes)) + concurrencies = [1, 4, 16] if concurrencies is None else list(concurrencies) + if not concurrencies or len(set(concurrencies)) != len(concurrencies) or any( + value not in (1, 4, 16) for value in concurrencies + ): + raise ValueError("concurrencies must be distinct choices from 1, 4, 16") + if backend not in BACKENDS: + raise ValueError("backend must be numpy or sqlite-vec") + if min(dim, queries, iterations, k, batch_size, tenants) < 1 or min(warmups, mixed_writes) < 0: + raise ValueError("dimensions/counts must be positive; warmups and mixed_writes nonnegative") + source_before = _source_snapshot() + inputs, cells, storage = [], [], [] + query_vectors = _normalized_random(np.random.default_rng(seed + 1), queries, dim) + query_hash = hashlib.sha256(query_vectors.tobytes()).hexdigest() + backend_class = "" + config = {"sizes": sizes, "dimension": dim, "queries": queries, "iterations": iterations, + "warmups": warmups, "k": k, "seed": seed, "backend": backend, + "concurrencies": concurrencies, "mixed_writes_per_cell": mixed_writes, + "batch_size": batch_size, "tenant_scopes": tenants, "file_backed": True, + "vector_generator": _GENERATOR, "inputs": inputs, "queries_sha256": query_hash} + + def checkpoint_phase(phase: str, size: int, **details) -> None: + if checkpoint: + checkpoint({"schema": "engraphis-scale-checkpoint/v1", "status": "incomplete", + "phase": phase, "corpus_size": size, "config": config, + "source_before": source_before, "backend_class": backend_class, + "completed_cells": cells, "completed_storage": storage, **details}) + if progress: + progress({"backend": backend, "corpus_size": size, "stage": phase, + **({"concurrency": details["concurrency"]} if "concurrency" in details else {})}) + + for size in sizes: + checkpoint_phase("population", size) + # TemporaryDirectory owns exactly this newly created directory; no caller + # path is recursively removed, and every Store closes before cleanup. + with tempfile.TemporaryDirectory(prefix="egr-scale-") as folder: + path = Path(folder) / "corpus.db" + started = time.perf_counter() + store = Store(str(path)) + try: + index = get_vector_index(store, dim=dim, prefer=backend) + backend_class = type(index).__name__ + initial_startup_ms = (time.perf_counter() - started) * 1000 + workspace_id = store.get_or_create_workspace("vector-scale") + repo_ids = [store.get_or_create_repo(workspace_id, f"scope-{number}") + for number in range(tenants)] + rng = np.random.default_rng(seed) + digest = hashlib.sha256() + began = time.perf_counter() + batch_latencies = [] + for offset in range(0, size, batch_size): + stop = min(size, offset + batch_size) + vectors = _normalized_random(rng, stop - offset, dim) + digest.update(vectors.tobytes()) + batch_started = time.perf_counter() + _insert(store, index, range(offset, stop), vectors, workspace_id, repo_ids) + batch_latencies.append((time.perf_counter() - batch_started) * 1000) + if progress and (stop == size or stop % 10_000 == 0): + progress({"backend": backend, "corpus_size": size, + "records_written": stop, "stage": "population"}) + if hasattr(index, "mark_rebuild_complete"): + checkpoint_phase("native_publication", size) + index.mark_rebuild_complete() + population_seconds = time.perf_counter() - began + inputs.append({"corpus_size": size, "vectors_sha256": digest.hexdigest()}) + populated_disk = _disk(path) + store.close() + checkpoint_phase("restart", size, population_seconds=population_seconds) + began = time.perf_counter() + store = Store(str(path)) + index = get_vector_index(store, dim=dim, prefer=backend) + restart_ms = (time.perf_counter() - began) * 1000 + if getattr(index, "requires_rebuild", False): + raise RuntimeError("completed native mirror was not ready after restart") + flt = SearchFilter(workspace_id=workspace_id, repo_id=repo_ids[0]) + cold = _reads(index, query_vectors, k, flt, 1, 1) + # Reference parity is measured outside the latency cells. This is + # NumPy/native agreement, not an independent retrieval-quality eval. + reference = NumpyVectorIndex(store, dim=dim) + reference_results = [reference.search(query, k, filter=flt) + for query in query_vectors] + expected_hash = _result_hash(reference_results * iterations) + expected_count = min(k, (size + tenants - 1) // tenants) + for concurrency in concurrencies: + if warmups: + _reads(index, query_vectors, k, flt, concurrency, warmups) + measured = _reads(index, query_vectors, k, flt, concurrency, iterations) + parity = measured["result_ids_sha256"] == expected_hash + if not parity or measured["result_counts"] != [expected_count]: + raise RuntimeError("exact-index results differ from the NumPy reference") + cells.append({"corpus_size": size, "concurrency": concurrency, + "status": "complete", "numpy_reference_parity": parity, + **measured}) + checkpoint_phase("reads_complete", size, concurrency=concurrency) + mixed = [] + for position, concurrency in enumerate(concurrencies): + mixed.append(_mixed(store, index, query_vectors, k, flt, concurrency, + mixed_writes, size + position * mixed_writes, + workspace_id, repo_ids, rng)) + checkpoint_phase("mixed_cell_complete", size, current_mixed=mixed) + checkpoint_phase("native_rebuild", size, current_mixed=mixed) + rebuilt = _native_rebuild(store, index, dim, batch_size) + checkpoint_phase("mixed_and_rebuild_complete", size, + current_mixed=mixed, rebuild=rebuilt) + expected_rows = size + len(concurrencies) * mixed_writes + store.close() + store = Store(str(path)) + reopened = get_vector_index(store, dim=dim, prefer=backend) + actual_rows = int(store.conn.execute("SELECT COUNT(*) FROM memories").fetchone()[0]) + vector_rows = int(store.conn.execute("SELECT COUNT(*) FROM mem_vectors").fetchone()[0]) + if (actual_rows != expected_rows or vector_rows != expected_rows + or getattr(reopened, "requires_rebuild", False)): + raise RuntimeError("mixed writes or native rebuild did not survive restart") + storage.append({"corpus_size": size, "initial_startup_ms": initial_startup_ms, + "restart_ms": restart_ms, "population_seconds": population_seconds, + "population_records_per_second": size / max(population_seconds, 1e-9), + "population_batch_latency_ms": _latency_ms(batch_latencies), + "connection_cold_reads": cold, "mixed": mixed, "rebuild": rebuilt, + "durable_memory_rows": actual_rows, "durable_vector_rows": vector_rows, + "populated_disk": populated_disk, "final_disk": _disk(path), + "memory": _memory()}) + checkpoint_phase("corpus_complete", size) + finally: + store.close() + source_after = _source_snapshot() + try: + native_version = importlib.metadata.version("sqlite-vec") if backend == "sqlite-vec" else None + except importlib.metadata.PackageNotFoundError: + native_version = "unavailable" + return report_envelope( + suite="file-backed-exact-index-scale/v1", dataset_path=Path(__file__), config=config, + records=[{"question_id": f"n{cell['corpus_size']}-c{cell['concurrency']}", + "category": "exact_index_throughput", "latency_ms": cell["latency_ms"]["p50"]} + for cell in cells], + metrics={"cells": cells, "storage": storage, "hardware": _hardware(), + "source_before": source_before, "source_after": source_after, + "source_stable": source_before == source_after, + "measurement_scope": "synthetic scoped exact-index and storage operations; not end-to-end recall", + "timing_scope": "latency is execution including storage locks, excludes executor queue; throughput includes queue", + "cold_scope": "new connection after population; operating-system disk cache is not flushed", + "unmeasured": ["embedding/model latency", "extraction and conflict resolution", + "agent task quality", "full recall and context packing", + "multi-process contention", "independent repeated processes"], + "percentile_scope": "descriptive samples, not tail-SLO confidence bounds"}, + source_paths=[_ROOT / name for name in _SOURCES if (_ROOT / name).is_file()], + models={"embedding": {"identity": "none; precomputed synthetic vectors"}, + "vector_backend": {"identity": backend_class, "native_version": native_version}, + "tokenizer": {"identity": "not applicable; no reader context"}}, + token_accounting={"identity": "not_applicable", "revision": None, + "scope": "no reader context", "method": "not_measured"}, + command=["python", "-m", "eval.vector_scale", "--file-backed", "--backend", backend, + "--sizes", ",".join(map(str, sizes)), "--dim", str(dim), + "--queries", str(queries), "--iterations", str(iterations), + "--warmups", str(warmups), "--k", str(k), "--seed", str(seed), + "--concurrencies", ",".join(map(str, concurrencies)), + "--mixed-writes", str(mixed_writes), "--batch-size", str(batch_size), + "--tenants", str(tenants), *(["--progress"] if progress else [])], + ) diff --git a/eval/vector_scan_plan.py b/eval/vector_scan_plan.py new file mode 100644 index 00000000..7510e157 --- /dev/null +++ b/eval/vector_scan_plan.py @@ -0,0 +1,141 @@ +"""Compare scoped keyset scan query plans without changing ranking or locking.""" +from __future__ import annotations + +import argparse +import hashlib +from pathlib import Path +import tempfile +import time + +import numpy as np + +from engraphis.backends.vector_numpy import NumpyVectorIndex +from engraphis.core.interfaces import SearchFilter +from engraphis.core.store import Store, VECTOR_SCAN_BATCH +from eval.benchmark import report_envelope, write_canonical_artifact +from eval.vector_scale import _normalized_random, parse_sizes +from eval.vector_scale_storage import _ROOT, _SOURCES, _hardware, _insert, _source_snapshot + + +def _digest_batch(digest, mids, matrix): + for mid, vector in zip(mids, matrix): + digest.update(mid.encode()) + digest.update(vector.tobytes()) + + +def scan(store, flt, dim, *, vector_first, sorted_first=False): + where, params = store._where(flt, False, alias="m") + where.extend(("v.dim=?", "length(v.vector)=?", "v.id>?")) + params.extend((dim, dim * 4)) + join = "CROSS JOIN" if vector_first else "JOIN" + sql = (f"SELECT v.id,v.vector FROM mem_vectors v {join} memories m ON m.id=v.id WHERE " + + " AND ".join(where) + " ORDER BY v.id LIMIT ?") + plan = [str(row[3]) for row in store.conn.execute( + "EXPLAIN QUERY PLAN " + sql, (*params, "", VECTOR_SCAN_BATCH), + ).fetchall()] + after_id, count, digest = "", 0, hashlib.sha256() + started = time.perf_counter() + with store.read_snapshot(): + while True: + selected_sql = sql.replace("CROSS JOIN", "JOIN") if sorted_first and not after_id else sql + rows = store.conn.fetchall(selected_sql, (*params, after_id, VECTOR_SCAN_BATCH)) + mids = [str(row["id"]) for row in rows] + payload = b"".join(row["vector"] for row in rows) + matrix = np.frombuffer(payload, dtype=np.float32).reshape(len(mids), dim) + _digest_batch(digest, mids, matrix) + count += len(rows) + if len(rows) < VECTOR_SCAN_BATCH: + break + after_id = str(rows[-1]["id"]) + return {"elapsed_seconds": time.perf_counter() - started, + "rows": count, "result_sha256": digest.hexdigest(), "query_plan": plan} + + +def adaptive_scan(store, flt, dim): + count, digest = 0, hashlib.sha256() + started = time.perf_counter() + for mids, matrix in store.iter_vector_matrices(flt, dim=dim): + _digest_batch(digest, mids, matrix) + count += len(mids) + return {"elapsed_seconds": time.perf_counter() - started, + "rows": count, "result_sha256": digest.hexdigest(), + "query_plan": ["production first scoped sorted batch; later vector-primary-key batches"]} + + +def run_comparison(sizes, *, dim=256, batch_size=500, seed=20260731): + sizes = parse_sizes(",".join(map(str, sizes))) + if min(dim, batch_size) < 1: + raise ValueError("dimension and batch_size must be positive") + before, cells = _source_snapshot(), [] + for size in sizes: + with tempfile.TemporaryDirectory(prefix="egr-plan-") as folder: + store = Store(str(Path(folder) / "corpus.db")) + try: + index = NumpyVectorIndex(store, dim=dim) + workspace_id = store.get_or_create_workspace("scan-plan") + scope_ids = [store.get_or_create_repo(workspace_id, f"scope-{i}") for i in range(6)] + # One corpus supports narrow, moderate and broad filter probes. + repo_ids = ([scope_ids[0]] + [scope_ids[1]] * 10 + [scope_ids[2]] * 21 + + [scope_ids[3]] * 50 + [scope_ids[4]] * 250 + [scope_ids[5]] * 668) + rng = np.random.default_rng(seed) + for offset in range(0, size, batch_size): + stop = min(size, offset + batch_size) + vectors = _normalized_random(rng, stop - offset, dim) + _insert(store, index, range(offset, stop), vectors, workspace_id, repo_ids) + for percent, repo_id in ((0.1, scope_ids[0]), (1, scope_ids[1]), + (2.1, scope_ids[2]), (5, scope_ids[3]), + (25, scope_ids[4]), (100, None)): + flt = SearchFilter(workspace_id=workspace_id, repo_id=repo_id) + pair = [] + for strategy, vector_first in (("planner_selected_join", False), + ("vector_primary_key_first", True)): + result = scan(store, flt, dim, vector_first=vector_first) + pair.append(result) + cells.append({"strategy": strategy, "corpus_size": size, + "target_scope_percent": percent, **result}) + result = adaptive_scan(store, flt, dim) + pair.append(result) + cells.append({"strategy": "adaptive_snapshot", "corpus_size": size, + "target_scope_percent": percent, **result}) + result = scan(store, flt, dim, vector_first=True, sorted_first=True) + pair.append(result) + cells.append({"strategy": "scoped_first_sorted_batch", "corpus_size": size, + "target_scope_percent": percent, **result}) + if len({entry["result_sha256"] for entry in pair}) != 1: + raise RuntimeError("join-order optimization changed filtered vectors") + finally: + store.close() + after = _source_snapshot() + return report_envelope( + suite="vector-scan-plan/counterfactual-v1", dataset_path=Path(__file__), + config={"sizes": sizes, "dimension": dim, "batch_size": batch_size, "seed": seed, + "target_scope_percentages": [0.1, 1, 2.1, 5, 25, 100]}, + records=[{"question_id": f"{cell['strategy']}-{cell['corpus_size']}-{cell['target_scope_percent']}", + "category": "scoped_vector_scan"} for cell in cells], + metrics={"cells": cells, "hardware": _hardware(), "source_before": before, + "source_after": after, "source_stable": before == after, + "measurement_scope": "same scoped bounded vector scan; only join order differs", + "unmeasured": ["historical release behavior", "independent process repetitions", + "external contention", "end-to-end recall"]}, + source_paths=[_ROOT / name for name in _SOURCES] + [Path(__file__)], + command=["python", "-m", "eval.vector_scan_plan", "--sizes", ",".join(map(str, sizes)), + "--dim", str(dim), "--batch-size", str(batch_size), "--seed", str(seed)], + ) + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--sizes", default="1000,10000,100000") + parser.add_argument("--dim", type=int, default=256) + parser.add_argument("--batch-size", type=int, default=500) + parser.add_argument("--seed", type=int, default=20260731) + parser.add_argument("--output", required=True) + args = parser.parse_args(argv) + report = run_comparison(parse_sizes(args.sizes), dim=args.dim, + batch_size=args.batch_size, seed=args.seed) + print(write_canonical_artifact(report, args.output)) + return 0 if report["metrics"]["source_stable"] else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/integrations/pi/index.ts b/integrations/pi/index.ts index e47dd9ed..d77eee11 100644 --- a/integrations/pi/index.ts +++ b/integrations/pi/index.ts @@ -194,6 +194,9 @@ export default function engraphisPiExtension(pi: ExtensionAPI) { executionMode: "sequential", parameters: EXECUTE_ACTION_PARAMETERS, execute: async (_toolCallId, params, signal, _onUpdate, ctx) => { + if (typeof params.capability_id !== "string" || typeof params.schema_digest !== "string") { + throw new Error("Rediscover the action before executing it."); + } const key = actionKey(params.capability_id, params.schema_digest); const action = discoveredActions.get(key); if (!action) { diff --git a/integrations/pi/src/generated-contract.ts b/integrations/pi/src/generated-contract.ts new file mode 100644 index 00000000..03fe5443 --- /dev/null +++ b/integrations/pi/src/generated-contract.ts @@ -0,0 +1,517 @@ +// Generated by scripts/export_mcp_contract.py. Do not edit. +export const SMART_SCHEMAS = { + "engraphis_conflict_review": { + "properties": { + "limit": { + "default": 50, + "description": "Max items to return.", + "maximum": 100, + "minimum": 1, + "title": "Limit", + "type": "integer" + }, + "repo": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional repository scope.", + "title": "Repo" + }, + "workspace": { + "default": "default", + "description": "Workspace to review.", + "maxLength": 200, + "title": "Workspace", + "type": "string" + } + }, + "title": "engraphis_conflict_reviewArguments", + "type": "object" + }, + "engraphis_discover_actions": { + "properties": { + "category": { + "default": "", + "description": "Optional area: memory, governance, code, audit, or ops.", + "maxLength": 100, + "title": "Category", + "type": "string" + }, + "intent": { + "default": "any", + "description": "Optional side effect: any, read, write, admin, or destructive.", + "pattern": "^(any|read|write|admin|destructive)$", + "title": "Intent", + "type": "string" + }, + "limit": { + "default": 1, + "description": "Number of ranked actions to return.", + "maximum": 3, + "minimum": 1, + "title": "Limit", + "type": "integer" + }, + "task": { + "description": "Describe the capability needed, without pasting memory content.", + "maxLength": 2000, + "minLength": 1, + "title": "Task", + "type": "string" + } + }, + "required": [ + "task" + ], + "title": "engraphis_discover_actionsArguments", + "type": "object" + }, + "engraphis_execute_action": { + "properties": { + "arguments": { + "additionalProperties": true, + "description": "Arguments matching the discovered schema.", + "title": "Arguments", + "type": "object" + }, + "capability_id": { + "description": "Capability id returned by discover_actions.", + "maxLength": 128, + "minLength": 8, + "title": "Capability Id", + "type": "string" + }, + "schema_digest": { + "description": "Schema digest returned by discovery.", + "maxLength": 128, + "minLength": 8, + "title": "Schema Digest", + "type": "string" + } + }, + "required": [ + "capability_id", + "schema_digest", + "arguments" + ], + "title": "engraphis_execute_actionArguments", + "type": "object" + }, + "engraphis_execute_read": { + "properties": { + "arguments": { + "additionalProperties": true, + "description": "Arguments matching the discovered schema.", + "title": "Arguments", + "type": "object" + }, + "capability_id": { + "description": "Capability id returned by discover_actions.", + "maxLength": 128, + "minLength": 8, + "title": "Capability Id", + "type": "string" + }, + "schema_digest": { + "description": "Schema digest returned by discovery.", + "maxLength": 128, + "minLength": 8, + "title": "Schema Digest", + "type": "string" + } + }, + "required": [ + "capability_id", + "schema_digest", + "arguments" + ], + "title": "engraphis_execute_readArguments", + "type": "object" + }, + "engraphis_get_memory": { + "properties": { + "memory_id": { + "description": "Memory id to read.", + "maxLength": 200, + "minLength": 1, + "title": "Memory Id", + "type": "string" + }, + "repo": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional repository scope.", + "title": "Repo" + }, + "workspace": { + "default": "default", + "description": "Workspace containing the memory.", + "maxLength": 200, + "title": "Workspace", + "type": "string" + } + }, + "required": [ + "memory_id" + ], + "title": "engraphis_get_memoryArguments", + "type": "object" + }, + "engraphis_recall_context": { + "properties": { + "format": { + "default": "full", + "description": "Context format: 'full' or 'gist'.", + "title": "Format", + "type": "string" + }, + "k": { + "default": 50, + "description": "Maximum source memories.", + "maximum": 50, + "minimum": 1, + "title": "K", + "type": "integer" + }, + "query": { + "description": "Question or task needing prior context.", + "maxLength": 100000, + "minLength": 1, + "title": "Query", + "type": "string" + }, + "repo": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional repository.", + "title": "Repo" + }, + "session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional active session.", + "title": "Session Id" + }, + "token_budget": { + "default": 1024, + "description": "Hard returned-context token budget.", + "maximum": 32768, + "minimum": 0, + "title": "Token Budget", + "type": "integer" + }, + "workspace": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional workspace.", + "title": "Workspace" + } + }, + "required": [ + "query" + ], + "title": "smart_recall_contextArguments", + "type": "object" + }, + "engraphis_remember": { + "properties": { + "claim_kind": { + "default": "", + "description": "Optional claim predicate/category (for example 'configured_value').", + "maxLength": 200, + "title": "Claim Kind", + "type": "string" + }, + "content": { + "description": "Durable fact, decision, preference, or procedure.", + "maxLength": 100000, + "minLength": 1, + "title": "Content", + "type": "string" + }, + "importance": { + "default": 0.0, + "description": "Salience from 0 to 1.", + "maximum": 1.0, + "minimum": 0.0, + "title": "Importance", + "type": "number" + }, + "mtype": { + "default": "semantic", + "description": "semantic, episodic, procedural, or working.", + "title": "Mtype", + "type": "string" + }, + "repo": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional repository.", + "title": "Repo" + }, + "session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional active session.", + "title": "Session Id" + }, + "subject_key": { + "default": "", + "description": "Optional stable claim subject (for example 'api.rate_limit'). Matching keys make supersession safer and deterministic.", + "maxLength": 1000, + "title": "Subject Key", + "type": "string" + }, + "workspace": { + "default": "default", + "description": "Workspace for the memory.", + "maxLength": 200, + "title": "Workspace", + "type": "string" + } + }, + "required": [ + "content" + ], + "title": "smart_rememberArguments", + "type": "object" + }, + "engraphis_session": { + "properties": { + "action": { + "default": "start", + "description": "start to resume work, or end to save its handoff.", + "title": "Action", + "type": "string" + }, + "agent": { + "default": "", + "description": "Optional agent name.", + "maxLength": 200, + "title": "Agent", + "type": "string" + }, + "force_new": { + "default": false, + "description": "Start only: branch a new session instead of reusing an exact active task.", + "title": "Force New", + "type": "boolean" + }, + "goal": { + "default": "", + "description": "Task goal; start returns bounded relevant context.", + "maxLength": 1000, + "title": "Goal", + "type": "string" + }, + "open_threads": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Unresolved follow-ups.", + "title": "Open Threads" + }, + "outcome": { + "default": "", + "description": "Optional outcome label.", + "maxLength": 1000, + "title": "Outcome", + "type": "string" + }, + "repo": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional repository scope.", + "title": "Repo" + }, + "session_id": { + "default": "", + "description": "Session id required to end a session.", + "maxLength": 200, + "title": "Session Id", + "type": "string" + }, + "summary": { + "default": "", + "description": "Short final handoff.", + "maxLength": 100000, + "title": "Summary", + "type": "string" + }, + "token_budget": { + "default": 512, + "description": "Goal-context budget when starting.", + "maximum": 32768, + "minimum": 0, + "title": "Token Budget", + "type": "integer" + }, + "workspace": { + "default": "default", + "description": "Workspace for a started session.", + "maxLength": 200, + "title": "Workspace", + "type": "string" + } + }, + "title": "engraphis_sessionArguments", + "type": "object" + }, + "engraphis_update_memory": { + "properties": { + "actor": { + "default": "user", + "description": "Optional local-mode actor label; authenticated team mode uses the caller identity.", + "maxLength": 200, + "title": "Actor", + "type": "string" + }, + "importance": { + "anyOf": [ + { + "maximum": 1.0, + "minimum": 0.0, + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional importance 0..1.", + "title": "Importance" + }, + "memory_id": { + "description": "Memory id to update.", + "maxLength": 200, + "minLength": 1, + "title": "Memory Id", + "type": "string" + }, + "mtype": { + "anyOf": [ + { + "maxLength": 50, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional memory type (working|episodic|semantic|procedural).", + "title": "Mtype" + }, + "repo": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional repository scope.", + "title": "Repo" + }, + "title": { + "anyOf": [ + { + "maxLength": 500, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional new title.", + "title": "Title" + }, + "workspace": { + "default": "default", + "description": "Workspace containing the memory.", + "maxLength": 200, + "title": "Workspace", + "type": "string" + } + }, + "required": [ + "memory_id" + ], + "title": "engraphis_update_memoryArguments", + "type": "object" + } +} as const; diff --git a/integrations/pi/src/tool-schemas.ts b/integrations/pi/src/tool-schemas.ts index 244a9cc5..38f32669 100644 --- a/integrations/pi/src/tool-schemas.ts +++ b/integrations/pi/src/tool-schemas.ts @@ -2,130 +2,17 @@ import { Type } from "typebox"; import type { EngraphisRuntimeConfig } from "./config.ts"; -const OPTIONAL_REPO = Type.Optional(Type.Union([ - Type.String({ description: "Repository scope within the workspace.", maxLength: 200 }), - Type.Null(), -], { default: null })); - -const WRITABLE_SCOPE = { - repo: OPTIONAL_REPO, - workspace: Type.Optional(Type.String({ default: "default", description: "Top-level memory workspace.", maxLength: 200 })), -}; - -const RECALL_SCOPE = { - repo: OPTIONAL_REPO, - workspace: Type.Optional(Type.Union([ - Type.String({ description: "Optional workspace; omit for local cross-workspace recall.", maxLength: 200 }), - Type.Null(), - ], { default: null })), -}; - -/** The Smart session tool starts/resumes and ends sessions with one stable schema. */ -export const SESSION_PARAMETERS = Type.Object({ - ...WRITABLE_SCOPE, - action: Type.Optional(Type.Union([ - Type.Literal("start"), - Type.Literal("end"), - ], { default: "start", description: "Start/resume work or save its handoff." })), - agent: Type.Optional(Type.String({ default: "pi", description: "Agent label. Defaults to pi.", maxLength: 200 })), - force_new: Type.Optional(Type.Boolean({ default: false, description: "Start a new session instead of reusing an exact active session." })), - goal: Type.Optional(Type.String({ default: "", description: "What this session is trying to accomplish.", maxLength: 1_000 })), - session_id: Type.Optional(Type.String({ default: "", description: "Session id required when action is end.", maxLength: 200 })), - summary: Type.Optional(Type.String({ default: "", description: "Concise handoff for the next session.", maxLength: 100_000 })), - outcome: Type.Optional(Type.String({ default: "", description: "Short outcome, such as shipped or blocked.", maxLength: 1_000 })), - open_threads: Type.Optional(Type.Union([ - Type.Array(Type.String({ description: "Unresolved item to carry forward." })), - Type.Null(), - ], { default: null })), - token_budget: Type.Optional(Type.Integer({ default: 512, description: "Goal-context token budget (0-32768).", minimum: 0, maximum: 32768 })), -}); - -export const RECALL_CONTEXT_PARAMETERS = Type.Object({ - ...RECALL_SCOPE, - k: Type.Optional(Type.Integer({ default: 8, description: "Candidate-memory limit (1-50).", minimum: 1, maximum: 50 })), - query: Type.String({ description: "The prior context needed for the current task.", minLength: 1, maxLength: 100_000 }), - session_id: Type.Optional(Type.Union([ - Type.String({ description: "Active Engraphis session id, if known." }), - Type.Null(), - ], { default: null })), - token_budget: Type.Optional(Type.Integer({ default: 1_024, description: "Maximum packed-context tokens (0-32768).", minimum: 0, maximum: 32768 })), -}); - -export const REMEMBER_PARAMETERS = Type.Object({ - ...WRITABLE_SCOPE, - content: Type.String({ description: "Durable fact, decision, preference, bug cause/fix, or reusable procedure.", minLength: 1, maxLength: 100_000 }), - importance: Type.Optional(Type.Number({ default: 0, description: "Salience from 0 to 1.", minimum: 0, maximum: 1 })), - mtype: Type.Optional(Type.Union([ - Type.Literal("semantic"), - Type.Literal("episodic"), - Type.Literal("procedural"), - Type.Literal("working"), - ], { default: "semantic" })), - session_id: Type.Optional(Type.Union([ - Type.String({ description: "Active Engraphis session id, if known." }), - Type.Null(), - ], { default: null })), -}); - -export const GET_MEMORY_PARAMETERS = Type.Object({ - ...WRITABLE_SCOPE, - memory_id: Type.String({ description: "Memory id to read.", minLength: 1, maxLength: 200 }), -}); - -export const UPDATE_MEMORY_PARAMETERS = Type.Object({ - ...WRITABLE_SCOPE, - memory_id: Type.String({ description: "Memory id to edit.", minLength: 1, maxLength: 200 }), - title: Type.Optional(Type.Union([ - Type.String({ description: "Replacement title.", maxLength: 500 }), - Type.Null(), - ], { default: null })), - mtype: Type.Optional(Type.Union([ - Type.Literal("semantic"), - Type.Literal("episodic"), - Type.Literal("procedural"), - Type.Literal("working"), - Type.Null(), - ], { default: null })), - importance: Type.Optional(Type.Union([ - Type.Number({ description: "Replacement salience from 0 to 1.", minimum: 0, maximum: 1 }), - Type.Null(), - ], { default: null })), - actor: Type.Optional(Type.String({ default: "user", description: "Audit actor label.", maxLength: 200 })), -}); - -export const CONFLICT_REVIEW_PARAMETERS = Type.Object({ - ...WRITABLE_SCOPE, - limit: Type.Optional(Type.Integer({ default: 50, description: "Maximum review items (1-100).", minimum: 1, maximum: 100 })), -}); - -export const DISCOVER_ACTIONS_PARAMETERS = Type.Object({ - task: Type.String({ description: "Describe the advanced capability needed without pasting memory content.", minLength: 1, maxLength: 2_000 }), - category: Type.Optional(Type.Union([ - Type.Literal("memory"), - Type.Literal("governance"), - Type.Literal("code"), - Type.Literal("audit"), - Type.Literal("ops"), - ], { default: "", maxLength: 100 })), - intent: Type.Optional(Type.Union([ - Type.Literal("any"), - Type.Literal("read"), - Type.Literal("write"), - Type.Literal("admin"), - Type.Literal("destructive"), - ], { default: "any" })), - limit: Type.Optional(Type.Integer({ default: 1, description: "Number of matching actions (1-3).", minimum: 1, maximum: 3 })), -}); - -const EXECUTE_PARAMETERS = { - capability_id: Type.String({ description: "Capability id returned by engraphis_discover_actions.", minLength: 8, maxLength: 128 }), - schema_digest: Type.String({ description: "Schema digest returned by engraphis_discover_actions.", minLength: 8, maxLength: 128 }), - arguments: Type.Record(Type.String(), Type.Unknown({ description: "Arguments matching the discovered action schema." })), -}; - -export const EXECUTE_READ_PARAMETERS = Type.Object(EXECUTE_PARAMETERS); - -export const EXECUTE_ACTION_PARAMETERS = Type.Object(EXECUTE_PARAMETERS); +import { SMART_SCHEMAS } from "./generated-contract.ts"; + +export const SESSION_PARAMETERS = Type.Unsafe>(SMART_SCHEMAS.engraphis_session); +export const RECALL_CONTEXT_PARAMETERS = Type.Unsafe>(SMART_SCHEMAS.engraphis_recall_context); +export const REMEMBER_PARAMETERS = Type.Unsafe>(SMART_SCHEMAS.engraphis_remember); +export const GET_MEMORY_PARAMETERS = Type.Unsafe>(SMART_SCHEMAS.engraphis_get_memory); +export const UPDATE_MEMORY_PARAMETERS = Type.Unsafe>(SMART_SCHEMAS.engraphis_update_memory); +export const CONFLICT_REVIEW_PARAMETERS = Type.Unsafe>(SMART_SCHEMAS.engraphis_conflict_review); +export const DISCOVER_ACTIONS_PARAMETERS = Type.Unsafe>(SMART_SCHEMAS.engraphis_discover_actions); +export const EXECUTE_READ_PARAMETERS = Type.Unsafe>(SMART_SCHEMAS.engraphis_execute_read); +export const EXECUTE_ACTION_PARAMETERS = Type.Unsafe>(SMART_SCHEMAS.engraphis_execute_action); /** Add explicit configured defaults without overriding a model-supplied scope. */ export function applyScopeDefaults( diff --git a/integrations/pi/test/mcp-client.integration.ts b/integrations/pi/test/mcp-client.integration.ts index fcda66f1..0588c9da 100644 --- a/integrations/pi/test/mcp-client.integration.ts +++ b/integrations/pi/test/mcp-client.integration.ts @@ -13,7 +13,7 @@ const PROJECT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", ".." test("discovers and calls the installed Engraphis MCP server", { timeout: 30_000 }, async () => { const database = join(tmpdir(), `engraphis-pi-${randomUUID()}.db`); const publicCommand = process.env.ENGRAPHIS_PI_TEST_COMMAND; - const client = new EngraphisMcpClient({ + const config = { // Exercise the same public console entry that a published Pi package launches. // Release/CI sets the override after installing this checkout. Local development // uses the checkout module so an older globally installed console script cannot @@ -25,8 +25,10 @@ test("discovers and calls the installed Engraphis MCP server", { timeout: 30_000 ENGRAPHIS_DB_PATH: database, // Keep CI deterministic and avoid downloading/loading the optional embedding model. ENGRAPHIS_EMBED_MODEL: "", + ENGRAPHIS_EXTRACTOR: "none", }, - }); + }; + const client = new EngraphisMcpClient(config); try { const status = await client.status(); @@ -71,6 +73,26 @@ test("discovers and calls the installed Engraphis MCP server", { timeout: 30_000 }); assert.equal(result.isError, false); assert.match(result.content?.[0]?.text ?? "", /"memories"/); + + const saved = await client.callTool("engraphis_remember", { + workspace: "default", content: "The Atlas deployment target is staging.", + subject_key: "atlas.deployment", claim_kind: "deployment_target", + }); + assert.equal(saved.isError, false); + await client.close(); + const reconnected = new EngraphisMcpClient(config); + try { + const recalled = await reconnected.callTool("engraphis_recall_context", { + workspace: "default", query: "Atlas deployment target", format: "gist", + token_budget: 512, + }); + assert.equal(recalled.isError, false); + const packet = JSON.parse(recalled.content?.[0]?.text ?? "{}"); + assert.match(packet.context, /staging/); + assert.ok(packet.sources.length > 0, "restart recall must retain source provenance"); + } finally { + await reconnected.close(); + } } finally { await client.close(); await Promise.all([database, `${database}-wal`, `${database}-shm`].map((path) => rm(path, { force: true }))); diff --git a/integrations/prime_agent/src/engraphis_prime_agent/_contract.py b/integrations/prime_agent/src/engraphis_prime_agent/_contract.py new file mode 100644 index 00000000..2dfecc84 --- /dev/null +++ b/integrations/prime_agent/src/engraphis_prime_agent/_contract.py @@ -0,0 +1,322 @@ +"""Generated by scripts/export_mcp_contract.py. Do not edit.""" +SMART_SCHEMAS = {'engraphis_conflict_review': {'properties': {'limit': {'default': 50, + 'description': 'Max items to return.', + 'maximum': 100, + 'minimum': 1, + 'title': 'Limit', + 'type': 'integer'}, + 'repo': {'anyOf': [{'maxLength': 200, + 'type': 'string'}, + {'type': 'null'}], + 'default': None, + 'description': 'Optional repository scope.', + 'title': 'Repo'}, + 'workspace': {'default': 'default', + 'description': 'Workspace to review.', + 'maxLength': 200, + 'title': 'Workspace', + 'type': 'string'}}, + 'title': 'engraphis_conflict_reviewArguments', + 'type': 'object'}, + 'engraphis_discover_actions': {'properties': {'category': {'default': '', + 'description': 'Optional area: memory, ' + 'governance, code, ' + 'audit, or ops.', + 'maxLength': 100, + 'title': 'Category', + 'type': 'string'}, + 'intent': {'default': 'any', + 'description': 'Optional side effect: ' + 'any, read, write, admin, ' + 'or destructive.', + 'pattern': '^(any|read|write|admin|destructive)$', + 'title': 'Intent', + 'type': 'string'}, + 'limit': {'default': 1, + 'description': 'Number of ranked actions ' + 'to return.', + 'maximum': 3, + 'minimum': 1, + 'title': 'Limit', + 'type': 'integer'}, + 'task': {'description': 'Describe the capability ' + 'needed, without pasting ' + 'memory content.', + 'maxLength': 2000, + 'minLength': 1, + 'title': 'Task', + 'type': 'string'}}, + 'required': ['task'], + 'title': 'engraphis_discover_actionsArguments', + 'type': 'object'}, + 'engraphis_execute_action': {'properties': {'arguments': {'additionalProperties': True, + 'description': 'Arguments matching the ' + 'discovered schema.', + 'title': 'Arguments', + 'type': 'object'}, + 'capability_id': {'description': 'Capability id ' + 'returned by ' + 'discover_actions.', + 'maxLength': 128, + 'minLength': 8, + 'title': 'Capability Id', + 'type': 'string'}, + 'schema_digest': {'description': 'Schema digest ' + 'returned by ' + 'discovery.', + 'maxLength': 128, + 'minLength': 8, + 'title': 'Schema Digest', + 'type': 'string'}}, + 'required': ['capability_id', 'schema_digest', 'arguments'], + 'title': 'engraphis_execute_actionArguments', + 'type': 'object'}, + 'engraphis_execute_read': {'properties': {'arguments': {'additionalProperties': True, + 'description': 'Arguments matching the ' + 'discovered schema.', + 'title': 'Arguments', + 'type': 'object'}, + 'capability_id': {'description': 'Capability id ' + 'returned by ' + 'discover_actions.', + 'maxLength': 128, + 'minLength': 8, + 'title': 'Capability Id', + 'type': 'string'}, + 'schema_digest': {'description': 'Schema digest ' + 'returned by ' + 'discovery.', + 'maxLength': 128, + 'minLength': 8, + 'title': 'Schema Digest', + 'type': 'string'}}, + 'required': ['capability_id', 'schema_digest', 'arguments'], + 'title': 'engraphis_execute_readArguments', + 'type': 'object'}, + 'engraphis_get_memory': {'properties': {'memory_id': {'description': 'Memory id to read.', + 'maxLength': 200, + 'minLength': 1, + 'title': 'Memory Id', + 'type': 'string'}, + 'repo': {'anyOf': [{'maxLength': 200, 'type': 'string'}, + {'type': 'null'}], + 'default': None, + 'description': 'Optional repository scope.', + 'title': 'Repo'}, + 'workspace': {'default': 'default', + 'description': 'Workspace containing the ' + 'memory.', + 'maxLength': 200, + 'title': 'Workspace', + 'type': 'string'}}, + 'required': ['memory_id'], + 'title': 'engraphis_get_memoryArguments', + 'type': 'object'}, + 'engraphis_recall_context': {'properties': {'format': {'default': 'full', + 'description': "Context format: 'full' or " + "'gist'.", + 'title': 'Format', + 'type': 'string'}, + 'k': {'default': 50, + 'description': 'Maximum source memories.', + 'maximum': 50, + 'minimum': 1, + 'title': 'K', + 'type': 'integer'}, + 'query': {'description': 'Question or task needing ' + 'prior context.', + 'maxLength': 100000, + 'minLength': 1, + 'title': 'Query', + 'type': 'string'}, + 'repo': {'anyOf': [{'maxLength': 200, + 'type': 'string'}, + {'type': 'null'}], + 'default': None, + 'description': 'Optional repository.', + 'title': 'Repo'}, + 'session_id': {'anyOf': [{'type': 'string'}, + {'type': 'null'}], + 'default': None, + 'description': 'Optional active ' + 'session.', + 'title': 'Session Id'}, + 'token_budget': {'default': 1024, + 'description': 'Hard ' + 'returned-context ' + 'token budget.', + 'maximum': 32768, + 'minimum': 0, + 'title': 'Token Budget', + 'type': 'integer'}, + 'workspace': {'anyOf': [{'maxLength': 200, + 'type': 'string'}, + {'type': 'null'}], + 'default': None, + 'description': 'Optional workspace.', + 'title': 'Workspace'}}, + 'required': ['query'], + 'title': 'smart_recall_contextArguments', + 'type': 'object'}, + 'engraphis_remember': {'properties': {'claim_kind': {'default': '', + 'description': 'Optional claim ' + 'predicate/category (for ' + "example 'configured_value').", + 'maxLength': 200, + 'title': 'Claim Kind', + 'type': 'string'}, + 'content': {'description': 'Durable fact, decision, ' + 'preference, or procedure.', + 'maxLength': 100000, + 'minLength': 1, + 'title': 'Content', + 'type': 'string'}, + 'importance': {'default': 0.0, + 'description': 'Salience from 0 to 1.', + 'maximum': 1.0, + 'minimum': 0.0, + 'title': 'Importance', + 'type': 'number'}, + 'mtype': {'default': 'semantic', + 'description': 'semantic, episodic, procedural, ' + 'or working.', + 'title': 'Mtype', + 'type': 'string'}, + 'repo': {'anyOf': [{'maxLength': 200, 'type': 'string'}, + {'type': 'null'}], + 'default': None, + 'description': 'Optional repository.', + 'title': 'Repo'}, + 'session_id': {'anyOf': [{'type': 'string'}, + {'type': 'null'}], + 'default': None, + 'description': 'Optional active session.', + 'title': 'Session Id'}, + 'subject_key': {'default': '', + 'description': 'Optional stable claim ' + 'subject (for example ' + "'api.rate_limit'). Matching " + 'keys make supersession ' + 'safer and deterministic.', + 'maxLength': 1000, + 'title': 'Subject Key', + 'type': 'string'}, + 'workspace': {'default': 'default', + 'description': 'Workspace for the memory.', + 'maxLength': 200, + 'title': 'Workspace', + 'type': 'string'}}, + 'required': ['content'], + 'title': 'smart_rememberArguments', + 'type': 'object'}, + 'engraphis_session': {'properties': {'action': {'default': 'start', + 'description': 'start to resume work, or end to ' + 'save its handoff.', + 'title': 'Action', + 'type': 'string'}, + 'agent': {'default': '', + 'description': 'Optional agent name.', + 'maxLength': 200, + 'title': 'Agent', + 'type': 'string'}, + 'force_new': {'default': False, + 'description': 'Start only: branch a new ' + 'session instead of reusing an ' + 'exact active task.', + 'title': 'Force New', + 'type': 'boolean'}, + 'goal': {'default': '', + 'description': 'Task goal; start returns bounded ' + 'relevant context.', + 'maxLength': 1000, + 'title': 'Goal', + 'type': 'string'}, + 'open_threads': {'anyOf': [{'items': {'type': 'string'}, + 'type': 'array'}, + {'type': 'null'}], + 'default': None, + 'description': 'Unresolved follow-ups.', + 'title': 'Open Threads'}, + 'outcome': {'default': '', + 'description': 'Optional outcome label.', + 'maxLength': 1000, + 'title': 'Outcome', + 'type': 'string'}, + 'repo': {'anyOf': [{'maxLength': 200, 'type': 'string'}, + {'type': 'null'}], + 'default': None, + 'description': 'Optional repository scope.', + 'title': 'Repo'}, + 'session_id': {'default': '', + 'description': 'Session id required to end a ' + 'session.', + 'maxLength': 200, + 'title': 'Session Id', + 'type': 'string'}, + 'summary': {'default': '', + 'description': 'Short final handoff.', + 'maxLength': 100000, + 'title': 'Summary', + 'type': 'string'}, + 'token_budget': {'default': 512, + 'description': 'Goal-context budget when ' + 'starting.', + 'maximum': 32768, + 'minimum': 0, + 'title': 'Token Budget', + 'type': 'integer'}, + 'workspace': {'default': 'default', + 'description': 'Workspace for a started ' + 'session.', + 'maxLength': 200, + 'title': 'Workspace', + 'type': 'string'}}, + 'title': 'engraphis_sessionArguments', + 'type': 'object'}, + 'engraphis_update_memory': {'properties': {'actor': {'default': 'user', + 'description': 'Optional local-mode actor ' + 'label; authenticated team ' + 'mode uses the caller ' + 'identity.', + 'maxLength': 200, + 'title': 'Actor', + 'type': 'string'}, + 'importance': {'anyOf': [{'maximum': 1.0, + 'minimum': 0.0, + 'type': 'number'}, + {'type': 'null'}], + 'default': None, + 'description': 'Optional importance ' + '0..1.', + 'title': 'Importance'}, + 'memory_id': {'description': 'Memory id to update.', + 'maxLength': 200, + 'minLength': 1, + 'title': 'Memory Id', + 'type': 'string'}, + 'mtype': {'anyOf': [{'maxLength': 50, 'type': 'string'}, + {'type': 'null'}], + 'default': None, + 'description': 'Optional memory type ' + '(working|episodic|semantic|procedural).', + 'title': 'Mtype'}, + 'repo': {'anyOf': [{'maxLength': 200, 'type': 'string'}, + {'type': 'null'}], + 'default': None, + 'description': 'Optional repository scope.', + 'title': 'Repo'}, + 'title': {'anyOf': [{'maxLength': 500, + 'type': 'string'}, + {'type': 'null'}], + 'default': None, + 'description': 'Optional new title.', + 'title': 'Title'}, + 'workspace': {'default': 'default', + 'description': 'Workspace containing the ' + 'memory.', + 'maxLength': 200, + 'title': 'Workspace', + 'type': 'string'}}, + 'required': ['memory_id'], + 'title': 'engraphis_update_memoryArguments', + 'type': 'object'}} diff --git a/integrations/prime_agent/src/engraphis_prime_agent/tools.py b/integrations/prime_agent/src/engraphis_prime_agent/tools.py index 9b679480..31db0b01 100644 --- a/integrations/prime_agent/src/engraphis_prime_agent/tools.py +++ b/integrations/prime_agent/src/engraphis_prime_agent/tools.py @@ -1,13 +1,16 @@ """9 Smart tool factories, each a (args, ctx) -> dict callable. -Schema and semantics are translated 1:1 from -integrations/pi/src/tool-schemas.ts. The resulting callables work with +Schemas are generated from the registered Smart MCP tools. The resulting callables work with both EngraphisPrimeAgent and any prime-agent tool-registration surface that matches the (args: dict, ctx: dict | None) -> dict contract. """ from __future__ import annotations from typing import Any, Awaitable, Callable +from copy import deepcopy +import re + +from ._contract import SMART_SCHEMAS from .config import EngraphisRuntimeConfig from .mcp_client import EngraphisMcpClient, EngraphisMcpToolError @@ -21,163 +24,32 @@ [dict[str, Any], dict[str, Any] | None], Awaitable[dict[str, Any]] ] -# --- JSON Schemas (translated from tool-schemas.ts) ------------------------- -# The same defaults, bounds, and descriptions; identical behaviour across Pi -# and prime-agent integrations. - -_SESSION_SCHEMA: dict[str, Any] = { - "type": "object", - "additionalProperties": False, - "properties": { - "action": { - "type": "string", - "enum": ["start", "end", "start_session", "end_session"], - "default": "start", - }, - # The wrapper supplies the registered agent name when the caller omits - # this optional field. Keeping it optional also lets the framework - # invoke the lifecycle tool without duplicating registration metadata. - "agent": {"type": "string", "minLength": 1, "maxLength": 200}, - "force_new": {"type": "boolean", "default": False}, - "goal": {"type": "string", "maxLength": 1000, "default": ""}, - "session_id": {"type": "string", "maxLength": 200, "default": ""}, - "summary": {"type": "string", "maxLength": 100000, "default": ""}, - "outcome": {"type": "string", "maxLength": 1000, "default": ""}, - "open_threads": { - "type": ["array", "null"], - "items": {"type": "string"}, - "default": None, - }, - "token_budget": {"type": "integer", "minimum": 0, "maximum": 32768, "default": 512}, - "workspace": {"type": "string", "maxLength": 200}, - "repo": {"type": ["string", "null"], "maxLength": 200, "default": None}, - }, - "required": [], -} - -_RECALL_CONTEXT_SCHEMA: dict[str, Any] = { - "type": "object", - "additionalProperties": False, - "properties": { - "query": {"type": "string", "minLength": 1, "maxLength": 100000}, - "k": {"type": "integer", "minimum": 1, "maximum": 50, "default": 50}, - "session_id": {"type": ["string", "null"], "default": None}, - "token_budget": { - "type": "integer", - "minimum": 0, - "maximum": 32768, - "default": 1024, - }, - "workspace": {"type": ["string", "null"], "maxLength": 200, "default": None}, - "repo": {"type": ["string", "null"], "maxLength": 200, "default": None}, - }, - "required": ["query"], -} - -_REMEMBER_SCHEMA: dict[str, Any] = { - "type": "object", - "additionalProperties": False, - "properties": { - "content": {"type": "string", "minLength": 1, "maxLength": 100000}, - "mtype": { - "type": "string", - "enum": ["semantic", "episodic", "procedural", "working"], - "default": "semantic", - }, - "importance": {"type": "number", "minimum": 0, "maximum": 1, "default": 0}, - "session_id": {"type": ["string", "null"], "default": None}, - "workspace": {"type": "string", "maxLength": 200}, - "repo": {"type": ["string", "null"], "maxLength": 200, "default": None}, - "subject_key": {"type": "string", "maxLength": 1000}, - "claim_kind": {"type": "string", "maxLength": 200}, - }, - "required": ["content"], -} +# Generated fields, types, nullability, bounds and defaults follow the runtime. +# These local semantic checks preserve documented aliases and enum validation. -_DISCOVER_ACTIONS_SCHEMA: dict[str, Any] = { - "type": "object", - "additionalProperties": False, - "properties": { - "task": {"type": "string", "minLength": 1, "maxLength": 2000}, - "category": { - "type": "string", - "enum": ["memory", "governance", "code", "audit", "ops", ""], - "maxLength": 100, - "default": "", - }, - "intent": { - "type": "string", - "enum": ["any", "read", "write", "admin", "destructive"], - "default": "any", - }, - "limit": {"type": "integer", "minimum": 1, "maximum": 3, "default": 1}, - }, - "required": ["task"], -} - -_EXECUTE_PARAM_PROPS = { - "capability_id": {"type": "string", "minLength": 8, "maxLength": 128}, - "schema_digest": {"type": "string", "minLength": 8, "maxLength": 128}, - "arguments": {"type": "object", "additionalProperties": True}, -} - -_EXECUTE_READ_SCHEMA: dict[str, Any] = { - "type": "object", - "additionalProperties": False, - "properties": _EXECUTE_PARAM_PROPS, - "required": ["capability_id", "schema_digest", "arguments"], -} - -_EXECUTE_ACTION_SCHEMA: dict[str, Any] = { - "type": "object", - "additionalProperties": False, - "properties": _EXECUTE_PARAM_PROPS, - "required": ["capability_id", "schema_digest", "arguments"], -} - -_GET_MEMORY_SCHEMA: dict[str, Any] = { - "type": "object", - "additionalProperties": False, - "properties": { - "memory_id": {"type": "string", "minLength": 1, "maxLength": 200}, - "workspace": {"type": "string", "maxLength": 200}, - "repo": {"type": ["string", "null"], "maxLength": 200, "default": None}, - }, - "required": ["memory_id"], -} - -_UPDATE_MEMORY_SCHEMA: dict[str, Any] = { - "type": "object", - "additionalProperties": False, - "properties": { - "memory_id": {"type": "string", "minLength": 1, "maxLength": 200}, - "title": {"type": ["string", "null"], "maxLength": 500, "default": None}, - "mtype": { - "type": ["string", "null"], - "enum": ["semantic", "episodic", "procedural", "working", None], - "default": None, - }, - "importance": {"type": ["number", "null"], "minimum": 0, "maximum": 1, "default": None}, - "actor": {"type": "string", "maxLength": 200, "default": "user"}, - "workspace": {"type": "string", "maxLength": 200}, - "repo": {"type": ["string", "null"], "maxLength": 200, "default": None}, - }, - "required": ["memory_id"], -} +_SCHEMAS = deepcopy(SMART_SCHEMAS) +for _schema in _SCHEMAS.values(): + _schema["additionalProperties"] = False + _schema.setdefault("required", []) +_SCHEMAS["engraphis_session"]["properties"]["action"]["enum"] = [ + "start", "end", "start_session", "end_session", +] +_SCHEMAS["engraphis_remember"]["properties"]["mtype"]["enum"] = [ + "semantic", "episodic", "procedural", "working", +] +_SCHEMAS["engraphis_discover_actions"]["properties"]["category"]["enum"] = [ + "memory", "governance", "code", "audit", "ops", "", +] +_SESSION_SCHEMA = _SCHEMAS["engraphis_session"] +_RECALL_CONTEXT_SCHEMA = _SCHEMAS["engraphis_recall_context"] +_REMEMBER_SCHEMA = _SCHEMAS["engraphis_remember"] +_GET_MEMORY_SCHEMA = _SCHEMAS["engraphis_get_memory"] +_UPDATE_MEMORY_SCHEMA = _SCHEMAS["engraphis_update_memory"] +_CONFLICT_REVIEW_SCHEMA = _SCHEMAS["engraphis_conflict_review"] +_DISCOVER_ACTIONS_SCHEMA = _SCHEMAS["engraphis_discover_actions"] +_EXECUTE_READ_SCHEMA = _SCHEMAS["engraphis_execute_read"] +_EXECUTE_ACTION_SCHEMA = _SCHEMAS["engraphis_execute_action"] -_CONFLICT_REVIEW_SCHEMA: dict[str, Any] = { - "type": "object", - "additionalProperties": False, - "properties": { - "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, - "workspace": {"type": "string", "maxLength": 200}, - "repo": {"type": ["string", "null"], "maxLength": 200, "default": None}, - }, - # All three parameters are optional; the empty list documents that - # explicitly so consumers don't have to guess whether the missing - # `required` key means "all fields implicit" or "no fields required". - "required": [], -} _DESC: dict[str, str] = { "engraphis_session": ( @@ -361,19 +233,24 @@ def _coerce_type(value: Any, declared: Any) -> bool: declared = [declared] # bool is a subclass of int in Python; reject it where the schema # says "integer" / "number" so a stray `True` is not silently accepted. - for t in declared: - py = _TYPE_RANK.get(t) - if py is None: - continue - if t in ("integer", "number") and isinstance(value, bool): - continue - if isinstance(value, py): - return True - return False + for t in declared: + py = _TYPE_RANK.get(t) + if py is None: + continue + if t in ("integer", "number") and isinstance(value, bool): + continue + if isinstance(value, py): + return True + return False def _validate_schema(schema: dict[str, Any], value: Any, path: str = "") -> list[str]: errors: list[str] = [] + if "anyOf" in schema: + if not any(not _validate_schema(branch, value, path) for branch in schema["anyOf"]): + return [f"{path or 'value'}: does not match any allowed type"] + if "pattern" in schema and isinstance(value, str) and re.search(schema["pattern"], value) is None: + errors.append(f"{path or 'value'}: does not match required pattern") declared_type = schema.get("type") if declared_type is not None: if not _coerce_type(value, declared_type): diff --git a/integrations/prime_agent/tests/test_tools.py b/integrations/prime_agent/tests/test_tools.py index 8c88469a..52b496ba 100644 --- a/integrations/prime_agent/tests/test_tools.py +++ b/integrations/prime_agent/tests/test_tools.py @@ -48,8 +48,9 @@ def test_remember_schema_declares_keyed_claim_fields_as_properties() -> None: assert {"subject_key", "claim_kind"} <= set(schema["properties"]) assert "subject_key" not in schema assert "claim_kind" not in schema - assert schema["properties"]["subject_key"] == {"type": "string", "maxLength": 1000} - assert schema["properties"]["claim_kind"] == {"type": "string", "maxLength": 200} + from engraphis_prime_agent._contract import SMART_SCHEMAS + for key in ("subject_key", "claim_kind"): + assert schema["properties"][key] == SMART_SCHEMAS["engraphis_remember"]["properties"][key] def test_session_agent_is_optional_for_registered_lifecycle_calls() -> None: @@ -371,3 +372,11 @@ def test_all_tool_schemas_have_unique_property_names_within_tool() -> None: assert len(props) == len(set(props)), ( f"{name} schema has duplicate property names: {list(props)}" ) + + +def test_generated_recall_fields_and_nullable_constraints(): + schema = dict(TOOL_SPECS)["engraphis_recall_context"] + assert schema["properties"]["format"]["default"] == "full" + assert validate_args("engraphis_recall_context", {"query": "fact", "format": "gist", "workspace": None}) + with pytest.raises(Exception, match="allowed type"): + validate_args("engraphis_recall_context", {"query": "fact", "workspace": "x" * 201}) diff --git a/playwright.config.js b/playwright.config.js index 84121c7c..27d9ed37 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -13,7 +13,7 @@ module.exports = defineConfig({ retries: 0, use: { baseURL: playwrightBaseURL, - trace: 'on-first-retry', + trace: 'retain-on-failure', screenshot: 'only-on-failure', }, webServer: { @@ -27,8 +27,12 @@ module.exports = defineConfig({ // The single Playwright web server owns this process-local database for the run. ENGRAPHIS_DB_PATH: ':memory:', ENGRAPHIS_EMBED_MODEL: '', + ENGRAPHIS_EXTRACTOR: 'none', ENGRAPHIS_LOOP_INTERVAL: '0', ENGRAPHIS_HOST: '127.0.0.1', + // Public test fixture, never the owner's configured credential. The real + // browser smoke uses this to exercise governed source approval. + ENGRAPHIS_API_TOKEN: 'engraphis-playwright-local-only', ENGRAPHIS_SERVICE_MODE: 'customer', }, }, @@ -37,5 +41,15 @@ module.exports = defineConfig({ name: 'chromium', use: { browserName: 'chromium' }, }, + { + name: 'firefox-smoke', + testMatch: '**/workspace-smoke.spec.js', + use: { browserName: 'firefox' }, + }, + { + name: 'webkit-smoke', + testMatch: '**/workspace-smoke.spec.js', + use: { browserName: 'webkit' }, + }, ], }); diff --git a/scripts/check_commercial_manifest.py b/scripts/check_commercial_manifest.py index 429efa2e..a2dcf7bf 100644 --- a/scripts/check_commercial_manifest.py +++ b/scripts/check_commercial_manifest.py @@ -106,7 +106,8 @@ def _check_repository(manifest: dict, errors: list[str]) -> None: if monthly and not (10 * monthly <= annual <= 12 * monthly): _fail(errors, "%s annual price is not a sane multiple of monthly" % plan) - expected_trial = {"days": 3, "card_required": False, "plans": ["pro", "team"]} + expected_trial = {"days": 3, "card_required": False, "plans": ["pro", "team"], + "days_by_plan": {"pro": 3, "team": 10}} trial = manifest.get("trial", {}) for key, value in expected_trial.items(): if trial.get(key) != value: @@ -213,6 +214,11 @@ def _check_website(manifest: dict, website: Path, errors: list[str]) -> None: for claim in required: if claim not in text: _fail(errors, "website is missing manifest claim: %s" % claim) + for plan, days in manifest["trial"]["days_by_plan"].items(): + if not re.search(r'plan=' + plan + r'&interval=monthly#billing[^>]*>\s*' + str(days) + r'-day free trial', text): + _fail(errors, "website trial copy does not match " + plan) + if re.search(r"29\s*tools", text): + _fail(errors, "website advertises a stale MCP tool count") for plan in ("pro", "team"): for interval, product in manifest["plans"][plan]["products"].items(): if product["checkout_url"] not in text: @@ -222,10 +228,15 @@ def _check_website(manifest: dict, website: Path, errors: list[str]) -> None: def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--website-root", type=Path) + parser.add_argument("--cloud-contract", type=Path, help="Secret-free cloud product contract JSON") args = parser.parse_args() manifest = json.loads(MANIFEST_PATH.read_text(encoding="utf-8")) errors: list[str] = [] _check_repository(manifest, errors) + if args.cloud_contract: + cloud = json.loads(args.cloud_contract.read_text(encoding="utf-8-sig")) + if cloud.get("schema_version") != 1 or cloud.get("trial", {}).get("days_by_plan") != manifest["trial"]["days_by_plan"]: + _fail(errors, "cloud trial authority differs from the public per-plan contract") if args.website_root: _check_website(manifest, args.website_root.resolve(), errors) if errors: diff --git a/scripts/export_mcp_contract.py b/scripts/export_mcp_contract.py new file mode 100644 index 00000000..baa8131a --- /dev/null +++ b/scripts/export_mcp_contract.py @@ -0,0 +1,65 @@ +"""Generate versioned MCP schemas and integration inputs from the registered tools.""" +from __future__ import annotations + +import argparse +import hashlib +import json +from pathlib import Path +from pprint import pformat +import sys + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) + + +def build_contract() -> dict: + from engraphis.mcp_server import classic_mcp, smart_mcp + + surfaces = {} + for name, server in (("smart", smart_mcp), ("classic", classic_mcp)): + surfaces[name] = [ + {"name": tool.name, "description": tool.description, + "inputSchema": tool.parameters, + "annotations": tool.annotations.model_dump(mode="json", exclude_none=True) + if tool.annotations else {}} + for _, tool in sorted(server._tool_manager._tools.items()) + ] + encoded = json.dumps(surfaces, sort_keys=True, separators=(",", ":")).encode() + return {"schema": "engraphis-mcp-contract/v1", "sha256": hashlib.sha256(encoded).hexdigest(), + "surfaces": surfaces} + + +def artifacts(contract: dict) -> dict[Path, str]: + schemas = {tool["name"]: tool["inputSchema"] for tool in contract["surfaces"]["smart"]} + return { + ROOT / "docs/MCP_CONTRACT.json": json.dumps(contract, indent=2, sort_keys=True) + "\n", + ROOT / "integrations/pi/src/generated-contract.ts": + "// Generated by scripts/export_mcp_contract.py. Do not edit.\n" + "export const SMART_SCHEMAS = " + json.dumps(schemas, indent=2, sort_keys=True) + " as const;\n", + ROOT / "integrations/prime_agent/src/engraphis_prime_agent/_contract.py": + '"""Generated by scripts/export_mcp_contract.py. Do not edit."""\n' + "SMART_SCHEMAS = " + pformat(schemas, sort_dicts=True, width=100) + "\n", + } + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--check", action="store_true") + args = parser.parse_args() + stale = [] + for path, expected in artifacts(build_contract()).items(): + if args.check: + if not path.exists() or path.read_text(encoding="utf-8") != expected: + stale.append(str(path.relative_to(ROOT))) + else: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(expected, encoding="utf-8", newline="\n") + if stale: + print("MCP contract drift: " + ", ".join(stale)) + return 1 + print("MCP contract check: OK" if args.check else "MCP contract exported") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/init.py b/scripts/init.py index 5610356b..7b3b06b6 100644 --- a/scripts/init.py +++ b/scripts/init.py @@ -3,12 +3,12 @@ Closes the biggest first-run gap: with no configuration, an installed build puts its database in the platform user-data directory, where most people never think to look. This command writes the process-selected trusted config file with an explicit absolute -DB path (and optional API token), then prints exact MCP snippets to paste into Claude -Code / Cursor / Cline / Zed. +DB path and a local API token, then prints exact MCP snippets to paste into Codex, +Claude Code / Cursor / Cline / Zed. engraphis-init # write ~/.engraphis/config.env engraphis-init --db ~/mem.db # choose the database location - engraphis-init --token # also generate a bearer token for the HTTP APIs + engraphis-init --token # compatibility flag: new configs always receive a local API token engraphis-init --encrypted # require SQLCipher and provision a private DB key file engraphis-init --force # overwrite the trusted config file engraphis-init --check # doctor: verify install, extras, DB writability @@ -21,6 +21,7 @@ import argparse import json import secrets +import shutil import sqlite3 import sys from pathlib import Path @@ -56,16 +57,25 @@ def _try_import(name: str): return None -def cmd_check() -> int: +def cmd_check(*, json_output: bool = False) -> int: """Doctor: report what's installed and whether the configured DB is usable.""" - failures = 0 - print(f"engraphis doctor - python {sys.version.split()[0]}") + checks: list[dict[str, str]] = [] + + def report(code: str, status: str, label: str, detail: str = "") -> None: + checks.append({"code": code, "status": status, "label": label, "detail": detail}) + + def fail_database(exc: Exception) -> None: + message = str(exc).lower() + code = "database_locked" if "locked" in message or "busy" in message else "database_unwritable" + detail = "Close the process holding the database lock and retry." if code == "database_locked" else ( + "Check the database path, directory permissions, free disk space and configured encryption key." + ) + report(code, "fail", "database writable", detail) if _try_import("numpy") is None: - _fail("numpy (required core)", "pip install numpy") - failures += 1 + report("numpy", "fail", "numpy (required core)", "pip install numpy") else: - _ok("numpy (required core)") + report("numpy", "ok", "numpy (required core)") for mod, label, hint in [ ("mcp", "MCP server extra", 'pip install "engraphis[mcp]"'), @@ -76,45 +86,101 @@ def cmd_check() -> int: "optional - regex code indexer is the fallback"), ]: available = _try_import(mod) is not None - (_ok if available else _miss)(label, "" if available else hint) + report(mod, "ok" if available else "optional", label, "" if available else hint) + + if _try_import("pytesseract") is not None: + ocr = shutil.which("tesseract") is not None + report("ocr_executable", "ok" if ocr else "optional", "OCR executable", + "" if ocr else "Install Tesseract to enable image OCR; other document formats remain available.") from engraphis.config import settings - db = Path(settings.db_path).expanduser() + db_name = str(settings.db_path) + db = Path(db_name).expanduser() + conn: Any = None + database_stage = "connection" try: - db.parent.mkdir(parents=True, exist_ok=True) + if db_name != ":memory:": + db.parent.mkdir(parents=True, exist_ok=True) connector = connector_from_env() - conn: Any = ( + conn = ( connector(str(db)) if connector is not None - else sqlite3.connect(str(db)) + else sqlite3.connect(str(db), timeout=2.0) ) - conn.execute("PRAGMA user_version") - conn.close() - _ok("database writable", str(db)) + conn.execute("PRAGMA busy_timeout=2000") + conn.execute("PRAGMA user_version").fetchone() + report("database_readable", "ok", "database readable", str(db)) + # A TEMP table would only probe the temp database. Exercise the main database + # under a transaction, then roll back both schema and data unconditionally. + probe = "_engraphis_doctor_" + secrets.token_hex(12) + try: + conn.execute("BEGIN IMMEDIATE") + conn.execute(f'CREATE TABLE "{probe}" (value INTEGER)') + conn.execute(f'INSERT INTO "{probe}" (value) VALUES (1)') + finally: + conn.rollback() + report("database_writable", "ok", "database writable", str(db)) + database_stage = "schema" + from engraphis.core.schema import SCHEMA_VERSION + has_migrations = conn.execute( + "SELECT 1 FROM sqlite_master WHERE type='table' AND name='schema_migrations'" + ).fetchone() + if has_migrations: + version = conn.execute("SELECT MAX(version) FROM schema_migrations").fetchone()[0] or 0 + if version > SCHEMA_VERSION: + report("schema_newer", "fail", "database schema", + "This database requires a newer Engraphis version. Upgrade before opening it.") + else: + report("database_schema", "ok", "database schema", + f"version {version}; supported through {SCHEMA_VERSION}") + else: + report("database_schema", "optional", "database schema", + "Not initialized; the engine will initialize the database on first use.") except Exception as exc: - _fail("database writable", f"{db}: {exc}") - failures += 1 - - _ok("local core", "single-user features available without a hosted subscription") + if database_stage == "schema": + report("database_schema", "fail", "database schema", + "The schema could not be verified. Check this database with the matching Engraphis version.") + else: + fail_database(exc) + finally: + if conn is not None: + conn.close() + + report("local_core", "ok", "local core", "single-user features available without a hosted subscription") + if settings.api_token: + report("browser_approval", "ok", "source review", "Use engraphis-dashboard to open an authenticated local browser.") + else: + report("browser_approval", "optional", "source review", + "Prompt approval needs a local API token. Set ENGRAPHIS_API_TOKEN in your private config, " + "restart the dashboard and open it with engraphis-dashboard. Tokenless browsing remains available.") try: from engraphis.cloud_session import configured if configured(require_compute=False): - _ok("Engraphis Cloud", "installation connected") + report("cloud", "ok", "Engraphis Cloud", "installation connected") else: - _miss("Engraphis Cloud", "not connected (optional for the local core)") + report("cloud", "optional", "Engraphis Cloud", "not connected (optional for the local core)") except Exception: - _miss("Engraphis Cloud", "saved session unavailable; reconnect if needed") + report("cloud", "optional", "Engraphis Cloud", "saved session unavailable; reconnect if needed") try: from engraphis.backends.embedder_st import get_embedder - emb = get_embedder(settings.embed_model or None, dim=settings.embed_dim or 384) + emb = get_embedder(settings.embed_model or None, dim=settings.embed_dim or 384, + require_exact=bool(settings.embed_model)) emb.embed(["engraphis doctor check"]) - _ok("embedder functional", f"{type(emb).__name__} ({getattr(emb, 'dim', 384)}d)") + report("embedder", "ok", "embedder functional", f"{type(emb).__name__} ({getattr(emb, 'dim', 384)}d)") except Exception as exc: - _fail("embedder functional", f"{type(exc).__name__}: {exc}") - failures += 1 + report("embedder", "fail", "embedder functional", + f"{type(exc).__name__}: check the configured model and its dependencies, or select offline embeddings.") - print("all good" if failures == 0 else f"{failures} problem(s) found") + failures = sum(check["status"] == "fail" for check in checks) + if json_output: + print(json.dumps({"schema_version": 1, "python": sys.version.split()[0], + "ok": failures == 0, "failures": failures, "checks": checks})) + else: + print(f"engraphis doctor - python {sys.version.split()[0]}") + for check in checks: + {"ok": _ok, "optional": _miss, "fail": _fail}[check["status"]](check["label"], check["detail"]) + print("all good" if failures == 0 else f"{failures} problem(s) found") return 0 if failures == 0 else 1 @@ -255,7 +321,7 @@ def main(argv=None) -> int: ap.add_argument("--db", default="engraphis.db", help="database file (default: ./engraphis.db)") ap.add_argument("--token", action="store_true", - help="generate an ENGRAPHIS_API_TOKEN for the HTTP APIs") + help="compatibility flag: new configuration always receives a local API token") encryption = ap.add_mutually_exclusive_group() encryption.add_argument( "--encrypted", action="store_true", @@ -272,12 +338,16 @@ def main(argv=None) -> int: ) ap.add_argument("--check", action="store_true", help="doctor mode: verify the installation without writing config") + ap.add_argument("--json", action="store_true", help="emit structured doctor diagnostics (requires --check)") + ap.add_argument("--extras", help="record installed capabilities for future updates, e.g. server,mcp or none") ap.add_argument("--prefetch", action="store_true", help="pre-cache the configured embedding model for instant MCP startup") args = ap.parse_args(argv) + if args.json and not args.check: + ap.error("--json requires --check") if args.check: - return cmd_check() + return cmd_check(json_output=args.json) if args.prefetch: return cmd_prefetch() @@ -287,7 +357,14 @@ def main(argv=None) -> int: except (OSError, RuntimeError, ValueError) as exc: _fail("trusted configuration", str(exc)) return 1 - token = secrets.token_urlsafe(24) if args.token else "" + # Approval is intentionally restricted to an authenticated local browser. Give + # new setups a usable review path while leaving every existing config untouched. + token = secrets.token_urlsafe(24) + from scripts.installation_profile import normalize_extras, write_profile + try: + selected_extras = normalize_extras(args.extras) if args.extras is not None else None + except ValueError as exc: + ap.error(str(exc)) sqlcipher_available = _try_import("sqlcipher3") is not None if args.encrypted and not sqlcipher_available: _fail("SQLCipher encryption", 'install it with: pip install "engraphis[encryption]"') @@ -338,6 +415,14 @@ def main(argv=None) -> int: if token: print(" api token -> generated (in trusted config; send as 'Authorization: Bearer ...')") + if selected_extras is not None: + try: + write_profile(selected_extras, config_path=env_file) + except OSError: + _fail("installation profile", "Could not record capabilities in the private configuration directory.") + return 1 + print(" update capabilities -> " + (",".join(selected_extras) or "base package only")) + mcp_env = {"ENGRAPHIS_DB_PATH": str(db_path)} if key_path is not None: mcp_env["ENGRAPHIS_DB_KEY_FILE"] = str(key_path) @@ -350,11 +435,19 @@ def main(argv=None) -> int: if key_path is not None: command += f' --env ENGRAPHIS_DB_KEY_FILE="{key_path}"' print(command + " -- engraphis-mcp") + print("\nConnect your agent - Codex:") + codex_command = f' codex mcp add engraphis --env ENGRAPHIS_DB_PATH="{db_path}"' + if key_path is not None: + codex_command += f' --env ENGRAPHIS_DB_KEY_FILE="{key_path}"' + print(codex_command + " -- engraphis-mcp") print("\nCursor / Cline / Zed / Windsurf (mcp config):") print(json.dumps(snippet, indent=2)) print("\nNext steps:") print(" engraphis-dashboard # product UI on http://127.0.0.1:8700") print(" engraphis-init --check # verify the install") + print(" In the dashboard: create a workspace, save one project decision, review its source and Approve for prompt.") + print(" Then Ask about the decision to see its cited source.") + print(" Open its citation to review the source; edit the record when the decision changes.") print(" Free forever at the core - start the 3-day Pro trial or subscribe at " "https://api.engraphis.com/account?plan=pro&interval=monthly#billing") return 0 diff --git a/scripts/installation_profile.py b/scripts/installation_profile.py new file mode 100644 index 00000000..4d30f869 --- /dev/null +++ b/scripts/installation_profile.py @@ -0,0 +1,64 @@ +"""Explicit installation intent, scoped to a trusted config and Python environment. + +Package metadata cannot tell which extras the owner selected. Only an explicit setup +choice creates this profile; old or unreadable profiles retain the updater's fallback. +""" +from __future__ import annotations + +import hashlib +import json +import os +import re +import sys +from pathlib import Path +from typing import Optional + +from engraphis.private_state import atomic_private_text, ensure_owner_private_dir, read_private_text + + +def normalize_extras(value: str) -> list[str]: + """Validate package-extra names without interpreting them as command arguments.""" + if value.strip().casefold() in {"", "none", "base"}: + return [] + names = [name.strip().lower() for name in value.split(",") if name.strip()] + if not names or any(not re.fullmatch(r"[a-z0-9][a-z0-9_.-]*", name) for name in names): + raise ValueError("extras must be comma-separated package-extra names or 'none'") + return sorted(set(names)) + + +def _environment() -> str: + return os.path.normcase(str(Path(sys.prefix).resolve())) + + +def profile_path(config_path: Optional[Path] = None) -> Path: + if config_path is None: + from engraphis.config import trusted_env_path + config_path = trusted_env_path() + key = hashlib.sha256(_environment().encode("utf-8")).hexdigest()[:24] + return config_path.parent / "installations" / (key + ".json") + + +def write_profile(extras: list[str], *, config_path: Optional[Path] = None) -> None: + path = profile_path(config_path) + names = normalize_extras(",".join(extras)) + ensure_owner_private_dir(path.parent) + atomic_private_text(path, json.dumps({"schema_version": 1, "environment": _environment(), + "extras": names}, sort_keys=True) + "\n") + + +def read_profile() -> Optional[list[str]]: + """Return explicit intent, including [] for base, or None when it is unknown.""" + try: + raw = read_private_text(profile_path(), max_bytes=8192, owner_only=True) + if not raw: + return None + data = json.loads(raw) + if not isinstance(data, dict) or data.get("schema_version") != 1 or data.get("environment") != _environment(): + return None + extras = data.get("extras") + if not isinstance(extras, list) or not all(isinstance(name, str) for name in extras): + return None + normalized = normalize_extras(",".join(extras)) + return normalized if normalized == extras else None + except (OSError, ValueError, RuntimeError): + return None diff --git a/scripts/update.py b/scripts/update.py index 09f90350..ae131764 100644 --- a/scripts/update.py +++ b/scripts/update.py @@ -595,7 +595,12 @@ def _git_update(check_only: bool = False) -> None: print("Nothing to update.") return + # Resolve intent before switching the source tree, since an older release may + # not contain the installation-profile module used by this updater. + editable_extras = _explicit_installation_extras() or "" + install_target = str(project_dir) + editable_extras print(f"Update available: {local[:8]} -> {remote_sha[:8]} ({tag})") + print(f"Editable install target: {install_target}") if check_only: return @@ -628,7 +633,7 @@ def _git_update(check_only: bool = False) -> None: stage = "reinstall" print(f"Reinstalling from {project_dir}...") _run( - [sys.executable, "-m", "pip", "install", "-e", str(project_dir)], + [sys.executable, "-m", "pip", "install", "-e", install_target], "Reinstalling the editable checkout", _PIP_INSTALL_TIMEOUT_S, check=True, @@ -649,7 +654,7 @@ def _git_update(check_only: bool = False) -> None: manual = "Run `%s` and `%s` to restore the previous installation." % ( subprocess.list2cmdline([git, "-C", str(project_dir), "checkout", original_ref]), subprocess.list2cmdline( - [sys.executable, "-m", "pip", "install", "-e", str(project_dir)] + [sys.executable, "-m", "pip", "install", "-e", install_target] ), ) try: @@ -662,7 +667,7 @@ def _git_update(check_only: bool = False) -> None: env=_git_env(), ) _run( - [sys.executable, "-m", "pip", "install", "-e", str(project_dir)], + [sys.executable, "-m", "pip", "install", "-e", install_target], "Reinstalling the previous checkout", _PIP_INSTALL_TIMEOUT_S, check=True, @@ -683,13 +688,13 @@ def _git_update(check_only: bool = False) -> None: print(f"Updated to {tag}.") -def _installed_extras() -> str: +def _explicit_installation_extras() -> Optional[str]: """Return a safe extras suffix for update targets. Wheel metadata records which extras *could* install a requirement, not which extras the user selected. Treating every ``extra ==`` marker as installed therefore turned a core or server install into an arbitrary combination of - extras. Use the explicit override when supplied; otherwise install ``all`` so an + extras. Use the explicit override, then the setup profile; otherwise install ``all`` so an update never silently drops an existing optional surface. Set ``ENGRAPHIS_UPDATE_EXTRAS=none`` for a deliberate base-only update. """ @@ -704,12 +709,22 @@ def _installed_extras() -> str: "ENGRAPHIS_UPDATE_EXTRAS must be a comma-separated list of package extras or 'none'" ) return "[" + ",".join(sorted(set(names))) + "]" - return "[all]" + from scripts.installation_profile import read_profile + selected = read_profile() + if selected is not None: + return "[" + ",".join(selected) + "]" if selected else "" + return None + + +def _installed_extras() -> str: + selected = _explicit_installation_extras() + return selected if selected is not None else "[all]" def _pip_update(method: str, check_only: bool = False) -> None: """Update a pip install (PyPI or git).""" extras = _installed_extras() + print(f"Update capabilities: {extras or 'base package only'}") if method == "git": git = shutil.which("git") remote = _installed_git_url() @@ -764,6 +779,7 @@ def _pip_update(method: str, check_only: bool = False) -> None: def _pipx_update(check_only: bool = False) -> None: """Update a pipx install.""" extras = _installed_extras() + print(f"Update capabilities: {extras or 'base package only'}") if check_only: target = "engraphis" + extras + ( "==" + LATEST_TAG[1:] if LATEST_TAG else "" diff --git a/skills/engraphis-memory/references/TOOLS.md b/skills/engraphis-memory/references/TOOLS.md index 39e29e61..5afb8242 100644 --- a/skills/engraphis-memory/references/TOOLS.md +++ b/skills/engraphis-memory/references/TOOLS.md @@ -98,7 +98,7 @@ bodies already represented in `context`. - `query (str)`; `workspace (str, None)`; `repo (str, None)`; `session_id (str, None)`; `mtypes (list[str], None)`; `k (int, 50)`. - `token_budget (int, 1024)`: hard packed-context budget, `0..32768`. -- `format (str, "full")`: context format; `full` for full packed text, `gist` for one-line concise memory summaries. +- `format (str, "full")`: `full` or compatibility alias `gist`; both return the same budgeted, cited evidence with complete conditions and code whitespace. `gist` adds a format marker without another summary or an extra savings claim. - `retrieval_profile (str, "balanced")`: `balanced` is the default legacy hybrid; `auto` is explicit opt-in, with `fast`, `lexical`, `graph`, and `code` available for deliberate routing. The specialized graph/code profiles prioritize their named evidence while retaining supporting diff --git a/tests/e2e/commercial.spec.js b/tests/e2e/commercial.spec.js index 377d1796..9f90e500 100644 --- a/tests/e2e/commercial.spec.js +++ b/tests/e2e/commercial.spec.js @@ -51,7 +51,7 @@ function licenseFor(plan, features, overrides = {}) { // The control plane refuses a second trial for any organization that already holds an // entitlement, so a connected customer is never offered one — only an installation // that belongs to no organization is. - trial: { used: false, active: false, ends_at: 0, available: !paid, trial_days: 3 }, + trial: { used: false, active: false, ends_at: 0, available: !paid, trial_days: 3, days_by_plan: { pro: 3, team: 10 } }, ...overrides, }; } @@ -291,6 +291,20 @@ async function openView(page, name) { await expect(page.locator(`#view-${name}`)).toHaveClass(/\bactive\b/); } +test('Classic omits unknown Team trial duration from an older license response', async ({ page }) => { + const legacy = licenseFor('local', []); + delete legacy.trial.days_by_plan; + await mockLocalClient(page, 402, null, null, legacy); + await page.goto('/classic'); + await openView(page, 'team'); + const team = page.locator('#team-body'); + await expect(team.getByRole('link', { name: 'Start Team trial', exact: true })).toHaveAttribute('href', /trial=team/); + await expect(team).toContainText('review its duration in Cloud'); + await expect(team).not.toContainText('3 active days'); + await openView(page, 'settings'); + await expect(page.locator('.settings-license-panel').getByRole('link', { name: 'Start 3-day Pro trial' })).toBeVisible(); +}); + test('local dashboard keeps generic Pro and Team CTAs out of settings', async ({ page }) => { const errors = recordBrowserErrors(page); const calls = await mockLocalClient(page); @@ -305,19 +319,19 @@ test('local dashboard keeps generic Pro and Team CTAs out of settings', async ({ const licensePanel = page.locator('.settings-license-panel'); await expect(licensePanel.getByText('LOCAL CORE', { exact: true })).toBeVisible(); await expect(licensePanel.getByRole('link', { name: 'Start 3-day Pro trial' })).toBeVisible(); - await expect(licensePanel.getByRole('link', { name: 'Start 3-day Team trial' })).toHaveCount(0); + await expect(licensePanel.getByRole('link', { name: 'Start 10-day Team trial' })).toHaveCount(0); await expect(licensePanel).not.toContainText('Support continued Engraphis development with Pro.'); await openView(page, 'team'); const team = page.locator('#team-body'); await expect(team.getByText('Engraphis Team Cloud', { exact: false })).toBeVisible(); - await expect(team.getByRole('link', { name: 'Start 3-day Team trial' })) + await expect(team.getByRole('link', { name: 'Start 10-day Team trial' })) .toHaveAttribute( 'href', 'https://cloud.engraphis.test/team?plan=team&interval=monthly&trial=team&utm_source=engraphis&utm_medium=product&utm_campaign=pro_conversion&utm_content=team_tab#billing', ); await expect(team.getByRole('link', { name: 'Open Team Cloud' })).toHaveCount(0); - await expect(team).toContainText('exactly 3 active days'); + await expect(team).toContainText('exactly 10 active days'); await expect(team).toContainText( 'Private-service account grace is capped at 24 hours, never extends Team access, and never restricts the free local core.', ); @@ -396,7 +410,7 @@ test('a paying Team customer sees TEAM with Team administration unlocked', async // A paying customer is offered the account portal, never another trial. await expect(licensePanel.getByRole('link', { name: 'Open Engraphis Cloud' })) .toHaveAttribute('href', 'https://cloud.engraphis.test/account?utm_source=engraphis&utm_medium=product&utm_campaign=pro_conversion&utm_content=license'); - await expect(licensePanel.getByRole('link', { name: 'Start 3-day Team trial' })) + await expect(licensePanel.getByRole('link', { name: 'Start 10-day Team trial' })) .toHaveCount(0); await expect(licensePanel.getByRole('link', { name: 'Start 3-day Pro trial' })) .toHaveCount(0); @@ -408,7 +422,7 @@ test('a paying Team customer sees TEAM with Team administration unlocked', async const team = page.locator('#team-body'); await expect(team).toContainText('Your TEAM subscription includes this'); await expect(team).not.toContainText('does not include'); - await expect(team.getByRole('link', { name: 'Start 3-day Team trial' })).toHaveCount(0); + await expect(team.getByRole('link', { name: 'Start 10-day Team trial' })).toHaveCount(0); expect(errors).toEqual([]); }); @@ -456,7 +470,7 @@ test('a lapsed Team subscription is sent to billing, not to a spent trial', asyn await expect(licensePanel.getByRole('link', { name: 'Update billing' })) .toHaveAttribute('href', 'https://cloud.engraphis.test/account?utm_source=engraphis&utm_medium=product&utm_campaign=pro_conversion&utm_content=license'); // A lapsed subscription is a billing problem, not an unspent trial. - await expect(licensePanel.getByRole('link', { name: 'Start 3-day Team trial' })) + await expect(licensePanel.getByRole('link', { name: 'Start 10-day Team trial' })) .toHaveCount(0); await expect(licensePanel.getByRole('link', { name: 'Start 3-day Pro trial' })) .toHaveCount(0); @@ -580,7 +594,7 @@ test('An unconfigured local install starts the Cloud trial directly from either await openView(page, 'analytics'); const analytics = page.locator('#analytics-body'); await expect(analytics).toContainText( - 'Hosted insights and maintenance come on automatically—no settings, toggles, or worker setup.', + 'After connecting, explicitly approve each workspace in Manage > Settings.', ); await expect(analytics).not.toContainText('Connect this installation to Engraphis Cloud'); await expect(analytics.getByRole('link', { name: 'Start 3-day Pro trial' })) @@ -592,7 +606,7 @@ test('An unconfigured local install starts the Cloud trial directly from either await openView(page, 'automation'); const automation = page.locator('#automation-body'); await expect(automation).toContainText( - 'Hosted insights and maintenance come on automatically—no settings, toggles, or worker setup.', + 'After connecting, explicitly approve each workspace in Manage > Settings.', ); await expect(automation).not.toContainText('Connect this installation to Engraphis Cloud'); await expect(automation.getByRole('link', { name: 'Start 3-day Pro trial' })) @@ -614,7 +628,7 @@ test('Analytics turns an unconnected local installation into a Pro opportunity', const analytics = page.locator('#analytics-body'); await expect(analytics).toContainText('See the memory your team is about to lose.'); await expect(analytics).toContainText( - 'Hosted insights and maintenance come on automatically—no settings, toggles, or worker setup.' + 'After connecting, explicitly approve each workspace in Manage > Settings.' ); await expect(analytics).toContainText('Secret and session-scoped memories stay local.'); await expect(analytics).not.toContainText('ENGRAPHIS_MANAGED_COMPUTE_CONSENT'); @@ -645,7 +659,7 @@ test('Automation policy save presents the hosted-maintenance value when Cloud is const result = page.locator('#au-result'); await expect(result).toContainText('Let your memory improve after you log off.'); await expect(result).toContainText( - 'Hosted insights and maintenance come on automatically—no settings, toggles, or worker setup.' + 'After connecting, explicitly approve each workspace in Manage > Settings.' ); await expect(result).not.toContainText('ENGRAPHIS_MANAGED_COMPUTE_CONSENT'); await expect(result.getByRole('link', { name: 'Start 3-day Pro trial' })) @@ -669,8 +683,10 @@ test('A subscribed customer sees included hosted features, never a repurchase pr const analytics = page.locator('#analytics-body'); await expect(analytics).toContainText( - 'Hosted insights and maintenance are on by default—nothing else to configure.' + 'Readable managed processing is paused until you approve this workspace in Manage > Settings.' ); + await expect(analytics.getByRole('link', { name: 'Review workspace processing' })) + .toHaveAttribute('href', /\/\?view=manage&tab=settings&workspace=/); await expect(analytics.getByRole('link', { name: 'Open Engraphis Cloud' })) .toHaveAttribute('href', 'https://cloud.engraphis.test/account?utm_source=engraphis&utm_medium=product&utm_campaign=pro_conversion&utm_content=managed_analytics'); await expect(analytics.getByRole('link', { name: 'Subscribe to Pro' })).toHaveCount(0); diff --git a/tests/e2e/ledger.spec.js b/tests/e2e/ledger.spec.js index d9b65ab1..691bf4bd 100644 --- a/tests/e2e/ledger.spec.js +++ b/tests/e2e/ledger.spec.js @@ -29,7 +29,7 @@ function license() { cloud_access_active: false, access_state: 'inactive', plan_source: 'local', - trial: { used: false, active: false, available: true, trial_days: 3 }, + trial: { used: false, active: false, available: true, trial_days: 3, days_by_plan: { pro: 3, team: 10 } }, pro_upgrade_url: 'https://cloud.engraphis.test/pro', team_upgrade_url: 'https://cloud.engraphis.test/team', pro_monthly_upgrade_url: 'https://cloud.engraphis.test/account?plan=pro&interval=monthly#billing', @@ -49,6 +49,7 @@ async function mockApi(page, options = {}) { requests.documentImports = []; requests.contextSavingsQueries = []; requests.graphQueries = []; + requests.libraryQueries = []; const audit = options.audit || []; const receipts = options.receipts || []; const workspaceList = options.workspaces || [{ name: workspace, memories: memories.length }]; @@ -101,16 +102,26 @@ async function mockApi(page, options = {}) { } if (path === '/stats') { return ok({ - memories: memories.length, - total_rows: memories.length, + memories: memoriesFor(requestUrl).length, + total_rows: memoriesFor(requestUrl).length, workspaces: 1, sessions: 1, by_type: { semantic: 1, procedural: 1 }, }); } if (path === '/memories') { + requests.libraryQueries.push(Object.fromEntries(requestUrl.searchParams)); + const search = (requestUrl.searchParams.get('q') || '').toLowerCase(); + const type = requestUrl.searchParams.get('mtype'); + const matches = memoriesFor(requestUrl).filter(memory => + (!search || `${memory.title || ''} ${memory.content || ''}`.toLowerCase().includes(search)) + && (!type || (memory.memory_type || memory.mtype || 'semantic') === type)); + const offset = Number((requestUrl.searchParams.get('cursor') || 'page-0').replace('page-', '')); + const limit = Number(requestUrl.searchParams.get('limit') || 200); + const result = matches.slice(offset, offset + limit); return ok({ workspace: requestUrl.searchParams.get('workspace') || workspace, - memories: memoriesFor(requestUrl) }); + memories: result, count: result.length, total_count: matches.length, + next_cursor: offset + limit < matches.length ? `page-${offset + limit}` : null }); } if (path.startsWith('/memory/')) { const id = path.split('/').pop(); @@ -303,6 +314,246 @@ function browserErrors(page) { return errors; } +async function openProcessingSettings(page) { + await page.goto('/?view=manage'); + await page.locator('#manage-settings-tab').click(); +} + +test('A processing-controls link opens the named authorized workspace without enabling it', async ({ page }) => { + const selected = 'second workspace & review'; + const requests = await mockApi(page, { workspaces: [ + { name: workspace, memories: 20 }, { name: selected, memories: 1 }, + ] }); + await page.route('**/api/managed-processing**', route => route.fulfill({ + json: { enabled: false, confirmation_required: true }, + })); + await page.goto(`/?view=manage&tab=settings&workspace=${encodeURIComponent(selected)}`); + await expect(page.locator('#workspace-select')).toHaveValue(selected); + await expect(page.locator('#manage-settings-tab')).toHaveAttribute('aria-selected', 'true'); + await expect(page.locator('#managed-processing-status')).toContainText(`Managed processing is off for ${selected}.`); + await expect(page.locator('#managed-processing-approval')).not.toBeChecked(); + await expect(page.locator('#managed-processing-enable')).toBeDisabled(); + expect(requests.details.filter(item => item.method === 'POST')).toEqual([]); + + // An unknown name from a URL cannot select an unauthorized workspace. + await page.goto('/?view=manage&tab=settings&workspace=not-permitted'); + await expect(page.locator('#workspace-select')).not.toHaveValue('not-permitted'); + await expect(page.locator('#managed-processing-enable')).toBeDisabled(); +}); + +test('Managed processing stays off until explicit approval receives a backend acknowledgement', async ({ page }) => { + await mockApi(page); + let releaseApproval; + const writes = []; + await page.route('**/api/managed-processing**', async route => { + if (route.request().method() === 'POST') { + writes.push(route.request().postDataJSON()); + await new Promise(resolve => { releaseApproval = resolve; }); + return route.fulfill({ json: { enabled: true, remote_revision: 1, remote_sync_pending: false } }); + } + return route.fulfill({ json: { enabled: false, confirmation_required: true, operator_disabled: false } }); + }); + await openProcessingSettings(page); + await expect(page.locator('#managed-processing-status')).toContainText('Managed processing is off'); + await expect(page.locator('#managed-processing-enable')).toBeDisabled(); + expect(writes).toEqual([]); + await page.locator('#managed-processing-approval').check(); + await page.locator('#managed-processing-enable').click(); + await expect.poll(() => writes.length).toBe(1); + expect(writes[0]).toEqual({ workspace, enabled: true, confirmed: true }); + await expect(page.locator('#managed-processing-status')).toHaveText('Requesting workspace approval…'); + await page.locator('#managed-processing-approval').uncheck(); + await page.locator('#managed-processing-approval').check(); + await expect(page.locator('#managed-processing-enable')).toBeDisabled(); + await expect(page.locator('#managed-processing-disable')).toBeDisabled(); + releaseApproval(); + await expect(page.locator('#managed-processing-status')).toHaveText(`Managed processing is enabled for ${workspace}.`); + await expect(page.locator('#managed-processing-approval')).not.toBeChecked(); +}); + +test('Managed processing opt-out remains visibly pending until its retry is acknowledged', async ({ page }) => { + await mockApi(page); + const writes = []; + await page.route('**/api/managed-processing**', route => { + if (route.request().method() === 'POST') { + writes.push(route.request().postDataJSON()); + return route.fulfill({ json: { enabled: false, remote_sync_pending: writes.length === 1, remote_revision: 2 } }); + } + return route.fulfill({ json: { enabled: true, remote_sync_pending: false } }); + }); + await openProcessingSettings(page); + await page.locator('#managed-processing-disable').click(); + await expect(page.locator('#managed-processing-status')).toContainText('Uploads are paused locally. Cloud confirmation is pending'); + await expect(page.locator('#managed-processing-disable')).toBeEnabled(); + await page.locator('#managed-processing-disable').click(); + await expect(page.locator('#managed-processing-status')).toContainText('Managed processing is off'); + expect(writes).toEqual([{ workspace, enabled: false, confirmed: false }, { workspace, enabled: false, confirmed: false }]); +}); + +test('Managed processing ignores a previous workspace policy that arrives late', async ({ page }) => { + const next = 'processing-next'; + await mockApi(page, { workspaces: [{ name: workspace, memories: 2 }, { name: next, memories: 1 }] }); + let releasePrevious; + let previousDelivered = false; + await page.route('**/api/managed-processing**', async route => { + const selected = new URL(route.request().url()).searchParams.get('workspace'); + if (selected === workspace) { + await new Promise(resolve => { releasePrevious = resolve; }); + await route.fulfill({ json: { enabled: true } }).catch(() => {}); + previousDelivered = true; + return; + } + return route.fulfill({ json: { enabled: false, confirmation_required: true } }); + }); + await openProcessingSettings(page); + await expect.poll(() => typeof releasePrevious).toBe('function'); + await page.locator('#workspace-select').selectOption(next); + await expect(page.locator('#managed-processing-status')).toContainText(`Managed processing is off for ${next}.`); + releasePrevious(); + await expect.poll(() => previousDelivered).toBe(true); + await expect(page.locator('#managed-processing-status')).toContainText(`Managed processing is off for ${next}.`); + await expect(page.locator('#managed-processing-approval')).not.toBeChecked(); +}); + +test('Managed processing reload recovers from an unknown write outcome without silently retrying', async ({ page }) => { + await mockApi(page); + let writes = 0; + let reads = 0; + await page.route('**/api/managed-processing**', route => { + if (route.request().method() === 'POST') { + writes += 1; + return route.fulfill({ status: 503, json: { detail: 'Cloud acknowledgement unavailable' } }); + } + reads += 1; + return route.fulfill({ json: { enabled: false, confirmation_required: true } }); + }); + await openProcessingSettings(page); + await page.locator('#managed-processing-approval').check(); + await page.locator('#managed-processing-enable').click(); + await expect(page.locator('#managed-processing-status')).toContainText('Reload the policy to verify the outcome'); + await expect(page.locator('#managed-processing-enable')).toBeDisabled(); + await page.locator('#managed-processing-reload').click(); + await expect(page.locator('#managed-processing-status')).toContainText('Managed processing is off'); + expect(writes).toBe(1); + expect(reads).toBe(2); +}); + +test('Library searches beyond its first page and keeps server-side type filters while paging', async ({ page }) => { + const large = Array.from({ length: 1201 }, (_, index) => ({ + id: `mem_page_${index}`, title: `Project decision ${index}`, + content: index === 1200 ? 'The oldest unique migration decision' : `Decision ${index}`, + memory_type: index % 2 ? 'procedural' : 'semantic', + })); + const requests = await mockApi(page, { memoriesByWorkspace: { [workspace]: large } }); + await page.goto('/?view=library'); + await expect(page.locator('#library-list [role="option"]')).toHaveCount(100); + await expect(page.locator('#library-count')).toHaveText('1–100 of 1,201 memories'); + await page.locator('#library-next').click(); + await expect(page.locator('#library-count')).toHaveText('101–200 of 1,201 memories'); + await expect(page.locator('#library-list')).toContainText('Project decision 100'); + await page.locator('#library-previous').click(); + await expect(page.locator('#library-count')).toHaveText('1–100 of 1,201 memories'); + await page.locator('#library-filter').fill('oldest unique'); + await expect(page.locator('#library-count')).toHaveText('1 memory'); + await expect(page.locator('#library-list')).toContainText('Project decision 1200'); + await page.locator('#library-filter').fill(''); + await page.locator('#library-type').selectOption('procedural'); + await expect(page.locator('#library-count')).toHaveText('1–100 of 600 memories'); + await page.locator('#library-next').click(); + await expect(page.locator('#library-count')).toHaveText('101–200 of 600 memories'); + expect(requests.libraryQueries.at(-1).mtype).toBe('procedural'); + expect(requests.libraryQueries.at(-1).cursor).toBe('page-100'); +}); + +test('Library ignores a superseded server search even when it finishes last', async ({ page }) => { + await mockApi(page); + let releaseSlow; + let delivered = false; + await page.route('**/api/memories?**', async route => { + const search = new URL(route.request().url()).searchParams.get('q'); + if (search !== 'slow') return route.fallback(); + await new Promise(resolve => { releaseSlow = resolve; }); + await route.fulfill({ json: { memories: [memories[0]], total_count: 1, next_cursor: null } }).catch(() => {}); + delivered = true; + }); + await page.goto('/?view=library'); + await page.locator('#library-filter').fill('slow'); + await expect.poll(() => typeof releaseSlow).toBe('function'); + await page.locator('#library-filter').fill('safe'); + await expect(page.locator('#library-list')).toContainText('Safe rendering'); + releaseSlow(); + await expect.poll(() => delivered).toBe(true); + await expect(page.locator('#library-list')).not.toContainText('Database choice'); + await expect(page.locator('#library-list')).toContainText('Safe rendering'); +}); + +test('Library restarts a stale page without dropping its search', async ({ page }) => { + const large = Array.from({ length: 120 }, (_, index) => ({ + id: `mem_stale_${index}`, title: `Scoped decision ${index}`, content: 'Keep this query', + })); + await mockApi(page, { memoriesByWorkspace: { [workspace]: large } }); + await page.route('**/api/memories?**', route => { + if (!new URL(route.request().url()).searchParams.has('cursor')) return route.fallback(); + return route.fulfill({ status: 409, contentType: 'application/json', + body: JSON.stringify({ detail: { code: 'cursor_stale', error: 'Memory changed' } }) }); + }); + await page.goto('/?view=library'); + await page.locator('#library-filter').fill('Keep this query'); + await expect(page.locator('#library-next')).toBeEnabled(); + await page.locator('#library-next').click(); + await expect(page.locator('#notice-banner')).toContainText('Showing the first page'); + await expect(page.locator('#library-count')).toHaveText('1–100 of 120 memories'); + await expect(page.locator('#library-filter')).toHaveValue('Keep this query'); + await expect(page.locator('#library-previous')).toBeDisabled(); +}); + +for (const failed of ['answer', 'recall']) { + test(`Ask preserves the successful panel when ${failed} fails`, async ({ page }) => { + await mockApi(page); + await page.route(`**/api/${failed}${failed === 'recall' ? '?**' : ''}`, route => + route.fulfill({ status: 503, contentType: 'application/json', body: '{"detail":"Temporarily unavailable"}' })); + await page.goto('/?view=ask'); + await page.locator('#ask-input').fill('Which database?'); + await page.getByRole('button', { name: 'Grounded answer', exact: true }).click(); + if (failed === 'recall') { + await expect(page.locator('#answer-panel')).toContainText('Postgres 16 is the main database. [1]'); + await expect(page.locator('#retrieval-list')).toContainText('Raw retrieval is unavailable'); + } else { + await expect(page.locator('#answer-panel')).toContainText('Grounded Ask is unavailable'); + await expect(page.locator('#retrieval-list')).toContainText('Postgres 16 is the main database.'); + } + }); +} + +test('Ask paints a completed answer while its preview stalls, then reports the deadline', async ({ page }) => { + await page.clock.install(); + await mockApi(page); + let previewRequested = false; + await page.route('**/api/recall?**', () => { previewRequested = true; }); + await page.goto('/?view=ask'); + await page.locator('#ask-input').fill('Which database?'); + await page.getByRole('button', { name: 'Grounded answer', exact: true }).click(); + await expect.poll(() => previewRequested).toBe(true); + await expect(page.locator('#answer-panel')).toContainText('Postgres 16 is the main database. [1]'); + await page.clock.fastForward(31_000); + await expect(page.locator('#retrieval-list')).toContainText('The request timed out'); + await expect(page.locator('#answer-panel')).toContainText('Postgres 16 is the main database. [1]'); +}); + +test('First-run guidance opens workspace creation and the first memory editor', async ({ page }) => { + await mockApi(page, { workspaces: [] }); + await page.goto('/'); + await expect(page.locator('#first-memory-add')).toHaveText('Create your first workspace'); + await page.locator('#first-memory-add').click(); + await expect(page.locator('#new-workspace-name')).toBeFocused(); + await page.unroute('**/api/**'); + await mockApi(page, { memoriesByWorkspace: { [workspace]: [] }, workspaces: [{ name: workspace, memories: 0 }] }); + await page.goto('/?view=today'); + await expect(page.locator('#first-memory-add')).toHaveText('Add your first memory'); + await page.locator('#first-memory-add').click(); + await expect(page.locator('#editor-memory-title')).toBeFocused(); +}); + test('Ledger is live, safe, lazy, accessible, and responsive', async ({ page }) => { const errors = browserErrors(page); const assetRequests = []; @@ -1801,8 +2052,8 @@ test('Ledger exposes local LLM setup and extraction controls', async ({ page }) await expect(provider).toHaveValue('openai'); await expect(model).toHaveValue('gpt-4o-mini'); await expect(page.getByLabel('Local .env setup')).toHaveValue(/ENGRAPHIS_LLM_PROVIDER=openai/); - await expect(page.getByRole('button', { name: 'Turn on' })).toBeDisabled(); - await expect(page.getByRole('button', { name: 'Turn off' })).toBeDisabled(); + await expect(page.locator('#llm-connection').getByRole('button', { name: 'Turn on' })).toBeDisabled(); + await expect(page.locator('#llm-connection').getByRole('button', { name: 'Turn off' })).toBeDisabled(); await provider.selectOption('anthropic'); await expect(model).toHaveValue('claude-3-5-sonnet-20241022'); @@ -1826,7 +2077,7 @@ test('Ledger applies the configured LLM extraction toggle', async ({ page }) => await page.getByRole('button', { name: 'Manage' }).click(); await page.getByRole('tab', { name: 'Settings' }).click(); - const turnOn = page.getByRole('button', { name: 'Turn on' }); + const turnOn = page.locator('#llm-connection').getByRole('button', { name: 'Turn on' }); await expect(turnOn).toBeEnabled(); await expect(page.getByText(/Retention supervision is ON/)).toBeVisible(); await expect(page.getByText(/bounded excerpt/)).toBeVisible(); @@ -1837,9 +2088,9 @@ test('Ledger applies the configured LLM extraction toggle', async ({ page }) => }); await turnOn.click(); await expect(page.getByText('ON', { exact: true })).toBeVisible(); - await expect(page.getByRole('button', { name: 'Turn off' })).toBeEnabled(); + await expect(page.locator('#llm-connection').getByRole('button', { name: 'Turn off' })).toBeEnabled(); - await page.getByRole('button', { name: 'Turn off' }).click(); + await page.locator('#llm-connection').getByRole('button', { name: 'Turn off' }).click(); await expect(page.getByText('OFF', { exact: true })).toBeVisible(); await expect(page.getByText(/Retention supervision is ON/)).toBeVisible(); await expect(turnOn).toBeEnabled(); @@ -1921,6 +2172,18 @@ test('Ledger gives active Pro members direct Cloud access and saves hosted polic expect(errors).toEqual([]); }); +test('Ledger omits unknown Team trial duration from an older license response', async ({ page }) => { + const legacy = license(); + delete legacy.trial.days_by_plan; + await mockApi(page, { license: legacy }); + await page.goto('/'); + await page.getByRole('button', { name: 'Manage' }).click(); + await page.getByRole('tab', { name: 'Plans & billing' }).click(); + await expect(page.locator('#plan-cards [data-pro-cta="team"]')).toHaveText('Start Team trial'); + await expect(page.locator('#plan-cards [data-pro-cta="pro"]')).toHaveText('Start 3-day Pro trial'); + await expect(page.locator('#plan-cards [data-pro-cta="team"]')).toHaveAttribute('href', /trial=team/); +}); + test('billing cadence selects the exact Pro and Team checkout target', async ({ page }) => { await mockApi(page); await page.goto('/'); @@ -1953,6 +2216,8 @@ test('billing cadence selects the exact Pro and Team checkout target', async ({ const pro = page.locator('#plan-cards [data-pro-cta="pro"]'); const team = page.locator('#plan-cards [data-pro-cta="team"]'); + await expect(pro).toHaveText('Start 3-day Pro trial'); + await expect(team).toHaveText('Start 10-day Team trial'); await expect(pro).toHaveAttribute( 'href', 'https://cloud.engraphis.test/account?plan=pro&interval=monthly&trial=pro&utm_source=engraphis&utm_medium=product&utm_campaign=pro_conversion&utm_content=plans#billing', diff --git a/tests/e2e/workspace-smoke.spec.js b/tests/e2e/workspace-smoke.spec.js new file mode 100644 index 00000000..95156f7e --- /dev/null +++ b/tests/e2e/workspace-smoke.spec.js @@ -0,0 +1,48 @@ +const { test, expect } = require('@playwright/test'); + +test('a real local workspace saves, recalls, and corrects a memory with history', async ({ page, browserName }) => { + const errors = []; + page.on('pageerror', error => errors.push(error.message)); + page.on('dialog', dialog => dialog.accept(dialog.type() === 'prompt' ? 'Reviewed the local smoke-test fixture.' : undefined)); + const workspace = `browser-smoke-${browserName}-${Date.now()}`; + await page.goto('/#token=engraphis-playwright-local-only'); + await expect(page.locator('#connection-status')).toContainText('Local engine connected'); + await expect(page).not.toHaveURL(/token=/); + await page.locator('.nav-item[data-view="manage"]').click(); + await page.locator('#create-workspace-toggle').click(); + await page.locator('#new-workspace-name').fill(workspace); + await page.locator('#create-workspace-form button[type="submit"]').click(); + await expect(page.locator('#workspace-select')).toHaveValue(workspace); + await page.locator('.nav-item[data-view="today"]').click(); + await expect(page.locator('#first-memory-add')).toBeVisible(); + await page.locator('#first-memory-add').click(); + await expect(page.locator('#editor-memory-title')).toBeFocused(); + await page.locator('#editor-memory-title').fill('Smoke database decision'); + await page.locator('#editor-memory-content').fill('The smoke database retains temporal history.'); + await page.locator('#memory-editor button[type="submit"]').click(); + await expect(page.locator('#library-count')).toHaveText('1 memory'); + await page.locator('#library-list [role="option"]').press('Enter'); + await expect(page.locator('#memory-detail')).toContainText('human:ledger'); + await expect(page.locator('#memory-detail')).toHaveAccessibleName('Smoke database decision'); + await page.locator('#memory-detail').getByRole('button', { name: 'Approve for prompt…' }).click(); + await expect(page.locator('#memory-detail')).toContainText('approved'); + await page.locator('.nav-item[data-view="ask"]').click(); + await page.locator('#ask-input').fill('The smoke database retains temporal history.'); + await page.getByRole('button', { name: 'Grounded answer', exact: true }).click(); + await expect(page.locator('#retrieval-list')).toContainText('The smoke database retains temporal history.'); + await expect(page.locator('#answer-panel')).toContainText('The smoke database retains temporal history.'); + await page.locator('.retrieval-details summary').click(); + await page.locator('#retrieval-list button').first().click(); + await page.locator('#memory-detail').getByRole('button', { name: 'Edit', exact: true }).click(); + await page.locator('#editor-memory-content').fill('The smoke database retains verified temporal history.'); + await page.locator('#memory-editor button[type="submit"]').click(); + await expect(page.locator('#library-list')).toContainText('retains verified temporal history'); + await page.locator('#library-list [role="option"]').filter({ hasText: 'retains verified temporal history' }).press('Enter'); + await expect(page.locator('#memory-detail')).toContainText('Supersession chain'); + await page.setViewportSize({ width: 390, height: 844 }); + expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); + await page.reload(); + await expect(page.locator('#workspace-select')).toHaveValue(workspace); + await expect(page.locator('#library-list')).toContainText('retains verified temporal history'); + expect(errors).toEqual([]); +}); diff --git a/tests/test_cloud_features.py b/tests/test_cloud_features.py index 2e621a63..cfff6242 100644 --- a/tests/test_cloud_features.py +++ b/tests/test_cloud_features.py @@ -20,7 +20,7 @@ from engraphis.service import MemoryService, set_current_user -def _service() -> MemoryService: +def _service(*, approved: bool = True) -> MemoryService: service = MemoryService.create(":memory:") service.remember( "A normal managed-compute memory.", @@ -36,6 +36,8 @@ def _service() -> MemoryService: "password=do-not-upload", secret["id"]), ) service.store.conn.commit() + if approved: + service.set_managed_processing_policy("acme", enabled=True, confirmed=True, remote_revision=2) return service @@ -52,86 +54,49 @@ def configured(**kwargs): return calls -def test_a_local_installation_with_no_cloud_account_never_uploads(monkeypatch) -> None: - """No account means no agreement to rely on, so a purely local install is denied.""" - +def test_missing_workspace_approval_never_uploads(monkeypatch) -> None: monkeypatch.delenv("ENGRAPHIS_MANAGED_COMPUTE_CONSENT", raising=False) - _cloud_session(monkeypatch, connected=False) - - assert cloud_features.managed_compute_consent() is False - with pytest.raises( - CloudFeatureError, match="Managed compute is turned off" - ) as captured: - build_managed_snapshot(_service(), "acme") - + service = _service(approved=False) + assert cloud_features.managed_compute_consent(service, "acme") is False + with pytest.raises(CloudFeatureError, match="approval for this workspace") as captured: + build_managed_snapshot(service, "acme", consent=True) assert captured.value.status == 409 assert captured.value.code == "consent_required" - # The customer is never told to hand-edit an environment variable. - assert "ENGRAPHIS_MANAGED_COMPUTE_CONSENT" not in str(captured.value) - - -def test_connecting_to_the_cloud_is_itself_the_managed_compute_consent( - monkeypatch, -) -> None: - """Connecting accepts the terms covering managed compute; nothing else is asked for.""" - - monkeypatch.delenv("ENGRAPHIS_MANAGED_COMPUTE_CONSENT", raising=False) - calls = _cloud_session(monkeypatch, connected=True) - - assert cloud_features.managed_compute_consent() is True - - _, snapshot = build_managed_snapshot(_service(), "acme") - assert snapshot["managed_compute_consent"] is True - # A session without a compute URL is still an account that accepted the terms. - assert calls and all(call == {"require_compute": False} for call in calls) - - -def test_a_connected_installation_can_be_opted_back_out(monkeypatch) -> None: - """``=0`` is the operator override that withdraws a connected installation.""" - - monkeypatch.setenv("ENGRAPHIS_MANAGED_COMPUTE_CONSENT", "0") - _cloud_session(monkeypatch, connected=True) - - assert cloud_features.managed_compute_consent() is False - with pytest.raises(CloudFeatureError, match="Managed compute is turned off"): - build_managed_snapshot(_service(), "acme") - -@pytest.mark.parametrize("blank", ["", " "]) -def test_a_blank_override_defers_to_the_cloud_account(monkeypatch, blank) -> None: - """An empty variable is not an answer — it must not read as an opt-out.""" - monkeypatch.setenv("ENGRAPHIS_MANAGED_COMPUTE_CONSENT", blank) +def test_connecting_or_environment_cannot_grant_workspace_approval(monkeypatch) -> None: _cloud_session(monkeypatch, connected=True) - assert cloud_features.managed_compute_consent() is True - - _cloud_session(monkeypatch, connected=False) + monkeypatch.setenv("ENGRAPHIS_MANAGED_COMPUTE_CONSENT", "yes") + service = _service(approved=False) assert cloud_features.managed_compute_consent() is False + assert cloud_features.managed_compute_consent(service, "acme") is False + with pytest.raises(CloudFeatureError, match="approval for this workspace"): + build_managed_snapshot(service, "acme", consent=True) -def test_consent_is_never_the_reason_a_dashboard_fails_to_render(monkeypatch) -> None: - """An unreadable session file denies consent instead of raising into the view.""" - - monkeypatch.delenv("ENGRAPHIS_MANAGED_COMPUTE_CONSENT", raising=False) - - def explode(**kwargs): - raise OSError("session state is unreadable") - - monkeypatch.setattr(cloud_features, "cloud_session_configured", explode) - - assert cloud_features.managed_compute_consent() is False - +def test_operator_override_can_only_deny_workspace_approval(monkeypatch) -> None: + service = _service() + monkeypatch.setenv("ENGRAPHIS_MANAGED_COMPUTE_CONSENT", "0") + assert cloud_features.managed_compute_consent(service, "acme") is False + with pytest.raises(CloudFeatureError, match="approval for this workspace"): + build_managed_snapshot(service, "acme") -def test_snapshot_accepts_environment_opt_in(monkeypatch) -> None: - """A truthy override forces consent on even with no cloud session at all.""" - monkeypatch.setenv("ENGRAPHIS_MANAGED_COMPUTE_CONSENT", "yes") - _cloud_session(monkeypatch, connected=False) +@pytest.mark.parametrize("override", ["", " ", "yes"]) +def test_explicit_workspace_approval_enables_snapshot(monkeypatch, override) -> None: + monkeypatch.setenv("ENGRAPHIS_MANAGED_COMPUTE_CONSENT", override) + service = _service() + assert cloud_features.managed_compute_consent(service, "acme") is True + _, snapshot = build_managed_snapshot(service, "acme") + assert snapshot["managed_compute_consent"] is True + assert snapshot["processing_policy_revision"] == 2 - _, snapshot = build_managed_snapshot(_service(), "acme") - assert cloud_features.managed_compute_consent() is True - assert snapshot["managed_compute_consent"] is True +def test_policy_read_failure_denies_processing() -> None: + class Broken: + def _clean_ws(self, workspace): + raise OSError("unreadable local state") + assert cloud_features.managed_compute_consent(Broken(), "acme") is False @pytest.mark.parametrize("status", [401, 403, 409, 503]) @@ -198,7 +163,7 @@ def test_direct_cloud_client_rejects_header_control_characters(monkeypatch) -> N def test_explicit_false_consent_cannot_be_overridden_by_environment(monkeypatch) -> None: monkeypatch.setenv("ENGRAPHIS_MANAGED_COMPUTE_CONSENT", "1") - with pytest.raises(CloudFeatureError, match="Managed compute is turned off"): + with pytest.raises(CloudFeatureError, match="approval for this workspace"): build_managed_snapshot(_service(), "acme", consent=False) @@ -245,6 +210,7 @@ def test_snapshot_fails_closed_on_unknown_sensitivity() -> None: def test_snapshot_excludes_pending_and_quarantined_memory() -> None: service = MemoryService.create(":memory:") approved = service.remember("Approved release fact.", workspace="acme") + service.set_managed_processing_policy("acme", enabled=True, confirmed=True) pending = service.remember("Pending imported fact.", workspace="acme") quarantined = service.remember("Quarantined imported fact.", workspace="acme") service.store.conn.execute( @@ -288,6 +254,7 @@ def test_snapshot_excludes_pending_and_quarantined_memory() -> None: def test_workspace_snapshot_never_uploads_session_scoped_content() -> None: service = MemoryService.create(":memory:") service.remember("shared seed", workspace="acme") + service.set_managed_processing_policy("acme", enabled=True, confirmed=True) try: set_current_user({ "id": "usr_alice", "email": "alice@example.test", "role": "member", diff --git a/tests/test_context_economy.py b/tests/test_context_economy.py index c3a34f0f..98579ac2 100644 --- a/tests/test_context_economy.py +++ b/tests/test_context_economy.py @@ -245,7 +245,8 @@ def test_codemem_public_break_even_baseline_is_reproducible() -> None: } assert tight["methods"]["full_history"]["cumulative_query_context_tokens"] == 1180 assert tight["methods"]["recency_window"]["cumulative_query_context_tokens"] == 1180 - assert tight["methods"]["engraphis"]["cumulative_query_context_tokens"] == 1064 + # Complete-unit omission no longer spends the final tokens on partial claims. + assert tight["methods"]["engraphis"]["cumulative_query_context_tokens"] == 1031 assert roomy["methods"]["engraphis"]["cumulative_query_context_tokens"] == 1066 for report in (tight, roomy): for method in report["methods"].values(): @@ -263,5 +264,5 @@ def test_codemem_public_break_even_baseline_is_reproducible() -> None: report["methods"]["engraphis"]["quality"] == report["methods"]["full_history"]["quality"] ) - assert tight["engraphis_vs_full_history"]["break_even_query_count"] == 142 + assert tight["engraphis_vs_full_history"]["break_even_query_count"] == 111 assert roomy["engraphis_vs_full_history"]["break_even_query_count"] == 144 diff --git a/tests/test_context_evidence_preservation.py b/tests/test_context_evidence_preservation.py new file mode 100644 index 00000000..d7eef088 --- /dev/null +++ b/tests/test_context_evidence_preservation.py @@ -0,0 +1,165 @@ +"""Adversarial claim changes must survive context compression.""" +import pytest + +from engraphis.core.context import DeterministicContextPacker +from engraphis.core.interfaces import Candidate, MemoryRecord + + +def _candidate(memory_id, content, *, score=1.0, summary=""): + return Candidate( + id=memory_id, score=score, arm="lexical", + record=MemoryRecord(id=memory_id, content=content, summary=summary), + ) + + +@pytest.mark.parametrize("left,right", [ + ("30 days", "90 days"), + ("30 seconds", "30 minutes"), + ("2026-09-01", "2026-09-02"), + ("production", "staging"), + ("ServiceAlpha", "ServiceBeta"), + ("ServiceAlpha", "servicealpha"), + ("0.5", "-0.5"), + ("10.5", "10,5"), + ("enabled", "disabled"), + ("not authorized", "authorized"), + ("unless the service is unavailable", "unless the test suite passes"), + ("except production", "except staging"), +]) +@pytest.mark.parametrize("reverse", [False, True]) +def test_changed_claim_is_never_removed_as_a_near_duplicate(left, right, reverse): + template = "The operational policy requires the service to record the setting as {}." + texts = [template.format(left), template.format(right)] + candidates = [ + _candidate("mem_a", texts[0], score=0.95), + _candidate("mem_b", texts[1], score=0.90), + ] + if reverse: + candidates.reverse() + result = DeterministicContextPacker().pack("operational policy setting", candidates, 500) + assert {chunk.id for chunk in result.chunks} == {"mem_a", "mem_b"} + assert all(text in result.context for text in texts) + + +@pytest.mark.parametrize("summary", [ + "Production deploys are allowed unless staging tests pass.", + "Staging deploys are allowed unless production tests pass.", + "Production deploys are allowed unless production tests fail.", +]) +def test_summary_cannot_change_a_condition_binding(summary): + content = "Production deploys are allowed unless production tests pass." + result = DeterministicContextPacker().pack( + "production deploys", [_candidate("mem_policy", content, summary=summary)], 100, + ) + assert result.chunks[0].excerpt == content + assert result.chunks[0].reason == "full" + + +def test_summary_cannot_replace_numerical_claim_with_different_value(): + result = DeterministicContextPacker().pack( + "retention days", + [_candidate("mem_retention", "Logs remain available for 90 days.", + summary="Logs remain available for 30 days.")], 100, + ) + assert result.chunks[0].excerpt == "Logs remain available for 90 days." + + +@pytest.mark.parametrize("content", [ + "Production deployments are permitted unless the operator has rejected the release.", + "Production deployments are permitted only after the operator verifies the backup.", + "The retention duration for production audit logs is 90 days.", + "The permitted timeout is 30 minutes for the production worker.", +]) +def test_tight_budget_never_keeps_a_partial_qualified_or_numerical_claim(content): + packer = DeterministicContextPacker() + candidate = _candidate("mem_policy", content) + for budget in range(1, packer.count_tokens(content) + 5): + result = packer.pack("production policy", [candidate], budget) + assert result.usage.context_tokens <= budget + assert not result.chunks or result.chunks[0].excerpt == content + + +def test_shared_clause_and_new_following_clause_keep_their_source_binding(): + common = "Deployments require approval from the release owner." + novel = "The backup retention duration is 90 days." + result = DeterministicContextPacker().pack( + "deployment backup policy", + [_candidate("mem_a", common, score=1.5), + _candidate("mem_b", common + " " + novel, score=0.9)], + 100, + ) + assert result.context.count(common) == 2 + assert novel in result.context + assert result.chunks[1].excerpt == common + " " + novel + assert result.chunks[1].reason == "full" + + +@pytest.mark.parametrize("content", [ + "Le déploiement est autorisé sauf en environnement de production.", + "允许部署到测试环境,除非操作员拒绝此次发布。", + "El despliegue está permitido salvo en el entorno de producción.", + "The artifact destination is the private production registry.", + "Release authorization belongs to ServiceAlpha in the staging environment.", +]) +def test_multilingual_or_subject_tail_is_never_removed_by_prefix_fitting(content): + packer = DeterministicContextPacker(token_counter=len, token_counter_identity="test.characters") + candidate = _candidate("mem_multilingual", content) + for budget in (8, len(content) // 2, len(content) - 1, len(content) + 8): + result = packer.pack("deployment", [candidate], budget) + assert result.usage.context_tokens <= budget + assert not result.chunks or result.chunks[0].excerpt == content + + +def test_semicolon_condition_remains_bound_to_its_governing_claim(): + first = "Deployments may proceed; production requires operator approval." + second = "Deployments may proceed; staging requires operator approval." + result = DeterministicContextPacker().pack( + "deployment approval", [_candidate("mem_a", first), _candidate("mem_b", second)], 100, + ) + assert [chunk.excerpt for chunk in result.chunks] == [first, second] + + +@pytest.mark.parametrize("reverse", [False, True]) +def test_identical_bodies_retain_distinct_environment_titles_and_citations(reverse): + content = "Logs remain available for 30 days." + candidates = [ + _candidate("mem_production", content, score=0.95), + _candidate("mem_staging", content, score=0.90), + ] + candidates[0].record.title = "Production log retention" + candidates[1].record.title = "Staging log retention" + if reverse: + candidates.reverse() + + result = DeterministicContextPacker().pack( + "production and staging log retention", candidates, 500, + ) + + assert {chunk.id for chunk in result.chunks} == {"mem_production", "mem_staging"} + assert "[1] Production log retention\n" + content in result.context + assert "[2] Staging log retention\n" + content in result.context + + +def test_repeated_clause_retains_its_own_subject_and_condition_binding(): + first = "Production uses Atlas. It retains logs for 30 days." + second = "Staging uses Boreal. It retains logs for 30 days. Except during incident response." + result = DeterministicContextPacker().pack( + "production staging log retention", + [_candidate("mem_production", first), _candidate("mem_staging", second)], 500, + ) + + assert {chunk.id: chunk.excerpt for chunk in result.chunks} == { + "mem_production": first, "mem_staging": second, + } + + +def test_independent_sources_keep_separate_citations_for_identical_evidence(): + content = "The default request timeout is 30 seconds." + candidates = [_candidate("mem_config", content), _candidate("mem_test", content)] + candidates[0].record.provenance = {"source": "repo/config.py"} + candidates[1].record.provenance = {"source": "repo/tests/test_config.py"} + result = DeterministicContextPacker().pack("request timeout evidence", candidates, 500) + + assert [chunk.id for chunk in result.chunks] == ["mem_config", "mem_test"] + assert result.context.count(content) == 2 + assert result.usage.packed_count == 2 diff --git a/tests/test_context_packer.py b/tests/test_context_packer.py index 280f3427..6634b361 100644 --- a/tests/test_context_packer.py +++ b/tests/test_context_packer.py @@ -1,242 +1,247 @@ -"""Focused contracts for DeterministicContextPacker, clause redundancy pruning, and score-elbow gating.""" - -from __future__ import annotations - -from typing import Optional - -from engraphis.core.context import ( - ContextPackResult, - DeterministicContextPacker, - pack_context, -) -from engraphis.core.interfaces import Candidate, MemoryRecord -from tests.test_context_packing import * # noqa: F401, F403 - - -def _candidate_item( - memory_id: str, - content: str, - *, - score: float = 1.0, - arm: str = "semantic", - title: str = "Deployment Policy", - summary: str = "", - metadata: Optional[dict[str, object]] = None, -) -> Candidate: - return Candidate( - id=memory_id, - score=score, - arm=arm, - record=MemoryRecord( - id=memory_id, - title=title, - content=content, - summary=summary, - repo_id="repo_demo", - metadata=metadata or {}, - ), - ) - - -def test_context_pack_result_tuple_contract_and_attributes() -> None: - packer = DeterministicContextPacker() - c1 = _candidate_item("mem_1", "Primary deployment rules.") - res = packer.pack("deploy", [c1], token_budget=50) - - context, chunks, usage = res - assert isinstance(res, tuple) - assert len(res) == 3 - assert res[0] == context - assert res[1] == chunks - assert res[2] == usage - - assert res.context == context - assert res.chunks == chunks - assert res.packed_chunks == chunks - assert res.packed == chunks - assert res.usage == usage - - -def test_pack_context_functional_api_and_method_alias() -> None: - c1 = _candidate_item("mem_1", "Primary deployment rules.") - res1 = pack_context("deploy", [c1], token_budget=50) - assert isinstance(res1, ContextPackResult) - assert res1.chunks[0].id == "mem_1" - - packer = DeterministicContextPacker() - res2 = packer.pack_context("deploy", [c1], token_budget=50) - assert res1 == res2 - - -def test_inter_candidate_clause_redundancy_pruning_packs_novel_delta() -> None: - packer = DeterministicContextPacker() - # Candidate 1 establishes the rule - c1 = _candidate_item( - "mem_primary", - "Production deployments require approval from the release manager before rollout. " - "Database migrations must run during the off-peak maintenance window.", - score=0.95, - title="Production Deployment Guide", - ) - # Candidate 2 duplicates sentence 1 verbatim, but adds a novel sentence - c2 = _candidate_item( - "mem_checklist", - "Production deployments require approval from the release manager before rollout. " - "Canary analysis must run for 30 minutes before full promotion.", - score=0.85, - title="Release Checklist", - ) - - context, chunks, usage = packer.pack("deployment policy", [c1, c2], token_budget=150) - - assert len(chunks) == 2 - assert chunks[0].id == "mem_primary" - assert chunks[1].id == "mem_checklist" - - assert "Production deployments require approval" in chunks[0].excerpt - assert "Database migrations must run" in chunks[0].excerpt - - assert "Canary analysis must run for 30 minutes" in chunks[1].excerpt - assert "Production deployments require approval" not in chunks[1].excerpt - assert chunks[1].truncated is True - assert chunks[1].reason == "novel_delta" - - assert context.count("Production deployments require approval") == 1 - assert "[2] Release Checklist\nCanary analysis must run" in context - - -def test_completely_redundant_candidate_is_omitted() -> None: - packer = DeterministicContextPacker() - c1 = _candidate_item( - "mem_first", - "Production deployments require approval from the release manager before rollout.", - score=0.95, - title="Release Rule", - ) - c2 = _candidate_item( - "mem_second", - "Production deployments require approval from the release manager before rollout.", - score=0.90, - title="Duplicate Rule", - ) - - context, chunks, usage = packer.pack("deployment approval", [c1, c2], token_budget=100) - - assert len(chunks) == 1 - assert chunks[0].id == "mem_first" - assert "[2]" not in context - assert usage.packed_count == 1 - assert usage.omitted_count == 1 - - -def test_redundancy_pruning_preserves_qualifier_modifications() -> None: - packer = DeterministicContextPacker() - c1 = _candidate_item( - "mem_base", - "Production deployments require approval from the release manager before rollout.", - score=0.95, - ) - c2 = _candidate_item( - "mem_exception", - "Production deployments require approval from the release manager before rollout, " - "unless an emergency hotfix is authorized by the CTO.", - score=0.88, - title="Emergency Override", - ) - - context, chunks, usage = packer.pack("deployment approval", [c1, c2], token_budget=150) - - assert len(chunks) == 2 - assert "unless an emergency hotfix is authorized by the CTO" in chunks[1].excerpt - - -def test_elastic_score_elbow_gating_prunes_low_confidence_tail() -> None: - packer = DeterministicContextPacker() - c1 = _candidate_item( - "mem_high1", - "Rollout window is between 02:00 and 04:00 UTC.", - score=0.95, - title="Window", - ) - c2 = _candidate_item( - "mem_high2", - "Rollout team must be on call during the window.", - score=0.90, - title="Team", - ) - c3 = _candidate_item( - "mem_tail1", - "Random unrelated note mentioning deploy casually.", - score=0.12, - title="Unrelated 1", - ) - c4 = _candidate_item( - "mem_tail2", - "Another noisy mention from months ago.", - score=0.08, - title="Unrelated 2", - ) - - context, chunks, usage = packer.pack( - "rollout window", [c1, c2, c3, c4], token_budget=200 - ) - - assert [chunk.id for chunk in chunks] == ["mem_high1", "mem_high2"] - assert usage.packed_count == 2 - assert usage.omitted_count == 2 - - -def test_elastic_score_elbow_preserves_gradual_score_decline() -> None: - packer = DeterministicContextPacker() - candidates = [ - _candidate_item("mem_1", "Primary fact Alpha.", score=0.90, title="Alpha"), - _candidate_item("mem_2", "Secondary fact Beta.", score=0.78, title="Beta"), - _candidate_item("mem_3", "Tertiary fact Gamma.", score=0.68, title="Gamma"), - ] - - _, chunks, usage = packer.pack("fact inquiry", candidates, token_budget=150) - - assert [chunk.id for chunk in chunks] == ["mem_1", "mem_2", "mem_3"] - assert usage.packed_count == 3 - - -def test_elastic_score_elbow_preserves_bridge_arm_evidence() -> None: - packer = DeterministicContextPacker() - c1 = _candidate_item( - "mem_vector", - "Generic architecture notes.", - score=0.85, - arm="semantic", - title="Notes", - ) - c2 = _candidate_item( - "mem_bridge", - "Service auth calls database cluster directly.", - score=0.32, - arm="graph", - title="Dependency Graph", - ) - - _, chunks, usage = packer.pack( - "why dependency path between auth and database", - [c1, c2], - token_budget=100, - ) - - assert any(chunk.id == "mem_bridge" for chunk in chunks) - - -def test_toggling_redundancy_pruning_and_elbow_gating_flags() -> None: - unpruned_packer = DeterministicContextPacker(redundancy_pruning=False) - c1 = _candidate_item("mem_1", "Deployments must pass all checks.", score=0.95) - c2 = _candidate_item("mem_2", "Deployments must pass all checks.", score=0.90) - - _, chunks_unpruned, _ = unpruned_packer.pack("deploy checks", [c1, c2], token_budget=100) - assert len(chunks_unpruned) == 2 - - ungated_packer = DeterministicContextPacker(score_elbow_gating=False) - c_high = _candidate_item("mem_h", "High relevance fact.", score=0.95) - c_tail = _candidate_item("mem_t", "Tail fact.", score=0.10) - - _, chunks_ungated, _ = ungated_packer.pack("relevance", [c_high, c_tail], token_budget=100) - assert len(chunks_ungated) == 2 +"""Focused contracts for cited evidence preservation and score-elbow gating.""" + +from __future__ import annotations + +from typing import Optional + +from engraphis.core.context import ( + ContextPackResult, + DeterministicContextPacker, + pack_context, +) +from engraphis.core.interfaces import Candidate, MemoryRecord +from tests.test_context_packing import * # noqa: F401, F403 + + +def _candidate_item( + memory_id: str, + content: str, + *, + score: float = 1.0, + arm: str = "semantic", + title: str = "Deployment Policy", + summary: str = "", + metadata: Optional[dict[str, object]] = None, +) -> Candidate: + return Candidate( + id=memory_id, + score=score, + arm=arm, + record=MemoryRecord( + id=memory_id, + title=title, + content=content, + summary=summary, + repo_id="repo_demo", + metadata=metadata or {}, + ), + ) + + +def test_context_pack_result_tuple_contract_and_attributes() -> None: + packer = DeterministicContextPacker() + c1 = _candidate_item("mem_1", "Primary deployment rules.") + res = packer.pack("deploy", [c1], token_budget=50) + + context, chunks, usage = res + assert isinstance(res, tuple) + assert len(res) == 3 + assert res[0] == context + assert res[1] == chunks + assert res[2] == usage + + assert res.context == context + assert res.chunks == chunks + assert res.packed_chunks == chunks + assert res.packed == chunks + assert res.usage == usage + + +def test_pack_context_functional_api_and_method_alias() -> None: + c1 = _candidate_item("mem_1", "Primary deployment rules.") + res1 = pack_context("deploy", [c1], token_budget=50) + assert isinstance(res1, ContextPackResult) + assert res1.chunks[0].id == "mem_1" + + packer = DeterministicContextPacker() + res2 = packer.pack_context("deploy", [c1], token_budget=50) + assert res1 == res2 + + +def test_shared_clause_keeps_its_own_citation_and_surrounding_evidence() -> None: + packer = DeterministicContextPacker() + # Candidate 1 establishes the rule + c1 = _candidate_item( + "mem_primary", + "Production deployments require approval from the release manager before rollout. " + "Database migrations must run during the off-peak maintenance window.", + score=0.95, + title="Production Deployment Guide", + ) + # Candidate 2 duplicates sentence 1 verbatim, but adds a novel sentence + c2 = _candidate_item( + "mem_checklist", + "Production deployments require approval from the release manager before rollout. " + "Canary analysis must run for 30 minutes before full promotion.", + score=0.85, + title="Release Checklist", + ) + + context, chunks, usage = packer.pack("deployment policy", [c1, c2], token_budget=150) + + assert len(chunks) == 2 + assert chunks[0].id == "mem_primary" + assert chunks[1].id == "mem_checklist" + + assert "Production deployments require approval" in chunks[0].excerpt + assert "Database migrations must run" in chunks[0].excerpt + + assert "Canary analysis must run for 30 minutes" in chunks[1].excerpt + assert chunks[1].excerpt == c2.record.content + assert chunks[1].truncated is False + assert chunks[1].reason == "full" + + assert context.count("Production deployments require approval") == 2 + assert "[2] Release Checklist\nProduction deployments require approval" in context + + +def test_identical_text_with_distinct_titles_keeps_both_sources() -> None: + packer = DeterministicContextPacker() + c1 = _candidate_item( + "mem_first", + "Production deployments require approval from the release manager before rollout.", + score=0.95, + title="Release Rule", + ) + c2 = _candidate_item( + "mem_second", + "Production deployments require approval from the release manager before rollout.", + score=0.90, + title="Duplicate Rule", + ) + + context, chunks, usage = packer.pack("deployment approval", [c1, c2], token_budget=100) + + assert [chunk.id for chunk in chunks] == ["mem_first", "mem_second"] + assert "[2] Duplicate Rule" in context + assert usage.packed_count == 2 + assert usage.omitted_count == 0 + + +def test_redundancy_pruning_preserves_qualifier_modifications() -> None: + packer = DeterministicContextPacker() + c1 = _candidate_item( + "mem_base", + "Production deployments require approval from the release manager before rollout.", + score=0.95, + ) + c2 = _candidate_item( + "mem_exception", + "Production deployments require approval from the release manager before rollout, " + "unless an emergency hotfix is authorized by the CTO.", + score=0.88, + title="Emergency Override", + ) + + context, chunks, usage = packer.pack("deployment approval", [c1, c2], token_budget=150) + + assert len(chunks) == 2 + assert "unless an emergency hotfix is authorized by the CTO" in chunks[1].excerpt + + +def test_elastic_score_elbow_gating_prunes_low_confidence_tail() -> None: + packer = DeterministicContextPacker() + c1 = _candidate_item( + "mem_high1", + "Rollout window is between 02:00 and 04:00 UTC.", + score=0.95, + title="Window", + ) + c2 = _candidate_item( + "mem_high2", + "Rollout team must be on call during the window.", + score=0.90, + title="Team", + ) + c3 = _candidate_item( + "mem_tail1", + "Random unrelated note mentioning deploy casually.", + score=0.12, + title="Unrelated 1", + ) + c4 = _candidate_item( + "mem_tail2", + "Another noisy mention from months ago.", + score=0.08, + title="Unrelated 2", + ) + + context, chunks, usage = packer.pack( + "rollout window", [c1, c2, c3, c4], token_budget=200 + ) + + assert [chunk.id for chunk in chunks] == ["mem_high1", "mem_high2"] + assert usage.packed_count == 2 + assert usage.omitted_count == 2 + + +def test_elastic_score_elbow_preserves_gradual_score_decline() -> None: + packer = DeterministicContextPacker() + candidates = [ + _candidate_item("mem_1", "Primary fact Alpha.", score=0.90, title="Alpha"), + _candidate_item("mem_2", "Secondary fact Beta.", score=0.78, title="Beta"), + _candidate_item("mem_3", "Tertiary fact Gamma.", score=0.68, title="Gamma"), + ] + + _, chunks, usage = packer.pack("fact inquiry", candidates, token_budget=150) + + assert [chunk.id for chunk in chunks] == ["mem_1", "mem_2", "mem_3"] + assert usage.packed_count == 3 + + +def test_elastic_score_elbow_preserves_bridge_arm_evidence() -> None: + packer = DeterministicContextPacker() + c1 = _candidate_item( + "mem_vector", + "Generic architecture notes.", + score=0.85, + arm="semantic", + title="Notes", + ) + c2 = _candidate_item( + "mem_bridge", + "Service auth calls database cluster directly.", + score=0.32, + arm="graph", + title="Dependency Graph", + ) + + _, chunks, usage = packer.pack( + "why dependency path between auth and database", + [c1, c2], + token_budget=100, + ) + + assert any(chunk.id == "mem_bridge" for chunk in chunks) + + +def test_toggling_redundancy_pruning_and_elbow_gating_flags() -> None: + unpruned_packer = DeterministicContextPacker(redundancy_pruning=False) + c1 = _candidate_item("mem_1", "Deployments must pass all checks.", score=0.95) + c2 = _candidate_item("mem_2", "Deployments must pass all checks.", score=0.90) + + _, chunks_unpruned, _ = unpruned_packer.pack("deploy checks", [c1, c2], token_budget=100) + assert len(chunks_unpruned) == 2 + # The legacy pruning flag remains accepted, but never removes evidence + # from a distinct cited source merely because its text is identical. + _, chunks_compatible, _ = DeterministicContextPacker(redundancy_pruning=True).pack( + "deploy checks", [c1, c2], token_budget=100, + ) + assert chunks_compatible == chunks_unpruned + + ungated_packer = DeterministicContextPacker(score_elbow_gating=False) + c_high = _candidate_item("mem_h", "High relevance fact.", score=0.95) + c_tail = _candidate_item("mem_t", "Tail fact.", score=0.10) + + _, chunks_ungated, _ = ungated_packer.pack("relevance", [c_high, c_tail], token_budget=100) + assert len(chunks_ungated) == 2 diff --git a/tests/test_context_packing.py b/tests/test_context_packing.py index 30017b07..9a3b8442 100644 --- a/tests/test_context_packing.py +++ b/tests/test_context_packing.py @@ -75,7 +75,7 @@ def test_unfit_header_does_not_block_a_later_compact_source() -> None: assert usage.context_tokens <= 6 -def test_compact_header_retries_use_the_compact_budget_with_non_additive_counter() -> None: +def test_nonadditive_counter_omits_a_claim_that_cannot_fit_whole() -> None: class NonAdditiveCounter: identity = "test.non_additive" @@ -101,7 +101,8 @@ def __call__(self, text: str) -> int: "titled evidence", [candidate], token_budget=7, ) - assert chunks + assert context == "" + assert chunks == [] assert usage.context_tokens == packer.count_tokens(context) assert usage.context_tokens <= usage.budget_tokens == 7 @@ -168,8 +169,8 @@ def __call__(self, text: str) -> int: context, chunks, usage = packer.pack("alpha", [candidate], token_budget=3) - assert context.startswith("[1]\nAlpha") - assert chunks[0].excerpt.startswith("Alpha") + assert context == "" + assert chunks == [] assert usage.context_tokens <= usage.budget_tokens == 3 @@ -215,8 +216,10 @@ def test_sentence_aligned_summary_excerpt_leaves_room_for_more_evidence() -> Non _candidate( "mem_rollout", "The release ledger records extensive historical rollout details. " - "Platform Reliability owns deployment evidence and maintains the signed " - "release ledger with the complete verification record for every rollout.", + "Platform Reliability owns deployment evidence. " + "The signed release ledger keeps the complete verification record for " + "every production rollout and post-release review, including approvals, " + "rollbacks, and independently signed audit receipts.", score=1.0, title="", summary=( @@ -314,7 +317,7 @@ def test_tight_budget_does_not_strip_other_negative_qualifiers(qualifier: str) - assert usage.packed_count == 0 -def test_custom_counter_can_truncate_inside_one_regex_token() -> None: +def test_custom_counter_cannot_truncate_inside_one_evidence_unit() -> None: class CharacterCounter: identity = "test.characters" @@ -335,9 +338,10 @@ def __call__(self, text: str) -> int: token_budget=24, ) - assert chunks and chunks[0].truncated + assert chunks == [] assert usage.context_tokens == counter(context) - assert 0 < usage.context_tokens <= usage.budget_tokens == 24 + assert usage.context_tokens == 0 + assert usage.budget_tokens == 24 def test_supersession_and_claim_family_deduplication_keep_best_candidate() -> None: diff --git a/tests/test_dashboard_auth_placement.py b/tests/test_dashboard_auth_placement.py index 60c1cd07..84937461 100644 --- a/tests/test_dashboard_auth_placement.py +++ b/tests/test_dashboard_auth_placement.py @@ -22,7 +22,7 @@ def test_dashboard_has_no_local_team_auth_or_license_activation_ui(): assert removed not in html assert "activateLicense" not in script assert "'/license/activate'" not in script - assert "Start ${TRIAL_DAYS}-day ${name} trial" in script + assert "days=licTrialDays(plan)" in script assert "hostedCta('team','team_tab')" in script # ``plan: local`` is the free customer runtime, not a paid local plan. assert "raw==='pro'||raw==='team'" in script @@ -190,9 +190,8 @@ def test_hosted_transfer_and_llm_consents_distinguish_sync_from_compute(): # ── a paying customer must never be sold the plan they already own ──────────── # The hosted views route a failed request to one of three answers. A 409 is a conflict, -# and ``consent_required`` means hosted work has not reached the installation yet. The consent -# panel can explain Pro to a local customer, while an existing subscriber sees their included -# feature rather than a second purchase or setup prompt. +# and ``consent_required`` means readable processing lacks workspace approval. The consent +# panel links to the selected workspace controls while retaining honest Cloud account actions. # # ``_route`` below executes the shipped routing rather than asserting on its source: the # regression it guards (409 folded into ``hostedFeatureUnavailable``) kept every string @@ -201,7 +200,7 @@ def test_hosted_transfer_and_llm_consents_distinguish_sync_from_compute(): # The access-state readers the panel copy is now derived from. They are bundled as the # real shipped functions rather than stubbed, so "does this customer get offered a # trial" is answered here by the code that answers it in the browser. - "licAccessState", "licAccessLive", "licTrialActive", "licTrialAvailable", + "licAccessState", "licAccessLive", "licTrialActive", "licTrialAvailable", "licTrialDays", "licPlanName", "licPlanKey", "licTrialEnds", "fmtDay", "lockReason", "withCtaAttribution", "hostedAccountUrl", "hostedPlanUrl", "hostedCta", "ctaLinkHtml", "unlockHtml", "managedConsentHtml", @@ -224,7 +223,7 @@ def test_hosted_transfer_and_llm_consents_distinguish_sync_from_compute(): function renderAnalytics(){return '
'} function fmtRel(){return 'just now'} function toast(){} -const TRIAL_DAYS = 3, WS = 'workspace'; +const WS = 'workspace'; let CURRENT_VIEW = 'overview'; // The default is an unconnected installation: no hosted plan, unspent trial, and the // control plane says a trial may still be started. A case can replace ``access_state`` and @@ -233,7 +232,8 @@ def test_hosted_transfer_and_llm_consents_distinguish_sync_from_compute(): team_upgrade_url:'https://engraphis.com/pricing?plan=team', upgrade_url:'https://engraphis.com/pricing', plan:'local', access_state:'inactive', - trial:{used:false, active:false, available:true, ends_at:0}}; + trial:{used:false, active:false, available:true, ends_at:0, + trial_days:3, days_by_plan:{pro:3, team:10}}}; let LIC = LIC_BASE; const location = {href:'https://127.0.0.1:8077/'}; let THROWN = null; @@ -310,16 +310,16 @@ def test_a_trial_eligible_local_installation_is_answered_with_the_consent_panel( "See the memory your team is about to lose.") in rendered["html"] assert "Start 3-day Pro trial" in rendered["html"] assert "Annual Pro option" in rendered["html"] - assert "Hosted insights and maintenance come on automatically" in rendered["html"] + assert "explicitly approve each workspace in Manage > Settings" in rendered["html"] assert "Secret and session-scoped memories stay local." in rendered["html"] - # Consent travels with the cloud account; the customer is never sent to edit .env. + # Workspace approval is separate from Cloud access; no .env edit is needed. assert "ENGRAPHIS_MANAGED_COMPUTE_CONSENT" not in rendered["html"] assert rendered["pill"] == "CLOUD" @pytest.mark.skipif(shutil.which("node") is None, reason="node is required to run the UI") @pytest.mark.parametrize("view", ["analytics", "automation"]) -def test_a_consent_panel_sends_an_existing_subscriber_to_cloud_not_checkout(tmp_path, view): +def test_a_consent_panel_links_subscribers_to_workspace_approval_without_checkout(tmp_path, view): rendered = _route(tmp_path, [{ "name": "subscriber", "view": view, "error": {"status": 409, "detail": {"code": "consent_required"}}, @@ -330,15 +330,18 @@ def test_a_consent_panel_sends_an_existing_subscriber_to_cloud_not_checkout(tmp_ }])["subscriber"] assert "Open Engraphis Cloud" in rendered["html"] - assert "Hosted insights and maintenance are on by default" in rendered["html"] + assert "paused until you approve this workspace in Manage > Settings" in rendered["html"] + assert 'href="/?view=manage&tab=settings&workspace=workspace"' in rendered["html"] + assert "on by default" not in rendered["html"] + assert "Encrypted Cloud Sync is a separate choice" in rendered["html"] assert "Purchase Pro license" not in rendered["html"] assert "Start 3-day Pro trial" not in rendered["html"] @pytest.mark.skipif(shutil.which("node") is None, reason="node is required to run the UI") @pytest.mark.parametrize("view", ["analytics", "automation"]) -def test_classic_hosted_tabs_offer_the_local_trial_without_a_setup_step(tmp_path, view): - """Classic keeps the automatic three-day Cloud entry, not a local setup workflow.""" +def test_classic_hosted_tabs_distinguish_trial_access_from_workspace_approval(tmp_path, view): + """Classic offers Pro access and links to the independent workspace controls.""" rendered = _route(tmp_path, [{ "name": "classic-trial", "view": view, @@ -347,11 +350,12 @@ def test_classic_hosted_tabs_offer_the_local_trial_without_a_setup_step(tmp_path html = rendered["html"] assert "Start 3-day Pro trial" in html - assert "Hosted insights and maintenance come on automatically" in html - assert "no settings, toggles, or worker setup" in html - # The two Cloud links are the complete unconnected path: trial or purchase. The - # Classic tab must not add a local button for connecting, enabling, or configuring. - assert html.count(""']/g, c=>( {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]))} function safeUrl(u){return (u && typeof u === 'string') ? u : '#'} -const TRIAL_DAYS = 3; // Three distinct hosted targets, so a button carrying the wrong one is visible rather // than hidden behind a shared URL. // ``upgrade_url`` is deliberately the Pro checkout here: that is what @@ -491,7 +494,8 @@ def test_a_transient_hosted_conflict_is_not_answered_with_a_purchase_panel( upgrade_url:'https://engraphis.example/checkout/pro', account_url:'https://engraphis.example/account', plan:'local', access_state:'inactive', - trial:{used:false, active:false, available:false, ends_at:0}}; + trial:{used:false, active:false, available:false, ends_at:0, + trial_days:3, days_by_plan:{pro:3, team:10}}}; let LIC = LIC_BASE; const location = {href:'https://127.0.0.1:8700/'}; """ @@ -572,7 +576,8 @@ def test_each_access_state_offers_the_one_action_that_can_succeed( "name": state, "lic": {"plan": "pro", "access_state": state, "trial": {"used": state != "inactive", "active": state == "trial", - "available": state == "inactive", "ends_at": 0}}, + "available": state == "inactive", "ends_at": 0, + "trial_days": 3, "days_by_plan": {"pro": 3, "team": 10}}}, }])[state]["html"] if expected: @@ -604,7 +609,8 @@ def test_a_paying_team_customer_is_not_told_team_is_excluded(tmp_path): {"name": "team-expired", "lic": {"plan": "team", "access_state": "trial_expired"}}, {"name": "free", "lic": {"plan": "local", "access_state": "inactive", "trial": {"used": False, "active": False, - "available": True, "ends_at": 0}}}, + "available": True, "ends_at": 0, + "trial_days": 3, "days_by_plan": {"pro": 3, "team": 10}}}}, ]) assert rows["team-active"]["teamNote"] == ( @@ -623,7 +629,7 @@ def test_a_paying_team_customer_is_not_told_team_is_excluded(tmp_path): assert rows["pro-active"]["teamNote"] == "Your PRO subscription does not include this." assert "no longer active" in rows["team-lapsed"]["teamNote"] assert "free trial has ended" in rows["team-expired"]["teamNote"] - assert "exactly 3 active days" in rows["free"]["teamNote"] + assert "exactly 10 active days" in rows["free"]["teamNote"] def test_only_an_entitlement_status_may_draw_the_purchase_panel(): @@ -684,7 +690,7 @@ def test_pro_upgrade_panel_lists_every_pro_benefit_and_state_specific_cta(): styles = STYLES.read_text(encoding="utf-8") assert 'class="upgrade-panel"' in script - assert "Start ${TRIAL_DAYS}-day ${name} trial" in script + assert "days=licTrialDays(plan)" in script assert "Subscribe to ${name}" in script for benefit in ( "Hosted Cloud Sync across your installations", diff --git a/tests/test_dashboard_v2.py b/tests/test_dashboard_v2.py index 2f767022..8d1b5a9c 100644 --- a/tests/test_dashboard_v2.py +++ b/tests/test_dashboard_v2.py @@ -1821,10 +1821,13 @@ def test_dashboard_automation_uses_active_workspace_and_discloses_upload_boundar assert "/maintenance/run?workspace=" in source assert "Preview snapshot" not in source assert "uploads the selected workspace’s normal and sensitive memory content" in source - # The upload boundary is still disclosed, but consent now travels with the cloud - # account: the dashboard must not name the operator override anywhere. + # Processing requires a separate workspace choice; never imply an account or plan + # enables it, and do not expose the operator override as the user control. assert "ENGRAPHIS_MANAGED_COMPUTE_CONSENT" not in source - assert "Hosted work is automatic with Pro." in source + assert "Readable processing requires your workspace approval." in source + assert "Approve workspace processing in Manage > Settings" in source + assert "Hosted work is automatic with Pro." not in source + assert "starts automatically with Pro" not in source def test_portfolio_and_report_analytics_are_hosted_only(monkeypatch, tmp_path): diff --git a/tests/test_documentation_contracts.py b/tests/test_documentation_contracts.py index 9fed25b4..90ddf12f 100644 --- a/tests/test_documentation_contracts.py +++ b/tests/test_documentation_contracts.py @@ -6,6 +6,9 @@ from pathlib import Path +from engraphis.core.schema import SCHEMA_VERSION + + ROOT = Path(__file__).resolve().parents[1] @@ -240,10 +243,10 @@ def test_schema_and_erasure_docs_match_live_export_policy() -> None: erasure = _read("docs/SECURE_ERASURE.md") schema = _read("engraphis/core/schema.py") - assert "SCHEMA_VERSION = 16" in schema - assert agents.count("`SCHEMA_VERSION = 16`") == 2 - assert "schema 16" in readme - assert "schema 16" in changelog + assert f"SCHEMA_VERSION = {SCHEMA_VERSION}" in schema + assert agents.count(f"`SCHEMA_VERSION = {SCHEMA_VERSION}`") == 2 + assert f"schema {SCHEMA_VERSION}" in readme + assert f"schema {SCHEMA_VERSION}" in changelog for document in (agents, readme, changelog, sync, erasure): normalized = " ".join(document.split()) @@ -274,7 +277,7 @@ def test_document_import_docs_describe_the_source_neutral_contract() -> None: assert format_name in guide for safety_term in ("symlink", "secret", "unsupported", "resumable", "temporal", "conflict"): assert safety_term in guide - assert "SCHEMA_VERSION = 16" in agents + assert f"SCHEMA_VERSION = {SCHEMA_VERSION}" in agents assert "source-neutral" in agents assert "rich Markdown adapter" in obsidian assert "DOCUMENT_IMPORT.md" in obsidian diff --git a/tests/test_engine.py b/tests/test_engine.py index 00d280de..82ccdf8f 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -453,7 +453,7 @@ def fail_scan(*_args, **_kwargs): raise sqlite3.DatabaseError("database-secret") monkeypatch.setattr(eng.index, "search", fail_search) - monkeypatch.setattr(eng.store, "iter_vectors", fail_scan) + monkeypatch.setattr(eng.store, "iter_vector_matrices", fail_scan) before = eng.store.conn.execute("SELECT COUNT(*) FROM memories").fetchone()[0] with caplog.at_level("WARNING"): diff --git a/tests/test_fts_insert_scaling.py b/tests/test_fts_insert_scaling.py new file mode 100644 index 00000000..18f04340 --- /dev/null +++ b/tests/test_fts_insert_scaling.py @@ -0,0 +1,13 @@ +from eval.fts_insert_scaling import run_comparison + + +def test_counterfactual_preserves_canonical_vector_and_mirror_counts(): + report = run_comparison([3, 8], dim=4, batch_size=2) + cells = report["metrics"]["cells"] + assert len(cells) == 4 + assert {cell["strategy"] for cell in cells} == {"forced_legacy_delete", "new_row_insert"} + for cell in cells: + assert set(cell["verified_row_counts"].values()) == {cell["corpus_size"]} + assert cell["elapsed_seconds"] > 0 + assert cell["disk"]["database_bytes"] > 0 + assert report["metrics"]["source_stable"] is True diff --git a/tests/test_graph_engine_asset.py b/tests/test_graph_engine_asset.py index 1c85c8ea..7b72c0fe 100644 --- a/tests/test_graph_engine_asset.py +++ b/tests/test_graph_engine_asset.py @@ -410,7 +410,7 @@ def test_show_all_lazily_loads_its_renderer_after_the_main_engine_is_ready() -> report = _run_routing("all-loaded") assert report["appended"] == [ - "/v2-assets/engraphis-graph-every.js?v=20260823-every-19" + "/v2-assets/engraphis-graph-every.js?v=20260905-every-20" ] assert report["beforeSettle"] == {"engine": 0, "classic": 0} assert report["engine"] == 1 @@ -10394,7 +10394,7 @@ def test_primary_graph_dependencies_are_lazy_retryable_and_csp_clean() -> None: "'/v2-assets/engraphis-graph.js?v=20260903-rotation-balance-1'" ) assert d3 < force_graph < renderer - assert '/v2-assets/ledger.js?v=20260903-rotation-balance-1' in markup + assert '/v2-assets/ledger.js?v=20260905-processing-review-1' in markup assert "if (graphAssetsPromise === attempt) releaseGraphAssetsAttempt(attempt)" in loader assert "graphAssetsRetry = Math.min(graphAssetsRetry + 1, 10)" in loader all_loader = source[source.index("function ensureGraphAllAsset()"): diff --git a/tests/test_hosted_plan_resolution.py b/tests/test_hosted_plan_resolution.py index b139a153..36983e51 100644 --- a/tests/test_hosted_plan_resolution.py +++ b/tests/test_hosted_plan_resolution.py @@ -1959,6 +1959,32 @@ def test_an_unconnected_installation_is_the_one_place_a_trial_is_offered() -> No assert payload["trial"]["used"] is False +def test_license_discloses_manifest_trial_days_by_plan_and_retains_legacy_days(monkeypatch) -> None: + from engraphis import commercial + + payload = v2_api.get_license() + assert payload["trial"]["days_by_plan"] == {"pro": 3, "team": 10} + assert payload["trial"]["trial_days"] == 3 + assert payload["trial_seconds"] == 3 * 24 * 60 * 60 + monkeypatch.setattr(commercial, "manifest", lambda: { + "trial": {"days_by_plan": {"pro": 5, "team": 17}}, + }) + assert v2_api.get_license()["trial"]["days_by_plan"] == {"pro": 5, "team": 17} + assert v2_api.get_license()["trial"]["trial_days"] == 3 + + +@pytest.mark.parametrize("trial", [{}, None, {"days_by_plan": {"pro": 3, "team": "10"}}, + {"days_by_plan": {"pro": 3, "team": True}}, + {"days_by_plan": {"pro": 3, "team": 0}}]) +def test_unknown_team_trial_duration_is_never_inferred_from_legacy_days(monkeypatch, trial) -> None: + from engraphis import commercial + + monkeypatch.setattr(commercial, "manifest", lambda: {"trial": trial}) + payload = v2_api.get_license() + assert "team" not in payload["trial"]["days_by_plan"] + assert payload["trial"]["trial_days"] == 3 + + def test_a_connected_but_unanswered_installation_offers_no_trial(monkeypatch) -> None: """First boot after onboarding: the plan is inferred, the trial must not be. @@ -2207,12 +2233,13 @@ def test_the_dashboard_never_offers_a_trial_it_was_not_told_is_available() -> No assert "LIC.trial.used" not in script hosted_cta = script[script.index("function hostedCta("):] hosted_cta = hosted_cta[:hosted_cta.index("\n")] - assert "Start ${TRIAL_DAYS}-day ${name} trial" in hosted_cta + assert "days=licTrialDays(plan)" in hosted_cta + assert "Start ${days?`${days}-day `:''}${name} trial" in hosted_cta assert "licTrialAvailable()&&state==='inactive'" in hosted_cta team = script[script.index("async function loadTeam()"): script.index("/* health + settings */")] assert "hostedCta('team','team_tab')" in team - marker = "Start exactly '+TRIAL_DAYS+' days free" + marker = "esc(hostedCta('pro','analytics').label)" index = script.index(marker) window = script[max(0, index - 400):index] assert "licTrialAvailable()" in window, marker diff --git a/tests/test_init.py b/tests/test_init.py index 7388eb83..8b839340 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -1,5 +1,7 @@ """engraphis-init — onboarding command. Runs on the numpy-only gate (stdlib only).""" import os +import json +import sqlite3 from pathlib import Path import subprocess import sys @@ -34,9 +36,12 @@ def test_init_writes_trusted_env_with_absolute_db_path(tmp_path, monkeypatch, ca env = _config_env(tmp_path).read_text() out = capsys.readouterr().out assert "ENGRAPHIS_DB_PATH=" in env + assert "ENGRAPHIS_API_TOKEN=" in env assert str((tmp_path / "mem" / "engraphis.db").resolve()) in env assert not (tmp_path / ".env").exists() assert "engraphis-mcp" in out and "mcpServers" in out + generated_token = next(line.split("=", 1)[1] for line in env.splitlines() if line.startswith("ENGRAPHIS_API_TOKEN=")) + assert len(generated_token) >= 24 and generated_token not in out out.encode("ascii") @@ -238,6 +243,17 @@ def test_doctor_reports_functional_embedder(tmp_path, monkeypatch, capsys): assert "embedder functional" in out +def test_doctor_explains_tokenless_review_setup(tmp_path, monkeypatch, capsys): + _fresh_settings(monkeypatch, tmp_path) + import engraphis.config as cfg + monkeypatch.setattr(cfg.settings, "api_token", "") + assert main(["--check", "--json"]) == 0 + report = json.loads(capsys.readouterr().out) + review = next(check for check in report["checks"] if check["code"] == "browser_approval") + assert review["status"] == "optional" + assert "private config" in review["detail"] and "engraphis-dashboard" in review["detail"] + + def test_prefetch_command_reports_ready_or_offline(tmp_path, monkeypatch, capsys): _fresh_settings(monkeypatch, tmp_path) # Test prefetch with offline deterministic model @@ -248,3 +264,101 @@ def test_prefetch_command_reports_ready_or_offline(tmp_path, monkeypatch, capsys out = capsys.readouterr().out assert "deterministic offline embedder is active" in out + +def test_doctor_json_probes_writes_without_leaving_schema_or_rows(tmp_path, monkeypatch, capsys): + _fresh_settings(monkeypatch, tmp_path) + path = tmp_path / "doc.db" + with sqlite3.connect(path) as conn: + conn.execute("CREATE TABLE preserved (value TEXT)") + conn.execute("INSERT INTO preserved VALUES ('existing record')") + assert main(["--check", "--json"]) == 0 + report = json.loads(capsys.readouterr().out) + assert report["schema_version"] == 1 and report["ok"] + assert any(check["code"] == "database_writable" for check in report["checks"]) + with sqlite3.connect(path) as conn: + assert conn.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall() == [("preserved",)] + assert conn.execute("SELECT value FROM preserved").fetchall() == [("existing record",)] + + +def test_doctor_rejects_readable_but_read_only_database(tmp_path, monkeypatch, capsys): + _fresh_settings(monkeypatch, tmp_path) + path = tmp_path / "doc.db" + connect = sqlite3.connect + with connect(path) as conn: + conn.execute("CREATE TABLE preserved (value TEXT)") + monkeypatch.setattr(init_script, "connector_from_env", lambda: None) + monkeypatch.setattr(init_script.sqlite3, "connect", lambda *_args, **_kwargs: connect(path.as_uri() + "?mode=ro", uri=True)) + assert main(["--check", "--json"]) == 1 + report = json.loads(capsys.readouterr().out) + assert not report["ok"] + checks = {check["code"]: check for check in report["checks"]} + assert checks["database_readable"]["status"] == "ok" + assert checks["database_unwritable"]["status"] == "fail" + assert "database_writable" not in checks + + +def test_doctor_rejects_a_newer_database_schema(tmp_path, monkeypatch, capsys): + _fresh_settings(monkeypatch, tmp_path) + from engraphis.core.schema import SCHEMA_VERSION + with sqlite3.connect(tmp_path / "doc.db") as conn: + conn.execute("CREATE TABLE schema_migrations (version INTEGER)") + conn.execute("INSERT INTO schema_migrations VALUES (?)", (SCHEMA_VERSION + 1,)) + assert main(["--check", "--json"]) == 1 + report = json.loads(capsys.readouterr().out) + assert any(check["code"] == "schema_newer" for check in report["checks"]) + + +def test_doctor_reports_lock_contention_without_leaving_probe_state(tmp_path, monkeypatch, capsys): + _fresh_settings(monkeypatch, tmp_path) + path = tmp_path / "doc.db" + blocker = sqlite3.connect(path) + try: + blocker.execute("CREATE TABLE preserved (value TEXT)") + blocker.execute("BEGIN EXCLUSIVE") + assert main(["--check", "--json"]) == 1 + report = json.loads(capsys.readouterr().out) + assert any(check["code"] == "database_locked" for check in report["checks"]) + finally: + blocker.rollback() + blocker.close() + with sqlite3.connect(path) as conn: + assert conn.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall() == [("preserved",)] + + +def test_doctor_does_not_mask_a_broken_configured_embedder_or_echo_its_error(tmp_path, monkeypatch, capsys): + _fresh_settings(monkeypatch, tmp_path) + import engraphis.config as cfg + import engraphis.backends.embedder_st as backend + monkeypatch.setattr(cfg.settings, "embed_model", "missing-configured-model") + + def fail(*_args, **kwargs): + assert kwargs["require_exact"] is True + raise RuntimeError("private-provider-secret") + + monkeypatch.setattr(backend, "get_embedder", fail) + assert main(["--check", "--json"]) == 1 + output = capsys.readouterr().out + assert "private-provider-secret" not in output + report = json.loads(output) + assert any(check["code"] == "embedder" and check["status"] == "fail" for check in report["checks"]) + + +def test_init_records_only_explicit_installation_intent(tmp_path, monkeypatch, capsys): + from scripts import installation_profile + monkeypatch.chdir(tmp_path) + path = installation_profile.profile_path(_config_env(tmp_path)) + assert main(["--no-encryption"]) == 0 + assert not path.exists() + assert main(["--extras", "server,mcp", "--no-encryption"]) == 0 + assert json.loads(path.read_text())["extras"] == ["mcp", "server"] + output = capsys.readouterr().out + assert "codex mcp add engraphis" in output + assert "save one project decision" in output + + +def test_init_rejects_invalid_extras_before_writing_config(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + with pytest.raises(SystemExit) as exc: + main(["--extras", "server;owned"]) + assert exc.value.code == 2 + assert not _config_env(tmp_path).exists() diff --git a/tests/test_installation_profile.py b/tests/test_installation_profile.py new file mode 100644 index 00000000..4c5900ad --- /dev/null +++ b/tests/test_installation_profile.py @@ -0,0 +1,54 @@ +"""Explicit install profiles preserve capability choices without guessing wheel metadata.""" +import json + +import pytest + +from scripts import installation_profile as profiles +from scripts import update + + +@pytest.fixture(autouse=True) +def isolated_profile(tmp_path, monkeypatch): + path = tmp_path / "private" / "profile.json" + monkeypatch.setattr(profiles, "profile_path", lambda _config_path=None: path) + monkeypatch.delenv("ENGRAPHIS_UPDATE_EXTRAS", raising=False) + return path + + +@pytest.mark.parametrize("extras,expected", [([], ""), (["mcp"], "[mcp]"), (["all"], "[all]"), + (["mcp", "server"], "[mcp,server]")]) +def test_update_preserves_explicit_profile(extras, expected): + profiles.write_profile(extras) + assert profiles.read_profile() == extras + assert update._installed_extras() == expected + assert update._explicit_installation_extras() == expected + + +def test_explicit_environment_override_wins(monkeypatch): + profiles.write_profile(["server"]) + monkeypatch.setenv("ENGRAPHIS_UPDATE_EXTRAS", "none") + assert update._installed_extras() == "" + + +def test_missing_or_invalid_profile_retains_legacy_fallback(isolated_profile): + assert update._installed_extras() == "[all]" + profiles.write_profile(["mcp"]) + isolated_profile.write_text("not JSON") + assert profiles.read_profile() is None + assert update._installed_extras() == "[all]" + + +def test_profile_cannot_select_extras_for_another_python_environment(isolated_profile): + profiles.write_profile(["mcp"]) + data = json.loads(isolated_profile.read_text()) + data["environment"] = "/another/python" + isolated_profile.write_text(json.dumps(data)) + assert profiles.read_profile() is None + + +def test_profile_rejects_unsafe_or_unvalidated_package_arguments(isolated_profile): + profiles.write_profile(["mcp"]) + data = json.loads(isolated_profile.read_text()) + data["extras"] = ["--index-url=untrusted"] + isolated_profile.write_text(json.dumps(data)) + assert profiles.read_profile() is None diff --git a/tests/test_managed_processing_policy.py b/tests/test_managed_processing_policy.py new file mode 100644 index 00000000..52dff0a7 --- /dev/null +++ b/tests/test_managed_processing_policy.py @@ -0,0 +1,345 @@ +"""Workspace approval is explicit, durable and cannot come from legacy settings.""" +import json +import threading +from concurrent.futures import ThreadPoolExecutor + +import pytest + +from engraphis.cloud_features import CloudFeatureError, build_managed_snapshot +from engraphis.service import MemoryService + + +@pytest.fixture +def svc(tmp_path, monkeypatch): + monkeypatch.delenv("ENGRAPHIS_MANAGED_COMPUTE_CONSENT", raising=False) + service = MemoryService.create(str(tmp_path / "policy.db")) + service.remember("A useful fact", workspace="a") + service.remember("A different fact", workspace="b") + yield service + service.engine.close() + + +def test_legacy_state_requires_confirmation_and_preserves_data(svc, monkeypatch): + monkeypatch.setenv("ENGRAPHIS_MANAGED_COMPUTE_CONSENT", "true") + before = svc.store.conn.execute("SELECT count(*) FROM memories").fetchone()[0] + policy = svc.managed_processing_policy("a") + assert policy["confirmation_required"] and not policy["enabled"] + with pytest.raises(CloudFeatureError, match="approval"): + build_managed_snapshot(svc, "a", consent=True) + assert svc.store.conn.execute("SELECT count(*) FROM memories").fetchone()[0] == before + + +def test_workspace_isolation_restart_and_optout(svc): + with pytest.raises(ValueError, match="explicit"): + svc.set_managed_processing_policy("a", enabled=True) + svc.set_managed_processing_policy("a", enabled=True, confirmed=True, remote_revision=2) + assert svc.managed_processing_policy("a")["enabled"] + assert not svc.managed_processing_policy("b")["enabled"] + other = MemoryService.create(svc.store.path) + try: + assert other.managed_processing_policy("a")["remote_revision"] == 2 + other.set_managed_processing_policy("a", enabled=False) + assert not svc.managed_processing_policy("a")["enabled"] + with pytest.raises(CloudFeatureError): + build_managed_snapshot(svc, "a") + finally: + other.engine.close() + + +@pytest.mark.parametrize("revision", ["bad", True, -1, None]) +def test_corrupt_state_fails_closed_and_can_be_reconfirmed(svc, revision): + wid = svc._lookup_workspace("a") + svc.store.conn.execute( + "INSERT INTO sync_state(key,value,updated_at) VALUES (?,?,0)", + ("managed_processing_policy:" + wid, json.dumps({ + "schema": "engraphis-managed-processing/v1", "revision": revision, + "enabled": True, "confirmed": True, + })), + ) + svc.store.conn.commit() + assert not svc.managed_processing_policy("a")["enabled"] + assert svc.set_managed_processing_policy("a", enabled=True, confirmed=True)["enabled"] + + +def test_http_acknowledgement_and_failed_optout(svc, monkeypatch): + from fastapi import FastAPI + from fastapi.testclient import TestClient + from engraphis.routes import v2_api + from engraphis.cloud_features import CloudFeatureClient + + class Cloud: + fail = False + calls = [] + + def get_processing_policy(self, wid): + return {"enabled": False, "revision": 1} + + def set_processing_policy(self, wid, **kwargs): + self.calls.append((wid, kwargs)) + if self.fail: + raise CloudFeatureError("unavailable", status=503) + return {"enabled": kwargs["enabled"], "revision": 2} + + cloud = Cloud() + monkeypatch.setattr(CloudFeatureClient, "from_environment", lambda _: cloud) + monkeypatch.setattr(v2_api, "service", lambda: svc) + app = FastAPI() + app.include_router(v2_api.router) + with TestClient(app) as client: + assert client.post("/api/managed-processing", json={ + "workspace": "a", "enabled": True, + }).status_code == 400 + assert cloud.calls == [] + for payload in ({"enabled": "true"}, {"enabled": True, "confirmed": "true"}): + assert client.post("/api/managed-processing", json={ + "workspace": "a", **payload, + }).status_code == 422 + cloud.fail = True + assert client.post("/api/managed-processing", json={ + "workspace": "a", "enabled": True, "confirmed": True, + }).status_code == 503 + assert not svc.managed_processing_policy("a")["enabled"] + cloud.fail = False + enabled = client.post("/api/managed-processing", json={ + "workspace": "a", "enabled": True, "confirmed": True, + }).json() + assert enabled["enabled"] and enabled["remote_revision"] == 2 + cloud.fail = True + disabled = client.post("/api/managed-processing", json={ + "workspace": "a", "enabled": False, + }).json() + assert not disabled["enabled"] and disabled["remote_sync_pending"] + assert "may continue" in disabled["notice"] + + +def test_delayed_enable_acknowledgement_cannot_overwrite_newer_optout(svc, monkeypatch): + from fastapi import FastAPI + from fastapi.testclient import TestClient + from engraphis.routes import v2_api + from engraphis.cloud_features import CloudFeatureClient + + enable_started, release_enable = threading.Event(), threading.Event() + + class Cloud: + def get_processing_policy(self, wid): + return {"enabled": False, "revision": 1} + + def set_processing_policy(self, wid, **kwargs): + if kwargs["enabled"]: + enable_started.set() + assert release_enable.wait(10) + return {"enabled": True, "revision": 1} + return {"enabled": False, "revision": 2} + + monkeypatch.setattr(CloudFeatureClient, "from_environment", lambda _: Cloud()) + monkeypatch.setattr(v2_api, "service", lambda: svc) + app = FastAPI() + app.include_router(v2_api.router) + with TestClient(app) as client, ThreadPoolExecutor(max_workers=1) as pool: + enabling = pool.submit(client.post, "/api/managed-processing", json={ + "workspace": "a", "enabled": True, "confirmed": True, + }) + try: + assert enable_started.wait(10) + disabled = client.post("/api/managed-processing", json={ + "workspace": "a", "enabled": False, + }) + assert disabled.status_code == 200 + assert not disabled.json()["enabled"] + finally: + release_enable.set() + rejected = enabling.result(timeout=10) + assert rejected.status_code == 409 + assert rejected.json()["detail"]["code"] == "processing_policy_changed" + policy = svc.managed_processing_policy("a") + assert not policy["enabled"] and policy["remote_revision"] == 2 + with pytest.raises(CloudFeatureError, match="approval"): + build_managed_snapshot(svc, "a") + + +@pytest.mark.parametrize("delay_at", ["get", "put"]) +def test_newer_optout_fences_delayed_cloud_enable(svc, monkeypatch, delay_at): + from fastapi import FastAPI + from fastapi.testclient import TestClient + from engraphis.cloud_features import CloudFeatureClient + from engraphis.routes import v2_api + + started, release = threading.Event(), threading.Event() + + class Cloud: + revision = 1 + enabled = False + gets = 0 + puts = [] + + def get_processing_policy(self, wid): + self.gets += 1 + if delay_at == "get" and self.gets == 1: + started.set() + assert release.wait(10) + return {"enabled": self.enabled, "revision": self.revision} + + def set_processing_policy(self, wid, **kwargs): + self.puts.append(kwargs) + if delay_at == "put" and kwargs["enabled"]: + started.set() + assert release.wait(10) + if kwargs["revision"] != self.revision: + raise CloudFeatureError("stale revision", status=409) + self.revision += 1 + self.enabled = kwargs["enabled"] + return {"enabled": self.enabled, "revision": self.revision} + + cloud = Cloud() + monkeypatch.setattr(CloudFeatureClient, "from_environment", lambda _: cloud) + monkeypatch.setattr(v2_api, "service", lambda: svc) + app = FastAPI() + app.include_router(v2_api.router) + with TestClient(app) as client, ThreadPoolExecutor(max_workers=1) as pool: + enabling = pool.submit(client.post, "/api/managed-processing", json={ + "workspace": "a", "enabled": True, "confirmed": True, + }) + try: + assert started.wait(10) + disabled = client.post("/api/managed-processing", json={ + "workspace": "a", "enabled": False, + }) + assert disabled.status_code == 200 + assert not disabled.json()["remote_sync_pending"] + finally: + release.set() + assert enabling.result(timeout=10).status_code == 409 + assert not cloud.enabled and cloud.revision == 2 + assert not svc.managed_processing_policy("a")["enabled"] + assert sum(call["enabled"] for call in cloud.puts) == (delay_at == "put") + + +def test_optout_retries_remote_conflict_while_local_intent_is_current(svc, monkeypatch): + from fastapi import FastAPI + from fastapi.testclient import TestClient + from engraphis.cloud_features import CloudFeatureClient + from engraphis.routes import v2_api + + enable_started, release_enable, enable_applied = ( + threading.Event(), threading.Event(), threading.Event() + ) + + class Cloud: + revision = 1 + enabled = False + off_revisions = [] + + def get_processing_policy(self, wid): + return {"enabled": self.enabled, "revision": self.revision} + + def set_processing_policy(self, wid, **kwargs): + if kwargs["enabled"]: + enable_started.set() + assert release_enable.wait(10) + else: + self.off_revisions.append(kwargs["revision"]) + if len(self.off_revisions) == 1: + release_enable.set() + assert enable_applied.wait(10) + if kwargs["revision"] != self.revision: + raise CloudFeatureError("stale revision", status=409) + self.revision += 1 + self.enabled = kwargs["enabled"] + if self.enabled: + enable_applied.set() + return {"enabled": self.enabled, "revision": self.revision} + + cloud = Cloud() + monkeypatch.setattr(CloudFeatureClient, "from_environment", lambda _: cloud) + monkeypatch.setattr(v2_api, "service", lambda: svc) + app = FastAPI() + app.include_router(v2_api.router) + with TestClient(app) as client, ThreadPoolExecutor(max_workers=1) as pool: + enabling = pool.submit(client.post, "/api/managed-processing", json={ + "workspace": "a", "enabled": True, "confirmed": True, + }) + try: + assert enable_started.wait(10) + disabled = client.post("/api/managed-processing", json={ + "workspace": "a", "enabled": False, + }) + assert disabled.status_code == 200 + assert not disabled.json()["remote_sync_pending"] + finally: + release_enable.set() + assert enabling.result(timeout=10).status_code == 409 + assert cloud.off_revisions == [1, 2] + assert not cloud.enabled and cloud.revision == 3 + assert svc.managed_processing_policy("a")["remote_revision"] == 3 + + +def test_optout_does_not_retry_after_newer_local_approval(svc, monkeypatch): + from fastapi import FastAPI + from fastapi.testclient import TestClient + from engraphis.cloud_features import CloudFeatureClient + from engraphis.routes import v2_api + + class Cloud: + calls = 0 + + def get_processing_policy(self, wid): + return {"enabled": False, "revision": 1} + + def set_processing_policy(self, wid, **kwargs): + self.calls += 1 + svc.set_managed_processing_policy( + "a", enabled=True, confirmed=True, remote_revision=2) + raise CloudFeatureError("stale revision", status=409) + + cloud = Cloud() + monkeypatch.setattr(CloudFeatureClient, "from_environment", lambda _: cloud) + monkeypatch.setattr(v2_api, "service", lambda: svc) + app = FastAPI() + app.include_router(v2_api.router) + with TestClient(app) as client: + disabled = client.post("/api/managed-processing", json={ + "workspace": "a", "enabled": False, + }) + assert disabled.status_code == 409 and cloud.calls == 1 + assert svc.managed_processing_policy("a")["enabled"] + + +def test_cloud_client_sends_required_revision(monkeypatch): + from engraphis.cloud_features import CloudFeatureClient + + calls = [] + monkeypatch.setattr(CloudFeatureClient, "_request", lambda *args: calls.append(args[1:]) or {}) + cloud = CloudFeatureClient("https://compute.example", "org_test", "test-token") + cloud.set_processing_policy("ws_test", enabled=True, confirmed=True, revision=7) + assert calls == [("PUT", "/v1/organizations/org_test/workspaces/ws_test/processing-policy", { + "enabled": True, "confirmed": True, "revision": 7, + })] + with pytest.raises(CloudFeatureError, match="revision"): + cloud.set_processing_policy("ws_test", enabled=False, revision=True) + assert len(calls) == 1 + + +def test_expired_cloud_session_stops_local_uploads_with_remote_pending(svc, monkeypatch): + from fastapi import FastAPI + from fastapi.testclient import TestClient + from engraphis.cloud_features import CloudFeatureClient + from engraphis.routes import v2_api + + svc.set_managed_processing_policy("a", enabled=True, confirmed=True, remote_revision=2) + + def expired(_): + raise CloudFeatureError("active cloud entitlement required", status=402) + + monkeypatch.setattr(CloudFeatureClient, "from_environment", expired) + monkeypatch.setattr(v2_api, "service", lambda: svc) + app = FastAPI() + app.include_router(v2_api.router) + with TestClient(app) as client: + disabled = client.post("/api/managed-processing", json={ + "workspace": "a", "enabled": False, + }) + assert disabled.status_code == 200 + assert not disabled.json()["enabled"] and disabled.json()["remote_sync_pending"] + assert "may continue" in disabled.json()["notice"] + with pytest.raises(CloudFeatureError, match="approval"): + build_managed_snapshot(svc, "a") diff --git a/tests/test_manual_graph_probe.py b/tests/test_manual_graph_probe.py new file mode 100644 index 00000000..01a4b0d0 --- /dev/null +++ b/tests/test_manual_graph_probe.py @@ -0,0 +1,40 @@ +"""The manual graph probe must never attach to an unrelated dashboard.""" +from __future__ import annotations + +import shutil +import subprocess +from pathlib import Path + +import pytest + + +ROOT = Path(__file__).resolve().parents[1] + + +@pytest.mark.skipif(shutil.which("node") is None, reason="Node is required for the manual probe") +def test_graph_probe_rejects_an_occupied_port_without_contacting_its_owner(): + script = r""" +const assert = require('node:assert/strict'); +const net = require('node:net'); +const path = require('node:path'); +(async () => { + let contacted = false; + const owner = net.createServer(socket => { contacted = true; socket.end(); }); + await new Promise(resolve => owner.listen(0, '127.0.0.1', resolve)); + try { + process.env.ENGRAPHIS_PLAYWRIGHT_PORT = String(owner.address().port); + const before = process.cwd(); + const probe = require('./tools/galaxy_mode_test.js'); + assert.equal(probe.REPO, path.resolve('.')); + assert.equal(process.cwd(), before); + await assert.rejects(probe.startServer(), /unavailable/); + assert.equal(contacted, false); + assert.equal(owner.listening, true); + } finally { + await new Promise(resolve => owner.close(resolve)); + } +})().catch(error => { console.error(error); process.exitCode = 1; }); +""" + result = subprocess.run(["node", "-e", script], cwd=ROOT, capture_output=True, + text=True, timeout=15) + assert result.returncode == 0, result.stderr diff --git a/tests/test_mcp_contract.py b/tests/test_mcp_contract.py new file mode 100644 index 00000000..b87ca207 --- /dev/null +++ b/tests/test_mcp_contract.py @@ -0,0 +1,29 @@ +"""Published schemas and shipped integrations must follow the registered server.""" +import json + +import pytest + +pytest.importorskip("mcp") +from scripts.export_mcp_contract import ROOT, artifacts, build_contract + + +def test_generated_artifacts_match_registered_tools(): + contract = build_contract() + for path, expected in artifacts(contract).items(): + assert path.read_text(encoding="utf-8") == expected, str(path) + assert contract["schema"] == "engraphis-mcp-contract/v1" + assert len(contract["sha256"]) == 64 + schemas = {item["name"]: item["inputSchema"] for item in contract["surfaces"]["smart"]} + assert schemas["engraphis_recall_context"]["properties"]["k"]["default"] == 50 + assert schemas["engraphis_recall_context"]["properties"]["format"]["default"] == "full" + assert {"subject_key", "claim_kind"} <= schemas["engraphis_remember"]["properties"].keys() + classic = {item["name"] for item in contract["surfaces"]["classic"]} + assert "engraphis_recall" in classic + assert "engraphis_discover_actions" not in classic + + +def test_contract_is_secret_free_public_metadata(): + text = (ROOT / "docs/MCP_CONTRACT.json").read_text(encoding="utf-8") + contract = json.loads(text) + assert all(set(tool) == {"name", "description", "inputSchema", "annotations"} + for surface in contract["surfaces"].values() for tool in surface) diff --git a/tests/test_mcp_server.py b/tests/test_mcp_server.py index 1acc4981..d13e5466 100644 --- a/tests/test_mcp_server.py +++ b/tests/test_mcp_server.py @@ -1324,23 +1324,101 @@ def test_recall_context_prunes_default_diagnostics_when_disabled(monkeypatch): assert "sources" in res -def test_recall_context_gist_format_saves_tokens(monkeypatch): - import engraphis.mcp_server as srv - from engraphis.service import MemoryService - srv.set_service(MemoryService.create(":memory:")) - long_content = ( - "Deploy rule 1: Always verify preflight checks before triggering production deploy. " - "All integration tests must pass in staging environment with zero failures. " - "Database migrations must be executed in backward-compatible transactions. " - "The on-call release engineer must monitor metrics for at least fifteen minutes post-rollout." +@pytest.mark.parametrize("budget", [0, 10, 1000]) +def test_gist_preserves_packed_evidence_and_exact_usage(monkeypatch, budget): + from engraphis.core.context import RegexTokenCounter + + srv = _module_with_memory_db(monkeypatch) + srv.service().remember( + "Regular weekly operations require review of every configured monitoring alert " + "and all scheduled maintenance windows before allocation. Deploy is frozen.", + workspace="acme", ) - srv.service().remember(long_content, workspace="acme") - full_res = json.loads(srv.engraphis_recall_context("deploy", workspace="acme", format="full")) - gist_res = json.loads(srv.engraphis_recall_context("deploy", workspace="acme", format="gist")) - assert gist_res["format"] == "gist" - assert gist_res["usage"]["context_tokens"] < full_res["usage"]["context_tokens"] - assert "[1]" in gist_res["context"] - assert "mem_" in gist_res["context"] + full = json.loads(srv.engraphis_recall_context( + "Deploy", workspace="acme", token_budget=budget, + )) + gist = json.loads(srv.engraphis_recall_context( + "Deploy", workspace="acme", token_budget=budget, format="gist", + )) + assert gist["format"] == "gist" + assert gist["context"] == full["context"] + assert gist["sources"] == full["sources"] + emitted = RegexTokenCounter()(gist["context"]) + usage = gist["usage"] + assert emitted == usage["context_tokens"] == usage["emitted_tokens"] <= budget + assert usage["saved_tokens"] == usage["estimated_saved_tokens"] == max( + 0, usage["source_tokens"] - emitted, + ) + assert usage["savings_ratio"] == usage["estimated_savings_ratio"] + if budget == 10: + assert "Deploy is frozen." in gist["context"] + assert "Regular weekly operations" not in gist["context"] + + +@pytest.mark.parametrize("budget", [10, 1000]) +def test_gist_never_removes_a_qualified_claims_condition(monkeypatch, budget): + srv = _module_with_memory_db(monkeypatch) + content = ( + "Production deployment is allowed after the release engineer completes the " + "operational validation checklist and verifies every prepared rollback procedure " + "unless the incident commander declares a freeze." + ) + written = srv.service().remember(content, workspace="acme") + # A stored summary that drops the condition must not bypass packer validation. + srv.service().store.conn.execute("UPDATE memories SET summary=? WHERE id=?", ( + "Production deployment is allowed.", written["id"], + )) + srv.service().store.conn.commit() + result = json.loads(srv.engraphis_recall_context( + "Production deployment allowed", workspace="acme", token_budget=budget, format="gist", + )) + assert result["context"] == ("[1]\n" + content if budget == 1000 else "") + + +def test_gist_does_not_reread_records_after_the_recall_snapshot(monkeypatch): + from copy import deepcopy + + srv = _module_with_memory_db(monkeypatch) + svc = srv.service() + svc.remember("Deploy is frozen.", workspace="acme") + snapshot = svc.recall("Deploy", workspace="acme", token_budget=32, response_mode="compact") + monkeypatch.setattr(svc, "recall", lambda *args, **kwargs: deepcopy(snapshot)) + + def forbidden_reread(*args, **kwargs): + raise AssertionError("The formatter must not reread records outside the snapshot") + + monkeypatch.setattr(svc.store, "get_memory", forbidden_reread) + result = json.loads(srv.engraphis_recall_context( + "Deploy", workspace="acme", token_budget=32, format="gist", + )) + assert result["context"] == snapshot["context"] + assert result["sources"][0]["id"] == snapshot["packed_sources"][0]["id"] +@pytest.mark.parametrize("format", ["full", "gist"]) +def test_context_response_cap_omits_whole_evidence_and_updates_usage(monkeypatch, format): + from engraphis.core.context import RegexTokenCounter + + srv = _module_with_memory_db(monkeypatch) + srv.service().remember( + "Production deployment is allowed.\n\n" + "Unless the incident commander declares a freeze, in which case every release is blocked.", + workspace="acme", + ) + full = json.loads(srv.engraphis_recall_context( + "Production deployment allowed", workspace="acme", token_budget=1000, format=format, + )) + assert "Unless the incident commander" in full["context"] + cap = full["usage"]["actual_response_tokens"] - 5 + bounded = json.loads(srv.engraphis_recall_context( + "Production deployment allowed", workspace="acme", token_budget=1000, + format=format, max_response_tokens=cap, + )) + assert bounded["context"] == "" + usage = bounded["usage"] + assert usage["context_tokens"] == usage["emitted_tokens"] == 0 + assert usage["packed_count"] == 0 + assert usage["omitted_count"] == full["usage"]["packed_count"] + full["usage"]["omitted_count"] + assert usage["saved_tokens"] == usage["estimated_saved_tokens"] == usage["source_tokens"] + assert RegexTokenCounter()(json.dumps(bounded, ensure_ascii=False)) == usage["actual_response_tokens"] <= cap diff --git a/tests/test_memory_browsing.py b/tests/test_memory_browsing.py new file mode 100644 index 00000000..470e20ee --- /dev/null +++ b/tests/test_memory_browsing.py @@ -0,0 +1,134 @@ +"""Cross-surface temporal and pagination behavior, independent of embeddings.""" +import base64 +import json +import time + +import pytest + +from engraphis.core.browsing import BrowseCursorStale +from engraphis.core.interfaces import MemoryRecord, MemoryType, Scope +from engraphis.service import MemoryService, WorkspaceBindingError + + +@pytest.fixture +def svc(): + service = MemoryService.create(":memory:", graph_extractor="none") + yield service + service.store.close() + + +def seed(svc, *, count=1, workspace="w"): + wid = svc.store.get_or_create_workspace(workspace) + for i in range(count): + svc.store.add_memory(MemoryRecord( + id=f"mem_{workspace}_{i:06d}", workspace_id=wid, scope=Scope.WORKSPACE, + mtype=MemoryType.SEMANTIC if i % 2 == 0 else MemoryType.PROCEDURAL, + content=f"distinct browse record {i}", title=f"record {i}", + valid_from=10, ingested_at=10, + )) + svc.store.conn.commit() + return wid + + +def test_browse_all_1201_records_and_search_oldest(svc): + seed(svc, count=1201) + cursor = "" + ids = [] + while True: + page = svc.list_memories(workspace="w", limit=100, cursor=cursor) + assert page["total_count"] == 1201 + assert page["count"] <= 100 + ids.extend(row["id"] for row in page["memories"]) + cursor = page["next_cursor"] + if not cursor: + break + assert len(ids) == len(set(ids)) == 1201 + match = svc.list_memories(workspace="w", q="record 1200") + assert match["total_count"] == 1 + assert match["memories"][0]["id"] == "mem_w_001200" + assert svc.list_memories(workspace="w", mtype="procedural")["total_count"] == 600 + + +def test_temporal_browsing_matches_canonical_history(svc): + wid = seed(svc, count=6) + now = time.time() + updates = [("valid_from", now + 100, 0), ("valid_to", now + 100, 1), + ("ingested_at", now + 100, 2), ("expired_at", now - 1, 3)] + for field, timestamp, index in updates: + svc.store.conn.execute(f"UPDATE memories SET {field}=? WHERE id=?", + (timestamp, f"mem_w_{index:06d}")) + svc.store.conn.execute("UPDATE memories SET scope='session' WHERE id='mem_w_000004'") + svc.store.conn.execute( + "UPDATE memories SET valid_to=?,valid_to_recorded_at=? WHERE id='mem_w_000005'", + (now - 10, now + 10), + ) + svc.store.conn.commit() + live = svc.list_memories(workspace="w", valid_at=now, known_at=now) + assert {r["id"] for r in live["memories"]} == {"mem_w_000001", "mem_w_000005"} + assert svc.list_memories(workspace="w", valid_at=now, known_at=now + 20)["count"] == 1 + assert wid + + +def test_cursors_bind_filters_and_invalidate_on_changes(svc): + seed(svc, count=3) + page = svc.list_memories(workspace="w", limit=1) + with pytest.raises(ValueError, match="match the query"): + svc.list_memories(workspace="w", q="different", cursor=page["next_cursor"]) + svc.store.conn.execute("UPDATE memories SET sort_order=1 WHERE id='mem_w_000002'") + svc.store.conn.commit() + with pytest.raises(BrowseCursorStale): + svc.list_memories(workspace="w", limit=1, cursor=page["next_cursor"]) + + +def test_browse_preserves_caller_transaction_and_workspace_binding(svc): + seed(svc) + svc.store.conn.execute("UPDATE memories SET title='Uncommitted' WHERE id='mem_w_000000'") + assert svc.list_memories(workspace="w")["memories"][0]["title"] == "Uncommitted" + assert svc.store.conn.transaction_owned_by_current_thread() + svc.store.conn.rollback() + assert svc.list_memories(workspace="w")["memories"][0]["title"] == "record 0" + svc.allowed_workspaces = frozenset({"allowed"}) + svc.store.allowed_workspaces = svc.allowed_workspaces + with pytest.raises(WorkspaceBindingError): + svc.list_memories(workspace="w") + + +def test_browse_escapes_search_metacharacters(svc): + seed(svc, count=2) + assert svc.list_memories(workspace="w", q="%")["count"] == 0 + assert svc.list_memories(workspace="w", q="_")["count"] == 0 + assert svc.list_memories(workspace="w", q="\\")["count"] == 0 + + +@pytest.mark.parametrize("field", ["anchors", "position"]) +@pytest.mark.parametrize("magnitude", [30, 500]) +def test_cursor_rejects_oversized_numeric_values_as_validation_errors(svc, field, magnitude): + seed(svc, count=2) + first = svc.list_memories(workspace="w", limit=1) + payload = json.loads(base64.urlsafe_b64decode(first["next_cursor"])) + payload[field][1] = 10 ** magnitude + cursor = base64.urlsafe_b64encode(json.dumps(payload).encode()).decode() + with pytest.raises(ValueError, match="invalid memory cursor"): + svc.list_memories(workspace="w", limit=1, cursor=cursor) + + +def test_browse_http_pagination_and_stale_cursor(svc, monkeypatch): + pytest.importorskip("fastapi") + from fastapi import FastAPI + from fastapi.testclient import TestClient + from engraphis.routes import v2_api + + seed(svc, count=3) + monkeypatch.setattr(v2_api, "service", lambda: svc) + app = FastAPI() + app.include_router(v2_api.router) + with TestClient(app) as client: + first = client.get("/api/memories", params={"workspace": "w", "limit": 1}) + assert first.status_code == 200 + svc.store.conn.execute("UPDATE memories SET title='changed' WHERE id='mem_w_000000'") + svc.store.conn.commit() + stale = client.get("/api/memories", params={ + "workspace": "w", "limit": 1, "cursor": first.json()["next_cursor"], + }) + assert stale.status_code == 409 + assert stale.json()["detail"]["code"] == "cursor_stale" diff --git a/tests/test_native_coverage_equivalence.py b/tests/test_native_coverage_equivalence.py new file mode 100644 index 00000000..47d4fedc --- /dev/null +++ b/tests/test_native_coverage_equivalence.py @@ -0,0 +1,60 @@ +"""Cardinality excludes extras only after full per-ID/content verification.""" +import numpy as np +import pytest + +pytest.importorskip("sqlite_vec") + +from engraphis.backends.vector_sqlitevec import ( # noqa: E402 + SqliteVecVectorIndex, _native_mirror_covers_canonical, +) +from engraphis.core.interfaces import MemoryRecord # noqa: E402 +from engraphis.core.store import Store # noqa: E402 +from eval.native_coverage_scaling import _legacy_reverse_scan, run_comparison # noqa: E402 + + +@pytest.mark.parametrize("mutation,expected", [ + ("complete", True), ("missing", False), ("extra", False), + ("zero_stale", False), ("zero_clean", True), ("wrong_dimension", False), + ("stale_vector", False), ("orphan", False), ("malformed", False), + ("balanced_missing_extra", False), +]) +def test_count_equivalence_preserves_full_mirror_integrity(mutation, expected): + store = Store(":memory:") + try: + index = SqliteVecVectorIndex(store, dim=4) + wid = store.get_or_create_workspace("coverage") + vectors = np.eye(4, dtype=np.float32)[:3] + mids = ["mem_coverage_a", "mem_coverage_b", "mem_coverage_c"] + for mid, vector in zip(mids, vectors): + store.add_memory(MemoryRecord(id=mid, content="coverage evidence", workspace_id=wid)) + store.put_vector(mid, vector) + index.upsert(mids, vectors) + if mutation in ("missing", "balanced_missing_extra"): + index.delete([mids[0]]) + if mutation in ("extra", "balanced_missing_extra"): + index.upsert(["mem_extra"], vectors[:1]) + if mutation in ("zero_stale", "zero_clean"): + store.put_vector(mids[0], np.zeros(4, dtype=np.float32)) + if mutation == "zero_clean": + index.delete([mids[0]]) + if mutation == "wrong_dimension": + store.put_vector(mids[0], np.ones(3, dtype=np.float32)) + if mutation == "stale_vector": + index.upsert([mids[0]], vectors[1:2]) + if mutation == "orphan": + store.conn.execute("DELETE FROM memories WHERE id=?", (mids[0],)) + if mutation == "malformed": + store.conn.execute("UPDATE mem_vectors SET vector=? WHERE id=?", (b"x", mids[0])) + store.conn.commit() + with store.read_snapshot(): + assert _legacy_reverse_scan(store.conn, 4) is expected + assert _native_mirror_covers_canonical(store.conn, 4) is expected + finally: + store.close() + + +def test_native_coverage_ablation_executes_both_methods(): + report = run_comparison([3, 7], dim=4, batch_size=2) + assert len(report["metrics"]["cells"]) == 4 + assert all(cell["coverage_verified"] for cell in report["metrics"]["cells"]) + assert report["metrics"]["source_stable"] diff --git a/tests/test_obsidian_import_schema.py b/tests/test_obsidian_import_schema.py index 99a1504c..72bb4501 100644 --- a/tests/test_obsidian_import_schema.py +++ b/tests/test_obsidian_import_schema.py @@ -522,7 +522,7 @@ def test_v13_writable_upgrade_creates_durable_current_manifest_schema(tmp_path): workspace_id = _prepare_v13_database(db) upgraded = Store(str(db)) try: - assert upgraded.schema_version == SCHEMA_VERSION == 16 + assert upgraded.schema_version == SCHEMA_VERSION assert upgraded.conn.execute( "SELECT id FROM workspaces WHERE id=?", (workspace_id,) ).fetchone() is not None @@ -575,7 +575,7 @@ def test_v13_read_only_refuses_without_writing_then_accepts_upgraded_db(tmp_path writable.close() readonly = Store(str(db), read_only=True) try: - assert readonly.schema_version == 16 + assert readonly.schema_version == SCHEMA_VERSION with pytest.raises(sqlite3.OperationalError): readonly.conn.execute( "INSERT INTO workspaces(id,name) VALUES ('ws_nope','nope')" @@ -645,7 +645,7 @@ def test_v14_manifest_upgrade_preserves_lineage_and_accepts_documents(tmp_path): upgraded = Store(str(db)) try: - assert upgraded.schema_version == 16 + assert upgraded.schema_version == SCHEMA_VERSION assert upgraded.conn.execute( "SELECT session_id FROM jobs WHERE id=?", (job_id,) ).fetchone()["session_id"] == session_id @@ -685,7 +685,7 @@ def test_v15_upgrade_adds_nullable_job_session_scope(tmp_path): upgraded = Store(str(db)) try: - assert upgraded.schema_version == 16 + assert upgraded.schema_version == SCHEMA_VERSION assert "session_id" in { row["name"] for row in upgraded.conn.execute("PRAGMA table_info(jobs)") } diff --git a/tests/test_pro_cta.py b/tests/test_pro_cta.py index 067fc99f..8805d945 100644 --- a/tests/test_pro_cta.py +++ b/tests/test_pro_cta.py @@ -1,5 +1,11 @@ +import json +import re +import shutil +import subprocess from pathlib import Path +import pytest + ROOT = Path(__file__).resolve().parents[1] SUPPORT_COPY = "Support continued Engraphis development with Pro." @@ -30,8 +36,65 @@ def test_dashboard_shells_share_the_pro_cta_contract(): # compatibility shell still matches the generated source exactly. for shell in (ledger, classic): assert "Subscribe to ${name}" in shell - assert "Start 3-day ${name} trial" in ledger - assert "Start ${TRIAL_DAYS}-day ${name} trial" in classic + + +def _function(script, name): + """Extract the actual declaration and stop at the next peer declaration.""" + match = re.search(r"^(?P *)function " + re.escape(name) + r"\(", script, re.MULTILINE) + assert match is not None, f"missing production helper: {name}" + end = re.search(r"^" + re.escape(match["indent"]) + r"(?:async )?function ", + script[match.end():], re.MULTILINE) + return script[match.start():match.end() + end.start() if end else len(script)].rstrip() + + +@pytest.mark.skipif(shutil.which("node") is None, reason="node is required to run the UI") +@pytest.mark.parametrize("shell", ["ledger", "classic"]) +def test_trial_ctas_use_each_disclosed_plan_duration_without_guessing_team(tmp_path, shell): + """Execute both shipped helpers: a Pro fallback must never become a Team promise.""" + if shell == "ledger": + script = (ROOT / "engraphis/dashboard_assets/ledger.js").read_text(encoding="utf-8") + functions = ("licenseAccessState", "licensePlanKey", "licenseTrialAvailable", + "licenseHasHostedAccess", "licenseTrialDays", "hostedCta") + else: + script = (ROOT / "engraphis/classic_assets/dashboard.js").read_text(encoding="utf-8") + functions = ("licAccessState", "licPlanKey", "licTrialAvailable", + "licAccessLive", "licTrialDays", "hostedCta") + cases = [ + ({"trial_days": 3, "days_by_plan": {"pro": 3, "team": 10}}, + ["Start 3-day Pro trial", "Start 10-day Team trial"]), + ({"trial_days": 3, "days_by_plan": {"pro": 5, "team": 17}}, + ["Start 5-day Pro trial", "Start 17-day Team trial"]), + ({"trial_days": 3}, ["Start 3-day Pro trial", "Start Team trial"]), + ({"trial_days": 3, "days_by_plan": {"team": "10"}}, + ["Start 3-day Pro trial", "Start Team trial"]), + ({"trial_days": 3, "days_by_plan": {"team": True}}, + ["Start 3-day Pro trial", "Start Team trial"]), + ({"trial_days": 3, "days_by_plan": {"team": 0}}, + ["Start 3-day Pro trial", "Start Team trial"]), + ({}, ["Start Pro trial", "Start Team trial"]), + ] + # Isolate display semantics; real checkout routing is covered by the browser suite. + setup = """ +const state = {license:null}; +let LIC = null; +function hostedPlanUrl(plan,trial){return `https://example.test/?plan=${plan}&trial=${trial}`} +function hostedAccountUrl(){return 'https://example.test/account'} +""" + driver = """ +const output = JSON.parse(process.argv[2]).map(trial => { + LIC = state.license = {plan:'local', plan_source:'local', access_state:'inactive', + trial:{available:true, ...trial}}; + return ['pro','team'].map(plan => hostedCta(plan).label); +}); +process.stdout.write(JSON.stringify(output)); +""" + runner = tmp_path / "trial_ctas.js" + runner.write_text("\n".join([setup, *(_function(script, name) for name in functions), driver]), + encoding="utf-8") + result = subprocess.run(["node", str(runner), json.dumps([trial for trial, _ in cases])], + capture_output=True, text=True, timeout=30) + assert result.returncode == 0, result.stderr + assert json.loads(result.stdout) == [expected for _, expected in cases] def test_public_pro_ctas_use_documentation_attribution(): diff --git a/tests/test_recall.py b/tests/test_recall.py index 2a3db701..9130fa6c 100644 --- a/tests/test_recall.py +++ b/tests/test_recall.py @@ -762,6 +762,47 @@ def test_graph_arm_expands_an_older_unmentioned_link_endpoint_from_incidence(mon assert older_unmentioned in scores +def test_graph_arm_keeps_older_two_hop_link_evidence_beyond_500_newer_memories(): + from engraphis.core.interfaces import Node + + store, emb, eng = _engine() + wid = store.get_or_create_workspace("older-two-hop") + rid = store.get_or_create_repo(wid, "repo") + redis = store.upsert_entity(Node( + id="", name="Redis", ntype="technology", workspace_id=wid, repo_id=rid, + )) + attached = _add(store, emb, wid, rid, "The cache migration is attached evidence.", + valid_from=1000, ingested_at=1000) + intermediate = _add(store, emb, wid, rid, "The migration requires a staged rollout.", + valid_from=1000, ingested_at=1000) + older_evidence = _add(store, emb, wid, rid, "The rollout needs a signed backup first.", + valid_from=1000, ingested_at=1000) + store.link_memory_entity( + memory_id=attached, entity_id=redis, workspace_id=wid, repo_id=rid, + source_kind="test", confidence=1.0, + ) + store.add_link(attached, intermediate, relation="supports") + store.add_link(intermediate, older_evidence, relation="supports") + try: + with store.write_transaction(): + for index in range(501): + store.add_memory(MemoryRecord( + id="", content=f"Unrelated filler number {index}.", + workspace_id=wid, repo_id=rid, + valid_from=2000 + index, ingested_at=2000 + index, + )) + flt = SearchFilter(workspace_id=wid, repo_id=rid) + assert older_evidence not in store.list_memory_ids(flt, limit=500) + + scores = eng._graph_arm_ppr( + "How does Redis relate to the rollout?", flt, now=10**12, + ) + + assert {attached, intermediate, older_evidence}.issubset(scores) + finally: + store.close() + + def test_entity_backfill_preserves_closed_workspace_memory_history(): from engraphis.core.interfaces import Node diff --git a/tests/test_resolver_acceptance.py b/tests/test_resolver_acceptance.py new file mode 100644 index 00000000..7d470db7 --- /dev/null +++ b/tests/test_resolver_acceptance.py @@ -0,0 +1,107 @@ +"""Acceptance metrics must catch lost writes, including false duplicate decisions.""" +import json +from types import SimpleNamespace + +from engraphis.core.engine import MemoryEngine +from engraphis.core.interfaces import MemoryRecord +from engraphis.core.resolve import ResolutionOp, resolve +from eval import resolver_reworded_corrections as evaluator + + +def _dataset(tmp_path, rows): + path = tmp_path / "resolver.jsonl" + path.write_text("\n".join(json.dumps(row) for row in rows), encoding="utf-8") + return path + + +def test_negative_noop_is_an_error_and_fails_cli(tmp_path, monkeypatch): + path = _dataset(tmp_path, [{ + "id": "distinct", "neighbor": "Old claim.", + "candidate": "Different claim.", "expected": "add", + }]) + monkeypatch.setattr(evaluator, "resolve", lambda *a, **k: SimpleNamespace( + op=ResolutionOp.NOOP, reason="incorrect duplicate", + )) + report = evaluator.evaluate(path) + assert report["false_invalidations"] == 0 + assert report["false_noops"] == 1 + assert report["distinct_fact_error_rate"] == 1 + assert evaluator.main(["--dataset", str(path)]) == 1 + + +def test_end_to_end_evaluator_uses_real_writes_and_not_injected_similarity(tmp_path, monkeypatch): + path = _dataset(tmp_path, [ + {"id": "correction", "neighbor": "The cache TTL is 30 seconds.", + "candidate": "The cache TTL is 90 seconds.", "expected": "invalidate", + "subject_key": "cache", "claim_kind": "ttl"}, + {"id": "distinct", "neighbor": "The production worker runs daily.", + "candidate": "The staging worker runs daily.", "expected": "add"}, + ]) + + def no_unit_shortcut(*args, **kwargs): + raise AssertionError("production evaluation must use the real write path") + + monkeypatch.setattr(evaluator, "resolve", no_unit_shortcut) + report = evaluator.evaluate(path, end_to_end=True) + assert report["execution"] == "production_write_path" + assert report["similarity_injected"] is False + assert report["correction_recall"] == 1 + assert report["distinct_fact_error_rate"] == 0 + assert report["lost_distinct_facts"] == 0 + + +def test_reordered_environment_bindings_keep_distinct_unkeyed_facts(): + old = "The staging database holds 300 connections in production environment." + new = "The production database holds 300 connections in staging environment." + result = resolve(new, [(0.9, MemoryRecord(id="mem_old", content=old))]) + assert result.op == ResolutionOp.RELATE + engine = MemoryEngine.create(":memory:") + try: + workspace = engine.store.get_or_create_workspace("acceptance") + first = engine.remember_with_resolution(old, workspace_id=workspace) + second = engine.remember_with_resolution(new, workspace_id=workspace) + assert first["id"] != second["id"] + assert engine.store.get_memory(first["id"]).valid_to is None + assert engine.store.get_memory(second["id"]).content == new + finally: + engine.store.close() + + +def test_repeated_identical_environment_bindings_still_deduplicate(): + text = "The staging database holds 300 connections in production environment." + result = resolve(text, [(0.9, MemoryRecord(id="mem_old", content=text))]) + assert result.op == ResolutionOp.NOOP + + +def test_acceptance_json_reports_complete_counts_without_source_text(tmp_path, capsys): + old = "The production cache expires after 30 seconds." + new = "The production cache expires after 90 seconds." + path = _dataset(tmp_path, [{ + "id": "correction", "neighbor": old, "candidate": new, + "expected": "invalidate", + }]) + assert evaluator.main(["--dataset", str(path), "--end-to-end", "--json"]) == 0 + output = capsys.readouterr().out + report = json.loads(output) + assert report["protocol"]["n_total"] == report["protocol"]["n_scored"] == 1 + assert report["metrics"]["correction_precision"] == 1 + assert report["metrics"]["similarity_injected"] is False + assert old not in output and new not in output and str(tmp_path) not in output + assert report["suite"]["sha256"] + assert report["system"]["config_sha256"] + assert report["protocol"]["command"] == [ + "python", "-m", "eval.resolver_reworded_corrections", + "--dataset", path.name, "--json", "--end-to-end", + ] + assert {"engine.py", "store.py", "embedder_deterministic.py"} <= { + source["name"] for source in report["suite"]["sources"] + } + + +def test_repo_dataset_and_audit_mode_are_replayable(capsys): + assert evaluator.main(["--dataset", str(evaluator.DATASET), "--json", "--audit-only"]) == 0 + report = json.loads(capsys.readouterr().out) + assert report["protocol"]["command"] == [ + "python", "-m", "eval.resolver_reworded_corrections", + "--dataset", "eval/datasets/resolver_reworded_corrections.jsonl", "--json", "--audit-only", + ] diff --git a/tests/test_storage_concurrency_repair.py b/tests/test_storage_concurrency_repair.py new file mode 100644 index 00000000..34a3a641 --- /dev/null +++ b/tests/test_storage_concurrency_repair.py @@ -0,0 +1,397 @@ +"""Database writer boundaries, durable external repair, and bounded exact search.""" +import hashlib +import multiprocessing +import shutil +import sqlite3 +import threading +from concurrent.futures import ThreadPoolExecutor + +import numpy as np +import pytest + +from engraphis.core.interfaces import MemoryRecord, Scope, SearchFilter +from engraphis.core.vector_repair import canonical_search_required, index_repair_identity +from engraphis.factory import create_memory_engine + + +class ExternalIndex: + index_identity = "test-storage-concurrency-repair" + + def __init__(self): + self.rows = {} + self.fail = False + self.published = [] + + def search(self, vector, k, *, filter=None): + query = vector / max(float(np.linalg.norm(vector)), 1e-12) + return sorted(((mid, float(value @ query)) for mid, value in self.rows.items()), + key=lambda row: (-row[1], row[0]))[:k] + + def upsert(self, ids, vectors, meta=None, *, commit=True): + if self.fail: + raise RuntimeError("injected external failure") + self.published.extend(ids) + for mid, vector in zip(ids, vectors): + self.rows[mid] = vector / max(float(np.linalg.norm(vector)), 1e-12) + + def delete(self, ids, *, commit=True): + if self.fail: + raise RuntimeError("injected external failure") + for mid in ids: + self.rows.pop(mid, None) + + +def _use_external(engine, index): + engine.index = index + engine.recall_engine.index = index + + +def _process_remember(path, workspace, ready, proceed, results): + engine = create_memory_engine(path, auto_evolve=False) + try: + ready.put(True) + if not proceed.wait(20): + raise RuntimeError("writer synchronization timed out") + result = engine.remember_with_resolution( + "Atlas stores memory in SQLite.", workspace_id=workspace, + ) + results.put((result["op"], result["id"])) + finally: + engine.close() + + +def _process_claim(path, workspace, seconds, ready, proceed, results): + engine = create_memory_engine(path, auto_evolve=False) + try: + ready.put(True) + if not proceed.wait(20): + raise RuntimeError("writer synchronization timed out") + result = engine.remember_with_resolution( + f"The cache TTL is {seconds} seconds.", workspace_id=workspace, + subject_key="cache", claim_kind="ttl", + ) + results.put((result["op"], result["id"])) + finally: + engine.close() + + +def test_independent_engine_writes_resolve_after_writer_reservation(tmp_path, monkeypatch): + path = str(tmp_path / "concurrent.db") + first = create_memory_engine(path, auto_evolve=False) + workspace = first.store.get_or_create_workspace("concurrent") + second = create_memory_engine(path, auto_evolve=False) + barrier = threading.Barrier(2, timeout=10) + for engine in (first, second): + original = engine.embedder.embed + + def embed(texts, *, kind="text", original=original, engine=engine): + # Expensive embedding must remain outside the database reservation. + assert not engine.store.conn.transaction_owned_by_current_thread() + vectors = original(texts, kind=kind) + barrier.wait() + return vectors + + monkeypatch.setattr(engine.embedder, "embed", embed) + try: + with ThreadPoolExecutor(max_workers=2) as pool: + results = list(pool.map(lambda engine: engine.remember_with_resolution( + "Atlas stores memory in SQLite.", workspace_id=workspace, + ), (first, second))) + assert sorted(item["op"] for item in results) == ["add", "noop"] + assert len({item["id"] for item in results}) == 1 + assert first.store.count_memories() == 1 + finally: + first.close() + second.close() + + +def test_separate_process_writers_do_not_duplicate(tmp_path): + path = str(tmp_path / "processes.db") + engine = create_memory_engine(path, auto_evolve=False) + workspace = engine.store.get_or_create_workspace("processes") + engine.close() + context = multiprocessing.get_context("spawn") + ready, results = context.Queue(), context.Queue() + proceed = context.Event() + workers = [context.Process(target=_process_remember, + args=(path, workspace, ready, proceed, results)) + for _ in range(2)] + try: + for worker in workers: + worker.start() + for _ in workers: + assert ready.get(timeout=20) + proceed.set() + observed = [results.get(timeout=20) for _ in workers] + assert sorted(item[0] for item in observed) == ["add", "noop"] + assert len({item[1] for item in observed}) == 1 + for worker in workers: + worker.join(timeout=20) + assert worker.exitcode == 0 + finally: + for worker in workers: + if worker.is_alive(): + worker.terminate() + worker.join(timeout=10) + ready.close() + results.close() + + +def test_separate_process_contradictions_preserve_one_current_claim_and_history(tmp_path): + path = str(tmp_path / "contradictions.db") + engine = create_memory_engine(path, auto_evolve=False) + workspace = engine.store.get_or_create_workspace("processes") + original = engine.remember( + "The cache TTL is 30 seconds.", workspace_id=workspace, + subject_key="cache", claim_kind="ttl", + ) + engine.close() + context = multiprocessing.get_context("spawn") + ready, results = context.Queue(), context.Queue() + proceed = context.Event() + workers = [context.Process(target=_process_claim, + args=(path, workspace, seconds, ready, proceed, results)) + for seconds in (90, 180)] + try: + for worker in workers: + worker.start() + for _ in workers: + assert ready.get(timeout=20) + proceed.set() + observed = [results.get(timeout=20) for _ in workers] + assert [item[0] for item in observed] == ["invalidate", "invalidate"] + assert len({item[1] for item in observed}) == 2 + for worker in workers: + worker.join(timeout=20) + assert worker.exitcode == 0 + finally: + for worker in workers: + if worker.is_alive(): + worker.terminate() + worker.join(timeout=10) + ready.close() + results.close() + reopened = create_memory_engine(path, auto_evolve=False) + try: + ids = [original, *(item[1] for item in observed)] + records = [reopened.store.get_memory(mid) for mid in ids] + assert {record.content for record in records} == { + "The cache TTL is 30 seconds.", "The cache TTL is 90 seconds.", + "The cache TTL is 180 seconds.", + } + live = [record for record in records if record.valid_to is None] + assert len(live) == 1 and live[0].id != original + predecessor = reopened.store.get_memory(live[0].metadata["supersedes"][0]) + assert predecessor.id != original + assert predecessor.metadata["supersedes"] == [original] + assert predecessor.valid_to is not None + assert reopened.store.conn.execute("PRAGMA foreign_key_check").fetchall() == [] + finally: + reopened.close() + + +def test_partial_external_failure_preserves_resolution_and_repairs_live(): + engine = create_memory_engine(auto_evolve=False) + index = ExternalIndex() + _use_external(engine, index) + workspace = engine.store.get_or_create_workspace("repair") + try: + engine.remember("Humpback whales sing underwater.", workspace_id=workspace) + index.fail = True + first = engine.remember_with_resolution( + "Atlas stores memory in SQLite.", workspace_id=workspace, + ) + assert canonical_search_required(index, engine.store) + index.fail = False + repeated = engine.remember_with_resolution( + "Atlas stores memory in SQLite.", workspace_id=workspace, + ) + assert repeated["op"] == "noop" + assert repeated["id"] == first["id"] + repaired = engine.repair_vector_index() + assert repaired == {"attempted": 1, "repaired": 1, "pending": 0} + assert first["id"] in index.rows + assert not canonical_search_required(index, engine.store) + finally: + engine.close() + + +def test_repair_survives_restart_and_never_republishes_erased_memory(tmp_path): + path = str(tmp_path / "repair.db") + engine = create_memory_engine(path, auto_evolve=False) + index = ExternalIndex() + _use_external(engine, index) + workspace = engine.store.get_or_create_workspace("repair") + first = engine.remember("Atlas stores memory in SQLite.", workspace_id=workspace) + index.fail = True + second = engine.remember("Whales sing underwater.", workspace_id=workspace) + # The canonical erasure transaction queues an external DELETE even when the + # backend is unavailable. No memory content is retained in that work item. + engine.store.secure_erase_memory(first) + engine.close() + reopened = create_memory_engine(path, auto_evolve=False) + _use_external(reopened, index) + index.fail = False + prior_publications = len(index.published) + try: + rows = reopened.store.conn.execute("SELECT * FROM vector_index_repairs").fetchall() + assert len(rows) == 2 + assert set(rows[0].keys()) == {"identity", "memory_id", "generation"} + outcome = reopened.repair_vector_index() + assert outcome == {"attempted": 2, "repaired": 2, "pending": 0} + assert first not in index.rows + assert first not in index.published[prior_publications:] + assert second in index.rows + finally: + reopened.close() + + +def test_repair_work_rolls_back_with_failed_authoritative_write(monkeypatch): + engine = create_memory_engine(auto_evolve=False) + index = ExternalIndex() + _use_external(engine, index) + workspace = engine.store.get_or_create_workspace("rollback") + + def fail(*args, **kwargs): + raise RuntimeError("injected late failure") + + monkeypatch.setattr(engine, "_evolve", fail) + try: + with pytest.raises(RuntimeError, match="injected late failure"): + engine.remember("Atlas stores memory in SQLite.", workspace_id=workspace) + assert engine.store.count_memories() == 0 + assert engine.store.conn.execute("SELECT COUNT(*) FROM vector_index_repairs").fetchone()[0] == 0 + assert not index.rows + finally: + engine.close() + + +def test_bounded_vector_batches_preserve_exact_order_and_scope(monkeypatch): + import engraphis.core.store as store_module + + monkeypatch.setattr(store_module, "VECTOR_SCAN_BATCH", 7) + engine = create_memory_engine(embed_dim=3, auto_evolve=False) + workspace = engine.store.get_or_create_workspace("vectors") + other = engine.store.get_or_create_workspace("other") + try: + for number in range(40): + engine.store.add_memory(MemoryRecord( + id=f"mem_{number:04d}", content=str(number), scope=Scope.WORKSPACE, + workspace_id=workspace, embedding=np.array([1.0, number % 4, 0.0]), + metadata={"embed_model": engine.embedding_space}, + )) + engine.store.add_memory(MemoryRecord( + id="mem_other", content="other", scope=Scope.WORKSPACE, workspace_id=other, + embedding=np.array([1.0, 0.0, 0.0]), + metadata={"embed_model": engine.embedding_space}, + )) + flt = SearchFilter(workspace_id=workspace) + query = np.array([1.0, 0.0, 0.0], dtype=np.float32) + ids, matrix = engine.store.vector_matrix(flt, dim=3) + expected = sorted(zip(ids, (matrix @ query).tolist()), key=lambda item: (-item[1], item[0]))[:11] + original = engine.store.iter_vector_matrices + sizes = [] + + def batches(*args, **kwargs): + for batch_ids, batch_matrix in original(*args, **kwargs): + sizes.append(len(batch_ids)) + yield batch_ids, batch_matrix + + monkeypatch.setattr(engine.store, "iter_vector_matrices", batches) + actual = engine.index.search(query, 11, filter=flt) + assert actual == expected + assert len(sizes) > 1 and max(sizes) <= 7 + assert not engine.store.conn.in_transaction + finally: + engine.close() + + +def test_external_target_identity_does_not_persist_connection_secrets(): + engine = create_memory_engine(auto_evolve=False) + index = ExternalIndex() + index.index_identity = "https://user:credential@example.invalid/private" + try: + target = index_repair_identity(index, engine.store) + assert target.startswith("index:v1:") + assert "credential" not in target + finally: + engine.close() + + +def test_recall_uses_canonical_vectors_and_reports_pending_repairs(monkeypatch): + from engraphis import factory + from engraphis.backends import DeterministicEmbedder + from engraphis.core.retrieval_policy import ProfileConfig + + class SemanticEmbedder(DeterministicEmbedder): + supports_semantic_search = True + embedding_mode = "semantic" + + monkeypatch.setattr(factory, "get_embedder", lambda *args, **kwargs: SemanticEmbedder(384)) + engine = create_memory_engine(auto_evolve=False) + index = ExternalIndex() + _use_external(engine, index) + workspace = engine.store.get_or_create_workspace("semantic-repair") + try: + engine.remember("Whales sing underwater.", workspace_id=workspace) + index.fail = True + memory = engine.remember("Atlas stores memory in SQLite.", workspace_id=workspace) + result = engine.recall_engine.recall( + "Atlas stores memory in SQLite.", SearchFilter(workspace_id=workspace), k=1, + arm_config=ProfileConfig("vector_only", True, False, False, False), + ) + assert result.chunks[0]["id"] == memory + assert result.vector_search_source == "canonical" + assert result.vector_index_repairs_pending == 1 + assert result.vector_search_ready + finally: + engine.close() + + +def test_v16_upgrade_adds_durable_repair_without_changing_memories(tmp_path): + from engraphis.core.store import Store + + path = str(tmp_path / "v16.db") + engine = create_memory_engine(path, auto_evolve=False) + workspace = engine.store.get_or_create_workspace("migration") + memory = engine.remember("Atlas stores memory in SQLite.", workspace_id=workspace) + engine.close() + # Model the v16 shape for the only tables/triggers this additive migration owns. + with sqlite3.connect(path) as previous: + for action in ("insert", "update", "delete"): + previous.execute(f"DROP TRIGGER trg_vector_repair_{action}") + for table in ("vector_index_repairs", "vector_index_targets", "vector_store_state"): + previous.execute(f"DROP TABLE {table}") + previous.execute("DELETE FROM schema_migrations WHERE version=17") + with Store(path) as upgraded: + assert upgraded.schema_version == 17 + assert upgraded.get_memory(memory).content == "Atlas stores memory in SQLite." + assert upgraded.vector_generation() == 0 + assert upgraded.conn.execute("PRAGMA foreign_key_check").fetchall() == [] + backup = tmp_path / "v16.db.pre-migration-v17.bak" + assert backup.exists() + with sqlite3.connect(str(backup)) as recovery: + assert recovery.execute("SELECT MAX(version) FROM schema_migrations").fetchone()[0] == 16 + assert recovery.execute("SELECT content FROM memories WHERE id=?", (memory,)).fetchone()[0] == ( + "Atlas stores memory in SQLite." + ) + # Restore into a disposable copy, start the real engine, and exercise retrieval + # plus a governed correction. The immutable backup and original stay untouched. + backup_digest = hashlib.sha256(backup.read_bytes()).digest() + restored_path = tmp_path / "restored.db" + shutil.copyfile(backup, restored_path) + restored = create_memory_engine(str(restored_path), auto_evolve=False) + try: + recalled = restored.recall("Atlas stores memory in SQLite.", workspace_id=workspace) + assert recalled.count == 1 and "SQLite" in recalled.context + corrected = restored.correct(memory, "Atlas stores restored memory in SQLite.", reason="recovery drill") + assert restored.store.get_memory(memory).valid_to is not None + assert restored.store.get_memory(corrected["id"]).metadata["supersedes"] == [memory] + assert restored.store.conn.execute("PRAGMA integrity_check").fetchone()[0] == "ok" + assert restored.store.conn.execute("PRAGMA foreign_key_check").fetchall() == [] + finally: + restored.close() + assert hashlib.sha256(backup.read_bytes()).digest() == backup_digest + with sqlite3.connect(path) as original: + assert original.execute("SELECT valid_to FROM memories WHERE id=?", (memory,)).fetchone()[0] is None + assert original.execute("SELECT count(*) FROM memories").fetchone()[0] == 1 diff --git a/tests/test_store_fts_insert.py b/tests/test_store_fts_insert.py new file mode 100644 index 00000000..49d41c9d --- /dev/null +++ b/tests/test_store_fts_insert.py @@ -0,0 +1,167 @@ +"""Atomic new-row insertion avoids an unindexed FTS scan; repair still replaces.""" +import pytest + +from engraphis.core.interfaces import MemoryRecord +from engraphis.core.store import Store + + +@pytest.fixture(params=["fts5", "fallback"]) +def store(request): + store = Store(":memory:") + if request.param == "fts5" and not store.has_fts5: + store.close() + pytest.skip("SQLite FTS5 unavailable") + if request.param == "fallback": + store.conn.execute("DROP TABLE mem_fts") + store.conn.execute( + "CREATE TABLE mem_fts(id TEXT PRIMARY KEY, title TEXT, content TEXT, keywords TEXT)" + ) + store.has_fts5 = False + store.conn.commit() + yield store + store.close() + + +def _record(store, mid="mem_fts_insert", content="original searchable evidence"): + return MemoryRecord(id=mid, content=content, + workspace_id=store.get_or_create_workspace("fts-insert")) + + +def _mirrors(store, mid): + return [row["content"] for row in store.conn.execute( + "SELECT content FROM mem_fts WHERE id=?", (mid,), + ).fetchall()] + + +def test_new_rows_do_not_scan_fts_and_updates_replace_searchable_content(store): + calls = [] + + def observe(sql): + if "DELETE FROM mem_fts" in sql: + calls.append(sql) + + store.conn.set_trace_callback(observe) + record = _record(store) + store.add_memory(record) + store.add_memory(_record(store, mid="mem_fts_second")) + assert calls == [] + assert _mirrors(store, record.id) == [record.content] + record.content = "replacement searchable evidence" + store.add_memory(record) + assert len(calls) == 1 + assert _mirrors(store, record.id) == [record.content] + assert store.fts_search("replacement", k=5)[0][0] == record.id + assert record.id not in {mid for mid, _ in store.fts_search("original", k=5)} + + +def test_update_cleans_existing_duplicate_mirrors(store): + if not store.has_fts5: + pytest.skip("plain-table primary key already prevents duplicates") + record = _record(store) + store.add_memory(record) + store.conn.execute( + "INSERT INTO mem_fts(id,title,content,keywords) VALUES (?,?,?,?)", + (record.id, "", "stale duplicate", ""), + ) + store.conn.commit() + assert len(_mirrors(store, record.id)) == 2 + record.content = "correct replacement" + store.add_memory(record) + assert _mirrors(store, record.id) == [record.content] + + +def test_explicit_fts_repair_replaces_orphan_mirror(store): + store.conn.execute( + "INSERT INTO mem_fts(id,title,content,keywords) VALUES (?,?,?,?)", + ("mem_orphan", "", "stale orphan", ""), + ) + store._fts_upsert("mem_orphan", "", "repaired orphan", "") + assert _mirrors(store, "mem_orphan") == ["repaired orphan"] + + +@pytest.mark.parametrize("first_insert", [True, False]) +def test_new_canonical_insert_replaces_preexisting_orphan(store, first_insert): + record = _record(store, mid="mem_legacy_orphan", content="correct canonical evidence") + store.conn.execute( + "INSERT INTO mem_fts(id,title,content,keywords) VALUES (?,?,?,?)", + (record.id, "", "stale orphan", ""), + ) + if store.has_fts5: + store.conn.execute( + "INSERT INTO mem_fts(id,title,content,keywords) VALUES (?,?,?,?)", + (record.id, "", "second stale orphan", ""), + ) + store.conn.commit() + if not first_insert: + store.add_memory(_record(store, mid="mem_unrelated")) + store.add_memory(record) + assert _mirrors(store, record.id) == [record.content] + assert store.fts_search("correct", k=5)[0][0] == record.id + + +def test_orphan_repair_inventory_remains_safe_after_outer_rollback(store): + record = _record(store, mid="mem_legacy_orphan") + store.conn.execute( + "INSERT INTO mem_fts(id,title,content,keywords) VALUES (?,?,?,?)", + (record.id, "", "legacy orphan", ""), + ) + store.conn.commit() + store.conn.execute("BEGIN IMMEDIATE") + store.add_memory(record, commit=False) + store.conn.rollback() + assert _mirrors(store, record.id) == ["legacy orphan"] + store.add_memory(record) + assert _mirrors(store, record.id) == [record.content] + + +def test_direct_fts_repair_after_inventory_is_safe_for_later_canonical_insert(store): + store.add_memory(_record(store)) + store._fts_upsert("mem_later_orphan", "", "orphan created by repair", "") + store.conn.commit() + record = _record(store, mid="mem_later_orphan", content="canonical replacement") + store.add_memory(record) + assert _mirrors(store, record.id) == [record.content] + + +def test_failed_mirror_insert_rolls_back_canonical_and_fts_rows(store, monkeypatch): + record = _record(store) + original = store._fts_upsert + + def fail_after_mirror(*args, **kwargs): + original(*args, **kwargs) + raise RuntimeError("failure after mirror insertion") + + monkeypatch.setattr(store, "_fts_upsert", fail_after_mirror) + with pytest.raises(RuntimeError, match="failure after mirror"): + store.add_memory(record) + assert store.get_memory(record.id) is None + assert _mirrors(store, record.id) == [] + monkeypatch.undo() + store.add_memory(record) + assert _mirrors(store, record.id) == [record.content] + + +def test_outer_transaction_rollback_removes_both_new_rows(store): + record = _record(store) + store.conn.execute("BEGIN IMMEDIATE") + store.add_memory(record, commit=False) + assert _mirrors(store, record.id) == [record.content] + store.conn.rollback() + assert store.get_memory(record.id) is None + assert _mirrors(store, record.id) == [] + + +def test_erase_removes_all_mirrors_before_same_id_is_reinserted(store): + record = _record(store) + store.add_memory(record) + if store.has_fts5: + store.conn.execute( + "INSERT INTO mem_fts(id,title,content,keywords) VALUES (?,?,?,?)", + (record.id, "", "stale duplicate", ""), + ) + store.conn.commit() + store.secure_erase_memory(record.id) + assert store.get_memory(record.id) is None + assert _mirrors(store, record.id) == [] + store.add_memory(record) + assert _mirrors(store, record.id) == [record.content] diff --git a/tests/test_sync.py b/tests/test_sync.py index b980c030..b8ff9a90 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -3048,10 +3048,10 @@ def test_apply_bundle_rolls_back_a_failed_inflight_store_write(monkeypatch): monkeypatch.setattr(sync_mod, "APPLY_BATCH", 2) real_fts_upsert = store._fts_upsert - def exploding_fts_upsert(mid, title, content, keywords): + def exploding_fts_upsert(mid, title, content, keywords, **kwargs): if mid == "mem_2": raise RuntimeError("fts on fire") - return real_fts_upsert(mid, title, content, keywords) + return real_fts_upsert(mid, title, content, keywords, **kwargs) monkeypatch.setattr(store, "_fts_upsert", exploding_fts_upsert) with pytest.raises(RuntimeError, match="fts on fire"): diff --git a/tests/test_update.py b/tests/test_update.py index 4e741681..cb027a0f 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -13,6 +13,12 @@ from scripts import update +@pytest.fixture(autouse=True) +def isolate_installation_profile(tmp_path, monkeypatch): + from scripts import installation_profile + monkeypatch.setattr(installation_profile, "profile_path", lambda _config_path=None: tmp_path / "profile.json") + + class _FakeProcess: """A ``Popen`` stand-in: the updater reads ``pid``/``returncode`` and drains once. diff --git a/tests/test_vector_numpy.py b/tests/test_vector_numpy.py index 4615906a..3525748c 100644 --- a/tests/test_vector_numpy.py +++ b/tests/test_vector_numpy.py @@ -196,14 +196,14 @@ def unexpected_iter(*args, **kwargs): raise AssertionError("search must not hydrate vectors row by row") calls = [] - original_matrix = store.vector_matrix + original_matrix = store.iter_vector_matrices def traced_matrix(*args, **kwargs): calls.append((args, kwargs)) return original_matrix(*args, **kwargs) monkeypatch.setattr(store, "iter_vectors", unexpected_iter) - monkeypatch.setattr(store, "vector_matrix", traced_matrix) + monkeypatch.setattr(store, "iter_vector_matrices", traced_matrix) flt = SearchFilter(workspace_id=wid, repo_id=allowed_repo) query = np.array([1.0, 0.0, 0.0], dtype=np.float32) diff --git a/tests/test_vector_scale_storage.py b/tests/test_vector_scale_storage.py new file mode 100644 index 00000000..27548fbb --- /dev/null +++ b/tests/test_vector_scale_storage.py @@ -0,0 +1,104 @@ +import json + +import pytest + +from eval.benchmark import validate_report, write_canonical_artifact +from eval import vector_scale_storage as scale + + +def _run(**kwargs): + return scale.run_file_backed( + [5, 12], dim=8, queries=2, iterations=1, warmups=0, + k=2, seed=17, batch_size=3, mixed_writes=1, **kwargs, + ) + + +def test_file_backed_matrix_reports_durable_writes_and_resource_boundaries(tmp_path): + report = _run(backend="numpy") + assert validate_report(report) == [] + cells = report["metrics"]["cells"] + assert {(row["corpus_size"], row["concurrency"]) for row in cells} == { + (size, concurrency) for size in (5, 12) for concurrency in (1, 4, 16) + } + assert all(row["numpy_reference_parity"] for row in cells) + for row in report["metrics"]["storage"]: + assert row["durable_memory_rows"] == row["durable_vector_rows"] == row["corpus_size"] + 3 + assert all(item["committed_writes"] == 1 for item in row["mixed"]) + assert row["restart_ms"] >= 0 + assert row["final_disk"]["database_bytes"] > 0 + assert row["rebuild"]["measured"] is False + assert report["metrics"]["source_stable"] + assert report["models"]["vector_backend"]["identity"] == "NumpyVectorIndex" + command = report["protocol"]["command"] + for flag, value in (("--dim", "8"), ("--queries", "2"), ("--iterations", "1"), + ("--warmups", "0"), ("--k", "2"), ("--seed", "17"), + ("--mixed-writes", "1"), ("--batch-size", "3"), + ("--concurrencies", "1,4,16"), ("--tenants", "4")): + assert command[command.index(flag) + 1] == value + assert "not end-to-end" in report["metrics"]["measurement_scope"] + encoded = json.dumps(report) + assert "Synthetic exact-index record" not in encoded + assert "corpus.db" not in encoded + assert str(tmp_path) not in encoded + written = write_canonical_artifact(report, tmp_path / "scale.json") + assert written["sha256"] + + +def test_invalid_matrix_is_rejected_before_opening_storage(monkeypatch): + def must_not_open(*args, **kwargs): + raise AssertionError("validation must precede storage mutation") + + monkeypatch.setattr(scale, "Store", must_not_open) + with pytest.raises(ValueError, match="concurrencies"): + _run(concurrencies=[1, 1]) + with pytest.raises(ValueError, match="positive"): + scale.run_file_backed([10], batch_size=0) + + +def test_source_drift_is_reported_without_claiming_matching_evidence(monkeypatch): + snapshots = iter([{"revision": "before"}, {"revision": "after"}]) + monkeypatch.setattr(scale, "_source_snapshot", lambda: next(snapshots)) + report = _run(backend="numpy", concurrencies=[1]) + assert report["metrics"]["source_stable"] is False + + +def test_native_rebuild_and_restart_match_numpy_when_available(): + pytest.importorskip("sqlite_vec") + native = _run(backend="sqlite-vec", concurrencies=[1]) + numpy = _run(backend="numpy", concurrencies=[1]) + assert native["protocol"]["config"]["inputs"] == numpy["protocol"]["config"]["inputs"] + assert [cell["result_ids_sha256"] for cell in native["metrics"]["cells"]] == [ + cell["result_ids_sha256"] for cell in numpy["metrics"]["cells"] + ] + for row in native["metrics"]["storage"]: + assert row["rebuild"]["measured"] is True + assert row["rebuild"]["records_replayed"] == row["durable_memory_rows"] + assert native["models"]["vector_backend"]["identity"] == "SqliteVecVectorIndex" + + +def test_interrupted_matrix_preserves_completed_cell_timings(tmp_path, monkeypatch): + original = scale._reads + calls = 0 + + def interrupted_reads(*args, **kwargs): + nonlocal calls + calls += 1 + if calls == 3: # cold read and the first measured cell already completed + raise RuntimeError("simulated interruption") + return original(*args, **kwargs) + + monkeypatch.setattr(scale, "_reads", interrupted_reads) + output = tmp_path / "interrupted.checkpoint.json" + with pytest.raises(RuntimeError, match="simulated interruption"): + _run(checkpoint=lambda payload: scale.write_scale_checkpoint(output, payload)) + checkpoint = json.loads(output.read_text()) + assert checkpoint["status"] == "incomplete" + assert checkpoint["config"]["seed"] == 17 + assert checkpoint["source_before"]["files"] + assert len(checkpoint["completed_cells"]) == 1 + cell = checkpoint["completed_cells"][0] + assert cell["latency_ms"]["p50"] > 0 + assert cell["numpy_reference_parity"] + assert checkpoint["completed_storage"] == [] + assert "Synthetic exact-index record" not in output.read_text() + assert not list(tmp_path.glob("*.tmp")) diff --git a/tests/test_vector_scan_plan.py b/tests/test_vector_scan_plan.py new file mode 100644 index 00000000..e2eb06f4 --- /dev/null +++ b/tests/test_vector_scan_plan.py @@ -0,0 +1,15 @@ +from eval.vector_scan_plan import run_comparison + + +def test_join_order_ablation_preserves_selective_and_full_vectors(): + report = run_comparison([11], dim=4, batch_size=3) + cells = report["metrics"]["cells"] + assert len(cells) == 24 + for offset in range(0, len(cells), 4): + baseline, optimized, adaptive, scoped_first = cells[offset:offset + 4] + assert baseline["result_sha256"] == optimized["result_sha256"] + assert baseline["result_sha256"] == adaptive["result_sha256"] + assert baseline["result_sha256"] == scoped_first["result_sha256"] + assert baseline["rows"] == optimized["rows"] + assert not any("TEMP B-TREE" in line for line in optimized["query_plan"]) + assert report["metrics"]["source_stable"] diff --git a/tests/test_vector_snapshot_plan.py b/tests/test_vector_snapshot_plan.py new file mode 100644 index 00000000..521e825b --- /dev/null +++ b/tests/test_vector_snapshot_plan.py @@ -0,0 +1,81 @@ +import threading + +import numpy as np +import pytest + +from engraphis.core import store as store_module +from engraphis.core.interfaces import MemoryRecord, SearchFilter +from engraphis.core.store import Store + + +@pytest.mark.parametrize("count", [0, 1, 2, 3, 7]) +def test_adaptive_scan_returns_exact_sorted_bounded_batches(count, monkeypatch): + store = Store(":memory:") + try: + wid = store.get_or_create_workspace("snapshot-plan") + repo = store.get_or_create_repo(wid, "selected") + other = store.get_or_create_repo(wid, "other") + for number in reversed(range(9)): + store.add_memory(MemoryRecord( + id=f"mem_order_{number}", content="vector scan evidence", workspace_id=wid, + repo_id=repo if number < count else other, + embedding=np.array([number + 1, 1, 0, 0], dtype=np.float32), + )) + monkeypatch.setattr(store_module, "VECTOR_SCAN_BATCH", 2) + flt = SearchFilter(workspace_id=wid, repo_id=repo) + batches = list(store.iter_vector_matrices(flt, dim=4)) + assert all(1 <= len(mids) <= 2 and matrix.shape == (len(mids), 4) + for mids, matrix in batches) + mids = [mid for ids, _ in batches for mid in ids] + assert mids == [f"mem_order_{number}" for number in range(count)] + expected = dict(store.iter_vectors(flt, dim=4)) + for ids, matrix in batches: + for mid, vector in zip(ids, matrix): + np.testing.assert_array_equal(vector, expected[mid]) + assert not store.conn.in_transaction + finally: + store.close() + + +@pytest.mark.parametrize("count", [1, 5]) +def test_early_generator_close_releases_snapshot_for_waiting_writer(count, monkeypatch): + store = Store(":memory:") + worker = None + stream = None + try: + wid = store.get_or_create_workspace("snapshot-close") + for number in range(count): + store.add_memory(MemoryRecord( + id=f"mem_snapshot_{number}", content="snapshot evidence", workspace_id=wid, + embedding=np.array([1, number + 1, 0, 0], dtype=np.float32), + )) + monkeypatch.setattr(store_module, "VECTOR_SCAN_BATCH", 2) + stream = iter(store.iter_vector_matrices(dim=4)) + next(stream) + started, finished = threading.Event(), threading.Event() + errors = [] + + def write(): + started.set() + try: + store.put_vector("mem_snapshot_0", np.array([0, 1, 0, 0], dtype=np.float32)) + store.conn.commit() + except Exception as error: + errors.append(error) + finally: + finished.set() + + worker = threading.Thread(target=write) + worker.start() + assert started.wait(1) + assert not finished.wait(0.05) + stream.close() + assert finished.wait(2) + assert not errors + assert not store.conn.in_transaction + finally: + if stream is not None: + stream.close() + if worker is not None: + worker.join(timeout=2) + store.close() diff --git a/tests/test_vector_sqlitevec_backend.py b/tests/test_vector_sqlitevec_backend.py index d23e8d8f..dba74523 100644 --- a/tests/test_vector_sqlitevec_backend.py +++ b/tests/test_vector_sqlitevec_backend.py @@ -60,6 +60,40 @@ def test_knn_search_returns_ranked_hits(): store.close() +def test_unchanged_engine_reopen_does_not_replay_native_vectors(tmp_path, monkeypatch): + path = str(tmp_path / "unchanged-native.db") + engine = MemoryEngine.create(path, embed_dim=DIM, vector_backend="sqlite-vec", auto_evolve=False) + workspace = engine.store.get_or_create_workspace("unchanged") + engine.remember("Atlas stores memory in SQLite.", workspace_id=workspace) + engine.close() + + def unexpected(*args, **kwargs): + raise AssertionError("a verified unchanged index must not replay its vectors") + + monkeypatch.setattr(SqliteVecVectorIndex, "upsert", unexpected) + reopened = MemoryEngine.create(path, embed_dim=DIM, vector_backend="sqlite-vec", auto_evolve=False) + try: + assert reopened.index.can_skip_hydration() + finally: + reopened.close() + + +def test_canonical_change_invalidates_native_hydration_shortcut(): + store, wid, rid, emb, index = _fixture() + try: + _make(store, index, emb, wid, rid, "original indexed memory") + index.mark_rebuild_complete() + assert index.can_skip_hydration() + vec = emb.embed(["later canonical-only memory"])[0] + store.add_memory(MemoryRecord( + id="", content="later canonical-only memory", scope=Scope.REPO, + workspace_id=wid, repo_id=rid, embedding=vec, + )) + assert not index.can_skip_hydration() + finally: + store.close() + + def test_k_larger_than_index_is_capped_not_an_error(): store, wid, rid, emb, index = _fixture() only = _make(store, index, emb, wid, rid, "single resident vector") @@ -553,6 +587,78 @@ def fail_after_native_upsert(*_args, **_kwargs): eng.store.close() +@pytest.mark.parametrize("failure_at", [1, 2]) +def test_remember_many_native_failure_rolls_back_the_entire_batch(monkeypatch, failure_at): + engine = MemoryEngine.create( + ":memory:", embed_dim=DIM, vector_backend="sqlite-vec", auto_evolve=False, + ) + workspace_id = engine.store.get_or_create_workspace("native-batch-rollback") + original_id = engine.remember("Original retained evidence.", workspace_id=workspace_id) + original_upsert = engine.index.upsert + publications = 0 + + def fail_publication(*args, **kwargs): + nonlocal publications + publications += 1 + if publications == failure_at: + raise RuntimeError("injected native batch failure") + return original_upsert(*args, **kwargs) + + monkeypatch.setattr(engine.index, "upsert", fail_publication) + try: + with pytest.raises(RuntimeError, match="injected native batch failure"): + engine.remember_many( + ["Whales sing underwater.", "Atlas stores memory in SQLite."], + workspace_id=workspace_id, + ) + assert publications == failure_at + for table in ("memories", "mem_vectors", "mem_fts", "mem_vec_ann"): + assert [row[0] for row in engine.store.conn.execute( + f"SELECT id FROM {table}", + ).fetchall()] == [original_id] + assert not engine.store.conn.in_transaction + finally: + engine.close() + + +def test_remember_many_publishes_native_and_canonical_rows_in_one_commit(tmp_path, monkeypatch): + path = str(tmp_path / "native-batch.db") + engine = MemoryEngine.create( + path, embed_dim=DIM, vector_backend="sqlite-vec", auto_evolve=False, + ) + workspace_id = engine.store.get_or_create_workspace("native-batch-visible") + observer = MemoryEngine.create( + path, embed_dim=DIM, vector_backend="sqlite-vec", auto_evolve=False, + ) + original_upsert = engine.index.upsert + publications = [] + + def observe_publication(memory_ids, *args, **kwargs): + assert engine.store.conn.transaction_owned_by_current_thread() + original_upsert(memory_ids, *args, **kwargs) + publications.extend(memory_ids) + assert observer.store.count_memories() == 0 + assert observer.store.conn.execute("SELECT COUNT(*) FROM mem_vec_ann").fetchone()[0] == 0 + + monkeypatch.setattr(engine.index, "upsert", observe_publication) + try: + results = engine.remember_many( + ["Whales sing underwater.", "Atlas stores memory in SQLite."], + workspace_id=workspace_id, + ) + expected_ids = {item["id"] for item in results} + assert len(publications) == len(expected_ids) == 2 + assert set(publications) == expected_ids + for table in ("memories", "mem_vectors", "mem_vec_ann"): + assert {row[0] for row in observer.store.conn.execute( + f"SELECT id FROM {table}", + ).fetchall()} == expected_ids + assert not engine.store.conn.in_transaction + finally: + observer.close() + engine.close() + + def test_filtered_search_widens_past_invisible_rows_to_full_scan(): """A workspace dense with rows the filter hides forces the widening loop all the way to its full-scan cap — the k visible hits must still all be found.""" diff --git a/tools/galaxy_mode_test.js b/tools/galaxy_mode_test.js index 3b39cc87..63fee4fa 100644 --- a/tools/galaxy_mode_test.js +++ b/tools/galaxy_mode_test.js @@ -10,15 +10,17 @@ // capture the engine's settings + diagnostics at each point. // - We print a per-mode summary block at the end. -const { chromium } = require('@playwright/test'); const { spawn } = require('child_process'); const path = require('path'); +const net = require('net'); +const { randomBytes } = require('crypto'); -const REPO = __dirname; -process.chdir(REPO); +const REPO = path.resolve(__dirname, '..'); -const PORT = process.env.ENGRAPHIS_PLAYWRIGHT_PORT || 8801; -const BASE = `http://127.0.0.1:${PORT}`; +const configuredPort = process.env.ENGRAPHIS_PLAYWRIGHT_PORT; +let PORT = 0; +let BASE = ''; +const TEST_TOKEN = randomBytes(24).toString('hex'); const WORKSPACE = 'graph-manual-test'; const memoryCount = 8; @@ -35,11 +37,15 @@ const SPACETIME_SLIDERS = [ function log(msg) { console.log(`[${new Date().toISOString().slice(11, 19)}] ${msg}`); } function err(msg) { console.error(`[ERR] ${msg}`); } -async function waitForServer(url, timeoutMs = 60000) { +async function waitForServer(url, proc, timeoutMs = 60000) { const start = Date.now(); while (Date.now() - start < timeoutMs) { + if (proc.exitCode !== null || proc.signalCode || proc.spawnError) return false; try { - const res = await fetch(url); + const res = await fetch(url, { + headers: { Authorization: `Bearer ${TEST_TOKEN}` }, + signal: AbortSignal.timeout(1000), + }); if (res.ok) return true; } catch (_) { /* not ready */ } await new Promise(r => setTimeout(r, 500)); @@ -47,13 +53,38 @@ async function waitForServer(url, timeoutMs = 60000) { return false; } +async function reservePort() { + const requestedPort = configuredPort === undefined ? 0 : Number(configuredPort); + if (!Number.isInteger(requestedPort) || requestedPort < 0 || requestedPort > 65535) { + throw new Error(`ENGRAPHIS_PLAYWRIGHT_PORT must be an integer from 0 to 65535; got ${configuredPort}`); + } + PORT = await new Promise((resolve, reject) => { + const probe = net.createServer(); + probe.once('error', error => reject(new Error( + `Dashboard port ${requestedPort} is unavailable: ${error.message}`, + ))); + probe.listen(requestedPort, '127.0.0.1', () => { + const address = probe.address(); + if (!address || typeof address !== 'object') { + probe.close(() => reject(new Error('Could not determine the reserved dashboard port'))); + return; + } + probe.close(error => error ? reject(error) : resolve(address.port)); + }); + }); + BASE = `http://127.0.0.1:${PORT}`; +} + async function startServer() { + await reservePort(); log(`Starting dashboard on port ${PORT}...`); const proc = spawn('python', ['-m', 'scripts.start_dashboard', '--no-open', '--port', String(PORT)], { - cwd: REPO, shell: true, + cwd: REPO, shell: false, windowsHide: true, env: { ...process.env, ENGRAPHIS_DB_PATH: ':memory:', + ENGRAPHIS_API_TOKEN: TEST_TOKEN, + ENGRAPHIS_EXTRACTOR: 'none', ENGRAPHIS_EMBED_MODEL: '', ENGRAPHIS_LOOP_INTERVAL: '0', ENGRAPHIS_HOST: '127.0.0.1', @@ -61,9 +92,10 @@ async function startServer() { }, stdio: ['ignore', 'pipe', 'pipe'], }); + proc.once('error', error => { proc.spawnError = error; }); proc.stdout.on('data', d => process.stdout.write(`[srv] ${d}`)); proc.stderr.on('data', d => process.stderr.write(`[srv-err] ${d}`)); - const ready = await waitForServer(`${BASE}/api/health`); + const ready = await waitForServer(`${BASE}/api/workspaces`, proc); if (!ready) { proc.kill(); throw new Error('Server failed to start'); } log('Server ready'); return proc; @@ -269,6 +301,7 @@ function meanRadius(arr) { } async function main() { + const { chromium } = require('@playwright/test'); let serverProc = null; let browser = null; const allResults = []; @@ -469,4 +502,5 @@ async function main() { process.exit(exitCode); } -main(); +module.exports = { REPO, reservePort, startServer }; +if (require.main === module) main(); From 696aa07c8493c07e5c10c1fa60f2639b8ae45f57 Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Sat, 5 Sep 2026 05:57:23 -0400 Subject: [PATCH 06/10] docs: record reliability review and validation evidence --- docs/evidence/reliability/catalog.json | 157 +++++ docs/evidence/reliability/catalog.json.sha256 | 1 + .../fts-insert-counterfactual-20260905.json | 1 + ...insert-counterfactual-20260905.json.sha256 | 1 + .../reliability/line-ending-equivalence.json | 55 ++ ...tive-coverage-counterfactual-20260905.json | 1 + ...verage-counterfactual-20260905.json.sha256 | 1 + .../reliability/offline-gates-final.json | 315 ++++++++++ .../reliability/offline-gates-pr-final.json | 315 ++++++++++ docs/evidence/reliability/offline-gates.json | 44 ++ .../pr-benchmark-source-check.json | 37 ++ docs/evidence/reliability/pr-review.json | 98 ++++ ...e-source-before-trial-test-correction.json | 199 +++++++ .../public-complete-source-final.json | 365 ++++++++++++ ...-pr-source-before-contract-correction.json | 381 +++++++++++++ .../reliability/public-pr-source-final.json | 383 +++++++++++++ .../reliability/resolver-unit-20260905.json | 1 + .../resolver-unit-20260905.json.sha256 | 1 + .../resolver-write-acceptance-20260905.json | 1 + ...lver-write-acceptance-20260905.json.sha256 | 1 + .../evidence/reliability/source-manifest.json | 490 ++++++++++++++++ docs/evidence/reliability/validation.json | 536 ++++++++++++++++++ ...or-scale-incomplete-baseline-20260905.json | 1 + ...e-incomplete-baseline-20260905.json.sha256 | 1 + ...ctor-scale-native-incomplete-20260905.json | 1 + ...ale-native-incomplete-20260905.json.sha256 | 1 + .../vector-scale-numpy-20260905.json | 1 + .../vector-scale-numpy-20260905.json.sha256 | 1 + ...vector-scale-numpy-corrected-20260905.json | 1 + ...py-corrected-20260905.json.checkpoint.json | 1 + ...scale-numpy-corrected-20260905.json.sha256 | 1 + ...r-scale-sqlite-vec-corrected-20260905.json | 1 + ...ec-corrected-20260905.json.checkpoint.json | 1 + ...-sqlite-vec-corrected-20260905.json.sha256 | 1 + .../vector-scale-summary-20260905.md | 112 ++++ .../vector-scan-adaptive-20260905.json | 1 + .../vector-scan-adaptive-20260905.json.sha256 | 1 + .../vector-scan-plan-20260905.json | 1 + .../vector-scan-plan-20260905.json.sha256 | 1 + 39 files changed, 3512 insertions(+) create mode 100644 docs/evidence/reliability/catalog.json create mode 100644 docs/evidence/reliability/catalog.json.sha256 create mode 100644 docs/evidence/reliability/fts-insert-counterfactual-20260905.json create mode 100644 docs/evidence/reliability/fts-insert-counterfactual-20260905.json.sha256 create mode 100644 docs/evidence/reliability/line-ending-equivalence.json create mode 100644 docs/evidence/reliability/native-coverage-counterfactual-20260905.json create mode 100644 docs/evidence/reliability/native-coverage-counterfactual-20260905.json.sha256 create mode 100644 docs/evidence/reliability/offline-gates-final.json create mode 100644 docs/evidence/reliability/offline-gates-pr-final.json create mode 100644 docs/evidence/reliability/offline-gates.json create mode 100644 docs/evidence/reliability/pr-benchmark-source-check.json create mode 100644 docs/evidence/reliability/pr-review.json create mode 100644 docs/evidence/reliability/public-complete-source-before-trial-test-correction.json create mode 100644 docs/evidence/reliability/public-complete-source-final.json create mode 100644 docs/evidence/reliability/public-pr-source-before-contract-correction.json create mode 100644 docs/evidence/reliability/public-pr-source-final.json create mode 100644 docs/evidence/reliability/resolver-unit-20260905.json create mode 100644 docs/evidence/reliability/resolver-unit-20260905.json.sha256 create mode 100644 docs/evidence/reliability/resolver-write-acceptance-20260905.json create mode 100644 docs/evidence/reliability/resolver-write-acceptance-20260905.json.sha256 create mode 100644 docs/evidence/reliability/source-manifest.json create mode 100644 docs/evidence/reliability/validation.json create mode 100644 docs/evidence/reliability/vector-scale-incomplete-baseline-20260905.json create mode 100644 docs/evidence/reliability/vector-scale-incomplete-baseline-20260905.json.sha256 create mode 100644 docs/evidence/reliability/vector-scale-native-incomplete-20260905.json create mode 100644 docs/evidence/reliability/vector-scale-native-incomplete-20260905.json.sha256 create mode 100644 docs/evidence/reliability/vector-scale-numpy-20260905.json create mode 100644 docs/evidence/reliability/vector-scale-numpy-20260905.json.sha256 create mode 100644 docs/evidence/reliability/vector-scale-numpy-corrected-20260905.json create mode 100644 docs/evidence/reliability/vector-scale-numpy-corrected-20260905.json.checkpoint.json create mode 100644 docs/evidence/reliability/vector-scale-numpy-corrected-20260905.json.sha256 create mode 100644 docs/evidence/reliability/vector-scale-sqlite-vec-corrected-20260905.json create mode 100644 docs/evidence/reliability/vector-scale-sqlite-vec-corrected-20260905.json.checkpoint.json create mode 100644 docs/evidence/reliability/vector-scale-sqlite-vec-corrected-20260905.json.sha256 create mode 100644 docs/evidence/reliability/vector-scale-summary-20260905.md create mode 100644 docs/evidence/reliability/vector-scan-adaptive-20260905.json create mode 100644 docs/evidence/reliability/vector-scan-adaptive-20260905.json.sha256 create mode 100644 docs/evidence/reliability/vector-scan-plan-20260905.json create mode 100644 docs/evidence/reliability/vector-scan-plan-20260905.json.sha256 diff --git a/docs/evidence/reliability/catalog.json b/docs/evidence/reliability/catalog.json new file mode 100644 index 00000000..a6f559ce --- /dev/null +++ b/docs/evidence/reliability/catalog.json @@ -0,0 +1,157 @@ +{ + "schema": "engraphis-reliability-evidence-catalog/v1", + "date": "2026-09-05", + "source_manifest": "source-manifest.json", + "validation": "validation.json", + "public_base_commit": "cb03dbe104394b7760ef402a2b1917eac5e8accc", + "files": { + "fts-insert-counterfactual-20260905.json": { + "sha256": "b0a4603e983246790682ee5db4b5d5fab65b8ffb72dce002c85049f5112bff16", + "bytes": 7397 + }, + "fts-insert-counterfactual-20260905.json.sha256": { + "sha256": "adefc02066e41db9e30feb70104db80b705060564680788679f3017f3f8ff149", + "bytes": 106 + }, + "line-ending-equivalence.json": { + "sha256": "30af49759315c1cff0fa622799671219ddcff1ab0b13c4ee73de2e79ce69fc9f", + "bytes": 2933 + }, + "native-coverage-counterfactual-20260905.json": { + "sha256": "df0eec12c370c5ee06ec2125e5e8b275ef6c654dab820f25427211cede3faa17", + "bytes": 6155 + }, + "native-coverage-counterfactual-20260905.json.sha256": { + "sha256": "985b3025e03ca5eab40d4c08965076cf6dacb41c2b24cec900595bbf3fb38e9c", + "bytes": 111 + }, + "offline-gates-final.json": { + "sha256": "882d34b85fe3600b6c90b72eac6007d93d52e8c8e25c2e529faa75538766bddf", + "bytes": 27299 + }, + "offline-gates-pr-final.json": { + "sha256": "1dd10fe157fc656bdb43785598362f02c6a657a2f9de006670b92e6e1f637c18", + "bytes": 27300 + }, + "offline-gates.json": { + "sha256": "aa2f2e3e0312491d57e6d6571dbb92a96570c8655f55bfe7b63a3dc6ca2fc2dc", + "bytes": 3663 + }, + "pr-benchmark-source-check.json": { + "sha256": "08886c2f7afb7fcb51513ce23b3234dc9ab46362e6349706248f9a0a84f00db1", + "bytes": 1164 + }, + "pr-review.json": { + "sha256": "389291bb867e5e1c63c45c1f704079de49afbe91d8e9f05cb21f64cec41a2bc1", + "bytes": 4732 + }, + "public-complete-source-before-trial-test-correction.json": { + "sha256": "9378843f71b39317a192673089fed1f2904581873f454824d7f3eeb6b6355404", + "bytes": 18931 + }, + "public-complete-source-final.json": { + "sha256": "71ca7b0f6da0c701a0bcf81da5c797a26d01d5d791bc7b3656f4244b16b83d86", + "bytes": 25781 + }, + "public-pr-source-before-contract-correction.json": { + "sha256": "b09090d618b2b996e9d7ff178a61f6f84dacc911b76a1bf6d4f426319f58aedf", + "bytes": 27470 + }, + "public-pr-source-final.json": { + "sha256": "f5683a78cb71e70810ea2bcde046e8c9a428159f0ab92c8cdcc4b5f151f8449d", + "bytes": 27677 + }, + "resolver-unit-20260905.json": { + "sha256": "7559397b4d0a003f32d0a0eae5b7925dd3cd4109de05c63f8c24c34583e82a76", + "bytes": 3173 + }, + "resolver-unit-20260905.json.sha256": { + "sha256": "535027bd5f60e5fdf8adb19da3c1cae910f06a5ef4cc87552171fc66b1e31443", + "bytes": 95 + }, + "resolver-write-acceptance-20260905.json": { + "sha256": "f13920bddd4e89adad64a156faf5f2914e57856df71d5479a0abb5f21bbfe977", + "bytes": 3570 + }, + "resolver-write-acceptance-20260905.json.sha256": { + "sha256": "c661a88a839417967b7b97fe6884d6a595a53563a054ac8a4c4a2c3c308caa57", + "bytes": 107 + }, + "source-manifest.json": { + "sha256": "9b7c713ad5cb39222c59563897f21069e8f4848c32e6b401670dfc2c1f10b66a", + "bytes": 20106 + }, + "validation.json": { + "sha256": "54b8ef400f488ce3c9aa5cca93e6684341b77e36b35d6bf0e855ab6364ff9e78", + "bytes": 21607 + }, + "vector-scale-incomplete-baseline-20260905.json": { + "sha256": "45c4f61518012e35e09b6d274dec3015d00d4948f2b8279c0f56760dec080544", + "bytes": 4438 + }, + "vector-scale-incomplete-baseline-20260905.json.sha256": { + "sha256": "ba9188ce9f46df741b5e9bb44f4b061e089bc63b03523c1a93c2f0f9ed6a23db", + "bytes": 113 + }, + "vector-scale-native-incomplete-20260905.json": { + "sha256": "1b73068e071f4e39b644522a25512282fe986fda9ad5724ad44369eb25a3ceb1", + "bytes": 4931 + }, + "vector-scale-native-incomplete-20260905.json.sha256": { + "sha256": "2f8b0955d3370f0bde4655a81bcde602a8a649f1d2ce9d7dfecb4cf0b3cc1160", + "bytes": 111 + }, + "vector-scale-numpy-20260905.json": { + "sha256": "049d22200d0e088f03f42a13c8a0ae4031c3c9c3c12113c3f6196855e2f0d479", + "bytes": 15216 + }, + "vector-scale-numpy-20260905.json.sha256": { + "sha256": "c3319574d704e22c140d50842ffd96957b1cac108550de43c59a5c79b6323d40", + "bytes": 99 + }, + "vector-scale-numpy-corrected-20260905.json": { + "sha256": "ab4ba40faad58b7ecd3aff96a50ac45742f78f670ed79bed32b1802162d54e6b", + "bytes": 15171 + }, + "vector-scale-numpy-corrected-20260905.json.checkpoint.json": { + "sha256": "c4412e0638afdf876e12484fd69bab40db7addbbcbcb6b26852ba82ba2a2ce66", + "bytes": 196 + }, + "vector-scale-numpy-corrected-20260905.json.sha256": { + "sha256": "08c5916bb64fea4a6153657e1caa5bf2ed91c8ecd815e3aa176f5cfd3e0f9dc5", + "bytes": 109 + }, + "vector-scale-sqlite-vec-corrected-20260905.json": { + "sha256": "3cae847fcff2a1c0c5c680b2b050912ff2eb35e488a55be10f23832a4e6c787c", + "bytes": 15228 + }, + "vector-scale-sqlite-vec-corrected-20260905.json.checkpoint.json": { + "sha256": "ac00bab59242a7ca4c7f1a25fddf523591f17598bae85598d1a57413e80f14d4", + "bytes": 201 + }, + "vector-scale-sqlite-vec-corrected-20260905.json.sha256": { + "sha256": "5ef3e419537c839243ad0298222f11019c06740339f924aaa0e6f969b412cc58", + "bytes": 114 + }, + "vector-scale-summary-20260905.md": { + "sha256": "270a71a3ec891189ae90c02be0dbd8ef5c32f75c2cca04452c8f2b65929dd645", + "bytes": 13636 + }, + "vector-scan-adaptive-20260905.json": { + "sha256": "a421a8e73180104f0f5612780dfc929b678083e59494beab66a6bb621ebcc6e7", + "bytes": 15312 + }, + "vector-scan-adaptive-20260905.json.sha256": { + "sha256": "45d0f2c0a75c62583854927f2c9ff49de3077290aa6c6438e44504a454a8c180", + "bytes": 101 + }, + "vector-scan-plan-20260905.json": { + "sha256": "bb4f15d60d89e9e153456fb238acc3ea13fec68cfc673fdffb922a8f4e6ed63b", + "bytes": 8558 + }, + "vector-scan-plan-20260905.json.sha256": { + "sha256": "a9cfbb5f18426cfd2a63f6b014498fc79b268b22d114ad05335e711b16f80901", + "bytes": 97 + } + } +} diff --git a/docs/evidence/reliability/catalog.json.sha256 b/docs/evidence/reliability/catalog.json.sha256 new file mode 100644 index 00000000..43c96685 --- /dev/null +++ b/docs/evidence/reliability/catalog.json.sha256 @@ -0,0 +1 @@ +a9df9558ffeb568619695fcac2e7600ad3af36baf9653a6a6f42f70c061ce627 catalog.json diff --git a/docs/evidence/reliability/fts-insert-counterfactual-20260905.json b/docs/evidence/reliability/fts-insert-counterfactual-20260905.json new file mode 100644 index 00000000..a511dfe8 --- /dev/null +++ b/docs/evidence/reliability/fts-insert-counterfactual-20260905.json @@ -0,0 +1 @@ +{"environment":{"implementation":"CPython","machine":"AMD64","packages":{"engraphis":"1.7.1","numpy":"2.4.5","sentence-transformers":"6.0.0","torch":"2.13.0","transformers":"5.15.1"},"platform":"Windows-11-10.0.26100-SP0","python":"3.12.10"},"exclusions":[],"metrics":{"cells":[{"corpus_size":1000,"disk":{"database_bytes":610304,"shared_memory_bytes":32768,"total_bytes":3168664,"wal_bytes":2525592},"elapsed_seconds":0.3982855000067502,"records_per_second":2510.7617525193655,"strategy":"forced_legacy_delete","verified_row_counts":{"mem_fts":1000,"mem_vectors":1000,"memories":1000}},{"corpus_size":5000,"disk":{"database_bytes":9351168,"shared_memory_bytes":32768,"total_bytes":14801768,"wal_bytes":5417832},"elapsed_seconds":4.695537700026762,"records_per_second":1064.840774246473,"strategy":"forced_legacy_delete","verified_row_counts":{"mem_fts":5000,"mem_vectors":5000,"memories":5000}},{"corpus_size":10000,"disk":{"database_bytes":21340160,"shared_memory_bytes":32768,"total_bytes":26811360,"wal_bytes":5438432},"elapsed_seconds":16.6495715000201,"records_per_second":600.6160578960202,"strategy":"forced_legacy_delete","verified_row_counts":{"mem_fts":10000,"mem_vectors":10000,"memories":10000}},{"corpus_size":1000,"disk":{"database_bytes":610304,"shared_memory_bytes":32768,"total_bytes":3168664,"wal_bytes":2525592},"elapsed_seconds":0.246164400014095,"records_per_second":4062.3258275475314,"strategy":"new_row_insert","verified_row_counts":{"mem_fts":1000,"mem_vectors":1000,"memories":1000}},{"corpus_size":5000,"disk":{"database_bytes":9363456,"shared_memory_bytes":32768,"total_bytes":14809936,"wal_bytes":5413712},"elapsed_seconds":1.1883210999658331,"records_per_second":4207.616948099097,"strategy":"new_row_insert","verified_row_counts":{"mem_fts":5000,"mem_vectors":5000,"memories":5000}},{"corpus_size":10000,"disk":{"database_bytes":21364736,"shared_memory_bytes":32768,"total_bytes":26831816,"wal_bytes":5434312},"elapsed_seconds":2.494954699999653,"records_per_second":4008.0888041780445,"strategy":"new_row_insert","verified_row_counts":{"mem_fts":10000,"mem_vectors":10000,"memories":10000}}],"hardware":{"architecture":"AMD64","blas_thread_limits":{"MKL_NUM_THREADS":"1","OMP_NUM_THREADS":"1","OPENBLAS_NUM_THREADS":"1"},"cpu":"Intel(R) Core(TM) i7-10700KF CPU @ 3.80GHz","logical_cpus":16,"physical_ram_bytes":34221301760,"sqlite":"3.49.1"},"measurement_scope":"current synthetic Store+NumPy writes, forcing the former FTS deletion in one arm","source_after":{"files":{"engraphis/backends/vector_numpy.py":"c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72","engraphis/backends/vector_sqlitevec.py":"7c1e8457287e32d19ebbf9ec42123e5c56087e0607d60309540e6f9691ae3bca","engraphis/core/interfaces.py":"5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22","engraphis/core/schema.py":"99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686","engraphis/core/store.py":"df32758d9b546a7755b29df6cdfcf163e426ddd020e23c6b0fcf59741949e404","engraphis/core/vector_search.py":"75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234","eval/benchmark.py":"ed828e3548e8fb05ffafe9479791e545e56504421ff88676e4bf4466d6beaf91","eval/vector_scale.py":"9eefa5c387fc4648126aacbf0b0914c7874770c1b3a282e4c74e3ddd4d5ac569","eval/vector_scale_storage.py":"afd056b02b33a31b7a57f7558b872f45a7c783bda28709686d1c5882ebacc7f6"},"tracked_diff_sha256":"226ee92b9962f3c26cca08aa8168139c2121be04bfab13755b92ac10fb8a1790"},"source_before":{"files":{"engraphis/backends/vector_numpy.py":"c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72","engraphis/backends/vector_sqlitevec.py":"7c1e8457287e32d19ebbf9ec42123e5c56087e0607d60309540e6f9691ae3bca","engraphis/core/interfaces.py":"5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22","engraphis/core/schema.py":"99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686","engraphis/core/store.py":"df32758d9b546a7755b29df6cdfcf163e426ddd020e23c6b0fcf59741949e404","engraphis/core/vector_search.py":"75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234","eval/benchmark.py":"ed828e3548e8fb05ffafe9479791e545e56504421ff88676e4bf4466d6beaf91","eval/vector_scale.py":"9eefa5c387fc4648126aacbf0b0914c7874770c1b3a282e4c74e3ddd4d5ac569","eval/vector_scale_storage.py":"afd056b02b33a31b7a57f7558b872f45a7c783bda28709686d1c5882ebacc7f6"},"tracked_diff_sha256":"226ee92b9962f3c26cca08aa8168139c2121be04bfab13755b92ac10fb8a1790"},"source_stable":true,"unmeasured":["historical release behavior","embedding or resolution latency","independent process repetitions","external contention"]},"models":{"embedding":{"identity":"none; precomputed synthetic vectors"},"vector_backend":{"identity":"NumpyVectorIndex"}},"privacy":{"content_fingerprint_policy":"omitted","raw_answer_policy":"omitted","raw_context_policy":"omitted","raw_query_policy":"omitted"},"protocol":{"command":["python","-m","eval.fts_insert_scaling","--sizes","1000,5000,10000","--dim","256","--batch-size","500","--seed","20260731"],"config":{"batch_size":500,"dimension":256,"seed":20260731,"sizes":[1000,5000,10000]},"n_scored":6,"n_total":6,"token_accounting":{"identity":"unspecified","method":"unspecified","revision":null,"scope":"unspecified"}},"records":[{"category":"canonical_storage_population","question_id":"forced_legacy_delete-1000"},{"category":"canonical_storage_population","question_id":"forced_legacy_delete-5000"},{"category":"canonical_storage_population","question_id":"forced_legacy_delete-10000"},{"category":"canonical_storage_population","question_id":"new_row_insert-1000"},{"category":"canonical_storage_population","question_id":"new_row_insert-5000"},{"category":"canonical_storage_population","question_id":"new_row_insert-10000"}],"schema":"engraphis-benchmark/v2","suite":{"dataset":"fts_insert_scaling.py","name":"fts-new-insert-scaling/counterfactual-v1","sha256":"a8ecfbe19695f6d4659c3edf389b007c965b214b64fdd58d5ee86635fa766144","sources":[{"bytes":10319,"name":"vector_scale.py","sha256":"9eefa5c387fc4648126aacbf0b0914c7874770c1b3a282e4c74e3ddd4d5ac569"},{"bytes":21111,"name":"vector_scale_storage.py","sha256":"afd056b02b33a31b7a57f7558b872f45a7c783bda28709686d1c5882ebacc7f6"},{"bytes":60944,"name":"benchmark.py","sha256":"ed828e3548e8fb05ffafe9479791e545e56504421ff88676e4bf4466d6beaf91"},{"bytes":6163,"name":"vector_numpy.py","sha256":"c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72"},{"bytes":24225,"name":"vector_sqlitevec.py","sha256":"7c1e8457287e32d19ebbf9ec42123e5c56087e0607d60309540e6f9691ae3bca"},{"bytes":2349,"name":"vector_search.py","sha256":"75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234"},{"bytes":465730,"name":"store.py","sha256":"df32758d9b546a7755b29df6cdfcf163e426ddd020e23c6b0fcf59741949e404"},{"bytes":39996,"name":"schema.py","sha256":"99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686"},{"bytes":31040,"name":"interfaces.py","sha256":"5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22"},{"bytes":5247,"name":"fts_insert_scaling.py","sha256":"a8ecfbe19695f6d4659c3edf389b007c965b214b64fdd58d5ee86635fa766144"}]},"system":{"config_sha256":"2429ed8f1a9ab373c104e1a8572b5c15b4ae64c09a476778cf29a578df9848ee","dirty_state_sha256":"07c4221ff756b64bbc20cc5618b3c754d38f24d14d31e91b7c77bc65e869819b","git_commit":"c37ba0eb18408fe500cd70cd73fb5dcd7679b89c","git_dirty":true}} diff --git a/docs/evidence/reliability/fts-insert-counterfactual-20260905.json.sha256 b/docs/evidence/reliability/fts-insert-counterfactual-20260905.json.sha256 new file mode 100644 index 00000000..e4691888 --- /dev/null +++ b/docs/evidence/reliability/fts-insert-counterfactual-20260905.json.sha256 @@ -0,0 +1 @@ +b0a4603e983246790682ee5db4b5d5fab65b8ffb72dce002c85049f5112bff16 fts-insert-counterfactual-20260905.json diff --git a/docs/evidence/reliability/line-ending-equivalence.json b/docs/evidence/reliability/line-ending-equivalence.json new file mode 100644 index 00000000..ab0deffd --- /dev/null +++ b/docs/evidence/reliability/line-ending-equivalence.json @@ -0,0 +1,55 @@ +{ + "schema": "engraphis-line-ending-equivalence/v1", + "content_unchanged_except_line_endings": true, + "reason": "retain pure tracked baseline convention and remove unrelated whole-file diff noise before final suite", + "files": [ + { + "path": "engraphis/core/context.py", + "before_sha256": "34320c44f96be4bf4e7260264621902c598b8c49a6df27a1c28f3ebed55201ac", + "after_sha256": "3fbc5341adb353ad9b57c9b6368cdf166d1a116876af012214078d89eb53ddef", + "normalized_sha256": "3fbc5341adb353ad9b57c9b6368cdf166d1a116876af012214078d89eb53ddef" + }, + { + "path": "engraphis/core/engine.py", + "before_sha256": "942de6672902b9b19d8d86fe45984533ced0d1fcd3b857d8bf3eec8bc60a792d", + "after_sha256": "0d76a5f028234933933aff7adb7078455051086ae1db9d0b41360540ed7e83e3", + "normalized_sha256": "0d76a5f028234933933aff7adb7078455051086ae1db9d0b41360540ed7e83e3" + }, + { + "path": "engraphis/core/recall.py", + "before_sha256": "fb77fb97d5c9e6e6d14d9d3d5b074c894cbd7c2472b7a25fa84b280343b13625", + "after_sha256": "7eaabf69a1c3f4555a307fc6e294562edb5c6c15e4df627daf09187b25f8fb15", + "normalized_sha256": "7eaabf69a1c3f4555a307fc6e294562edb5c6c15e4df627daf09187b25f8fb15" + }, + { + "path": "tests/test_recall.py", + "before_sha256": "345799aea86d0ec0500e31b8fd2ebd7b50a89ae09cc22e470b617b199aec9354", + "after_sha256": "8035ff3959d956f600d6ff13ad14f6a5e4602f9167407d6bb11dcb7d63187152", + "normalized_sha256": "8035ff3959d956f600d6ff13ad14f6a5e4602f9167407d6bb11dcb7d63187152" + }, + { + "path": "tests/test_vector_sqlitevec_backend.py", + "before_sha256": "68411784738ef6fc8a7c19d08d7903296629c2958a2f966fd3cbec3bb11fb5bf", + "after_sha256": "2ff0682c9050d4ee30a052d6a7820ca55091234ef96c7e9ab2b7c052abcc7347", + "normalized_sha256": "2ff0682c9050d4ee30a052d6a7820ca55091234ef96c7e9ab2b7c052abcc7347" + }, + { + "path": "CHANGELOG.md", + "before_sha256": "de0bfc1c527d0c163a177e449e94f3b6fcb935ba4b8a07bc793c88a598230561", + "after_sha256": "bdbb14b215401efcd08e4009f6ee9ca8eaa8ad1180ea0541ee4e0ffd8b4d07c9", + "normalized_sha256": "f057badf154df63cd68ce009005d03ec3a8e37589113be3582008455d0f30263" + }, + { + "path": "scripts/update.py", + "before_sha256": "a2e68c1ccb0a746efb369d26e3309069bd1e87cde0b087d3cda4783fda52b489", + "after_sha256": "5f28d0eeab28dd321084ad0f88aa6aff521016ac1056c12031b977371f9e333d", + "normalized_sha256": "e3599e8269f92cd0c4af17002c29633840afcd12005de6afaa811cf77cf7aadf" + }, + { + "path": "tests/test_graph_engine_asset.py", + "before_sha256": "f3bf92600abb069c95c354b04ee973d9c37ac758348052b001fe443ebb36a00c", + "after_sha256": "f3bf92600abb069c95c354b04ee973d9c37ac758348052b001fe443ebb36a00c", + "normalized_sha256": "f3bf92600abb069c95c354b04ee973d9c37ac758348052b001fe443ebb36a00c" + } + ] +} diff --git a/docs/evidence/reliability/native-coverage-counterfactual-20260905.json b/docs/evidence/reliability/native-coverage-counterfactual-20260905.json new file mode 100644 index 00000000..6366b901 --- /dev/null +++ b/docs/evidence/reliability/native-coverage-counterfactual-20260905.json @@ -0,0 +1 @@ +{"environment":{"implementation":"CPython","machine":"AMD64","packages":{"engraphis":"1.7.1","numpy":"2.4.5","sentence-transformers":"6.0.0","torch":"2.13.0","transformers":"5.15.1"},"platform":"Windows-11-10.0.26100-SP0","python":"3.12.10"},"exclusions":[],"metrics":{"cells":[{"corpus_size":1000,"coverage_verified":true,"elapsed_seconds":0.13169480004580691,"strategy":"legacy_reverse_scan"},{"corpus_size":1000,"coverage_verified":true,"elapsed_seconds":0.06237729999702424,"strategy":"verified_cardinality"},{"corpus_size":5000,"coverage_verified":true,"elapsed_seconds":0.9200944000040181,"strategy":"legacy_reverse_scan"},{"corpus_size":5000,"coverage_verified":true,"elapsed_seconds":0.44797269999980927,"strategy":"verified_cardinality"},{"corpus_size":10000,"coverage_verified":true,"elapsed_seconds":2.02940200001467,"strategy":"legacy_reverse_scan"},{"corpus_size":10000,"coverage_verified":true,"elapsed_seconds":0.8885010000085458,"strategy":"verified_cardinality"}],"hardware":{"architecture":"AMD64","blas_thread_limits":{"MKL_NUM_THREADS":"1","OMP_NUM_THREADS":"1","OPENBLAS_NUM_THREADS":"1"},"cpu":"Intel(R) Core(TM) i7-10700KF CPU @ 3.80GHz","logical_cpus":16,"physical_ram_bytes":34221301760,"sqlite":"3.49.1"},"measurement_scope":"same current canonical/native store; only the verification traversal differs","source_after":{"files":{"engraphis/backends/vector_numpy.py":"c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72","engraphis/backends/vector_sqlitevec.py":"6148e14ceaacc19239b64c642a3fba0e98797c78cec356210157afadf475a08b","engraphis/core/interfaces.py":"5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22","engraphis/core/schema.py":"99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686","engraphis/core/store.py":"8659a5fd216e65f9e1e8b44870ffc1ff26e85aa77a26f831146977b727a11c35","engraphis/core/vector_search.py":"75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234","eval/benchmark.py":"ed828e3548e8fb05ffafe9479791e545e56504421ff88676e4bf4466d6beaf91","eval/vector_scale.py":"3f9c327d9eca1a857512ddc208e972933be1a7d4fa7fa0017aca0cbf8fe7bb6d","eval/vector_scale_storage.py":"33501cceb0aa1a5cfc49cba9caef9e4a92e4c758a0419f8b3a4e59bed34f757e"},"tracked_diff_sha256":"72a4d1621126c5a46c6b28507962076c17b8ea104c3b8e1a645d552eaf309ad5"},"source_before":{"files":{"engraphis/backends/vector_numpy.py":"c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72","engraphis/backends/vector_sqlitevec.py":"6148e14ceaacc19239b64c642a3fba0e98797c78cec356210157afadf475a08b","engraphis/core/interfaces.py":"5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22","engraphis/core/schema.py":"99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686","engraphis/core/store.py":"8659a5fd216e65f9e1e8b44870ffc1ff26e85aa77a26f831146977b727a11c35","engraphis/core/vector_search.py":"75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234","eval/benchmark.py":"ed828e3548e8fb05ffafe9479791e545e56504421ff88676e4bf4466d6beaf91","eval/vector_scale.py":"3f9c327d9eca1a857512ddc208e972933be1a7d4fa7fa0017aca0cbf8fe7bb6d","eval/vector_scale_storage.py":"33501cceb0aa1a5cfc49cba9caef9e4a92e4c758a0419f8b3a4e59bed34f757e"},"tracked_diff_sha256":"72a4d1621126c5a46c6b28507962076c17b8ea104c3b8e1a645d552eaf309ad5"},"source_stable":true,"unmeasured":["historical release behavior","independent process repetitions","external contention","end-to-end recall"]},"models":{},"privacy":{"content_fingerprint_policy":"omitted","raw_answer_policy":"omitted","raw_context_policy":"omitted","raw_query_policy":"omitted"},"protocol":{"command":["python","-m","eval.native_coverage_scaling","--sizes","1000,5000,10000","--dim","256","--batch-size","500","--seed","20260731"],"config":{"batch_size":500,"dimension":256,"seed":20260731,"sizes":[1000,5000,10000]},"n_scored":6,"n_total":6,"token_accounting":{"identity":"unspecified","method":"unspecified","revision":null,"scope":"unspecified"}},"records":[{"category":"native_coverage_verification","question_id":"legacy_reverse_scan-1000"},{"category":"native_coverage_verification","question_id":"verified_cardinality-1000"},{"category":"native_coverage_verification","question_id":"legacy_reverse_scan-5000"},{"category":"native_coverage_verification","question_id":"verified_cardinality-5000"},{"category":"native_coverage_verification","question_id":"legacy_reverse_scan-10000"},{"category":"native_coverage_verification","question_id":"verified_cardinality-10000"}],"schema":"engraphis-benchmark/v2","suite":{"dataset":"native_coverage_scaling.py","name":"native-coverage-scaling/counterfactual-v1","sha256":"0c7c3f5c80570c7ab02310f14927eb82399d1a624d65df895e937ed87339fe9c","sources":[{"bytes":11055,"name":"vector_scale.py","sha256":"3f9c327d9eca1a857512ddc208e972933be1a7d4fa7fa0017aca0cbf8fe7bb6d"},{"bytes":22641,"name":"vector_scale_storage.py","sha256":"33501cceb0aa1a5cfc49cba9caef9e4a92e4c758a0419f8b3a4e59bed34f757e"},{"bytes":60944,"name":"benchmark.py","sha256":"ed828e3548e8fb05ffafe9479791e545e56504421ff88676e4bf4466d6beaf91"},{"bytes":6163,"name":"vector_numpy.py","sha256":"c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72"},{"bytes":23317,"name":"vector_sqlitevec.py","sha256":"6148e14ceaacc19239b64c642a3fba0e98797c78cec356210157afadf475a08b"},{"bytes":2349,"name":"vector_search.py","sha256":"75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234"},{"bytes":465965,"name":"store.py","sha256":"8659a5fd216e65f9e1e8b44870ffc1ff26e85aa77a26f831146977b727a11c35"},{"bytes":39996,"name":"schema.py","sha256":"99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686"},{"bytes":31040,"name":"interfaces.py","sha256":"5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22"},{"bytes":7688,"name":"native_coverage_scaling.py","sha256":"0c7c3f5c80570c7ab02310f14927eb82399d1a624d65df895e937ed87339fe9c"}]},"system":{"config_sha256":"2429ed8f1a9ab373c104e1a8572b5c15b4ae64c09a476778cf29a578df9848ee","dirty_state_sha256":"1c4883fe9dac6decfa02b83601d2a4e8b507b511f397ba1e353e0feffbd320d2","git_commit":"c37ba0eb18408fe500cd70cd73fb5dcd7679b89c","git_dirty":true}} diff --git a/docs/evidence/reliability/native-coverage-counterfactual-20260905.json.sha256 b/docs/evidence/reliability/native-coverage-counterfactual-20260905.json.sha256 new file mode 100644 index 00000000..f6c25cd8 --- /dev/null +++ b/docs/evidence/reliability/native-coverage-counterfactual-20260905.json.sha256 @@ -0,0 +1 @@ +df0eec12c370c5ee06ec2125e5e8b275ef6c654dab820f25427211cede3faa17 native-coverage-counterfactual-20260905.json diff --git a/docs/evidence/reliability/offline-gates-final.json b/docs/evidence/reliability/offline-gates-final.json new file mode 100644 index 00000000..267e45f6 --- /dev/null +++ b/docs/evidence/reliability/offline-gates-final.json @@ -0,0 +1,315 @@ +{ + "schema": "engraphis-offline-validation/v1", + "date": "2026-09-05", + "source_before": { + "engraphis/core/browsing.py": "9adce7bbe8791fe4e76b892977d22cc72958c5d8ef3f98c58f50efdc1bb2fd36", + "engraphis/core/vector_repair.py": "425901615d3b602e3310af1611ebb3c0b78f3d793694c4ca8fffb55f00d07904", + "engraphis/core/vector_search.py": "75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234", + "eval/datasets/resolver_write_acceptance.jsonl": "e8db06aa41a93e4892e81d60bb71f06e656088f3eba36ebe37710850a97db6f7", + "eval/fts_insert_scaling.py": "a8ecfbe19695f6d4659c3edf389b007c965b214b64fdd58d5ee86635fa766144", + "eval/native_coverage_scaling.py": "0c7c3f5c80570c7ab02310f14927eb82399d1a624d65df895e937ed87339fe9c", + "eval/vector_scale_storage.py": "bda0c1139596eb9232cd75c826fc941101b6116bae32ad7895525d43f9d35484", + "eval/vector_scan_plan.py": "92de2f3b41aa457dcfee11998936ce35e023fe61596ed98f3836c998a6e9a1cd", + "engraphis/backends/__init__.py": "a9f22b9278362904166614081f1df78469d453601b298ce4e8afdba8a3722b25", + "engraphis/backends/codegraph.py": "83e723a91068d23694092fbe00157fcb2597061d453eb36f955bf55bc5b4d35e", + "engraphis/backends/embedder_api.py": "56a6bceea4f757325dcea987b0339e41d3875634b1ae5103f0537a358cf5878b", + "engraphis/backends/embedder_deterministic.py": "ec8b23de7e7e8273416125f5876ca96f55e4ae7881841bae55783ab0ba9130ad", + "engraphis/backends/embedder_st.py": "e1c20fd980e07060387e3f9fa37fe02a916959fc8abce4e6067a62699c726de1", + "engraphis/backends/encrypted_db.py": "204553016af879236575a34646a86e03d3433216f663de55cac9d2e54185355c", + "engraphis/backends/extractor.py": "f2e3455ab7f14caee1d5b5c4ef071e498e90118f1b0ddcb8510c969583b0fc57", + "engraphis/backends/graph_extractor.py": "88561efa0d3fabc447a0a005b10e36261379d46218cf62d905e6928cd2fda676", + "engraphis/backends/model_source.py": "8c3c7681f95214a2bbabd8de222e5ee11f42fe13402d27365654ae75fb363d4e", + "engraphis/backends/postgres_schema.py": "8468578c3add701d30d5eaa36d768ded2375d116e55f1836e09f6107a07a267e", + "engraphis/backends/query_planner.py": "bbdd77afc9b5523421b85b2ae63c8da7f5a7b777265450e0d21708a83e7bb23c", + "engraphis/backends/reranker.py": "747761d6cbfa421388974bcfd98d844f92391d80f4bf6a4feca00b0c7a6908ca", + "engraphis/backends/resources.py": "47cc867c3aecc8bd95fa284bc5bb04715f3339c19a0a11512973ef6171c95944", + "engraphis/backends/retention.py": "381d9371e3951d762f8b55eb54711de5697642acb39de99a714f246c059ecbd0", + "engraphis/backends/sync_folder.py": "e4f70a92a17f6a365910670df041e6e3ca421d44ada2827917cd66b4dc067bfa", + "engraphis/backends/sync_relay.py": "b8b9ad265453aba17ba7c27a355e12a793469b3e44cb217943c6fad9382a3006", + "engraphis/backends/vector_numpy.py": "c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72", + "engraphis/backends/vector_sqlitevec.py": "6148e14ceaacc19239b64c642a3fba0e98797c78cec356210157afadf475a08b", + "engraphis/core/__init__.py": "dd5143729c3939237f04636f437032b1f2d3a5f7d82c91bbc2a5a283c3f0ebaa", + "engraphis/core/adaptive_context.py": "cc5ce48109bb0d5230a5b2b8424b829c853feec5b5d5b82596413f2279b0c9f9", + "engraphis/core/codegraph_export.py": "4641074258d7b23498f92dd45053a0fbb111863eaad2001c08e5e3c2dc2fd54f", + "engraphis/core/conflicts.py": "28530be25a4af0bffd8f609b965b33ca7f93199a70887789b2148fda8a61a486", + "engraphis/core/consolidate.py": "147dd9359db3bf9843aa951de2328aff90c4b6013f6fe05386e602e3da92dc5e", + "engraphis/core/context.py": "7defbaf15e10996cfa442cd0fb4d546f544e2f77a2ae7a64ce9193a4c24489a9", + "engraphis/core/documents.py": "84385db39ba44e06b58b4b26dbf954228ff4abed7230f28a7280166fa6457861", + "engraphis/core/engine.py": "59ca726b3dbb5767ef4c9fb1f0142486ae60d9a823990350f507e4056929eace", + "engraphis/core/fsutil.py": "6db770fa8bd3e1a57dfa70eb8e8bc46d48c2dc53ead1b0b43eeecf085ef58cfc", + "engraphis/core/graph_layers.py": "64d74ab01c77119f6343ba6f1d6a84f9653f1a5d34d47ce7966f3ac31b29d2ea", + "engraphis/core/graph_policy.py": "ec5b373d01adb2de87df31d9f543130018e9a73faaaed239a184f14a32646615", + "engraphis/core/graph_scene.py": "6348b9b6bb20c2e0689c8696452032c0b372d96cab312bfa508a07a6645386f8", + "engraphis/core/graphrank.py": "1279a58396104d3f906bfd5ec75b32efedfefe52201467bf19d80be3517017a5", + "engraphis/core/grounded.py": "86796c69e6a77927971c5d57e70d9c5d06793db1ff375a9dc1a73885cc2569ea", + "engraphis/core/ids.py": "80c2c0a0635af86ada33b46e5743f28001ff1004b727bbb790aa2393c234d86b", + "engraphis/core/interfaces.py": "5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22", + "engraphis/core/obsidian.py": "991267c153cb7c4c40f7fe8f50aa71688892e250aaaf383b22c9e5910dc263b7", + "engraphis/core/poisoning.py": "5bc67169ee8032f3777f2d3969dcf71821437bd473ce4f917c4b41a50e845fb6", + "engraphis/core/query_planner.py": "8d67af852564f95badfb60ac57a830a65820a01228d8837103cb125956e267a7", + "engraphis/core/recall.py": "6a7692a2f404bf32d346d35d10810b763379c1cf59419f3be423b456d2f60191", + "engraphis/core/resolve.py": "ffc17dd39331bf0c6881fa2efa9bcca73f0b7c968135fffaa4dbb025bb32c674", + "engraphis/core/retention_policy.py": "864c03bdb6e743cd0002c706de471e920f1fa1f1a9918ab343ef4c2042b47429", + "engraphis/core/retrieval_policy.py": "7e970ac57762a1e55091bac46314c3d60aba0bf2cbb11405ce0f7e032448866e", + "engraphis/core/savings.py": "cfbcfc7e476f4e28028555cd519696e23099f6210cf0b733225832aeaa0bc7dc", + "engraphis/core/schema.py": "99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686", + "engraphis/core/scoring.py": "f5b6ac291edf0968b3de83cb1951a97d5bfd8a8d079199cb2ef95bba0884c89a", + "engraphis/core/secrets.py": "a4835ba06e2616156528df6365ca1aba6cba0c97cdf834a709d2797a371099d2", + "engraphis/core/store.py": "007272ec42011faaae25bdbaf8905cf0d2945bf6f451b60aa7a90fff0550debd", + "engraphis/core/sync.py": "6ecf4e2e1de83697a99f706efda261d2c59c1ab351c8a67ed003115d35221145", + "engraphis/core/textutil.py": "acd65031729fa5d91d09527b8eb52518b83cfce77a55e6b7ae2d94686e35c1a6", + "engraphis/core/user_model.py": "3147ec8ee7cfd855783f639874b63f331cdc822bd8bd9298cfb326dd18666026", + "eval/__init__.py": "639f0c6d9d6aac8ff6dc605a34a0a301058905cc53bff4eaed5912247f0e7c56", + "eval/ablation.py": "16f159dee75d2f96cc42f230c2403fa19ea0bda7091c4823660da553463a194a", + "eval/adversarial_memory_security.py": "35dd8d981bcbad50e9815465be420b05b62a9dea28a78eb6cc1e09ec51c320c4", + "eval/agent_benchmarks.py": "91c157b1d3d445fe0daa236edf21decb7e3713ad9bbaacbf0a46c0d8a5d1f2ee", + "eval/benchmark.py": "ed828e3548e8fb05ffafe9479791e545e56504421ff88676e4bf4466d6beaf91", + "eval/chunking_eval.py": "a16544353940c0a8c40cea3b9932d3399b35ea5994b809b78f5dbe4a952c467f", + "eval/code_agent_ab.py": "d98bba6b77700ff6bf86ff0d2cf518a67e5a2676ef51bbddabbb2ff26e1f3aaa", + "eval/code_arm.py": "d211166fce1b8a4173848e1617873b7e84aadeff43746effe7483c54bbdb6f1d", + "eval/consolidation_ranking.py": "917b578d4e0bcb929bf1a1a37611acf7716a520c076abf4ade0d8d12a1c455de", + "eval/context_economy.py": "709ac7cc866855f96d7717ab2bea12e8b0d3a140d15fb978a2a929ad085931f2", + "eval/context_efficiency_guardrails.py": "22afd1a6fe17219e74701dc587ec35f569a5bf22bea270944525f51746723f14", + "eval/datasets/adversarial.jsonl": "65ea84d0c2388cf5a317170ac5ac327454d09040788a8f036b2e1adf87a3204e", + "eval/datasets/code_arm.jsonl": "894411c42e049abcb4f9ed1e506d753ea30649b2be2c3e0b0d82fa5c2e3b8b50", + "eval/datasets/codemem.jsonl": "341313023c22850a2e14f02742b571ad1deca824f886a1654a59541304c01f3c", + "eval/datasets/consolidation_ranking.jsonl": "d7008550a276ea81ff057725d1dba4893e7842886c0e932f9dd9a043287028ae", + "eval/datasets/context_routing_stress.jsonl": "ead3b71cfb90b41e69ffeec9f8f5ecb57cb53ea7078c299bfd98d04913c7197a", + "eval/datasets/graph_layer_routing.jsonl": "e37451ef5c5f8532b81d6cbe02c9c5a84fb6eed3b96354a1fba9d8b987438ca5", + "eval/datasets/graph_multihop.jsonl": "aee614cfe6e597fbb04643931cbb235ba9edca1b9a5b4bfce9eae4a29927ab3f", + "eval/datasets/grounded_distractors.jsonl": "caf77f56f4f23f33af0e9ecb32696f3bfa27dc64c99bd0190586bf9501c1cd37", + "eval/datasets/handoff_quality.jsonl": "190e061c8e63d5e8fe16fc67f492a1c5b85e2bbc4e32f31cdf07db5166959cc2", + "eval/datasets/longdoc.jsonl": "7f5ade95e1f283d0db8cf78e53ed8995d3534f847e616d2c0005fd8da37ac790", + "eval/datasets/proactive_ranking.jsonl": "5b0cfbde93ca57297638798a0813537d116f968345dd0bfcd54d7c4d37a2896f", + "eval/datasets/redteam_poisoning.jsonl": "abf4180eb147393c1d41e5a8cdb8214621ee5c7759fc23750258ef9a2f779b49", + "eval/datasets/resolver_reworded_corrections.jsonl": "5b796d8852117b30060cf019e93fd775fea256ca492ce0fedf9b4786f0a0421b", + "eval/datasets/sample.jsonl": "b41a041bc289cff06411c9fed6502c7526bed04f03cc0e5adee78e70537b7cc9", + "eval/external.py": "d93cc071e555c31f4c5521ab0b2aeb370df24656e5ee7b60904113d97b1416ae", + "eval/extractor_quality.py": "50820fe7f821d111e17e4e77a3d1159e7d6979d110b052c4f254a5b309c2652a", + "eval/graph_every_bench.py": "79da573c5edf315f71bfab412d3ea283b8da45d1fdabc78ddd19302ead09e4f0", + "eval/graph_traversal.py": "b094f75c3a1d75ba3cf19e372187d692d3c595a1d9bde19bbe95ae0c79a5175f", + "eval/grounded.py": "053d5193b716a2c3e507fcd44057d392de910a4442b46bd7cc1f30ac0ba68541", + "eval/handoff_quality.py": "7daf635510764e236f48ca7e2537a85d8ebc1ad995513144329c1f0236405937", + "eval/harness.py": "d9c5f960e891a3d0912e519eec554a970c973b3b8893df861f0258aa16342310", + "eval/hosted_evidence.py": "7946cd8c1e3aa291268271b2aa11210d5f09c9b05ba635aa0b64bef07bdcee45", + "eval/hosted_ledger.py": "a53036d12ff671371c148910a816fb20c7e7f3346250a303722352f47b06c476", + "eval/hosted_luna.py": "4dbf02a65eec38bcde92d82952a0b372abbac1f68378d11a04528f4a31a835dc", + "eval/longmemeval_v2.py": "defb4d47f453aa4615a8f101b82df3fadf64f0f9eae0ce1021d86d3750dad437", + "eval/longmemeval_v2_evidence.py": "8562d32590e4267e033cb1da2a7fda626bdc2630ebbf8547df282896f34abf8b", + "eval/longmemeval_v2_matrix.py": "ca085cd59481813cce5dbdfbc94f40f67173ac3a1bc3dce6f6d3e09eb7b08153", + "eval/metrics.py": "16857e2cf6ed339cb57a26c9bfa1879444b4d279bd972e5a9fa644ed1308afe0", + "eval/performance.py": "e17ea78095e4e592717bc5d9d8e34d55fd98c3fd28d1a8104a20c227d4d619c9", + "eval/planned_recall.py": "12f3a3f36ca01e2fac717ed286edae36929a84febe7e3116f259b1295eaf4443", + "eval/proactive_ranking.py": "8610541f1d547f9c0eb46d078dbcaa670c0a97157acc37f96ec08b482cb7a6ab", + "eval/productivity.py": "6d4644ebdc44472aeb3879963774fab276bfa269b781139c46ded48774a22717", + "eval/public_readiness.py": "5ce8a18d0bfe09e75e88548a589b8fc6d2cbf04c0f8cd1ce51cd9519136a2751", + "eval/redteam_poisoning.py": "fce120cc3adf2ee966b59cd3ea7f20d242a0af49b52492143543130fe14c0cc8", + "eval/reinforcement.py": "72ed766775a2658eaa728afec51c0ac22d97a90e813111954df66a6ec50f2bef", + "eval/resolver_reworded_corrections.py": "f89e0920986efecd736380f0b8021aae553cfb304761f489d49828e6ffec537e", + "eval/resource_hierarchy.py": "5ab6c989bb143c4386749c45a33c190447e829c1b5657e8c0aca30f34bd69461", + "eval/run_longmemeval_v2.py": "2e003d8ecf4f44ac5a3f80bd3ba8fe18bc74e769298fefe63fde8bb2a98f3a04", + "eval/vector_scale.py": "3f9c327d9eca1a857512ddc208e972933be1a7d4fa7fa0017aca0cbf8fe7bb6d" + }, + "source_after": { + "engraphis/core/browsing.py": "9adce7bbe8791fe4e76b892977d22cc72958c5d8ef3f98c58f50efdc1bb2fd36", + "engraphis/core/vector_repair.py": "425901615d3b602e3310af1611ebb3c0b78f3d793694c4ca8fffb55f00d07904", + "engraphis/core/vector_search.py": "75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234", + "eval/datasets/resolver_write_acceptance.jsonl": "e8db06aa41a93e4892e81d60bb71f06e656088f3eba36ebe37710850a97db6f7", + "eval/fts_insert_scaling.py": "a8ecfbe19695f6d4659c3edf389b007c965b214b64fdd58d5ee86635fa766144", + "eval/native_coverage_scaling.py": "0c7c3f5c80570c7ab02310f14927eb82399d1a624d65df895e937ed87339fe9c", + "eval/vector_scale_storage.py": "bda0c1139596eb9232cd75c826fc941101b6116bae32ad7895525d43f9d35484", + "eval/vector_scan_plan.py": "92de2f3b41aa457dcfee11998936ce35e023fe61596ed98f3836c998a6e9a1cd", + "engraphis/backends/__init__.py": "a9f22b9278362904166614081f1df78469d453601b298ce4e8afdba8a3722b25", + "engraphis/backends/codegraph.py": "83e723a91068d23694092fbe00157fcb2597061d453eb36f955bf55bc5b4d35e", + "engraphis/backends/embedder_api.py": "56a6bceea4f757325dcea987b0339e41d3875634b1ae5103f0537a358cf5878b", + "engraphis/backends/embedder_deterministic.py": "ec8b23de7e7e8273416125f5876ca96f55e4ae7881841bae55783ab0ba9130ad", + "engraphis/backends/embedder_st.py": "e1c20fd980e07060387e3f9fa37fe02a916959fc8abce4e6067a62699c726de1", + "engraphis/backends/encrypted_db.py": "204553016af879236575a34646a86e03d3433216f663de55cac9d2e54185355c", + "engraphis/backends/extractor.py": "f2e3455ab7f14caee1d5b5c4ef071e498e90118f1b0ddcb8510c969583b0fc57", + "engraphis/backends/graph_extractor.py": "88561efa0d3fabc447a0a005b10e36261379d46218cf62d905e6928cd2fda676", + "engraphis/backends/model_source.py": "8c3c7681f95214a2bbabd8de222e5ee11f42fe13402d27365654ae75fb363d4e", + "engraphis/backends/postgres_schema.py": "8468578c3add701d30d5eaa36d768ded2375d116e55f1836e09f6107a07a267e", + "engraphis/backends/query_planner.py": "bbdd77afc9b5523421b85b2ae63c8da7f5a7b777265450e0d21708a83e7bb23c", + "engraphis/backends/reranker.py": "747761d6cbfa421388974bcfd98d844f92391d80f4bf6a4feca00b0c7a6908ca", + "engraphis/backends/resources.py": "47cc867c3aecc8bd95fa284bc5bb04715f3339c19a0a11512973ef6171c95944", + "engraphis/backends/retention.py": "381d9371e3951d762f8b55eb54711de5697642acb39de99a714f246c059ecbd0", + "engraphis/backends/sync_folder.py": "e4f70a92a17f6a365910670df041e6e3ca421d44ada2827917cd66b4dc067bfa", + "engraphis/backends/sync_relay.py": "b8b9ad265453aba17ba7c27a355e12a793469b3e44cb217943c6fad9382a3006", + "engraphis/backends/vector_numpy.py": "c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72", + "engraphis/backends/vector_sqlitevec.py": "6148e14ceaacc19239b64c642a3fba0e98797c78cec356210157afadf475a08b", + "engraphis/core/__init__.py": "dd5143729c3939237f04636f437032b1f2d3a5f7d82c91bbc2a5a283c3f0ebaa", + "engraphis/core/adaptive_context.py": "cc5ce48109bb0d5230a5b2b8424b829c853feec5b5d5b82596413f2279b0c9f9", + "engraphis/core/codegraph_export.py": "4641074258d7b23498f92dd45053a0fbb111863eaad2001c08e5e3c2dc2fd54f", + "engraphis/core/conflicts.py": "28530be25a4af0bffd8f609b965b33ca7f93199a70887789b2148fda8a61a486", + "engraphis/core/consolidate.py": "147dd9359db3bf9843aa951de2328aff90c4b6013f6fe05386e602e3da92dc5e", + "engraphis/core/context.py": "7defbaf15e10996cfa442cd0fb4d546f544e2f77a2ae7a64ce9193a4c24489a9", + "engraphis/core/documents.py": "84385db39ba44e06b58b4b26dbf954228ff4abed7230f28a7280166fa6457861", + "engraphis/core/engine.py": "59ca726b3dbb5767ef4c9fb1f0142486ae60d9a823990350f507e4056929eace", + "engraphis/core/fsutil.py": "6db770fa8bd3e1a57dfa70eb8e8bc46d48c2dc53ead1b0b43eeecf085ef58cfc", + "engraphis/core/graph_layers.py": "64d74ab01c77119f6343ba6f1d6a84f9653f1a5d34d47ce7966f3ac31b29d2ea", + "engraphis/core/graph_policy.py": "ec5b373d01adb2de87df31d9f543130018e9a73faaaed239a184f14a32646615", + "engraphis/core/graph_scene.py": "6348b9b6bb20c2e0689c8696452032c0b372d96cab312bfa508a07a6645386f8", + "engraphis/core/graphrank.py": "1279a58396104d3f906bfd5ec75b32efedfefe52201467bf19d80be3517017a5", + "engraphis/core/grounded.py": "86796c69e6a77927971c5d57e70d9c5d06793db1ff375a9dc1a73885cc2569ea", + "engraphis/core/ids.py": "80c2c0a0635af86ada33b46e5743f28001ff1004b727bbb790aa2393c234d86b", + "engraphis/core/interfaces.py": "5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22", + "engraphis/core/obsidian.py": "991267c153cb7c4c40f7fe8f50aa71688892e250aaaf383b22c9e5910dc263b7", + "engraphis/core/poisoning.py": "5bc67169ee8032f3777f2d3969dcf71821437bd473ce4f917c4b41a50e845fb6", + "engraphis/core/query_planner.py": "8d67af852564f95badfb60ac57a830a65820a01228d8837103cb125956e267a7", + "engraphis/core/recall.py": "6a7692a2f404bf32d346d35d10810b763379c1cf59419f3be423b456d2f60191", + "engraphis/core/resolve.py": "ffc17dd39331bf0c6881fa2efa9bcca73f0b7c968135fffaa4dbb025bb32c674", + "engraphis/core/retention_policy.py": "864c03bdb6e743cd0002c706de471e920f1fa1f1a9918ab343ef4c2042b47429", + "engraphis/core/retrieval_policy.py": "7e970ac57762a1e55091bac46314c3d60aba0bf2cbb11405ce0f7e032448866e", + "engraphis/core/savings.py": "cfbcfc7e476f4e28028555cd519696e23099f6210cf0b733225832aeaa0bc7dc", + "engraphis/core/schema.py": "99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686", + "engraphis/core/scoring.py": "f5b6ac291edf0968b3de83cb1951a97d5bfd8a8d079199cb2ef95bba0884c89a", + "engraphis/core/secrets.py": "a4835ba06e2616156528df6365ca1aba6cba0c97cdf834a709d2797a371099d2", + "engraphis/core/store.py": "007272ec42011faaae25bdbaf8905cf0d2945bf6f451b60aa7a90fff0550debd", + "engraphis/core/sync.py": "6ecf4e2e1de83697a99f706efda261d2c59c1ab351c8a67ed003115d35221145", + "engraphis/core/textutil.py": "acd65031729fa5d91d09527b8eb52518b83cfce77a55e6b7ae2d94686e35c1a6", + "engraphis/core/user_model.py": "3147ec8ee7cfd855783f639874b63f331cdc822bd8bd9298cfb326dd18666026", + "eval/__init__.py": "639f0c6d9d6aac8ff6dc605a34a0a301058905cc53bff4eaed5912247f0e7c56", + "eval/ablation.py": "16f159dee75d2f96cc42f230c2403fa19ea0bda7091c4823660da553463a194a", + "eval/adversarial_memory_security.py": "35dd8d981bcbad50e9815465be420b05b62a9dea28a78eb6cc1e09ec51c320c4", + "eval/agent_benchmarks.py": "91c157b1d3d445fe0daa236edf21decb7e3713ad9bbaacbf0a46c0d8a5d1f2ee", + "eval/benchmark.py": "ed828e3548e8fb05ffafe9479791e545e56504421ff88676e4bf4466d6beaf91", + "eval/chunking_eval.py": "a16544353940c0a8c40cea3b9932d3399b35ea5994b809b78f5dbe4a952c467f", + "eval/code_agent_ab.py": "d98bba6b77700ff6bf86ff0d2cf518a67e5a2676ef51bbddabbb2ff26e1f3aaa", + "eval/code_arm.py": "d211166fce1b8a4173848e1617873b7e84aadeff43746effe7483c54bbdb6f1d", + "eval/consolidation_ranking.py": "917b578d4e0bcb929bf1a1a37611acf7716a520c076abf4ade0d8d12a1c455de", + "eval/context_economy.py": "709ac7cc866855f96d7717ab2bea12e8b0d3a140d15fb978a2a929ad085931f2", + "eval/context_efficiency_guardrails.py": "22afd1a6fe17219e74701dc587ec35f569a5bf22bea270944525f51746723f14", + "eval/datasets/adversarial.jsonl": "65ea84d0c2388cf5a317170ac5ac327454d09040788a8f036b2e1adf87a3204e", + "eval/datasets/code_arm.jsonl": "894411c42e049abcb4f9ed1e506d753ea30649b2be2c3e0b0d82fa5c2e3b8b50", + "eval/datasets/codemem.jsonl": "341313023c22850a2e14f02742b571ad1deca824f886a1654a59541304c01f3c", + "eval/datasets/consolidation_ranking.jsonl": "d7008550a276ea81ff057725d1dba4893e7842886c0e932f9dd9a043287028ae", + "eval/datasets/context_routing_stress.jsonl": "ead3b71cfb90b41e69ffeec9f8f5ecb57cb53ea7078c299bfd98d04913c7197a", + "eval/datasets/graph_layer_routing.jsonl": "e37451ef5c5f8532b81d6cbe02c9c5a84fb6eed3b96354a1fba9d8b987438ca5", + "eval/datasets/graph_multihop.jsonl": "aee614cfe6e597fbb04643931cbb235ba9edca1b9a5b4bfce9eae4a29927ab3f", + "eval/datasets/grounded_distractors.jsonl": "caf77f56f4f23f33af0e9ecb32696f3bfa27dc64c99bd0190586bf9501c1cd37", + "eval/datasets/handoff_quality.jsonl": "190e061c8e63d5e8fe16fc67f492a1c5b85e2bbc4e32f31cdf07db5166959cc2", + "eval/datasets/longdoc.jsonl": "7f5ade95e1f283d0db8cf78e53ed8995d3534f847e616d2c0005fd8da37ac790", + "eval/datasets/proactive_ranking.jsonl": "5b0cfbde93ca57297638798a0813537d116f968345dd0bfcd54d7c4d37a2896f", + "eval/datasets/redteam_poisoning.jsonl": "abf4180eb147393c1d41e5a8cdb8214621ee5c7759fc23750258ef9a2f779b49", + "eval/datasets/resolver_reworded_corrections.jsonl": "5b796d8852117b30060cf019e93fd775fea256ca492ce0fedf9b4786f0a0421b", + "eval/datasets/sample.jsonl": "b41a041bc289cff06411c9fed6502c7526bed04f03cc0e5adee78e70537b7cc9", + "eval/external.py": "d93cc071e555c31f4c5521ab0b2aeb370df24656e5ee7b60904113d97b1416ae", + "eval/extractor_quality.py": "50820fe7f821d111e17e4e77a3d1159e7d6979d110b052c4f254a5b309c2652a", + "eval/graph_every_bench.py": "79da573c5edf315f71bfab412d3ea283b8da45d1fdabc78ddd19302ead09e4f0", + "eval/graph_traversal.py": "b094f75c3a1d75ba3cf19e372187d692d3c595a1d9bde19bbe95ae0c79a5175f", + "eval/grounded.py": "053d5193b716a2c3e507fcd44057d392de910a4442b46bd7cc1f30ac0ba68541", + "eval/handoff_quality.py": "7daf635510764e236f48ca7e2537a85d8ebc1ad995513144329c1f0236405937", + "eval/harness.py": "d9c5f960e891a3d0912e519eec554a970c973b3b8893df861f0258aa16342310", + "eval/hosted_evidence.py": "7946cd8c1e3aa291268271b2aa11210d5f09c9b05ba635aa0b64bef07bdcee45", + "eval/hosted_ledger.py": "a53036d12ff671371c148910a816fb20c7e7f3346250a303722352f47b06c476", + "eval/hosted_luna.py": "4dbf02a65eec38bcde92d82952a0b372abbac1f68378d11a04528f4a31a835dc", + "eval/longmemeval_v2.py": "defb4d47f453aa4615a8f101b82df3fadf64f0f9eae0ce1021d86d3750dad437", + "eval/longmemeval_v2_evidence.py": "8562d32590e4267e033cb1da2a7fda626bdc2630ebbf8547df282896f34abf8b", + "eval/longmemeval_v2_matrix.py": "ca085cd59481813cce5dbdfbc94f40f67173ac3a1bc3dce6f6d3e09eb7b08153", + "eval/metrics.py": "16857e2cf6ed339cb57a26c9bfa1879444b4d279bd972e5a9fa644ed1308afe0", + "eval/performance.py": "e17ea78095e4e592717bc5d9d8e34d55fd98c3fd28d1a8104a20c227d4d619c9", + "eval/planned_recall.py": "12f3a3f36ca01e2fac717ed286edae36929a84febe7e3116f259b1295eaf4443", + "eval/proactive_ranking.py": "8610541f1d547f9c0eb46d078dbcaa670c0a97157acc37f96ec08b482cb7a6ab", + "eval/productivity.py": "6d4644ebdc44472aeb3879963774fab276bfa269b781139c46ded48774a22717", + "eval/public_readiness.py": "5ce8a18d0bfe09e75e88548a589b8fc6d2cbf04c0f8cd1ce51cd9519136a2751", + "eval/redteam_poisoning.py": "fce120cc3adf2ee966b59cd3ea7f20d242a0af49b52492143543130fe14c0cc8", + "eval/reinforcement.py": "72ed766775a2658eaa728afec51c0ac22d97a90e813111954df66a6ec50f2bef", + "eval/resolver_reworded_corrections.py": "f89e0920986efecd736380f0b8021aae553cfb304761f489d49828e6ffec537e", + "eval/resource_hierarchy.py": "5ab6c989bb143c4386749c45a33c190447e829c1b5657e8c0aca30f34bd69461", + "eval/run_longmemeval_v2.py": "2e003d8ecf4f44ac5a3f80bd3ba8fe18bc74e769298fefe63fde8bb2a98f3a04", + "eval/vector_scale.py": "3f9c327d9eca1a857512ddc208e972933be1a7d4fa7fa0017aca0cbf8fe7bb6d" + }, + "source_stable": true, + "environment": { + "ENGRAPHIS_EXTRACTOR": "none", + "PYTHONIOENCODING": "utf-8" + }, + "boundary": "Deterministic authored offline fixtures. No paid provider or independent coding-agent task evaluation.", + "checks": [ + { + "command": [ + "python", + "-m", + "eval.harness", + "--dataset", + "eval/datasets/sample.jsonl", + "--k", + "5" + ], + "exit_code": 0, + "seconds": 0.6209549999912269, + "output": "\nEngraphis eval - 9 questions @ k=5\n recall@k : 1.000\n hit@k : 1.000\n mrr@k : 0.889\n ndcg@k : 0.918\n answer_token_recall : 1.000\n\n", + "error": "" + }, + { + "command": [ + "python", + "-m", + "eval.harness", + "--dataset", + "eval/datasets/codemem.jsonl", + "--k", + "5" + ], + "exit_code": 0, + "seconds": 0.8100120999733917, + "output": "\nEngraphis eval - 26 questions @ k=5\n recall@k : 1.000\n hit@k : 1.000\n mrr@k : 0.962\n ndcg@k : 0.972\n answer_token_recall : 1.000\n\n", + "error": "" + }, + { + "command": [ + "python", + "-m", + "eval.ablation" + ], + "exit_code": 0, + "seconds": 1.162209099973552, + "output": "Engraphis ablation - recall@5\n vector-only : 1.0\n hybrid-1hop : 1.0\n hybrid-ppr : 1.0\n\nEngraphis ordinary-recall age ablation\n equal-reinforcement score delta (recent - 1y old): 0.00000000 (expected 0.00000000)\n\nEngraphis semantic-confidence micro-ablation (not a benchmark)\n default weak singleton wins : False\n opt-in calibrated lexical wins: True\n\nEngraphis ablation (multi-hop graph dataset) - arm-level recall@5\n (answers sit 2 entity-hops from the query; which arm can REACH them?)\n vector arm : 0.6667\n graph 1-hop : 0.0 (reaches 1 hop only)\n graph PPR : 1.0 (multi-hop walk)\n\nEngraphis retrieval-policy fixture - recall@5\n balanced : 0.6667\n auto : 1.0 (opt-in graph specialization)\n", + "error": "" + }, + { + "command": [ + "python", + "-m", + "eval.reinforcement" + ], + "exit_code": 0, + "seconds": 0.44589209998957813, + "output": "{\n \"checks\": {\n \"create_1000_under_10_days\": true,\n \"diminishing_create_gain\": true,\n \"diminishing_recall_gain\": true,\n \"finite\": true,\n \"nonnegative_create_gain\": true,\n \"nonnegative_recall_gain\": true,\n \"recall_1000_under_5_days\": true,\n \"recall_burst_90d_retention_below_1e_6\": true,\n \"within_policy_cap\": true\n },\n \"create_1000_stability_days\": 9.981381213109778,\n \"passed\": true,\n \"recall_1000_retention_after_90d\": 3.072187294081352e-10,\n \"recall_1000_stability_days\": 4.108939650691841,\n \"schema\": \"engraphis-reinforcement-eval/v1\"\n}\n", + "error": "" + }, + { + "command": [ + "python", + "-m", + "eval.adversarial_memory_security" + ], + "exit_code": 0, + "seconds": 0.7954388000071049, + "output": "Engraphis adversarial memory-security gate (offline deterministic fixture)\n instruction_content_quarantined: 1.000 (1/1)\n pending_content_review_gated: 1.000 (1/1)\n external_self_approval_downgraded: 1.000 (1/1)\n poisoned_content_absent_from_prompt_context: 1.000 (1/1)\n poisoned_direct_edge_absent_from_prompt_graph: 1.000 (1/1)\n poisoned_supported_edge_absent_from_prompt_graph: 1.000 (1/1)\n self_asserted_edge_absent_from_prompt_graph: 1.000 (1/1)\n trusted_memory_available_in_prompt_graph: 1.000 (1/1)\n result: PASS\n", + "error": "" + }, + { + "command": [ + "python", + "-m", + "eval.grounded" + ], + "exit_code": 0, + "seconds": 0.7731530000455678, + "output": "\nEngraphis grounded-recall eval (deterministic embedder)\n answerable -> grounded : 1.000 (5/5)\n off-topic -> abstained : 1.000 (6/6)\n decision accuracy : 1.000 (11/11)\n\n", + "error": "" + }, + { + "command": [ + "python", + "-m", + "eval.code_arm" + ], + "exit_code": 0, + "seconds": 1.0313526000245474, + "output": "code arm eval (recall@5, offline deterministic fixture)\n arm-isolated recall@5:\n vector 0.0000\n lexical 0.0000\n code 1.0000\n full-pipeline recall@5:\n balanced 0.0000\n code 1.0000\n strict lift (code arm reaches what vector/lexical miss): PASS\n", + "error": "" + } + ] +} diff --git a/docs/evidence/reliability/offline-gates-pr-final.json b/docs/evidence/reliability/offline-gates-pr-final.json new file mode 100644 index 00000000..b8d01ad2 --- /dev/null +++ b/docs/evidence/reliability/offline-gates-pr-final.json @@ -0,0 +1,315 @@ +{ + "schema": "engraphis-offline-validation/v1", + "date": "2026-09-05", + "source_before": { + "engraphis/core/browsing.py": "9adce7bbe8791fe4e76b892977d22cc72958c5d8ef3f98c58f50efdc1bb2fd36", + "engraphis/core/vector_repair.py": "425901615d3b602e3310af1611ebb3c0b78f3d793694c4ca8fffb55f00d07904", + "engraphis/core/vector_search.py": "75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234", + "eval/datasets/resolver_write_acceptance.jsonl": "e8db06aa41a93e4892e81d60bb71f06e656088f3eba36ebe37710850a97db6f7", + "eval/fts_insert_scaling.py": "a8ecfbe19695f6d4659c3edf389b007c965b214b64fdd58d5ee86635fa766144", + "eval/native_coverage_scaling.py": "0c7c3f5c80570c7ab02310f14927eb82399d1a624d65df895e937ed87339fe9c", + "eval/vector_scale_storage.py": "bda0c1139596eb9232cd75c826fc941101b6116bae32ad7895525d43f9d35484", + "eval/vector_scan_plan.py": "92de2f3b41aa457dcfee11998936ce35e023fe61596ed98f3836c998a6e9a1cd", + "engraphis/backends/__init__.py": "a9f22b9278362904166614081f1df78469d453601b298ce4e8afdba8a3722b25", + "engraphis/backends/codegraph.py": "83e723a91068d23694092fbe00157fcb2597061d453eb36f955bf55bc5b4d35e", + "engraphis/backends/embedder_api.py": "56a6bceea4f757325dcea987b0339e41d3875634b1ae5103f0537a358cf5878b", + "engraphis/backends/embedder_deterministic.py": "ec8b23de7e7e8273416125f5876ca96f55e4ae7881841bae55783ab0ba9130ad", + "engraphis/backends/embedder_st.py": "e1c20fd980e07060387e3f9fa37fe02a916959fc8abce4e6067a62699c726de1", + "engraphis/backends/encrypted_db.py": "204553016af879236575a34646a86e03d3433216f663de55cac9d2e54185355c", + "engraphis/backends/extractor.py": "f2e3455ab7f14caee1d5b5c4ef071e498e90118f1b0ddcb8510c969583b0fc57", + "engraphis/backends/graph_extractor.py": "88561efa0d3fabc447a0a005b10e36261379d46218cf62d905e6928cd2fda676", + "engraphis/backends/model_source.py": "8c3c7681f95214a2bbabd8de222e5ee11f42fe13402d27365654ae75fb363d4e", + "engraphis/backends/postgres_schema.py": "8468578c3add701d30d5eaa36d768ded2375d116e55f1836e09f6107a07a267e", + "engraphis/backends/query_planner.py": "bbdd77afc9b5523421b85b2ae63c8da7f5a7b777265450e0d21708a83e7bb23c", + "engraphis/backends/reranker.py": "747761d6cbfa421388974bcfd98d844f92391d80f4bf6a4feca00b0c7a6908ca", + "engraphis/backends/resources.py": "47cc867c3aecc8bd95fa284bc5bb04715f3339c19a0a11512973ef6171c95944", + "engraphis/backends/retention.py": "381d9371e3951d762f8b55eb54711de5697642acb39de99a714f246c059ecbd0", + "engraphis/backends/sync_folder.py": "e4f70a92a17f6a365910670df041e6e3ca421d44ada2827917cd66b4dc067bfa", + "engraphis/backends/sync_relay.py": "b8b9ad265453aba17ba7c27a355e12a793469b3e44cb217943c6fad9382a3006", + "engraphis/backends/vector_numpy.py": "c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72", + "engraphis/backends/vector_sqlitevec.py": "6148e14ceaacc19239b64c642a3fba0e98797c78cec356210157afadf475a08b", + "engraphis/core/__init__.py": "dd5143729c3939237f04636f437032b1f2d3a5f7d82c91bbc2a5a283c3f0ebaa", + "engraphis/core/adaptive_context.py": "cc5ce48109bb0d5230a5b2b8424b829c853feec5b5d5b82596413f2279b0c9f9", + "engraphis/core/codegraph_export.py": "4641074258d7b23498f92dd45053a0fbb111863eaad2001c08e5e3c2dc2fd54f", + "engraphis/core/conflicts.py": "28530be25a4af0bffd8f609b965b33ca7f93199a70887789b2148fda8a61a486", + "engraphis/core/consolidate.py": "147dd9359db3bf9843aa951de2328aff90c4b6013f6fe05386e602e3da92dc5e", + "engraphis/core/context.py": "3fbc5341adb353ad9b57c9b6368cdf166d1a116876af012214078d89eb53ddef", + "engraphis/core/documents.py": "84385db39ba44e06b58b4b26dbf954228ff4abed7230f28a7280166fa6457861", + "engraphis/core/engine.py": "0d76a5f028234933933aff7adb7078455051086ae1db9d0b41360540ed7e83e3", + "engraphis/core/fsutil.py": "6db770fa8bd3e1a57dfa70eb8e8bc46d48c2dc53ead1b0b43eeecf085ef58cfc", + "engraphis/core/graph_layers.py": "64d74ab01c77119f6343ba6f1d6a84f9653f1a5d34d47ce7966f3ac31b29d2ea", + "engraphis/core/graph_policy.py": "ec5b373d01adb2de87df31d9f543130018e9a73faaaed239a184f14a32646615", + "engraphis/core/graph_scene.py": "6348b9b6bb20c2e0689c8696452032c0b372d96cab312bfa508a07a6645386f8", + "engraphis/core/graphrank.py": "1279a58396104d3f906bfd5ec75b32efedfefe52201467bf19d80be3517017a5", + "engraphis/core/grounded.py": "86796c69e6a77927971c5d57e70d9c5d06793db1ff375a9dc1a73885cc2569ea", + "engraphis/core/ids.py": "80c2c0a0635af86ada33b46e5743f28001ff1004b727bbb790aa2393c234d86b", + "engraphis/core/interfaces.py": "5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22", + "engraphis/core/obsidian.py": "991267c153cb7c4c40f7fe8f50aa71688892e250aaaf383b22c9e5910dc263b7", + "engraphis/core/poisoning.py": "5bc67169ee8032f3777f2d3969dcf71821437bd473ce4f917c4b41a50e845fb6", + "engraphis/core/query_planner.py": "8d67af852564f95badfb60ac57a830a65820a01228d8837103cb125956e267a7", + "engraphis/core/recall.py": "7eaabf69a1c3f4555a307fc6e294562edb5c6c15e4df627daf09187b25f8fb15", + "engraphis/core/resolve.py": "ffc17dd39331bf0c6881fa2efa9bcca73f0b7c968135fffaa4dbb025bb32c674", + "engraphis/core/retention_policy.py": "864c03bdb6e743cd0002c706de471e920f1fa1f1a9918ab343ef4c2042b47429", + "engraphis/core/retrieval_policy.py": "7e970ac57762a1e55091bac46314c3d60aba0bf2cbb11405ce0f7e032448866e", + "engraphis/core/savings.py": "cfbcfc7e476f4e28028555cd519696e23099f6210cf0b733225832aeaa0bc7dc", + "engraphis/core/schema.py": "99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686", + "engraphis/core/scoring.py": "f5b6ac291edf0968b3de83cb1951a97d5bfd8a8d079199cb2ef95bba0884c89a", + "engraphis/core/secrets.py": "a4835ba06e2616156528df6365ca1aba6cba0c97cdf834a709d2797a371099d2", + "engraphis/core/store.py": "007272ec42011faaae25bdbaf8905cf0d2945bf6f451b60aa7a90fff0550debd", + "engraphis/core/sync.py": "6ecf4e2e1de83697a99f706efda261d2c59c1ab351c8a67ed003115d35221145", + "engraphis/core/textutil.py": "acd65031729fa5d91d09527b8eb52518b83cfce77a55e6b7ae2d94686e35c1a6", + "engraphis/core/user_model.py": "3147ec8ee7cfd855783f639874b63f331cdc822bd8bd9298cfb326dd18666026", + "eval/__init__.py": "639f0c6d9d6aac8ff6dc605a34a0a301058905cc53bff4eaed5912247f0e7c56", + "eval/ablation.py": "16f159dee75d2f96cc42f230c2403fa19ea0bda7091c4823660da553463a194a", + "eval/adversarial_memory_security.py": "35dd8d981bcbad50e9815465be420b05b62a9dea28a78eb6cc1e09ec51c320c4", + "eval/agent_benchmarks.py": "91c157b1d3d445fe0daa236edf21decb7e3713ad9bbaacbf0a46c0d8a5d1f2ee", + "eval/benchmark.py": "ed828e3548e8fb05ffafe9479791e545e56504421ff88676e4bf4466d6beaf91", + "eval/chunking_eval.py": "a16544353940c0a8c40cea3b9932d3399b35ea5994b809b78f5dbe4a952c467f", + "eval/code_agent_ab.py": "d98bba6b77700ff6bf86ff0d2cf518a67e5a2676ef51bbddabbb2ff26e1f3aaa", + "eval/code_arm.py": "d211166fce1b8a4173848e1617873b7e84aadeff43746effe7483c54bbdb6f1d", + "eval/consolidation_ranking.py": "917b578d4e0bcb929bf1a1a37611acf7716a520c076abf4ade0d8d12a1c455de", + "eval/context_economy.py": "709ac7cc866855f96d7717ab2bea12e8b0d3a140d15fb978a2a929ad085931f2", + "eval/context_efficiency_guardrails.py": "22afd1a6fe17219e74701dc587ec35f569a5bf22bea270944525f51746723f14", + "eval/datasets/adversarial.jsonl": "65ea84d0c2388cf5a317170ac5ac327454d09040788a8f036b2e1adf87a3204e", + "eval/datasets/code_arm.jsonl": "894411c42e049abcb4f9ed1e506d753ea30649b2be2c3e0b0d82fa5c2e3b8b50", + "eval/datasets/codemem.jsonl": "341313023c22850a2e14f02742b571ad1deca824f886a1654a59541304c01f3c", + "eval/datasets/consolidation_ranking.jsonl": "d7008550a276ea81ff057725d1dba4893e7842886c0e932f9dd9a043287028ae", + "eval/datasets/context_routing_stress.jsonl": "ead3b71cfb90b41e69ffeec9f8f5ecb57cb53ea7078c299bfd98d04913c7197a", + "eval/datasets/graph_layer_routing.jsonl": "e37451ef5c5f8532b81d6cbe02c9c5a84fb6eed3b96354a1fba9d8b987438ca5", + "eval/datasets/graph_multihop.jsonl": "aee614cfe6e597fbb04643931cbb235ba9edca1b9a5b4bfce9eae4a29927ab3f", + "eval/datasets/grounded_distractors.jsonl": "caf77f56f4f23f33af0e9ecb32696f3bfa27dc64c99bd0190586bf9501c1cd37", + "eval/datasets/handoff_quality.jsonl": "190e061c8e63d5e8fe16fc67f492a1c5b85e2bbc4e32f31cdf07db5166959cc2", + "eval/datasets/longdoc.jsonl": "7f5ade95e1f283d0db8cf78e53ed8995d3534f847e616d2c0005fd8da37ac790", + "eval/datasets/proactive_ranking.jsonl": "5b0cfbde93ca57297638798a0813537d116f968345dd0bfcd54d7c4d37a2896f", + "eval/datasets/redteam_poisoning.jsonl": "abf4180eb147393c1d41e5a8cdb8214621ee5c7759fc23750258ef9a2f779b49", + "eval/datasets/resolver_reworded_corrections.jsonl": "5b796d8852117b30060cf019e93fd775fea256ca492ce0fedf9b4786f0a0421b", + "eval/datasets/sample.jsonl": "b41a041bc289cff06411c9fed6502c7526bed04f03cc0e5adee78e70537b7cc9", + "eval/external.py": "d93cc071e555c31f4c5521ab0b2aeb370df24656e5ee7b60904113d97b1416ae", + "eval/extractor_quality.py": "50820fe7f821d111e17e4e77a3d1159e7d6979d110b052c4f254a5b309c2652a", + "eval/graph_every_bench.py": "79da573c5edf315f71bfab412d3ea283b8da45d1fdabc78ddd19302ead09e4f0", + "eval/graph_traversal.py": "b094f75c3a1d75ba3cf19e372187d692d3c595a1d9bde19bbe95ae0c79a5175f", + "eval/grounded.py": "053d5193b716a2c3e507fcd44057d392de910a4442b46bd7cc1f30ac0ba68541", + "eval/handoff_quality.py": "7daf635510764e236f48ca7e2537a85d8ebc1ad995513144329c1f0236405937", + "eval/harness.py": "d9c5f960e891a3d0912e519eec554a970c973b3b8893df861f0258aa16342310", + "eval/hosted_evidence.py": "7946cd8c1e3aa291268271b2aa11210d5f09c9b05ba635aa0b64bef07bdcee45", + "eval/hosted_ledger.py": "a53036d12ff671371c148910a816fb20c7e7f3346250a303722352f47b06c476", + "eval/hosted_luna.py": "4dbf02a65eec38bcde92d82952a0b372abbac1f68378d11a04528f4a31a835dc", + "eval/longmemeval_v2.py": "defb4d47f453aa4615a8f101b82df3fadf64f0f9eae0ce1021d86d3750dad437", + "eval/longmemeval_v2_evidence.py": "8562d32590e4267e033cb1da2a7fda626bdc2630ebbf8547df282896f34abf8b", + "eval/longmemeval_v2_matrix.py": "ca085cd59481813cce5dbdfbc94f40f67173ac3a1bc3dce6f6d3e09eb7b08153", + "eval/metrics.py": "16857e2cf6ed339cb57a26c9bfa1879444b4d279bd972e5a9fa644ed1308afe0", + "eval/performance.py": "e17ea78095e4e592717bc5d9d8e34d55fd98c3fd28d1a8104a20c227d4d619c9", + "eval/planned_recall.py": "12f3a3f36ca01e2fac717ed286edae36929a84febe7e3116f259b1295eaf4443", + "eval/proactive_ranking.py": "8610541f1d547f9c0eb46d078dbcaa670c0a97157acc37f96ec08b482cb7a6ab", + "eval/productivity.py": "6d4644ebdc44472aeb3879963774fab276bfa269b781139c46ded48774a22717", + "eval/public_readiness.py": "5ce8a18d0bfe09e75e88548a589b8fc6d2cbf04c0f8cd1ce51cd9519136a2751", + "eval/redteam_poisoning.py": "fce120cc3adf2ee966b59cd3ea7f20d242a0af49b52492143543130fe14c0cc8", + "eval/reinforcement.py": "72ed766775a2658eaa728afec51c0ac22d97a90e813111954df66a6ec50f2bef", + "eval/resolver_reworded_corrections.py": "f89e0920986efecd736380f0b8021aae553cfb304761f489d49828e6ffec537e", + "eval/resource_hierarchy.py": "5ab6c989bb143c4386749c45a33c190447e829c1b5657e8c0aca30f34bd69461", + "eval/run_longmemeval_v2.py": "2e003d8ecf4f44ac5a3f80bd3ba8fe18bc74e769298fefe63fde8bb2a98f3a04", + "eval/vector_scale.py": "3f9c327d9eca1a857512ddc208e972933be1a7d4fa7fa0017aca0cbf8fe7bb6d" + }, + "source_after": { + "engraphis/core/browsing.py": "9adce7bbe8791fe4e76b892977d22cc72958c5d8ef3f98c58f50efdc1bb2fd36", + "engraphis/core/vector_repair.py": "425901615d3b602e3310af1611ebb3c0b78f3d793694c4ca8fffb55f00d07904", + "engraphis/core/vector_search.py": "75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234", + "eval/datasets/resolver_write_acceptance.jsonl": "e8db06aa41a93e4892e81d60bb71f06e656088f3eba36ebe37710850a97db6f7", + "eval/fts_insert_scaling.py": "a8ecfbe19695f6d4659c3edf389b007c965b214b64fdd58d5ee86635fa766144", + "eval/native_coverage_scaling.py": "0c7c3f5c80570c7ab02310f14927eb82399d1a624d65df895e937ed87339fe9c", + "eval/vector_scale_storage.py": "bda0c1139596eb9232cd75c826fc941101b6116bae32ad7895525d43f9d35484", + "eval/vector_scan_plan.py": "92de2f3b41aa457dcfee11998936ce35e023fe61596ed98f3836c998a6e9a1cd", + "engraphis/backends/__init__.py": "a9f22b9278362904166614081f1df78469d453601b298ce4e8afdba8a3722b25", + "engraphis/backends/codegraph.py": "83e723a91068d23694092fbe00157fcb2597061d453eb36f955bf55bc5b4d35e", + "engraphis/backends/embedder_api.py": "56a6bceea4f757325dcea987b0339e41d3875634b1ae5103f0537a358cf5878b", + "engraphis/backends/embedder_deterministic.py": "ec8b23de7e7e8273416125f5876ca96f55e4ae7881841bae55783ab0ba9130ad", + "engraphis/backends/embedder_st.py": "e1c20fd980e07060387e3f9fa37fe02a916959fc8abce4e6067a62699c726de1", + "engraphis/backends/encrypted_db.py": "204553016af879236575a34646a86e03d3433216f663de55cac9d2e54185355c", + "engraphis/backends/extractor.py": "f2e3455ab7f14caee1d5b5c4ef071e498e90118f1b0ddcb8510c969583b0fc57", + "engraphis/backends/graph_extractor.py": "88561efa0d3fabc447a0a005b10e36261379d46218cf62d905e6928cd2fda676", + "engraphis/backends/model_source.py": "8c3c7681f95214a2bbabd8de222e5ee11f42fe13402d27365654ae75fb363d4e", + "engraphis/backends/postgres_schema.py": "8468578c3add701d30d5eaa36d768ded2375d116e55f1836e09f6107a07a267e", + "engraphis/backends/query_planner.py": "bbdd77afc9b5523421b85b2ae63c8da7f5a7b777265450e0d21708a83e7bb23c", + "engraphis/backends/reranker.py": "747761d6cbfa421388974bcfd98d844f92391d80f4bf6a4feca00b0c7a6908ca", + "engraphis/backends/resources.py": "47cc867c3aecc8bd95fa284bc5bb04715f3339c19a0a11512973ef6171c95944", + "engraphis/backends/retention.py": "381d9371e3951d762f8b55eb54711de5697642acb39de99a714f246c059ecbd0", + "engraphis/backends/sync_folder.py": "e4f70a92a17f6a365910670df041e6e3ca421d44ada2827917cd66b4dc067bfa", + "engraphis/backends/sync_relay.py": "b8b9ad265453aba17ba7c27a355e12a793469b3e44cb217943c6fad9382a3006", + "engraphis/backends/vector_numpy.py": "c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72", + "engraphis/backends/vector_sqlitevec.py": "6148e14ceaacc19239b64c642a3fba0e98797c78cec356210157afadf475a08b", + "engraphis/core/__init__.py": "dd5143729c3939237f04636f437032b1f2d3a5f7d82c91bbc2a5a283c3f0ebaa", + "engraphis/core/adaptive_context.py": "cc5ce48109bb0d5230a5b2b8424b829c853feec5b5d5b82596413f2279b0c9f9", + "engraphis/core/codegraph_export.py": "4641074258d7b23498f92dd45053a0fbb111863eaad2001c08e5e3c2dc2fd54f", + "engraphis/core/conflicts.py": "28530be25a4af0bffd8f609b965b33ca7f93199a70887789b2148fda8a61a486", + "engraphis/core/consolidate.py": "147dd9359db3bf9843aa951de2328aff90c4b6013f6fe05386e602e3da92dc5e", + "engraphis/core/context.py": "3fbc5341adb353ad9b57c9b6368cdf166d1a116876af012214078d89eb53ddef", + "engraphis/core/documents.py": "84385db39ba44e06b58b4b26dbf954228ff4abed7230f28a7280166fa6457861", + "engraphis/core/engine.py": "0d76a5f028234933933aff7adb7078455051086ae1db9d0b41360540ed7e83e3", + "engraphis/core/fsutil.py": "6db770fa8bd3e1a57dfa70eb8e8bc46d48c2dc53ead1b0b43eeecf085ef58cfc", + "engraphis/core/graph_layers.py": "64d74ab01c77119f6343ba6f1d6a84f9653f1a5d34d47ce7966f3ac31b29d2ea", + "engraphis/core/graph_policy.py": "ec5b373d01adb2de87df31d9f543130018e9a73faaaed239a184f14a32646615", + "engraphis/core/graph_scene.py": "6348b9b6bb20c2e0689c8696452032c0b372d96cab312bfa508a07a6645386f8", + "engraphis/core/graphrank.py": "1279a58396104d3f906bfd5ec75b32efedfefe52201467bf19d80be3517017a5", + "engraphis/core/grounded.py": "86796c69e6a77927971c5d57e70d9c5d06793db1ff375a9dc1a73885cc2569ea", + "engraphis/core/ids.py": "80c2c0a0635af86ada33b46e5743f28001ff1004b727bbb790aa2393c234d86b", + "engraphis/core/interfaces.py": "5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22", + "engraphis/core/obsidian.py": "991267c153cb7c4c40f7fe8f50aa71688892e250aaaf383b22c9e5910dc263b7", + "engraphis/core/poisoning.py": "5bc67169ee8032f3777f2d3969dcf71821437bd473ce4f917c4b41a50e845fb6", + "engraphis/core/query_planner.py": "8d67af852564f95badfb60ac57a830a65820a01228d8837103cb125956e267a7", + "engraphis/core/recall.py": "7eaabf69a1c3f4555a307fc6e294562edb5c6c15e4df627daf09187b25f8fb15", + "engraphis/core/resolve.py": "ffc17dd39331bf0c6881fa2efa9bcca73f0b7c968135fffaa4dbb025bb32c674", + "engraphis/core/retention_policy.py": "864c03bdb6e743cd0002c706de471e920f1fa1f1a9918ab343ef4c2042b47429", + "engraphis/core/retrieval_policy.py": "7e970ac57762a1e55091bac46314c3d60aba0bf2cbb11405ce0f7e032448866e", + "engraphis/core/savings.py": "cfbcfc7e476f4e28028555cd519696e23099f6210cf0b733225832aeaa0bc7dc", + "engraphis/core/schema.py": "99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686", + "engraphis/core/scoring.py": "f5b6ac291edf0968b3de83cb1951a97d5bfd8a8d079199cb2ef95bba0884c89a", + "engraphis/core/secrets.py": "a4835ba06e2616156528df6365ca1aba6cba0c97cdf834a709d2797a371099d2", + "engraphis/core/store.py": "007272ec42011faaae25bdbaf8905cf0d2945bf6f451b60aa7a90fff0550debd", + "engraphis/core/sync.py": "6ecf4e2e1de83697a99f706efda261d2c59c1ab351c8a67ed003115d35221145", + "engraphis/core/textutil.py": "acd65031729fa5d91d09527b8eb52518b83cfce77a55e6b7ae2d94686e35c1a6", + "engraphis/core/user_model.py": "3147ec8ee7cfd855783f639874b63f331cdc822bd8bd9298cfb326dd18666026", + "eval/__init__.py": "639f0c6d9d6aac8ff6dc605a34a0a301058905cc53bff4eaed5912247f0e7c56", + "eval/ablation.py": "16f159dee75d2f96cc42f230c2403fa19ea0bda7091c4823660da553463a194a", + "eval/adversarial_memory_security.py": "35dd8d981bcbad50e9815465be420b05b62a9dea28a78eb6cc1e09ec51c320c4", + "eval/agent_benchmarks.py": "91c157b1d3d445fe0daa236edf21decb7e3713ad9bbaacbf0a46c0d8a5d1f2ee", + "eval/benchmark.py": "ed828e3548e8fb05ffafe9479791e545e56504421ff88676e4bf4466d6beaf91", + "eval/chunking_eval.py": "a16544353940c0a8c40cea3b9932d3399b35ea5994b809b78f5dbe4a952c467f", + "eval/code_agent_ab.py": "d98bba6b77700ff6bf86ff0d2cf518a67e5a2676ef51bbddabbb2ff26e1f3aaa", + "eval/code_arm.py": "d211166fce1b8a4173848e1617873b7e84aadeff43746effe7483c54bbdb6f1d", + "eval/consolidation_ranking.py": "917b578d4e0bcb929bf1a1a37611acf7716a520c076abf4ade0d8d12a1c455de", + "eval/context_economy.py": "709ac7cc866855f96d7717ab2bea12e8b0d3a140d15fb978a2a929ad085931f2", + "eval/context_efficiency_guardrails.py": "22afd1a6fe17219e74701dc587ec35f569a5bf22bea270944525f51746723f14", + "eval/datasets/adversarial.jsonl": "65ea84d0c2388cf5a317170ac5ac327454d09040788a8f036b2e1adf87a3204e", + "eval/datasets/code_arm.jsonl": "894411c42e049abcb4f9ed1e506d753ea30649b2be2c3e0b0d82fa5c2e3b8b50", + "eval/datasets/codemem.jsonl": "341313023c22850a2e14f02742b571ad1deca824f886a1654a59541304c01f3c", + "eval/datasets/consolidation_ranking.jsonl": "d7008550a276ea81ff057725d1dba4893e7842886c0e932f9dd9a043287028ae", + "eval/datasets/context_routing_stress.jsonl": "ead3b71cfb90b41e69ffeec9f8f5ecb57cb53ea7078c299bfd98d04913c7197a", + "eval/datasets/graph_layer_routing.jsonl": "e37451ef5c5f8532b81d6cbe02c9c5a84fb6eed3b96354a1fba9d8b987438ca5", + "eval/datasets/graph_multihop.jsonl": "aee614cfe6e597fbb04643931cbb235ba9edca1b9a5b4bfce9eae4a29927ab3f", + "eval/datasets/grounded_distractors.jsonl": "caf77f56f4f23f33af0e9ecb32696f3bfa27dc64c99bd0190586bf9501c1cd37", + "eval/datasets/handoff_quality.jsonl": "190e061c8e63d5e8fe16fc67f492a1c5b85e2bbc4e32f31cdf07db5166959cc2", + "eval/datasets/longdoc.jsonl": "7f5ade95e1f283d0db8cf78e53ed8995d3534f847e616d2c0005fd8da37ac790", + "eval/datasets/proactive_ranking.jsonl": "5b0cfbde93ca57297638798a0813537d116f968345dd0bfcd54d7c4d37a2896f", + "eval/datasets/redteam_poisoning.jsonl": "abf4180eb147393c1d41e5a8cdb8214621ee5c7759fc23750258ef9a2f779b49", + "eval/datasets/resolver_reworded_corrections.jsonl": "5b796d8852117b30060cf019e93fd775fea256ca492ce0fedf9b4786f0a0421b", + "eval/datasets/sample.jsonl": "b41a041bc289cff06411c9fed6502c7526bed04f03cc0e5adee78e70537b7cc9", + "eval/external.py": "d93cc071e555c31f4c5521ab0b2aeb370df24656e5ee7b60904113d97b1416ae", + "eval/extractor_quality.py": "50820fe7f821d111e17e4e77a3d1159e7d6979d110b052c4f254a5b309c2652a", + "eval/graph_every_bench.py": "79da573c5edf315f71bfab412d3ea283b8da45d1fdabc78ddd19302ead09e4f0", + "eval/graph_traversal.py": "b094f75c3a1d75ba3cf19e372187d692d3c595a1d9bde19bbe95ae0c79a5175f", + "eval/grounded.py": "053d5193b716a2c3e507fcd44057d392de910a4442b46bd7cc1f30ac0ba68541", + "eval/handoff_quality.py": "7daf635510764e236f48ca7e2537a85d8ebc1ad995513144329c1f0236405937", + "eval/harness.py": "d9c5f960e891a3d0912e519eec554a970c973b3b8893df861f0258aa16342310", + "eval/hosted_evidence.py": "7946cd8c1e3aa291268271b2aa11210d5f09c9b05ba635aa0b64bef07bdcee45", + "eval/hosted_ledger.py": "a53036d12ff671371c148910a816fb20c7e7f3346250a303722352f47b06c476", + "eval/hosted_luna.py": "4dbf02a65eec38bcde92d82952a0b372abbac1f68378d11a04528f4a31a835dc", + "eval/longmemeval_v2.py": "defb4d47f453aa4615a8f101b82df3fadf64f0f9eae0ce1021d86d3750dad437", + "eval/longmemeval_v2_evidence.py": "8562d32590e4267e033cb1da2a7fda626bdc2630ebbf8547df282896f34abf8b", + "eval/longmemeval_v2_matrix.py": "ca085cd59481813cce5dbdfbc94f40f67173ac3a1bc3dce6f6d3e09eb7b08153", + "eval/metrics.py": "16857e2cf6ed339cb57a26c9bfa1879444b4d279bd972e5a9fa644ed1308afe0", + "eval/performance.py": "e17ea78095e4e592717bc5d9d8e34d55fd98c3fd28d1a8104a20c227d4d619c9", + "eval/planned_recall.py": "12f3a3f36ca01e2fac717ed286edae36929a84febe7e3116f259b1295eaf4443", + "eval/proactive_ranking.py": "8610541f1d547f9c0eb46d078dbcaa670c0a97157acc37f96ec08b482cb7a6ab", + "eval/productivity.py": "6d4644ebdc44472aeb3879963774fab276bfa269b781139c46ded48774a22717", + "eval/public_readiness.py": "5ce8a18d0bfe09e75e88548a589b8fc6d2cbf04c0f8cd1ce51cd9519136a2751", + "eval/redteam_poisoning.py": "fce120cc3adf2ee966b59cd3ea7f20d242a0af49b52492143543130fe14c0cc8", + "eval/reinforcement.py": "72ed766775a2658eaa728afec51c0ac22d97a90e813111954df66a6ec50f2bef", + "eval/resolver_reworded_corrections.py": "f89e0920986efecd736380f0b8021aae553cfb304761f489d49828e6ffec537e", + "eval/resource_hierarchy.py": "5ab6c989bb143c4386749c45a33c190447e829c1b5657e8c0aca30f34bd69461", + "eval/run_longmemeval_v2.py": "2e003d8ecf4f44ac5a3f80bd3ba8fe18bc74e769298fefe63fde8bb2a98f3a04", + "eval/vector_scale.py": "3f9c327d9eca1a857512ddc208e972933be1a7d4fa7fa0017aca0cbf8fe7bb6d" + }, + "source_stable": true, + "environment": { + "ENGRAPHIS_EXTRACTOR": "none", + "PYTHONIOENCODING": "utf-8" + }, + "boundary": "Deterministic authored offline fixtures. No paid provider or independent coding-agent task evaluation.", + "checks": [ + { + "command": [ + "python", + "-m", + "eval.harness", + "--dataset", + "eval/datasets/sample.jsonl", + "--k", + "5" + ], + "exit_code": 0, + "seconds": 0.5675395000143908, + "output": "\nEngraphis eval - 9 questions @ k=5\n recall@k : 1.000\n hit@k : 1.000\n mrr@k : 0.889\n ndcg@k : 0.918\n answer_token_recall : 1.000\n\n", + "error": "" + }, + { + "command": [ + "python", + "-m", + "eval.harness", + "--dataset", + "eval/datasets/codemem.jsonl", + "--k", + "5" + ], + "exit_code": 0, + "seconds": 0.7904866000171751, + "output": "\nEngraphis eval - 26 questions @ k=5\n recall@k : 1.000\n hit@k : 1.000\n mrr@k : 0.962\n ndcg@k : 0.972\n answer_token_recall : 1.000\n\n", + "error": "" + }, + { + "command": [ + "python", + "-m", + "eval.ablation" + ], + "exit_code": 0, + "seconds": 1.0525584999704733, + "output": "Engraphis ablation - recall@5\n vector-only : 1.0\n hybrid-1hop : 1.0\n hybrid-ppr : 1.0\n\nEngraphis ordinary-recall age ablation\n equal-reinforcement score delta (recent - 1y old): 0.00000000 (expected 0.00000000)\n\nEngraphis semantic-confidence micro-ablation (not a benchmark)\n default weak singleton wins : False\n opt-in calibrated lexical wins: True\n\nEngraphis ablation (multi-hop graph dataset) - arm-level recall@5\n (answers sit 2 entity-hops from the query; which arm can REACH them?)\n vector arm : 0.6667\n graph 1-hop : 0.0 (reaches 1 hop only)\n graph PPR : 1.0 (multi-hop walk)\n\nEngraphis retrieval-policy fixture - recall@5\n balanced : 0.6667\n auto : 1.0 (opt-in graph specialization)\n", + "error": "" + }, + { + "command": [ + "python", + "-m", + "eval.reinforcement" + ], + "exit_code": 0, + "seconds": 0.44519279996166006, + "output": "{\n \"checks\": {\n \"create_1000_under_10_days\": true,\n \"diminishing_create_gain\": true,\n \"diminishing_recall_gain\": true,\n \"finite\": true,\n \"nonnegative_create_gain\": true,\n \"nonnegative_recall_gain\": true,\n \"recall_1000_under_5_days\": true,\n \"recall_burst_90d_retention_below_1e_6\": true,\n \"within_policy_cap\": true\n },\n \"create_1000_stability_days\": 9.981381213109778,\n \"passed\": true,\n \"recall_1000_retention_after_90d\": 3.072187294081352e-10,\n \"recall_1000_stability_days\": 4.108939650691841,\n \"schema\": \"engraphis-reinforcement-eval/v1\"\n}\n", + "error": "" + }, + { + "command": [ + "python", + "-m", + "eval.adversarial_memory_security" + ], + "exit_code": 0, + "seconds": 0.7455995000200346, + "output": "Engraphis adversarial memory-security gate (offline deterministic fixture)\n instruction_content_quarantined: 1.000 (1/1)\n pending_content_review_gated: 1.000 (1/1)\n external_self_approval_downgraded: 1.000 (1/1)\n poisoned_content_absent_from_prompt_context: 1.000 (1/1)\n poisoned_direct_edge_absent_from_prompt_graph: 1.000 (1/1)\n poisoned_supported_edge_absent_from_prompt_graph: 1.000 (1/1)\n self_asserted_edge_absent_from_prompt_graph: 1.000 (1/1)\n trusted_memory_available_in_prompt_graph: 1.000 (1/1)\n result: PASS\n", + "error": "" + }, + { + "command": [ + "python", + "-m", + "eval.grounded" + ], + "exit_code": 0, + "seconds": 0.7352112000226043, + "output": "\nEngraphis grounded-recall eval (deterministic embedder)\n answerable -> grounded : 1.000 (5/5)\n off-topic -> abstained : 1.000 (6/6)\n decision accuracy : 1.000 (11/11)\n\n", + "error": "" + }, + { + "command": [ + "python", + "-m", + "eval.code_arm" + ], + "exit_code": 0, + "seconds": 0.9965413000318222, + "output": "code arm eval (recall@5, offline deterministic fixture)\n arm-isolated recall@5:\n vector 0.0000\n lexical 0.0000\n code 1.0000\n full-pipeline recall@5:\n balanced 0.0000\n code 1.0000\n strict lift (code arm reaches what vector/lexical miss): PASS\n", + "error": "" + } + ] +} diff --git a/docs/evidence/reliability/offline-gates.json b/docs/evidence/reliability/offline-gates.json new file mode 100644 index 00000000..be3d8574 --- /dev/null +++ b/docs/evidence/reliability/offline-gates.json @@ -0,0 +1,44 @@ +[ + { + "command": "python -m eval.harness --dataset eval/datasets/sample.jsonl --k 5", + "exit_code": 0, + "output": "\nEngraphis eval - 9 questions @ k=5\n recall@k : 1.000\n hit@k : 1.000\n mrr@k : 0.889\n ndcg@k : 0.918\n answer_token_recall : 1.000\n\n", + "error": "" + }, + { + "command": "python -m eval.harness --dataset eval/datasets/codemem.jsonl --k 5", + "exit_code": 0, + "output": "\nEngraphis eval - 26 questions @ k=5\n recall@k : 1.000\n hit@k : 1.000\n mrr@k : 0.962\n ndcg@k : 0.972\n answer_token_recall : 1.000\n\n", + "error": "" + }, + { + "command": "python -m eval.ablation", + "exit_code": 0, + "output": "Engraphis ablation - recall@5\n vector-only : 1.0\n hybrid-1hop : 1.0\n hybrid-ppr : 1.0\n\nEngraphis ordinary-recall age ablation\n equal-reinforcement score delta (recent - 1y old): 0.00000000 (expected 0.00000000)\n\nEngraphis semantic-confidence micro-ablation (not a benchmark)\n default weak singleton wins : False\n opt-in calibrated lexical wins: True\n\nEngraphis ablation (multi-hop graph dataset) - arm-level recall@5\n (answers sit 2 entity-hops from the query; which arm can REACH them?)\n vector arm : 0.6667\n graph 1-hop : 0.0 (reaches 1 hop only)\n graph PPR : 1.0 (multi-hop walk)\n\nEngraphis retrieval-policy fixture - recall@5\n balanced : 0.6667\n auto : 1.0 (opt-in graph specialization)\n", + "error": "" + }, + { + "command": "python -m eval.reinforcement", + "exit_code": 0, + "output": "{\n \"checks\": {\n \"create_1000_under_10_days\": true,\n \"diminishing_create_gain\": true,\n \"diminishing_recall_gain\": true,\n \"finite\": true,\n \"nonnegative_create_gain\": true,\n \"nonnegative_recall_gain\": true,\n \"recall_1000_under_5_days\": true,\n \"recall_burst_90d_retention_below_1e_6\": true,\n \"within_policy_cap\": true\n },\n \"create_1000_stability_days\": 9.981381213109778,\n \"passed\": true,\n \"recall_1000_retention_after_90d\": 3.072187294081352e-10,\n \"recall_1000_stability_days\": 4.108939650691841,\n \"schema\": \"engraphis-reinforcement-eval/v1\"\n}\n", + "error": "" + }, + { + "command": "python -m eval.adversarial_memory_security", + "exit_code": 0, + "output": "Engraphis adversarial memory-security gate (offline deterministic fixture)\n instruction_content_quarantined: 1.000 (1/1)\n pending_content_review_gated: 1.000 (1/1)\n external_self_approval_downgraded: 1.000 (1/1)\n poisoned_content_absent_from_prompt_context: 1.000 (1/1)\n poisoned_direct_edge_absent_from_prompt_graph: 1.000 (1/1)\n poisoned_supported_edge_absent_from_prompt_graph: 1.000 (1/1)\n self_asserted_edge_absent_from_prompt_graph: 1.000 (1/1)\n trusted_memory_available_in_prompt_graph: 1.000 (1/1)\n result: PASS\n", + "error": "" + }, + { + "command": "python -m eval.grounded", + "exit_code": 0, + "output": "\nEngraphis grounded-recall eval (deterministic embedder)\n answerable -> grounded : 1.000 (5/5)\n off-topic -> abstained : 1.000 (6/6)\n decision accuracy : 1.000 (11/11)\n\n", + "error": "" + }, + { + "command": "python -m eval.code_arm", + "exit_code": 0, + "output": "code arm eval (recall@5, offline deterministic fixture)\n arm-isolated recall@5:\n vector 0.0000\n lexical 0.0000\n code 1.0000\n full-pipeline recall@5:\n balanced 0.0000\n code 1.0000\n strict lift (code arm reaches what vector/lexical miss): PASS\n", + "error": "" + } +] \ No newline at end of file diff --git a/docs/evidence/reliability/pr-benchmark-source-check.json b/docs/evidence/reliability/pr-benchmark-source-check.json new file mode 100644 index 00000000..23d7eb35 --- /dev/null +++ b/docs/evidence/reliability/pr-benchmark-source-check.json @@ -0,0 +1,37 @@ +{ + "schema": "engraphis-benchmark-source-check/v1", + "checks": [ + { + "artifact": "vector-scale-numpy-corrected-20260905.json", + "source_files": [ + "eval/vector_scale.py", + "eval/vector_scale_storage.py", + "eval/benchmark.py", + "engraphis/backends/vector_numpy.py", + "engraphis/backends/vector_sqlitevec.py", + "engraphis/core/vector_search.py", + "engraphis/core/store.py", + "engraphis/core/schema.py", + "engraphis/core/interfaces.py" + ], + "all_measured_source_hashes_match": true, + "cells": 6 + }, + { + "artifact": "vector-scale-sqlite-vec-corrected-20260905.json", + "source_files": [ + "eval/vector_scale.py", + "eval/vector_scale_storage.py", + "eval/benchmark.py", + "engraphis/backends/vector_numpy.py", + "engraphis/backends/vector_sqlitevec.py", + "engraphis/core/vector_search.py", + "engraphis/core/store.py", + "engraphis/core/schema.py", + "engraphis/core/interfaces.py" + ], + "all_measured_source_hashes_match": true, + "cells": 6 + } + ] +} diff --git a/docs/evidence/reliability/pr-review.json b/docs/evidence/reliability/pr-review.json new file mode 100644 index 00000000..86b37c11 --- /dev/null +++ b/docs/evidence/reliability/pr-review.json @@ -0,0 +1,98 @@ +{ + "schema": "engraphis-pr-review/v1", + "date": "2026-09-05", + "scope": "Complete original public feature branch relative to released main plus all non-ignored local candidates in the public, private cloud and private website repositories.", + "workers": { + "count": 4, + "depth": 1, + "areas": [ + "architecture/storage", + "retrieval/interfaces/experience", + "historical branch and stash reconciliation", + "security/private cloud" + ], + "routing": "internal workers in the current task; parent integration only; no descendants or separate tasks" + }, + "reconciliation": { + "historical_noncurrent_branches": 16, + "already_merged_exact_trees": 9, + "ancestors_of_merged_heads": 3, + "all_changed_blobs_in_main": 2, + "superseded_drafts": 2, + "stashes": 1, + "stash_unique_blobs": 0, + "auxiliary_worktree_clean": true, + "preserved": true, + "private_details": "retained in the private repository and local review inventory, not copied here" + }, + "confirmed_review_repairs": [ + "No cross-memory clause pruning: identical text can have distinct title, source, subject, pronoun or condition bindings.", + "Native store-sharing batch vectors commit or roll back with canonical state.", + "Keep the established 12000-memory graph window; a 500 cutoff loses older two-hop evidence.", + "MCP gist retains canonical packed context within its budget; response caps retain or omit complete context and refresh accounting.", + "Classic workspace approval controls and failure messages consistently disclose explicit opt-in.", + "Website structured data, plan durations, MCP interface claims and release smoke version follow authoritative contracts.", + "Private SQLite policy revision updates reject stale enable races, including concurrent creation.", + "Encoded auth routes share the canonical upstream authentication abuse budget.", + "Original manual graph diagnostic reserves its own port and private in-memory server before any request." + ], + "parent_integration_corrections": { + "failed_full_run": "public-pr-source-before-contract-correction.json", + "isolated_reproduction": { + "failed": 2, + "passed_after_correction": 2 + }, + "cause": "Old automatic-processing copy and pinned Ledger cache version in two existing assertions; production consent-error toasts were also corrected.", + "line_ending_proof": "line-ending-equivalence.json" + }, + "checks": { + "core_focused": [ + { + "passed": 324, + "skipped": 2 + }, + { + "passed": 186, + "overlaps_previous": true + } + ], + "ui_python": 119, + "ui_browser_chromium": 9, + "website_unit": 24, + "website_browser_chromium_axe": 11, + "pi_unit": 21, + "pi_process_restart": 1, + "pi_typescript_package": "passed", + "prime_unit": 136, + "prime_skipped": 1, + "manual_probe": 1, + "manual_owned_server_smoke": "passed; full physics diagnostic not run", + "cloud_unit": 1157, + "cloud_skipped": 2, + "edge_total": 13, + "edge_real_workerd_included": 4, + "cross_repository_policy_races": 3, + "static_contracts": "Ruff, Pyright, MCP export, CSP, commercial public/cloud/site checks passed", + "offline_gates": "offline-gates-pr-final.json" + }, + "candidate_scan": { + "high_specificity_credential_candidates": 2, + "disposition": "Both are deliberate non-live URL-validation fixtures in tests/test_hosted_plan_resolution.py and tests/test_storage_concurrency_repair.py; no real credential found. Ignored data and credentials are excluded." + }, + "final_reviewed_source": { + "engraphis/core/context.py": "3fbc5341adb353ad9b57c9b6368cdf166d1a116876af012214078d89eb53ddef", + "engraphis/core/engine.py": "0d76a5f028234933933aff7adb7078455051086ae1db9d0b41360540ed7e83e3", + "engraphis/core/recall.py": "7eaabf69a1c3f4555a307fc6e294562edb5c6c15e4df627daf09187b25f8fb15", + "engraphis/mcp_server.py": "ad49631bcc7ef9dede5d2b8b1182f27eb8d01fd103be4dfeb13014bbbd84081d", + "engraphis/classic_assets/dashboard.js": "5b2f8e6bdcef718b481ceee77687440b5814b89b95483858246f56a19a03496d", + "engraphis/static/dashboard.js": "5b2f8e6bdcef718b481ceee77687440b5814b89b95483858246f56a19a03496d", + "engraphis/dashboard_assets/ledger.js": "9a7abb256636302ec5970fbfcfc96ea8e961e49b26d3a01389c2b51d0a0b8c4f" + }, + "limitations": [ + "local validation is not release approval", + "PostgreSQL and supported remote CI gates are separate", + "no paid evaluation or user study", + "no production restore or credential rotation", + "website main-contract check depends on public change merging" + ] +} diff --git a/docs/evidence/reliability/public-complete-source-before-trial-test-correction.json b/docs/evidence/reliability/public-complete-source-before-trial-test-correction.json new file mode 100644 index 00000000..d99d50c2 --- /dev/null +++ b/docs/evidence/reliability/public-complete-source-before-trial-test-correction.json @@ -0,0 +1,199 @@ +{ + "schema": "engraphis-validation-source/v1", + "command": [ + "python", + "-m", + "pytest", + "tests/", + "-q", + "--tb=short" + ], + "environment": { + "ENGRAPHIS_EXTRACTOR": "none", + "sqlite_vec": "0.1.9 isolated installation" + }, + "elapsed_seconds": 448.2978430999792, + "exit_code": 1, + "source_before": { + ".github/workflows/ci.yml": "368fdd94d2e8b45995301b45f5771cce2a127fec9b0774d8e86a736fa8e02317", + "AGENTS.md": "77571f62390e516b445849eef4e9aa9ea7b6292efc7143868e38785f46e1f50a", + "CHANGELOG.md": "4a4b4adce6cdceb56c900a0657bd6036fdd4bd6c4a535d6a35402808df759593", + "README.md": "34975b33dda2e51e167793f78041d918e993589bf688d0d1013e71cf4210d3c3", + "docs/HOSTED_PLANS.md": "676fe44c6bf7bffdbd926eb269090004eff28cd00fc61501018588ab52e1df24", + "docs/HOSTING_RAILWAY.md": "81e530eea41b85f43b3746535db8bd5d16c2c45df39ad2fb1ebf3bf09e3b24bc", + "docs/MCP_CONTRACT.json": "5af24a7eea28a1891c1b93e62bdfb30c4888f8e2e41e1922480b51f4b8990ed1", + "docs/MCP_TOOLS.md": "4ff942fb37484b54b88ec92e941e6dcd2010398ded29bfec4a94174d3eaaff51", + "docs/PAID_EVALUATION_PROPOSAL.md": "dbdc3d91305df86c26365962044ed481d7f1f980afa0c526b3feafa08d007de4", + "docs/RAILWAY_TEMPLATE.md": "3a4cef99ad95b7a2c78e1dffc8fe860230d59c39d3e3267b727c51d792988546", + "docs/SYNC.md": "fe2f5ff8158ca4aa505684d9ee6b077108195b3e8cfd75dd9d5d5b77c40a11e7", + "engraphis/backends/vector_numpy.py": "c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72", + "engraphis/backends/vector_sqlitevec.py": "6148e14ceaacc19239b64c642a3fba0e98797c78cec356210157afadf475a08b", + "engraphis/classic_assets/dashboard.js": "98f7f267dc58936d5a3741be3a80959deafc541323bc47794a76730181f7b0c3", + "engraphis/classic_assets/index.html": "3173ab69f2be4dbdf4310cc23eab6f85cc537e5bcede83fe998b7906b8496460", + "engraphis/cloud_features.py": "a1bec76216d4f1276a3313dabc1d29a1666378e11863d05d1a02e695cf6d7293", + "engraphis/commercial.py": "184f312066a9e682e51a0abeff042f1c0e8eed2d47470157b23930b5a17633aa", + "engraphis/commercial_manifest.json": "27dfb332a5b4f3ceeed22aef37e1f55e1c051ab69cc9eedc4f826ddf4c752162", + "engraphis/core/browsing.py": "9adce7bbe8791fe4e76b892977d22cc72958c5d8ef3f98c58f50efdc1bb2fd36", + "engraphis/core/context.py": "7defbaf15e10996cfa442cd0fb4d546f544e2f77a2ae7a64ce9193a4c24489a9", + "engraphis/core/engine.py": "59ca726b3dbb5767ef4c9fb1f0142486ae60d9a823990350f507e4056929eace", + "engraphis/core/interfaces.py": "5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22", + "engraphis/core/recall.py": "6a7692a2f404bf32d346d35d10810b763379c1cf59419f3be423b456d2f60191", + "engraphis/core/resolve.py": "ffc17dd39331bf0c6881fa2efa9bcca73f0b7c968135fffaa4dbb025bb32c674", + "engraphis/core/schema.py": "99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686", + "engraphis/core/store.py": "007272ec42011faaae25bdbaf8905cf0d2945bf6f451b60aa7a90fff0550debd", + "engraphis/core/vector_repair.py": "425901615d3b602e3310af1611ebb3c0b78f3d793694c4ca8fffb55f00d07904", + "engraphis/core/vector_search.py": "75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234", + "engraphis/dashboard_assets/engraphis-graph-every.js": "ca84b13d1b6ccb72f347d726b977a8e19f51423e8beff7b072f73788f7cd086f", + "engraphis/dashboard_assets/index.html": "cd1caefe7f24d56deced15c994111eb1f19804424d773e2ba5f1ee4149f05cf4", + "engraphis/dashboard_assets/ledger.js": "b3b97b105012df7eaac73ade2b9740cf30106c5e6b00b229e505906e1f38173a", + "engraphis/dashboard_assets/managed-processing.js": "9508c6b6dacab08304e3c08fd65871758b3043180840e5a8d9d64f6f8b3f000e", + "engraphis/managed_processing.py": "6a04b90ccf62dcd2f70cec1c8994b62e8242638ff0430dc20dca1cb42ac8792b", + "engraphis/routes/v2_api.py": "e01e196ea087d61a7fbad770f7b2c9dcebb968a30b55c82e46040b6af2210a10", + "engraphis/service.py": "572e61628fbd1d8a9e5ce3e21c6feaf15bf915e52027366edeca6b7cee314aba", + "engraphis/static/dashboard.js": "98f7f267dc58936d5a3741be3a80959deafc541323bc47794a76730181f7b0c3", + "engraphis/static/index.html": "c8a2f7c178830be853d359700e59b3d6a19dfdebc62896588a568d7614d0bd77", + "eval/datasets/resolver_write_acceptance.jsonl": "e8db06aa41a93e4892e81d60bb71f06e656088f3eba36ebe37710850a97db6f7", + "eval/fts_insert_scaling.py": "a8ecfbe19695f6d4659c3edf389b007c965b214b64fdd58d5ee86635fa766144", + "eval/native_coverage_scaling.py": "0c7c3f5c80570c7ab02310f14927eb82399d1a624d65df895e937ed87339fe9c", + "eval/resolver_reworded_corrections.py": "f89e0920986efecd736380f0b8021aae553cfb304761f489d49828e6ffec537e", + "eval/vector_scale.py": "3f9c327d9eca1a857512ddc208e972933be1a7d4fa7fa0017aca0cbf8fe7bb6d", + "eval/vector_scale_storage.py": "bda0c1139596eb9232cd75c826fc941101b6116bae32ad7895525d43f9d35484", + "eval/vector_scan_plan.py": "92de2f3b41aa457dcfee11998936ce35e023fe61596ed98f3836c998a6e9a1cd", + "integrations/pi/index.ts": "454ab9d7b095d76cd138404dba45bdbe627c1f8cd6b5284881cd910f64e05a0c", + "integrations/pi/src/generated-contract.ts": "906711cba159d75b8c5440f12796be78cd2f8f4b423971e4009bb3c0d61223c6", + "integrations/pi/src/tool-schemas.ts": "daf238df8a1541a2b61590a3281f65f206fb5fe8d9af2bbb292ec4794453fd2c", + "integrations/pi/test/mcp-client.integration.ts": "45bee1e9e9bb33b3199f1df1ecad93b9f8f95a972ab26b1fc3c3f13b3bdec512", + "integrations/prime_agent/src/engraphis_prime_agent/_contract.py": "4b868447886cc7f6708c1667b5807882c822728db2b0efb4860283ba4fac400b", + "integrations/prime_agent/src/engraphis_prime_agent/tools.py": "b126532c8740f4a93c76abbfa701fb35a50fe1bc6dc45da06b33ecfaeaa7472d", + "integrations/prime_agent/tests/test_tools.py": "36e905860667343ccd2bd97ed5ff05a63fc32e247dbf5b73dfc07796dcc3fec5", + "playwright.config.js": "89aaaece6f36ba55fa35406983f0eb1f2695e4872e6793b44a53604120a06ee9", + "scripts/check_commercial_manifest.py": "2eace66d9e9870f70dd804eb3b62766f34d75add314631bccc3d1ee6b482c1b3", + "scripts/export_mcp_contract.py": "65d177342aa3bef63fb5d9f6c687fd1c3e72dcc85154dcd8108d0d887ac2e9ff", + "scripts/init.py": "5435ad275bf76a564d9ecdd3122a7bc3b84d18e56e0ec828757f4400c20fb735", + "scripts/installation_profile.py": "994da94a696c1eb06b83683735fdafb6235b562465800f1b98f3df63b14d523c", + "scripts/update.py": "a2e68c1ccb0a746efb369d26e3309069bd1e87cde0b087d3cda4783fda52b489", + "tests/e2e/commercial.spec.js": "20603ba52bc7258666125f1422106787f7bc69e20eb80c25bbec1cf94f81bfc9", + "tests/e2e/ledger.spec.js": "c0c89f96b630a71c0f664b7395cfd8bd67de027f415137801eb8a512bff40eff", + "tests/e2e/workspace-smoke.spec.js": "01f63458123e824bcf1e14d6505ecf7c776fe6ffd9e0bdc556b65e34880b22e4", + "tests/test_cloud_features.py": "951f4ae8775a1f99eff89d2fd18d227c16fdcc24f892532118fe6fb83d085ef2", + "tests/test_context_economy.py": "891102b713a04ff34dedfccab4ebc0d51fe6c7617b469d30794cce7970f9e41d", + "tests/test_context_evidence_preservation.py": "508d3a7b07afe71b4eca0e551b66ec291ca6c4ded1cced562f5aedff68614549", + "tests/test_context_packing.py": "e19938d0f9beaba4c3919f10e6eb2d2625cb00f6cf11ef8bf0595423026f8fdf", + "tests/test_documentation_contracts.py": "aee709f8b77e01a1792ce0b16532401b2bb00371a3a1915b12f1c01bdaf2946f", + "tests/test_engine.py": "3afe96d40007ae4c16de9ea89be8cbd318c4aa03fd266264e232da764d83aeaa", + "tests/test_fts_insert_scaling.py": "7f1168828c73f2f07e1c31419a581dd42784752767f74b8fcbc78e10815c8a14", + "tests/test_graph_engine_asset.py": "665cdb9ecf609d8e93402e1c2701d082b88d098aae2a8b361612077b4650190d", + "tests/test_hosted_plan_resolution.py": "f71a3f00fccb1fa9cd2b69a35912a565ba06ffa789059f9943d2e6a749a6ba0e", + "tests/test_init.py": "3fbced7c627bd590a75015e07608ac10138d835d2fd0bf7938de3bc1bf3fb121", + "tests/test_installation_profile.py": "61de0f3235600310ca77b900dcdf5f9b2d8a78bd5425f0fd3c35b680e6250301", + "tests/test_managed_processing_policy.py": "7c65bb7547b29e28ce04b835574bb2cd2bc7b97d2f3a38bccb81014dcfb27d35", + "tests/test_mcp_contract.py": "da34ca5d4303e112fb55168ad112de340b178d388038fd31dd4256da45d47fb1", + "tests/test_memory_browsing.py": "85128315f486d8287db99de857c2b96142f7c46de289c25dfa0ad5f4ee3b743d", + "tests/test_native_coverage_equivalence.py": "b4aea900361794c73526139363bd4435c843a6ab90e14d3912730009c909f351", + "tests/test_obsidian_import_schema.py": "0461e02a9543dac685bc1f8e09f0f7194f7f0a8df15cc5d5f46887c1c1e41381", + "tests/test_resolver_acceptance.py": "2115f5ab530f10e78544da94bf0dce138a23edf9cc64c0b441e971fbda1705af", + "tests/test_storage_concurrency_repair.py": "3040e01d5481e0d28fad43e57f74c01d4244f91bf2127628b322d60e0c098490", + "tests/test_store_fts_insert.py": "195dc8c7118f6b1f1a98e6e38c3f71cbc3f5568601b4022022660f31ed09f6c4", + "tests/test_sync.py": "58f434afae95141610b87ec2656105d8daae4e61eca46a9b57972b3cbe475ee8", + "tests/test_update.py": "b95c0a9cba23997c608b823eafc397027011a9c873bedde7ae95b7e6cf442f8d", + "tests/test_vector_numpy.py": "5fba0cbba8e94a3e2f24925ac8554db7998ef17e3577d1b8ee6842b740ead54a", + "tests/test_vector_scale_storage.py": "efb23b7748d327b0dce9b102ea581f7cf38fd96b233c8be798834bf78e99686b", + "tests/test_vector_scan_plan.py": "9312e44f73fe619ce64b73a5b1d8b637b45cd78fe7f408a174d786d6f34aec39", + "tests/test_vector_snapshot_plan.py": "83c49c621d4570c2a61a35c304665c27db80cf9da1c60ecabc670cfc9e7f9ad0", + "tests/test_vector_sqlitevec_backend.py": "75524f5e6562e389ff031aa2346e567fdc4f8baa1d6c62ea7f59c626d8d51114" + }, + "source_after": { + ".github/workflows/ci.yml": "368fdd94d2e8b45995301b45f5771cce2a127fec9b0774d8e86a736fa8e02317", + "AGENTS.md": "77571f62390e516b445849eef4e9aa9ea7b6292efc7143868e38785f46e1f50a", + "CHANGELOG.md": "4a4b4adce6cdceb56c900a0657bd6036fdd4bd6c4a535d6a35402808df759593", + "README.md": "34975b33dda2e51e167793f78041d918e993589bf688d0d1013e71cf4210d3c3", + "docs/HOSTED_PLANS.md": "676fe44c6bf7bffdbd926eb269090004eff28cd00fc61501018588ab52e1df24", + "docs/HOSTING_RAILWAY.md": "81e530eea41b85f43b3746535db8bd5d16c2c45df39ad2fb1ebf3bf09e3b24bc", + "docs/MCP_CONTRACT.json": "5af24a7eea28a1891c1b93e62bdfb30c4888f8e2e41e1922480b51f4b8990ed1", + "docs/MCP_TOOLS.md": "4ff942fb37484b54b88ec92e941e6dcd2010398ded29bfec4a94174d3eaaff51", + "docs/PAID_EVALUATION_PROPOSAL.md": "dbdc3d91305df86c26365962044ed481d7f1f980afa0c526b3feafa08d007de4", + "docs/RAILWAY_TEMPLATE.md": "3a4cef99ad95b7a2c78e1dffc8fe860230d59c39d3e3267b727c51d792988546", + "docs/SYNC.md": "fe2f5ff8158ca4aa505684d9ee6b077108195b3e8cfd75dd9d5d5b77c40a11e7", + "engraphis/backends/vector_numpy.py": "c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72", + "engraphis/backends/vector_sqlitevec.py": "6148e14ceaacc19239b64c642a3fba0e98797c78cec356210157afadf475a08b", + "engraphis/classic_assets/dashboard.js": "98f7f267dc58936d5a3741be3a80959deafc541323bc47794a76730181f7b0c3", + "engraphis/classic_assets/index.html": "3173ab69f2be4dbdf4310cc23eab6f85cc537e5bcede83fe998b7906b8496460", + "engraphis/cloud_features.py": "a1bec76216d4f1276a3313dabc1d29a1666378e11863d05d1a02e695cf6d7293", + "engraphis/commercial.py": "184f312066a9e682e51a0abeff042f1c0e8eed2d47470157b23930b5a17633aa", + "engraphis/commercial_manifest.json": "27dfb332a5b4f3ceeed22aef37e1f55e1c051ab69cc9eedc4f826ddf4c752162", + "engraphis/core/browsing.py": "9adce7bbe8791fe4e76b892977d22cc72958c5d8ef3f98c58f50efdc1bb2fd36", + "engraphis/core/context.py": "7defbaf15e10996cfa442cd0fb4d546f544e2f77a2ae7a64ce9193a4c24489a9", + "engraphis/core/engine.py": "59ca726b3dbb5767ef4c9fb1f0142486ae60d9a823990350f507e4056929eace", + "engraphis/core/interfaces.py": "5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22", + "engraphis/core/recall.py": "6a7692a2f404bf32d346d35d10810b763379c1cf59419f3be423b456d2f60191", + "engraphis/core/resolve.py": "ffc17dd39331bf0c6881fa2efa9bcca73f0b7c968135fffaa4dbb025bb32c674", + "engraphis/core/schema.py": "99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686", + "engraphis/core/store.py": "007272ec42011faaae25bdbaf8905cf0d2945bf6f451b60aa7a90fff0550debd", + "engraphis/core/vector_repair.py": "425901615d3b602e3310af1611ebb3c0b78f3d793694c4ca8fffb55f00d07904", + "engraphis/core/vector_search.py": "75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234", + "engraphis/dashboard_assets/engraphis-graph-every.js": "ca84b13d1b6ccb72f347d726b977a8e19f51423e8beff7b072f73788f7cd086f", + "engraphis/dashboard_assets/index.html": "cd1caefe7f24d56deced15c994111eb1f19804424d773e2ba5f1ee4149f05cf4", + "engraphis/dashboard_assets/ledger.js": "b3b97b105012df7eaac73ade2b9740cf30106c5e6b00b229e505906e1f38173a", + "engraphis/dashboard_assets/managed-processing.js": "9508c6b6dacab08304e3c08fd65871758b3043180840e5a8d9d64f6f8b3f000e", + "engraphis/managed_processing.py": "6a04b90ccf62dcd2f70cec1c8994b62e8242638ff0430dc20dca1cb42ac8792b", + "engraphis/routes/v2_api.py": "e01e196ea087d61a7fbad770f7b2c9dcebb968a30b55c82e46040b6af2210a10", + "engraphis/service.py": "572e61628fbd1d8a9e5ce3e21c6feaf15bf915e52027366edeca6b7cee314aba", + "engraphis/static/dashboard.js": "98f7f267dc58936d5a3741be3a80959deafc541323bc47794a76730181f7b0c3", + "engraphis/static/index.html": "c8a2f7c178830be853d359700e59b3d6a19dfdebc62896588a568d7614d0bd77", + "eval/datasets/resolver_write_acceptance.jsonl": "e8db06aa41a93e4892e81d60bb71f06e656088f3eba36ebe37710850a97db6f7", + "eval/fts_insert_scaling.py": "a8ecfbe19695f6d4659c3edf389b007c965b214b64fdd58d5ee86635fa766144", + "eval/native_coverage_scaling.py": "0c7c3f5c80570c7ab02310f14927eb82399d1a624d65df895e937ed87339fe9c", + "eval/resolver_reworded_corrections.py": "f89e0920986efecd736380f0b8021aae553cfb304761f489d49828e6ffec537e", + "eval/vector_scale.py": "3f9c327d9eca1a857512ddc208e972933be1a7d4fa7fa0017aca0cbf8fe7bb6d", + "eval/vector_scale_storage.py": "bda0c1139596eb9232cd75c826fc941101b6116bae32ad7895525d43f9d35484", + "eval/vector_scan_plan.py": "92de2f3b41aa457dcfee11998936ce35e023fe61596ed98f3836c998a6e9a1cd", + "integrations/pi/index.ts": "454ab9d7b095d76cd138404dba45bdbe627c1f8cd6b5284881cd910f64e05a0c", + "integrations/pi/src/generated-contract.ts": "906711cba159d75b8c5440f12796be78cd2f8f4b423971e4009bb3c0d61223c6", + "integrations/pi/src/tool-schemas.ts": "daf238df8a1541a2b61590a3281f65f206fb5fe8d9af2bbb292ec4794453fd2c", + "integrations/pi/test/mcp-client.integration.ts": "45bee1e9e9bb33b3199f1df1ecad93b9f8f95a972ab26b1fc3c3f13b3bdec512", + "integrations/prime_agent/src/engraphis_prime_agent/_contract.py": "4b868447886cc7f6708c1667b5807882c822728db2b0efb4860283ba4fac400b", + "integrations/prime_agent/src/engraphis_prime_agent/tools.py": "b126532c8740f4a93c76abbfa701fb35a50fe1bc6dc45da06b33ecfaeaa7472d", + "integrations/prime_agent/tests/test_tools.py": "36e905860667343ccd2bd97ed5ff05a63fc32e247dbf5b73dfc07796dcc3fec5", + "playwright.config.js": "89aaaece6f36ba55fa35406983f0eb1f2695e4872e6793b44a53604120a06ee9", + "scripts/check_commercial_manifest.py": "2eace66d9e9870f70dd804eb3b62766f34d75add314631bccc3d1ee6b482c1b3", + "scripts/export_mcp_contract.py": "65d177342aa3bef63fb5d9f6c687fd1c3e72dcc85154dcd8108d0d887ac2e9ff", + "scripts/init.py": "5435ad275bf76a564d9ecdd3122a7bc3b84d18e56e0ec828757f4400c20fb735", + "scripts/installation_profile.py": "994da94a696c1eb06b83683735fdafb6235b562465800f1b98f3df63b14d523c", + "scripts/update.py": "a2e68c1ccb0a746efb369d26e3309069bd1e87cde0b087d3cda4783fda52b489", + "tests/e2e/commercial.spec.js": "20603ba52bc7258666125f1422106787f7bc69e20eb80c25bbec1cf94f81bfc9", + "tests/e2e/ledger.spec.js": "c0c89f96b630a71c0f664b7395cfd8bd67de027f415137801eb8a512bff40eff", + "tests/e2e/workspace-smoke.spec.js": "01f63458123e824bcf1e14d6505ecf7c776fe6ffd9e0bdc556b65e34880b22e4", + "tests/test_cloud_features.py": "951f4ae8775a1f99eff89d2fd18d227c16fdcc24f892532118fe6fb83d085ef2", + "tests/test_context_economy.py": "891102b713a04ff34dedfccab4ebc0d51fe6c7617b469d30794cce7970f9e41d", + "tests/test_context_evidence_preservation.py": "508d3a7b07afe71b4eca0e551b66ec291ca6c4ded1cced562f5aedff68614549", + "tests/test_context_packing.py": "e19938d0f9beaba4c3919f10e6eb2d2625cb00f6cf11ef8bf0595423026f8fdf", + "tests/test_documentation_contracts.py": "aee709f8b77e01a1792ce0b16532401b2bb00371a3a1915b12f1c01bdaf2946f", + "tests/test_engine.py": "3afe96d40007ae4c16de9ea89be8cbd318c4aa03fd266264e232da764d83aeaa", + "tests/test_fts_insert_scaling.py": "7f1168828c73f2f07e1c31419a581dd42784752767f74b8fcbc78e10815c8a14", + "tests/test_graph_engine_asset.py": "665cdb9ecf609d8e93402e1c2701d082b88d098aae2a8b361612077b4650190d", + "tests/test_hosted_plan_resolution.py": "f71a3f00fccb1fa9cd2b69a35912a565ba06ffa789059f9943d2e6a749a6ba0e", + "tests/test_init.py": "3fbced7c627bd590a75015e07608ac10138d835d2fd0bf7938de3bc1bf3fb121", + "tests/test_installation_profile.py": "61de0f3235600310ca77b900dcdf5f9b2d8a78bd5425f0fd3c35b680e6250301", + "tests/test_managed_processing_policy.py": "7c65bb7547b29e28ce04b835574bb2cd2bc7b97d2f3a38bccb81014dcfb27d35", + "tests/test_mcp_contract.py": "da34ca5d4303e112fb55168ad112de340b178d388038fd31dd4256da45d47fb1", + "tests/test_memory_browsing.py": "85128315f486d8287db99de857c2b96142f7c46de289c25dfa0ad5f4ee3b743d", + "tests/test_native_coverage_equivalence.py": "b4aea900361794c73526139363bd4435c843a6ab90e14d3912730009c909f351", + "tests/test_obsidian_import_schema.py": "0461e02a9543dac685bc1f8e09f0f7194f7f0a8df15cc5d5f46887c1c1e41381", + "tests/test_resolver_acceptance.py": "2115f5ab530f10e78544da94bf0dce138a23edf9cc64c0b441e971fbda1705af", + "tests/test_storage_concurrency_repair.py": "3040e01d5481e0d28fad43e57f74c01d4244f91bf2127628b322d60e0c098490", + "tests/test_store_fts_insert.py": "195dc8c7118f6b1f1a98e6e38c3f71cbc3f5568601b4022022660f31ed09f6c4", + "tests/test_sync.py": "58f434afae95141610b87ec2656105d8daae4e61eca46a9b57972b3cbe475ee8", + "tests/test_update.py": "b95c0a9cba23997c608b823eafc397027011a9c873bedde7ae95b7e6cf442f8d", + "tests/test_vector_numpy.py": "5fba0cbba8e94a3e2f24925ac8554db7998ef17e3577d1b8ee6842b740ead54a", + "tests/test_vector_scale_storage.py": "efb23b7748d327b0dce9b102ea581f7cf38fd96b233c8be798834bf78e99686b", + "tests/test_vector_scan_plan.py": "9312e44f73fe619ce64b73a5b1d8b637b45cd78fe7f408a174d786d6f34aec39", + "tests/test_vector_snapshot_plan.py": "83c49c621d4570c2a61a35c304665c27db80cf9da1c60ecabc670cfc9e7f9ad0", + "tests/test_vector_sqlitevec_backend.py": "75524f5e6562e389ff031aa2346e567fdc4f8baa1d6c62ea7f59c626d8d51114" + }, + "source_stable": true, + "changed_files": [], + "exclusions": [ + "docs/evidence/**", + "docs/RELIABILITY_PROGRAM.md: final report assembled separately" + ] +} diff --git a/docs/evidence/reliability/public-complete-source-final.json b/docs/evidence/reliability/public-complete-source-final.json new file mode 100644 index 00000000..1a424463 --- /dev/null +++ b/docs/evidence/reliability/public-complete-source-final.json @@ -0,0 +1,365 @@ +{ + "schema": "engraphis-validation-source/v1", + "command": [ + "python", + "-m", + "pytest", + "tests/", + "-q", + "-o", + "addopts=", + "--tb=short", + "--junitxml", + "" + ], + "environment": { + "ENGRAPHIS_EXTRACTOR": "none", + "sqlite_vec": "0.1.9 isolated installation" + }, + "elapsed_seconds": 380.7047358000418, + "exit_code": 0, + "source_before": { + ".github/workflows/ci.yml": "368fdd94d2e8b45995301b45f5771cce2a127fec9b0774d8e86a736fa8e02317", + "AGENTS.md": "77571f62390e516b445849eef4e9aa9ea7b6292efc7143868e38785f46e1f50a", + "CHANGELOG.md": "4a4b4adce6cdceb56c900a0657bd6036fdd4bd6c4a535d6a35402808df759593", + "README.md": "34975b33dda2e51e167793f78041d918e993589bf688d0d1013e71cf4210d3c3", + "docs/HOSTED_PLANS.md": "676fe44c6bf7bffdbd926eb269090004eff28cd00fc61501018588ab52e1df24", + "docs/HOSTING_RAILWAY.md": "81e530eea41b85f43b3746535db8bd5d16c2c45df39ad2fb1ebf3bf09e3b24bc", + "docs/MCP_CONTRACT.json": "5af24a7eea28a1891c1b93e62bdfb30c4888f8e2e41e1922480b51f4b8990ed1", + "docs/MCP_TOOLS.md": "4ff942fb37484b54b88ec92e941e6dcd2010398ded29bfec4a94174d3eaaff51", + "docs/PAID_EVALUATION_PROPOSAL.md": "dbdc3d91305df86c26365962044ed481d7f1f980afa0c526b3feafa08d007de4", + "docs/RAILWAY_TEMPLATE.md": "3a4cef99ad95b7a2c78e1dffc8fe860230d59c39d3e3267b727c51d792988546", + "docs/SYNC.md": "fe2f5ff8158ca4aa505684d9ee6b077108195b3e8cfd75dd9d5d5b77c40a11e7", + "engraphis/backends/vector_numpy.py": "c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72", + "engraphis/backends/vector_sqlitevec.py": "6148e14ceaacc19239b64c642a3fba0e98797c78cec356210157afadf475a08b", + "engraphis/classic_assets/dashboard.js": "98f7f267dc58936d5a3741be3a80959deafc541323bc47794a76730181f7b0c3", + "engraphis/classic_assets/index.html": "3173ab69f2be4dbdf4310cc23eab6f85cc537e5bcede83fe998b7906b8496460", + "engraphis/cloud_features.py": "a1bec76216d4f1276a3313dabc1d29a1666378e11863d05d1a02e695cf6d7293", + "engraphis/commercial.py": "184f312066a9e682e51a0abeff042f1c0e8eed2d47470157b23930b5a17633aa", + "engraphis/commercial_manifest.json": "27dfb332a5b4f3ceeed22aef37e1f55e1c051ab69cc9eedc4f826ddf4c752162", + "engraphis/core/browsing.py": "9adce7bbe8791fe4e76b892977d22cc72958c5d8ef3f98c58f50efdc1bb2fd36", + "engraphis/core/context.py": "7defbaf15e10996cfa442cd0fb4d546f544e2f77a2ae7a64ce9193a4c24489a9", + "engraphis/core/engine.py": "59ca726b3dbb5767ef4c9fb1f0142486ae60d9a823990350f507e4056929eace", + "engraphis/core/interfaces.py": "5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22", + "engraphis/core/recall.py": "6a7692a2f404bf32d346d35d10810b763379c1cf59419f3be423b456d2f60191", + "engraphis/core/resolve.py": "ffc17dd39331bf0c6881fa2efa9bcca73f0b7c968135fffaa4dbb025bb32c674", + "engraphis/core/schema.py": "99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686", + "engraphis/core/store.py": "007272ec42011faaae25bdbaf8905cf0d2945bf6f451b60aa7a90fff0550debd", + "engraphis/core/vector_repair.py": "425901615d3b602e3310af1611ebb3c0b78f3d793694c4ca8fffb55f00d07904", + "engraphis/core/vector_search.py": "75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234", + "engraphis/dashboard_assets/engraphis-graph-every.js": "ca84b13d1b6ccb72f347d726b977a8e19f51423e8beff7b072f73788f7cd086f", + "engraphis/dashboard_assets/index.html": "cd1caefe7f24d56deced15c994111eb1f19804424d773e2ba5f1ee4149f05cf4", + "engraphis/dashboard_assets/ledger.js": "b3b97b105012df7eaac73ade2b9740cf30106c5e6b00b229e505906e1f38173a", + "engraphis/dashboard_assets/managed-processing.js": "9508c6b6dacab08304e3c08fd65871758b3043180840e5a8d9d64f6f8b3f000e", + "engraphis/managed_processing.py": "6a04b90ccf62dcd2f70cec1c8994b62e8242638ff0430dc20dca1cb42ac8792b", + "engraphis/routes/v2_api.py": "e01e196ea087d61a7fbad770f7b2c9dcebb968a30b55c82e46040b6af2210a10", + "engraphis/service.py": "572e61628fbd1d8a9e5ce3e21c6feaf15bf915e52027366edeca6b7cee314aba", + "engraphis/static/dashboard.js": "98f7f267dc58936d5a3741be3a80959deafc541323bc47794a76730181f7b0c3", + "engraphis/static/index.html": "c8a2f7c178830be853d359700e59b3d6a19dfdebc62896588a568d7614d0bd77", + "eval/datasets/resolver_write_acceptance.jsonl": "e8db06aa41a93e4892e81d60bb71f06e656088f3eba36ebe37710850a97db6f7", + "eval/fts_insert_scaling.py": "a8ecfbe19695f6d4659c3edf389b007c965b214b64fdd58d5ee86635fa766144", + "eval/native_coverage_scaling.py": "0c7c3f5c80570c7ab02310f14927eb82399d1a624d65df895e937ed87339fe9c", + "eval/resolver_reworded_corrections.py": "f89e0920986efecd736380f0b8021aae553cfb304761f489d49828e6ffec537e", + "eval/vector_scale.py": "3f9c327d9eca1a857512ddc208e972933be1a7d4fa7fa0017aca0cbf8fe7bb6d", + "eval/vector_scale_storage.py": "bda0c1139596eb9232cd75c826fc941101b6116bae32ad7895525d43f9d35484", + "eval/vector_scan_plan.py": "92de2f3b41aa457dcfee11998936ce35e023fe61596ed98f3836c998a6e9a1cd", + "integrations/pi/index.ts": "454ab9d7b095d76cd138404dba45bdbe627c1f8cd6b5284881cd910f64e05a0c", + "integrations/pi/src/generated-contract.ts": "906711cba159d75b8c5440f12796be78cd2f8f4b423971e4009bb3c0d61223c6", + "integrations/pi/src/tool-schemas.ts": "daf238df8a1541a2b61590a3281f65f206fb5fe8d9af2bbb292ec4794453fd2c", + "integrations/pi/test/mcp-client.integration.ts": "45bee1e9e9bb33b3199f1df1ecad93b9f8f95a972ab26b1fc3c3f13b3bdec512", + "integrations/prime_agent/src/engraphis_prime_agent/_contract.py": "4b868447886cc7f6708c1667b5807882c822728db2b0efb4860283ba4fac400b", + "integrations/prime_agent/src/engraphis_prime_agent/tools.py": "b126532c8740f4a93c76abbfa701fb35a50fe1bc6dc45da06b33ecfaeaa7472d", + "integrations/prime_agent/tests/test_tools.py": "36e905860667343ccd2bd97ed5ff05a63fc32e247dbf5b73dfc07796dcc3fec5", + "playwright.config.js": "89aaaece6f36ba55fa35406983f0eb1f2695e4872e6793b44a53604120a06ee9", + "scripts/check_commercial_manifest.py": "2eace66d9e9870f70dd804eb3b62766f34d75add314631bccc3d1ee6b482c1b3", + "scripts/export_mcp_contract.py": "65d177342aa3bef63fb5d9f6c687fd1c3e72dcc85154dcd8108d0d887ac2e9ff", + "scripts/init.py": "5435ad275bf76a564d9ecdd3122a7bc3b84d18e56e0ec828757f4400c20fb735", + "scripts/installation_profile.py": "994da94a696c1eb06b83683735fdafb6235b562465800f1b98f3df63b14d523c", + "scripts/update.py": "a2e68c1ccb0a746efb369d26e3309069bd1e87cde0b087d3cda4783fda52b489", + "tests/e2e/commercial.spec.js": "20603ba52bc7258666125f1422106787f7bc69e20eb80c25bbec1cf94f81bfc9", + "tests/e2e/ledger.spec.js": "c0c89f96b630a71c0f664b7395cfd8bd67de027f415137801eb8a512bff40eff", + "tests/e2e/workspace-smoke.spec.js": "01f63458123e824bcf1e14d6505ecf7c776fe6ffd9e0bdc556b65e34880b22e4", + "tests/test_cloud_features.py": "951f4ae8775a1f99eff89d2fd18d227c16fdcc24f892532118fe6fb83d085ef2", + "tests/test_context_economy.py": "891102b713a04ff34dedfccab4ebc0d51fe6c7617b469d30794cce7970f9e41d", + "tests/test_context_evidence_preservation.py": "508d3a7b07afe71b4eca0e551b66ec291ca6c4ded1cced562f5aedff68614549", + "tests/test_context_packing.py": "e19938d0f9beaba4c3919f10e6eb2d2625cb00f6cf11ef8bf0595423026f8fdf", + "tests/test_dashboard_auth_placement.py": "e837b9a1c3ec4938307574e2ce1f209d75c06545777430cd4e320bdc4be1da62", + "tests/test_documentation_contracts.py": "aee709f8b77e01a1792ce0b16532401b2bb00371a3a1915b12f1c01bdaf2946f", + "tests/test_engine.py": "3afe96d40007ae4c16de9ea89be8cbd318c4aa03fd266264e232da764d83aeaa", + "tests/test_fts_insert_scaling.py": "7f1168828c73f2f07e1c31419a581dd42784752767f74b8fcbc78e10815c8a14", + "tests/test_graph_engine_asset.py": "665cdb9ecf609d8e93402e1c2701d082b88d098aae2a8b361612077b4650190d", + "tests/test_hosted_plan_resolution.py": "f71a3f00fccb1fa9cd2b69a35912a565ba06ffa789059f9943d2e6a749a6ba0e", + "tests/test_init.py": "3fbced7c627bd590a75015e07608ac10138d835d2fd0bf7938de3bc1bf3fb121", + "tests/test_installation_profile.py": "61de0f3235600310ca77b900dcdf5f9b2d8a78bd5425f0fd3c35b680e6250301", + "tests/test_managed_processing_policy.py": "7c65bb7547b29e28ce04b835574bb2cd2bc7b97d2f3a38bccb81014dcfb27d35", + "tests/test_mcp_contract.py": "da34ca5d4303e112fb55168ad112de340b178d388038fd31dd4256da45d47fb1", + "tests/test_memory_browsing.py": "85128315f486d8287db99de857c2b96142f7c46de289c25dfa0ad5f4ee3b743d", + "tests/test_native_coverage_equivalence.py": "b4aea900361794c73526139363bd4435c843a6ab90e14d3912730009c909f351", + "tests/test_obsidian_import_schema.py": "0461e02a9543dac685bc1f8e09f0f7194f7f0a8df15cc5d5f46887c1c1e41381", + "tests/test_pro_cta.py": "ec692d6f2bfcea3726a3eaa819b4495c2312faa9746f8bd0e6b6e90e887378f2", + "tests/test_resolver_acceptance.py": "2115f5ab530f10e78544da94bf0dce138a23edf9cc64c0b441e971fbda1705af", + "tests/test_storage_concurrency_repair.py": "3040e01d5481e0d28fad43e57f74c01d4244f91bf2127628b322d60e0c098490", + "tests/test_store_fts_insert.py": "195dc8c7118f6b1f1a98e6e38c3f71cbc3f5568601b4022022660f31ed09f6c4", + "tests/test_sync.py": "58f434afae95141610b87ec2656105d8daae4e61eca46a9b57972b3cbe475ee8", + "tests/test_update.py": "b95c0a9cba23997c608b823eafc397027011a9c873bedde7ae95b7e6cf442f8d", + "tests/test_vector_numpy.py": "5fba0cbba8e94a3e2f24925ac8554db7998ef17e3577d1b8ee6842b740ead54a", + "tests/test_vector_scale_storage.py": "efb23b7748d327b0dce9b102ea581f7cf38fd96b233c8be798834bf78e99686b", + "tests/test_vector_scan_plan.py": "9312e44f73fe619ce64b73a5b1d8b637b45cd78fe7f408a174d786d6f34aec39", + "tests/test_vector_snapshot_plan.py": "83c49c621d4570c2a61a35c304665c27db80cf9da1c60ecabc670cfc9e7f9ad0", + "tests/test_vector_sqlitevec_backend.py": "75524f5e6562e389ff031aa2346e567fdc4f8baa1d6c62ea7f59c626d8d51114" + }, + "source_after": { + ".github/workflows/ci.yml": "368fdd94d2e8b45995301b45f5771cce2a127fec9b0774d8e86a736fa8e02317", + "AGENTS.md": "77571f62390e516b445849eef4e9aa9ea7b6292efc7143868e38785f46e1f50a", + "CHANGELOG.md": "4a4b4adce6cdceb56c900a0657bd6036fdd4bd6c4a535d6a35402808df759593", + "README.md": "34975b33dda2e51e167793f78041d918e993589bf688d0d1013e71cf4210d3c3", + "docs/HOSTED_PLANS.md": "676fe44c6bf7bffdbd926eb269090004eff28cd00fc61501018588ab52e1df24", + "docs/HOSTING_RAILWAY.md": "81e530eea41b85f43b3746535db8bd5d16c2c45df39ad2fb1ebf3bf09e3b24bc", + "docs/MCP_CONTRACT.json": "5af24a7eea28a1891c1b93e62bdfb30c4888f8e2e41e1922480b51f4b8990ed1", + "docs/MCP_TOOLS.md": "4ff942fb37484b54b88ec92e941e6dcd2010398ded29bfec4a94174d3eaaff51", + "docs/PAID_EVALUATION_PROPOSAL.md": "dbdc3d91305df86c26365962044ed481d7f1f980afa0c526b3feafa08d007de4", + "docs/RAILWAY_TEMPLATE.md": "3a4cef99ad95b7a2c78e1dffc8fe860230d59c39d3e3267b727c51d792988546", + "docs/SYNC.md": "fe2f5ff8158ca4aa505684d9ee6b077108195b3e8cfd75dd9d5d5b77c40a11e7", + "engraphis/backends/vector_numpy.py": "c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72", + "engraphis/backends/vector_sqlitevec.py": "6148e14ceaacc19239b64c642a3fba0e98797c78cec356210157afadf475a08b", + "engraphis/classic_assets/dashboard.js": "98f7f267dc58936d5a3741be3a80959deafc541323bc47794a76730181f7b0c3", + "engraphis/classic_assets/index.html": "3173ab69f2be4dbdf4310cc23eab6f85cc537e5bcede83fe998b7906b8496460", + "engraphis/cloud_features.py": "a1bec76216d4f1276a3313dabc1d29a1666378e11863d05d1a02e695cf6d7293", + "engraphis/commercial.py": "184f312066a9e682e51a0abeff042f1c0e8eed2d47470157b23930b5a17633aa", + "engraphis/commercial_manifest.json": "27dfb332a5b4f3ceeed22aef37e1f55e1c051ab69cc9eedc4f826ddf4c752162", + "engraphis/core/browsing.py": "9adce7bbe8791fe4e76b892977d22cc72958c5d8ef3f98c58f50efdc1bb2fd36", + "engraphis/core/context.py": "7defbaf15e10996cfa442cd0fb4d546f544e2f77a2ae7a64ce9193a4c24489a9", + "engraphis/core/engine.py": "59ca726b3dbb5767ef4c9fb1f0142486ae60d9a823990350f507e4056929eace", + "engraphis/core/interfaces.py": "5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22", + "engraphis/core/recall.py": "6a7692a2f404bf32d346d35d10810b763379c1cf59419f3be423b456d2f60191", + "engraphis/core/resolve.py": "ffc17dd39331bf0c6881fa2efa9bcca73f0b7c968135fffaa4dbb025bb32c674", + "engraphis/core/schema.py": "99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686", + "engraphis/core/store.py": "007272ec42011faaae25bdbaf8905cf0d2945bf6f451b60aa7a90fff0550debd", + "engraphis/core/vector_repair.py": "425901615d3b602e3310af1611ebb3c0b78f3d793694c4ca8fffb55f00d07904", + "engraphis/core/vector_search.py": "75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234", + "engraphis/dashboard_assets/engraphis-graph-every.js": "ca84b13d1b6ccb72f347d726b977a8e19f51423e8beff7b072f73788f7cd086f", + "engraphis/dashboard_assets/index.html": "cd1caefe7f24d56deced15c994111eb1f19804424d773e2ba5f1ee4149f05cf4", + "engraphis/dashboard_assets/ledger.js": "b3b97b105012df7eaac73ade2b9740cf30106c5e6b00b229e505906e1f38173a", + "engraphis/dashboard_assets/managed-processing.js": "9508c6b6dacab08304e3c08fd65871758b3043180840e5a8d9d64f6f8b3f000e", + "engraphis/managed_processing.py": "6a04b90ccf62dcd2f70cec1c8994b62e8242638ff0430dc20dca1cb42ac8792b", + "engraphis/routes/v2_api.py": "e01e196ea087d61a7fbad770f7b2c9dcebb968a30b55c82e46040b6af2210a10", + "engraphis/service.py": "572e61628fbd1d8a9e5ce3e21c6feaf15bf915e52027366edeca6b7cee314aba", + "engraphis/static/dashboard.js": "98f7f267dc58936d5a3741be3a80959deafc541323bc47794a76730181f7b0c3", + "engraphis/static/index.html": "c8a2f7c178830be853d359700e59b3d6a19dfdebc62896588a568d7614d0bd77", + "eval/datasets/resolver_write_acceptance.jsonl": "e8db06aa41a93e4892e81d60bb71f06e656088f3eba36ebe37710850a97db6f7", + "eval/fts_insert_scaling.py": "a8ecfbe19695f6d4659c3edf389b007c965b214b64fdd58d5ee86635fa766144", + "eval/native_coverage_scaling.py": "0c7c3f5c80570c7ab02310f14927eb82399d1a624d65df895e937ed87339fe9c", + "eval/resolver_reworded_corrections.py": "f89e0920986efecd736380f0b8021aae553cfb304761f489d49828e6ffec537e", + "eval/vector_scale.py": "3f9c327d9eca1a857512ddc208e972933be1a7d4fa7fa0017aca0cbf8fe7bb6d", + "eval/vector_scale_storage.py": "bda0c1139596eb9232cd75c826fc941101b6116bae32ad7895525d43f9d35484", + "eval/vector_scan_plan.py": "92de2f3b41aa457dcfee11998936ce35e023fe61596ed98f3836c998a6e9a1cd", + "integrations/pi/index.ts": "454ab9d7b095d76cd138404dba45bdbe627c1f8cd6b5284881cd910f64e05a0c", + "integrations/pi/src/generated-contract.ts": "906711cba159d75b8c5440f12796be78cd2f8f4b423971e4009bb3c0d61223c6", + "integrations/pi/src/tool-schemas.ts": "daf238df8a1541a2b61590a3281f65f206fb5fe8d9af2bbb292ec4794453fd2c", + "integrations/pi/test/mcp-client.integration.ts": "45bee1e9e9bb33b3199f1df1ecad93b9f8f95a972ab26b1fc3c3f13b3bdec512", + "integrations/prime_agent/src/engraphis_prime_agent/_contract.py": "4b868447886cc7f6708c1667b5807882c822728db2b0efb4860283ba4fac400b", + "integrations/prime_agent/src/engraphis_prime_agent/tools.py": "b126532c8740f4a93c76abbfa701fb35a50fe1bc6dc45da06b33ecfaeaa7472d", + "integrations/prime_agent/tests/test_tools.py": "36e905860667343ccd2bd97ed5ff05a63fc32e247dbf5b73dfc07796dcc3fec5", + "playwright.config.js": "89aaaece6f36ba55fa35406983f0eb1f2695e4872e6793b44a53604120a06ee9", + "scripts/check_commercial_manifest.py": "2eace66d9e9870f70dd804eb3b62766f34d75add314631bccc3d1ee6b482c1b3", + "scripts/export_mcp_contract.py": "65d177342aa3bef63fb5d9f6c687fd1c3e72dcc85154dcd8108d0d887ac2e9ff", + "scripts/init.py": "5435ad275bf76a564d9ecdd3122a7bc3b84d18e56e0ec828757f4400c20fb735", + "scripts/installation_profile.py": "994da94a696c1eb06b83683735fdafb6235b562465800f1b98f3df63b14d523c", + "scripts/update.py": "a2e68c1ccb0a746efb369d26e3309069bd1e87cde0b087d3cda4783fda52b489", + "tests/e2e/commercial.spec.js": "20603ba52bc7258666125f1422106787f7bc69e20eb80c25bbec1cf94f81bfc9", + "tests/e2e/ledger.spec.js": "c0c89f96b630a71c0f664b7395cfd8bd67de027f415137801eb8a512bff40eff", + "tests/e2e/workspace-smoke.spec.js": "01f63458123e824bcf1e14d6505ecf7c776fe6ffd9e0bdc556b65e34880b22e4", + "tests/test_cloud_features.py": "951f4ae8775a1f99eff89d2fd18d227c16fdcc24f892532118fe6fb83d085ef2", + "tests/test_context_economy.py": "891102b713a04ff34dedfccab4ebc0d51fe6c7617b469d30794cce7970f9e41d", + "tests/test_context_evidence_preservation.py": "508d3a7b07afe71b4eca0e551b66ec291ca6c4ded1cced562f5aedff68614549", + "tests/test_context_packing.py": "e19938d0f9beaba4c3919f10e6eb2d2625cb00f6cf11ef8bf0595423026f8fdf", + "tests/test_dashboard_auth_placement.py": "e837b9a1c3ec4938307574e2ce1f209d75c06545777430cd4e320bdc4be1da62", + "tests/test_documentation_contracts.py": "aee709f8b77e01a1792ce0b16532401b2bb00371a3a1915b12f1c01bdaf2946f", + "tests/test_engine.py": "3afe96d40007ae4c16de9ea89be8cbd318c4aa03fd266264e232da764d83aeaa", + "tests/test_fts_insert_scaling.py": "7f1168828c73f2f07e1c31419a581dd42784752767f74b8fcbc78e10815c8a14", + "tests/test_graph_engine_asset.py": "665cdb9ecf609d8e93402e1c2701d082b88d098aae2a8b361612077b4650190d", + "tests/test_hosted_plan_resolution.py": "f71a3f00fccb1fa9cd2b69a35912a565ba06ffa789059f9943d2e6a749a6ba0e", + "tests/test_init.py": "3fbced7c627bd590a75015e07608ac10138d835d2fd0bf7938de3bc1bf3fb121", + "tests/test_installation_profile.py": "61de0f3235600310ca77b900dcdf5f9b2d8a78bd5425f0fd3c35b680e6250301", + "tests/test_managed_processing_policy.py": "7c65bb7547b29e28ce04b835574bb2cd2bc7b97d2f3a38bccb81014dcfb27d35", + "tests/test_mcp_contract.py": "da34ca5d4303e112fb55168ad112de340b178d388038fd31dd4256da45d47fb1", + "tests/test_memory_browsing.py": "85128315f486d8287db99de857c2b96142f7c46de289c25dfa0ad5f4ee3b743d", + "tests/test_native_coverage_equivalence.py": "b4aea900361794c73526139363bd4435c843a6ab90e14d3912730009c909f351", + "tests/test_obsidian_import_schema.py": "0461e02a9543dac685bc1f8e09f0f7194f7f0a8df15cc5d5f46887c1c1e41381", + "tests/test_pro_cta.py": "ec692d6f2bfcea3726a3eaa819b4495c2312faa9746f8bd0e6b6e90e887378f2", + "tests/test_resolver_acceptance.py": "2115f5ab530f10e78544da94bf0dce138a23edf9cc64c0b441e971fbda1705af", + "tests/test_storage_concurrency_repair.py": "3040e01d5481e0d28fad43e57f74c01d4244f91bf2127628b322d60e0c098490", + "tests/test_store_fts_insert.py": "195dc8c7118f6b1f1a98e6e38c3f71cbc3f5568601b4022022660f31ed09f6c4", + "tests/test_sync.py": "58f434afae95141610b87ec2656105d8daae4e61eca46a9b57972b3cbe475ee8", + "tests/test_update.py": "b95c0a9cba23997c608b823eafc397027011a9c873bedde7ae95b7e6cf442f8d", + "tests/test_vector_numpy.py": "5fba0cbba8e94a3e2f24925ac8554db7998ef17e3577d1b8ee6842b740ead54a", + "tests/test_vector_scale_storage.py": "efb23b7748d327b0dce9b102ea581f7cf38fd96b233c8be798834bf78e99686b", + "tests/test_vector_scan_plan.py": "9312e44f73fe619ce64b73a5b1d8b637b45cd78fe7f408a174d786d6f34aec39", + "tests/test_vector_snapshot_plan.py": "83c49c621d4570c2a61a35c304665c27db80cf9da1c60ecabc670cfc9e7f9ad0", + "tests/test_vector_sqlitevec_backend.py": "75524f5e6562e389ff031aa2346e567fdc4f8baa1d6c62ea7f59c626d8d51114" + }, + "source_stable": true, + "changed_files": [], + "exclusions": [ + "docs/evidence/**", + "docs/RELIABILITY_PROGRAM.md: final report assembled separately" + ], + "tests": { + "tests": 4773, + "errors": 0, + "failures": 0, + "skipped": 37, + "passed": 4736, + "reported_seconds": 372.119 + }, + "skipped_cases": [ + { + "test": "tests.test_code_index_route_security::test_code_index_rejects_symlink_escape", + "reason": "directory symlinks are unavailable in this environment" + }, + { + "test": "tests.test_codegraph::test_symlinked_file_is_not_followed_out_of_root", + "reason": "symlinks not supported on this platform" + }, + { + "test": "tests.test_commercial_hardening::test_the_credential_state_directory_is_owner_only", + "reason": "POSIX permission semantics" + }, + { + "test": "tests.test_config::test_explicit_env_file_must_be_owner_private_on_posix", + "reason": "POSIX permission bits are not authoritative on Windows" + }, + { + "test": "tests.test_engine::test_index_repo_rejects_root_symlink_that_resolves_outside_approved_root", + "reason": "symlinks not supported in this environment" + }, + { + "test": "tests.test_engine::test_index_repo_never_reads_a_symlink_that_escapes_root", + "reason": "symlinks not supported in this environment" + }, + { + "test": "tests.test_hosted_ledger::test_private_ledger_enforces_owner_only_directory_and_file_modes", + "reason": "POSIX permission contract" + }, + { + "test": "tests.test_hosted_ledger::test_private_ledger_rejects_symlink_leaves_without_touching_target[ledger]", + "reason": "symlink creation unavailable" + }, + { + "test": "tests.test_hosted_ledger::test_private_ledger_rejects_symlink_leaves_without_touching_target[lock]", + "reason": "symlink creation unavailable" + }, + { + "test": "tests.test_hosted_ledger::test_private_ledger_rejects_public_existing_leaves[ledger]", + "reason": "POSIX permission contract" + }, + { + "test": "tests.test_hosted_ledger::test_private_ledger_rejects_public_existing_leaves[lock]", + "reason": "POSIX permission contract" + }, + { + "test": "tests.test_hosted_luna::test_worker_timeout_terminates_sdk_descendants", + "reason": "POSIX process groups are required" + }, + { + "test": "tests.test_init::test_init_rejects_an_insecure_existing_trusted_env", + "reason": "POSIX permission bits do not apply on Windows" + }, + { + "test": "tests.test_init::test_generated_trusted_env_and_parent_are_private", + "reason": "POSIX permission bits do not apply on Windows" + }, + { + "test": "tests.test_init::test_generated_encryption_key_is_private", + "reason": "POSIX permission bits do not apply on Windows" + }, + { + "test": "tests.test_llm_config::test_persist_project_env_preserves_the_existing_project_directory_mode", + "reason": "POSIX permission semantics" + }, + { + "test": "tests.test_memory_routes_fixes::test_legacy_folder_import_skips_symlink_escape", + "reason": "symlinks not supported in this environment" + }, + { + "test": "tests.test_obsidian_parser::test_vault_scan_skips_hidden_config_symlinks_and_rejects_secrets", + "reason": "symlinks unavailable on this platform" + }, + { + "test": "tests.test_obsidian_parser::test_vault_root_symlink_is_rejected", + "reason": "directory symlinks unavailable on this platform" + }, + { + "test": "tests.test_release_evidence::test_release_environment_command_emits_a_cyclonedx_sbom", + "reason": "release-only CycloneDX tool" + }, + { + "test": "tests.test_service::test_import_folder_symlink_escape_blocked", + "reason": "symlinks not supported in this environment" + }, + { + "test": "tests.test_store_fts_insert::test_update_cleans_existing_duplicate_mirrors[fallback]", + "reason": "plain-table primary key already prevents duplicates" + }, + { + "test": "tests.test_store_v4_migration::test_v4_backup_is_owner_only_even_under_permissive_umask", + "reason": "POSIX permission-bit contract" + }, + { + "test": "tests.test_sync::test_folder_transport_safe_named_symlink_marks_pull_incomplete", + "reason": "symlinks unavailable (e.g. unprivileged Windows)" + }, + { + "test": "tests.test_sync::test_folder_transport_push_never_writes_through_planted_symlinks", + "reason": "symlinks unavailable (e.g. unprivileged Windows)" + }, + { + "test": "tests.test_update_check::test_default_cache_directory_is_owner_private", + "reason": "POSIX permission bits are not authoritative on Windows" + }, + { + "test": "tests.test_encrypted_store::test_encrypts_at_rest_unreadable_without_key", + "reason": "encryption extra not installed" + }, + { + "test": "tests.test_encrypted_store::test_recall_and_reopen_work_encrypted", + "reason": "encryption extra not installed" + }, + { + "test": "tests.test_encrypted_store::test_wrong_key_is_rejected", + "reason": "encryption extra not installed" + }, + { + "test": "tests.test_encrypted_store::test_existing_encrypted_database_opens_read_only_without_sidecar_mutation", + "reason": "encryption extra not installed" + }, + { + "test": "tests.test_encrypted_store::test_encrypted_read_only_open_rejects_wrong_key", + "reason": "encryption extra not installed" + }, + { + "test": "tests.test_encrypted_store::test_encrypted_read_only_open_rejects_active_wal_before_connector", + "reason": "encryption extra not installed" + }, + { + "test": "tests.test_encrypted_store::test_key_from_file", + "reason": "encryption extra not installed" + }, + { + "test": "tests.test_encrypted_store::test_encrypted_manifest_snapshot_is_immutable_and_read_only", + "reason": "encryption extra not installed" + }, + { + "test": "tests.test_encrypted_store::test_passphrase_key_non_hex", + "reason": "encryption extra not installed" + }, + { + "test": "tests.test_encrypted_store::test_no_key_is_plaintext_and_backward_compatible", + "reason": "encryption extra not installed" + }, + { + "test": "tests.test_encrypted_store::test_key_pragma_escapes_quotes", + "reason": "encryption extra not installed" + } + ] +} diff --git a/docs/evidence/reliability/public-pr-source-before-contract-correction.json b/docs/evidence/reliability/public-pr-source-before-contract-correction.json new file mode 100644 index 00000000..5e8890cc --- /dev/null +++ b/docs/evidence/reliability/public-pr-source-before-contract-correction.json @@ -0,0 +1,381 @@ +{ + "schema": "engraphis-validation-source/v1", + "command": [ + "python", + "-m", + "pytest", + "tests/", + "-q", + "-o", + "addopts=", + "--tb=short", + "--junitxml", + "" + ], + "environment": { + "ENGRAPHIS_EXTRACTOR": "none", + "sqlite_vec": "0.1.9 isolated installation" + }, + "elapsed_seconds": 400.2645857000025, + "exit_code": 1, + "source_before": { + ".claude-plugin/skill-assets.sha256": "ec68564272a1fa5dc1e7bef660d777faf13881fd1d7662bdc5cd2d5c6eaec0b8", + ".github/workflows/ci.yml": "368fdd94d2e8b45995301b45f5771cce2a127fec9b0774d8e86a736fa8e02317", + "AGENTS.md": "77571f62390e516b445849eef4e9aa9ea7b6292efc7143868e38785f46e1f50a", + "CHANGELOG.md": "de0bfc1c527d0c163a177e449e94f3b6fcb935ba4b8a07bc793c88a598230561", + "README.md": "34975b33dda2e51e167793f78041d918e993589bf688d0d1013e71cf4210d3c3", + "docs/HOSTED_PLANS.md": "676fe44c6bf7bffdbd926eb269090004eff28cd00fc61501018588ab52e1df24", + "docs/HOSTING_RAILWAY.md": "81e530eea41b85f43b3746535db8bd5d16c2c45df39ad2fb1ebf3bf09e3b24bc", + "docs/MCP_CONTRACT.json": "b6db612c7660ddab19a4ad0786a7c94c9a660ab3c53aecc0130813a5ccba0259", + "docs/MCP_TOOLS.md": "604132977e2ade2e2b4dbb09c42197ee044b184724f745f8e1d3f0ed70c0f6fb", + "docs/PAID_EVALUATION_PROPOSAL.md": "dbdc3d91305df86c26365962044ed481d7f1f980afa0c526b3feafa08d007de4", + "docs/RAILWAY_TEMPLATE.md": "3a4cef99ad95b7a2c78e1dffc8fe860230d59c39d3e3267b727c51d792988546", + "docs/SYNC.md": "fe2f5ff8158ca4aa505684d9ee6b077108195b3e8cfd75dd9d5d5b77c40a11e7", + "engraphis/backends/vector_numpy.py": "c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72", + "engraphis/backends/vector_sqlitevec.py": "6148e14ceaacc19239b64c642a3fba0e98797c78cec356210157afadf475a08b", + "engraphis/classic_assets/dashboard.js": "c912bfef9fdf7a2113d781200a0c1ce9cae92bb01db53a4c4f07fbcbe82dd861", + "engraphis/classic_assets/index.html": "bd7f37e88cf9bf21be2490906e2f620b251d1d3db787e41793d4c8a898392089", + "engraphis/cloud_features.py": "a1bec76216d4f1276a3313dabc1d29a1666378e11863d05d1a02e695cf6d7293", + "engraphis/commercial.py": "184f312066a9e682e51a0abeff042f1c0e8eed2d47470157b23930b5a17633aa", + "engraphis/commercial_manifest.json": "27dfb332a5b4f3ceeed22aef37e1f55e1c051ab69cc9eedc4f826ddf4c752162", + "engraphis/core/browsing.py": "9adce7bbe8791fe4e76b892977d22cc72958c5d8ef3f98c58f50efdc1bb2fd36", + "engraphis/core/context.py": "34320c44f96be4bf4e7260264621902c598b8c49a6df27a1c28f3ebed55201ac", + "engraphis/core/engine.py": "942de6672902b9b19d8d86fe45984533ced0d1fcd3b857d8bf3eec8bc60a792d", + "engraphis/core/interfaces.py": "5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22", + "engraphis/core/recall.py": "fb77fb97d5c9e6e6d14d9d3d5b074c894cbd7c2472b7a25fa84b280343b13625", + "engraphis/core/resolve.py": "ffc17dd39331bf0c6881fa2efa9bcca73f0b7c968135fffaa4dbb025bb32c674", + "engraphis/core/schema.py": "99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686", + "engraphis/core/store.py": "007272ec42011faaae25bdbaf8905cf0d2945bf6f451b60aa7a90fff0550debd", + "engraphis/core/vector_repair.py": "425901615d3b602e3310af1611ebb3c0b78f3d793694c4ca8fffb55f00d07904", + "engraphis/core/vector_search.py": "75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234", + "engraphis/dashboard_assets/engraphis-graph-every.js": "ca84b13d1b6ccb72f347d726b977a8e19f51423e8beff7b072f73788f7cd086f", + "engraphis/dashboard_assets/index.html": "86b8f1935ece771523506d3410dd08294edb8183c2abaecd091418f37f17f00b", + "engraphis/dashboard_assets/ledger.js": "9a7abb256636302ec5970fbfcfc96ea8e961e49b26d3a01389c2b51d0a0b8c4f", + "engraphis/dashboard_assets/managed-processing.js": "9508c6b6dacab08304e3c08fd65871758b3043180840e5a8d9d64f6f8b3f000e", + "engraphis/managed_processing.py": "6a04b90ccf62dcd2f70cec1c8994b62e8242638ff0430dc20dca1cb42ac8792b", + "engraphis/mcp_server.py": "ad49631bcc7ef9dede5d2b8b1182f27eb8d01fd103be4dfeb13014bbbd84081d", + "engraphis/routes/v2_api.py": "e01e196ea087d61a7fbad770f7b2c9dcebb968a30b55c82e46040b6af2210a10", + "engraphis/service.py": "572e61628fbd1d8a9e5ce3e21c6feaf15bf915e52027366edeca6b7cee314aba", + "engraphis/static/dashboard.js": "c912bfef9fdf7a2113d781200a0c1ce9cae92bb01db53a4c4f07fbcbe82dd861", + "engraphis/static/index.html": "4f79a3266ddab53a4ef407fc31393295a0d5991a8f72431b2b958be114ac8f7e", + "eval/datasets/resolver_write_acceptance.jsonl": "e8db06aa41a93e4892e81d60bb71f06e656088f3eba36ebe37710850a97db6f7", + "eval/fts_insert_scaling.py": "a8ecfbe19695f6d4659c3edf389b007c965b214b64fdd58d5ee86635fa766144", + "eval/native_coverage_scaling.py": "0c7c3f5c80570c7ab02310f14927eb82399d1a624d65df895e937ed87339fe9c", + "eval/resolver_reworded_corrections.py": "f89e0920986efecd736380f0b8021aae553cfb304761f489d49828e6ffec537e", + "eval/vector_scale.py": "3f9c327d9eca1a857512ddc208e972933be1a7d4fa7fa0017aca0cbf8fe7bb6d", + "eval/vector_scale_storage.py": "bda0c1139596eb9232cd75c826fc941101b6116bae32ad7895525d43f9d35484", + "eval/vector_scan_plan.py": "92de2f3b41aa457dcfee11998936ce35e023fe61596ed98f3836c998a6e9a1cd", + "integrations/pi/index.ts": "454ab9d7b095d76cd138404dba45bdbe627c1f8cd6b5284881cd910f64e05a0c", + "integrations/pi/src/generated-contract.ts": "906711cba159d75b8c5440f12796be78cd2f8f4b423971e4009bb3c0d61223c6", + "integrations/pi/src/tool-schemas.ts": "daf238df8a1541a2b61590a3281f65f206fb5fe8d9af2bbb292ec4794453fd2c", + "integrations/pi/test/mcp-client.integration.ts": "45bee1e9e9bb33b3199f1df1ecad93b9f8f95a972ab26b1fc3c3f13b3bdec512", + "integrations/prime_agent/src/engraphis_prime_agent/_contract.py": "4b868447886cc7f6708c1667b5807882c822728db2b0efb4860283ba4fac400b", + "integrations/prime_agent/src/engraphis_prime_agent/tools.py": "b126532c8740f4a93c76abbfa701fb35a50fe1bc6dc45da06b33ecfaeaa7472d", + "integrations/prime_agent/tests/test_tools.py": "36e905860667343ccd2bd97ed5ff05a63fc32e247dbf5b73dfc07796dcc3fec5", + "playwright.config.js": "89aaaece6f36ba55fa35406983f0eb1f2695e4872e6793b44a53604120a06ee9", + "scripts/check_commercial_manifest.py": "2eace66d9e9870f70dd804eb3b62766f34d75add314631bccc3d1ee6b482c1b3", + "scripts/export_mcp_contract.py": "65d177342aa3bef63fb5d9f6c687fd1c3e72dcc85154dcd8108d0d887ac2e9ff", + "scripts/init.py": "5435ad275bf76a564d9ecdd3122a7bc3b84d18e56e0ec828757f4400c20fb735", + "scripts/installation_profile.py": "994da94a696c1eb06b83683735fdafb6235b562465800f1b98f3df63b14d523c", + "scripts/update.py": "a2e68c1ccb0a746efb369d26e3309069bd1e87cde0b087d3cda4783fda52b489", + "skills/engraphis-memory/references/TOOLS.md": "33874c7c7a1c0911b0e73c7d22addc9828963d5436cb315fe7c6c5587c6b911d", + "tests/e2e/commercial.spec.js": "c9e080d5807ab7484e1738cd731dba98871afe6dc8af6ad720dfd1cd34c9d6e0", + "tests/e2e/ledger.spec.js": "e895543ebd03fd80ce19f2678e0a0231f4ae3401dff475f6d781613dfdf4d213", + "tests/e2e/workspace-smoke.spec.js": "01f63458123e824bcf1e14d6505ecf7c776fe6ffd9e0bdc556b65e34880b22e4", + "tests/test_cloud_features.py": "951f4ae8775a1f99eff89d2fd18d227c16fdcc24f892532118fe6fb83d085ef2", + "tests/test_context_economy.py": "891102b713a04ff34dedfccab4ebc0d51fe6c7617b469d30794cce7970f9e41d", + "tests/test_context_evidence_preservation.py": "6121a71511c25a302f6fed5912d9e70d1f783765b88701edf0994b8d6c714d0c", + "tests/test_context_packer.py": "bb2cbf4615cbd5aca678da60eac79b3a6c6eb16265830ea645627a024601304e", + "tests/test_context_packing.py": "e19938d0f9beaba4c3919f10e6eb2d2625cb00f6cf11ef8bf0595423026f8fdf", + "tests/test_dashboard_auth_placement.py": "aba4826b85aae1fbb367bfee7ffbd2133c947eefbdb5a0512a9f9821fd22d1cb", + "tests/test_documentation_contracts.py": "aee709f8b77e01a1792ce0b16532401b2bb00371a3a1915b12f1c01bdaf2946f", + "tests/test_engine.py": "3afe96d40007ae4c16de9ea89be8cbd318c4aa03fd266264e232da764d83aeaa", + "tests/test_fts_insert_scaling.py": "7f1168828c73f2f07e1c31419a581dd42784752767f74b8fcbc78e10815c8a14", + "tests/test_graph_engine_asset.py": "665cdb9ecf609d8e93402e1c2701d082b88d098aae2a8b361612077b4650190d", + "tests/test_hosted_plan_resolution.py": "f71a3f00fccb1fa9cd2b69a35912a565ba06ffa789059f9943d2e6a749a6ba0e", + "tests/test_init.py": "3fbced7c627bd590a75015e07608ac10138d835d2fd0bf7938de3bc1bf3fb121", + "tests/test_installation_profile.py": "61de0f3235600310ca77b900dcdf5f9b2d8a78bd5425f0fd3c35b680e6250301", + "tests/test_managed_processing_policy.py": "7c65bb7547b29e28ce04b835574bb2cd2bc7b97d2f3a38bccb81014dcfb27d35", + "tests/test_manual_graph_probe.py": "9d27578e5df38c892e60b56360952fbf77ce2292a52f64d5b137962f6dfb40cd", + "tests/test_mcp_contract.py": "da34ca5d4303e112fb55168ad112de340b178d388038fd31dd4256da45d47fb1", + "tests/test_mcp_server.py": "fca4d0663ea5d2d5bcfb8494a73dc553638d77eb5bd755b98c6793c819716f65", + "tests/test_memory_browsing.py": "85128315f486d8287db99de857c2b96142f7c46de289c25dfa0ad5f4ee3b743d", + "tests/test_native_coverage_equivalence.py": "b4aea900361794c73526139363bd4435c843a6ab90e14d3912730009c909f351", + "tests/test_obsidian_import_schema.py": "0461e02a9543dac685bc1f8e09f0f7194f7f0a8df15cc5d5f46887c1c1e41381", + "tests/test_pro_cta.py": "ec692d6f2bfcea3726a3eaa819b4495c2312faa9746f8bd0e6b6e90e887378f2", + "tests/test_recall.py": "345799aea86d0ec0500e31b8fd2ebd7b50a89ae09cc22e470b617b199aec9354", + "tests/test_resolver_acceptance.py": "2115f5ab530f10e78544da94bf0dce138a23edf9cc64c0b441e971fbda1705af", + "tests/test_storage_concurrency_repair.py": "3040e01d5481e0d28fad43e57f74c01d4244f91bf2127628b322d60e0c098490", + "tests/test_store_fts_insert.py": "195dc8c7118f6b1f1a98e6e38c3f71cbc3f5568601b4022022660f31ed09f6c4", + "tests/test_sync.py": "58f434afae95141610b87ec2656105d8daae4e61eca46a9b57972b3cbe475ee8", + "tests/test_update.py": "b95c0a9cba23997c608b823eafc397027011a9c873bedde7ae95b7e6cf442f8d", + "tests/test_vector_numpy.py": "5fba0cbba8e94a3e2f24925ac8554db7998ef17e3577d1b8ee6842b740ead54a", + "tests/test_vector_scale_storage.py": "efb23b7748d327b0dce9b102ea581f7cf38fd96b233c8be798834bf78e99686b", + "tests/test_vector_scan_plan.py": "9312e44f73fe619ce64b73a5b1d8b637b45cd78fe7f408a174d786d6f34aec39", + "tests/test_vector_snapshot_plan.py": "83c49c621d4570c2a61a35c304665c27db80cf9da1c60ecabc670cfc9e7f9ad0", + "tests/test_vector_sqlitevec_backend.py": "68411784738ef6fc8a7c19d08d7903296629c2958a2f966fd3cbec3bb11fb5bf", + "tools/galaxy_mode_test.js": "2f70a633c3b9cd902903cecaadf7c84cb3ab61e71db1702d88da14e18fc69bfc" + }, + "source_after": { + ".claude-plugin/skill-assets.sha256": "ec68564272a1fa5dc1e7bef660d777faf13881fd1d7662bdc5cd2d5c6eaec0b8", + ".github/workflows/ci.yml": "368fdd94d2e8b45995301b45f5771cce2a127fec9b0774d8e86a736fa8e02317", + "AGENTS.md": "77571f62390e516b445849eef4e9aa9ea7b6292efc7143868e38785f46e1f50a", + "CHANGELOG.md": "de0bfc1c527d0c163a177e449e94f3b6fcb935ba4b8a07bc793c88a598230561", + "README.md": "34975b33dda2e51e167793f78041d918e993589bf688d0d1013e71cf4210d3c3", + "docs/HOSTED_PLANS.md": "676fe44c6bf7bffdbd926eb269090004eff28cd00fc61501018588ab52e1df24", + "docs/HOSTING_RAILWAY.md": "81e530eea41b85f43b3746535db8bd5d16c2c45df39ad2fb1ebf3bf09e3b24bc", + "docs/MCP_CONTRACT.json": "b6db612c7660ddab19a4ad0786a7c94c9a660ab3c53aecc0130813a5ccba0259", + "docs/MCP_TOOLS.md": "604132977e2ade2e2b4dbb09c42197ee044b184724f745f8e1d3f0ed70c0f6fb", + "docs/PAID_EVALUATION_PROPOSAL.md": "dbdc3d91305df86c26365962044ed481d7f1f980afa0c526b3feafa08d007de4", + "docs/RAILWAY_TEMPLATE.md": "3a4cef99ad95b7a2c78e1dffc8fe860230d59c39d3e3267b727c51d792988546", + "docs/SYNC.md": "fe2f5ff8158ca4aa505684d9ee6b077108195b3e8cfd75dd9d5d5b77c40a11e7", + "engraphis/backends/vector_numpy.py": "c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72", + "engraphis/backends/vector_sqlitevec.py": "6148e14ceaacc19239b64c642a3fba0e98797c78cec356210157afadf475a08b", + "engraphis/classic_assets/dashboard.js": "c912bfef9fdf7a2113d781200a0c1ce9cae92bb01db53a4c4f07fbcbe82dd861", + "engraphis/classic_assets/index.html": "bd7f37e88cf9bf21be2490906e2f620b251d1d3db787e41793d4c8a898392089", + "engraphis/cloud_features.py": "a1bec76216d4f1276a3313dabc1d29a1666378e11863d05d1a02e695cf6d7293", + "engraphis/commercial.py": "184f312066a9e682e51a0abeff042f1c0e8eed2d47470157b23930b5a17633aa", + "engraphis/commercial_manifest.json": "27dfb332a5b4f3ceeed22aef37e1f55e1c051ab69cc9eedc4f826ddf4c752162", + "engraphis/core/browsing.py": "9adce7bbe8791fe4e76b892977d22cc72958c5d8ef3f98c58f50efdc1bb2fd36", + "engraphis/core/context.py": "34320c44f96be4bf4e7260264621902c598b8c49a6df27a1c28f3ebed55201ac", + "engraphis/core/engine.py": "942de6672902b9b19d8d86fe45984533ced0d1fcd3b857d8bf3eec8bc60a792d", + "engraphis/core/interfaces.py": "5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22", + "engraphis/core/recall.py": "fb77fb97d5c9e6e6d14d9d3d5b074c894cbd7c2472b7a25fa84b280343b13625", + "engraphis/core/resolve.py": "ffc17dd39331bf0c6881fa2efa9bcca73f0b7c968135fffaa4dbb025bb32c674", + "engraphis/core/schema.py": "99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686", + "engraphis/core/store.py": "007272ec42011faaae25bdbaf8905cf0d2945bf6f451b60aa7a90fff0550debd", + "engraphis/core/vector_repair.py": "425901615d3b602e3310af1611ebb3c0b78f3d793694c4ca8fffb55f00d07904", + "engraphis/core/vector_search.py": "75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234", + "engraphis/dashboard_assets/engraphis-graph-every.js": "ca84b13d1b6ccb72f347d726b977a8e19f51423e8beff7b072f73788f7cd086f", + "engraphis/dashboard_assets/index.html": "86b8f1935ece771523506d3410dd08294edb8183c2abaecd091418f37f17f00b", + "engraphis/dashboard_assets/ledger.js": "9a7abb256636302ec5970fbfcfc96ea8e961e49b26d3a01389c2b51d0a0b8c4f", + "engraphis/dashboard_assets/managed-processing.js": "9508c6b6dacab08304e3c08fd65871758b3043180840e5a8d9d64f6f8b3f000e", + "engraphis/managed_processing.py": "6a04b90ccf62dcd2f70cec1c8994b62e8242638ff0430dc20dca1cb42ac8792b", + "engraphis/mcp_server.py": "ad49631bcc7ef9dede5d2b8b1182f27eb8d01fd103be4dfeb13014bbbd84081d", + "engraphis/routes/v2_api.py": "e01e196ea087d61a7fbad770f7b2c9dcebb968a30b55c82e46040b6af2210a10", + "engraphis/service.py": "572e61628fbd1d8a9e5ce3e21c6feaf15bf915e52027366edeca6b7cee314aba", + "engraphis/static/dashboard.js": "c912bfef9fdf7a2113d781200a0c1ce9cae92bb01db53a4c4f07fbcbe82dd861", + "engraphis/static/index.html": "4f79a3266ddab53a4ef407fc31393295a0d5991a8f72431b2b958be114ac8f7e", + "eval/datasets/resolver_write_acceptance.jsonl": "e8db06aa41a93e4892e81d60bb71f06e656088f3eba36ebe37710850a97db6f7", + "eval/fts_insert_scaling.py": "a8ecfbe19695f6d4659c3edf389b007c965b214b64fdd58d5ee86635fa766144", + "eval/native_coverage_scaling.py": "0c7c3f5c80570c7ab02310f14927eb82399d1a624d65df895e937ed87339fe9c", + "eval/resolver_reworded_corrections.py": "f89e0920986efecd736380f0b8021aae553cfb304761f489d49828e6ffec537e", + "eval/vector_scale.py": "3f9c327d9eca1a857512ddc208e972933be1a7d4fa7fa0017aca0cbf8fe7bb6d", + "eval/vector_scale_storage.py": "bda0c1139596eb9232cd75c826fc941101b6116bae32ad7895525d43f9d35484", + "eval/vector_scan_plan.py": "92de2f3b41aa457dcfee11998936ce35e023fe61596ed98f3836c998a6e9a1cd", + "integrations/pi/index.ts": "454ab9d7b095d76cd138404dba45bdbe627c1f8cd6b5284881cd910f64e05a0c", + "integrations/pi/src/generated-contract.ts": "906711cba159d75b8c5440f12796be78cd2f8f4b423971e4009bb3c0d61223c6", + "integrations/pi/src/tool-schemas.ts": "daf238df8a1541a2b61590a3281f65f206fb5fe8d9af2bbb292ec4794453fd2c", + "integrations/pi/test/mcp-client.integration.ts": "45bee1e9e9bb33b3199f1df1ecad93b9f8f95a972ab26b1fc3c3f13b3bdec512", + "integrations/prime_agent/src/engraphis_prime_agent/_contract.py": "4b868447886cc7f6708c1667b5807882c822728db2b0efb4860283ba4fac400b", + "integrations/prime_agent/src/engraphis_prime_agent/tools.py": "b126532c8740f4a93c76abbfa701fb35a50fe1bc6dc45da06b33ecfaeaa7472d", + "integrations/prime_agent/tests/test_tools.py": "36e905860667343ccd2bd97ed5ff05a63fc32e247dbf5b73dfc07796dcc3fec5", + "playwright.config.js": "89aaaece6f36ba55fa35406983f0eb1f2695e4872e6793b44a53604120a06ee9", + "scripts/check_commercial_manifest.py": "2eace66d9e9870f70dd804eb3b62766f34d75add314631bccc3d1ee6b482c1b3", + "scripts/export_mcp_contract.py": "65d177342aa3bef63fb5d9f6c687fd1c3e72dcc85154dcd8108d0d887ac2e9ff", + "scripts/init.py": "5435ad275bf76a564d9ecdd3122a7bc3b84d18e56e0ec828757f4400c20fb735", + "scripts/installation_profile.py": "994da94a696c1eb06b83683735fdafb6235b562465800f1b98f3df63b14d523c", + "scripts/update.py": "a2e68c1ccb0a746efb369d26e3309069bd1e87cde0b087d3cda4783fda52b489", + "skills/engraphis-memory/references/TOOLS.md": "33874c7c7a1c0911b0e73c7d22addc9828963d5436cb315fe7c6c5587c6b911d", + "tests/e2e/commercial.spec.js": "c9e080d5807ab7484e1738cd731dba98871afe6dc8af6ad720dfd1cd34c9d6e0", + "tests/e2e/ledger.spec.js": "e895543ebd03fd80ce19f2678e0a0231f4ae3401dff475f6d781613dfdf4d213", + "tests/e2e/workspace-smoke.spec.js": "01f63458123e824bcf1e14d6505ecf7c776fe6ffd9e0bdc556b65e34880b22e4", + "tests/test_cloud_features.py": "951f4ae8775a1f99eff89d2fd18d227c16fdcc24f892532118fe6fb83d085ef2", + "tests/test_context_economy.py": "891102b713a04ff34dedfccab4ebc0d51fe6c7617b469d30794cce7970f9e41d", + "tests/test_context_evidence_preservation.py": "6121a71511c25a302f6fed5912d9e70d1f783765b88701edf0994b8d6c714d0c", + "tests/test_context_packer.py": "bb2cbf4615cbd5aca678da60eac79b3a6c6eb16265830ea645627a024601304e", + "tests/test_context_packing.py": "e19938d0f9beaba4c3919f10e6eb2d2625cb00f6cf11ef8bf0595423026f8fdf", + "tests/test_dashboard_auth_placement.py": "aba4826b85aae1fbb367bfee7ffbd2133c947eefbdb5a0512a9f9821fd22d1cb", + "tests/test_documentation_contracts.py": "aee709f8b77e01a1792ce0b16532401b2bb00371a3a1915b12f1c01bdaf2946f", + "tests/test_engine.py": "3afe96d40007ae4c16de9ea89be8cbd318c4aa03fd266264e232da764d83aeaa", + "tests/test_fts_insert_scaling.py": "7f1168828c73f2f07e1c31419a581dd42784752767f74b8fcbc78e10815c8a14", + "tests/test_graph_engine_asset.py": "665cdb9ecf609d8e93402e1c2701d082b88d098aae2a8b361612077b4650190d", + "tests/test_hosted_plan_resolution.py": "f71a3f00fccb1fa9cd2b69a35912a565ba06ffa789059f9943d2e6a749a6ba0e", + "tests/test_init.py": "3fbced7c627bd590a75015e07608ac10138d835d2fd0bf7938de3bc1bf3fb121", + "tests/test_installation_profile.py": "61de0f3235600310ca77b900dcdf5f9b2d8a78bd5425f0fd3c35b680e6250301", + "tests/test_managed_processing_policy.py": "7c65bb7547b29e28ce04b835574bb2cd2bc7b97d2f3a38bccb81014dcfb27d35", + "tests/test_manual_graph_probe.py": "9d27578e5df38c892e60b56360952fbf77ce2292a52f64d5b137962f6dfb40cd", + "tests/test_mcp_contract.py": "da34ca5d4303e112fb55168ad112de340b178d388038fd31dd4256da45d47fb1", + "tests/test_mcp_server.py": "fca4d0663ea5d2d5bcfb8494a73dc553638d77eb5bd755b98c6793c819716f65", + "tests/test_memory_browsing.py": "85128315f486d8287db99de857c2b96142f7c46de289c25dfa0ad5f4ee3b743d", + "tests/test_native_coverage_equivalence.py": "b4aea900361794c73526139363bd4435c843a6ab90e14d3912730009c909f351", + "tests/test_obsidian_import_schema.py": "0461e02a9543dac685bc1f8e09f0f7194f7f0a8df15cc5d5f46887c1c1e41381", + "tests/test_pro_cta.py": "ec692d6f2bfcea3726a3eaa819b4495c2312faa9746f8bd0e6b6e90e887378f2", + "tests/test_recall.py": "345799aea86d0ec0500e31b8fd2ebd7b50a89ae09cc22e470b617b199aec9354", + "tests/test_resolver_acceptance.py": "2115f5ab530f10e78544da94bf0dce138a23edf9cc64c0b441e971fbda1705af", + "tests/test_storage_concurrency_repair.py": "3040e01d5481e0d28fad43e57f74c01d4244f91bf2127628b322d60e0c098490", + "tests/test_store_fts_insert.py": "195dc8c7118f6b1f1a98e6e38c3f71cbc3f5568601b4022022660f31ed09f6c4", + "tests/test_sync.py": "58f434afae95141610b87ec2656105d8daae4e61eca46a9b57972b3cbe475ee8", + "tests/test_update.py": "b95c0a9cba23997c608b823eafc397027011a9c873bedde7ae95b7e6cf442f8d", + "tests/test_vector_numpy.py": "5fba0cbba8e94a3e2f24925ac8554db7998ef17e3577d1b8ee6842b740ead54a", + "tests/test_vector_scale_storage.py": "efb23b7748d327b0dce9b102ea581f7cf38fd96b233c8be798834bf78e99686b", + "tests/test_vector_scan_plan.py": "9312e44f73fe619ce64b73a5b1d8b637b45cd78fe7f408a174d786d6f34aec39", + "tests/test_vector_snapshot_plan.py": "83c49c621d4570c2a61a35c304665c27db80cf9da1c60ecabc670cfc9e7f9ad0", + "tests/test_vector_sqlitevec_backend.py": "68411784738ef6fc8a7c19d08d7903296629c2958a2f966fd3cbec3bb11fb5bf", + "tools/galaxy_mode_test.js": "2f70a633c3b9cd902903cecaadf7c84cb3ab61e71db1702d88da14e18fc69bfc" + }, + "source_stable": true, + "changed_files": [], + "exclusions": [ + "docs/evidence/**", + "docs/RELIABILITY_PROGRAM.md: final report assembled separately" + ], + "tests": { + "tests": 4789, + "errors": 0, + "failures": 2, + "skipped": 37, + "passed": 4750, + "reported_seconds": 392.14 + }, + "skipped_cases": [ + { + "test": "tests.test_code_index_route_security::test_code_index_rejects_symlink_escape", + "reason": "directory symlinks are unavailable in this environment" + }, + { + "test": "tests.test_codegraph::test_symlinked_file_is_not_followed_out_of_root", + "reason": "symlinks not supported on this platform" + }, + { + "test": "tests.test_commercial_hardening::test_the_credential_state_directory_is_owner_only", + "reason": "POSIX permission semantics" + }, + { + "test": "tests.test_config::test_explicit_env_file_must_be_owner_private_on_posix", + "reason": "POSIX permission bits are not authoritative on Windows" + }, + { + "test": "tests.test_engine::test_index_repo_rejects_root_symlink_that_resolves_outside_approved_root", + "reason": "symlinks not supported in this environment" + }, + { + "test": "tests.test_engine::test_index_repo_never_reads_a_symlink_that_escapes_root", + "reason": "symlinks not supported in this environment" + }, + { + "test": "tests.test_hosted_ledger::test_private_ledger_enforces_owner_only_directory_and_file_modes", + "reason": "POSIX permission contract" + }, + { + "test": "tests.test_hosted_ledger::test_private_ledger_rejects_symlink_leaves_without_touching_target[ledger]", + "reason": "symlink creation unavailable" + }, + { + "test": "tests.test_hosted_ledger::test_private_ledger_rejects_symlink_leaves_without_touching_target[lock]", + "reason": "symlink creation unavailable" + }, + { + "test": "tests.test_hosted_ledger::test_private_ledger_rejects_public_existing_leaves[ledger]", + "reason": "POSIX permission contract" + }, + { + "test": "tests.test_hosted_ledger::test_private_ledger_rejects_public_existing_leaves[lock]", + "reason": "POSIX permission contract" + }, + { + "test": "tests.test_hosted_luna::test_worker_timeout_terminates_sdk_descendants", + "reason": "POSIX process groups are required" + }, + { + "test": "tests.test_init::test_init_rejects_an_insecure_existing_trusted_env", + "reason": "POSIX permission bits do not apply on Windows" + }, + { + "test": "tests.test_init::test_generated_trusted_env_and_parent_are_private", + "reason": "POSIX permission bits do not apply on Windows" + }, + { + "test": "tests.test_init::test_generated_encryption_key_is_private", + "reason": "POSIX permission bits do not apply on Windows" + }, + { + "test": "tests.test_llm_config::test_persist_project_env_preserves_the_existing_project_directory_mode", + "reason": "POSIX permission semantics" + }, + { + "test": "tests.test_memory_routes_fixes::test_legacy_folder_import_skips_symlink_escape", + "reason": "symlinks not supported in this environment" + }, + { + "test": "tests.test_obsidian_parser::test_vault_scan_skips_hidden_config_symlinks_and_rejects_secrets", + "reason": "symlinks unavailable on this platform" + }, + { + "test": "tests.test_obsidian_parser::test_vault_root_symlink_is_rejected", + "reason": "directory symlinks unavailable on this platform" + }, + { + "test": "tests.test_release_evidence::test_release_environment_command_emits_a_cyclonedx_sbom", + "reason": "release-only CycloneDX tool" + }, + { + "test": "tests.test_service::test_import_folder_symlink_escape_blocked", + "reason": "symlinks not supported in this environment" + }, + { + "test": "tests.test_store_fts_insert::test_update_cleans_existing_duplicate_mirrors[fallback]", + "reason": "plain-table primary key already prevents duplicates" + }, + { + "test": "tests.test_store_v4_migration::test_v4_backup_is_owner_only_even_under_permissive_umask", + "reason": "POSIX permission-bit contract" + }, + { + "test": "tests.test_sync::test_folder_transport_safe_named_symlink_marks_pull_incomplete", + "reason": "symlinks unavailable (e.g. unprivileged Windows)" + }, + { + "test": "tests.test_sync::test_folder_transport_push_never_writes_through_planted_symlinks", + "reason": "symlinks unavailable (e.g. unprivileged Windows)" + }, + { + "test": "tests.test_update_check::test_default_cache_directory_is_owner_private", + "reason": "POSIX permission bits are not authoritative on Windows" + }, + { + "test": "tests.test_encrypted_store::test_encrypts_at_rest_unreadable_without_key", + "reason": "encryption extra not installed" + }, + { + "test": "tests.test_encrypted_store::test_recall_and_reopen_work_encrypted", + "reason": "encryption extra not installed" + }, + { + "test": "tests.test_encrypted_store::test_wrong_key_is_rejected", + "reason": "encryption extra not installed" + }, + { + "test": "tests.test_encrypted_store::test_existing_encrypted_database_opens_read_only_without_sidecar_mutation", + "reason": "encryption extra not installed" + }, + { + "test": "tests.test_encrypted_store::test_encrypted_read_only_open_rejects_wrong_key", + "reason": "encryption extra not installed" + }, + { + "test": "tests.test_encrypted_store::test_encrypted_read_only_open_rejects_active_wal_before_connector", + "reason": "encryption extra not installed" + }, + { + "test": "tests.test_encrypted_store::test_key_from_file", + "reason": "encryption extra not installed" + }, + { + "test": "tests.test_encrypted_store::test_encrypted_manifest_snapshot_is_immutable_and_read_only", + "reason": "encryption extra not installed" + }, + { + "test": "tests.test_encrypted_store::test_passphrase_key_non_hex", + "reason": "encryption extra not installed" + }, + { + "test": "tests.test_encrypted_store::test_no_key_is_plaintext_and_backward_compatible", + "reason": "encryption extra not installed" + }, + { + "test": "tests.test_encrypted_store::test_key_pragma_escapes_quotes", + "reason": "encryption extra not installed" + } + ] +} diff --git a/docs/evidence/reliability/public-pr-source-final.json b/docs/evidence/reliability/public-pr-source-final.json new file mode 100644 index 00000000..e025d1ec --- /dev/null +++ b/docs/evidence/reliability/public-pr-source-final.json @@ -0,0 +1,383 @@ +{ + "schema": "engraphis-validation-source/v1", + "command": [ + "python", + "-m", + "pytest", + "tests/", + "-q", + "-o", + "addopts=", + "--tb=short", + "--junitxml", + "" + ], + "environment": { + "ENGRAPHIS_EXTRACTOR": "none", + "sqlite_vec": "0.1.9 isolated installation" + }, + "elapsed_seconds": 395.1142165000201, + "exit_code": 0, + "source_before": { + ".claude-plugin/skill-assets.sha256": "ec68564272a1fa5dc1e7bef660d777faf13881fd1d7662bdc5cd2d5c6eaec0b8", + ".github/workflows/ci.yml": "368fdd94d2e8b45995301b45f5771cce2a127fec9b0774d8e86a736fa8e02317", + "AGENTS.md": "77571f62390e516b445849eef4e9aa9ea7b6292efc7143868e38785f46e1f50a", + "CHANGELOG.md": "bdbb14b215401efcd08e4009f6ee9ca8eaa8ad1180ea0541ee4e0ffd8b4d07c9", + "README.md": "34975b33dda2e51e167793f78041d918e993589bf688d0d1013e71cf4210d3c3", + "docs/HOSTED_PLANS.md": "676fe44c6bf7bffdbd926eb269090004eff28cd00fc61501018588ab52e1df24", + "docs/HOSTING_RAILWAY.md": "81e530eea41b85f43b3746535db8bd5d16c2c45df39ad2fb1ebf3bf09e3b24bc", + "docs/MCP_CONTRACT.json": "b6db612c7660ddab19a4ad0786a7c94c9a660ab3c53aecc0130813a5ccba0259", + "docs/MCP_TOOLS.md": "604132977e2ade2e2b4dbb09c42197ee044b184724f745f8e1d3f0ed70c0f6fb", + "docs/PAID_EVALUATION_PROPOSAL.md": "dbdc3d91305df86c26365962044ed481d7f1f980afa0c526b3feafa08d007de4", + "docs/RAILWAY_TEMPLATE.md": "3a4cef99ad95b7a2c78e1dffc8fe860230d59c39d3e3267b727c51d792988546", + "docs/SYNC.md": "fe2f5ff8158ca4aa505684d9ee6b077108195b3e8cfd75dd9d5d5b77c40a11e7", + "engraphis/backends/vector_numpy.py": "c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72", + "engraphis/backends/vector_sqlitevec.py": "6148e14ceaacc19239b64c642a3fba0e98797c78cec356210157afadf475a08b", + "engraphis/classic_assets/dashboard.js": "5b2f8e6bdcef718b481ceee77687440b5814b89b95483858246f56a19a03496d", + "engraphis/classic_assets/index.html": "bd7f37e88cf9bf21be2490906e2f620b251d1d3db787e41793d4c8a898392089", + "engraphis/cloud_features.py": "a1bec76216d4f1276a3313dabc1d29a1666378e11863d05d1a02e695cf6d7293", + "engraphis/commercial.py": "184f312066a9e682e51a0abeff042f1c0e8eed2d47470157b23930b5a17633aa", + "engraphis/commercial_manifest.json": "27dfb332a5b4f3ceeed22aef37e1f55e1c051ab69cc9eedc4f826ddf4c752162", + "engraphis/core/browsing.py": "9adce7bbe8791fe4e76b892977d22cc72958c5d8ef3f98c58f50efdc1bb2fd36", + "engraphis/core/context.py": "3fbc5341adb353ad9b57c9b6368cdf166d1a116876af012214078d89eb53ddef", + "engraphis/core/engine.py": "0d76a5f028234933933aff7adb7078455051086ae1db9d0b41360540ed7e83e3", + "engraphis/core/interfaces.py": "5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22", + "engraphis/core/recall.py": "7eaabf69a1c3f4555a307fc6e294562edb5c6c15e4df627daf09187b25f8fb15", + "engraphis/core/resolve.py": "ffc17dd39331bf0c6881fa2efa9bcca73f0b7c968135fffaa4dbb025bb32c674", + "engraphis/core/schema.py": "99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686", + "engraphis/core/store.py": "007272ec42011faaae25bdbaf8905cf0d2945bf6f451b60aa7a90fff0550debd", + "engraphis/core/vector_repair.py": "425901615d3b602e3310af1611ebb3c0b78f3d793694c4ca8fffb55f00d07904", + "engraphis/core/vector_search.py": "75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234", + "engraphis/dashboard_assets/engraphis-graph-every.js": "ca84b13d1b6ccb72f347d726b977a8e19f51423e8beff7b072f73788f7cd086f", + "engraphis/dashboard_assets/index.html": "86b8f1935ece771523506d3410dd08294edb8183c2abaecd091418f37f17f00b", + "engraphis/dashboard_assets/ledger.js": "9a7abb256636302ec5970fbfcfc96ea8e961e49b26d3a01389c2b51d0a0b8c4f", + "engraphis/dashboard_assets/managed-processing.js": "9508c6b6dacab08304e3c08fd65871758b3043180840e5a8d9d64f6f8b3f000e", + "engraphis/managed_processing.py": "6a04b90ccf62dcd2f70cec1c8994b62e8242638ff0430dc20dca1cb42ac8792b", + "engraphis/mcp_server.py": "ad49631bcc7ef9dede5d2b8b1182f27eb8d01fd103be4dfeb13014bbbd84081d", + "engraphis/routes/v2_api.py": "e01e196ea087d61a7fbad770f7b2c9dcebb968a30b55c82e46040b6af2210a10", + "engraphis/service.py": "572e61628fbd1d8a9e5ce3e21c6feaf15bf915e52027366edeca6b7cee314aba", + "engraphis/static/dashboard.js": "5b2f8e6bdcef718b481ceee77687440b5814b89b95483858246f56a19a03496d", + "engraphis/static/index.html": "4f79a3266ddab53a4ef407fc31393295a0d5991a8f72431b2b958be114ac8f7e", + "eval/datasets/resolver_write_acceptance.jsonl": "e8db06aa41a93e4892e81d60bb71f06e656088f3eba36ebe37710850a97db6f7", + "eval/fts_insert_scaling.py": "a8ecfbe19695f6d4659c3edf389b007c965b214b64fdd58d5ee86635fa766144", + "eval/native_coverage_scaling.py": "0c7c3f5c80570c7ab02310f14927eb82399d1a624d65df895e937ed87339fe9c", + "eval/resolver_reworded_corrections.py": "f89e0920986efecd736380f0b8021aae553cfb304761f489d49828e6ffec537e", + "eval/vector_scale.py": "3f9c327d9eca1a857512ddc208e972933be1a7d4fa7fa0017aca0cbf8fe7bb6d", + "eval/vector_scale_storage.py": "bda0c1139596eb9232cd75c826fc941101b6116bae32ad7895525d43f9d35484", + "eval/vector_scan_plan.py": "92de2f3b41aa457dcfee11998936ce35e023fe61596ed98f3836c998a6e9a1cd", + "integrations/pi/index.ts": "454ab9d7b095d76cd138404dba45bdbe627c1f8cd6b5284881cd910f64e05a0c", + "integrations/pi/src/generated-contract.ts": "906711cba159d75b8c5440f12796be78cd2f8f4b423971e4009bb3c0d61223c6", + "integrations/pi/src/tool-schemas.ts": "daf238df8a1541a2b61590a3281f65f206fb5fe8d9af2bbb292ec4794453fd2c", + "integrations/pi/test/mcp-client.integration.ts": "45bee1e9e9bb33b3199f1df1ecad93b9f8f95a972ab26b1fc3c3f13b3bdec512", + "integrations/prime_agent/src/engraphis_prime_agent/_contract.py": "4b868447886cc7f6708c1667b5807882c822728db2b0efb4860283ba4fac400b", + "integrations/prime_agent/src/engraphis_prime_agent/tools.py": "b126532c8740f4a93c76abbfa701fb35a50fe1bc6dc45da06b33ecfaeaa7472d", + "integrations/prime_agent/tests/test_tools.py": "36e905860667343ccd2bd97ed5ff05a63fc32e247dbf5b73dfc07796dcc3fec5", + "playwright.config.js": "89aaaece6f36ba55fa35406983f0eb1f2695e4872e6793b44a53604120a06ee9", + "scripts/check_commercial_manifest.py": "2eace66d9e9870f70dd804eb3b62766f34d75add314631bccc3d1ee6b482c1b3", + "scripts/export_mcp_contract.py": "65d177342aa3bef63fb5d9f6c687fd1c3e72dcc85154dcd8108d0d887ac2e9ff", + "scripts/init.py": "5435ad275bf76a564d9ecdd3122a7bc3b84d18e56e0ec828757f4400c20fb735", + "scripts/installation_profile.py": "994da94a696c1eb06b83683735fdafb6235b562465800f1b98f3df63b14d523c", + "scripts/update.py": "5f28d0eeab28dd321084ad0f88aa6aff521016ac1056c12031b977371f9e333d", + "skills/engraphis-memory/references/TOOLS.md": "33874c7c7a1c0911b0e73c7d22addc9828963d5436cb315fe7c6c5587c6b911d", + "tests/e2e/commercial.spec.js": "c9e080d5807ab7484e1738cd731dba98871afe6dc8af6ad720dfd1cd34c9d6e0", + "tests/e2e/ledger.spec.js": "e895543ebd03fd80ce19f2678e0a0231f4ae3401dff475f6d781613dfdf4d213", + "tests/e2e/workspace-smoke.spec.js": "01f63458123e824bcf1e14d6505ecf7c776fe6ffd9e0bdc556b65e34880b22e4", + "tests/test_cloud_features.py": "951f4ae8775a1f99eff89d2fd18d227c16fdcc24f892532118fe6fb83d085ef2", + "tests/test_context_economy.py": "891102b713a04ff34dedfccab4ebc0d51fe6c7617b469d30794cce7970f9e41d", + "tests/test_context_evidence_preservation.py": "6121a71511c25a302f6fed5912d9e70d1f783765b88701edf0994b8d6c714d0c", + "tests/test_context_packer.py": "bb2cbf4615cbd5aca678da60eac79b3a6c6eb16265830ea645627a024601304e", + "tests/test_context_packing.py": "e19938d0f9beaba4c3919f10e6eb2d2625cb00f6cf11ef8bf0595423026f8fdf", + "tests/test_dashboard_auth_placement.py": "aba4826b85aae1fbb367bfee7ffbd2133c947eefbdb5a0512a9f9821fd22d1cb", + "tests/test_dashboard_v2.py": "114f3d104f73761d7032987d8e57dd3d5354e73a86ee787105a1930d1086732b", + "tests/test_documentation_contracts.py": "aee709f8b77e01a1792ce0b16532401b2bb00371a3a1915b12f1c01bdaf2946f", + "tests/test_engine.py": "3afe96d40007ae4c16de9ea89be8cbd318c4aa03fd266264e232da764d83aeaa", + "tests/test_fts_insert_scaling.py": "7f1168828c73f2f07e1c31419a581dd42784752767f74b8fcbc78e10815c8a14", + "tests/test_graph_engine_asset.py": "f3bf92600abb069c95c354b04ee973d9c37ac758348052b001fe443ebb36a00c", + "tests/test_hosted_plan_resolution.py": "f71a3f00fccb1fa9cd2b69a35912a565ba06ffa789059f9943d2e6a749a6ba0e", + "tests/test_init.py": "3fbced7c627bd590a75015e07608ac10138d835d2fd0bf7938de3bc1bf3fb121", + "tests/test_installation_profile.py": "61de0f3235600310ca77b900dcdf5f9b2d8a78bd5425f0fd3c35b680e6250301", + "tests/test_managed_processing_policy.py": "7c65bb7547b29e28ce04b835574bb2cd2bc7b97d2f3a38bccb81014dcfb27d35", + "tests/test_manual_graph_probe.py": "9d27578e5df38c892e60b56360952fbf77ce2292a52f64d5b137962f6dfb40cd", + "tests/test_mcp_contract.py": "da34ca5d4303e112fb55168ad112de340b178d388038fd31dd4256da45d47fb1", + "tests/test_mcp_server.py": "fca4d0663ea5d2d5bcfb8494a73dc553638d77eb5bd755b98c6793c819716f65", + "tests/test_memory_browsing.py": "85128315f486d8287db99de857c2b96142f7c46de289c25dfa0ad5f4ee3b743d", + "tests/test_native_coverage_equivalence.py": "b4aea900361794c73526139363bd4435c843a6ab90e14d3912730009c909f351", + "tests/test_obsidian_import_schema.py": "0461e02a9543dac685bc1f8e09f0f7194f7f0a8df15cc5d5f46887c1c1e41381", + "tests/test_pro_cta.py": "ec692d6f2bfcea3726a3eaa819b4495c2312faa9746f8bd0e6b6e90e887378f2", + "tests/test_recall.py": "8035ff3959d956f600d6ff13ad14f6a5e4602f9167407d6bb11dcb7d63187152", + "tests/test_resolver_acceptance.py": "2115f5ab530f10e78544da94bf0dce138a23edf9cc64c0b441e971fbda1705af", + "tests/test_storage_concurrency_repair.py": "3040e01d5481e0d28fad43e57f74c01d4244f91bf2127628b322d60e0c098490", + "tests/test_store_fts_insert.py": "195dc8c7118f6b1f1a98e6e38c3f71cbc3f5568601b4022022660f31ed09f6c4", + "tests/test_sync.py": "58f434afae95141610b87ec2656105d8daae4e61eca46a9b57972b3cbe475ee8", + "tests/test_update.py": "b95c0a9cba23997c608b823eafc397027011a9c873bedde7ae95b7e6cf442f8d", + "tests/test_vector_numpy.py": "5fba0cbba8e94a3e2f24925ac8554db7998ef17e3577d1b8ee6842b740ead54a", + "tests/test_vector_scale_storage.py": "efb23b7748d327b0dce9b102ea581f7cf38fd96b233c8be798834bf78e99686b", + "tests/test_vector_scan_plan.py": "9312e44f73fe619ce64b73a5b1d8b637b45cd78fe7f408a174d786d6f34aec39", + "tests/test_vector_snapshot_plan.py": "83c49c621d4570c2a61a35c304665c27db80cf9da1c60ecabc670cfc9e7f9ad0", + "tests/test_vector_sqlitevec_backend.py": "2ff0682c9050d4ee30a052d6a7820ca55091234ef96c7e9ab2b7c052abcc7347", + "tools/galaxy_mode_test.js": "2f70a633c3b9cd902903cecaadf7c84cb3ab61e71db1702d88da14e18fc69bfc" + }, + "source_after": { + ".claude-plugin/skill-assets.sha256": "ec68564272a1fa5dc1e7bef660d777faf13881fd1d7662bdc5cd2d5c6eaec0b8", + ".github/workflows/ci.yml": "368fdd94d2e8b45995301b45f5771cce2a127fec9b0774d8e86a736fa8e02317", + "AGENTS.md": "77571f62390e516b445849eef4e9aa9ea7b6292efc7143868e38785f46e1f50a", + "CHANGELOG.md": "bdbb14b215401efcd08e4009f6ee9ca8eaa8ad1180ea0541ee4e0ffd8b4d07c9", + "README.md": "34975b33dda2e51e167793f78041d918e993589bf688d0d1013e71cf4210d3c3", + "docs/HOSTED_PLANS.md": "676fe44c6bf7bffdbd926eb269090004eff28cd00fc61501018588ab52e1df24", + "docs/HOSTING_RAILWAY.md": "81e530eea41b85f43b3746535db8bd5d16c2c45df39ad2fb1ebf3bf09e3b24bc", + "docs/MCP_CONTRACT.json": "b6db612c7660ddab19a4ad0786a7c94c9a660ab3c53aecc0130813a5ccba0259", + "docs/MCP_TOOLS.md": "604132977e2ade2e2b4dbb09c42197ee044b184724f745f8e1d3f0ed70c0f6fb", + "docs/PAID_EVALUATION_PROPOSAL.md": "dbdc3d91305df86c26365962044ed481d7f1f980afa0c526b3feafa08d007de4", + "docs/RAILWAY_TEMPLATE.md": "3a4cef99ad95b7a2c78e1dffc8fe860230d59c39d3e3267b727c51d792988546", + "docs/SYNC.md": "fe2f5ff8158ca4aa505684d9ee6b077108195b3e8cfd75dd9d5d5b77c40a11e7", + "engraphis/backends/vector_numpy.py": "c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72", + "engraphis/backends/vector_sqlitevec.py": "6148e14ceaacc19239b64c642a3fba0e98797c78cec356210157afadf475a08b", + "engraphis/classic_assets/dashboard.js": "5b2f8e6bdcef718b481ceee77687440b5814b89b95483858246f56a19a03496d", + "engraphis/classic_assets/index.html": "bd7f37e88cf9bf21be2490906e2f620b251d1d3db787e41793d4c8a898392089", + "engraphis/cloud_features.py": "a1bec76216d4f1276a3313dabc1d29a1666378e11863d05d1a02e695cf6d7293", + "engraphis/commercial.py": "184f312066a9e682e51a0abeff042f1c0e8eed2d47470157b23930b5a17633aa", + "engraphis/commercial_manifest.json": "27dfb332a5b4f3ceeed22aef37e1f55e1c051ab69cc9eedc4f826ddf4c752162", + "engraphis/core/browsing.py": "9adce7bbe8791fe4e76b892977d22cc72958c5d8ef3f98c58f50efdc1bb2fd36", + "engraphis/core/context.py": "3fbc5341adb353ad9b57c9b6368cdf166d1a116876af012214078d89eb53ddef", + "engraphis/core/engine.py": "0d76a5f028234933933aff7adb7078455051086ae1db9d0b41360540ed7e83e3", + "engraphis/core/interfaces.py": "5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22", + "engraphis/core/recall.py": "7eaabf69a1c3f4555a307fc6e294562edb5c6c15e4df627daf09187b25f8fb15", + "engraphis/core/resolve.py": "ffc17dd39331bf0c6881fa2efa9bcca73f0b7c968135fffaa4dbb025bb32c674", + "engraphis/core/schema.py": "99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686", + "engraphis/core/store.py": "007272ec42011faaae25bdbaf8905cf0d2945bf6f451b60aa7a90fff0550debd", + "engraphis/core/vector_repair.py": "425901615d3b602e3310af1611ebb3c0b78f3d793694c4ca8fffb55f00d07904", + "engraphis/core/vector_search.py": "75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234", + "engraphis/dashboard_assets/engraphis-graph-every.js": "ca84b13d1b6ccb72f347d726b977a8e19f51423e8beff7b072f73788f7cd086f", + "engraphis/dashboard_assets/index.html": "86b8f1935ece771523506d3410dd08294edb8183c2abaecd091418f37f17f00b", + "engraphis/dashboard_assets/ledger.js": "9a7abb256636302ec5970fbfcfc96ea8e961e49b26d3a01389c2b51d0a0b8c4f", + "engraphis/dashboard_assets/managed-processing.js": "9508c6b6dacab08304e3c08fd65871758b3043180840e5a8d9d64f6f8b3f000e", + "engraphis/managed_processing.py": "6a04b90ccf62dcd2f70cec1c8994b62e8242638ff0430dc20dca1cb42ac8792b", + "engraphis/mcp_server.py": "ad49631bcc7ef9dede5d2b8b1182f27eb8d01fd103be4dfeb13014bbbd84081d", + "engraphis/routes/v2_api.py": "e01e196ea087d61a7fbad770f7b2c9dcebb968a30b55c82e46040b6af2210a10", + "engraphis/service.py": "572e61628fbd1d8a9e5ce3e21c6feaf15bf915e52027366edeca6b7cee314aba", + "engraphis/static/dashboard.js": "5b2f8e6bdcef718b481ceee77687440b5814b89b95483858246f56a19a03496d", + "engraphis/static/index.html": "4f79a3266ddab53a4ef407fc31393295a0d5991a8f72431b2b958be114ac8f7e", + "eval/datasets/resolver_write_acceptance.jsonl": "e8db06aa41a93e4892e81d60bb71f06e656088f3eba36ebe37710850a97db6f7", + "eval/fts_insert_scaling.py": "a8ecfbe19695f6d4659c3edf389b007c965b214b64fdd58d5ee86635fa766144", + "eval/native_coverage_scaling.py": "0c7c3f5c80570c7ab02310f14927eb82399d1a624d65df895e937ed87339fe9c", + "eval/resolver_reworded_corrections.py": "f89e0920986efecd736380f0b8021aae553cfb304761f489d49828e6ffec537e", + "eval/vector_scale.py": "3f9c327d9eca1a857512ddc208e972933be1a7d4fa7fa0017aca0cbf8fe7bb6d", + "eval/vector_scale_storage.py": "bda0c1139596eb9232cd75c826fc941101b6116bae32ad7895525d43f9d35484", + "eval/vector_scan_plan.py": "92de2f3b41aa457dcfee11998936ce35e023fe61596ed98f3836c998a6e9a1cd", + "integrations/pi/index.ts": "454ab9d7b095d76cd138404dba45bdbe627c1f8cd6b5284881cd910f64e05a0c", + "integrations/pi/src/generated-contract.ts": "906711cba159d75b8c5440f12796be78cd2f8f4b423971e4009bb3c0d61223c6", + "integrations/pi/src/tool-schemas.ts": "daf238df8a1541a2b61590a3281f65f206fb5fe8d9af2bbb292ec4794453fd2c", + "integrations/pi/test/mcp-client.integration.ts": "45bee1e9e9bb33b3199f1df1ecad93b9f8f95a972ab26b1fc3c3f13b3bdec512", + "integrations/prime_agent/src/engraphis_prime_agent/_contract.py": "4b868447886cc7f6708c1667b5807882c822728db2b0efb4860283ba4fac400b", + "integrations/prime_agent/src/engraphis_prime_agent/tools.py": "b126532c8740f4a93c76abbfa701fb35a50fe1bc6dc45da06b33ecfaeaa7472d", + "integrations/prime_agent/tests/test_tools.py": "36e905860667343ccd2bd97ed5ff05a63fc32e247dbf5b73dfc07796dcc3fec5", + "playwright.config.js": "89aaaece6f36ba55fa35406983f0eb1f2695e4872e6793b44a53604120a06ee9", + "scripts/check_commercial_manifest.py": "2eace66d9e9870f70dd804eb3b62766f34d75add314631bccc3d1ee6b482c1b3", + "scripts/export_mcp_contract.py": "65d177342aa3bef63fb5d9f6c687fd1c3e72dcc85154dcd8108d0d887ac2e9ff", + "scripts/init.py": "5435ad275bf76a564d9ecdd3122a7bc3b84d18e56e0ec828757f4400c20fb735", + "scripts/installation_profile.py": "994da94a696c1eb06b83683735fdafb6235b562465800f1b98f3df63b14d523c", + "scripts/update.py": "5f28d0eeab28dd321084ad0f88aa6aff521016ac1056c12031b977371f9e333d", + "skills/engraphis-memory/references/TOOLS.md": "33874c7c7a1c0911b0e73c7d22addc9828963d5436cb315fe7c6c5587c6b911d", + "tests/e2e/commercial.spec.js": "c9e080d5807ab7484e1738cd731dba98871afe6dc8af6ad720dfd1cd34c9d6e0", + "tests/e2e/ledger.spec.js": "e895543ebd03fd80ce19f2678e0a0231f4ae3401dff475f6d781613dfdf4d213", + "tests/e2e/workspace-smoke.spec.js": "01f63458123e824bcf1e14d6505ecf7c776fe6ffd9e0bdc556b65e34880b22e4", + "tests/test_cloud_features.py": "951f4ae8775a1f99eff89d2fd18d227c16fdcc24f892532118fe6fb83d085ef2", + "tests/test_context_economy.py": "891102b713a04ff34dedfccab4ebc0d51fe6c7617b469d30794cce7970f9e41d", + "tests/test_context_evidence_preservation.py": "6121a71511c25a302f6fed5912d9e70d1f783765b88701edf0994b8d6c714d0c", + "tests/test_context_packer.py": "bb2cbf4615cbd5aca678da60eac79b3a6c6eb16265830ea645627a024601304e", + "tests/test_context_packing.py": "e19938d0f9beaba4c3919f10e6eb2d2625cb00f6cf11ef8bf0595423026f8fdf", + "tests/test_dashboard_auth_placement.py": "aba4826b85aae1fbb367bfee7ffbd2133c947eefbdb5a0512a9f9821fd22d1cb", + "tests/test_dashboard_v2.py": "114f3d104f73761d7032987d8e57dd3d5354e73a86ee787105a1930d1086732b", + "tests/test_documentation_contracts.py": "aee709f8b77e01a1792ce0b16532401b2bb00371a3a1915b12f1c01bdaf2946f", + "tests/test_engine.py": "3afe96d40007ae4c16de9ea89be8cbd318c4aa03fd266264e232da764d83aeaa", + "tests/test_fts_insert_scaling.py": "7f1168828c73f2f07e1c31419a581dd42784752767f74b8fcbc78e10815c8a14", + "tests/test_graph_engine_asset.py": "f3bf92600abb069c95c354b04ee973d9c37ac758348052b001fe443ebb36a00c", + "tests/test_hosted_plan_resolution.py": "f71a3f00fccb1fa9cd2b69a35912a565ba06ffa789059f9943d2e6a749a6ba0e", + "tests/test_init.py": "3fbced7c627bd590a75015e07608ac10138d835d2fd0bf7938de3bc1bf3fb121", + "tests/test_installation_profile.py": "61de0f3235600310ca77b900dcdf5f9b2d8a78bd5425f0fd3c35b680e6250301", + "tests/test_managed_processing_policy.py": "7c65bb7547b29e28ce04b835574bb2cd2bc7b97d2f3a38bccb81014dcfb27d35", + "tests/test_manual_graph_probe.py": "9d27578e5df38c892e60b56360952fbf77ce2292a52f64d5b137962f6dfb40cd", + "tests/test_mcp_contract.py": "da34ca5d4303e112fb55168ad112de340b178d388038fd31dd4256da45d47fb1", + "tests/test_mcp_server.py": "fca4d0663ea5d2d5bcfb8494a73dc553638d77eb5bd755b98c6793c819716f65", + "tests/test_memory_browsing.py": "85128315f486d8287db99de857c2b96142f7c46de289c25dfa0ad5f4ee3b743d", + "tests/test_native_coverage_equivalence.py": "b4aea900361794c73526139363bd4435c843a6ab90e14d3912730009c909f351", + "tests/test_obsidian_import_schema.py": "0461e02a9543dac685bc1f8e09f0f7194f7f0a8df15cc5d5f46887c1c1e41381", + "tests/test_pro_cta.py": "ec692d6f2bfcea3726a3eaa819b4495c2312faa9746f8bd0e6b6e90e887378f2", + "tests/test_recall.py": "8035ff3959d956f600d6ff13ad14f6a5e4602f9167407d6bb11dcb7d63187152", + "tests/test_resolver_acceptance.py": "2115f5ab530f10e78544da94bf0dce138a23edf9cc64c0b441e971fbda1705af", + "tests/test_storage_concurrency_repair.py": "3040e01d5481e0d28fad43e57f74c01d4244f91bf2127628b322d60e0c098490", + "tests/test_store_fts_insert.py": "195dc8c7118f6b1f1a98e6e38c3f71cbc3f5568601b4022022660f31ed09f6c4", + "tests/test_sync.py": "58f434afae95141610b87ec2656105d8daae4e61eca46a9b57972b3cbe475ee8", + "tests/test_update.py": "b95c0a9cba23997c608b823eafc397027011a9c873bedde7ae95b7e6cf442f8d", + "tests/test_vector_numpy.py": "5fba0cbba8e94a3e2f24925ac8554db7998ef17e3577d1b8ee6842b740ead54a", + "tests/test_vector_scale_storage.py": "efb23b7748d327b0dce9b102ea581f7cf38fd96b233c8be798834bf78e99686b", + "tests/test_vector_scan_plan.py": "9312e44f73fe619ce64b73a5b1d8b637b45cd78fe7f408a174d786d6f34aec39", + "tests/test_vector_snapshot_plan.py": "83c49c621d4570c2a61a35c304665c27db80cf9da1c60ecabc670cfc9e7f9ad0", + "tests/test_vector_sqlitevec_backend.py": "2ff0682c9050d4ee30a052d6a7820ca55091234ef96c7e9ab2b7c052abcc7347", + "tools/galaxy_mode_test.js": "2f70a633c3b9cd902903cecaadf7c84cb3ab61e71db1702d88da14e18fc69bfc" + }, + "source_stable": true, + "changed_files": [], + "exclusions": [ + "docs/evidence/**", + "docs/RELIABILITY_PROGRAM.md: final report assembled separately" + ], + "tests": { + "tests": 4789, + "errors": 0, + "failures": 0, + "skipped": 37, + "passed": 4752, + "reported_seconds": 386.654 + }, + "skipped_cases": [ + { + "test": "tests.test_code_index_route_security::test_code_index_rejects_symlink_escape", + "reason": "directory symlinks are unavailable in this environment" + }, + { + "test": "tests.test_codegraph::test_symlinked_file_is_not_followed_out_of_root", + "reason": "symlinks not supported on this platform" + }, + { + "test": "tests.test_commercial_hardening::test_the_credential_state_directory_is_owner_only", + "reason": "POSIX permission semantics" + }, + { + "test": "tests.test_config::test_explicit_env_file_must_be_owner_private_on_posix", + "reason": "POSIX permission bits are not authoritative on Windows" + }, + { + "test": "tests.test_engine::test_index_repo_rejects_root_symlink_that_resolves_outside_approved_root", + "reason": "symlinks not supported in this environment" + }, + { + "test": "tests.test_engine::test_index_repo_never_reads_a_symlink_that_escapes_root", + "reason": "symlinks not supported in this environment" + }, + { + "test": "tests.test_hosted_ledger::test_private_ledger_enforces_owner_only_directory_and_file_modes", + "reason": "POSIX permission contract" + }, + { + "test": "tests.test_hosted_ledger::test_private_ledger_rejects_symlink_leaves_without_touching_target[ledger]", + "reason": "symlink creation unavailable" + }, + { + "test": "tests.test_hosted_ledger::test_private_ledger_rejects_symlink_leaves_without_touching_target[lock]", + "reason": "symlink creation unavailable" + }, + { + "test": "tests.test_hosted_ledger::test_private_ledger_rejects_public_existing_leaves[ledger]", + "reason": "POSIX permission contract" + }, + { + "test": "tests.test_hosted_ledger::test_private_ledger_rejects_public_existing_leaves[lock]", + "reason": "POSIX permission contract" + }, + { + "test": "tests.test_hosted_luna::test_worker_timeout_terminates_sdk_descendants", + "reason": "POSIX process groups are required" + }, + { + "test": "tests.test_init::test_init_rejects_an_insecure_existing_trusted_env", + "reason": "POSIX permission bits do not apply on Windows" + }, + { + "test": "tests.test_init::test_generated_trusted_env_and_parent_are_private", + "reason": "POSIX permission bits do not apply on Windows" + }, + { + "test": "tests.test_init::test_generated_encryption_key_is_private", + "reason": "POSIX permission bits do not apply on Windows" + }, + { + "test": "tests.test_llm_config::test_persist_project_env_preserves_the_existing_project_directory_mode", + "reason": "POSIX permission semantics" + }, + { + "test": "tests.test_memory_routes_fixes::test_legacy_folder_import_skips_symlink_escape", + "reason": "symlinks not supported in this environment" + }, + { + "test": "tests.test_obsidian_parser::test_vault_scan_skips_hidden_config_symlinks_and_rejects_secrets", + "reason": "symlinks unavailable on this platform" + }, + { + "test": "tests.test_obsidian_parser::test_vault_root_symlink_is_rejected", + "reason": "directory symlinks unavailable on this platform" + }, + { + "test": "tests.test_release_evidence::test_release_environment_command_emits_a_cyclonedx_sbom", + "reason": "release-only CycloneDX tool" + }, + { + "test": "tests.test_service::test_import_folder_symlink_escape_blocked", + "reason": "symlinks not supported in this environment" + }, + { + "test": "tests.test_store_fts_insert::test_update_cleans_existing_duplicate_mirrors[fallback]", + "reason": "plain-table primary key already prevents duplicates" + }, + { + "test": "tests.test_store_v4_migration::test_v4_backup_is_owner_only_even_under_permissive_umask", + "reason": "POSIX permission-bit contract" + }, + { + "test": "tests.test_sync::test_folder_transport_safe_named_symlink_marks_pull_incomplete", + "reason": "symlinks unavailable (e.g. unprivileged Windows)" + }, + { + "test": "tests.test_sync::test_folder_transport_push_never_writes_through_planted_symlinks", + "reason": "symlinks unavailable (e.g. unprivileged Windows)" + }, + { + "test": "tests.test_update_check::test_default_cache_directory_is_owner_private", + "reason": "POSIX permission bits are not authoritative on Windows" + }, + { + "test": "tests.test_encrypted_store::test_encrypts_at_rest_unreadable_without_key", + "reason": "encryption extra not installed" + }, + { + "test": "tests.test_encrypted_store::test_recall_and_reopen_work_encrypted", + "reason": "encryption extra not installed" + }, + { + "test": "tests.test_encrypted_store::test_wrong_key_is_rejected", + "reason": "encryption extra not installed" + }, + { + "test": "tests.test_encrypted_store::test_existing_encrypted_database_opens_read_only_without_sidecar_mutation", + "reason": "encryption extra not installed" + }, + { + "test": "tests.test_encrypted_store::test_encrypted_read_only_open_rejects_wrong_key", + "reason": "encryption extra not installed" + }, + { + "test": "tests.test_encrypted_store::test_encrypted_read_only_open_rejects_active_wal_before_connector", + "reason": "encryption extra not installed" + }, + { + "test": "tests.test_encrypted_store::test_key_from_file", + "reason": "encryption extra not installed" + }, + { + "test": "tests.test_encrypted_store::test_encrypted_manifest_snapshot_is_immutable_and_read_only", + "reason": "encryption extra not installed" + }, + { + "test": "tests.test_encrypted_store::test_passphrase_key_non_hex", + "reason": "encryption extra not installed" + }, + { + "test": "tests.test_encrypted_store::test_no_key_is_plaintext_and_backward_compatible", + "reason": "encryption extra not installed" + }, + { + "test": "tests.test_encrypted_store::test_key_pragma_escapes_quotes", + "reason": "encryption extra not installed" + } + ] +} diff --git a/docs/evidence/reliability/resolver-unit-20260905.json b/docs/evidence/reliability/resolver-unit-20260905.json new file mode 100644 index 00000000..60df56db --- /dev/null +++ b/docs/evidence/reliability/resolver-unit-20260905.json @@ -0,0 +1 @@ +{"environment": {"implementation": "CPython", "machine": "AMD64", "packages": {"engraphis": "1.7.1", "numpy": "2.4.5", "sentence-transformers": "6.0.0", "torch": "2.13.0", "transformers": "5.15.1"}, "platform": "Windows-11-10.0.26100-SP0", "python": "3.12.10"}, "exclusions": [], "metrics": {"correction_precision": 1.0, "correction_recall": 1.0, "distinct_fact_error_rate": 0.0, "execution": "resolver_unit", "false_invalidation_ids": [], "false_invalidations": 0, "false_noop_ids": [], "false_noops": 0, "lost_distinct_fact_ids": [], "lost_distinct_facts": 0, "missed_correction_ids": [], "missed_corrections": 0, "negatives": 6, "positives": 38, "positives_superseded": 38, "similarity_injected": true, "total": 44}, "models": {}, "privacy": {"content_fingerprint_policy": "omitted", "raw_answer_policy": "omitted", "raw_context_policy": "omitted", "raw_query_policy": "omitted"}, "protocol": {"command": ["python", "-m", "eval.resolver_reworded_corrections", "--dataset", "eval/datasets/resolver_reworded_corrections.jsonl", "--json"], "config": {"end_to_end": false, "evidence_scope": "authored regression corpus; not an independent quality benchmark", "offline": true}, "n_scored": 44, "n_total": 44, "token_accounting": {"identity": "unspecified", "method": "unspecified", "revision": null, "scope": "unspecified"}}, "records": [{"question_id": "rc01"}, {"question_id": "rc02"}, {"question_id": "rc03"}, {"question_id": "rc04"}, {"question_id": "rc05"}, {"question_id": "rc06"}, {"question_id": "rc07"}, {"question_id": "rc08"}, {"question_id": "rc09"}, {"question_id": "rc10"}, {"question_id": "rc11"}, {"question_id": "rc12"}, {"question_id": "rc13"}, {"question_id": "rc14"}, {"question_id": "rc15"}, {"question_id": "rc16"}, {"question_id": "rc17"}, {"question_id": "rc18"}, {"question_id": "rc19"}, {"question_id": "rc20"}, {"question_id": "rc21"}, {"question_id": "rc22"}, {"question_id": "rc23"}, {"question_id": "rc24"}, {"question_id": "rc25"}, {"question_id": "rc26"}, {"question_id": "rc27"}, {"question_id": "rc28"}, {"question_id": "rc29"}, {"question_id": "rc30"}, {"question_id": "rc31"}, {"question_id": "rc32"}, {"question_id": "rc33"}, {"question_id": "rc34"}, {"question_id": "rc35"}, {"question_id": "rc36"}, {"question_id": "df01"}, {"question_id": "df02"}, {"question_id": "df03"}, {"question_id": "df04"}, {"question_id": "df05"}, {"question_id": "df06"}, {"question_id": "df07"}, {"question_id": "df08"}], "schema": "engraphis-benchmark/v2", "suite": {"dataset": "resolver_reworded_corrections.jsonl", "name": "resolver-unit", "sha256": "5b796d8852117b30060cf019e93fd775fea256ca492ce0fedf9b4786f0a0421b", "sources": [{"bytes": 12511, "name": "resolver_reworded_corrections.py", "sha256": "f89e0920986efecd736380f0b8021aae553cfb304761f489d49828e6ffec537e"}, {"bytes": 44059, "name": "resolve.py", "sha256": "ffc17dd39331bf0c6881fa2efa9bcca73f0b7c968135fffaa4dbb025bb32c674"}]}, "system": {"config_sha256": "f332c6f681d4c59a1263fcd65b4aae2b28a1db252aff687890204859b04aaf33", "dirty_state_sha256": "1c4883fe9dac6decfa02b83601d2a4e8b507b511f397ba1e353e0feffbd320d2", "git_commit": "c37ba0eb18408fe500cd70cd73fb5dcd7679b89c", "git_dirty": true}} diff --git a/docs/evidence/reliability/resolver-unit-20260905.json.sha256 b/docs/evidence/reliability/resolver-unit-20260905.json.sha256 new file mode 100644 index 00000000..3e5a275d --- /dev/null +++ b/docs/evidence/reliability/resolver-unit-20260905.json.sha256 @@ -0,0 +1 @@ +7559397b4d0a003f32d0a0eae5b7925dd3cd4109de05c63f8c24c34583e82a76 resolver-unit-20260905.json diff --git a/docs/evidence/reliability/resolver-write-acceptance-20260905.json b/docs/evidence/reliability/resolver-write-acceptance-20260905.json new file mode 100644 index 00000000..c090cd4f --- /dev/null +++ b/docs/evidence/reliability/resolver-write-acceptance-20260905.json @@ -0,0 +1 @@ +{"environment": {"implementation": "CPython", "machine": "AMD64", "packages": {"engraphis": "1.7.1", "numpy": "2.4.5", "sentence-transformers": "6.0.0", "torch": "2.13.0", "transformers": "5.15.1"}, "platform": "Windows-11-10.0.26100-SP0", "python": "3.12.10"}, "exclusions": [], "metrics": {"correction_precision": 1.0, "correction_recall": 1.0, "distinct_fact_error_rate": 0.0, "execution": "production_write_path", "false_invalidation_ids": [], "false_invalidations": 0, "false_noop_ids": [], "false_noops": 0, "lost_distinct_fact_ids": [], "lost_distinct_facts": 0, "missed_correction_ids": [], "missed_corrections": 0, "negatives": 6, "positives": 4, "positives_superseded": 4, "similarity_injected": false, "total": 10}, "models": {}, "privacy": {"content_fingerprint_policy": "omitted", "raw_answer_policy": "omitted", "raw_context_policy": "omitted", "raw_query_policy": "omitted"}, "protocol": {"command": ["python", "-m", "eval.resolver_reworded_corrections", "--dataset", "eval/datasets/resolver_write_acceptance.jsonl", "--json", "--end-to-end"], "config": {"end_to_end": true, "evidence_scope": "authored regression corpus; not an independent quality benchmark", "offline": true}, "n_scored": 10, "n_total": 10, "token_accounting": {"identity": "unspecified", "method": "unspecified", "revision": null, "scope": "unspecified"}}, "records": [{"question_id": "write-ttl"}, {"question_id": "write-window"}, {"question_id": "write-reworded"}, {"question_id": "write-keyed-limit"}, {"question_id": "keep-environments"}, {"question_id": "keep-environment-binding"}, {"question_id": "keep-backup-binding"}, {"question_id": "keep-account-identity"}, {"question_id": "keep-subjects"}, {"question_id": "keep-different-attributes"}], "schema": "engraphis-benchmark/v2", "suite": {"dataset": "resolver_write_acceptance.jsonl", "name": "resolver-write-acceptance", "sha256": "e8db06aa41a93e4892e81d60bb71f06e656088f3eba36ebe37710850a97db6f7", "sources": [{"bytes": 12511, "name": "resolver_reworded_corrections.py", "sha256": "f89e0920986efecd736380f0b8021aae553cfb304761f489d49828e6ffec537e"}, {"bytes": 44059, "name": "resolve.py", "sha256": "ffc17dd39331bf0c6881fa2efa9bcca73f0b7c968135fffaa4dbb025bb32c674"}, {"bytes": 8756, "name": "factory.py", "sha256": "c8f0349b1c2e9b0fc389019ca3a41d36eb1bfb2230048f0db2c6f9fe9c15be61"}, {"bytes": 233878, "name": "engine.py", "sha256": "59ca726b3dbb5767ef4c9fb1f0142486ae60d9a823990350f507e4056929eace"}, {"bytes": 465965, "name": "store.py", "sha256": "8659a5fd216e65f9e1e8b44870ffc1ff26e85aa77a26f831146977b727a11c35"}, {"bytes": 39996, "name": "schema.py", "sha256": "99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686"}, {"bytes": 31040, "name": "interfaces.py", "sha256": "5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22"}, {"bytes": 2349, "name": "vector_search.py", "sha256": "75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234"}, {"bytes": 1741, "name": "vector_repair.py", "sha256": "425901615d3b602e3310af1611ebb3c0b78f3d793694c4ca8fffb55f00d07904"}, {"bytes": 6163, "name": "vector_numpy.py", "sha256": "c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72"}, {"bytes": 8347, "name": "embedder_deterministic.py", "sha256": "ec8b23de7e7e8273416125f5876ca96f55e4ae7881841bae55783ab0ba9130ad"}]}, "system": {"config_sha256": "ee045ea3b6856666fd62732a69814a5c737353b810a4b1a7b4a79a3186e1d101", "dirty_state_sha256": "1c4883fe9dac6decfa02b83601d2a4e8b507b511f397ba1e353e0feffbd320d2", "git_commit": "c37ba0eb18408fe500cd70cd73fb5dcd7679b89c", "git_dirty": true}} diff --git a/docs/evidence/reliability/resolver-write-acceptance-20260905.json.sha256 b/docs/evidence/reliability/resolver-write-acceptance-20260905.json.sha256 new file mode 100644 index 00000000..de87d869 --- /dev/null +++ b/docs/evidence/reliability/resolver-write-acceptance-20260905.json.sha256 @@ -0,0 +1 @@ +f13920bddd4e89adad64a156faf5f2914e57856df71d5479a0abb5f21bbfe977 resolver-write-acceptance-20260905.json diff --git a/docs/evidence/reliability/source-manifest.json b/docs/evidence/reliability/source-manifest.json new file mode 100644 index 00000000..d15785f5 --- /dev/null +++ b/docs/evidence/reliability/source-manifest.json @@ -0,0 +1,490 @@ +{ + "schema": "engraphis-reliability-sources/v1", + "date": "2026-09-05", + "public": { + "base_commit": "cb03dbe104394b7760ef402a2b1917eac5e8accc", + "implementation_checkpoint": "c37ba0eb18408fe500cd70cd73fb5dcd7679b89c", + "branch": "codex/reliable-agent-memory", + "candidate_state": "reviewed committed source; evidence assembled separately", + "preexisting_changes_reviewed": [ + "Four original unmerged commits from feat/context-packing-and-perf-v2", + "docs/MCP_TOOLS.md: pre-existing gist explanation corrected together with implementation during authorized review" + ], + "hash_scope": "all changed/non-ignored files relative to origin/main except evidence; unchanged files identified by base commit", + "files": { + ".claude-plugin/skill-assets.sha256": { + "sha256": "ec68564272a1fa5dc1e7bef660d777faf13881fd1d7662bdc5cd2d5c6eaec0b8", + "bytes": 628 + }, + ".github/workflows/ci.yml": { + "sha256": "368fdd94d2e8b45995301b45f5771cce2a127fec9b0774d8e86a736fa8e02317", + "bytes": 16539 + }, + "AGENTS.md": { + "sha256": "77571f62390e516b445849eef4e9aa9ea7b6292efc7143868e38785f46e1f50a", + "bytes": 20257 + }, + "CHANGELOG.md": { + "sha256": "bdbb14b215401efcd08e4009f6ee9ca8eaa8ad1180ea0541ee4e0ffd8b4d07c9", + "bytes": 116620 + }, + "README.md": { + "sha256": "34975b33dda2e51e167793f78041d918e993589bf688d0d1013e71cf4210d3c3", + "bytes": 58765 + }, + "docs/HOSTED_PLANS.md": { + "sha256": "676fe44c6bf7bffdbd926eb269090004eff28cd00fc61501018588ab52e1df24", + "bytes": 1799 + }, + "docs/HOSTING_RAILWAY.md": { + "sha256": "81e530eea41b85f43b3746535db8bd5d16c2c45df39ad2fb1ebf3bf09e3b24bc", + "bytes": 4376 + }, + "docs/MCP_CONTRACT.json": { + "sha256": "b6db612c7660ddab19a4ad0786a7c94c9a660ab3c53aecc0130813a5ccba0259", + "bytes": 136140 + }, + "docs/MCP_TOOLS.md": { + "sha256": "604132977e2ade2e2b4dbb09c42197ee044b184724f745f8e1d3f0ed70c0f6fb", + "bytes": 12735 + }, + "docs/PAID_EVALUATION_PROPOSAL.md": { + "sha256": "dbdc3d91305df86c26365962044ed481d7f1f980afa0c526b3feafa08d007de4", + "bytes": 4651 + }, + "docs/RAILWAY_TEMPLATE.md": { + "sha256": "3a4cef99ad95b7a2c78e1dffc8fe860230d59c39d3e3267b727c51d792988546", + "bytes": 1925 + }, + "docs/RELIABILITY_PROGRAM.md": { + "sha256": "a07d4cc8167db5ab03e84c09a5fe17b3584cfcfea92cabf6d09b371fb62ebcd7", + "bytes": 21540 + }, + "docs/SYNC.md": { + "sha256": "fe2f5ff8158ca4aa505684d9ee6b077108195b3e8cfd75dd9d5d5b77c40a11e7", + "bytes": 14194 + }, + "engraphis/backends/vector_numpy.py": { + "sha256": "c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72", + "bytes": 6163 + }, + "engraphis/backends/vector_sqlitevec.py": { + "sha256": "6148e14ceaacc19239b64c642a3fba0e98797c78cec356210157afadf475a08b", + "bytes": 23317 + }, + "engraphis/classic_assets/dashboard.js": { + "sha256": "5b2f8e6bdcef718b481ceee77687440b5814b89b95483858246f56a19a03496d", + "bytes": 211092 + }, + "engraphis/classic_assets/index.html": { + "sha256": "bd7f37e88cf9bf21be2490906e2f620b251d1d3db787e41793d4c8a898392089", + "bytes": 32383 + }, + "engraphis/cloud_features.py": { + "sha256": "a1bec76216d4f1276a3313dabc1d29a1666378e11863d05d1a02e695cf6d7293", + "bytes": 29135 + }, + "engraphis/commercial.py": { + "sha256": "184f312066a9e682e51a0abeff042f1c0e8eed2d47470157b23930b5a17633aa", + "bytes": 2023 + }, + "engraphis/commercial_manifest.json": { + "sha256": "27dfb332a5b4f3ceeed22aef37e1f55e1c051ab69cc9eedc4f826ddf4c752162", + "bytes": 2926 + }, + "engraphis/core/__init__.py": { + "sha256": "dd5143729c3939237f04636f437032b1f2d3a5f7d82c91bbc2a5a283c3f0ebaa", + "bytes": 1262 + }, + "engraphis/core/browsing.py": { + "sha256": "9adce7bbe8791fe4e76b892977d22cc72958c5d8ef3f98c58f50efdc1bb2fd36", + "bytes": 5826 + }, + "engraphis/core/context.py": { + "sha256": "3fbc5341adb353ad9b57c9b6368cdf166d1a116876af012214078d89eb53ddef", + "bytes": 25299 + }, + "engraphis/core/engine.py": { + "sha256": "0d76a5f028234933933aff7adb7078455051086ae1db9d0b41360540ed7e83e3", + "bytes": 234246 + }, + "engraphis/core/graphrank.py": { + "sha256": "1279a58396104d3f906bfd5ec75b32efedfefe52201467bf19d80be3517017a5", + "bytes": 5819 + }, + "engraphis/core/interfaces.py": { + "sha256": "5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22", + "bytes": 31040 + }, + "engraphis/core/recall.py": { + "sha256": "7eaabf69a1c3f4555a307fc6e294562edb5c6c15e4df627daf09187b25f8fb15", + "bytes": 108482 + }, + "engraphis/core/resolve.py": { + "sha256": "ffc17dd39331bf0c6881fa2efa9bcca73f0b7c968135fffaa4dbb025bb32c674", + "bytes": 44059 + }, + "engraphis/core/schema.py": { + "sha256": "99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686", + "bytes": 39996 + }, + "engraphis/core/store.py": { + "sha256": "007272ec42011faaae25bdbaf8905cf0d2945bf6f451b60aa7a90fff0550debd", + "bytes": 466184 + }, + "engraphis/core/vector_repair.py": { + "sha256": "425901615d3b602e3310af1611ebb3c0b78f3d793694c4ca8fffb55f00d07904", + "bytes": 1741 + }, + "engraphis/core/vector_search.py": { + "sha256": "75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234", + "bytes": 2349 + }, + "engraphis/dashboard_assets/engraphis-graph-every.js": { + "sha256": "ca84b13d1b6ccb72f347d726b977a8e19f51423e8beff7b072f73788f7cd086f", + "bytes": 80367 + }, + "engraphis/dashboard_assets/index.html": { + "sha256": "86b8f1935ece771523506d3410dd08294edb8183c2abaecd091418f37f17f00b", + "bytes": 58206 + }, + "engraphis/dashboard_assets/ledger.js": { + "sha256": "9a7abb256636302ec5970fbfcfc96ea8e961e49b26d3a01389c2b51d0a0b8c4f", + "bytes": 228549 + }, + "engraphis/dashboard_assets/managed-processing.js": { + "sha256": "9508c6b6dacab08304e3c08fd65871758b3043180840e5a8d9d64f6f8b3f000e", + "bytes": 3685 + }, + "engraphis/managed_processing.py": { + "sha256": "6a04b90ccf62dcd2f70cec1c8994b62e8242638ff0430dc20dca1cb42ac8792b", + "bytes": 4745 + }, + "engraphis/mcp_server.py": { + "sha256": "ad49631bcc7ef9dede5d2b8b1182f27eb8d01fd103be4dfeb13014bbbd84081d", + "bytes": 159278 + }, + "engraphis/routes/v2_api.py": { + "sha256": "e01e196ea087d61a7fbad770f7b2c9dcebb968a30b55c82e46040b6af2210a10", + "bytes": 181944 + }, + "engraphis/service.py": { + "sha256": "572e61628fbd1d8a9e5ce3e21c6feaf15bf915e52027366edeca6b7cee314aba", + "bytes": 602358 + }, + "engraphis/static/dashboard.js": { + "sha256": "5b2f8e6bdcef718b481ceee77687440b5814b89b95483858246f56a19a03496d", + "bytes": 211092 + }, + "engraphis/static/index.html": { + "sha256": "4f79a3266ddab53a4ef407fc31393295a0d5991a8f72431b2b958be114ac8f7e", + "bytes": 32671 + }, + "eval/datasets/resolver_write_acceptance.jsonl": { + "sha256": "e8db06aa41a93e4892e81d60bb71f06e656088f3eba36ebe37710850a97db6f7", + "bytes": 2025 + }, + "eval/fts_insert_scaling.py": { + "sha256": "a8ecfbe19695f6d4659c3edf389b007c965b214b64fdd58d5ee86635fa766144", + "bytes": 5247 + }, + "eval/native_coverage_scaling.py": { + "sha256": "0c7c3f5c80570c7ab02310f14927eb82399d1a624d65df895e937ed87339fe9c", + "bytes": 7688 + }, + "eval/resolver_reworded_corrections.py": { + "sha256": "f89e0920986efecd736380f0b8021aae553cfb304761f489d49828e6ffec537e", + "bytes": 12511 + }, + "eval/vector_scale.py": { + "sha256": "3f9c327d9eca1a857512ddc208e972933be1a7d4fa7fa0017aca0cbf8fe7bb6d", + "bytes": 11055 + }, + "eval/vector_scale_storage.py": { + "sha256": "bda0c1139596eb9232cd75c826fc941101b6116bae32ad7895525d43f9d35484", + "bytes": 22744 + }, + "eval/vector_scan_plan.py": { + "sha256": "92de2f3b41aa457dcfee11998936ce35e023fe61596ed98f3836c998a6e9a1cd", + "bytes": 7477 + }, + "integrations/pi/index.ts": { + "sha256": "454ab9d7b095d76cd138404dba45bdbe627c1f8cd6b5284881cd910f64e05a0c", + "bytes": 11088 + }, + "integrations/pi/src/generated-contract.ts": { + "sha256": "906711cba159d75b8c5440f12796be78cd2f8f4b423971e4009bb3c0d61223c6", + "bytes": 12841 + }, + "integrations/pi/src/tool-schemas.ts": { + "sha256": "daf238df8a1541a2b61590a3281f65f206fb5fe8d9af2bbb292ec4794453fd2c", + "bytes": 1797 + }, + "integrations/pi/test/mcp-client.integration.ts": { + "sha256": "45bee1e9e9bb33b3199f1df1ecad93b9f8f95a972ab26b1fc3c3f13b3bdec512", + "bytes": 3976 + }, + "integrations/prime_agent/src/engraphis_prime_agent/_contract.py": { + "sha256": "4b868447886cc7f6708c1667b5807882c822728db2b0efb4860283ba4fac400b", + "bytes": 25781 + }, + "integrations/prime_agent/src/engraphis_prime_agent/tools.py": { + "sha256": "b126532c8740f4a93c76abbfa701fb35a50fe1bc6dc45da06b33ecfaeaa7472d", + "bytes": 19174 + }, + "integrations/prime_agent/tests/test_tools.py": { + "sha256": "36e905860667343ccd2bd97ed5ff05a63fc32e247dbf5b73dfc07796dcc3fec5", + "bytes": 15133 + }, + "playwright.config.js": { + "sha256": "89aaaece6f36ba55fa35406983f0eb1f2695e4872e6793b44a53604120a06ee9", + "bytes": 1871 + }, + "scripts/check_commercial_manifest.py": { + "sha256": "2eace66d9e9870f70dd804eb3b62766f34d75add314631bccc3d1ee6b482c1b3", + "bytes": 11443 + }, + "scripts/export_mcp_contract.py": { + "sha256": "65d177342aa3bef63fb5d9f6c687fd1c3e72dcc85154dcd8108d0d887ac2e9ff", + "bytes": 2593 + }, + "scripts/init.py": { + "sha256": "5435ad275bf76a564d9ecdd3122a7bc3b84d18e56e0ec828757f4400c20fb735", + "bytes": 19779 + }, + "scripts/installation_profile.py": { + "sha256": "994da94a696c1eb06b83683735fdafb6235b562465800f1b98f3df63b14d523c", + "bytes": 2595 + }, + "scripts/update.py": { + "sha256": "5f28d0eeab28dd321084ad0f88aa6aff521016ac1056c12031b977371f9e333d", + "bytes": 36775 + }, + "skills/engraphis-memory/references/TOOLS.md": { + "sha256": "33874c7c7a1c0911b0e73c7d22addc9828963d5436cb315fe7c6c5587c6b911d", + "bytes": 34530 + }, + "tests/e2e/commercial.spec.js": { + "sha256": "c9e080d5807ab7484e1738cd731dba98871afe6dc8af6ad720dfd1cd34c9d6e0", + "bytes": 32320 + }, + "tests/e2e/ledger.spec.js": { + "sha256": "e895543ebd03fd80ce19f2678e0a0231f4ae3401dff475f6d781613dfdf4d213", + "bytes": 110950 + }, + "tests/e2e/workspace-smoke.spec.js": { + "sha256": "01f63458123e824bcf1e14d6505ecf7c776fe6ffd9e0bdc556b65e34880b22e4", + "bytes": 3523 + }, + "tests/test_cloud_features.py": { + "sha256": "951f4ae8775a1f99eff89d2fd18d227c16fdcc24f892532118fe6fb83d085ef2", + "bytes": 20426 + }, + "tests/test_context_economy.py": { + "sha256": "891102b713a04ff34dedfccab4ebc0d51fe6c7617b469d30794cce7970f9e41d", + "bytes": 10417 + }, + "tests/test_context_evidence_preservation.py": { + "sha256": "6121a71511c25a302f6fed5912d9e70d1f783765b88701edf0994b8d6c714d0c", + "bytes": 7528 + }, + "tests/test_context_packer.py": { + "sha256": "bb2cbf4615cbd5aca678da60eac79b3a6c6eb16265830ea645627a024601304e", + "bytes": 8613 + }, + "tests/test_context_packing.py": { + "sha256": "e19938d0f9beaba4c3919f10e6eb2d2625cb00f6cf11ef8bf0595423026f8fdf", + "bytes": 14593 + }, + "tests/test_dashboard_auth_placement.py": { + "sha256": "aba4826b85aae1fbb367bfee7ffbd2133c947eefbdb5a0512a9f9821fd22d1cb", + "bytes": 35156 + }, + "tests/test_dashboard_v2.py": { + "sha256": "114f3d104f73761d7032987d8e57dd3d5354e73a86ee787105a1930d1086732b", + "bytes": 99569 + }, + "tests/test_documentation_contracts.py": { + "sha256": "aee709f8b77e01a1792ce0b16532401b2bb00371a3a1915b12f1c01bdaf2946f", + "bytes": 11341 + }, + "tests/test_engine.py": { + "sha256": "3afe96d40007ae4c16de9ea89be8cbd318c4aa03fd266264e232da764d83aeaa", + "bytes": 126126 + }, + "tests/test_fts_insert_scaling.py": { + "sha256": "7f1168828c73f2f07e1c31419a581dd42784752767f74b8fcbc78e10815c8a14", + "bytes": 594 + }, + "tests/test_graph_engine_asset.py": { + "sha256": "f3bf92600abb069c95c354b04ee973d9c37ac758348052b001fe443ebb36a00c", + "bytes": 614537 + }, + "tests/test_hosted_plan_resolution.py": { + "sha256": "f71a3f00fccb1fa9cd2b69a35912a565ba06ffa789059f9943d2e6a749a6ba0e", + "bytes": 93837 + }, + "tests/test_init.py": { + "sha256": "3fbced7c627bd590a75015e07608ac10138d835d2fd0bf7938de3bc1bf3fb121", + "bytes": 14617 + }, + "tests/test_installation_profile.py": { + "sha256": "61de0f3235600310ca77b900dcdf5f9b2d8a78bd5425f0fd3c35b680e6250301", + "bytes": 2056 + }, + "tests/test_managed_processing_policy.py": { + "sha256": "7c65bb7547b29e28ce04b835574bb2cd2bc7b97d2f3a38bccb81014dcfb27d35", + "bytes": 14259 + }, + "tests/test_manual_graph_probe.py": { + "sha256": "9d27578e5df38c892e60b56360952fbf77ce2292a52f64d5b137962f6dfb40cd", + "bytes": 1463 + }, + "tests/test_mcp_contract.py": { + "sha256": "da34ca5d4303e112fb55168ad112de340b178d388038fd31dd4256da45d47fb1", + "bytes": 1381 + }, + "tests/test_mcp_server.py": { + "sha256": "fca4d0663ea5d2d5bcfb8494a73dc553638d77eb5bd755b98c6793c819716f65", + "bytes": 55540 + }, + "tests/test_memory_browsing.py": { + "sha256": "85128315f486d8287db99de857c2b96142f7c46de289c25dfa0ad5f4ee3b743d", + "bytes": 5588 + }, + "tests/test_native_coverage_equivalence.py": { + "sha256": "b4aea900361794c73526139363bd4435c843a6ab90e14d3912730009c909f351", + "bytes": 2744 + }, + "tests/test_obsidian_import_schema.py": { + "sha256": "0461e02a9543dac685bc1f8e09f0f7194f7f0a8df15cc5d5f46887c1c1e41381", + "bytes": 27533 + }, + "tests/test_pro_cta.py": { + "sha256": "ec692d6f2bfcea3726a3eaa819b4495c2312faa9746f8bd0e6b6e90e887378f2", + "bytes": 5215 + }, + "tests/test_recall.py": { + "sha256": "8035ff3959d956f600d6ff13ad14f6a5e4602f9167407d6bb11dcb7d63187152", + "bytes": 39710 + }, + "tests/test_resolver_acceptance.py": { + "sha256": "2115f5ab530f10e78544da94bf0dce138a23edf9cc64c0b441e971fbda1705af", + "bytes": 4848 + }, + "tests/test_storage_concurrency_repair.py": { + "sha256": "3040e01d5481e0d28fad43e57f74c01d4244f91bf2127628b322d60e0c098490", + "bytes": 17090 + }, + "tests/test_store_fts_insert.py": { + "sha256": "195dc8c7118f6b1f1a98e6e38c3f71cbc3f5568601b4022022660f31ed09f6c4", + "bytes": 6146 + }, + "tests/test_sync.py": { + "sha256": "58f434afae95141610b87ec2656105d8daae4e61eca46a9b57972b3cbe475ee8", + "bytes": 136475 + }, + "tests/test_update.py": { + "sha256": "b95c0a9cba23997c608b823eafc397027011a9c873bedde7ae95b7e6cf442f8d", + "bytes": 28564 + }, + "tests/test_vector_numpy.py": { + "sha256": "5fba0cbba8e94a3e2f24925ac8554db7998ef17e3577d1b8ee6842b740ead54a", + "bytes": 12605 + }, + "tests/test_vector_scale_storage.py": { + "sha256": "efb23b7748d327b0dce9b102ea581f7cf38fd96b233c8be798834bf78e99686b", + "bytes": 4697 + }, + "tests/test_vector_scan_plan.py": { + "sha256": "9312e44f73fe619ce64b73a5b1d8b637b45cd78fe7f408a174d786d6f34aec39", + "bytes": 760 + }, + "tests/test_vector_snapshot_plan.py": { + "sha256": "83c49c621d4570c2a61a35c304665c27db80cf9da1c60ecabc670cfc9e7f9ad0", + "bytes": 3122 + }, + "tests/test_vector_sqlitevec_backend.py": { + "sha256": "2ff0682c9050d4ee30a052d6a7820ca55091234ef96c7e9ab2b7c052abcc7347", + "bytes": 30270 + }, + "tools/galaxy_mode_test.js": { + "sha256": "2f70a633c3b9cd902903cecaadf7c84cb3ab61e71db1702d88da14e18fc69bfc", + "bytes": 21953 + } + }, + "source_commit": "d0606c2896bd6176756bdc0865748488c5df3677" + }, + "website": { + "base_commit": "2aa9f8d03717f2f1376f541104942a402c7077e4", + "implementation_checkpoint": "b67edda97cee437cff2d900e1a0786b9fc87677b", + "branch": "codex/current-product-contract", + "candidate_state": "reviewed local source before public PR submission; individual file hashes are authoritative", + "preexisting_changes_reviewed": [ + "about.html", + "assets/js/demo.js", + "index.html", + "product.html", + "test/site.test.mjs" + ], + "hash_scope": "all changed/non-ignored files relative to origin/main except evidence; unchanged files identified by base commit", + "files": { + ".github/workflows/site.yml": { + "sha256": "c2969f291558242b06632cde0c624ba09e88f0b0609ac2a14feeb7e386358968", + "bytes": 6477 + }, + ".gitignore": { + "sha256": "d6b35f5ed9e03394b63894e3bd9f7aae8a679ab511c192eff3511cbc51b7112b", + "bytes": 364 + }, + "about.html": { + "sha256": "43ec92f57303a35e58973079f60b07ed2c191cd1c741871989800613288c0054", + "bytes": 16057 + }, + "assets/js/demo.js": { + "sha256": "5fd7403c7179ff6bdcfaac05896fdac04ea0a0ca94f0e85251ed28903d70d0b1", + "bytes": 52631 + }, + "commercial_manifest.json": { + "sha256": "db4888fcd555440cd8591bb87732f7c640a940da4541763b23537f8b3bd649ef", + "bytes": 2823 + }, + "index.html": { + "sha256": "ec0d4d22ccc6abe73b9383280721b0661b5799b5403ce672e77d3a60727a8dd2", + "bytes": 21130 + }, + "product.html": { + "sha256": "5bfbb0fc035f552b515181fbd6f6d8d02d3ff4a2293ee0782edac2891026f230", + "bytes": 28461 + }, + "scripts/check_commercial_claims.py": { + "sha256": "262a24fde95143c198c6bee06d5d55c23904d922a744ba43ea2ceef99d0ebad9", + "bytes": 3865 + }, + "test/site.test.mjs": { + "sha256": "1c64ecc8f67a96e485a85c65156c3cb379b63cc6aa264493fc70d6b71e127d59", + "bytes": 19638 + } + } + }, + "private_cloud": { + "base_commit": "8cd1f3cb819c4bfb55ac44a74aa009a8b709efac", + "manifest": "engraphis-cloud/docs/evidence/reliability/source-manifest-pre-lock-refresh.json", + "manifest_sha256": "314b9787a862004724e0ac2b580d1a5be2e5604b39470724ed72254dffd5a516", + "privacy_boundary": "private source and history remain in their private repository; this identifies the reviewed checkpoint", + "boundary": "Reviewed Cloud source checkpoint before a separate pre-existing dependency-lock refresh; final Cloud delivery and locked-environment results are maintained privately." + }, + "dependencies": { + "numpy": "2.4.5", + "pytest": "9.0.3", + "fastapi": "0.141.1", + "mcp": "1.29.0", + "pydantic": "2.13.4", + "ruff": "0.16.4", + "pyright": "1.1.411", + "cryptography": "50.0.0", + "SQLAlchemy": "2.0.52", + "alembic": "1.19.1", + "httpx": "0.28.1" + } +} diff --git a/docs/evidence/reliability/validation.json b/docs/evidence/reliability/validation.json new file mode 100644 index 00000000..211765d1 --- /dev/null +++ b/docs/evidence/reliability/validation.json @@ -0,0 +1,536 @@ +{ + "schema": "engraphis-reliability-validation/v1", + "prepared_date": "2026-09-05", + "status": "reviewed PR candidate; release and deployment approval remain separate", + "source_manifest": "source-manifest.json", + "source_boundary": "The final candidate manifest identifies source relative to released main. The final PR public suite passed on stable production and test source; public-pr-source-final.json records the exact before/after hashes. Earlier failed checkpoints and corrections remain retained. Final report/evidence files are assembled separately. Results overlap and must not be added into a unique-test total.", + "environment": { + "os": "Windows 11 build 26100", + "python": "3.12.10", + "sqlite": "3.49.1", + "numpy": "2.4.5", + "fastapi": "0.141.1", + "mcp": "1.29.0", + "pydantic": "2.13.4", + "pytest": "9.0.3", + "ruff": "0.16.4", + "pyright": "1.1.411", + "offline_environment": { + "ENGRAPHIS_EXTRACTOR": "none" + }, + "native_backend": "sqlite-vec 0.1.9, isolated temporary installation" + }, + "checks": [ + { + "id": "public-full-suite", + "repository": "engraphis", + "command": "python -m pytest tests/ -q", + "passed": 4649, + "failed": 5, + "skipped": 40, + "warnings": 2, + "seconds": 384.32, + "result": "failures corrected and all five rerun successfully; not a clean monolithic run", + "failures": [ + "tests/test_benchmark_evidence.py::test_public_facing_docs_do_not_use_em_dashes", + "tests/test_commercial_hardening.py::test_both_dashboard_bundles_strip_control_characters_before_the_scheme_test", + "tests/test_dashboard_v2.py::test_classic_dashboard_script_mirrors_the_static_compatibility_asset", + "tests/test_graph_engine_asset.py::test_classic_dashboard_copies_share_the_canonical_route_gate", + "tests/test_graph_engine_asset.py::test_classic_graph_hides_implicit_co_occurrence_edge_labels" + ], + "correction": "Removed a forbidden em dash from the proposal and synchronized Classic compatibility assets/cache references.", + "superseded_by": "public-final-complete-suite" + }, + { + "id": "public-failure-rerun", + "repository": "engraphis", + "command": "python -m pytest tests/ --lf -q -o addopts='--strict-markers'", + "passed": 5, + "failed": 0, + "result": "passed at the corrective checkpoint; --lf is cache-dependent, use the five explicit node IDs above to reproduce later" + }, + { + "id": "graph-regression", + "repository": "engraphis", + "command": "python -m pytest tests/test_graph_engine_asset.py -q", + "passed": 238, + "failed": 0, + "seconds": 70.4 + }, + { + "id": "offline-evaluation", + "repository": "engraphis", + "artifact": "offline-gates.json", + "passed_commands": 7, + "result": "passed", + "boundary": "deterministic authored fixtures; not independent semantic or agent-task evidence" + }, + { + "id": "storage-fts-scale", + "repository": "engraphis", + "passed": 443, + "skipped": 4, + "failed": 0, + "result": "passed", + "boundary": "worker's focused storage/sync/security/scale collection after FTS insertion change" + }, + { + "id": "native-fts-focused", + "repository": "engraphis", + "passed": 24, + "skipped": 1, + "failed": 0, + "result": "passed", + "boundary": "isolated sqlite-vec 0.1.9; intentional FTS-fallback skip" + }, + { + "id": "pi-integration-unit", + "repository": "engraphis/integrations/pi", + "command": "npm test", + "passed": 21, + "failed": 0, + "result": "passed" + }, + { + "id": "pi-integration-restart", + "repository": "engraphis/integrations/pi", + "command": "npm run test:integration", + "passed": 1, + "failed": 0, + "result": "passed", + "boundary": "real MCP process, isolated temporary database; save, close process, reopen, recall with sources" + }, + { + "id": "pi-type-package", + "repository": "engraphis/integrations/pi", + "commands": [ + "npm run typecheck", + "npm run pack:check" + ], + "result": "passed" + }, + { + "id": "prime-integration", + "repository": "engraphis/integrations/prime_agent", + "command": "python -m pytest tests/ -q", + "passed": 136, + "skipped": 1, + "failed": 0, + "result": "passed" + }, + { + "id": "browser-ledger-and-journey", + "repository": "engraphis", + "command": "npx playwright test tests/e2e/ledger.spec.js tests/e2e/workspace-smoke.spec.js", + "passed": 47, + "failed": 2, + "total": 49, + "result": "two ambiguous locators fixed; targeted five-case rerun passed", + "boundary": "Chromium coverage plus real save/recall/correct/history/reload journeys on Firefox and WebKit on Windows; isolated local test token and database" + }, + { + "id": "browser-corrective-rerun", + "repository": "engraphis", + "passed": 5, + "failed": 0, + "result": "passed" + }, + { + "id": "browser-processing-controls", + "repository": "engraphis", + "passed": 4, + "failed": 0, + "result": "passed", + "boundary": "fixture-backed cloud acknowledgements; waiting, optout retry, stale workspace and failure states" + }, + { + "id": "trial-api-regression", + "repository": "engraphis", + "passed": 35, + "failed": 0, + "result": "passed", + "boundary": "canonical days_by_plan additive API and compatible legacy fields" + }, + { + "id": "trial-browser-regression", + "repository": "engraphis", + "passed": 4, + "failed": 0, + "result": "passed", + "boundary": "Team 10, Pro 3, billing cadence, unknown-duration fallback" + }, + { + "id": "website", + "repository": "engraphis.com", + "command": "npm test", + "passed": 24, + "failed": 0, + "result": "passed", + "boundary": "source tests; existing website work preserved; unpublished" + }, + { + "id": "cloud-first-complete-suite", + "repository": "engraphis-cloud", + "command": "python -m pytest tests/ -q", + "passed": 1150, + "skipped": 2, + "failed": 0, + "result": "passed before final policy-revision race tests; final run recorded separately", + "superseded_by": "cloud-final-complete-suite" + }, + { + "id": "edge-runtime", + "repository": "engraphis-cloud/team_edge", + "command": "npm test", + "passed": 12, + "failed": 0, + "result": "passed", + "boundary": "nine unit checks plus three actual workerd runtime checks; no production bindings or rotation verified" + }, + { + "id": "edge-typecheck", + "repository": "engraphis-cloud/team_edge", + "command": "npm run typecheck", + "result": "passed" + }, + { + "id": "public-policy-race-final", + "repository": "engraphis", + "command": "python -m pytest tests/test_managed_processing_policy.py tests/test_cloud_features.py -q -o addopts= --tb=short", + "passed": 48, + "failed": 0, + "seconds": 3.68, + "result": "passed", + "boundary": "required remote revision, local intent guards, delayed GET/PUT, optout-only conflict retry" + }, + { + "id": "cross-repository-processing", + "repository": "engraphis-cloud", + "command": "python scripts/check_processing_policy_compatibility.py --public-checkout ../engraphis", + "passed": 3, + "failed": 0, + "result": "passed", + "boundary": "actual public client/local API and private compute API using generated test keys/disposable databases and in-process ASGI transport; no production network" + }, + { + "id": "cloud-final-complete-suite", + "repository": "engraphis-cloud", + "command": "python -m pytest tests -q -o addopts= --tb=short", + "passed": 1155, + "skipped": 2, + "failed": 0, + "warnings": 8, + "seconds": 287.32, + "result": "passed", + "boundary": "includes policy revision races and expiry pre-read/pre-publication/scheduler/API regressions; two isolated-PostgreSQL integration gates skipped" + }, + { + "id": "integrated-final-static", + "repository": "engraphis", + "commands": [ + "ruff check .", + "pyright", + "python scripts/export_mcp_contract.py --check", + "python scripts/externalize_dashboard_assets.py", + "python scripts/check_commercial_manifest.py --website-root ../engraphis.com --cloud-contract " + ], + "result": "passed", + "boundary": "contract generation emitted one Pydantic IncompleteFieldDefinitionWarning; exit code was zero" + }, + { + "id": "integrated-correctness", + "repository": "engraphis", + "command": "python -m pytest tests/test_resolver_acceptance.py tests/test_context_evidence_preservation.py tests/test_memory_browsing.py tests/test_storage_concurrency_repair.py -q -o addopts= --tb=short", + "passed": 65, + "failed": 0, + "seconds": 6.03, + "result": "passed" + }, + { + "id": "resolver-evidence", + "repository": "engraphis", + "artifacts": [ + "resolver-unit-20260905.json", + "resolver-write-acceptance-20260905.json" + ], + "result": "passed", + "boundary": "44 resolver unit pairs inject similarity; 10 real-write pairs do not. Both report zero false NOOPs/false invalidations; authored fixtures, not held-out semantic quality." + }, + { + "id": "pi-final-process-restart", + "repository": "engraphis/integrations/pi", + "command": "npm run test:integration", + "passed": 1, + "failed": 0, + "seconds": 5.37, + "result": "passed" + }, + { + "id": "final-storage-native-checkpoint", + "repository": "engraphis", + "passed": 494, + "skipped": 3, + "failed": 0, + "seconds": 21.03, + "result": "passed", + "boundary": "Store/sync/NumPy/native/FTS/checkpoint/method-ablation/snapshot checks on frozen final storage implementation; Ruff/Pyright and whitespace passed" + }, + { + "id": "public-final-source-complete-before-trial-test-correction", + "repository": "engraphis", + "command": "python -m pytest tests/ -q --tb=short", + "passed": 4718, + "failed": 16, + "skipped": 37, + "warnings": 2, + "seconds": 448.2978430999792, + "result": "all 16 failures reproduced immediately; obsolete test harnesses/assertions corrected, both complete modules now pass", + "artifact": "public-complete-source-before-trial-test-correction.json", + "count_method": "Counted all 4771 outcome symbols across all 67 complete pytest progress lines; double quiet mode suppressed final totals. No other progress lines or error/xfail symbols.", + "boundary": "Frozen final production source, isolated sqlite-vec 0.1.9; obsolete trial test extraction/expected templates only.", + "superseded_by": "public-final-complete-suite" + }, + { + "id": "final-scale-matrix", + "repository": "engraphis", + "artifacts": [ + "vector-scale-numpy-corrected-20260905.json", + "vector-scale-sqlite-vec-corrected-20260905.json", + "vector-scale-summary-20260905.md" + ], + "result": "both backends completed all six cells with exact matching result IDs, durable mixed writes and reopen; native rebuild passed", + "cells": 12, + "boundary": "10k/100k synthetic precomputed 256-dimensional vectors, 25 percent scoped eligibility, concurrency 1/4/16, 32 timed search samples per cell; index/store measurements, not full agent/semantic results. Native reopen and concurrent search remain substantial costs; 5 percent scope diagnostic regression retained." + }, + { + "id": "trial-test-harness-reproduction", + "repository": "engraphis", + "command": "python -m pytest -o addopts='' tests/test_dashboard_auth_placement.py tests/test_pro_cta.py -q", + "passed": 34, + "failed": 16, + "result": "all 16 prior failures reproduced before edits" + }, + { + "id": "trial-test-harness-corrected", + "repository": "engraphis", + "command": "python -m pytest -o addopts='' tests/test_dashboard_auth_placement.py tests/test_pro_cta.py -q", + "passed": 52, + "failed": 0, + "result": "passed", + "boundary": "Only these two test modules changed. Extracted JS now includes the real helper; two added tests each exercise seven duration payloads against the real Classic and Ledger helpers. Production source unchanged." + }, + { + "id": "trial-correction-adjacent-regression", + "repository": "engraphis", + "passed": 35, + "deselected": 114, + "failed": 0, + "result": "passed" + }, + { + "id": "trial-correction-asset-buttons", + "repository": "engraphis", + "passed": 22, + "failed": 0, + "result": "passed" + }, + { + "id": "evidence-integrity", + "repository": "engraphis", + "result": "passed", + "checksum_receipts_verified": 11, + "paired_scale_cells_verified": 12, + "boundary": "SHA-256 receipts verified and corresponding size/concurrency/result-ID hashes compared; source hashes are retained in each benchmark report." + }, + { + "id": "offline-evaluation-final-source", + "repository": "engraphis", + "artifact": "offline-gates-final.json", + "passed_commands": 7, + "failed_commands": 0, + "seconds": 5.639012700004969, + "result": "passed", + "boundary": "Every required offline evaluation gate rerun on stable final core/backend/eval source with before/after SHA-256 maps; deterministic authored fixtures only." + }, + { + "id": "public-final-complete-suite", + "repository": "engraphis", + "command": "python -m pytest tests/ -q -o addopts= --tb=short --junitxml ", + "passed": 4736, + "failed": 0, + "errors": 0, + "skipped": 37, + "total": 4773, + "warnings": 2, + "seconds": 372.44, + "wrapper_elapsed_seconds": 380.7047358000418, + "result": "passed", + "artifact": "public-complete-source-final.json", + "boundary": "All final public production/test source stable throughout; native sqlite-vec 0.1.9 available in isolated installation. Final documentation/evidence assembly excluded from source snapshot." + }, + { + "id": "pr-core-review", + "repository": "engraphis", + "result": "approved after three reproduced defects were fixed", + "artifact": "pr-review.json", + "boundary": "Packing/source bindings, atomic native batch publication and 12000-memory graph-window regression. Focused runs overlap." + }, + { + "id": "pr-public-before-contract-correction", + "repository": "engraphis", + "passed": 4750, + "failed": 2, + "skipped": 37, + "warnings": 2, + "seconds": 392.47, + "artifact": "public-pr-source-before-contract-correction.json", + "result": "two obsolete consent/caching expectations reproduced in isolation and corrected; superseded by clean full rerun", + "superseded_by": "pr-public-complete" + }, + { + "id": "pr-public-complete", + "repository": "engraphis", + "command": "python -m pytest tests/ -q -o addopts= --tb=short --junitxml ", + "passed": 4752, + "failed": 0, + "errors": 0, + "skipped": 37, + "total": 4789, + "warnings": 2, + "seconds": 386.98, + "wrapper_elapsed_seconds": 395.1142165000201, + "artifact": "public-pr-source-final.json", + "result": "passed", + "boundary": "Final production and test source stable throughout; sqlite-vec 0.1.9 isolated installation." + }, + { + "id": "pr-offline-complete", + "repository": "engraphis", + "artifact": "offline-gates-pr-final.json", + "passed_commands": 7, + "failed_commands": 0, + "result": "passed", + "boundary": "Stable final core/backend/eval source; deterministic authored fixtures, not held-out task evidence." + }, + { + "id": "pr-ui-review", + "repository": "engraphis", + "passed": 119, + "failed": 0, + "result": "passed", + "browser_chromium": 9, + "boundary": "MCP budgets, Classic consent and workspace-specific settings; targeted review cases." + }, + { + "id": "pr-integration-final", + "repository": "engraphis", + "result": "passed", + "pi_unit": 21, + "pi_restart": 1, + "pi_typescript_and_package": "passed", + "prime_passed": 136, + "prime_skipped": 1 + }, + { + "id": "pr-cloud-complete", + "repository": "engraphis-cloud", + "passed": 1157, + "failed": 0, + "skipped": 2, + "warnings": 8, + "seconds": 216.66, + "result": "passed", + "boundary": "Private proof remains in its repository; SQLite concurrency, migration, authorization and cleanup; PostgreSQL tests skipped." + }, + { + "id": "pr-edge-final", + "repository": "engraphis-cloud/team_edge", + "passed": 13, + "failed": 0, + "actual_workerd_included": 4, + "typescript": "passed", + "dependency_audit": "zero vulnerabilities at review time", + "result": "passed", + "boundary": "Repeated after restoring original line endings. No production binding/rotation verification." + }, + { + "id": "pr-website-review", + "repository": "engraphis.com", + "unit_passed": 24, + "browser_chromium_axe_passed": 11, + "result": "local gates passed", + "boundary": "Existing remote canonical-main manifest gate requires public change to merge first; website is a dependent draft." + }, + { + "id": "pr-static-contracts", + "repository": "engraphis", + "commands": [ + "ruff check .", + "pyright", + "python scripts/export_mcp_contract.py --check", + "python scripts/externalize_dashboard_assets.py", + "python scripts/check_commercial_manifest.py --website-root ../engraphis.com --cloud-contract " + ], + "result": "passed" + }, + { + "id": "pr-scale-source-binding", + "repository": "engraphis", + "artifact": "pr-benchmark-source-check.json", + "result": "passed", + "source_files_per_backend": 9, + "paired_cells": 12, + "boundary": "All measured storage/index source files still exactly match the completed benchmark artifacts; unrelated review fixes do not imply new performance measurements." + } + ], + "pending": [], + "unverified": [ + "paid model evaluations", + "independent held-out coding-agent corpus", + "full 100000-memory remember/recall agent workload", + "one-million stress track", + "production configuration, credential rotation and staging restore drill", + "PostgreSQL integration gates", + "complete supported Python/platform CI matrix and Docker smoke", + "target-user usability measurements", + "bounded pilot and operational ownership" + ], + "authorizations": { + "paid_runs_executed": false, + "deployments": false, + "merges": false, + "pushes": "authorized for reviewed PR branches; submission receipts are in GitHub", + "external_messages": false, + "tests_used_production_data": false, + "credential_rotation": false, + "pr_submission_authorized": true + }, + "notes": [ + "Skipped tests are not passes.", + "Browser engines were exercised on Windows; this is not macOS Safari or the full operating-system matrix.", + "Benchmark reports define their own source hashes, hardware, dataset and measurement boundaries. The incomplete baseline is retained as failed measurement evidence.", + "Optional cleanup of two cancelled-run synthetic temporary benchmark directories was blocked by automatic approval review with the stated reason 'blocked by policy'. No deletion occurred; exact paths/action are recorded in vector-scale-summary-20260905.md.", + "The final complete public run reports exact JUnit outcome counts. Its 37 skipped cases/reasons are recorded in public-complete-source-final.json.", + "After final production code froze, only two obsolete trial-UI test modules were corrected; the final complete public gate then passed. Source/report assembly afterward is independently recorded in the final manifest.", + "All 16 historical noncurrent branches and one stash were reconciled; no unique recovery candidate remains. Private details stay private. Branches, stash and auxiliary worktree were preserved." + ], + "latest_results": { + "public_complete": "pr-public-complete", + "cloud_complete": "pr-cloud-complete", + "offline_gates": "pr-offline-complete", + "static_and_contracts": "pr-static-contracts", + "integrations": [ + "pr-integration-final" + ], + "browser": [ + "browser-ledger-and-journey", + "browser-corrective-rerun", + "browser-processing-controls", + "trial-browser-regression", + "pr-ui-review" + ], + "edge": "pr-edge-final", + "website": "pr-website-review", + "scale": "final-scale-matrix" + } +} diff --git a/docs/evidence/reliability/vector-scale-incomplete-baseline-20260905.json b/docs/evidence/reliability/vector-scale-incomplete-baseline-20260905.json new file mode 100644 index 00000000..a0a386fe --- /dev/null +++ b/docs/evidence/reliability/vector-scale-incomplete-baseline-20260905.json @@ -0,0 +1 @@ +{"environment":{"implementation":"CPython","machine":"AMD64","packages":{"engraphis":"1.7.1","numpy":"2.4.5","sentence-transformers":"6.0.0","torch":"2.13.0","transformers":"5.15.1"},"platform":"Windows-11-10.0.26100-SP0","python":"3.12.10"},"exclusions":[],"metrics":{"completed_measurements_persisted":false,"elapsed_wall_seconds_approximate":396,"hardware":{"architecture":"AMD64","blas_thread_limits":{"MKL_NUM_THREADS":null,"OMP_NUM_THREADS":null,"OPENBLAS_NUM_THREADS":null},"cpu":"Intel(R) Core(TM) i7-10700KF CPU @ 3.80GHz","logical_cpus":16,"physical_ram_bytes":34221301760,"sqlite":"3.49.1"},"latency_samples_recovered":false,"measurement_scope":"synthetic scoped exact-index and storage operations; incomplete observation only","observed_completed_corpus_size":10000,"observed_population_checkpoint":40000,"run_started_at":"2026-09-05T02:40:29-04:00","run_stop_verified_at":"2026-09-05T02:47:05-04:00","source_snapshot_at_stop":{"files":{"engraphis/backends/vector_numpy.py":"c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72","engraphis/backends/vector_sqlitevec.py":"7c1e8457287e32d19ebbf9ec42123e5c56087e0607d60309540e6f9691ae3bca","engraphis/core/interfaces.py":"5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22","engraphis/core/schema.py":"99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686","engraphis/core/store.py":"192aea3e6ba91ce5f93a33c340a15802c1c05647bf9b731816d1034e518f6919","engraphis/core/vector_search.py":"75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234","eval/benchmark.py":"ed828e3548e8fb05ffafe9479791e545e56504421ff88676e4bf4466d6beaf91","eval/vector_scale.py":"9eefa5c387fc4648126aacbf0b0914c7874770c1b3a282e4c74e3ddd4d5ac569","eval/vector_scale_storage.py":"afd056b02b33a31b7a57f7558b872f45a7c783bda28709686d1c5882ebacc7f6"},"tracked_diff_sha256":"509fe37b82ac3c03afc96ed70ae06f967df37db7fdec179e2298e9f938d7fa0a"},"status":"incomplete","stop_reason":"operator interrupted at confirmed unindexed FTS deletion bottleneck; no complete100k result exists","target_population":100000},"models":{},"privacy":{"content_fingerprint_policy":"omitted","raw_answer_policy":"omitted","raw_context_policy":"omitted","raw_query_policy":"omitted"},"protocol":{"command":["python","-m","eval.vector_scale","--file-backed","--backend","numpy","--sizes","10000,100000","--dim","256","--queries","16","--iterations","2","--warmups","1","--k","10","--seed","20260731","--concurrencies","1,4,16","--mixed-writes","4","--batch-size","500","--tenants","4","--progress"],"config":{"backend":"numpy","batch_size":500,"concurrencies":[1,4,16],"dimension":256,"iterations":2,"k":10,"mixed_writes_per_cell":4,"queries":16,"seed":20260731,"sizes":[10000,100000],"tenant_scopes":4,"warmups":1},"n_scored":0,"n_total":0,"token_accounting":{"identity":"unspecified","method":"unspecified","revision":null,"scope":"unspecified"}},"records":[],"schema":"engraphis-benchmark/v2","suite":{"dataset":"vector_scale_storage.py","name":"file-backed-exact-index-scale/incomplete-baseline","sha256":"afd056b02b33a31b7a57f7558b872f45a7c783bda28709686d1c5882ebacc7f6","sources":[{"bytes":10319,"name":"vector_scale.py","sha256":"9eefa5c387fc4648126aacbf0b0914c7874770c1b3a282e4c74e3ddd4d5ac569"},{"bytes":21111,"name":"vector_scale_storage.py","sha256":"afd056b02b33a31b7a57f7558b872f45a7c783bda28709686d1c5882ebacc7f6"},{"bytes":60944,"name":"benchmark.py","sha256":"ed828e3548e8fb05ffafe9479791e545e56504421ff88676e4bf4466d6beaf91"},{"bytes":6163,"name":"vector_numpy.py","sha256":"c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72"},{"bytes":24225,"name":"vector_sqlitevec.py","sha256":"7c1e8457287e32d19ebbf9ec42123e5c56087e0607d60309540e6f9691ae3bca"},{"bytes":2349,"name":"vector_search.py","sha256":"75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234"},{"bytes":463995,"name":"store.py","sha256":"192aea3e6ba91ce5f93a33c340a15802c1c05647bf9b731816d1034e518f6919"},{"bytes":39996,"name":"schema.py","sha256":"99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686"},{"bytes":31040,"name":"interfaces.py","sha256":"5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22"}]},"system":{"config_sha256":"91d64091e57f1bff32b756d13675373ffb5ce351a06cb119aafdd683043ef904","dirty_state_sha256":"d803da44ddad5a3676d43fd0325d9afe4b5c7f6d476eda557b788d9ff3cc96f3","git_commit":"c37ba0eb18408fe500cd70cd73fb5dcd7679b89c","git_dirty":true}} diff --git a/docs/evidence/reliability/vector-scale-incomplete-baseline-20260905.json.sha256 b/docs/evidence/reliability/vector-scale-incomplete-baseline-20260905.json.sha256 new file mode 100644 index 00000000..ac7f7b2f --- /dev/null +++ b/docs/evidence/reliability/vector-scale-incomplete-baseline-20260905.json.sha256 @@ -0,0 +1 @@ +45c4f61518012e35e09b6d274dec3015d00d4948f2b8279c0f56760dec080544 vector-scale-incomplete-baseline-20260905.json diff --git a/docs/evidence/reliability/vector-scale-native-incomplete-20260905.json b/docs/evidence/reliability/vector-scale-native-incomplete-20260905.json new file mode 100644 index 00000000..6a42dd45 --- /dev/null +++ b/docs/evidence/reliability/vector-scale-native-incomplete-20260905.json @@ -0,0 +1 @@ +{"environment":{"implementation":"CPython","machine":"AMD64","packages":{"engraphis":"1.7.1","numpy":"2.4.5","sentence-transformers":"6.0.0","torch":"2.13.0","transformers":"5.15.1"},"platform":"Windows-11-10.0.26100-SP0","python":"3.12.10"},"exclusions":[],"metrics":{"completed_measurements_persisted":false,"elapsed_wall_seconds_approximate":430,"hardware":{"architecture":"AMD64","blas_thread_limits":{"MKL_NUM_THREADS":null,"OMP_NUM_THREADS":null,"OPENBLAS_NUM_THREADS":null},"cpu":"Intel(R) Core(TM) i7-10700KF CPU @ 3.80GHz","logical_cpus":16,"physical_ram_bytes":34221301760,"sqlite":"3.49.1"},"latency_samples_recovered":false,"measurement_scope":"synthetic scoped exact-index and storage operations; incomplete checkpoint observation only","observed_completed_mixed_rebuild_corpus_size":10000,"observed_completed_read_cells":[{"concurrency":1,"corpus_size":10000,"numpy_reference_parity":true},{"concurrency":4,"corpus_size":10000,"numpy_reference_parity":true},{"concurrency":16,"corpus_size":10000,"numpy_reference_parity":true},{"concurrency":1,"corpus_size":100000,"numpy_reference_parity":true},{"concurrency":4,"corpus_size":100000,"numpy_reference_parity":true},{"concurrency":16,"corpus_size":100000,"numpy_reference_parity":true}],"observed_population_checkpoint":100000,"run_started_at":"2026-09-05T03:03:10-04:00","run_stop_verified_at":"2026-09-05T03:10:20-04:00","source_snapshot_at_stop":{"files":{"engraphis/backends/vector_numpy.py":"c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72","engraphis/backends/vector_sqlitevec.py":"7c1e8457287e32d19ebbf9ec42123e5c56087e0607d60309540e6f9691ae3bca","engraphis/core/interfaces.py":"5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22","engraphis/core/schema.py":"99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686","engraphis/core/store.py":"df32758d9b546a7755b29df6cdfcf163e426ddd020e23c6b0fcf59741949e404","engraphis/core/vector_search.py":"75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234","eval/benchmark.py":"ed828e3548e8fb05ffafe9479791e545e56504421ff88676e4bf4466d6beaf91","eval/vector_scale.py":"9eefa5c387fc4648126aacbf0b0914c7874770c1b3a282e4c74e3ddd4d5ac569","eval/vector_scale_storage.py":"afd056b02b33a31b7a57f7558b872f45a7c783bda28709686d1c5882ebacc7f6"},"tracked_diff_sha256":"226ee92b9962f3c26cca08aa8168139c2121be04bfab13755b92ac10fb8a1790"},"status":"incomplete","stop_reason":"operator interrupted repeated native coverage verification before100k mixed/rebuild/reopen completion","target_population":100000},"models":{},"privacy":{"content_fingerprint_policy":"omitted","raw_answer_policy":"omitted","raw_context_policy":"omitted","raw_query_policy":"omitted"},"protocol":{"command":["python","-m","eval.vector_scale","--file-backed","--backend","sqlite-vec","--sizes","10000,100000","--dim","256","--queries","16","--iterations","2","--warmups","1","--k","10","--seed","20260731","--concurrencies","1,4,16","--mixed-writes","4","--batch-size","500","--tenants","4","--progress"],"config":{"backend":"sqlite-vec","batch_size":500,"concurrencies":[1,4,16],"dimension":256,"iterations":2,"k":10,"mixed_writes_per_cell":4,"queries":16,"seed":20260731,"sizes":[10000,100000],"tenant_scopes":4,"warmups":1},"n_scored":0,"n_total":0,"token_accounting":{"identity":"unspecified","method":"unspecified","revision":null,"scope":"unspecified"}},"records":[],"schema":"engraphis-benchmark/v2","suite":{"dataset":"vector_scale_storage.py","name":"file-backed-exact-index-scale/incomplete-native-baseline","sha256":"afd056b02b33a31b7a57f7558b872f45a7c783bda28709686d1c5882ebacc7f6","sources":[{"bytes":10319,"name":"vector_scale.py","sha256":"9eefa5c387fc4648126aacbf0b0914c7874770c1b3a282e4c74e3ddd4d5ac569"},{"bytes":21111,"name":"vector_scale_storage.py","sha256":"afd056b02b33a31b7a57f7558b872f45a7c783bda28709686d1c5882ebacc7f6"},{"bytes":60944,"name":"benchmark.py","sha256":"ed828e3548e8fb05ffafe9479791e545e56504421ff88676e4bf4466d6beaf91"},{"bytes":6163,"name":"vector_numpy.py","sha256":"c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72"},{"bytes":24225,"name":"vector_sqlitevec.py","sha256":"7c1e8457287e32d19ebbf9ec42123e5c56087e0607d60309540e6f9691ae3bca"},{"bytes":2349,"name":"vector_search.py","sha256":"75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234"},{"bytes":465730,"name":"store.py","sha256":"df32758d9b546a7755b29df6cdfcf163e426ddd020e23c6b0fcf59741949e404"},{"bytes":39996,"name":"schema.py","sha256":"99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686"},{"bytes":31040,"name":"interfaces.py","sha256":"5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22"}]},"system":{"config_sha256":"adf3f031583c6e43e293ebc2d1d5130499cfbafe7622cb55a2398ad49824364e","dirty_state_sha256":"07c4221ff756b64bbc20cc5618b3c754d38f24d14d31e91b7c77bc65e869819b","git_commit":"c37ba0eb18408fe500cd70cd73fb5dcd7679b89c","git_dirty":true}} diff --git a/docs/evidence/reliability/vector-scale-native-incomplete-20260905.json.sha256 b/docs/evidence/reliability/vector-scale-native-incomplete-20260905.json.sha256 new file mode 100644 index 00000000..bea4b246 --- /dev/null +++ b/docs/evidence/reliability/vector-scale-native-incomplete-20260905.json.sha256 @@ -0,0 +1 @@ +1b73068e071f4e39b644522a25512282fe986fda9ad5724ad44369eb25a3ceb1 vector-scale-native-incomplete-20260905.json diff --git a/docs/evidence/reliability/vector-scale-numpy-20260905.json b/docs/evidence/reliability/vector-scale-numpy-20260905.json new file mode 100644 index 00000000..93ad5639 --- /dev/null +++ b/docs/evidence/reliability/vector-scale-numpy-20260905.json @@ -0,0 +1 @@ +{"environment":{"implementation":"CPython","machine":"AMD64","packages":{"engraphis":"1.7.1","numpy":"2.4.5","sentence-transformers":"6.0.0","torch":"2.13.0","transformers":"5.15.1"},"platform":"Windows-11-10.0.26100-SP0","python":"3.12.10"},"exclusions":[],"metrics":{"cells":[{"concurrency":1,"corpus_size":10000,"latency_ms":{"max":85.757,"mean":70.653,"min":60.543,"p50":67.437,"p95":81.39,"p99":84.468},"memory":{"process_lifetime_peak_rss_bytes":58044416,"rss_bytes":53174272},"numpy_reference_parity":true,"result_counts":[10],"result_ids_sha256":"b6db29e54c8e5a42c4cc996a7ec87bf90c6d6665829c4e03244ed7abe5f28a12","searches_per_second":14.144,"status":"complete","timed_searches":32,"wall_seconds":2.262508},{"concurrency":4,"corpus_size":10000,"latency_ms":{"max":327.758,"mean":283.764,"min":71.191,"p50":294.085,"p95":326.265,"p99":327.588},"memory":{"process_lifetime_peak_rss_bytes":58298368,"rss_bytes":53407744},"numpy_reference_parity":true,"result_counts":[10],"result_ids_sha256":"b6db29e54c8e5a42c4cc996a7ec87bf90c6d6665829c4e03244ed7abe5f28a12","searches_per_second":13.462,"status":"complete","timed_searches":32,"wall_seconds":2.377019},{"concurrency":16,"corpus_size":10000,"latency_ms":{"max":1241.372,"mean":916.716,"min":76.801,"p50":1104.839,"p95":1230.65,"p99":1239.658},"memory":{"process_lifetime_peak_rss_bytes":59551744,"rss_bytes":53506048},"numpy_reference_parity":true,"result_counts":[10],"result_ids_sha256":"b6db29e54c8e5a42c4cc996a7ec87bf90c6d6665829c4e03244ed7abe5f28a12","searches_per_second":13.697,"status":"complete","timed_searches":32,"wall_seconds":2.336337},{"concurrency":1,"corpus_size":100000,"latency_ms":{"max":2110.035,"mean":2004.287,"min":1872.601,"p50":2019.72,"p95":2078.035,"p99":2103.358},"memory":{"process_lifetime_peak_rss_bytes":64704512,"rss_bytes":53321728},"numpy_reference_parity":true,"result_counts":[10],"result_ids_sha256":"591227e294f2f039a6147720544c87c3bf66ad0310871256e9340d49d92cf220","searches_per_second":0.499,"status":"complete","timed_searches":32,"wall_seconds":64.13878},{"concurrency":4,"corpus_size":100000,"latency_ms":{"max":8494.676,"mean":7806.754,"min":2169.744,"p50":8093.907,"p95":8483.261,"p99":8491.611},"memory":{"process_lifetime_peak_rss_bytes":65224704,"rss_bytes":53649408},"numpy_reference_parity":true,"result_counts":[10],"result_ids_sha256":"591227e294f2f039a6147720544c87c3bf66ad0310871256e9340d49d92cf220","searches_per_second":0.489,"status":"complete","timed_searches":32,"wall_seconds":65.476339},{"concurrency":16,"corpus_size":100000,"latency_ms":{"max":32695.857,"mean":24068.743,"min":1940.019,"p50":30990.504,"p95":32500.637,"p99":32650.491},"memory":{"process_lifetime_peak_rss_bytes":67108864,"rss_bytes":54341632},"numpy_reference_parity":true,"result_counts":[10],"result_ids_sha256":"591227e294f2f039a6147720544c87c3bf66ad0310871256e9340d49d92cf220","searches_per_second":0.503,"status":"complete","timed_searches":32,"wall_seconds":63.671269}],"cold_scope":"new connection after population; operating-system disk cache is not flushed","hardware":{"architecture":"AMD64","blas_thread_limits":{"MKL_NUM_THREADS":"1","OMP_NUM_THREADS":"1","OPENBLAS_NUM_THREADS":"1"},"cpu":"Intel(R) Core(TM) i7-10700KF CPU @ 3.80GHz","logical_cpus":16,"physical_ram_bytes":34221301760,"sqlite":"3.49.1"},"measurement_scope":"synthetic scoped exact-index and storage operations; not end-to-end recall","percentile_scope":"descriptive samples, not tail-SLO confidence bounds","source_after":{"files":{"engraphis/backends/vector_numpy.py":"c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72","engraphis/backends/vector_sqlitevec.py":"7c1e8457287e32d19ebbf9ec42123e5c56087e0607d60309540e6f9691ae3bca","engraphis/core/interfaces.py":"5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22","engraphis/core/schema.py":"99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686","engraphis/core/store.py":"df32758d9b546a7755b29df6cdfcf163e426ddd020e23c6b0fcf59741949e404","engraphis/core/vector_search.py":"75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234","eval/benchmark.py":"ed828e3548e8fb05ffafe9479791e545e56504421ff88676e4bf4466d6beaf91","eval/vector_scale.py":"9eefa5c387fc4648126aacbf0b0914c7874770c1b3a282e4c74e3ddd4d5ac569","eval/vector_scale_storage.py":"afd056b02b33a31b7a57f7558b872f45a7c783bda28709686d1c5882ebacc7f6"},"tracked_diff_sha256":"226ee92b9962f3c26cca08aa8168139c2121be04bfab13755b92ac10fb8a1790"},"source_before":{"files":{"engraphis/backends/vector_numpy.py":"c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72","engraphis/backends/vector_sqlitevec.py":"7c1e8457287e32d19ebbf9ec42123e5c56087e0607d60309540e6f9691ae3bca","engraphis/core/interfaces.py":"5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22","engraphis/core/schema.py":"99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686","engraphis/core/store.py":"df32758d9b546a7755b29df6cdfcf163e426ddd020e23c6b0fcf59741949e404","engraphis/core/vector_search.py":"75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234","eval/benchmark.py":"ed828e3548e8fb05ffafe9479791e545e56504421ff88676e4bf4466d6beaf91","eval/vector_scale.py":"9eefa5c387fc4648126aacbf0b0914c7874770c1b3a282e4c74e3ddd4d5ac569","eval/vector_scale_storage.py":"afd056b02b33a31b7a57f7558b872f45a7c783bda28709686d1c5882ebacc7f6"},"tracked_diff_sha256":"226ee92b9962f3c26cca08aa8168139c2121be04bfab13755b92ac10fb8a1790"},"source_stable":true,"storage":[{"connection_cold_reads":{"latency_ms":{"max":87.559,"mean":70.343,"min":64.114,"p50":69.328,"p95":75.943,"p99":85.236},"memory":{"process_lifetime_peak_rss_bytes":57634816,"rss_bytes":52011008},"result_counts":[10],"result_ids_sha256":"44124c7e868aee3911730e181c9bc684258ab1ba3f0fd101e9ecd66385c4b179","searches_per_second":14.197,"timed_searches":16,"wall_seconds":1.126975},"corpus_size":10000,"durable_memory_rows":10012,"durable_vector_rows":10012,"final_disk":{"database_bytes":22495232,"shared_memory_bytes":32768,"total_bytes":22556872,"wal_bytes":28872},"initial_startup_ms":30.473300023004413,"memory":{"process_lifetime_peak_rss_bytes":59682816,"rss_bytes":49524736},"mixed":[{"committed_writes":4,"measured":true,"reader_concurrency":1,"reads":{"latency_ms":{"max":81.455,"mean":75.5,"min":67.151,"p50":78.051,"p95":81.317,"p99":81.428},"memory":{"process_lifetime_peak_rss_bytes":59551744,"rss_bytes":53710848},"result_counts":[10],"result_ids_sha256":"44124c7e868aee3911730e181c9bc684258ab1ba3f0fd101e9ecd66385c4b179","searches_per_second":13.234,"timed_searches":16,"wall_seconds":1.208977},"starting_corpus_size":10000,"wall_seconds":1.209789,"write_latency_ms":{"max":78.113,"mean":55.955,"min":8.427,"p50":68.639,"p95":76.715,"p99":77.834},"writer_concurrency":1},{"committed_writes":4,"measured":true,"reader_concurrency":4,"reads":{"latency_ms":{"max":380.11,"mean":282.915,"min":75.075,"p50":278.631,"p95":378.227,"p99":379.734},"memory":{"process_lifetime_peak_rss_bytes":59551744,"rss_bytes":53907456},"result_counts":[10],"result_ids_sha256":"44124c7e868aee3911730e181c9bc684258ab1ba3f0fd101e9ecd66385c4b179","searches_per_second":12.95,"timed_searches":16,"wall_seconds":1.23556},"starting_corpus_size":10004,"wall_seconds":1.236116,"write_latency_ms":{"max":344.175,"mean":239.516,"min":1.364,"p50":306.263,"p95":342.988,"p99":343.938},"writer_concurrency":1},{"committed_writes":4,"measured":true,"reader_concurrency":16,"reads":{"latency_ms":{"max":1211.088,"mean":621.986,"min":67.634,"p50":614.73,"p95":1143.252,"p99":1197.521},"memory":{"process_lifetime_peak_rss_bytes":59682816,"rss_bytes":54427648},"result_counts":[10],"result_ids_sha256":"44124c7e868aee3911730e181c9bc684258ab1ba3f0fd101e9ecd66385c4b179","searches_per_second":13.152,"timed_searches":16,"wall_seconds":1.21656},"starting_corpus_size":10008,"wall_seconds":1.217718,"write_latency_ms":{"max":710.844,"mean":304.276,"min":0.551,"p50":252.855,"p95":679.836,"p99":704.642},"writer_concurrency":1}],"populated_disk":{"database_bytes":21364736,"shared_memory_bytes":32768,"total_bytes":26831816,"wal_bytes":5434312},"population_batch_latency_ms":{"max":2400.662,"mean":262.844,"min":96.812,"p50":123.89,"p95":520.842,"p99":2024.698},"population_records_per_second":1882.6394567482353,"population_seconds":5.311691500013694,"rebuild":{"measured":false,"reason":"NumPy reads canonical vectors; no separate mirror"},"restart_ms":15.599300037138164},{"connection_cold_reads":{"latency_ms":{"max":2355.707,"mean":2134.061,"min":2029.694,"p50":2131.529,"p95":2286.915,"p99":2341.949},"memory":{"process_lifetime_peak_rss_bytes":63971328,"rss_bytes":53559296},"result_counts":[10],"result_ids_sha256":"822335c92dc337b652c12230419675d4c4bdffa303fae8bcd00de192e9f6c96c","searches_per_second":0.469,"timed_searches":16,"wall_seconds":34.146019},"corpus_size":100000,"durable_memory_rows":100012,"durable_vector_rows":100012,"final_disk":{"database_bytes":219684864,"shared_memory_bytes":32768,"total_bytes":219746504,"wal_bytes":28872},"initial_startup_ms":38.85669994633645,"memory":{"process_lifetime_peak_rss_bytes":67108864,"rss_bytes":51048448},"mixed":[{"committed_writes":4,"measured":true,"reader_concurrency":1,"reads":{"latency_ms":{"max":2380.544,"mean":2152.224,"min":1951.524,"p50":2142.928,"p95":2304.044,"p99":2365.244},"memory":{"process_lifetime_peak_rss_bytes":67108864,"rss_bytes":55320576},"result_counts":[10],"result_ids_sha256":"822335c92dc337b652c12230419675d4c4bdffa303fae8bcd00de192e9f6c96c","searches_per_second":0.465,"timed_searches":16,"wall_seconds":34.436723},"starting_corpus_size":100000,"wall_seconds":34.43737,"write_latency_ms":{"max":2245.326,"mean":1643.157,"min":71.514,"p50":2127.895,"p95":2233.331,"p99":2242.927},"writer_concurrency":1},{"committed_writes":4,"measured":true,"reader_concurrency":4,"reads":{"latency_ms":{"max":8936.423,"mean":7613.41,"min":1976.044,"p50":8316.463,"p95":8925.755,"p99":8934.289},"memory":{"process_lifetime_peak_rss_bytes":67108864,"rss_bytes":58064896},"result_counts":[10],"result_ids_sha256":"822335c92dc337b652c12230419675d4c4bdffa303fae8bcd00de192e9f6c96c","searches_per_second":0.477,"timed_searches":16,"wall_seconds":33.552109},"starting_corpus_size":100004,"wall_seconds":33.552636,"write_latency_ms":{"max":8745.599,"mean":6310.26,"min":1.694,"p50":8246.874,"p95":8726.478,"p99":8741.775},"writer_concurrency":1},{"committed_writes":4,"measured":true,"reader_concurrency":16,"reads":{"latency_ms":{"max":35170.874,"mean":18364.868,"min":2090.28,"p50":17823.732,"p95":33602.162,"p99":34857.132},"memory":{"process_lifetime_peak_rss_bytes":67108864,"rss_bytes":54886400},"result_counts":[10],"result_ids_sha256":"822335c92dc337b652c12230419675d4c4bdffa303fae8bcd00de192e9f6c96c","searches_per_second":0.455,"timed_searches":16,"wall_seconds":35.175936},"starting_corpus_size":100008,"wall_seconds":35.177765,"write_latency_ms":{"max":18490.139,"mean":8794.318,"min":0.76,"p50":8343.187,"p95":18219.304,"p99":18435.972},"writer_concurrency":1}],"populated_disk":{"database_bytes":218550272,"shared_memory_bytes":32768,"total_bytes":224128592,"wal_bytes":5545552},"population_batch_latency_ms":{"max":290.864,"mean":140.486,"min":94.635,"p50":136.868,"p95":177.311,"p99":211.245},"population_records_per_second":3501.251450554836,"population_seconds":28.561216300004162,"rebuild":{"measured":false,"reason":"NumPy reads canonical vectors; no separate mirror"},"restart_ms":14.63390002027154}],"timing_scope":"latency is execution including storage locks, excludes executor queue; throughput includes queue","unmeasured":["embedding/model latency","extraction and conflict resolution","agent task quality","full recall and context packing","multi-process contention","independent repeated processes"]},"models":{"embedding":{"identity":"none; precomputed synthetic vectors"},"tokenizer":{"identity":"not applicable; no reader context"},"vector_backend":{"identity":"NumpyVectorIndex","native_version":null}},"privacy":{"content_fingerprint_policy":"omitted","raw_answer_policy":"omitted","raw_context_policy":"omitted","raw_query_policy":"omitted"},"protocol":{"command":["python","-m","eval.vector_scale","--file-backed","--backend","numpy","--sizes","10000,100000","--dim","256","--queries","16","--iterations","2","--warmups","1","--k","10","--seed","20260731","--concurrencies","1,4,16","--mixed-writes","4","--batch-size","500","--tenants","4","--progress"],"config":{"backend":"numpy","batch_size":500,"concurrencies":[1,4,16],"dimension":256,"file_backed":true,"inputs":[{"corpus_size":10000,"vectors_sha256":"5a3092df8330ded628164601fc11bfd57d3c08e1a8f95c364774ba8927255120"},{"corpus_size":100000,"vectors_sha256":"9dbc1a3b5b27422858fc5dbb0c16d619dbedbf90267fb72361fea69dcb5d06d3"}],"iterations":2,"k":10,"mixed_writes_per_cell":4,"queries":16,"queries_sha256":"19f8cde59c67bc91f0871cbd94bcce18e32cfaee0d30f3eb94dbd2104447d27f","seed":20260731,"sizes":[10000,100000],"tenant_scopes":4,"vector_generator":"numpy.PCG64.standard_normal.float32.normalized.v1","warmups":1},"n_scored":6,"n_total":6,"token_accounting":{"identity":"not_applicable","method":"not_measured","revision":null,"scope":"no reader context"}},"records":[{"category":"exact_index_throughput","latency_ms":67.437,"question_id":"n10000-c1"},{"category":"exact_index_throughput","latency_ms":294.085,"question_id":"n10000-c4"},{"category":"exact_index_throughput","latency_ms":1104.839,"question_id":"n10000-c16"},{"category":"exact_index_throughput","latency_ms":2019.72,"question_id":"n100000-c1"},{"category":"exact_index_throughput","latency_ms":8093.907,"question_id":"n100000-c4"},{"category":"exact_index_throughput","latency_ms":30990.504,"question_id":"n100000-c16"}],"schema":"engraphis-benchmark/v2","suite":{"dataset":"vector_scale_storage.py","name":"file-backed-exact-index-scale/v1","sha256":"afd056b02b33a31b7a57f7558b872f45a7c783bda28709686d1c5882ebacc7f6","sources":[{"bytes":10319,"name":"vector_scale.py","sha256":"9eefa5c387fc4648126aacbf0b0914c7874770c1b3a282e4c74e3ddd4d5ac569"},{"bytes":21111,"name":"vector_scale_storage.py","sha256":"afd056b02b33a31b7a57f7558b872f45a7c783bda28709686d1c5882ebacc7f6"},{"bytes":60944,"name":"benchmark.py","sha256":"ed828e3548e8fb05ffafe9479791e545e56504421ff88676e4bf4466d6beaf91"},{"bytes":6163,"name":"vector_numpy.py","sha256":"c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72"},{"bytes":24225,"name":"vector_sqlitevec.py","sha256":"7c1e8457287e32d19ebbf9ec42123e5c56087e0607d60309540e6f9691ae3bca"},{"bytes":2349,"name":"vector_search.py","sha256":"75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234"},{"bytes":465730,"name":"store.py","sha256":"df32758d9b546a7755b29df6cdfcf163e426ddd020e23c6b0fcf59741949e404"},{"bytes":39996,"name":"schema.py","sha256":"99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686"},{"bytes":31040,"name":"interfaces.py","sha256":"5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22"}]},"system":{"config_sha256":"a3ca51c21a991af9d30b8b2e05029ec3b631127b85998f898a5601de2afcf5e3","dirty_state_sha256":"07c4221ff756b64bbc20cc5618b3c754d38f24d14d31e91b7c77bc65e869819b","git_commit":"c37ba0eb18408fe500cd70cd73fb5dcd7679b89c","git_dirty":true}} diff --git a/docs/evidence/reliability/vector-scale-numpy-20260905.json.sha256 b/docs/evidence/reliability/vector-scale-numpy-20260905.json.sha256 new file mode 100644 index 00000000..a08ca57e --- /dev/null +++ b/docs/evidence/reliability/vector-scale-numpy-20260905.json.sha256 @@ -0,0 +1 @@ +049d22200d0e088f03f42a13c8a0ae4031c3c9c3c12113c3f6196855e2f0d479 vector-scale-numpy-20260905.json diff --git a/docs/evidence/reliability/vector-scale-numpy-corrected-20260905.json b/docs/evidence/reliability/vector-scale-numpy-corrected-20260905.json new file mode 100644 index 00000000..3605deb7 --- /dev/null +++ b/docs/evidence/reliability/vector-scale-numpy-corrected-20260905.json @@ -0,0 +1 @@ +{"environment":{"implementation":"CPython","machine":"AMD64","packages":{"engraphis":"1.7.1","numpy":"2.4.5","sentence-transformers":"6.0.0","torch":"2.13.0","transformers":"5.15.1"},"platform":"Windows-11-10.0.26100-SP0","python":"3.12.10"},"exclusions":[],"metrics":{"cells":[{"concurrency":1,"corpus_size":10000,"latency_ms":{"max":86.518,"mean":62.464,"min":52.325,"p50":58.316,"p95":80.819,"p99":85.61},"memory":{"process_lifetime_peak_rss_bytes":57921536,"rss_bytes":53256192},"numpy_reference_parity":true,"result_counts":[10],"result_ids_sha256":"b6db29e54c8e5a42c4cc996a7ec87bf90c6d6665829c4e03244ed7abe5f28a12","searches_per_second":15.997,"status":"complete","timed_searches":32,"wall_seconds":2.000374},{"concurrency":4,"corpus_size":10000,"latency_ms":{"max":424.912,"mean":279.46,"min":70.916,"p50":268.058,"p95":393.975,"p99":416.295},"memory":{"process_lifetime_peak_rss_bytes":58417152,"rss_bytes":52969472},"numpy_reference_parity":true,"result_counts":[10],"result_ids_sha256":"b6db29e54c8e5a42c4cc996a7ec87bf90c6d6665829c4e03244ed7abe5f28a12","searches_per_second":13.748,"status":"complete","timed_searches":32,"wall_seconds":2.327689},{"concurrency":16,"corpus_size":10000,"latency_ms":{"max":1108.798,"mean":811.986,"min":69.71,"p50":1042.093,"p95":1099.69,"p99":1107.065},"memory":{"process_lifetime_peak_rss_bytes":59781120,"rss_bytes":54218752},"numpy_reference_parity":true,"result_counts":[10],"result_ids_sha256":"b6db29e54c8e5a42c4cc996a7ec87bf90c6d6665829c4e03244ed7abe5f28a12","searches_per_second":15.236,"status":"complete","timed_searches":32,"wall_seconds":2.100254},{"concurrency":1,"corpus_size":100000,"latency_ms":{"max":554.105,"mean":494.035,"min":451.626,"p50":495.837,"p95":542.234,"p99":550.786},"memory":{"process_lifetime_peak_rss_bytes":64761856,"rss_bytes":53776384},"numpy_reference_parity":true,"result_counts":[10],"result_ids_sha256":"591227e294f2f039a6147720544c87c3bf66ad0310871256e9340d49d92cf220","searches_per_second":2.024,"status":"complete","timed_searches":32,"wall_seconds":15.810786},{"concurrency":4,"corpus_size":100000,"latency_ms":{"max":2530.736,"mean":2199.637,"min":500.397,"p50":2340.836,"p95":2500.777,"p99":2525.383},"memory":{"process_lifetime_peak_rss_bytes":64778240,"rss_bytes":53673984},"numpy_reference_parity":true,"result_counts":[10],"result_ids_sha256":"591227e294f2f039a6147720544c87c3bf66ad0310871256e9340d49d92cf220","searches_per_second":1.738,"status":"complete","timed_searches":32,"wall_seconds":18.416906},{"concurrency":16,"corpus_size":100000,"latency_ms":{"max":8942.493,"mean":6716.429,"min":526.555,"p50":8632.554,"p95":8806.378,"p99":8905.387},"memory":{"process_lifetime_peak_rss_bytes":65998848,"rss_bytes":54403072},"numpy_reference_parity":true,"result_counts":[10],"result_ids_sha256":"591227e294f2f039a6147720544c87c3bf66ad0310871256e9340d49d92cf220","searches_per_second":1.811,"status":"complete","timed_searches":32,"wall_seconds":17.67229}],"cold_scope":"new connection after population; operating-system disk cache is not flushed","hardware":{"architecture":"AMD64","blas_thread_limits":{"MKL_NUM_THREADS":"1","OMP_NUM_THREADS":"1","OPENBLAS_NUM_THREADS":"1"},"cpu":"Intel(R) Core(TM) i7-10700KF CPU @ 3.80GHz","logical_cpus":16,"physical_ram_bytes":34221301760,"sqlite":"3.49.1"},"measurement_scope":"synthetic scoped exact-index and storage operations; not end-to-end recall","percentile_scope":"descriptive samples, not tail-SLO confidence bounds","source_after":{"files":{"engraphis/backends/vector_numpy.py":"c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72","engraphis/backends/vector_sqlitevec.py":"6148e14ceaacc19239b64c642a3fba0e98797c78cec356210157afadf475a08b","engraphis/core/interfaces.py":"5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22","engraphis/core/schema.py":"99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686","engraphis/core/store.py":"007272ec42011faaae25bdbaf8905cf0d2945bf6f451b60aa7a90fff0550debd","engraphis/core/vector_search.py":"75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234","eval/benchmark.py":"ed828e3548e8fb05ffafe9479791e545e56504421ff88676e4bf4466d6beaf91","eval/vector_scale.py":"3f9c327d9eca1a857512ddc208e972933be1a7d4fa7fa0017aca0cbf8fe7bb6d","eval/vector_scale_storage.py":"bda0c1139596eb9232cd75c826fc941101b6116bae32ad7895525d43f9d35484"},"tracked_diff_sha256":"b5de7d748eb33d21b2ccbed2d84b81216f40e479eea82ecccc4a1c747177b1fe"},"source_before":{"files":{"engraphis/backends/vector_numpy.py":"c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72","engraphis/backends/vector_sqlitevec.py":"6148e14ceaacc19239b64c642a3fba0e98797c78cec356210157afadf475a08b","engraphis/core/interfaces.py":"5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22","engraphis/core/schema.py":"99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686","engraphis/core/store.py":"007272ec42011faaae25bdbaf8905cf0d2945bf6f451b60aa7a90fff0550debd","engraphis/core/vector_search.py":"75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234","eval/benchmark.py":"ed828e3548e8fb05ffafe9479791e545e56504421ff88676e4bf4466d6beaf91","eval/vector_scale.py":"3f9c327d9eca1a857512ddc208e972933be1a7d4fa7fa0017aca0cbf8fe7bb6d","eval/vector_scale_storage.py":"bda0c1139596eb9232cd75c826fc941101b6116bae32ad7895525d43f9d35484"},"tracked_diff_sha256":"b5de7d748eb33d21b2ccbed2d84b81216f40e479eea82ecccc4a1c747177b1fe"},"source_stable":true,"storage":[{"connection_cold_reads":{"latency_ms":{"max":65.907,"mean":59.991,"min":54.544,"p50":60.295,"p95":64.345,"p99":65.595},"memory":{"process_lifetime_peak_rss_bytes":56983552,"rss_bytes":51535872},"result_counts":[10],"result_ids_sha256":"44124c7e868aee3911730e181c9bc684258ab1ba3f0fd101e9ecd66385c4b179","searches_per_second":16.644,"timed_searches":16,"wall_seconds":0.961314},"corpus_size":10000,"durable_memory_rows":10012,"durable_vector_rows":10012,"final_disk":{"database_bytes":22499328,"shared_memory_bytes":32768,"total_bytes":22560968,"wal_bytes":28872},"initial_startup_ms":28.210400021634996,"memory":{"process_lifetime_peak_rss_bytes":60354560,"rss_bytes":49893376},"mixed":[{"committed_writes":4,"measured":true,"reader_concurrency":1,"reads":{"latency_ms":{"max":67.103,"mean":60.98,"min":55.242,"p50":61.01,"p95":66.503,"p99":66.983},"memory":{"process_lifetime_peak_rss_bytes":59781120,"rss_bytes":53952512},"result_counts":[10],"result_ids_sha256":"44124c7e868aee3911730e181c9bc684258ab1ba3f0fd101e9ecd66385c4b179","searches_per_second":16.384,"timed_searches":16,"wall_seconds":0.976561},"starting_corpus_size":10000,"wall_seconds":0.977154,"write_latency_ms":{"max":61.263,"mean":46.209,"min":8.438,"p50":57.568,"p95":60.726,"p99":61.155},"writer_concurrency":1},{"committed_writes":4,"measured":true,"reader_concurrency":4,"reads":{"latency_ms":{"max":252.022,"mean":213.562,"min":59.253,"p50":230.828,"p95":250.557,"p99":251.729},"memory":{"process_lifetime_peak_rss_bytes":59781120,"rss_bytes":53506048},"result_counts":[10],"result_ids_sha256":"44124c7e868aee3911730e181c9bc684258ab1ba3f0fd101e9ecd66385c4b179","searches_per_second":16.967,"timed_searches":16,"wall_seconds":0.943004},"starting_corpus_size":10004,"wall_seconds":0.943536,"write_latency_ms":{"max":238.864,"mean":173.809,"min":1.854,"p50":227.26,"p95":237.154,"p99":238.522},"writer_concurrency":1},{"committed_writes":4,"measured":true,"reader_concurrency":16,"reads":{"latency_ms":{"max":1026.002,"mean":523.295,"min":62.885,"p50":528.255,"p95":961.893,"p99":1013.181},"memory":{"process_lifetime_peak_rss_bytes":60354560,"rss_bytes":54607872},"result_counts":[10],"result_ids_sha256":"44124c7e868aee3911730e181c9bc684258ab1ba3f0fd101e9ecd66385c4b179","searches_per_second":15.509,"timed_searches":16,"wall_seconds":1.031646},"starting_corpus_size":10008,"wall_seconds":1.032751,"write_latency_ms":{"max":530.307,"mean":258.038,"min":0.516,"p50":250.665,"p95":525.618,"p99":529.369},"writer_concurrency":1}],"populated_disk":{"database_bytes":21368832,"shared_memory_bytes":32768,"total_bytes":26831792,"wal_bytes":5430192},"population_batch_latency_ms":{"max":194.463,"mean":140.736,"min":107.511,"p50":139.033,"p95":182.206,"p99":192.011},"population_records_per_second":3473.7830122421715,"population_seconds":2.878705999988597,"rebuild":{"measured":false,"reason":"NumPy reads canonical vectors; no separate mirror"},"restart_ms":14.706699992530048},{"connection_cold_reads":{"latency_ms":{"max":586.684,"mean":498.281,"min":456.637,"p50":497.695,"p95":551.507,"p99":579.648},"memory":{"process_lifetime_peak_rss_bytes":64212992,"rss_bytes":53878784},"result_counts":[10],"result_ids_sha256":"822335c92dc337b652c12230419675d4c4bdffa303fae8bcd00de192e9f6c96c","searches_per_second":2.007,"timed_searches":16,"wall_seconds":7.97373},"corpus_size":100000,"durable_memory_rows":100012,"durable_vector_rows":100012,"final_disk":{"database_bytes":219824128,"shared_memory_bytes":32768,"total_bytes":219885768,"wal_bytes":28872},"initial_startup_ms":26.410100050270557,"memory":{"process_lifetime_peak_rss_bytes":66203648,"rss_bytes":51109888},"mixed":[{"committed_writes":4,"measured":true,"reader_concurrency":1,"reads":{"latency_ms":{"max":690.216,"mean":626.949,"min":547.583,"p50":640.612,"p95":689.457,"p99":690.065},"memory":{"process_lifetime_peak_rss_bytes":65998848,"rss_bytes":54222848},"result_counts":[10],"result_ids_sha256":"822335c92dc337b652c12230419675d4c4bdffa303fae8bcd00de192e9f6c96c","searches_per_second":1.595,"timed_searches":16,"wall_seconds":10.032556},"starting_corpus_size":100000,"wall_seconds":10.033163,"write_latency_ms":{"max":590.57,"mean":459.99,"min":81.183,"p50":584.104,"p95":590.26,"p99":590.508},"writer_concurrency":1},{"committed_writes":4,"measured":true,"reader_concurrency":4,"reads":{"latency_ms":{"max":2651.471,"mean":2146.798,"min":645.79,"p50":2271.882,"p95":2609.469,"p99":2643.071},"memory":{"process_lifetime_peak_rss_bytes":65998848,"rss_bytes":59609088},"result_counts":[10],"result_ids_sha256":"822335c92dc337b652c12230419675d4c4bdffa303fae8bcd00de192e9f6c96c","searches_per_second":1.711,"timed_searches":16,"wall_seconds":9.35238},"starting_corpus_size":100004,"wall_seconds":9.352831,"write_latency_ms":{"max":2593.644,"mean":1807.927,"min":4.396,"p50":2316.833,"p95":2553.175,"p99":2585.55},"writer_concurrency":1},{"committed_writes":4,"measured":true,"reader_concurrency":16,"reads":{"latency_ms":{"max":8189.788,"mean":4359.544,"min":538.121,"p50":4356.733,"p95":7828.967,"p99":8117.623},"memory":{"process_lifetime_peak_rss_bytes":66203648,"rss_bytes":54431744},"result_counts":[10],"result_ids_sha256":"822335c92dc337b652c12230419675d4c4bdffa303fae8bcd00de192e9f6c96c","searches_per_second":1.952,"timed_searches":16,"wall_seconds":8.195657},"starting_corpus_size":100008,"wall_seconds":8.197211,"write_latency_ms":{"max":4638.286,"mean":2049.139,"min":0.588,"p50":1778.842,"p95":4475.82,"p99":4605.793},"writer_concurrency":1}],"populated_disk":{"database_bytes":217600000,"shared_memory_bytes":32768,"total_bytes":223215400,"wal_bytes":5582632},"population_batch_latency_ms":{"max":1491.131,"mean":153.56,"min":94.885,"p50":140.803,"p95":189.817,"p99":335.424},"population_records_per_second":3206.2426827560166,"population_seconds":31.189154999970924,"rebuild":{"measured":false,"reason":"NumPy reads canonical vectors; no separate mirror"},"restart_ms":14.48099996196106}],"timing_scope":"latency is execution including storage locks, excludes executor queue; throughput includes queue","unmeasured":["embedding/model latency","extraction and conflict resolution","agent task quality","full recall and context packing","multi-process contention","independent repeated processes"]},"models":{"embedding":{"identity":"none; precomputed synthetic vectors"},"tokenizer":{"identity":"not applicable; no reader context"},"vector_backend":{"identity":"NumpyVectorIndex","native_version":null}},"privacy":{"content_fingerprint_policy":"omitted","raw_answer_policy":"omitted","raw_context_policy":"omitted","raw_query_policy":"omitted"},"protocol":{"command":["python","-m","eval.vector_scale","--file-backed","--backend","numpy","--sizes","10000,100000","--dim","256","--queries","16","--iterations","2","--warmups","1","--k","10","--seed","20260731","--concurrencies","1,4,16","--mixed-writes","4","--batch-size","500","--tenants","4","--progress"],"config":{"backend":"numpy","batch_size":500,"concurrencies":[1,4,16],"dimension":256,"file_backed":true,"inputs":[{"corpus_size":10000,"vectors_sha256":"5a3092df8330ded628164601fc11bfd57d3c08e1a8f95c364774ba8927255120"},{"corpus_size":100000,"vectors_sha256":"9dbc1a3b5b27422858fc5dbb0c16d619dbedbf90267fb72361fea69dcb5d06d3"}],"iterations":2,"k":10,"mixed_writes_per_cell":4,"queries":16,"queries_sha256":"19f8cde59c67bc91f0871cbd94bcce18e32cfaee0d30f3eb94dbd2104447d27f","seed":20260731,"sizes":[10000,100000],"tenant_scopes":4,"vector_generator":"numpy.PCG64.standard_normal.float32.normalized.v1","warmups":1},"n_scored":6,"n_total":6,"token_accounting":{"identity":"not_applicable","method":"not_measured","revision":null,"scope":"no reader context"}},"records":[{"category":"exact_index_throughput","latency_ms":58.316,"question_id":"n10000-c1"},{"category":"exact_index_throughput","latency_ms":268.058,"question_id":"n10000-c4"},{"category":"exact_index_throughput","latency_ms":1042.093,"question_id":"n10000-c16"},{"category":"exact_index_throughput","latency_ms":495.837,"question_id":"n100000-c1"},{"category":"exact_index_throughput","latency_ms":2340.836,"question_id":"n100000-c4"},{"category":"exact_index_throughput","latency_ms":8632.554,"question_id":"n100000-c16"}],"schema":"engraphis-benchmark/v2","suite":{"dataset":"vector_scale_storage.py","name":"file-backed-exact-index-scale/v1","sha256":"bda0c1139596eb9232cd75c826fc941101b6116bae32ad7895525d43f9d35484","sources":[{"bytes":11055,"name":"vector_scale.py","sha256":"3f9c327d9eca1a857512ddc208e972933be1a7d4fa7fa0017aca0cbf8fe7bb6d"},{"bytes":22744,"name":"vector_scale_storage.py","sha256":"bda0c1139596eb9232cd75c826fc941101b6116bae32ad7895525d43f9d35484"},{"bytes":60944,"name":"benchmark.py","sha256":"ed828e3548e8fb05ffafe9479791e545e56504421ff88676e4bf4466d6beaf91"},{"bytes":6163,"name":"vector_numpy.py","sha256":"c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72"},{"bytes":23317,"name":"vector_sqlitevec.py","sha256":"6148e14ceaacc19239b64c642a3fba0e98797c78cec356210157afadf475a08b"},{"bytes":2349,"name":"vector_search.py","sha256":"75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234"},{"bytes":466184,"name":"store.py","sha256":"007272ec42011faaae25bdbaf8905cf0d2945bf6f451b60aa7a90fff0550debd"},{"bytes":39996,"name":"schema.py","sha256":"99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686"},{"bytes":31040,"name":"interfaces.py","sha256":"5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22"}]},"system":{"config_sha256":"a3ca51c21a991af9d30b8b2e05029ec3b631127b85998f898a5601de2afcf5e3","dirty_state_sha256":"db6f20cf3f47efec0ae20b34ab4a298eb9350e09284968af1d9605dacd01e18d","git_commit":"c37ba0eb18408fe500cd70cd73fb5dcd7679b89c","git_dirty":true}} diff --git a/docs/evidence/reliability/vector-scale-numpy-corrected-20260905.json.checkpoint.json b/docs/evidence/reliability/vector-scale-numpy-corrected-20260905.json.checkpoint.json new file mode 100644 index 00000000..c5df0eb6 --- /dev/null +++ b/docs/evidence/reliability/vector-scale-numpy-corrected-20260905.json.checkpoint.json @@ -0,0 +1 @@ +{"artifact":"vector-scale-numpy-corrected-20260905.json","schema":"engraphis-scale-checkpoint/v1","sha256":"ab4ba40faad58b7ecd3aff96a50ac45742f78f670ed79bed32b1802162d54e6b","status":"complete"} diff --git a/docs/evidence/reliability/vector-scale-numpy-corrected-20260905.json.sha256 b/docs/evidence/reliability/vector-scale-numpy-corrected-20260905.json.sha256 new file mode 100644 index 00000000..7d83e66e --- /dev/null +++ b/docs/evidence/reliability/vector-scale-numpy-corrected-20260905.json.sha256 @@ -0,0 +1 @@ +ab4ba40faad58b7ecd3aff96a50ac45742f78f670ed79bed32b1802162d54e6b vector-scale-numpy-corrected-20260905.json diff --git a/docs/evidence/reliability/vector-scale-sqlite-vec-corrected-20260905.json b/docs/evidence/reliability/vector-scale-sqlite-vec-corrected-20260905.json new file mode 100644 index 00000000..43ac447e --- /dev/null +++ b/docs/evidence/reliability/vector-scale-sqlite-vec-corrected-20260905.json @@ -0,0 +1 @@ +{"environment":{"implementation":"CPython","machine":"AMD64","packages":{"engraphis":"1.7.1","numpy":"2.4.5","sentence-transformers":"6.0.0","torch":"2.13.0","transformers":"5.15.1"},"platform":"Windows-11-10.0.26100-SP0","python":"3.12.10"},"exclusions":[],"metrics":{"cells":[{"concurrency":1,"corpus_size":10000,"latency_ms":{"max":58.321,"mean":41.975,"min":24.654,"p50":39.591,"p95":56.319,"p99":57.81},"memory":{"process_lifetime_peak_rss_bytes":56217600,"rss_bytes":51625984},"numpy_reference_parity":true,"result_counts":[10],"result_ids_sha256":"b6db29e54c8e5a42c4cc996a7ec87bf90c6d6665829c4e03244ed7abe5f28a12","searches_per_second":23.79,"status":"complete","timed_searches":32,"wall_seconds":1.345099},{"concurrency":4,"corpus_size":10000,"latency_ms":{"max":210.4,"mean":158.0,"min":72.558,"p50":156.052,"p95":199.244,"p99":207.034},"memory":{"process_lifetime_peak_rss_bytes":56217600,"rss_bytes":51793920},"numpy_reference_parity":true,"result_counts":[10],"result_ids_sha256":"b6db29e54c8e5a42c4cc996a7ec87bf90c6d6665829c4e03244ed7abe5f28a12","searches_per_second":24.832,"status":"complete","timed_searches":32,"wall_seconds":1.288638},{"concurrency":16,"corpus_size":10000,"latency_ms":{"max":806.415,"mean":614.418,"min":347.161,"p50":610.213,"p95":792.479,"p99":806.125},"memory":{"process_lifetime_peak_rss_bytes":56217600,"rss_bytes":51253248},"numpy_reference_parity":true,"result_counts":[10],"result_ids_sha256":"b6db29e54c8e5a42c4cc996a7ec87bf90c6d6665829c4e03244ed7abe5f28a12","searches_per_second":23.99,"status":"complete","timed_searches":32,"wall_seconds":1.333894},{"concurrency":1,"corpus_size":100000,"latency_ms":{"max":387.628,"mean":356.122,"min":328.728,"p50":357.1,"p95":382.293,"p99":387.018},"memory":{"process_lifetime_peak_rss_bytes":62771200,"rss_bytes":53465088},"numpy_reference_parity":true,"result_counts":[10],"result_ids_sha256":"591227e294f2f039a6147720544c87c3bf66ad0310871256e9340d49d92cf220","searches_per_second":2.808,"status":"complete","timed_searches":32,"wall_seconds":11.397663},{"concurrency":4,"corpus_size":100000,"latency_ms":{"max":1747.364,"mean":1516.893,"min":1340.483,"p50":1487.517,"p95":1703.431,"p99":1736.983},"memory":{"process_lifetime_peak_rss_bytes":62771200,"rss_bytes":53592064},"numpy_reference_parity":true,"result_counts":[10],"result_ids_sha256":"591227e294f2f039a6147720544c87c3bf66ad0310871256e9340d49d92cf220","searches_per_second":2.619,"status":"complete","timed_searches":32,"wall_seconds":12.220653},{"concurrency":16,"corpus_size":100000,"latency_ms":{"max":7430.431,"mean":5230.052,"min":2169.437,"p50":5296.068,"p95":6475.36,"p99":7147.615},"memory":{"process_lifetime_peak_rss_bytes":62771200,"rss_bytes":52330496},"numpy_reference_parity":true,"result_counts":[10],"result_ids_sha256":"591227e294f2f039a6147720544c87c3bf66ad0310871256e9340d49d92cf220","searches_per_second":2.841,"status":"complete","timed_searches":32,"wall_seconds":11.262027}],"cold_scope":"new connection after population; operating-system disk cache is not flushed","hardware":{"architecture":"AMD64","blas_thread_limits":{"MKL_NUM_THREADS":"1","OMP_NUM_THREADS":"1","OPENBLAS_NUM_THREADS":"1"},"cpu":"Intel(R) Core(TM) i7-10700KF CPU @ 3.80GHz","logical_cpus":16,"physical_ram_bytes":34221301760,"sqlite":"3.49.1"},"measurement_scope":"synthetic scoped exact-index and storage operations; not end-to-end recall","percentile_scope":"descriptive samples, not tail-SLO confidence bounds","source_after":{"files":{"engraphis/backends/vector_numpy.py":"c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72","engraphis/backends/vector_sqlitevec.py":"6148e14ceaacc19239b64c642a3fba0e98797c78cec356210157afadf475a08b","engraphis/core/interfaces.py":"5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22","engraphis/core/schema.py":"99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686","engraphis/core/store.py":"007272ec42011faaae25bdbaf8905cf0d2945bf6f451b60aa7a90fff0550debd","engraphis/core/vector_search.py":"75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234","eval/benchmark.py":"ed828e3548e8fb05ffafe9479791e545e56504421ff88676e4bf4466d6beaf91","eval/vector_scale.py":"3f9c327d9eca1a857512ddc208e972933be1a7d4fa7fa0017aca0cbf8fe7bb6d","eval/vector_scale_storage.py":"bda0c1139596eb9232cd75c826fc941101b6116bae32ad7895525d43f9d35484"},"tracked_diff_sha256":"b5de7d748eb33d21b2ccbed2d84b81216f40e479eea82ecccc4a1c747177b1fe"},"source_before":{"files":{"engraphis/backends/vector_numpy.py":"c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72","engraphis/backends/vector_sqlitevec.py":"6148e14ceaacc19239b64c642a3fba0e98797c78cec356210157afadf475a08b","engraphis/core/interfaces.py":"5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22","engraphis/core/schema.py":"99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686","engraphis/core/store.py":"007272ec42011faaae25bdbaf8905cf0d2945bf6f451b60aa7a90fff0550debd","engraphis/core/vector_search.py":"75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234","eval/benchmark.py":"ed828e3548e8fb05ffafe9479791e545e56504421ff88676e4bf4466d6beaf91","eval/vector_scale.py":"3f9c327d9eca1a857512ddc208e972933be1a7d4fa7fa0017aca0cbf8fe7bb6d","eval/vector_scale_storage.py":"bda0c1139596eb9232cd75c826fc941101b6116bae32ad7895525d43f9d35484"},"tracked_diff_sha256":"b5de7d748eb33d21b2ccbed2d84b81216f40e479eea82ecccc4a1c747177b1fe"},"source_stable":true,"storage":[{"connection_cold_reads":{"latency_ms":{"max":51.55,"mean":39.544,"min":24.497,"p50":36.739,"p95":51.075,"p99":51.455},"memory":{"process_lifetime_peak_rss_bytes":52944896,"rss_bytes":50745344},"result_counts":[10],"result_ids_sha256":"44124c7e868aee3911730e181c9bc684258ab1ba3f0fd101e9ecd66385c4b179","searches_per_second":25.222,"timed_searches":16,"wall_seconds":0.634359},"corpus_size":10000,"durable_memory_rows":10012,"durable_vector_rows":10012,"final_disk":{"database_bytes":33751040,"shared_memory_bytes":32768,"total_bytes":33812680,"wal_bytes":28872},"initial_startup_ms":38.67119998903945,"memory":{"process_lifetime_peak_rss_bytes":58744832,"rss_bytes":52895744},"mixed":[{"committed_writes":4,"measured":true,"reader_concurrency":1,"reads":{"latency_ms":{"max":49.966,"mean":39.778,"min":24.222,"p50":36.373,"p95":49.005,"p99":49.774},"memory":{"process_lifetime_peak_rss_bytes":56217600,"rss_bytes":52006912},"result_counts":[10],"result_ids_sha256":"44124c7e868aee3911730e181c9bc684258ab1ba3f0fd101e9ecd66385c4b179","searches_per_second":25.096,"timed_searches":16,"wall_seconds":0.637545},"starting_corpus_size":10000,"wall_seconds":0.638152,"write_latency_ms":{"max":11.452,"mean":6.167,"min":0.931,"p50":6.143,"p95":11.074,"p99":11.376},"writer_concurrency":1},{"committed_writes":4,"measured":true,"reader_concurrency":4,"reads":{"latency_ms":{"max":208.029,"mean":158.746,"min":92.154,"p50":155.163,"p95":206.627,"p99":207.748},"memory":{"process_lifetime_peak_rss_bytes":56217600,"rss_bytes":52064256},"result_counts":[10],"result_ids_sha256":"44124c7e868aee3911730e181c9bc684258ab1ba3f0fd101e9ecd66385c4b179","searches_per_second":24.492,"timed_searches":16,"wall_seconds":0.653263},"starting_corpus_size":10004,"wall_seconds":0.653738,"write_latency_ms":{"max":43.582,"mean":14.297,"min":1.801,"p50":5.902,"p95":38.469,"p99":42.559},"writer_concurrency":1},{"committed_writes":4,"measured":true,"reader_concurrency":16,"reads":{"latency_ms":{"max":659.878,"mean":582.439,"min":384.941,"p50":574.131,"p95":657.22,"p99":659.347},"memory":{"process_lifetime_peak_rss_bytes":56217600,"rss_bytes":51433472},"result_counts":[10],"result_ids_sha256":"44124c7e868aee3911730e181c9bc684258ab1ba3f0fd101e9ecd66385c4b179","searches_per_second":24.17,"timed_searches":16,"wall_seconds":0.661981},"starting_corpus_size":10008,"wall_seconds":0.662478,"write_latency_ms":{"max":120.834,"mean":56.951,"min":3.137,"p50":51.917,"p95":113.089,"p99":119.285},"writer_concurrency":1}],"populated_disk":{"database_bytes":33726464,"shared_memory_bytes":32768,"total_bytes":38554944,"wal_bytes":4795712},"population_batch_latency_ms":{"max":291.265,"mean":227.676,"min":134.826,"p50":231.752,"p95":273.935,"p99":287.799},"population_records_per_second":1823.702064854598,"population_seconds":5.483351800008677,"rebuild":{"kind":"native_mirror_replay_from_canonical_vectors","measured":true,"records_replayed":10012,"seconds":1.690763},"restart_ms":849.2400000104681},{"connection_cold_reads":{"latency_ms":{"max":444.846,"mean":367.25,"min":328.743,"p50":357.668,"p95":422.34,"p99":440.345},"memory":{"process_lifetime_peak_rss_bytes":58744832,"rss_bytes":52252672},"result_counts":[10],"result_ids_sha256":"822335c92dc337b652c12230419675d4c4bdffa303fae8bcd00de192e9f6c96c","searches_per_second":2.722,"timed_searches":16,"wall_seconds":5.877111},"corpus_size":100000,"durable_memory_rows":100012,"durable_vector_rows":100012,"final_disk":{"database_bytes":329695232,"shared_memory_bytes":32768,"total_bytes":329756872,"wal_bytes":28872},"initial_startup_ms":34.037800040096045,"memory":{"process_lifetime_peak_rss_bytes":62771200,"rss_bytes":53272576},"mixed":[{"committed_writes":4,"measured":true,"reader_concurrency":1,"reads":{"latency_ms":{"max":514.191,"mean":407.641,"min":344.764,"p50":393.29,"p95":511.251,"p99":513.603},"memory":{"process_lifetime_peak_rss_bytes":62771200,"rss_bytes":53415936},"result_counts":[10],"result_ids_sha256":"822335c92dc337b652c12230419675d4c4bdffa303fae8bcd00de192e9f6c96c","searches_per_second":2.453,"timed_searches":16,"wall_seconds":6.523547},"starting_corpus_size":100000,"wall_seconds":6.524193,"write_latency_ms":{"max":133.57,"mean":79.371,"min":1.861,"p50":91.027,"p95":129.928,"p99":132.842},"writer_concurrency":1},{"committed_writes":4,"measured":true,"reader_concurrency":4,"reads":{"latency_ms":{"max":1877.192,"mean":1540.72,"min":1265.029,"p50":1568.69,"p95":1876.138,"p99":1876.982},"memory":{"process_lifetime_peak_rss_bytes":62771200,"rss_bytes":53493760},"result_counts":[10],"result_ids_sha256":"822335c92dc337b652c12230419675d4c4bdffa303fae8bcd00de192e9f6c96c","searches_per_second":2.549,"timed_searches":16,"wall_seconds":6.276704},"starting_corpus_size":100004,"wall_seconds":6.277296,"write_latency_ms":{"max":579.278,"mean":213.959,"min":3.208,"p50":136.675,"p95":514.114,"p99":566.246},"writer_concurrency":1},{"committed_writes":4,"measured":true,"reader_concurrency":16,"reads":{"latency_ms":{"max":6920.195,"mean":6595.596,"min":5596.571,"p50":6687.569,"p95":6919.419,"p99":6920.04},"memory":{"process_lifetime_peak_rss_bytes":62771200,"rss_bytes":52236288},"result_counts":[10],"result_ids_sha256":"822335c92dc337b652c12230419675d4c4bdffa303fae8bcd00de192e9f6c96c","searches_per_second":2.311,"timed_searches":16,"wall_seconds":6.924136},"starting_corpus_size":100008,"wall_seconds":6.924701,"write_latency_ms":{"max":1386.895,"mean":661.209,"min":3.07,"p50":627.436,"p95":1279.725,"p99":1365.461},"writer_concurrency":1}],"populated_disk":{"database_bytes":328536064,"shared_memory_bytes":32768,"total_bytes":336038424,"wal_bytes":7469592},"population_batch_latency_ms":{"max":2889.219,"mean":338.343,"min":127.529,"p50":319.909,"p95":482.452,"p99":543.163},"population_records_per_second":961.5270110461148,"population_seconds":104.00123850000091,"rebuild":{"kind":"native_mirror_replay_from_canonical_vectors","measured":true,"records_replayed":100012,"seconds":61.31294},"restart_ms":35132.382399984635}],"timing_scope":"latency is execution including storage locks, excludes executor queue; throughput includes queue","unmeasured":["embedding/model latency","extraction and conflict resolution","agent task quality","full recall and context packing","multi-process contention","independent repeated processes"]},"models":{"embedding":{"identity":"none; precomputed synthetic vectors"},"tokenizer":{"identity":"not applicable; no reader context"},"vector_backend":{"identity":"SqliteVecVectorIndex","native_version":"0.1.9"}},"privacy":{"content_fingerprint_policy":"omitted","raw_answer_policy":"omitted","raw_context_policy":"omitted","raw_query_policy":"omitted"},"protocol":{"command":["python","-m","eval.vector_scale","--file-backed","--backend","sqlite-vec","--sizes","10000,100000","--dim","256","--queries","16","--iterations","2","--warmups","1","--k","10","--seed","20260731","--concurrencies","1,4,16","--mixed-writes","4","--batch-size","500","--tenants","4","--progress"],"config":{"backend":"sqlite-vec","batch_size":500,"concurrencies":[1,4,16],"dimension":256,"file_backed":true,"inputs":[{"corpus_size":10000,"vectors_sha256":"5a3092df8330ded628164601fc11bfd57d3c08e1a8f95c364774ba8927255120"},{"corpus_size":100000,"vectors_sha256":"9dbc1a3b5b27422858fc5dbb0c16d619dbedbf90267fb72361fea69dcb5d06d3"}],"iterations":2,"k":10,"mixed_writes_per_cell":4,"queries":16,"queries_sha256":"19f8cde59c67bc91f0871cbd94bcce18e32cfaee0d30f3eb94dbd2104447d27f","seed":20260731,"sizes":[10000,100000],"tenant_scopes":4,"vector_generator":"numpy.PCG64.standard_normal.float32.normalized.v1","warmups":1},"n_scored":6,"n_total":6,"token_accounting":{"identity":"not_applicable","method":"not_measured","revision":null,"scope":"no reader context"}},"records":[{"category":"exact_index_throughput","latency_ms":39.591,"question_id":"n10000-c1"},{"category":"exact_index_throughput","latency_ms":156.052,"question_id":"n10000-c4"},{"category":"exact_index_throughput","latency_ms":610.213,"question_id":"n10000-c16"},{"category":"exact_index_throughput","latency_ms":357.1,"question_id":"n100000-c1"},{"category":"exact_index_throughput","latency_ms":1487.517,"question_id":"n100000-c4"},{"category":"exact_index_throughput","latency_ms":5296.068,"question_id":"n100000-c16"}],"schema":"engraphis-benchmark/v2","suite":{"dataset":"vector_scale_storage.py","name":"file-backed-exact-index-scale/v1","sha256":"bda0c1139596eb9232cd75c826fc941101b6116bae32ad7895525d43f9d35484","sources":[{"bytes":11055,"name":"vector_scale.py","sha256":"3f9c327d9eca1a857512ddc208e972933be1a7d4fa7fa0017aca0cbf8fe7bb6d"},{"bytes":22744,"name":"vector_scale_storage.py","sha256":"bda0c1139596eb9232cd75c826fc941101b6116bae32ad7895525d43f9d35484"},{"bytes":60944,"name":"benchmark.py","sha256":"ed828e3548e8fb05ffafe9479791e545e56504421ff88676e4bf4466d6beaf91"},{"bytes":6163,"name":"vector_numpy.py","sha256":"c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72"},{"bytes":23317,"name":"vector_sqlitevec.py","sha256":"6148e14ceaacc19239b64c642a3fba0e98797c78cec356210157afadf475a08b"},{"bytes":2349,"name":"vector_search.py","sha256":"75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234"},{"bytes":466184,"name":"store.py","sha256":"007272ec42011faaae25bdbaf8905cf0d2945bf6f451b60aa7a90fff0550debd"},{"bytes":39996,"name":"schema.py","sha256":"99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686"},{"bytes":31040,"name":"interfaces.py","sha256":"5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22"}]},"system":{"config_sha256":"2dfb70edc9f0b25e1dbf40945fa863eef355ee92dfad34b8ce24119714713682","dirty_state_sha256":"db6f20cf3f47efec0ae20b34ab4a298eb9350e09284968af1d9605dacd01e18d","git_commit":"c37ba0eb18408fe500cd70cd73fb5dcd7679b89c","git_dirty":true}} diff --git a/docs/evidence/reliability/vector-scale-sqlite-vec-corrected-20260905.json.checkpoint.json b/docs/evidence/reliability/vector-scale-sqlite-vec-corrected-20260905.json.checkpoint.json new file mode 100644 index 00000000..86ffa5ce --- /dev/null +++ b/docs/evidence/reliability/vector-scale-sqlite-vec-corrected-20260905.json.checkpoint.json @@ -0,0 +1 @@ +{"artifact":"vector-scale-sqlite-vec-corrected-20260905.json","schema":"engraphis-scale-checkpoint/v1","sha256":"3cae847fcff2a1c0c5c680b2b050912ff2eb35e488a55be10f23832a4e6c787c","status":"complete"} diff --git a/docs/evidence/reliability/vector-scale-sqlite-vec-corrected-20260905.json.sha256 b/docs/evidence/reliability/vector-scale-sqlite-vec-corrected-20260905.json.sha256 new file mode 100644 index 00000000..6d3bc486 --- /dev/null +++ b/docs/evidence/reliability/vector-scale-sqlite-vec-corrected-20260905.json.sha256 @@ -0,0 +1 @@ +3cae847fcff2a1c0c5c680b2b050912ff2eb35e488a55be10f23832a4e6c787c vector-scale-sqlite-vec-corrected-20260905.json diff --git a/docs/evidence/reliability/vector-scale-summary-20260905.md b/docs/evidence/reliability/vector-scale-summary-20260905.md new file mode 100644 index 00000000..44b3e09f --- /dev/null +++ b/docs/evidence/reliability/vector-scale-summary-20260905.md @@ -0,0 +1,112 @@ +# File-backed scale evidence, 2026-09-05 + +Both corrected matrices completed all six corpus/concurrency cells, all exact-result parity checks, mixed writes, and durable reopen checks. The native mirror rebuild also passed. This is synthetic scoped index/storage evidence; it does not establish semantic recall quality or coding-agent task success. + +**Capacity limits remain.** At 100k total records, 16-worker p50 latency is 8.633s for NumPy and 5.296s for sqlite-vec. Native reopen takes 35.132s and a full mirror rebuild takes 61.313s. These results do not establish the 100k concurrent agent operating target; no backend/ranking/grounding default was changed. + +**Scope tradeoff.** The selected first-sorted-batch strategy measured 235.479ms at a 5% eligible scope versus 132.031ms for repeated scope-driven batches (about 78% slower in this single comparison). It removes the severe unordered-probe cliff at 2.1% (55.942ms versus 240.465ms), and reduces the initial candidate scan cost at 25% (496.087ms versus 1326.383ms) and 100% (1084.019ms versus 16185.526ms). These are descriptive method comparisons on one corpus/process, with cache/order effects; they are not release speedups or SLO confidence bounds. + +**Provenance boundary.** The slow complete NumPy comparison is our initial bounded-matrix candidate. Released/base code materialized its matrix once; the initial candidate latency must not be described as released Engraphis performance. The FTS ablation forces the former deletion method on otherwise current code, and the native verification ablation preserves the former reverse-scan oracle. Neither is a historical release benchmark. + +**Protocol.** File-backed disposable SQLite, 10k/100k total records, 256-dimensional normalized PCG64 synthetic vectors, seed 20260731, four synthetic repository scopes, 25% eligible for the timed searches, k=10, 16 distinct queries, one warmup pass and two timed passes (32 search samples/cell), concurrency 1/4/16, four mixed writes per concurrency, and batches of 500 writes. Mixed phases use 16 reads and four single-record commits per cell. No model/tokenizer runs: precomputed vectors only. NumPy and native runs were sequential, with team CPU-heavy gates paused and the observed unrelated pytest process allowed to exit before timing. + +Host: Intel Core i7-10700KF at 3.80GHz, 16 logical CPUs, 34,221,301,760 bytes RAM, AMD64, SQLite 3.49.1; sqlite-vec 0.1.9 was loaded from an isolated install. OPENBLAS_NUM_THREADS, OMP_NUM_THREADS and MKL_NUM_THREADS were all 1. Background host activity was not controlled as a laboratory experiment. + +**Complete search cells.** Latency includes execution and shared-store lock waits, excludes executor queue time; throughput includes queue time. RSS is current resident memory after a cell. Peak RSS is cumulative for the process, not an isolated per-cell peak. + +| Backend | Records | Workers | p50 ms | p95 ms | p99 ms | Searches/s | RSS MiB | Process peak MiB | +|---|---:|---:|---:|---:|---:|---:|---:|---:| +| NumPy | 10000 | 1 | 58.316 | 80.819 | 85.610 | 15.997 | 50.79 | 55.24 | +| NumPy | 10000 | 4 | 268.058 | 393.975 | 416.295 | 13.748 | 50.52 | 55.71 | +| NumPy | 10000 | 16 | 1042.093 | 1099.690 | 1107.065 | 15.236 | 51.71 | 57.01 | +| NumPy | 100000 | 1 | 495.837 | 542.234 | 550.786 | 2.024 | 51.29 | 61.76 | +| NumPy | 100000 | 4 | 2340.836 | 2500.777 | 2525.383 | 1.738 | 51.19 | 61.78 | +| NumPy | 100000 | 16 | 8632.554 | 8806.378 | 8905.387 | 1.811 | 51.88 | 62.94 | +| sqlite-vec | 10000 | 1 | 39.591 | 56.319 | 57.810 | 23.790 | 49.23 | 53.61 | +| sqlite-vec | 10000 | 4 | 156.052 | 199.244 | 207.034 | 24.832 | 49.39 | 53.61 | +| sqlite-vec | 10000 | 16 | 610.213 | 792.479 | 806.125 | 23.990 | 48.88 | 53.61 | +| sqlite-vec | 100000 | 1 | 357.100 | 382.293 | 387.018 | 2.808 | 50.99 | 59.86 | +| sqlite-vec | 100000 | 4 | 1487.517 | 1703.431 | 1736.983 | 2.619 | 51.11 | 59.86 | +| sqlite-vec | 100000 | 16 | 5296.068 | 6475.360 | 7147.615 | 2.841 | 49.91 | 59.86 | + +**Storage and durability.** Population uses the real canonical Store/FTS/vector write path, bypassing extraction, embedding, resolution and full MemoryEngine writes. Native population includes publication verification. Reopen uses a new connection; the operating-system disk cache was not flushed. Disk is DB+WAL+SHM after final reopen, before closing the disposable store. + +| Backend | Records | Initial open ms | Reopen ms | Population s | Rebuild s | Final disk MiB | Durable memories/vectors | +|---|---:|---:|---:|---:|---:|---:|---:| +| NumPy | 10000 | 28.210 | 14.707 | 2.879 | not applicable | 21.52 | 10012 / 10012 | +| NumPy | 100000 | 26.410 | 14.481 | 31.189 | not applicable | 209.70 | 100012 / 100012 | +| sqlite-vec | 10000 | 38.671 | 849.240 | 5.483 | 1.691 | 32.25 | 10012 / 10012 | +| sqlite-vec | 100000 | 34.038 | 35132.382 | 104.001 | 61.313 | 314.48 | 100012 / 100012 | + +**Mixed cells.** Each has one writer and the listed reader concurrency. The corpus grows by four committed records after each cell; raw JSON records the exact starting count. Four write samples are descriptive only. + +| Backend | Initial records | Readers | Read p50 ms | Read p95 ms | Write p50 ms | Write p95 ms | Committed writes | +|---|---:|---:|---:|---:|---:|---:|---:| +| NumPy | 10000 | 1 | 61.010 | 66.503 | 57.568 | 60.726 | 4 | +| NumPy | 10004 | 4 | 230.828 | 250.557 | 227.260 | 237.154 | 4 | +| NumPy | 10008 | 16 | 528.255 | 961.893 | 250.665 | 525.618 | 4 | +| NumPy | 100000 | 1 | 640.612 | 689.457 | 584.104 | 590.260 | 4 | +| NumPy | 100004 | 4 | 2271.882 | 2609.469 | 2316.833 | 2553.175 | 4 | +| NumPy | 100008 | 16 | 4356.733 | 7828.967 | 1778.842 | 4475.820 | 4 | +| sqlite-vec | 10000 | 1 | 36.373 | 49.005 | 6.143 | 11.074 | 4 | +| sqlite-vec | 10004 | 4 | 155.163 | 206.627 | 5.902 | 38.469 | 4 | +| sqlite-vec | 10008 | 16 | 574.131 | 657.220 | 51.917 | 113.089 | 4 | +| sqlite-vec | 100000 | 1 | 393.290 | 511.251 | 91.027 | 129.928 | 4 | +| sqlite-vec | 100004 | 4 | 1568.690 | 1876.138 | 136.675 | 514.114 | 4 | +| sqlite-vec | 100008 | 16 | 6687.569 | 6919.419 | 627.436 | 1279.725 | 4 | + +**Measured changes and retained baselines.** + +| Artifact | Status and interpretation | SHA-256 | +|---|---|---| +| [vector-scale-numpy-corrected-20260905.json](vector-scale-numpy-corrected-20260905.json) | Complete final NumPy matrix | `ab4ba40faad58b7ecd3aff96a50ac45742f78f670ed79bed32b1802162d54e6b` | +| [vector-scale-sqlite-vec-corrected-20260905.json](vector-scale-sqlite-vec-corrected-20260905.json) | Complete final sqlite-vec 0.1.9 matrix | `3cae847fcff2a1c0c5c680b2b050912ff2eb35e488a55be10f23832a4e6c787c` | +| [vector-scale-numpy-20260905.json](vector-scale-numpy-20260905.json) | Complete initial bounded candidate; not released/base performance | `049d22200d0e088f03f42a13c8a0ae4031c3c9c3c12113c3f6196855e2f0d479` | +| [vector-scale-incomplete-baseline-20260905.json](vector-scale-incomplete-baseline-20260905.json) | Incomplete FTS baseline: about 396s, confirmed 40k/100k population checkpoint; no recovered latency samples | `45c4f61518012e35e09b6d274dec3015d00d4948f2b8279c0f56760dec080544` | +| [vector-scale-native-incomplete-20260905.json](vector-scale-native-incomplete-20260905.json) | Incomplete initial native coverage run: about 430s, six emitted read-parity checkpoints; 100k mixed/rebuild/reopen unfinished and no recovered latency samples | `1b73068e071f4e39b644522a25512282fe986fda9ad5724ad44369eb25a3ceb1` | +| [fts-insert-counterfactual-20260905.json](fts-insert-counterfactual-20260905.json) | Same-current-code deletion ablation: 10k population 16.650s forced former delete vs 2.495s corrected insert | `b0a4603e983246790682ee5db4b5d5fab65b8ffb72dce002c85049f5112bff16` | +| [native-coverage-counterfactual-20260905.json](native-coverage-counterfactual-20260905.json) | Same-store verification ablation: 10k reverse scan 2.029s vs full-content verification plus cardinality 0.889s | `df0eec12c370c5ee06ec2125e5e8b275ef6c654dab820f25427211cede3faa17` | +| [vector-scan-plan-20260905.json](vector-scan-plan-20260905.json) | Scoped JOIN vs vector-first selectivity comparison | `bb4f15d60d89e9e153456fb238acc3ea13fec68cfc673fdffb922a8f4e6ed63b` | +| [vector-scan-adaptive-20260905.json](vector-scan-adaptive-20260905.json) | Four scan candidates at 0.1/1/2.1/5/25/100%; selected scoped_first_sorted_batch | `a421a8e73180104f0f5612780dfc929b678083e59494beab66a6bb621ebcc6e7` | + +The two final `.checkpoint.json` files contain completed receipts matching their canonical artifact hashes. If a future run is interrupted, checkpoints atomically retain actual completed cells and timings with config/source identity and status `incomplete`. No missing native baseline numbers were reconstructed. + +**Final source identity.** Both corrected reports have `source_stable=true`, identical before/after hashes, the same generated corpus/query identities, and matching result-ID hashes for every corresponding cell. Current file hashes were checked again after completion. + +Git revision: `c37ba0eb18408fe500cd70cd73fb5dcd7679b89c` (dirty working tree). Measured tracked-source diff SHA-256: `b5de7d748eb33d21b2ccbed2d84b81216f40e479eea82ecccc4a1c747177b1fe`. + +| Measured source | SHA-256 | +|---|---| +| `engraphis/backends/vector_numpy.py` | `c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72` | +| `engraphis/backends/vector_sqlitevec.py` | `6148e14ceaacc19239b64c642a3fba0e98797c78cec356210157afadf475a08b` | +| `engraphis/core/interfaces.py` | `5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22` | +| `engraphis/core/schema.py` | `99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686` | +| `engraphis/core/store.py` | `007272ec42011faaae25bdbaf8905cf0d2945bf6f451b60aa7a90fff0550debd` | +| `engraphis/core/vector_search.py` | `75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234` | +| `eval/benchmark.py` | `ed828e3548e8fb05ffafe9479791e545e56504421ff88676e4bf4466d6beaf91` | +| `eval/vector_scale.py` | `3f9c327d9eca1a857512ddc208e972933be1a7d4fa7fa0017aca0cbf8fe7bb6d` | +| `eval/vector_scale_storage.py` | `bda0c1139596eb9232cd75c826fc941101b6116bae32ad7895525d43f9d35484` | + +**Replay commands.** Use fresh output names because artifacts and existing checkpoints are not overwritten. The optional native dependency must resolve to sqlite-vec 0.1.9; the command below reuses the isolated Windows install prepared for this run. + +```powershell +$env:ENGRAPHIS_EXTRACTOR='none' +$env:PYTHONIOENCODING='utf-8' +$env:OPENBLAS_NUM_THREADS='1' +$env:OMP_NUM_THREADS='1' +$env:MKL_NUM_THREADS='1' +$env:PYTHONPATH=Join-Path $env:TEMP 'engraphis-review-sqlitevec-c37ba0e' +python -m eval.vector_scale --file-backed --backend numpy --sizes 10000,100000 --dim 256 --queries 16 --iterations 2 --warmups 1 --k 10 --seed 20260731 --concurrencies 1,4,16 --mixed-writes 4 --batch-size 500 --tenants 4 --progress --output docs/evidence/reliability/vector-scale-numpy-replay.json +python -m eval.vector_scale --file-backed --backend sqlite-vec --sizes 10000,100000 --dim 256 --queries 16 --iterations 2 --warmups 1 --k 10 --seed 20260731 --concurrencies 1,4,16 --mixed-writes 4 --batch-size 500 --tenants 4 --progress --output docs/evidence/reliability/vector-scale-sqlite-vec-replay.json +python -m eval.fts_insert_scaling --sizes 1000,5000,10000 --dim 256 --batch-size 500 --seed 20260731 --output docs/evidence/reliability/fts-insert-replay.json +python -m eval.native_coverage_scaling --sizes 1000,5000,10000 --dim 256 --batch-size 500 --seed 20260731 --output docs/evidence/reliability/native-coverage-replay.json +python -m eval.vector_scan_plan --sizes 100000 --dim 256 --batch-size 500 --seed 20260731 --output docs/evidence/reliability/vector-scan-replay.json +``` + +The current scan diagnostic includes the selected production iterator; its earlier saved `adaptive_snapshot` comparison was the unordered-probe candidate. Replaying the final source therefore does not recreate that abandoned candidate. The selected `scoped_first_sorted_batch` method remains explicit in the diagnostic. + +**Validation and open evidence.** Final focused storage/sync/NumPy/native/FTS/snapshot/checkpoint/ablation gate: 494 passed, 3 skipped. Ruff, targeted Pyright and diff whitespace checks passed. The final full public gate passed 4,736 tests with 37 skipped on stable production/test source; see [validation.json](validation.json) and [the source receipt](public-complete-source-final.json). No descendants, paid model calls, servers, commits or pushes were used by this worker. + +One million records is supported as opt-in input but was not run. Independent process repetitions, process contention, semantic embeddings, extraction/resolution latency, full hybrid recall/context packing, tenant workload distributions beyond the diagnostic scopes, and real coding-agent task outcomes remain unmeasured. Percentiles from 32 search samples and four mixed-write samples do not establish tail-SLO confidence. The improvements and remaining limits should guide the next evaluation phase, without treating these synthetic timings as product-quality or release claims. + +**Remaining temporary artifacts.** Automatic approval review rejected the optional PowerShell action `Remove-Item -LiteralPath -Recurse -Force` targeting `C:\Users\jomie\AppData\Local\Temp\egr-scale-1624f50x` and `C:\Users\jomie\AppData\Local\Temp\egr-scale-vxkh5zh0`. The only stated reason was `blocked by policy`. No deletion ran. Both directories remain untouched; cleanup was not retried. Canonical evidence artifacts are unaffected. diff --git a/docs/evidence/reliability/vector-scan-adaptive-20260905.json b/docs/evidence/reliability/vector-scan-adaptive-20260905.json new file mode 100644 index 00000000..b71f7687 --- /dev/null +++ b/docs/evidence/reliability/vector-scan-adaptive-20260905.json @@ -0,0 +1 @@ +{"environment":{"implementation":"CPython","machine":"AMD64","packages":{"engraphis":"1.7.1","numpy":"2.4.5","sentence-transformers":"6.0.0","torch":"2.13.0","transformers":"5.15.1"},"platform":"Windows-11-10.0.26100-SP0","python":"3.12.10"},"exclusions":[],"metrics":{"cells":[{"corpus_size":100000,"elapsed_seconds":0.0035436999751254916,"query_plan":["SEARCH m USING INDEX idx_mem_scope (workspace_id=? AND repo_id=?)","SEARCH v USING INDEX sqlite_autoindex_mem_vectors_1 (id=?)","USE TEMP B-TREE FOR ORDER BY"],"result_sha256":"09ccf13b553a338fab69d732c9a1cffa91e7453499ebcb161c6894d24e0df9b5","rows":100,"strategy":"planner_selected_join","target_scope_percent":0.1},{"corpus_size":100000,"elapsed_seconds":0.2181422999710776,"query_plan":["SEARCH v USING INDEX sqlite_autoindex_mem_vectors_1 (id>?)","SEARCH m USING INDEX sqlite_autoindex_memories_1 (id=?)"],"result_sha256":"09ccf13b553a338fab69d732c9a1cffa91e7453499ebcb161c6894d24e0df9b5","rows":100,"strategy":"vector_primary_key_first","target_scope_percent":0.1},{"corpus_size":100000,"elapsed_seconds":0.00287129997741431,"query_plan":["production bounded scoped probe; vector-first for more than one batch"],"result_sha256":"09ccf13b553a338fab69d732c9a1cffa91e7453499ebcb161c6894d24e0df9b5","rows":100,"strategy":"adaptive_snapshot","target_scope_percent":0.1},{"corpus_size":100000,"elapsed_seconds":0.0009590999688953161,"query_plan":["SEARCH v USING INDEX sqlite_autoindex_mem_vectors_1 (id>?)","SEARCH m USING INDEX sqlite_autoindex_memories_1 (id=?)"],"result_sha256":"09ccf13b553a338fab69d732c9a1cffa91e7453499ebcb161c6894d24e0df9b5","rows":100,"strategy":"scoped_first_sorted_batch","target_scope_percent":0.1},{"corpus_size":100000,"elapsed_seconds":0.020357600005809218,"query_plan":["SEARCH m USING INDEX idx_mem_scope (workspace_id=? AND repo_id=?)","SEARCH v USING INDEX sqlite_autoindex_mem_vectors_1 (id=?)","USE TEMP B-TREE FOR ORDER BY"],"result_sha256":"4d01255756d35ad48e6f2b283d328e1818ddaa0f996f7cdf89703bf9ef48a67e","rows":1000,"strategy":"planner_selected_join","target_scope_percent":1},{"corpus_size":100000,"elapsed_seconds":0.22729959996649995,"query_plan":["SEARCH v USING INDEX sqlite_autoindex_mem_vectors_1 (id>?)","SEARCH m USING INDEX sqlite_autoindex_memories_1 (id=?)"],"result_sha256":"4d01255756d35ad48e6f2b283d328e1818ddaa0f996f7cdf89703bf9ef48a67e","rows":1000,"strategy":"vector_primary_key_first","target_scope_percent":1},{"corpus_size":100000,"elapsed_seconds":0.009635999973397702,"query_plan":["production bounded scoped probe; vector-first for more than one batch"],"result_sha256":"4d01255756d35ad48e6f2b283d328e1818ddaa0f996f7cdf89703bf9ef48a67e","rows":1000,"strategy":"adaptive_snapshot","target_scope_percent":1},{"corpus_size":100000,"elapsed_seconds":0.02159419999225065,"query_plan":["SEARCH v USING INDEX sqlite_autoindex_mem_vectors_1 (id>?)","SEARCH m USING INDEX sqlite_autoindex_memories_1 (id=?)"],"result_sha256":"4d01255756d35ad48e6f2b283d328e1818ddaa0f996f7cdf89703bf9ef48a67e","rows":1000,"strategy":"scoped_first_sorted_batch","target_scope_percent":1},{"corpus_size":100000,"elapsed_seconds":0.05377340002451092,"query_plan":["SEARCH m USING INDEX idx_mem_scope (workspace_id=? AND repo_id=?)","SEARCH v USING INDEX sqlite_autoindex_mem_vectors_1 (id=?)","USE TEMP B-TREE FOR ORDER BY"],"result_sha256":"53216e7ca07c687c665f9b9e8ef96bcc5ba33b6a3de03f6933630c6a24c19da9","rows":2100,"strategy":"planner_selected_join","target_scope_percent":2.1},{"corpus_size":100000,"elapsed_seconds":0.2708942999597639,"query_plan":["SEARCH v USING INDEX sqlite_autoindex_mem_vectors_1 (id>?)","SEARCH m USING INDEX sqlite_autoindex_memories_1 (id=?)"],"result_sha256":"53216e7ca07c687c665f9b9e8ef96bcc5ba33b6a3de03f6933630c6a24c19da9","rows":2100,"strategy":"vector_primary_key_first","target_scope_percent":2.1},{"corpus_size":100000,"elapsed_seconds":0.24046469997847453,"query_plan":["production bounded scoped probe; vector-first for more than one batch"],"result_sha256":"53216e7ca07c687c665f9b9e8ef96bcc5ba33b6a3de03f6933630c6a24c19da9","rows":2100,"strategy":"adaptive_snapshot","target_scope_percent":2.1},{"corpus_size":100000,"elapsed_seconds":0.055941900005564094,"query_plan":["SEARCH v USING INDEX sqlite_autoindex_mem_vectors_1 (id>?)","SEARCH m USING INDEX sqlite_autoindex_memories_1 (id=?)"],"result_sha256":"53216e7ca07c687c665f9b9e8ef96bcc5ba33b6a3de03f6933630c6a24c19da9","rows":2100,"strategy":"scoped_first_sorted_batch","target_scope_percent":2.1},{"corpus_size":100000,"elapsed_seconds":0.1320312999887392,"query_plan":["SEARCH m USING INDEX idx_mem_scope (workspace_id=? AND repo_id=?)","SEARCH v USING INDEX sqlite_autoindex_mem_vectors_1 (id=?)","USE TEMP B-TREE FOR ORDER BY"],"result_sha256":"f2e4175c662da5753664c46557b79fe8b989ab70d4bf254c6cae47082abd2c79","rows":5000,"strategy":"planner_selected_join","target_scope_percent":5},{"corpus_size":100000,"elapsed_seconds":0.24556319997645915,"query_plan":["SEARCH v USING INDEX sqlite_autoindex_mem_vectors_1 (id>?)","SEARCH m USING INDEX sqlite_autoindex_memories_1 (id=?)"],"result_sha256":"f2e4175c662da5753664c46557b79fe8b989ab70d4bf254c6cae47082abd2c79","rows":5000,"strategy":"vector_primary_key_first","target_scope_percent":5},{"corpus_size":100000,"elapsed_seconds":0.25984060001792386,"query_plan":["production bounded scoped probe; vector-first for more than one batch"],"result_sha256":"f2e4175c662da5753664c46557b79fe8b989ab70d4bf254c6cae47082abd2c79","rows":5000,"strategy":"adaptive_snapshot","target_scope_percent":5},{"corpus_size":100000,"elapsed_seconds":0.2354792000260204,"query_plan":["SEARCH v USING INDEX sqlite_autoindex_mem_vectors_1 (id>?)","SEARCH m USING INDEX sqlite_autoindex_memories_1 (id=?)"],"result_sha256":"f2e4175c662da5753664c46557b79fe8b989ab70d4bf254c6cae47082abd2c79","rows":5000,"strategy":"scoped_first_sorted_batch","target_scope_percent":5},{"corpus_size":100000,"elapsed_seconds":1.3263830000068992,"query_plan":["SEARCH m USING INDEX idx_mem_scope (workspace_id=? AND repo_id=?)","SEARCH v USING INDEX sqlite_autoindex_mem_vectors_1 (id=?)","USE TEMP B-TREE FOR ORDER BY"],"result_sha256":"e90d01f11f40b760275f24c69ed542b03daaf5e2969eebac53cd5eb18e466e36","rows":25000,"strategy":"planner_selected_join","target_scope_percent":25},{"corpus_size":100000,"elapsed_seconds":0.43986149999545887,"query_plan":["SEARCH v USING INDEX sqlite_autoindex_mem_vectors_1 (id>?)","SEARCH m USING INDEX sqlite_autoindex_memories_1 (id=?)"],"result_sha256":"e90d01f11f40b760275f24c69ed542b03daaf5e2969eebac53cd5eb18e466e36","rows":25000,"strategy":"vector_primary_key_first","target_scope_percent":25},{"corpus_size":100000,"elapsed_seconds":0.44714080000994727,"query_plan":["production bounded scoped probe; vector-first for more than one batch"],"result_sha256":"e90d01f11f40b760275f24c69ed542b03daaf5e2969eebac53cd5eb18e466e36","rows":25000,"strategy":"adaptive_snapshot","target_scope_percent":25},{"corpus_size":100000,"elapsed_seconds":0.4960873000090942,"query_plan":["SEARCH v USING INDEX sqlite_autoindex_mem_vectors_1 (id>?)","SEARCH m USING INDEX sqlite_autoindex_memories_1 (id=?)"],"result_sha256":"e90d01f11f40b760275f24c69ed542b03daaf5e2969eebac53cd5eb18e466e36","rows":25000,"strategy":"scoped_first_sorted_batch","target_scope_percent":25},{"corpus_size":100000,"elapsed_seconds":16.18552570004249,"query_plan":["SEARCH m USING INDEX idx_mem_scope (workspace_id=?)","SEARCH v USING INDEX sqlite_autoindex_mem_vectors_1 (id=?)","USE TEMP B-TREE FOR ORDER BY"],"result_sha256":"8e37c910810f6cb1396f91ee1748e20a368366aa407bdd9a3c11b825cf889218","rows":100000,"strategy":"planner_selected_join","target_scope_percent":100},{"corpus_size":100000,"elapsed_seconds":0.6762827000347897,"query_plan":["SEARCH v USING INDEX sqlite_autoindex_mem_vectors_1 (id>?)","SEARCH m USING INDEX sqlite_autoindex_memories_1 (id=?)"],"result_sha256":"8e37c910810f6cb1396f91ee1748e20a368366aa407bdd9a3c11b825cf889218","rows":100000,"strategy":"vector_primary_key_first","target_scope_percent":100},{"corpus_size":100000,"elapsed_seconds":0.6937315000104718,"query_plan":["production bounded scoped probe; vector-first for more than one batch"],"result_sha256":"8e37c910810f6cb1396f91ee1748e20a368366aa407bdd9a3c11b825cf889218","rows":100000,"strategy":"adaptive_snapshot","target_scope_percent":100},{"corpus_size":100000,"elapsed_seconds":1.0840191000024788,"query_plan":["SEARCH v USING INDEX sqlite_autoindex_mem_vectors_1 (id>?)","SEARCH m USING INDEX sqlite_autoindex_memories_1 (id=?)"],"result_sha256":"8e37c910810f6cb1396f91ee1748e20a368366aa407bdd9a3c11b825cf889218","rows":100000,"strategy":"scoped_first_sorted_batch","target_scope_percent":100}],"hardware":{"architecture":"AMD64","blas_thread_limits":{"MKL_NUM_THREADS":"1","OMP_NUM_THREADS":"1","OPENBLAS_NUM_THREADS":"1"},"cpu":"Intel(R) Core(TM) i7-10700KF CPU @ 3.80GHz","logical_cpus":16,"physical_ram_bytes":34221301760,"sqlite":"3.49.1"},"measurement_scope":"same scoped bounded vector scan; only join order differs","source_after":{"files":{"engraphis/backends/vector_numpy.py":"c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72","engraphis/backends/vector_sqlitevec.py":"6148e14ceaacc19239b64c642a3fba0e98797c78cec356210157afadf475a08b","engraphis/core/interfaces.py":"5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22","engraphis/core/schema.py":"99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686","engraphis/core/store.py":"9b821b3010c52f2e42667b1416f414bfd564de96f06790bfc91e8f181a2a0fa5","engraphis/core/vector_search.py":"75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234","eval/benchmark.py":"ed828e3548e8fb05ffafe9479791e545e56504421ff88676e4bf4466d6beaf91","eval/vector_scale.py":"3f9c327d9eca1a857512ddc208e972933be1a7d4fa7fa0017aca0cbf8fe7bb6d","eval/vector_scale_storage.py":"33501cceb0aa1a5cfc49cba9caef9e4a92e4c758a0419f8b3a4e59bed34f757e"},"tracked_diff_sha256":"e35bdda83af921db95b44a5c9aac32348d97c59b833a0d3abaff90322977f849"},"source_before":{"files":{"engraphis/backends/vector_numpy.py":"c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72","engraphis/backends/vector_sqlitevec.py":"6148e14ceaacc19239b64c642a3fba0e98797c78cec356210157afadf475a08b","engraphis/core/interfaces.py":"5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22","engraphis/core/schema.py":"99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686","engraphis/core/store.py":"9b821b3010c52f2e42667b1416f414bfd564de96f06790bfc91e8f181a2a0fa5","engraphis/core/vector_search.py":"75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234","eval/benchmark.py":"ed828e3548e8fb05ffafe9479791e545e56504421ff88676e4bf4466d6beaf91","eval/vector_scale.py":"3f9c327d9eca1a857512ddc208e972933be1a7d4fa7fa0017aca0cbf8fe7bb6d","eval/vector_scale_storage.py":"33501cceb0aa1a5cfc49cba9caef9e4a92e4c758a0419f8b3a4e59bed34f757e"},"tracked_diff_sha256":"e35bdda83af921db95b44a5c9aac32348d97c59b833a0d3abaff90322977f849"},"source_stable":true,"unmeasured":["historical release behavior","independent process repetitions","external contention","end-to-end recall"]},"models":{},"privacy":{"content_fingerprint_policy":"omitted","raw_answer_policy":"omitted","raw_context_policy":"omitted","raw_query_policy":"omitted"},"protocol":{"command":["python","-m","eval.vector_scan_plan","--sizes","100000","--dim","256","--batch-size","500","--seed","20260731"],"config":{"batch_size":500,"dimension":256,"seed":20260731,"sizes":[100000],"target_scope_percentages":[0.1,1,2.1,5,25,100]},"n_scored":24,"n_total":24,"token_accounting":{"identity":"unspecified","method":"unspecified","revision":null,"scope":"unspecified"}},"records":[{"category":"scoped_vector_scan","question_id":"planner_selected_join-100000-0.1"},{"category":"scoped_vector_scan","question_id":"vector_primary_key_first-100000-0.1"},{"category":"scoped_vector_scan","question_id":"adaptive_snapshot-100000-0.1"},{"category":"scoped_vector_scan","question_id":"scoped_first_sorted_batch-100000-0.1"},{"category":"scoped_vector_scan","question_id":"planner_selected_join-100000-1"},{"category":"scoped_vector_scan","question_id":"vector_primary_key_first-100000-1"},{"category":"scoped_vector_scan","question_id":"adaptive_snapshot-100000-1"},{"category":"scoped_vector_scan","question_id":"scoped_first_sorted_batch-100000-1"},{"category":"scoped_vector_scan","question_id":"planner_selected_join-100000-2.1"},{"category":"scoped_vector_scan","question_id":"vector_primary_key_first-100000-2.1"},{"category":"scoped_vector_scan","question_id":"adaptive_snapshot-100000-2.1"},{"category":"scoped_vector_scan","question_id":"scoped_first_sorted_batch-100000-2.1"},{"category":"scoped_vector_scan","question_id":"planner_selected_join-100000-5"},{"category":"scoped_vector_scan","question_id":"vector_primary_key_first-100000-5"},{"category":"scoped_vector_scan","question_id":"adaptive_snapshot-100000-5"},{"category":"scoped_vector_scan","question_id":"scoped_first_sorted_batch-100000-5"},{"category":"scoped_vector_scan","question_id":"planner_selected_join-100000-25"},{"category":"scoped_vector_scan","question_id":"vector_primary_key_first-100000-25"},{"category":"scoped_vector_scan","question_id":"adaptive_snapshot-100000-25"},{"category":"scoped_vector_scan","question_id":"scoped_first_sorted_batch-100000-25"},{"category":"scoped_vector_scan","question_id":"planner_selected_join-100000-100"},{"category":"scoped_vector_scan","question_id":"vector_primary_key_first-100000-100"},{"category":"scoped_vector_scan","question_id":"adaptive_snapshot-100000-100"},{"category":"scoped_vector_scan","question_id":"scoped_first_sorted_batch-100000-100"}],"schema":"engraphis-benchmark/v2","suite":{"dataset":"vector_scan_plan.py","name":"vector-scan-plan/counterfactual-v1","sha256":"e382594f70548dff80e0bfe2dec13b65216119f3d04ea606d829855c488f3903","sources":[{"bytes":11055,"name":"vector_scale.py","sha256":"3f9c327d9eca1a857512ddc208e972933be1a7d4fa7fa0017aca0cbf8fe7bb6d"},{"bytes":22641,"name":"vector_scale_storage.py","sha256":"33501cceb0aa1a5cfc49cba9caef9e4a92e4c758a0419f8b3a4e59bed34f757e"},{"bytes":60944,"name":"benchmark.py","sha256":"ed828e3548e8fb05ffafe9479791e545e56504421ff88676e4bf4466d6beaf91"},{"bytes":6163,"name":"vector_numpy.py","sha256":"c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72"},{"bytes":23317,"name":"vector_sqlitevec.py","sha256":"6148e14ceaacc19239b64c642a3fba0e98797c78cec356210157afadf475a08b"},{"bytes":2349,"name":"vector_search.py","sha256":"75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234"},{"bytes":466977,"name":"store.py","sha256":"9b821b3010c52f2e42667b1416f414bfd564de96f06790bfc91e8f181a2a0fa5"},{"bytes":39996,"name":"schema.py","sha256":"99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686"},{"bytes":31040,"name":"interfaces.py","sha256":"5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22"},{"bytes":7476,"name":"vector_scan_plan.py","sha256":"e382594f70548dff80e0bfe2dec13b65216119f3d04ea606d829855c488f3903"}]},"system":{"config_sha256":"443c1bf5f9d2f6b2c6801d236507692a399e55f4d1841ff472ae4401d3dcc243","dirty_state_sha256":"db6f20cf3f47efec0ae20b34ab4a298eb9350e09284968af1d9605dacd01e18d","git_commit":"c37ba0eb18408fe500cd70cd73fb5dcd7679b89c","git_dirty":true}} diff --git a/docs/evidence/reliability/vector-scan-adaptive-20260905.json.sha256 b/docs/evidence/reliability/vector-scan-adaptive-20260905.json.sha256 new file mode 100644 index 00000000..f1a20c69 --- /dev/null +++ b/docs/evidence/reliability/vector-scan-adaptive-20260905.json.sha256 @@ -0,0 +1 @@ +a421a8e73180104f0f5612780dfc929b678083e59494beab66a6bb621ebcc6e7 vector-scan-adaptive-20260905.json diff --git a/docs/evidence/reliability/vector-scan-plan-20260905.json b/docs/evidence/reliability/vector-scan-plan-20260905.json new file mode 100644 index 00000000..3ff2885e --- /dev/null +++ b/docs/evidence/reliability/vector-scan-plan-20260905.json @@ -0,0 +1 @@ +{"environment":{"implementation":"CPython","machine":"AMD64","packages":{"engraphis":"1.7.1","numpy":"2.4.5","sentence-transformers":"6.0.0","torch":"2.13.0","transformers":"5.15.1"},"platform":"Windows-11-10.0.26100-SP0","python":"3.12.10"},"exclusions":[],"metrics":{"cells":[{"corpus_size":100000,"elapsed_seconds":0.0034534999867901206,"query_plan":["SEARCH m USING INDEX idx_mem_scope (workspace_id=? AND repo_id=?)","SEARCH v USING INDEX sqlite_autoindex_mem_vectors_1 (id=?)","USE TEMP B-TREE FOR ORDER BY"],"result_sha256":"09ccf13b553a338fab69d732c9a1cffa91e7453499ebcb161c6894d24e0df9b5","rows":100,"strategy":"planner_selected_join","target_scope_percent":0.1},{"corpus_size":100000,"elapsed_seconds":0.23806790000526235,"query_plan":["SEARCH v USING INDEX sqlite_autoindex_mem_vectors_1 (id>?)","SEARCH m USING INDEX sqlite_autoindex_memories_1 (id=?)"],"result_sha256":"09ccf13b553a338fab69d732c9a1cffa91e7453499ebcb161c6894d24e0df9b5","rows":100,"strategy":"vector_primary_key_first","target_scope_percent":0.1},{"corpus_size":100000,"elapsed_seconds":0.020419900014530867,"query_plan":["SEARCH m USING INDEX idx_mem_scope (workspace_id=? AND repo_id=?)","SEARCH v USING INDEX sqlite_autoindex_mem_vectors_1 (id=?)","USE TEMP B-TREE FOR ORDER BY"],"result_sha256":"4d01255756d35ad48e6f2b283d328e1818ddaa0f996f7cdf89703bf9ef48a67e","rows":1000,"strategy":"planner_selected_join","target_scope_percent":1},{"corpus_size":100000,"elapsed_seconds":0.23032640002202243,"query_plan":["SEARCH v USING INDEX sqlite_autoindex_mem_vectors_1 (id>?)","SEARCH m USING INDEX sqlite_autoindex_memories_1 (id=?)"],"result_sha256":"4d01255756d35ad48e6f2b283d328e1818ddaa0f996f7cdf89703bf9ef48a67e","rows":1000,"strategy":"vector_primary_key_first","target_scope_percent":1},{"corpus_size":100000,"elapsed_seconds":1.2748100999742746,"query_plan":["SEARCH m USING INDEX idx_mem_scope (workspace_id=? AND repo_id=?)","SEARCH v USING INDEX sqlite_autoindex_mem_vectors_1 (id=?)","USE TEMP B-TREE FOR ORDER BY"],"result_sha256":"ebb0e0631dc875316656ac1e49e038bdddd4517635d0c7360fbffe252c2360ac","rows":25000,"strategy":"planner_selected_join","target_scope_percent":25},{"corpus_size":100000,"elapsed_seconds":0.3489755999762565,"query_plan":["SEARCH v USING INDEX sqlite_autoindex_mem_vectors_1 (id>?)","SEARCH m USING INDEX sqlite_autoindex_memories_1 (id=?)"],"result_sha256":"ebb0e0631dc875316656ac1e49e038bdddd4517635d0c7360fbffe252c2360ac","rows":25000,"strategy":"vector_primary_key_first","target_scope_percent":25},{"corpus_size":100000,"elapsed_seconds":14.40119899995625,"query_plan":["SEARCH m USING INDEX idx_mem_scope (workspace_id=?)","SEARCH v USING INDEX sqlite_autoindex_mem_vectors_1 (id=?)","USE TEMP B-TREE FOR ORDER BY"],"result_sha256":"8e37c910810f6cb1396f91ee1748e20a368366aa407bdd9a3c11b825cf889218","rows":100000,"strategy":"planner_selected_join","target_scope_percent":100},{"corpus_size":100000,"elapsed_seconds":0.7122808999847621,"query_plan":["SEARCH v USING INDEX sqlite_autoindex_mem_vectors_1 (id>?)","SEARCH m USING INDEX sqlite_autoindex_memories_1 (id=?)"],"result_sha256":"8e37c910810f6cb1396f91ee1748e20a368366aa407bdd9a3c11b825cf889218","rows":100000,"strategy":"vector_primary_key_first","target_scope_percent":100}],"hardware":{"architecture":"AMD64","blas_thread_limits":{"MKL_NUM_THREADS":"1","OMP_NUM_THREADS":"1","OPENBLAS_NUM_THREADS":"1"},"cpu":"Intel(R) Core(TM) i7-10700KF CPU @ 3.80GHz","logical_cpus":16,"physical_ram_bytes":34221301760,"sqlite":"3.49.1"},"measurement_scope":"same scoped bounded vector scan; only join order differs","source_after":{"files":{"engraphis/backends/vector_numpy.py":"c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72","engraphis/backends/vector_sqlitevec.py":"6148e14ceaacc19239b64c642a3fba0e98797c78cec356210157afadf475a08b","engraphis/core/interfaces.py":"5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22","engraphis/core/schema.py":"99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686","engraphis/core/store.py":"8659a5fd216e65f9e1e8b44870ffc1ff26e85aa77a26f831146977b727a11c35","engraphis/core/vector_search.py":"75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234","eval/benchmark.py":"ed828e3548e8fb05ffafe9479791e545e56504421ff88676e4bf4466d6beaf91","eval/vector_scale.py":"3f9c327d9eca1a857512ddc208e972933be1a7d4fa7fa0017aca0cbf8fe7bb6d","eval/vector_scale_storage.py":"33501cceb0aa1a5cfc49cba9caef9e4a92e4c758a0419f8b3a4e59bed34f757e"},"tracked_diff_sha256":"72a4d1621126c5a46c6b28507962076c17b8ea104c3b8e1a645d552eaf309ad5"},"source_before":{"files":{"engraphis/backends/vector_numpy.py":"c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72","engraphis/backends/vector_sqlitevec.py":"6148e14ceaacc19239b64c642a3fba0e98797c78cec356210157afadf475a08b","engraphis/core/interfaces.py":"5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22","engraphis/core/schema.py":"99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686","engraphis/core/store.py":"8659a5fd216e65f9e1e8b44870ffc1ff26e85aa77a26f831146977b727a11c35","engraphis/core/vector_search.py":"75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234","eval/benchmark.py":"ed828e3548e8fb05ffafe9479791e545e56504421ff88676e4bf4466d6beaf91","eval/vector_scale.py":"3f9c327d9eca1a857512ddc208e972933be1a7d4fa7fa0017aca0cbf8fe7bb6d","eval/vector_scale_storage.py":"33501cceb0aa1a5cfc49cba9caef9e4a92e4c758a0419f8b3a4e59bed34f757e"},"tracked_diff_sha256":"72a4d1621126c5a46c6b28507962076c17b8ea104c3b8e1a645d552eaf309ad5"},"source_stable":true,"unmeasured":["historical release behavior","independent process repetitions","external contention","end-to-end recall"]},"models":{},"privacy":{"content_fingerprint_policy":"omitted","raw_answer_policy":"omitted","raw_context_policy":"omitted","raw_query_policy":"omitted"},"protocol":{"command":["python","-m","eval.vector_scan_plan","--sizes","100000","--dim","256","--batch-size","500","--seed","20260731"],"config":{"batch_size":500,"dimension":256,"seed":20260731,"sizes":[100000],"target_scope_percentages":[0.1,1,25,100]},"n_scored":8,"n_total":8,"token_accounting":{"identity":"unspecified","method":"unspecified","revision":null,"scope":"unspecified"}},"records":[{"category":"scoped_vector_scan","question_id":"planner_selected_join-100000-0.1"},{"category":"scoped_vector_scan","question_id":"vector_primary_key_first-100000-0.1"},{"category":"scoped_vector_scan","question_id":"planner_selected_join-100000-1"},{"category":"scoped_vector_scan","question_id":"vector_primary_key_first-100000-1"},{"category":"scoped_vector_scan","question_id":"planner_selected_join-100000-25"},{"category":"scoped_vector_scan","question_id":"vector_primary_key_first-100000-25"},{"category":"scoped_vector_scan","question_id":"planner_selected_join-100000-100"},{"category":"scoped_vector_scan","question_id":"vector_primary_key_first-100000-100"}],"schema":"engraphis-benchmark/v2","suite":{"dataset":"vector_scan_plan.py","name":"vector-scan-plan/counterfactual-v1","sha256":"cda6d1b60f14373ba7ab243aec94f629e1d141409e93e2db88ffd22acfc21a8b","sources":[{"bytes":11055,"name":"vector_scale.py","sha256":"3f9c327d9eca1a857512ddc208e972933be1a7d4fa7fa0017aca0cbf8fe7bb6d"},{"bytes":22641,"name":"vector_scale_storage.py","sha256":"33501cceb0aa1a5cfc49cba9caef9e4a92e4c758a0419f8b3a4e59bed34f757e"},{"bytes":60944,"name":"benchmark.py","sha256":"ed828e3548e8fb05ffafe9479791e545e56504421ff88676e4bf4466d6beaf91"},{"bytes":6163,"name":"vector_numpy.py","sha256":"c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72"},{"bytes":23317,"name":"vector_sqlitevec.py","sha256":"6148e14ceaacc19239b64c642a3fba0e98797c78cec356210157afadf475a08b"},{"bytes":2349,"name":"vector_search.py","sha256":"75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234"},{"bytes":465965,"name":"store.py","sha256":"8659a5fd216e65f9e1e8b44870ffc1ff26e85aa77a26f831146977b727a11c35"},{"bytes":39996,"name":"schema.py","sha256":"99f3647602e2659035d5977e9d9814b378e58c2849483821e6e01d777b668686"},{"bytes":31040,"name":"interfaces.py","sha256":"5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22"},{"bytes":5886,"name":"vector_scan_plan.py","sha256":"cda6d1b60f14373ba7ab243aec94f629e1d141409e93e2db88ffd22acfc21a8b"}]},"system":{"config_sha256":"3e88cbe7e7aea454b5395d3b5ed6ab38365f356d336c004f3d4e8bbdd964375e","dirty_state_sha256":"1c4883fe9dac6decfa02b83601d2a4e8b507b511f397ba1e353e0feffbd320d2","git_commit":"c37ba0eb18408fe500cd70cd73fb5dcd7679b89c","git_dirty":true}} diff --git a/docs/evidence/reliability/vector-scan-plan-20260905.json.sha256 b/docs/evidence/reliability/vector-scan-plan-20260905.json.sha256 new file mode 100644 index 00000000..754fdf90 --- /dev/null +++ b/docs/evidence/reliability/vector-scan-plan-20260905.json.sha256 @@ -0,0 +1 @@ +bb4f15d60d89e9e153456fb238acc3ea13fec68cfc673fdffb922a8f4e6ed63b vector-scan-plan-20260905.json From b3bab7b7f1d7a9737090eec2a8d1d541a6cec741 Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Sat, 5 Sep 2026 06:20:14 -0400 Subject: [PATCH 07/10] test: isolate offline doctor and optional HTTP policy coverage --- docs/evidence/reliability/catalog.json | 9 +++- docs/evidence/reliability/catalog.json.sha256 | 2 +- .../reliability/public-ci-test-isolation.json | 53 +++++++++++++++++++ tests/test_init.py | 2 + tests/test_managed_processing_policy.py | 12 +++++ 5 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 docs/evidence/reliability/public-ci-test-isolation.json diff --git a/docs/evidence/reliability/catalog.json b/docs/evidence/reliability/catalog.json index a6f559ce..edbabd34 100644 --- a/docs/evidence/reliability/catalog.json +++ b/docs/evidence/reliability/catalog.json @@ -45,6 +45,10 @@ "sha256": "389291bb867e5e1c63c45c1f704079de49afbe91d8e9f05cb21f64cec41a2bc1", "bytes": 4732 }, + "public-ci-test-isolation.json": { + "sha256": "49af657e197634d622a0b7cc5b160394bcc3355a6590f8d6ea3be78b9b88fa13", + "bytes": 2287 + }, "public-complete-source-before-trial-test-correction.json": { "sha256": "9378843f71b39317a192673089fed1f2904581873f454824d7f3eeb6b6355404", "bytes": 18931 @@ -153,5 +157,8 @@ "sha256": "a9cfbb5f18426cfd2a63f6b014498fc79b268b22d114ad05335e711b16f80901", "bytes": 97 } - } + }, + "source_overlays": [ + "public-ci-test-isolation.json" + ] } diff --git a/docs/evidence/reliability/catalog.json.sha256 b/docs/evidence/reliability/catalog.json.sha256 index 43c96685..6f3d1d8f 100644 --- a/docs/evidence/reliability/catalog.json.sha256 +++ b/docs/evidence/reliability/catalog.json.sha256 @@ -1 +1 @@ -a9df9558ffeb568619695fcac2e7600ad3af36baf9653a6a6f42f70c061ce627 catalog.json +0144b724ab3a4826e87ab3ad6d915e51253b31b1f09a21ae07765a86a930fa32 catalog.json diff --git a/docs/evidence/reliability/public-ci-test-isolation.json b/docs/evidence/reliability/public-ci-test-isolation.json new file mode 100644 index 00000000..4b12981a --- /dev/null +++ b/docs/evidence/reliability/public-ci-test-isolation.json @@ -0,0 +1,53 @@ +{ + "schema": "engraphis-ci-followup/v1", + "date": "2026-09-05", + "parent_commit": "696aa07c8493c07e5c10c1fa60f2639b8ae45f57", + "initial_ci_run": "https://github.com/Coding-Dev-Tools/engraphis/actions/runs/33959397465", + "scope": "Two test-isolation corrections only; production sources are unchanged.", + "causes": [ + "Fresh Settings instances bypass the session fixture and selected a model absent in the offline Linux CI environment.", + "HTTP-only policy tests imported optional FastAPI/httpx dependencies in the NumPy-only Python 3.9 job." + ], + "repairs": [ + "Select empty ENGRAPHIS_EMBED_MODEL explicitly in the doctor test fixture. Preserve the explicit broken-model regression and strict production doctor.", + "Skip only HTTP cases when their optional stack is absent; keep core policy tests active on the minimum installation." + ], + "files": { + "tests/test_init.py": { + "sha256": "dc2d8ffc715007c57ef1bd299573ecba7dfe050d3c753117ee02ce8df3e67180", + "bytes": 14742 + }, + "tests/test_managed_processing_policy.py": { + "sha256": "d89a61d933a14d19a1be1f5488217a90cb2aa70030e8240d953fe2c432593ccf", + "bytes": 14604 + } + }, + "local_validation": { + "floor_before": "13 failed, 23 passed, 3 skipped", + "floor_after": "29 passed, 10 skipped", + "fullstack_after": "36 passed, 3 skipped", + "ruff": "pass", + "diff_check": "pass" + }, + "environments": { + "floor": { + "python": "3.9.25", + "numpy": "2.0.2", + "pytest": "8.4.2", + "os": "Windows" + }, + "full_stack": { + "python": "3.12", + "os": "Windows", + "http_policy_cases_run": 7 + } + }, + "initial_remote_evidence": { + "browser_scenarios_passed": 112, + "coverage_percent": 85.02, + "note": "Coverage and matrix jobs failed on the reproduced test setup cases; this is not a green complete run." + }, + "prior_full_suite_receipt": "public-pr-source-final.json", + "binding": "This file is a source overlay on source-manifest.json for the two changed tests. Earlier complete-suite evidence remains bound to its original source bytes.", + "limitations": "Both local runs use Windows. POSIX-only permission tests were skipped. Remote Linux CI remains parent followup." +} diff --git a/tests/test_init.py b/tests/test_init.py index 8b839340..1636bdff 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -27,6 +27,8 @@ def _write_private(path: Path, content: str) -> None: def _select_trusted_config(tmp_path, monkeypatch): path = _config_env(tmp_path) monkeypatch.setattr(init_script, "_trusted_env_file", lambda: path) + # Fresh Settings instances must keep the offline test configuration. + monkeypatch.setenv("ENGRAPHIS_EMBED_MODEL", "") return path diff --git a/tests/test_managed_processing_policy.py b/tests/test_managed_processing_policy.py index 52dff0a7..4d9a7fe7 100644 --- a/tests/test_managed_processing_policy.py +++ b/tests/test_managed_processing_policy.py @@ -9,6 +9,12 @@ from engraphis.service import MemoryService +@pytest.fixture +def _http_stack(): + pytest.importorskip("fastapi") + pytest.importorskip("httpx") + + @pytest.fixture def svc(tmp_path, monkeypatch): monkeypatch.delenv("ENGRAPHIS_MANAGED_COMPUTE_CONSENT", raising=False) @@ -61,6 +67,7 @@ def test_corrupt_state_fails_closed_and_can_be_reconfirmed(svc, revision): assert svc.set_managed_processing_policy("a", enabled=True, confirmed=True)["enabled"] +@pytest.mark.usefixtures("_http_stack") def test_http_acknowledgement_and_failed_optout(svc, monkeypatch): from fastapi import FastAPI from fastapi.testclient import TestClient @@ -112,6 +119,7 @@ def set_processing_policy(self, wid, **kwargs): assert "may continue" in disabled["notice"] +@pytest.mark.usefixtures("_http_stack") def test_delayed_enable_acknowledgement_cannot_overwrite_newer_optout(svc, monkeypatch): from fastapi import FastAPI from fastapi.testclient import TestClient @@ -158,6 +166,7 @@ def set_processing_policy(self, wid, **kwargs): @pytest.mark.parametrize("delay_at", ["get", "put"]) +@pytest.mark.usefixtures("_http_stack") def test_newer_optout_fences_delayed_cloud_enable(svc, monkeypatch, delay_at): from fastapi import FastAPI from fastapi.testclient import TestClient @@ -214,6 +223,7 @@ def set_processing_policy(self, wid, **kwargs): assert sum(call["enabled"] for call in cloud.puts) == (delay_at == "put") +@pytest.mark.usefixtures("_http_stack") def test_optout_retries_remote_conflict_while_local_intent_is_current(svc, monkeypatch): from fastapi import FastAPI from fastapi.testclient import TestClient @@ -273,6 +283,7 @@ def set_processing_policy(self, wid, **kwargs): assert svc.managed_processing_policy("a")["remote_revision"] == 3 +@pytest.mark.usefixtures("_http_stack") def test_optout_does_not_retry_after_newer_local_approval(svc, monkeypatch): from fastapi import FastAPI from fastapi.testclient import TestClient @@ -319,6 +330,7 @@ def test_cloud_client_sends_required_revision(monkeypatch): assert len(calls) == 1 +@pytest.mark.usefixtures("_http_stack") def test_expired_cloud_session_stops_local_uploads_with_remote_pending(svc, monkeypatch): from fastapi import FastAPI from fastapi.testclient import TestClient From b48850a9cfe38d02b38b1563961700ab534372f4 Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Sat, 5 Sep 2026 06:38:05 -0400 Subject: [PATCH 08/10] fix: address review feedback on repair queue indexing, editable extras, and MCP contract normalization --- docs/MCP_CONTRACT.json | 66 ++++++++-------- docs/RELIABILITY_PROGRAM.md | 88 ++++++++++++--------- docs/evidence/reliability/validation.json | 12 ++- engraphis/core/engine.py | 94 +++++++++++++++-------- engraphis/core/schema.py | 2 + engraphis/core/store.py | 37 +++++++++ engraphis/service.py | 18 +++-- scripts/export_mcp_contract.py | 7 +- scripts/update.py | 2 +- tests/test_mcp_contract.py | 61 +++++++++++++++ tests/test_storage_concurrency_repair.py | 22 ++++++ tests/test_update.py | 66 ++++++++++++++++ 12 files changed, 356 insertions(+), 119 deletions(-) diff --git a/docs/MCP_CONTRACT.json b/docs/MCP_CONTRACT.json index 77df85ea..84dfedb5 100644 --- a/docs/MCP_CONTRACT.json +++ b/docs/MCP_CONTRACT.json @@ -1,6 +1,6 @@ { "schema": "engraphis-mcp-contract/v1", - "sha256": "47af699b0c207132da7b663caa085d5579d573ee8d06e5ce06d4b16a2681002b", + "sha256": "1f0b4351e3a3d10b4404a86f47e30dfa85922f3aa08a86ce723e64aba6c793a3", "surfaces": { "classic": [ { @@ -11,7 +11,7 @@ "readOnlyHint": false, "title": "Grounded answer (compatibility alias)" }, - "description": "Backward-compatible alias for ``engraphis_recall_grounded``.\n\n Kept so existing agent configs that adopted the answer tool continue to work; new\n integrations should prefer ``engraphis_recall_grounded`` for the clearer name.\n ", + "description": "Backward-compatible alias for ``engraphis_recall_grounded``.\n\nKept so existing agent configs that adopted the answer tool continue to work; new\nintegrations should prefer ``engraphis_recall_grounded`` for the clearer name.", "inputSchema": { "properties": { "as_of": { @@ -197,7 +197,7 @@ "readOnlyHint": false, "title": "Check for an Engraphis update" }, - "description": "Report whether a newer Engraphis release is available, so an agent can proactively\n remind the user to upgrade.\n\n Cached ~24h and fail-silent; honors ``ENGRAPHIS_UPDATE_CHECK=0`` (then ``enabled`` is\n false). The default GitHub source is overridable via ``ENGRAPHIS_UPDATE_URL``. A stale\n lookup refreshes the persistent cache, and ``force=true`` rewrites it on every call,\n so this open-world tool is neither read-only nor idempotent.\n\n Returns:\n str: JSON ``{\"enabled\",\"current\",\"latest\",\"update_available\",\"url\",\"notice\"}``.\n ", + "description": "Report whether a newer Engraphis release is available, so an agent can proactively\nremind the user to upgrade.\n\nCached ~24h and fail-silent; honors ``ENGRAPHIS_UPDATE_CHECK=0`` (then ``enabled`` is\nfalse). The default GitHub source is overridable via ``ENGRAPHIS_UPDATE_URL``. A stale\nlookup refreshes the persistent cache, and ``force=true`` rewrites it on every call,\nso this open-world tool is neither read-only nor idempotent.\n\nReturns:\n str: JSON ``{\"enabled\",\"current\",\"latest\",\"update_available\",\"url\",\"notice\"}``.", "inputSchema": { "properties": { "force": { @@ -305,7 +305,7 @@ "readOnlyHint": true, "title": "Find a path through the code graph" }, - "description": "Return the shortest best-effort path between two code nodes.\n\n The path can cross definition, call, import, and symbol-alias edges. It is structural\n and name-based rather than type-resolved, so treat it as impact evidence rather than\n a compiler proof.\n ", + "description": "Return the shortest best-effort path between two code nodes.\n\nThe path can cross definition, call, import, and symbol-alias edges. It is structural\nand name-based rather than type-resolved, so treat it as impact evidence rather than\na compiler proof.", "inputSchema": { "properties": { "as_of": { @@ -403,7 +403,7 @@ "readOnlyHint": false, "title": "Consolidate memories (sleep-time sweep)" }, - "description": "Run one sleep-time consolidation sweep: recurring episodic memories on the same\n subject are distilled into one durable semantic digest (linked to its sources), and\n fully-decayed transient memories are archived (bi-temporally closed \u2014 never deleted,\n always audited, pinned memories exempt). Already-consolidated sources are skipped on\n retries. With ``profiles=True`` each entity's memories are also rolled into one durable\n profile digest. With ``structured=True`` a configured LLM may produce schema-validated\n facts/entities/relations; provider/schema failure falls back to the deterministic\n digest. A structured result may cite only part of a large cluster, allowing an\n identical later call to process the remainder, so the overall tool is conservatively\n non-idempotent. Good moments to call it: session end, or on a schedule. A real sweep\n requires explicit local-operator confirmation (``confirmed=true``); a ``dry_run``\n report does not mutate and needs none.\n\n Returns:\n str: JSON report ``{\"clusters_found\",\"digests_created\",\"archived\",\n \"skipped_already_consolidated\",\"compaction\",\"dry_run\"}`` \u2014 ``compaction`` reports\n the context tokens the sweep saved. With ``profiles=True`` a ``profiles`` block is\n added (``entities_considered``, ``profiles_created``, ``compaction``).\n ", + "description": "Run one sleep-time consolidation sweep: recurring episodic memories on the same\nsubject are distilled into one durable semantic digest (linked to its sources), and\nfully-decayed transient memories are archived (bi-temporally closed \u2014 never deleted,\nalways audited, pinned memories exempt). Already-consolidated sources are skipped on\nretries. With ``profiles=True`` each entity's memories are also rolled into one durable\nprofile digest. With ``structured=True`` a configured LLM may produce schema-validated\nfacts/entities/relations; provider/schema failure falls back to the deterministic\ndigest. A structured result may cite only part of a large cluster, allowing an\nidentical later call to process the remainder, so the overall tool is conservatively\nnon-idempotent. Good moments to call it: session end, or on a schedule. A real sweep\nrequires explicit local-operator confirmation (``confirmed=true``); a ``dry_run``\nreport does not mutate and needs none.\n\nReturns:\n str: JSON report ``{\"clusters_found\",\"digests_created\",\"archived\",\n \"skipped_already_consolidated\",\"compaction\",\"dry_run\"}`` \u2014 ``compaction`` reports\n the context tokens the sweep saved. With ``profiles=True`` a ``profiles`` block is\n added (``entities_considered``, ``profiles_created``, ``compaction``).", "inputSchema": { "properties": { "confirmed": { @@ -581,7 +581,7 @@ "readOnlyHint": false, "title": "Correct a memory" }, - "description": "Replace a memory's content without losing history: the old content is closed\n (bi-temporal invalidate, not deleted) and the correction is stored as a new memory\n that records what it corrects \u2014 so the audit trail and ``engraphis_why`` both still\n work afterward. Prefer this over retire+remember for fixes.\n\n Returns:\n str: JSON ``{\"id\",\"superseded\":[old_id],\"reason\"}`` or an actionable error if the\n id is unknown or doesn't belong to ``workspace``/``repo``.\n ", + "description": "Replace a memory's content without losing history: the old content is closed\n(bi-temporal invalidate, not deleted) and the correction is stored as a new memory\nthat records what it corrects \u2014 so the audit trail and ``engraphis_why`` both still\nwork afterward. Prefer this over retire+remember for fixes.\n\nReturns:\n str: JSON ``{\"id\",\"superseded\":[old_id],\"reason\"}`` or an actionable error if the\n id is unknown or doesn't belong to ``workspace``/``repo``.", "inputSchema": { "properties": { "memory_id": { @@ -645,7 +645,7 @@ "readOnlyHint": false, "title": "End a memory session" }, - "description": "Close a session with a summary/outcome so the next session can pick up the thread.\n An identical retry is an atomic no-op; a retry with a conflicting handoff is rejected,\n so this tool remains idempotent.\n\n Returns:\n str: JSON ``{\"session_id\",\"status\":\"summarized\",\"summary\",\"open_threads\"}`` or\n ``\"Error: ...\"`` if the session id is unknown.\n ", + "description": "Close a session with a summary/outcome so the next session can pick up the thread.\nAn identical retry is an atomic no-op; a retry with a conflicting handoff is rejected,\nso this tool remains idempotent.\n\nReturns:\n str: JSON ``{\"session_id\",\"status\":\"summarized\",\"summary\",\"open_threads\"}`` or\n ``\"Error: ...\"`` if the session id is unknown.", "inputSchema": { "properties": { "open_threads": { @@ -803,7 +803,7 @@ "readOnlyHint": false, "title": "Forget a memory (deprecated; use retire)" }, - "description": "Retire-with-history (deprecated compatibility alias for ``engraphis_retire``).\n\n It preserves the legacy ``status: \"forgotten\"`` response for existing clients;\n it still performs a temporal retirement and never deletes the memory. For\n irreversible removal of a leaked secret use ``engraphis_secure_erase`` with\n explicit confirmation instead.\n ", + "description": "Retire-with-history (deprecated compatibility alias for ``engraphis_retire``).\n\nIt preserves the legacy ``status: \"forgotten\"`` response for existing clients;\nit still performs a temporal retirement and never deletes the memory. For\nirreversible removal of a leaked secret use ``engraphis_secure_erase`` with\nexplicit confirmation instead.", "inputSchema": { "properties": { "confirmed": { @@ -865,7 +865,7 @@ "readOnlyHint": false, "title": "Index a repository's code graph" }, - "description": "Parse a repository into the code symbol graph: function/class/method definitions\n plus best-effort calls/imports edges. Run this once when you start working in a repo\n (or after large changes) so ``engraphis_search_code`` has something to search \u2014 uses\n AST parsing (tree-sitter) when available, a dependency-free regex fallback otherwise.\n Supported languages: Python, JavaScript, TypeScript, C#, C, and C++.\n\n Build/dependency directories (node_modules, bin, obj, target, .venv, \u2026) are skipped\n while walking, so a large non-Python repo indexes quickly instead of appearing to\n hang; add a ``.engraphisignore`` file (gitignore-style) at the repo root to skip\n project-specific generated files.\n\n Creates the workspace/repo if you haven't named them before (like\n engraphis_remember). Re-indexing is safe to call again; each file's symbols are\n replaced, not duplicated. Reads files from ``root_path`` on the local filesystem \u2014\n the same trust boundary as any other local tool you have, nothing is sent anywhere.\n Set ``ENGRAPHIS_INDEX_ROOTS`` to a path-separator-delimited absolute-path allow-list when\n repositories live outside the working, home, or temporary directories, or to narrow the\n defaults. Each completed scan appends a fresh operation receipt, so the MCP call is\n non-idempotent even when the code graph itself is unchanged.\n\n Returns:\n str: JSON ``{\"files_indexed\",\"symbols\",\"edges\",\"backend\"}``.\n ", + "description": "Parse a repository into the code symbol graph: function/class/method definitions\nplus best-effort calls/imports edges. Run this once when you start working in a repo\n(or after large changes) so ``engraphis_search_code`` has something to search \u2014 uses\nAST parsing (tree-sitter) when available, a dependency-free regex fallback otherwise.\nSupported languages: Python, JavaScript, TypeScript, C#, C, and C++.\n\nBuild/dependency directories (node_modules, bin, obj, target, .venv, \u2026) are skipped\nwhile walking, so a large non-Python repo indexes quickly instead of appearing to\nhang; add a ``.engraphisignore`` file (gitignore-style) at the repo root to skip\nproject-specific generated files.\n\nCreates the workspace/repo if you haven't named them before (like\nengraphis_remember). Re-indexing is safe to call again; each file's symbols are\nreplaced, not duplicated. Reads files from ``root_path`` on the local filesystem \u2014\nthe same trust boundary as any other local tool you have, nothing is sent anywhere.\nSet ``ENGRAPHIS_INDEX_ROOTS`` to a path-separator-delimited absolute-path allow-list when\nrepositories live outside the working, home, or temporary directories, or to narrow the\ndefaults. Each completed scan appends a fresh operation receipt, so the MCP call is\nnon-idempotent even when the code graph itself is unchanged.\n\nReturns:\n str: JSON ``{\"files_indexed\",\"symbols\",\"edges\",\"backend\"}``.", "inputSchema": { "properties": { "languages": { @@ -924,7 +924,7 @@ "readOnlyHint": false, "title": "Ingest raw text (extract facts first)" }, - "description": "Store raw text without hand-distilling it first \u2014 the extract-then-remember path.\n\n Prefer ``engraphis_remember`` when you already have a crisp fact; use this when you\n have a blob (transcript, notes, long status update) and want Engraphis to break it\n into separate, individually-recallable memories. Each extracted fact goes through\n the same conflict resolution and evolution as a normal remember.\n\n Returns:\n str: JSON ``{\"workspace\",\"repo\",\"count\",\"extracted\",\"facts\":[{\"id\",\"op\",...}]}``\n where ``extracted`` is false when no extractor is configured (passthrough).\n ", + "description": "Store raw text without hand-distilling it first \u2014 the extract-then-remember path.\n\nPrefer ``engraphis_remember`` when you already have a crisp fact; use this when you\nhave a blob (transcript, notes, long status update) and want Engraphis to break it\ninto separate, individually-recallable memories. Each extracted fact goes through\nthe same conflict resolution and evolution as a normal remember.\n\nReturns:\n str: JSON ``{\"workspace\",\"repo\",\"count\",\"extracted\",\"facts\":[{\"id\",\"op\",...}]}``\n where ``extracted`` is false when no extractor is configured (passthrough).", "inputSchema": { "properties": { "content": { @@ -1005,7 +1005,7 @@ "readOnlyHint": false, "title": "Ingest a live PostgreSQL schema" }, - "description": "Convert tables, columns, constraints, and foreign keys into a schema memory and\n entity graph. Requires the optional psycopg backend. An exact retry reuses its live\n point-in-time schema snapshot, but every invocation appends audit/receipt records,\n so the tool as a whole is not idempotent.", + "description": "Convert tables, columns, constraints, and foreign keys into a schema memory and\nentity graph. Requires the optional psycopg backend. An exact retry reuses its live\npoint-in-time schema snapshot, but every invocation appends audit/receipt records,\nso the tool as a whole is not idempotent.", "inputSchema": { "properties": { "dsn": { @@ -1070,7 +1070,7 @@ "readOnlyHint": false, "title": "Link two memories" }, - "description": "Explicitly connect two memories (A-MEM-style linking) \u2014 use when you notice two\n stored facts are related but a plain recall wouldn't surface that connection, e.g. a\n bug report and the memory describing its fix.\n\n Returns:\n str: JSON ``{\"a\",\"b\",\"relation\",\"layer\",\"reason\",\"linked\":true,\"receipt\":...}``\n or an actionable error if either id is unknown or doesn't belong to\n ``workspace``/``repo``.\n ", + "description": "Explicitly connect two memories (A-MEM-style linking) \u2014 use when you notice two\nstored facts are related but a plain recall wouldn't surface that connection, e.g. a\nbug report and the memory describing its fix.\n\nReturns:\n str: JSON ``{\"a\",\"b\",\"relation\",\"layer\",\"reason\",\"linked\":true,\"receipt\":...}``\n or an actionable error if either id is unknown or doesn't belong to\n ``workspace``/``repo``.", "inputSchema": { "properties": { "a": { @@ -1154,7 +1154,7 @@ "readOnlyHint": false, "title": "Link a code symbol to a memory" }, - "description": "Manually create a link between a code symbol and a memory.\n\n Use this when automatic indexing misses a relationship you know about \u2014 for example,\n linking a deployment function to the incident memory it resolved, or connecting a\n config constant to the decision that set its value. The link is idempotent: repeating\n the same call returns the existing link without duplication.\n\n Returns:\n str: JSON ``{\"link_id\",\"symbol_id\",\"memory_id\",\"relation\",\"workspace\",\"repo\",\"receipt\"}``.\n ", + "description": "Manually create a link between a code symbol and a memory.\n\nUse this when automatic indexing misses a relationship you know about \u2014 for example,\nlinking a deployment function to the incident memory it resolved, or connecting a\nconfig constant to the decision that set its value. The link is idempotent: repeating\nthe same call returns the existing link without duplication.\n\nReturns:\n str: JSON ``{\"link_id\",\"symbol_id\",\"memory_id\",\"relation\",\"workspace\",\"repo\",\"receipt\"}``.", "inputSchema": { "properties": { "confidence": { @@ -1227,7 +1227,7 @@ "readOnlyHint": false, "title": "Pin or unpin a memory" }, - "description": "Mark a memory as important enough to exempt from automatic decay/pruning \u2014 use for\n durable conventions or identity facts that must never silently fade.\n Every pin/unpin request is audited, including an identical retry, so the MCP call is\n deliberately annotated as non-idempotent even when the boolean value is unchanged.\n\n Returns:\n str: JSON ``{\"id\",\"pinned\"}`` or an actionable error if the id is unknown or doesn't\n belong to ``workspace``/``repo``.\n ", + "description": "Mark a memory as important enough to exempt from automatic decay/pruning \u2014 use for\ndurable conventions or identity facts that must never silently fade.\nEvery pin/unpin request is audited, including an identical retry, so the MCP call is\ndeliberately annotated as non-idempotent even when the boolean value is unchanged.\n\nReturns:\n str: JSON ``{\"id\",\"pinned\"}`` or an actionable error if the id is unknown or doesn't\n belong to ``workspace``/``repo``.", "inputSchema": { "properties": { "memory_id": { @@ -1282,7 +1282,7 @@ "readOnlyHint": false, "title": "Agent-ready proactive context" }, - "description": "Return an agent-ready context packet before the agent knows what to ask.\n\n Combines proactive recall, optional task-specific recall, and last-session handoff\n into a cited ``context_summary`` plus ``suggested_queries``. Deterministic by\n default; LLM synthesis is opt-in and accepted only when it cites source memories.\n When ``task`` or ``agent_state`` is supplied, the task-specific recall appends a\n privacy-safe receipt (without reinforcing memories), so the tool is conservatively\n annotated as mutating and non-idempotent.\n ", + "description": "Return an agent-ready context packet before the agent knows what to ask.\n\nCombines proactive recall, optional task-specific recall, and last-session handoff\ninto a cited ``context_summary`` plus ``suggested_queries``. Deterministic by\ndefault; LLM synthesis is opt-in and accepted only when it cites source memories.\nWhen ``task`` or ``agent_state`` is supplied, the task-specific recall appends a\nprivacy-safe receipt (without reinforcing memories), so the tool is conservatively\nannotated as mutating and non-idempotent.", "inputSchema": { "properties": { "agent_state": { @@ -1373,7 +1373,7 @@ "readOnlyHint": false, "title": "Promote a memory to a wider scope" }, - "description": "Widen a memory's visibility without losing its narrow-scope history.\n\n The wider record is stored first, inherits the source's protection,\n confidentiality, provenance, and learned stability, and is linked back to the\n bi-temporally closed source. Promotion must be strictly wider (session\u2192repo/workspace\n or repo\u2192workspace); it never edits scope in place. User-scope promotion is not yet\n supported because records remain workspace-bound.\n\n Returns:\n str: JSON ``{\"id\",\"promoted_from\",\"from_scope\",\"scope\",\"op\",\"reason\"}``\n plus a privacy receipt, or an actionable validation error.\n ", + "description": "Widen a memory's visibility without losing its narrow-scope history.\n\nThe wider record is stored first, inherits the source's protection,\nconfidentiality, provenance, and learned stability, and is linked back to the\nbi-temporally closed source. Promotion must be strictly wider (session\u2192repo/workspace\nor repo\u2192workspace); it never edits scope in place. User-scope promotion is not yet\nsupported because records remain workspace-bound.\n\nReturns:\n str: JSON ``{\"id\",\"promoted_from\",\"from_scope\",\"scope\",\"op\",\"reason\"}``\n plus a privacy receipt, or an actionable validation error.", "inputSchema": { "properties": { "memory_id": { @@ -1435,7 +1435,7 @@ "readOnlyHint": false, "title": "Recall relevant memories" }, - "description": "Retrieve the memories most relevant to a query (semantic vector + lexical + graph).\n\n Call this before answering or acting when prior context would help \u2014 to avoid re-asking\n the user, to recover decisions/conventions, or to resume earlier work.\n Successful calls append a privacy-safe recall receipt but do not strengthen weak\n neighbors merely because they were returned. Grounded recall reinforces cited\n evidence; an explicit-use caller can opt into reinforcement through the Python API.\n Because the receipt is stateful, this surface is neither read-only nor idempotent.\n\n Returns:\n str: JSON with ``{\"query\",\"count\",\"context\",\"degraded_mode\",\"semantic_support\",\n \"embedding_mode\",\"score_semantics\",\"memories\":[{\"id\",\n \"title\",\"content\",\"scope\",\"mtype\",\"repo_id\",\"score\",\"relative_score\",\n \"absolute_support\",\"arm\",\"retention\",\"provenance\"}]}``. ``score`` is a compatibility\n alias for the query-relative rank; use ``absolute_support`` (0..1) for an evidence floor.\n ``degraded_mode=true`` and ``semantic_support=false`` mean semantic vector retrieval\n was disabled because the active embedder is not declared semantic.\n Returns count 0 with a \"note\" if the workspace/repo isn't known yet.\n ", + "description": "Retrieve the memories most relevant to a query (semantic vector + lexical + graph).\n\nCall this before answering or acting when prior context would help \u2014 to avoid re-asking\nthe user, to recover decisions/conventions, or to resume earlier work.\nSuccessful calls append a privacy-safe recall receipt but do not strengthen weak\nneighbors merely because they were returned. Grounded recall reinforces cited\nevidence; an explicit-use caller can opt into reinforcement through the Python API.\nBecause the receipt is stateful, this surface is neither read-only nor idempotent.\n\nReturns:\n str: JSON with ``{\"query\",\"count\",\"context\",\"degraded_mode\",\"semantic_support\",\n \"embedding_mode\",\"score_semantics\",\"memories\":[{\"id\",\n \"title\",\"content\",\"scope\",\"mtype\",\"repo_id\",\"score\",\"relative_score\",\n \"absolute_support\",\"arm\",\"retention\",\"provenance\"}]}``. ``score`` is a compatibility\n alias for the query-relative rank; use ``absolute_support`` (0..1) for an evidence floor.\n ``degraded_mode=true`` and ``semantic_support=false`` mean semantic vector retrieval\n was disabled because the active embedder is not declared semantic.\n Returns count 0 with a \"note\" if the workspace/repo isn't known yet.", "inputSchema": { "properties": { "as_of": { @@ -1642,7 +1642,7 @@ "readOnlyHint": false, "title": "Recall token-efficient context" }, - "description": "Return one hard-budget context plus compact source identities.\n\n This is the recommended agent path: unlike legacy full recall, it does not\n repeat every complete memory body alongside the already-packed context. The\n response includes exact accounting for the declared counter, omitted/packed\n counts, privacy-safe savings metadata, and the same ``degraded_mode`` /\n ``semantic_support`` flags as ``engraphis_recall``.\n\n ``format=\"gist\"`` remains an accepted compatibility option. It returns the same\n evidence-safe packed context, including complete conditions and code whitespace,\n with a format marker. It does not apply another summary or claim extra savings.\n Use ``engraphis_get_memory`` for the full source behind a citation.\n ", + "description": "Return one hard-budget context plus compact source identities.\n\nThis is the recommended agent path: unlike legacy full recall, it does not\nrepeat every complete memory body alongside the already-packed context. The\nresponse includes exact accounting for the declared counter, omitted/packed\ncounts, privacy-safe savings metadata, and the same ``degraded_mode`` /\n``semantic_support`` flags as ``engraphis_recall``.\n\n``format=\"gist\"`` remains an accepted compatibility option. It returns the same\nevidence-safe packed context, including complete conditions and code whitespace,\nwith a format marker. It does not apply another summary or claim extra savings.\nUse ``engraphis_get_memory`` for the full source behind a citation.", "inputSchema": { "properties": { "as_of": { @@ -1842,7 +1842,7 @@ "readOnlyHint": false, "title": "Grounded recall (cited answer, or abstain)" }, - "description": "Answer a question *strictly from* stored memories, with citations \u2014 or abstain.\n\n Unlike ``engraphis_recall`` (which returns memories and leaves synthesis to you),\n this returns an answer assembled only from the retrieved memories, each claim tied\n to a ``[n]`` citation, and \u2014 crucially \u2014 refuses to answer when nothing in scope\n actually supports the query (``grounded: false``). Use it when you want a grounded,\n non-hallucinated answer and would rather get \"insufficient evidence\" than a guess.\n The deterministic default never introduces a claim that is not in a cited memory.\n When ``degraded_mode`` is true, its feature-hashing fallback is treated as lexical-only:\n semantic vector retrieval and semantic cosine support are disabled.\n With ``synthesize=True``, configured LLM prose is accepted only when citations hold.\n Every resolved call appends a privacy-safe receipt (including abstentions), and a\n grounded answer reinforces cited memories.\n\n Returns:\n str: JSON ``{\"query\",\"grounded\",\"abstained\",\"answer\",\"support\",\"reason\",\n \"degraded_mode\",\"semantic_support\",\"embedding_mode\",\n \"synthesized\":false,\"citations\":[{\"n\",\"id\",\"title\",\"content\",\"score\",\"support\",\n \"provenance\"}]}``. When ``grounded`` is false, ``answer`` is empty and ``reason``\n explains why (insufficient evidence, or unknown workspace/repo).\n ", + "description": "Answer a question *strictly from* stored memories, with citations \u2014 or abstain.\n\nUnlike ``engraphis_recall`` (which returns memories and leaves synthesis to you),\nthis returns an answer assembled only from the retrieved memories, each claim tied\nto a ``[n]`` citation, and \u2014 crucially \u2014 refuses to answer when nothing in scope\nactually supports the query (``grounded: false``). Use it when you want a grounded,\nnon-hallucinated answer and would rather get \"insufficient evidence\" than a guess.\nThe deterministic default never introduces a claim that is not in a cited memory.\nWhen ``degraded_mode`` is true, its feature-hashing fallback is treated as lexical-only:\nsemantic vector retrieval and semantic cosine support are disabled.\nWith ``synthesize=True``, configured LLM prose is accepted only when citations hold.\nEvery resolved call appends a privacy-safe receipt (including abstentions), and a\ngrounded answer reinforces cited memories.\n\nReturns:\n str: JSON ``{\"query\",\"grounded\",\"abstained\",\"answer\",\"support\",\"reason\",\n \"degraded_mode\",\"semantic_support\",\"embedding_mode\",\n \"synthesized\":false,\"citations\":[{\"n\",\"id\",\"title\",\"content\",\"score\",\"support\",\n \"provenance\"}]}``. When ``grounded`` is false, ``answer`` is empty and ``reason``\n explains why (insufficient evidence, or unknown workspace/repo).", "inputSchema": { "properties": { "as_of": { @@ -2070,7 +2070,7 @@ "readOnlyHint": true, "title": "What should I know right now" }, - "description": "Conscious/proactive recall: high-importance, recent, well-reinforced memories with\n no query needed \u2014 call this at the start of a task to load context before you've\n figured out what to ask for. When ``repo`` is given, also returns the most recent\n *ended* session's summary and unresolved ``open_threads`` for that repo, so you can\n pick up exactly where the last session left off. Authenticated callers only receive\n handoffs owned by their own user identity.\n\n Unlike query-based recall, this queryless ranking does not reinforce memories or append\n an operation receipt, so repeated calls are read-only and idempotent.\n\n Returns:\n str: JSON ``{\"memories\":[...], \"last_session\":{\"summary\",\"open_threads\",\"outcome\"}\n or {} if there is no prior session}``.\n ", + "description": "Conscious/proactive recall: high-importance, recent, well-reinforced memories with\nno query needed \u2014 call this at the start of a task to load context before you've\nfigured out what to ask for. When ``repo`` is given, also returns the most recent\n*ended* session's summary and unresolved ``open_threads`` for that repo, so you can\npick up exactly where the last session left off. Authenticated callers only receive\nhandoffs owned by their own user identity.\n\nUnlike query-based recall, this queryless ranking does not reinforce memories or append\nan operation receipt, so repeated calls are read-only and idempotent.\n\nReturns:\n str: JSON ``{\"memories\":[...], \"last_session\":{\"summary\",\"open_threads\",\"outcome\"}\n or {} if there is no prior session}``.", "inputSchema": { "properties": { "k": { @@ -2154,7 +2154,7 @@ "readOnlyHint": false, "title": "Log an episodic event" }, - "description": "Append a lightweight episodic log entry \u2014 lower ceremony than ``engraphis_remember``,\n for raw events you may later want consolidated into a durable fact (e.g. \"tried X, it\n deadlocked\" \u2014 three of these about the same thing is a signal worth promoting).\n\n Returns:\n str: JSON ``{\"id\",\"kind\"}``.\n ", + "description": "Append a lightweight episodic log entry \u2014 lower ceremony than ``engraphis_remember``,\nfor raw events you may later want consolidated into a durable fact (e.g. \"tried X, it\ndeadlocked\" \u2014 three of these about the same thing is a signal worth promoting).\n\nReturns:\n str: JSON ``{\"id\",\"kind\"}``.", "inputSchema": { "properties": { "content": { @@ -2224,7 +2224,7 @@ "readOnlyHint": false, "title": "Remember a fact" }, - "description": "Store a memory so it can be recalled in later turns, sessions, or repos.\n\n Use this whenever you learn something worth keeping: a convention, a decision and its\n rationale, a bug's cause and fix, a user preference, or a reusable procedure.\n\n Returns:\n str: JSON ``{\"id\",\"workspace\",\"repo\",\"scope\",\"mtype\",\"stored\":true,\"op\"}`` where\n ``op`` is ``\"add\"`` (new), ``\"noop\"`` (matched an existing memory almost exactly \u2014\n that one was reinforced, ``id`` points to it), or ``\"invalidate\"`` (superseded an\n existing memory on the same subject \u2014 see ``superseded`` for the old id(s); history\n is preserved, never deleted), ``\"relate\"`` (kept both uncertain neighboring claims and\n linked them), or ``\"quarantined\"`` (a suspicious explicitly untrusted payload was\n retained for governance inspection but excluded from normal recall). Quarantine returns\n content-free ``policy`` and ``reasons`` codes. Returns ``\"Error: \"`` if\n validation fails.\n ", + "description": "Store a memory so it can be recalled in later turns, sessions, or repos.\n\nUse this whenever you learn something worth keeping: a convention, a decision and its\nrationale, a bug's cause and fix, a user preference, or a reusable procedure.\n\nReturns:\n str: JSON ``{\"id\",\"workspace\",\"repo\",\"scope\",\"mtype\",\"stored\":true,\"op\"}`` where\n ``op`` is ``\"add\"`` (new), ``\"noop\"`` (matched an existing memory almost exactly \u2014\n that one was reinforced, ``id`` points to it), or ``\"invalidate\"`` (superseded an\n existing memory on the same subject \u2014 see ``superseded`` for the old id(s); history\n is preserved, never deleted), ``\"relate\"`` (kept both uncertain neighboring claims and\n linked them), or ``\"quarantined\"`` (a suspicious explicitly untrusted payload was\n retained for governance inspection but excluded from normal recall). Quarantine returns\n content-free ``policy`` and ``reasons`` codes. Returns ``\"Error: \"`` if\n validation fails.", "inputSchema": { "properties": { "claim_kind": { @@ -2416,7 +2416,7 @@ "readOnlyHint": false, "title": "Remember a batch of facts" }, - "description": "Store a batch of facts from parallel agents in one atomic, deduplicated write.\n\n Use this instead of many ``engraphis_remember`` calls when one turn produced a\n set of findings (fan-out sub-agents, a research sweep, a review council): the\n whole batch lands in a single transaction, each fact is resolved against the\n others (duplicates reinforce, keyed claims supersede), and facts sharing a\n ``subject_key`` or an explicit per-fact ``evidence_source`` get\n evidence-labeled graph edges so the merge is a growing graph rather than a\n pile of prose.\n\n Returns:\n str: JSON ``{\"workspace\",\"repo\",\"scope\",\"stored\":true,\"total\",\"ops\",\n \"results\":[{\"id\",\"op\",...}]}`` with one entry per input fact, in order.\n Returns ``\"Error: \"`` if validation fails or any fact cannot be\n stored (the whole batch rolls back in that case).\n ", + "description": "Store a batch of facts from parallel agents in one atomic, deduplicated write.\n\nUse this instead of many ``engraphis_remember`` calls when one turn produced a\nset of findings (fan-out sub-agents, a research sweep, a review council): the\nwhole batch lands in a single transaction, each fact is resolved against the\nothers (duplicates reinforce, keyed claims supersede), and facts sharing a\n``subject_key`` or an explicit per-fact ``evidence_source`` get\nevidence-labeled graph edges so the merge is a growing graph rather than a\npile of prose.\n\nReturns:\n str: JSON ``{\"workspace\",\"repo\",\"scope\",\"stored\":true,\"total\",\"ops\",\n \"results\":[{\"id\",\"op\",...}]}`` with one entry per input fact, in order.\n Returns ``\"Error: \"`` if validation fails or any fact cannot be\n stored (the whole batch rolls back in that case).", "inputSchema": { "properties": { "facts": { @@ -2514,7 +2514,7 @@ "readOnlyHint": false, "title": "Retire a memory" }, - "description": "Retire a memory: it stops appearing in recall, but history is preserved, not\n deleted (bi-temporal close, never a hard delete) \u2014 use ``engraphis_correct`` instead\n if you have replacement content, since that keeps the \"why\" chain intact.\n Every request appends an audit record, including an identical retry, so the MCP call\n is deliberately annotated as non-idempotent. Requires explicit local-operator\n confirmation (``confirmed=true``) because the stdio transport carries no role\n boundary.\n\n Returns:\n str: JSON ``{\"id\",\"status\":\"retired\",\"reason\"}`` or an actionable error if the\n id is unknown or doesn't belong to ``workspace``/``repo``.\n ", + "description": "Retire a memory: it stops appearing in recall, but history is preserved, not\ndeleted (bi-temporal close, never a hard delete) \u2014 use ``engraphis_correct`` instead\nif you have replacement content, since that keeps the \"why\" chain intact.\nEvery request appends an audit record, including an identical retry, so the MCP call\nis deliberately annotated as non-idempotent. Requires explicit local-operator\nconfirmation (``confirmed=true``) because the stdio transport carries no role\nboundary.\n\nReturns:\n str: JSON ``{\"id\",\"status\":\"retired\",\"reason\"}`` or an actionable error if the\n id is unknown or doesn't belong to ``workspace``/``repo``.", "inputSchema": { "properties": { "confirmed": { @@ -2576,7 +2576,7 @@ "readOnlyHint": true, "title": "Search the code symbol graph" }, - "description": "Find function/class/method definitions by name, with their callers \u2014 structural\n code search that costs far fewer tokens than grepping/reading whole files, and\n directly answers \"what calls this\" / \"what might break if I change it\".\n\n Returns:\n str: JSON ``{\"query\",\"symbols\":[{\"name\",\"fqname\",\"kind\",\"file\",\"span\",\n \"signature\",\"called_by\":[{\"src\",\"file\",\"line\"}]}]}``.\n ", + "description": "Find function/class/method definitions by name, with their callers \u2014 structural\ncode search that costs far fewer tokens than grepping/reading whole files, and\ndirectly answers \"what calls this\" / \"what might break if I change it\".\n\nReturns:\n str: JSON ``{\"query\",\"symbols\":[{\"name\",\"fqname\",\"kind\",\"file\",\"span\",\n \"signature\",\"called_by\":[{\"src\",\"file\",\"line\"}]}]}``.", "inputSchema": { "properties": { "as_of": { @@ -2666,7 +2666,7 @@ "readOnlyHint": false, "title": "Securely erase a leaked memory" }, - "description": "Irreversibly remove one accidentally stored secret from local persistence.\n\n Unlike retirement, this removes the memory, FTS/vector-index and derived graph/link\n rows, performs SQLite secure-delete/WAL/VACUUM maintenance, and scans recognised\n local SQLite recovery backups. It cannot erase copied exports, snapshots, remote\n peers, or data already read by a compromised/running agent; rotate the credential.\n Requires explicit local-operator confirmation (``confirmed=true``); the response\n carries the Store's ``impact`` report (receipt/event refs, backup note,\n WAL/vacuum status) for the rotation runbook (see docs/SYNC.md).\n ", + "description": "Irreversibly remove one accidentally stored secret from local persistence.\n\nUnlike retirement, this removes the memory, FTS/vector-index and derived graph/link\nrows, performs SQLite secure-delete/WAL/VACUUM maintenance, and scans recognised\nlocal SQLite recovery backups. It cannot erase copied exports, snapshots, remote\npeers, or data already read by a compromised/running agent; rotate the credential.\nRequires explicit local-operator confirmation (``confirmed=true``); the response\ncarries the Store's ``impact`` report (receipt/event refs, backup note,\nWAL/vacuum status) for the rotation runbook (see docs/SYNC.md).", "inputSchema": { "properties": { "confirmed": { @@ -2721,7 +2721,7 @@ "readOnlyHint": false, "title": "Start a memory session" }, - "description": "Open a session to group this work's memories and enable cross-session resume.\n\n Call this at the start of a task in a repo you've worked in before \u2014 if a previous\n session for the same authenticated user and agent was ended with a summary or open\n threads, they come back in ``bootstrap`` so you can resume without crossing another\n user or agent's handoff boundary.\n\n Exact retries are reused by default for the same ``(workspace, repo, authenticated\n user, agent, goal)`` identity. Different users, agents, or goals start distinct\n sessions automatically, and ``force_new=true`` always branches another session.\n Because that valid option creates a new row on every call, the tool as a whole is\n conservatively annotated as non-idempotent.\n\n Returns:\n str: JSON ``{\"session_id\",\"workspace\",\"repo\",\"goal\",\"status\":\"active\",\"reused\",\n \"bootstrap\":{\"summary\",\"open_threads\",\"outcome\"} or {} if there is no prior\n session}``. Pass ``session_id`` to engraphis_remember and engraphis_end_session.\n ", + "description": "Open a session to group this work's memories and enable cross-session resume.\n\nCall this at the start of a task in a repo you've worked in before \u2014 if a previous\nsession for the same authenticated user and agent was ended with a summary or open\nthreads, they come back in ``bootstrap`` so you can resume without crossing another\nuser or agent's handoff boundary.\n\nExact retries are reused by default for the same ``(workspace, repo, authenticated\nuser, agent, goal)`` identity. Different users, agents, or goals start distinct\nsessions automatically, and ``force_new=true`` always branches another session.\nBecause that valid option creates a new row on every call, the tool as a whole is\nconservatively annotated as non-idempotent.\n\nReturns:\n str: JSON ``{\"session_id\",\"workspace\",\"repo\",\"goal\",\"status\":\"active\",\"reused\",\n \"bootstrap\":{\"summary\",\"open_threads\",\"outcome\"} or {} if there is no prior\n session}``. Pass ``session_id`` to engraphis_remember and engraphis_end_session.", "inputSchema": { "properties": { "agent": { @@ -2780,7 +2780,7 @@ "readOnlyHint": true, "title": "Memory store stats" }, - "description": "Report memory counts (overall or for one workspace) \u2014 handy for onboarding/health.\n\n Returns:\n str: JSON ``{\"memories\",\"by_type\",\"workspaces\",\"sessions\",\"schema_version\"}``.\n ", + "description": "Report memory counts (overall or for one workspace) \u2014 handy for onboarding/health.\n\nReturns:\n str: JSON ``{\"memories\",\"by_type\",\"workspaces\",\"sessions\",\"schema_version\"}``.", "inputSchema": { "properties": { "workspace": { @@ -2811,7 +2811,7 @@ "readOnlyHint": true, "title": "Bi-temporal history of a fact" }, - "description": "Return every version of a fact in chronological order, including superseded ones.\n\n Use this for \"what did we believe and when\" / \"how has X changed over time\" \u2014 each\n entry carries ``valid_from``/``valid_to`` so you can see exactly when it was true.\n\n Returns:\n str: JSON ``{\"query\",\"history\":[{...memory fields..., \"valid_from\",\"valid_to\"}]}``\n oldest first. Raises an actionable error if the workspace/repo is unknown.\n ", + "description": "Return every version of a fact in chronological order, including superseded ones.\n\nUse this for \"what did we believe and when\" / \"how has X changed over time\" \u2014 each\nentry carries ``valid_from``/``valid_to`` so you can see exactly when it was true.\n\nReturns:\n str: JSON ``{\"query\",\"history\":[{...memory fields..., \"valid_from\",\"valid_to\"}]}``\n oldest first. Raises an actionable error if the workspace/repo is unknown.", "inputSchema": { "properties": { "limit": { @@ -2923,7 +2923,7 @@ "readOnlyHint": true, "title": "Explain the rationale behind a fact" }, - "description": "Surface the current answer *and* what it superseded, if anything.\n\n Use this for \"why is it like this\" / \"what did we used to do\" questions \u2014 it\n deliberately looks past the live view into bi-temporal history, which plain recall\n does not. The \"supersedes\" list is what makes this different from a vector search:\n those memories are no longer current but are not deleted, so the rationale chain\n (\"we used to do X, then switched to Y because Z\") stays answerable.\n\n Returns:\n str: JSON ``{\"query\",\"answer\":[...live memories...],\"supersedes\":[...what they\n replaced, if anything...]}``. Raises an actionable error if the workspace/repo\n is unknown.\n ", + "description": "Surface the current answer *and* what it superseded, if anything.\n\nUse this for \"why is it like this\" / \"what did we used to do\" questions \u2014 it\ndeliberately looks past the live view into bi-temporal history, which plain recall\ndoes not. The \"supersedes\" list is what makes this different from a vector search:\nthose memories are no longer current but are not deleted, so the rationale chain\n(\"we used to do X, then switched to Y because Z\") stays answerable.\n\nReturns:\n str: JSON ``{\"query\",\"answer\":[...live memories...],\"supersedes\":[...what they\n replaced, if anything...]}``. Raises an actionable error if the workspace/repo\n is unknown.", "inputSchema": { "properties": { "k": { @@ -2982,7 +2982,7 @@ "readOnlyHint": true, "title": "List pending/quarantined/conflicting memories" }, - "description": "Read-only inbox of pending/quarantined/conflicting memories for a reviewer.\n\n Scope and personal-folder authorization are enforced by ``MemoryService``. Pending\n and quarantined bodies are never returned to an agent; only approved conflict\n records may include a short excerpt.\n ", + "description": "Read-only inbox of pending/quarantined/conflicting memories for a reviewer.\n\nScope and personal-folder authorization are enforced by ``MemoryService``. Pending\nand quarantined bodies are never returned to an agent; only approved conflict\nrecords may include a short excerpt.", "inputSchema": { "properties": { "limit": { @@ -3161,7 +3161,7 @@ "readOnlyHint": true, "title": "Read one memory's governed record" }, - "description": "Return one memory's governed record (content, provenance, scope, temporal fields).\n\n Read-only and never reinforces. Pending/quarantined content is NOT returned to an\n agent \u2014 the tool answers ``not_prompt_eligible`` instead, so untrusted content never\n reaches model context through this surface.\n ", + "description": "Return one memory's governed record (content, provenance, scope, temporal fields).\n\nRead-only and never reinforces. Pending/quarantined content is NOT returned to an\nagent \u2014 the tool answers ``not_prompt_eligible`` instead, so untrusted content never\nreaches model context through this surface.", "inputSchema": { "properties": { "memory_id": { @@ -3497,7 +3497,7 @@ "readOnlyHint": false, "title": "Edit a memory's metadata fields" }, - "description": "Edit a memory's metadata fields (title/type/importance). An identical retry is an\n atomic no-op. Content edits must go through the governed correction path so bi-temporal\n history is preserved. Secret capture is rejected; provenance/trust/sensitivity are never\n editable here.", + "description": "Edit a memory's metadata fields (title/type/importance). An identical retry is an\natomic no-op. Content edits must go through the governed correction path so bi-temporal\nhistory is preserved. Secret capture is rejected; provenance/trust/sensitivity are never\neditable here.", "inputSchema": { "properties": { "actor": { diff --git a/docs/RELIABILITY_PROGRAM.md b/docs/RELIABILITY_PROGRAM.md index ca9d5bcb..a9bb4978 100644 --- a/docs/RELIABILITY_PROGRAM.md +++ b/docs/RELIABILITY_PROGRAM.md @@ -9,29 +9,31 @@ capacity, production readiness, or a completed user study. ## Source and delivery boundary -- Public implementation checkpoint: `c37ba0eb18408fe500cd70cd73fb5dcd7679b89c`, originally on - `feat/context-packing-and-perf-v2`. Delivery branch: `codex/reliable-agent-memory`. - Released v1.7.1 and the PR base are `cb03dbe104394b7760ef402a2b1917eac5e8accc`. +- Public implementation checkpoint: `c37ba0eb18408fe500cd70cd73fb5dcd7679b89c`, originally on + `feat/context-packing-and-perf-v2`. Delivery branch: `codex/reliable-agent-memory`. + Released v1.7.1 and the PR base are `cb03dbe104394b7760ef402a2b1917eac5e8accc`. - Private cloud base: `8cd1f3cb819c4bfb55ac44a74aa009a8b709efac`. -- Website changes are local to the adjacent `engraphis.com` repository. -- The authorized PR review includes the four original unmerged commits, the existing gist - documentation edit and website changes. Gist behavior and its documentation were corrected - together. No merge, deployment, credential rotation or release is authorized by PR submission. -- Exactly four internal workers contributed bounded implementation work, followed by a separate - four-worker review of core, interfaces, private cloud and historical local work. The parent integrated both batches. +- Review submissions: [public #201](https://github.com/Coding-Dev-Tools/engraphis/pull/201), + private Cloud #67, and dependent private website draft #12. Private source remains private. +- The authorized PR review includes the four original unmerged commits, the existing gist + documentation edit and website changes. Gist behavior and its documentation were corrected + together. No merge, deployment, credential rotation or release is authorized by PR submission. +- Exactly four internal workers contributed bounded implementation work, followed by a separate + four-worker review of core, interfaces, private cloud and historical local work. The parent integrated both batches. No descendant delegation, Orca routing or separate user-visible tasks were used. -- Current working-tree source hashes and results are recorded in [the evidence directory](evidence/reliability/). - A base commit alone does not identify the uncommitted implementation. +- Review checkpoint hashes and follow-up source overlays are recorded in [the evidence directory](evidence/reliability/). + Earlier receipts identify the bytes actually tested; they are not silently rebound to later changes. + The remaining public PR work uses an isolated checkout after the primary checkout moved to main. ## Findings register | ID | Classification and impact | Implemented response | Evidence / remaining boundary | |---|---|---|---| -| R01 | Reproduced: distinct facts disappeared in context packing | Remove cross-memory clause pruning; retain source-extractive summaries and complete-unit budget fallback | `tests/test_context_evidence_preservation.py`; numeric, condition, environment, title/source/pronoun bindings, multilingual and custom-counter cases. Existing memory identity/family deduplication remains. No semantic compression claim. | +| R01 | Reproduced: distinct facts disappeared in context packing | Remove cross-memory clause pruning; retain source-extractive summaries and complete-unit budget fallback | `tests/test_context_evidence_preservation.py`; numeric, condition, environment, title/source/pronoun bindings, multilingual and custom-counter cases. Existing memory identity/family deduplication remains. No semantic compression claim. | | R02 | Reproduced: browsing admitted future facts and hid still-current facts | Service listing delegates canonical Store temporal/scope predicates through `core/browsing.py` | `tests/test_memory_browsing.py`; current, future, expired, late-known and historical cases | | R03 | Reproduced: Library searched only its first fetched subset | Server text/type filtering, exact count and bounded cursor pages | 1,201-record oldest-result and complete unique traversal; browser loading/search/page recovery | | R04 | Reproduced: auxiliary Ask failure discarded a successful answer | Independent answer and preview state, deadlines and cancellation | Browser partial-success, timeout and stale-workspace cases | -| R05 | Reproduced: concurrent engines could insert duplicate writes; native batch publication could fail after canonical commit | Embed before writer reservation; discover/resolve/persist under SQLite transaction; store-sharing native batch publication joins that transaction | Separate-instance/process and native batch rollback tests in `test_storage_concurrency_repair.py` | +| R05 | Reproduced: concurrent engines could insert duplicate writes; native batch publication could fail after canonical commit | Embed before writer reservation; discover/resolve/persist under SQLite transaction; store-sharing native batch publication joins that transaction | Separate-instance/process and native batch rollback tests in `test_storage_concurrency_repair.py` | | R06 | Reproduced: incomplete derived index could miss canonical truth | Durable content-free repair work, idempotent retries, canonical fallback and readiness diagnostics | Outage, restart, erase and interrupted-repair tests; external adapters need stable index identity | | R07 | Reproduced: resolver evaluation missed false NOOP loss | Real write-path acceptance and false-NOOP/distinct-survival metrics; environment-role resolver correction | Original unit fixture: 44 pairs (38 corrections, six distinct facts). New real-write fixture: 10 pairs. Neither is independent held-out user evidence. JSON commands identify the actual dataset and retain input/source hashes. | | R08 | Reproduced quadratic fresh-insert work and measured candidate scan/verification costs | Fresh FTS inserts avoid full mirror scans while retaining orphan repair. Bounded scans sort the scoped first batch, then use vector-first keysets. Native verification checks every expected vector and exact unique-row cardinality | Controlled 10,000-row insertion comparison: 16.65 s with the former delete forced, 2.50 s corrected. Both final 10k/100k matrices completed. Concurrency, native restart and intermediate-scope latency remain material limits; details below. | @@ -40,11 +42,15 @@ capacity, production readiness, or a completed user study. | R11 | Source-backed hosted trust risk | Separate edge assertion secret, shared Durable Object budgets, persisted revocation | Actual workerd tests; production bindings, secret rotation and geographic behavior unverified | | R12 | Product-policy decision: readable processing needs explicit approval | Persisted local workspace policy and cloud revision-bound authority; legacy work paused | Local policy/browser tests, cloud migration/worker tests. Backend-first deployment remains required. | | R13 | Confirmed public contract drift | Team 10 days / Pro 3; secret-free cloud product export; generated MCP schemas consumed by Pi/Prime | Contract check spans public/cloud/site; site edits unpublished | -| R14 | Structural risk: concentrated modules | Narrow browsing, vector search/repair, setup profile and processing-control modules; compatibility facades retained | Broader Store/service/renderer and migration-executor extraction deferred to separately proven changes | -| R15 | Reproduced: MCP gist reread lost selected evidence and exceeded context budgets; response caps could detach qualifiers | Gist is a compatibility alias for canonical packed context; response caps keep or omit context whole and refresh usage | MCP budget, qualifier and retrieval-preservation regressions; generated contract refreshed | -| R16 | Reproduced: the unmerged 500-memory graph window excluded older two-hop evidence | Restore the established 12,000-memory window | Graph regression includes 501 newer unrelated memories; smaller windows require independent quality evidence | -| R17 | Reproduced: Classic consent copy described processing as enabled by default | Explain explicit workspace approval and link to the selected workspace's Ledger controls | Browser and authorization-placement tests; opening controls does not enable processing | -| R18 | Reproduced: concurrent SQLite cloud policy updates accepted stale enables; encoded auth paths escaped the stricter abuse budget | Conditional revision update with checked rowcount and first-row race handling; classify the upstream-equivalent decoded path | Independent SQLite policy writers and actual workerd regression; production deployment remains unverified | +| R14 | Structural risk: concentrated modules | Narrow browsing, vector search/repair, setup profile and processing-control modules; compatibility facades retained | Broader Store/service/renderer and migration-executor extraction deferred to separately proven changes | +| R15 | Reproduced: MCP gist reread lost selected evidence and exceeded context budgets; response caps could detach qualifiers | Gist is a compatibility alias for canonical packed context; response caps keep or omit context whole and refresh usage | MCP budget, qualifier and retrieval-preservation regressions; generated contract refreshed | +| R16 | Reproduced: the unmerged 500-memory graph window excluded older two-hop evidence | Restore the established 12,000-memory window | Graph regression includes 501 newer unrelated memories; smaller windows require independent quality evidence | +| R17 | Reproduced: Classic consent copy described processing as enabled by default | Explain explicit workspace approval and link to the selected workspace's Ledger controls | Browser and authorization-placement tests; opening controls does not enable processing | +| R18 | Reproduced: concurrent SQLite cloud policy updates accepted stale enables; encoded auth paths escaped the stricter abuse budget | Conditional revision update with checked rowcount and first-row race handling; classify the upstream-equivalent decoded path | Independent SQLite policy writers and actual workerd regression; production deployment remains unverified | +| R19 | Reproduced during GitHub review: successful title edits, erasure and embedding rebuilds could leave false index repair debt; late title publication could replay stale data | Publish title changes from canonical state and acknowledge only confirmed generations under the writer reservation; retain repair after failures | Delayed title, erasure/rebuild, rollback, generation and readiness regressions in `test_storage_concurrency_repair.py` | +| R20 | Query-plan reproduction: each large-backlog dequeue sorted remaining repairs | Cover the dequeue order with `(identity, generation, memory_id)` | Query-plan and existing-v17 reopen checks; no claim of a measured end-to-end speedup | +| R21 | Reproduced: editable upgrades treated unknown legacy installation intent as explicit base-only intent | Use the common extras selector for preview, install and rollback | 18 profile/override scenarios; updater and installation-profile suite | +| R22 | Reproduced on Python 3.13/3.14: MCP export changed because docstring indentation differed | Normalize tool descriptions with `inspect.cleandoc`; preserve schema/annotation and meaningful-description drift checks | Matched MCP/Pydantic runtimes; only 32 description fields and the digest differed | ## Architecture and compatibility decisions @@ -103,28 +109,28 @@ See [validation.json](evidence/reliability/validation.json) for commands, enviro counts, skips and source hashes. The deterministic tests do not stand in for user or paid-model evaluations. Browser approval controls use an isolated test token. -The final combined PR review suite passed **4,752 public tests**, with **37 skipped** and two warnings. -Production and test source stayed unchanged throughout that run; the before/after hashes and -skip reasons are in [the PR source receipt](evidence/reliability/public-pr-source-final.json). -The private suite passed **1,157 tests**, with **two PostgreSQL integration tests skipped**. -All seven required offline evaluation commands passed on the final public core/backend source. -Pi's 21 unit tests and actual MCP restart journey passed; Prime passed 136 tests with one skip. -The reviewed UI passed nine focused Chromium scenarios; the website passed 24 unit tests and -11 Chromium/axe scenarios. The edge passed 13 tests, including four actual workerd scenarios. -These focused results overlap other gates and are not a unique-test total. - -Earlier 4,736-public/1,155-private implementation checkpoints remain recorded separately. -The first combined PR run reproduced two obsolete assertions about automatic processing copy -and the Ledger cache version. Both failed in isolation, were corrected to enforce the current -approval contract, and passed before the clean complete rerun. The review also corrected two -misleading consent-error messages. [The review receipt](evidence/reliability/pr-review.json) -records the findings, historical-branch/stash reconciliation and remaining boundaries. +The initial combined local PR review suite passed **4,752 public tests**, with **37 skipped** and two warnings. +Production and test source stayed unchanged throughout that run; the before/after hashes and +skip reasons are in [the PR source receipt](evidence/reliability/public-pr-source-final.json). +The private suite passed **1,157 tests**, with **two PostgreSQL integration tests skipped**. +All seven required offline evaluation commands passed on that recorded core/backend checkpoint. +Pi's 21 unit tests and actual MCP restart journey passed; Prime passed 136 tests with one skip. +The reviewed UI passed nine focused Chromium scenarios; the website passed 24 unit tests and +11 Chromium/axe scenarios. The edge passed 13 tests, including four actual workerd scenarios. +These focused results overlap other gates and are not a unique-test total. + +Earlier 4,736-public/1,155-private implementation checkpoints remain recorded separately. +The first combined PR run reproduced two obsolete assertions about automatic processing copy +and the Ledger cache version. Both failed in isolation, were corrected to enforce the current +approval contract, and passed before the clean complete rerun. The review also corrected two +misleading consent-error messages. [The review receipt](evidence/reliability/pr-review.json) +records the findings, historical-branch/stash reconciliation and remaining boundaries. The [paid evaluation proposal](PAID_EVALUATION_PROPOSAL.md) specifies a 720-call matrix and a proposed $31 API limit. Its five-arm runner, frozen input selection and exact execution binding remain prerequisites; no paid run is authorized or recorded by this implementation. -Current local environment: Windows 11 build 26100, Python 3.12.10, SQLite 3.49.1, +Initial local validation environment: Windows 11 build 26100, Python 3.12.10, SQLite 3.49.1, NumPy 2.4.5, FastAPI 0.141.1, MCP 1.29.0, Pydantic 2.13.4. Native SQLite-vector checks use an isolated sqlite-vec 0.1.9 installation. Tests force `ENGRAPHIS_EXTRACTOR=none`. @@ -148,8 +154,13 @@ python -m eval.code_arm Run Pi and Prime tests from their own integration directories; their Python test package names otherwise collide with the root suite. Browser CI installs Chromium, -Firefox and WebKit. The full supported Python/platform CI matrix, remote PR checks, -PostgreSQL integration, Docker smoke and production restore remain separate evidence. +Firefox and WebKit. Remote checks are bound to each PR head and its tested merge tree. +Public checkpoint `b3bab7b7` passed Python 3.9 through 3.12, all 112 browser scenarios +and 85.02% coverage; Python 3.13/3.14 exposed the contract indentation issue in R22. +Cloud `25b5c8e` passed all three checks, including 82.50% branch coverage, real PostgreSQL +migration round trips and least-privilege roles, artifact/image gates and Team edge checks. +Production restore and production configuration remain unverified. +The follow-up receipts describe the later repairs; use the current PR checks for their remote status. ## Measured storage scale @@ -157,6 +168,9 @@ PostgreSQL integration, Docker smoke and production restore remain separate evid checksummed JSON artifacts include all 12 final backend/size/concurrency cells, mixed writes, reopen/rebuild checks, hardware, exact commands, source hashes and retained incomplete runs. Both final backends returned matching result IDs for identical synthetic inputs. +These measurements belong to the recorded pre-follow-up source checkpoint. Later repair-queue +schema/Store changes invalidate whole-file identity with that checkpoint; the old timings are +retained as historical evidence and are not measurements of the later PR head. At 100,000 total records, with 25% eligible for each scoped search: diff --git a/docs/evidence/reliability/validation.json b/docs/evidence/reliability/validation.json index 211765d1..af438890 100644 --- a/docs/evidence/reliability/validation.json +++ b/docs/evidence/reliability/validation.json @@ -3,7 +3,7 @@ "prepared_date": "2026-09-05", "status": "reviewed PR candidate; release and deployment approval remain separate", "source_manifest": "source-manifest.json", - "source_boundary": "The final candidate manifest identifies source relative to released main. The final PR public suite passed on stable production and test source; public-pr-source-final.json records the exact before/after hashes. Earlier failed checkpoints and corrections remain retained. Final report/evidence files are assembled separately. Results overlap and must not be added into a unique-test total.", + "source_boundary": "Initial PR review checkpoint. The complete public suite in public-pr-source-final.json is bound to its original before/after source hashes. Later fixes use explicit source overlays and their own validation, rather than rebinding old results. Report/evidence files are assembled separately. Results overlap and must not be added into a unique-test total.", "environment": { "os": "Windows 11 build 26100", "python": "3.12.10", @@ -510,8 +510,8 @@ "Browser engines were exercised on Windows; this is not macOS Safari or the full operating-system matrix.", "Benchmark reports define their own source hashes, hardware, dataset and measurement boundaries. The incomplete baseline is retained as failed measurement evidence.", "Optional cleanup of two cancelled-run synthetic temporary benchmark directories was blocked by automatic approval review with the stated reason 'blocked by policy'. No deletion occurred; exact paths/action are recorded in vector-scale-summary-20260905.md.", - "The final complete public run reports exact JUnit outcome counts. Its 37 skipped cases/reasons are recorded in public-complete-source-final.json.", - "After final production code froze, only two obsolete trial-UI test modules were corrected; the final complete public gate then passed. Source/report assembly afterward is independently recorded in the final manifest.", + "The initial combined PR complete run reports exact JUnit outcomes in public-pr-source-final.json, including its 37 skipped cases.", + "The initial combined PR suite followed two obsolete trial-UI assertion corrections. Subsequent GitHub feedback and platform CI repairs are recorded separately in source overlays.", "All 16 historical noncurrent branches and one stash were reconciled; no unique recovery candidate remains. Private details stay private. Branches, stash and auxiliary worktree were preserved." ], "latest_results": { @@ -532,5 +532,9 @@ "edge": "pr-edge-final", "website": "pr-website-review", "scale": "final-scale-matrix" - } + }, + "source_overlays": [ + "public-ci-test-isolation.json", + "pr-review-followup.json" + ] } diff --git a/engraphis/core/engine.py b/engraphis/core/engine.py index 290b6364..64a70a80 100644 --- a/engraphis/core/engine.py +++ b/engraphis/core/engine.py @@ -722,20 +722,33 @@ def _rebuild_versioned_embeddings(self) -> None: raise RuntimeError( "embedding rebuild was superseded by another process" ) + repair_target = index_repair_identity(self.index, self.store) + if repair_target is not None: + self.store.queue_vector_index_repairs(repair_target, excluded_ids) if excluded_ids: - if vector_index_requires_sync(self.index, self.store): - self.index.delete(excluded_ids, commit=False) marks = ",".join("?" for _ in excluded_ids) self.store.conn.execute( f"DELETE FROM mem_vectors WHERE id IN ({marks})", excluded_ids ) + if vector_index_requires_sync(self.index, self.store): + generations = self.store.vector_index_repair_generations( + repair_target, excluded_ids, + ) if repair_target is not None else {} + self.index.delete(excluded_ids, commit=False) + if repair_target is not None: + self.store.acknowledge_vector_index_repairs(repair_target, generations) # Keep the portable mirror current even when the active index is # sqlite-vec. A later NumPy fallback must see the same vector space. if vectors is not None: for record, vector in zip(eligible, vectors): self.store.put_vector(record.id, vector, model=fingerprint) if vector_index_requires_sync(self.index, self.store): + generations = self.store.vector_index_repair_generations( + repair_target, ids, + ) if repair_target is not None else {} _safe_upsert(self.index, ids, vectors, metadata, commit=False) + if repair_target is not None: + self.store.acknowledge_vector_index_repairs(repair_target, generations) self.store.conn.commit() removed += len(excluded_ids) rebuilt += len(eligible) @@ -2947,7 +2960,15 @@ def secure_erase(self, memory_id: str, *, actor: str = "user") -> dict: returned status explicitly reports that incomplete external cleanup. """ with self._write_lock: - target_ids = self.store.secure_erase_target_ids(memory_id) + repair_target = index_repair_identity(self.index, self.store) + transaction_started = not self.store.conn.transaction_owned_by_current_thread() + if repair_target is not None and not transaction_started: + raise RuntimeError( + "caller-owned transactions cannot erase through a separate vector " + "index; commit or roll back before erasing" + ) + attempted_ids: set[str] = set() + confirmed_ids: set[str] = set() index_cleanup = "not_configured" def delete_vectors(ids: list[str], *, in_store_transaction: bool = False) -> None: @@ -2961,43 +2982,48 @@ def delete_vectors(ids: list[str], *, in_store_transaction: bool = False) -> Non self.index.delete(ids) try: - delete_vectors(target_ids) - index_cleanup = "deleted" - except Exception: # noqa: BLE001 - must still erase the authoritative local copy - index_cleanup = "failed" - - # Serialize the final successor scan with the authoritative erase. Any - # successor that commits before BEGIN IMMEDIATE is acquired is included; - # a successor cannot commit between this scan and secure_erase_memory's - # destructive transaction. The external delete is intentionally performed - # while the Store transaction is held, so its final target set cannot go - # stale before the local rows are removed. - transaction_started = False - try: - if not self.store.conn.transaction_owned_by_current_thread(): - self.store.conn.execute("BEGIN IMMEDIATE") - transaction_started = True - refreshed = self.store.secure_erase_target_ids(memory_id) - new_ids = set(refreshed) - set(target_ids) - cleanup_ids = list(new_ids) if index_cleanup == "deleted" else [] - if cleanup_ids: + # Hold the writer through every provider delete and canonical + # erase. A competing repair cannot republish between them. + with self.store.write_transaction(): + target_ids = self.store.secure_erase_target_ids(memory_id) + if repair_target is not None: + self.store.queue_vector_index_repairs(repair_target, target_ids) + attempted_ids.update(target_ids) try: - delete_vectors(cleanup_ids, in_store_transaction=True) + delete_vectors(target_ids, in_store_transaction=True) + confirmed_ids.update(target_ids) + index_cleanup = "deleted" except Exception: # noqa: BLE001 - index_cleanup = "partial" - target_ids = refreshed - result = self.store.secure_erase_memory( - memory_id, actor=actor, _target_ids=target_ids, - _defer_maintenance=transaction_started, - ) - if transaction_started and self.store.conn.transaction_owned_by_current_thread(): - self.store.conn.commit() + index_cleanup = "failed" + refreshed = self.store.secure_erase_target_ids(memory_id) + if repair_target is not None: + self.store.queue_vector_index_repairs(repair_target, refreshed) + cleanup_ids = sorted(set(refreshed) - set(target_ids)) if index_cleanup == "deleted" else [] + if cleanup_ids: + attempted_ids.update(cleanup_ids) + try: + delete_vectors(cleanup_ids, in_store_transaction=True) + confirmed_ids.update(cleanup_ids) + except Exception: # noqa: BLE001 + index_cleanup = "partial" + result = self.store.secure_erase_memory( + memory_id, actor=actor, _target_ids=refreshed, + _defer_maintenance=transaction_started, + ) + if repair_target is not None: + self.store.acknowledge_vector_index_repairs( + repair_target, + self.store.vector_index_repair_generations(repair_target, confirmed_ids), + ) + if transaction_started: # Physical maintenance must run after the engine-owned transaction # commits; VACUUM is invalid while the erase transaction is active. result["maintenance"] = self.store.run_secure_erase_maintenance() except BaseException: - if transaction_started and self.store.conn.transaction_owned_by_current_thread(): - self.store.conn.rollback() + # Provider deletion cannot roll back with SQLite. Persist work + # for the restored canonical rows after the failed transaction. + if repair_target is not None and attempted_ids: + self.store.queue_vector_index_repairs(repair_target, attempted_ids) raise result["vector_index_cleanup"] = index_cleanup if index_cleanup in {"failed", "partial"}: diff --git a/engraphis/core/schema.py b/engraphis/core/schema.py index 3bffd6b6..f672e3e7 100644 --- a/engraphis/core/schema.py +++ b/engraphis/core/schema.py @@ -160,6 +160,8 @@ generation INTEGER NOT NULL, PRIMARY KEY(identity, memory_id) ); +CREATE INDEX IF NOT EXISTS idx_vector_index_repairs_queue + ON vector_index_repairs(identity, generation, memory_id); CREATE TRIGGER IF NOT EXISTS trg_vector_repair_insert AFTER INSERT ON mem_vectors BEGIN UPDATE vector_store_state SET generation=generation+1 WHERE singleton=1; diff --git a/engraphis/core/store.py b/engraphis/core/store.py index a213b4d0..b611b819 100644 --- a/engraphis/core/store.py +++ b/engraphis/core/store.py @@ -5348,6 +5348,43 @@ def vector_index_pending(self, identity: str) -> Optional[int]: ).fetchone() return int(row[0]) if row is not None else None + def queue_vector_index_repairs(self, identity: str, memory_ids: Iterable[str]) -> None: + """Queue explicit cleanup even when an orphan has no canonical vector row.""" + with self.write_transaction(): + self.register_vector_index(identity) + generation = self.vector_generation() + self.conn.executemany( + "INSERT INTO vector_index_repairs(identity,memory_id,generation) VALUES (?,?,?) " + "ON CONFLICT(identity,memory_id) DO UPDATE SET generation=excluded.generation", + [(identity, memory_id, generation) for memory_id in memory_ids], + ) + + def vector_index_repair_generations(self, identity: str, + memory_ids: Iterable[str]) -> dict[str, int]: + """Capture the debt generation before publishing under a writer reservation.""" + selected = list(memory_ids) + generations: dict[str, int] = {} + for offset in range(0, len(selected), 500): + batch = selected[offset:offset + 500] + marks = ",".join("?" for _ in batch) + for row in self.conn.execute( + "SELECT memory_id,generation FROM vector_index_repairs " + f"WHERE identity=? AND memory_id IN ({marks})", (identity, *batch), + ).fetchall(): + generations[str(row["memory_id"])] = int(row["generation"]) + return generations + + def acknowledge_vector_index_repairs(self, identity: str, + generations: dict[str, int]) -> None: + """Settle only confirmed generations; a newer canonical mutation stays queued.""" + with self.write_transaction(): + self.conn.executemany( + "DELETE FROM vector_index_repairs " + "WHERE identity=? AND memory_id=? AND generation=?", + [(identity, memory_id, generation) + for memory_id, generation in generations.items()], + ) + def vector_matrix(self, flt: Optional[SearchFilter] = None, *, include_invalid: bool = False, dim: int) -> tuple[list[str], np.ndarray]: """Materialize one filtered, fixed-width vector matrix for an exact scan. diff --git a/engraphis/service.py b/engraphis/service.py index 60e5a848..99272694 100644 --- a/engraphis/service.py +++ b/engraphis/service.py @@ -50,6 +50,7 @@ ) from engraphis.core.graph_layers import normalize_graph_layer from engraphis.core.context import RegexTokenCounter +from engraphis.core.vector_repair import index_repair_identity from engraphis.core.ids import new_id as make_id from engraphis.core.savings import annotate_usage, normalize_release_version from engraphis.core.interfaces import ( @@ -6508,16 +6509,14 @@ def _publish_memory_index_action( runs. A provider failure therefore becomes explicit repair debt; it must never be raised as though the canonical edit had rolled back. """ - operation, memory_id, vector, model = action + operation, memory_id, vector, _model = action try: - if operation == "delete": - self.engine.index.delete([memory_id]) - elif operation == "upsert" and vector is not None: - self.engine.index.upsert( - [memory_id], vector.reshape(1, -1), [{"model": model}], - ) - else: # pragma: no cover - action is constructed locally + if operation not in {"delete", "upsert"} or (operation == "upsert" and vector is None): raise RuntimeError("invalid deferred vector-index action") + # The captured payload may predate a later title edit or erasure. + # Replay current canonical state while holding the writer, and + # acknowledge only the generation actually published. + self.engine.repair_vector_index(limit=1, memory_id=memory_id) except Exception as exc: # noqa: BLE001 - canonical Store state is committed failure_type = type(exc).__name__ logger.warning( @@ -6597,6 +6596,9 @@ def _update_memory_transactional( pass if title_changed: text = f"{row['title']}\n{row['content']}" if row["title"] else row["content"] + repair_target = index_repair_identity(self.engine.index, self.store) + if repair_target is not None: + self.store.queue_vector_index_repairs(repair_target, [mid]) # Quarantined records and explicitly secret records are retained for # local governance only. A metadata edit must not turn either into a # semantic candidate or send its payload to an embedder. diff --git a/scripts/export_mcp_contract.py b/scripts/export_mcp_contract.py index baa8131a..59f5a4f9 100644 --- a/scripts/export_mcp_contract.py +++ b/scripts/export_mcp_contract.py @@ -3,6 +3,7 @@ import argparse import hashlib +import inspect import json from pathlib import Path from pprint import pformat @@ -18,7 +19,9 @@ def build_contract() -> dict: surfaces = {} for name, server in (("smart", smart_mcp), ("classic", classic_mcp)): surfaces[name] = [ - {"name": tool.name, "description": tool.description, + # Python 3.13+ removes common indentation from compiled docstrings. + # Normalize only that margin, retaining nested examples and all schema data. + {"name": tool.name, "description": inspect.cleandoc(tool.description), "inputSchema": tool.parameters, "annotations": tool.annotations.model_dump(mode="json", exclude_none=True) if tool.annotations else {}} @@ -50,7 +53,7 @@ def main() -> int: for path, expected in artifacts(build_contract()).items(): if args.check: if not path.exists() or path.read_text(encoding="utf-8") != expected: - stale.append(str(path.relative_to(ROOT))) + stale.append(path.relative_to(ROOT).as_posix()) else: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(expected, encoding="utf-8", newline="\n") diff --git a/scripts/update.py b/scripts/update.py index ae131764..d7ee8929 100644 --- a/scripts/update.py +++ b/scripts/update.py @@ -597,7 +597,7 @@ def _git_update(check_only: bool = False) -> None: # Resolve intent before switching the source tree, since an older release may # not contain the installation-profile module used by this updater. - editable_extras = _explicit_installation_extras() or "" + editable_extras = _installed_extras() install_target = str(project_dir) + editable_extras print(f"Update available: {local[:8]} -> {remote_sha[:8]} ({tag})") print(f"Editable install target: {install_target}") diff --git a/tests/test_mcp_contract.py b/tests/test_mcp_contract.py index b87ca207..79379f51 100644 --- a/tests/test_mcp_contract.py +++ b/tests/test_mcp_contract.py @@ -27,3 +27,64 @@ def test_contract_is_secret_free_public_metadata(): contract = json.loads(text) assert all(set(tool) == {"name", "description", "inputSchema", "annotations"} for surface in contract["surfaces"].values() for tool in surface) + +@pytest.mark.parametrize(("surface", "tool_name"), [ + ("classic", "engraphis_start_session"), + ("smart", "engraphis_update_memory"), +]) +def test_contract_normalizes_description_margin_without_losing_nested_text( + monkeypatch, surface, tool_name, +): + from engraphis.mcp_server import classic_mcp, smart_mcp + + server = {"classic": classic_mcp, "smart": smart_mcp}[surface] + tool = server._tool_manager._tools[tool_name] + description = ( + "Return the approved facts.\n\n" + "Returns:\n" + " A result with citations.\n\n" + "Example:\n" + " if approved:\n" + " recall()" + ) + # Python 3.12 preserves this margin in __doc__; 3.13+ removes it. + indented = "\n".join( + line if n == 0 else " " + line + for n, line in enumerate(description.split("\n")) + ) + "\n " + monkeypatch.setattr(tool, "description", indented) + old_runtime = build_contract() + monkeypatch.setattr(tool, "description", description) + new_runtime = build_contract() + + assert old_runtime == new_runtime + exported = next(item for item in old_runtime["surfaces"][surface] + if item["name"] == tool_name) + assert exported["description"] == description + + +@pytest.mark.parametrize("field", ["description", "parameters", "annotations"]) +def test_contract_check_still_rejects_meaningful_registered_tool_drift( + monkeypatch, capsys, field, +): + from copy import deepcopy + from engraphis.mcp_server import smart_mcp + from scripts import export_mcp_contract + + tool = smart_mcp._tool_manager._tools["engraphis_recall_context"] + if field == "description": + changed = tool.description + "\nExplicit approval is required." + elif field == "parameters": + changed = deepcopy(tool.parameters) + changed["properties"]["k"]["default"] += 1 + else: + changed = tool.annotations.model_copy( + update={"readOnlyHint": not tool.annotations.readOnlyHint} + ) + monkeypatch.setattr(tool, field, changed) + monkeypatch.setattr(export_mcp_contract.sys, "argv", ["export_mcp_contract.py", "--check"]) + + assert export_mcp_contract.main() == 1 + output = capsys.readouterr().out + assert "MCP contract drift:" in output + assert "MCP_CONTRACT.json" in output diff --git a/tests/test_storage_concurrency_repair.py b/tests/test_storage_concurrency_repair.py index 34a3a641..2ad75caa 100644 --- a/tests/test_storage_concurrency_repair.py +++ b/tests/test_storage_concurrency_repair.py @@ -395,3 +395,25 @@ def test_v16_upgrade_adds_durable_repair_without_changing_memories(tmp_path): with sqlite3.connect(path) as original: assert original.execute("SELECT valid_to FROM memories WHERE id=?", (memory,)).fetchone()[0] is None assert original.execute("SELECT count(*) FROM memories").fetchone()[0] == 1 + + +def test_repair_dequeue_uses_covering_order_index_without_sorting(): + engine = create_memory_engine(auto_evolve=False) + target = "query-plan-review" + try: + engine.store.register_vector_index(target) + engine.store.conn.executemany( + "INSERT INTO vector_index_repairs(identity,memory_id,generation) VALUES (?,?,?)", + [(target, f"mem_{index:05d}", (10_000 - index) // 3) for index in range(10_000)], + ) + engine.store.conn.commit() + sql = ("SELECT memory_id,generation FROM vector_index_repairs WHERE identity=? " + "ORDER BY generation,memory_id LIMIT 1") + plan = " ".join(str(row[3]) for row in engine.store.conn.execute( + "EXPLAIN QUERY PLAN " + sql, (target,), + ).fetchall()).upper() + assert "USE TEMP B-TREE" not in plan + assert "COVERING INDEX IDX_VECTOR_INDEX_REPAIRS_QUEUE" in plan + assert tuple(engine.store.conn.execute(sql, (target,)).fetchone()) == ("mem_09998", 0) + finally: + engine.close() diff --git a/tests/test_update.py b/tests/test_update.py index cb027a0f..c57cbbbe 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -230,6 +230,72 @@ def test_non_git_pep610_install_is_not_misclassified(monkeypatch): assert update._detect_install() == "pypi" +@pytest.mark.parametrize(("profile", "override", "extras"), [ + pytest.param(None, None, "[all]", id="unknown-legacy"), + pytest.param([], None, "", id="explicit-base"), + pytest.param(["server", "code"], None, "[code,server]", id="selected-profile"), + pytest.param(["code"], "server", "[server]", id="override-profile"), + pytest.param(["all"], "none", "", id="override-base"), + pytest.param(None, "code,server,code", "[code,server]", id="override-legacy"), +]) +@pytest.mark.parametrize("mode", ["check", "update", "rollback"]) +def test_editable_update_preserves_installation_capabilities( + monkeypatch, tmp_path, capsys, profile, override, extras, mode, +): + from scripts import installation_profile + + monkeypatch.delenv("ENGRAPHIS_UPDATE_EXTRAS", raising=False) + if profile is not None: + installation_profile.write_profile(profile) + if override is not None: + monkeypatch.setenv("ENGRAPHIS_UPDATE_EXTRAS", override) + project = tmp_path / "clone with spaces" + (project / ".git").mkdir(parents=True) + monkeypatch.setattr(update.shutil, "which", lambda _name: "git") + monkeypatch.setattr(update, "LATEST_TAG", "v1.2.3") + calls = [] + install_attempts = 0 + + def handler(command): + nonlocal install_attempts + if command[:4] == [sys.executable, "-m", "pip", "show"]: + return _FakeProcess(stdout=f"Editable project location: {project}\n") + if "rev-parse" in command: + return _FakeProcess(stdout="old-sha\n") + if "symbolic-ref" in command: + return _FakeProcess(stdout="main\n") + if "rev-list" in command: + return _FakeProcess(stdout="new-sha\n") + if command[:4] == [sys.executable, "-m", "pip", "install"]: + install_attempts += 1 + if mode == "rollback" and install_attempts == 1: + return _FakeProcess(returncode=1) + return _FakeProcess() + + monkeypatch.setattr(update.subprocess, "Popen", _spawner(handler, calls)) + if mode == "rollback": + with pytest.raises(subprocess.CalledProcessError): + update._git_update() + else: + update._git_update(check_only=mode == "check") + + target = str(project) + extras + assert f"Editable install target: {target}\n" in capsys.readouterr().out + commands = [command for command, _kwargs, _proc in calls] + installs = [ + command for command in commands + if command[:4] == [sys.executable, "-m", "pip", "install"] + ] + expected_count = {"check": 0, "update": 1, "rollback": 2}[mode] + assert installs == [ + [sys.executable, "-m", "pip", "install", "-e", target] + ] * expected_count + if mode == "check": + assert not any("checkout" in command for command in commands) + if mode == "rollback": + assert ["git", "-C", str(project), "checkout", "main"] in commands + + def test_failed_editable_reinstall_restores_original_branch(monkeypatch, tmp_path): project = tmp_path / "clone" (project / ".git").mkdir(parents=True) From 7148b72fe3d8976951124b622a354b89e0da1f0b Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Sat, 5 Sep 2026 07:12:54 -0400 Subject: [PATCH 09/10] fix: preserve evidence and repair recreated external indexes --- docs/RELIABILITY_PROGRAM.md | 8 +- docs/evidence/reliability/catalog.json | 27 +- docs/evidence/reliability/catalog.json.sha256 | 2 +- .../offline-gates-review-followup.json | 149 ++++++++ .../pr-benchmark-source-check.json | 21 +- .../reliability/pr-review-followup.json | 124 +++++++ .../resolver-title-unit-final.json | 1 + .../resolver-title-write-final.json | 1 + engraphis/core/engine.py | 17 +- engraphis/core/resolve.py | 24 +- engraphis/core/store.py | 9 +- engraphis/core/vector_repair.py | 3 + eval/datasets/resolver_write_acceptance.jsonl | 2 + eval/resolver_reworded_corrections.py | 22 +- tests/test_resolve.py | 68 ++++ tests/test_storage_concurrency_repair.py | 338 +++++++++++++++++- 16 files changed, 788 insertions(+), 28 deletions(-) create mode 100644 docs/evidence/reliability/offline-gates-review-followup.json create mode 100644 docs/evidence/reliability/pr-review-followup.json create mode 100644 docs/evidence/reliability/resolver-title-unit-final.json create mode 100644 docs/evidence/reliability/resolver-title-write-final.json diff --git a/docs/RELIABILITY_PROGRAM.md b/docs/RELIABILITY_PROGRAM.md index a9bb4978..810d5b75 100644 --- a/docs/RELIABILITY_PROGRAM.md +++ b/docs/RELIABILITY_PROGRAM.md @@ -35,7 +35,7 @@ capacity, production readiness, or a completed user study. | R04 | Reproduced: auxiliary Ask failure discarded a successful answer | Independent answer and preview state, deadlines and cancellation | Browser partial-success, timeout and stale-workspace cases | | R05 | Reproduced: concurrent engines could insert duplicate writes; native batch publication could fail after canonical commit | Embed before writer reservation; discover/resolve/persist under SQLite transaction; store-sharing native batch publication joins that transaction | Separate-instance/process and native batch rollback tests in `test_storage_concurrency_repair.py` | | R06 | Reproduced: incomplete derived index could miss canonical truth | Durable content-free repair work, idempotent retries, canonical fallback and readiness diagnostics | Outage, restart, erase and interrupted-repair tests; external adapters need stable index identity | -| R07 | Reproduced: resolver evaluation missed false NOOP loss | Real write-path acceptance and false-NOOP/distinct-survival metrics; environment-role resolver correction | Original unit fixture: 44 pairs (38 corrections, six distinct facts). New real-write fixture: 10 pairs. Neither is independent held-out user evidence. JSON commands identify the actual dataset and retain input/source hashes. | +| R07 | Reproduced: resolver evaluation missed false NOOP loss | Real write-path acceptance and false-NOOP/distinct-survival metrics; environment-role resolver correction | Original unit fixture: 44 pairs (38 corrections, six distinct facts). Initial real-write fixture: 10 pairs; the reviewed title-binding extension brings it to 12. Neither is independent held-out user evidence. JSON commands identify the actual dataset and retain input/source hashes. | | R08 | Reproduced quadratic fresh-insert work and measured candidate scan/verification costs | Fresh FTS inserts avoid full mirror scans while retaining orphan repair. Bounded scans sort the scoped first batch, then use vector-first keysets. Native verification checks every expected vector and exact unique-row cardinality | Controlled 10,000-row insertion comparison: 16.65 s with the former delete forced, 2.50 s corrected. Both final 10k/100k matrices completed. Concurrency, native restart and intermediate-scope latency remain material limits; details below. | | R09 | Reproduced diagnostic gaps | Real rolled-back write probe; JSON doctor; private tokens for new setup; installation intent profiles | Setup/update regression tests; live Windows evidence only | | R10 | Reproduced Windows long-path object-store failure | Confined extended paths and short unique staging filenames | Cloud object-store long-path regression and full suite | @@ -51,6 +51,8 @@ capacity, production readiness, or a completed user study. | R20 | Query-plan reproduction: each large-backlog dequeue sorted remaining repairs | Cover the dequeue order with `(identity, generation, memory_id)` | Query-plan and existing-v17 reopen checks; no claim of a measured end-to-end speedup | | R21 | Reproduced: editable upgrades treated unknown legacy installation intent as explicit base-only intent | Use the common extras selector for preview, install and rollback | 18 profile/override scenarios; updater and installation-profile suite | | R22 | Reproduced on Python 3.13/3.14: MCP export changed because docstring indentation differed | Normalize tool descriptions with `inspect.cleandoc`; preserve schema/annotation and meaningful-description drift checks | Matched MCP/Pydantic runtimes; only 32 description fields and the digest differed | +| R23 | Reproduced: a recreated external index with the same identity could be treated as complete | Honor the adapter rebuild signal, reseed canonical rows and retain repair on failure; keep healthy startup incremental | Same-identity restart cases include historical vectors, failed rebuild/retry and healthy startup | +| R24 | Reproduced: reordered display-title environments caused identical writes to be stored twice | Ignore only permutations of bare environment labels on identical content; preserve factual title and content bindings | Duplicate-label regressions plus real-write factual-title, arrow and body-binding protection; two titled acceptance cases | ## Architecture and compatibility decisions @@ -60,7 +62,9 @@ capacity, production readiness, or a completed user study. 2. Derived vectors remain repairable. `MemoryEngine.repair_vector_index(limit=100)` explicitly retries durable work. A stable per-index `index_identity` is required for trustworthy completeness. Unidentified external adapters use canonical search conservatively. - No background repair scheduler was added. + No background repair scheduler was added. External-index erasure requires an engine-owned + transaction: callers must finish a manually opened transaction before erasing, so a later + rollback cannot discard repair debt after provider deletion. Ordinary engine calls remain unchanged. 3. Schema 17 is additive: vector generation, index targets and pending memory IDs contain no memory text or credentials. Existing transactional migration and verified pre-migration backup behavior remain intact. Dirty native startup retains a full verification/rebuild; diff --git a/docs/evidence/reliability/catalog.json b/docs/evidence/reliability/catalog.json index edbabd34..ca3bae07 100644 --- a/docs/evidence/reliability/catalog.json +++ b/docs/evidence/reliability/catalog.json @@ -33,13 +33,21 @@ "sha256": "1dd10fe157fc656bdb43785598362f02c6a657a2f9de006670b92e6e1f637c18", "bytes": 27300 }, + "offline-gates-review-followup.json": { + "sha256": "dc6dcc75d293f6becaf42411ee2ca6c50c5f31bf2d354d9a5d9887223a37122d", + "bytes": 5972 + }, "offline-gates.json": { "sha256": "aa2f2e3e0312491d57e6d6571dbb92a96570c8655f55bfe7b63a3dc6ca2fc2dc", "bytes": 3663 }, "pr-benchmark-source-check.json": { - "sha256": "08886c2f7afb7fcb51513ce23b3234dc9ab46362e6349706248f9a0a84f00db1", - "bytes": 1164 + "sha256": "af6a1ecc06f933227dc07d3fb19ae184790afd40b639ca4226f77086246ed670", + "bytes": 1795 + }, + "pr-review-followup.json": { + "sha256": "dc63ca917127732d239ac60ea5ee1aa66dc44b6992db15a2f3ca5ca7e4aa802d", + "bytes": 8320 }, "pr-review.json": { "sha256": "389291bb867e5e1c63c45c1f704079de49afbe91d8e9f05cb21f64cec41a2bc1", @@ -65,6 +73,14 @@ "sha256": "f5683a78cb71e70810ea2bcde046e8c9a428159f0ab92c8cdcc4b5f151f8449d", "bytes": 27677 }, + "resolver-title-unit-final.json": { + "sha256": "4f7139c874e16c2eca6e58a57be478d34b99283b2972ce307c183533755f8c47", + "bytes": 3171 + }, + "resolver-title-write-final.json": { + "sha256": "d837833fbd181916ebb299ca7bdcb9846433af23db4428c995816ecc0a76493f", + "bytes": 3676 + }, "resolver-unit-20260905.json": { "sha256": "7559397b4d0a003f32d0a0eae5b7925dd3cd4109de05c63f8c24c34583e82a76", "bytes": 3173 @@ -86,8 +102,8 @@ "bytes": 20106 }, "validation.json": { - "sha256": "54b8ef400f488ce3c9aa5cca93e6684341b77e36b35d6bf0e855ab6364ff9e78", - "bytes": 21607 + "sha256": "b1352fbac30794374084073ab2703cda7e3b15fbe3afe462a98b75bc670c57a4", + "bytes": 21616 }, "vector-scale-incomplete-baseline-20260905.json": { "sha256": "45c4f61518012e35e09b6d274dec3015d00d4948f2b8279c0f56760dec080544", @@ -159,6 +175,7 @@ } }, "source_overlays": [ - "public-ci-test-isolation.json" + "public-ci-test-isolation.json", + "pr-review-followup.json" ] } diff --git a/docs/evidence/reliability/catalog.json.sha256 b/docs/evidence/reliability/catalog.json.sha256 index 6f3d1d8f..7f22692a 100644 --- a/docs/evidence/reliability/catalog.json.sha256 +++ b/docs/evidence/reliability/catalog.json.sha256 @@ -1 +1 @@ -0144b724ab3a4826e87ab3ad6d915e51253b31b1f09a21ae07765a86a930fa32 catalog.json +f29fbf50ecb672a518ea3c624888451f92a11ee000c018511f2ea23bddee6a80 catalog.json diff --git a/docs/evidence/reliability/offline-gates-review-followup.json b/docs/evidence/reliability/offline-gates-review-followup.json new file mode 100644 index 00000000..9a0085cc --- /dev/null +++ b/docs/evidence/reliability/offline-gates-review-followup.json @@ -0,0 +1,149 @@ +{ + "schema": "engraphis-followup-offline-gates/v1", + "date": "2026-09-05T11:04:39.101189+00:00", + "base": "b48850a9cfe38d02b38b1563961700ab534372f4", + "environment": { + "python": "3.12.10", + "ENGRAPHIS_EXTRACTOR": "none" + }, + "source_before": { + "engraphis/core/engine.py": "5307b6492c8c2ec7852ed490c99ef366061b4ea2c9325ff5abb1c51aac8db9b8", + "engraphis/core/resolve.py": "f01a6f55e44320ab04b97e516342f20155668863b2fc4765305e066d87586524", + "engraphis/core/schema.py": "88b2643f3905a4b27907a13dcb9379525a7cb4cf844b57aa6ef14d610b6cb312", + "engraphis/core/store.py": "4d5670a8e1a1d13c84db572f7598a4511c7804df4a26da28bede6d587d0454c5", + "engraphis/core/vector_repair.py": "0483a25ab21880479087d0e75d3d63887e0b1bed9f439c8650b8028c1294305f", + "engraphis/service.py": "5fc307739c238bbf646a549b1545f1759588b7dbd621019b41201662c039c675", + "eval/datasets/resolver_write_acceptance.jsonl": "757eb8aa46f691d11db700c4908419dce2044bb55a0b89158c2c6880d66dcaeb", + "eval/resolver_reworded_corrections.py": "a9054778a37b2175b46f04b674b4779e358931eee5d2ae52bbc5f953d234fb9e", + "scripts/export_mcp_contract.py": "5ddbd51fa9e1e2604b75596af532584ecb6ab775eabb39f428c7001e6580c336", + "scripts/update.py": "739bc1550aebf9209a07ab5a4ab56794adab2f4a01c3577bac2e1e08b7b17f23", + "tests/test_mcp_contract.py": "bdb06750dcbcc75f20ca106274a0df66abf7153059656a139bb2d0f6019bc36b", + "tests/test_resolve.py": "b748526cd931ddee6bb659c3dccc431b93725883af71f723f21eedd58e867c75", + "tests/test_storage_concurrency_repair.py": "5a42bc5883667695c3a8dc0b7e7e757b0576ce5ee6deb685a525f9768baa00c6", + "tests/test_update.py": "29f68743db2426df193fb1ef74e3e7f31fc4f8842f668ac2c97faea39369b092" + }, + "source_after": { + "engraphis/core/engine.py": "5307b6492c8c2ec7852ed490c99ef366061b4ea2c9325ff5abb1c51aac8db9b8", + "engraphis/core/resolve.py": "f01a6f55e44320ab04b97e516342f20155668863b2fc4765305e066d87586524", + "engraphis/core/schema.py": "88b2643f3905a4b27907a13dcb9379525a7cb4cf844b57aa6ef14d610b6cb312", + "engraphis/core/store.py": "4d5670a8e1a1d13c84db572f7598a4511c7804df4a26da28bede6d587d0454c5", + "engraphis/core/vector_repair.py": "0483a25ab21880479087d0e75d3d63887e0b1bed9f439c8650b8028c1294305f", + "engraphis/service.py": "5fc307739c238bbf646a549b1545f1759588b7dbd621019b41201662c039c675", + "eval/datasets/resolver_write_acceptance.jsonl": "757eb8aa46f691d11db700c4908419dce2044bb55a0b89158c2c6880d66dcaeb", + "eval/resolver_reworded_corrections.py": "a9054778a37b2175b46f04b674b4779e358931eee5d2ae52bbc5f953d234fb9e", + "scripts/export_mcp_contract.py": "5ddbd51fa9e1e2604b75596af532584ecb6ab775eabb39f428c7001e6580c336", + "scripts/update.py": "739bc1550aebf9209a07ab5a4ab56794adab2f4a01c3577bac2e1e08b7b17f23", + "tests/test_mcp_contract.py": "bdb06750dcbcc75f20ca106274a0df66abf7153059656a139bb2d0f6019bc36b", + "tests/test_resolve.py": "b748526cd931ddee6bb659c3dccc431b93725883af71f723f21eedd58e867c75", + "tests/test_storage_concurrency_repair.py": "5a42bc5883667695c3a8dc0b7e7e757b0576ce5ee6deb685a525f9768baa00c6", + "tests/test_update.py": "29f68743db2426df193fb1ef74e3e7f31fc4f8842f668ac2c97faea39369b092" + }, + "source_stable": true, + "checks": [ + { + "command": [ + "python", + "-m", + "eval.harness", + "--dataset", + "eval/datasets/sample.jsonl", + "--k", + "5" + ], + "exit_code": 0, + "elapsed_seconds": 0.56, + "log_sha256": "7fc4615bab013ff30f1b3b5815d071e029b755e24988f15388b44900fc0b861d" + }, + { + "command": [ + "python", + "-m", + "eval.harness", + "--dataset", + "eval/datasets/codemem.jsonl", + "--k", + "5" + ], + "exit_code": 0, + "elapsed_seconds": 0.775, + "log_sha256": "9f7b0d7d8446b8cb5c145fc1170a17cebfc9ef5e09dd5d1631950fa81d40b793" + }, + { + "command": [ + "python", + "-m", + "eval.ablation" + ], + "exit_code": 0, + "elapsed_seconds": 1.078, + "log_sha256": "cb618538f9de2fa1bc0f2c2f27b5e6779c6b51901bf43fc1deb2c6b53aafb240" + }, + { + "command": [ + "python", + "-m", + "eval.reinforcement" + ], + "exit_code": 0, + "elapsed_seconds": 0.438, + "log_sha256": "a1a4bac6e675f9492c5a350d57aeabaaed11b1e0ff8bace6a7260b4395e5ece4" + }, + { + "command": [ + "python", + "-m", + "eval.adversarial_memory_security" + ], + "exit_code": 0, + "elapsed_seconds": 0.758, + "log_sha256": "001a236fa8db72e086a608eb2adfc58caae129f9e48645c21a7e0de735da2b8e" + }, + { + "command": [ + "python", + "-m", + "eval.grounded" + ], + "exit_code": 0, + "elapsed_seconds": 0.776, + "log_sha256": "b523a71e90eb48d476edf3aa76dc14a82879037ece43d62f4bb24a389f7a8bd2" + }, + { + "command": [ + "python", + "-m", + "eval.code_arm" + ], + "exit_code": 0, + "elapsed_seconds": 1.069, + "log_sha256": "1650f56a85ccf130f8e42d0b386cce4afaa2287d45807e68f884ffda42f020d0" + }, + { + "command": [ + "python", + "-m", + "eval.resolver_reworded_corrections", + "--json", + "--dataset", + "eval/datasets/resolver_reworded_corrections.jsonl" + ], + "exit_code": 0, + "elapsed_seconds": 0.652, + "log_sha256": "4f7139c874e16c2eca6e58a57be478d34b99283b2972ce307c183533755f8c47" + }, + { + "command": [ + "python", + "-m", + "eval.resolver_reworded_corrections", + "--json", + "--end-to-end", + "--dataset", + "eval/datasets/resolver_write_acceptance.jsonl" + ], + "exit_code": 0, + "elapsed_seconds": 1.145, + "log_sha256": "d837833fbd181916ebb299ca7bdcb9846433af23db4428c995816ecc0a76493f" + } + ] +} diff --git a/docs/evidence/reliability/pr-benchmark-source-check.json b/docs/evidence/reliability/pr-benchmark-source-check.json index 23d7eb35..07132315 100644 --- a/docs/evidence/reliability/pr-benchmark-source-check.json +++ b/docs/evidence/reliability/pr-benchmark-source-check.json @@ -14,8 +14,12 @@ "engraphis/core/schema.py", "engraphis/core/interfaces.py" ], - "all_measured_source_hashes_match": true, - "cells": 6 + "all_measured_source_hashes_match": false, + "cells": 6, + "changed_since_measurement": [ + "engraphis/core/store.py", + "engraphis/core/schema.py" + ] }, { "artifact": "vector-scale-sqlite-vec-corrected-20260905.json", @@ -30,8 +34,15 @@ "engraphis/core/schema.py", "engraphis/core/interfaces.py" ], - "all_measured_source_hashes_match": true, - "cells": 6 + "all_measured_source_hashes_match": false, + "cells": 6, + "changed_since_measurement": [ + "engraphis/core/store.py", + "engraphis/core/schema.py" + ] } - ] + ], + "checked_checkpoint": "b48850a9cfe38d02b38b1563961700ab534372f4 plus follow-up source overlay", + "earlier_matching_checkpoint": "b3bab7b7f1d7a9737090eec2a8d1d541a6cec741", + "interpretation": "Measured artifacts remain intact and reproducible at their recorded source hashes. The schema/Store repair changes were not benchmarked; old timings do not characterize the later PR head." } diff --git a/docs/evidence/reliability/pr-review-followup.json b/docs/evidence/reliability/pr-review-followup.json new file mode 100644 index 00000000..3c246d3f --- /dev/null +++ b/docs/evidence/reliability/pr-review-followup.json @@ -0,0 +1,124 @@ +{ + "schema": "engraphis-pr-review-followup/v1", + "recorded_at": "2026-09-05T11:12:04.101400+00:00", + "pull_request": "https://github.com/Coding-Dev-Tools/engraphis/pull/201", + "baseline": "b3bab7b7f1d7a9737090eec2a8d1d541a6cec741", + "committed_review_checkpoint": "b48850a9cfe38d02b38b1563961700ab534372f4", + "source_boundary": "Working-tree bytes, including line endings, at the final local review. This overlays source-manifest.json and public-ci-test-isolation.json; it does not relabel earlier complete-suite or benchmark measurements.", + "source_sha256": { + "docs/MCP_CONTRACT.json": "302bd4b23fce0921439b78362b0295f6d176465322e216cdaf2d86ef6adfec84", + "docs/RELIABILITY_PROGRAM.md": "201b8476ec355bc1965fda43ce7d465489897450030c84ec9f47796ee1d7957b", + "engraphis/core/engine.py": "5307b6492c8c2ec7852ed490c99ef366061b4ea2c9325ff5abb1c51aac8db9b8", + "engraphis/core/resolve.py": "f01a6f55e44320ab04b97e516342f20155668863b2fc4765305e066d87586524", + "engraphis/core/schema.py": "88b2643f3905a4b27907a13dcb9379525a7cb4cf844b57aa6ef14d610b6cb312", + "engraphis/core/store.py": "4d5670a8e1a1d13c84db572f7598a4511c7804df4a26da28bede6d587d0454c5", + "engraphis/core/vector_repair.py": "0483a25ab21880479087d0e75d3d63887e0b1bed9f439c8650b8028c1294305f", + "engraphis/service.py": "5fc307739c238bbf646a549b1545f1759588b7dbd621019b41201662c039c675", + "eval/datasets/resolver_write_acceptance.jsonl": "757eb8aa46f691d11db700c4908419dce2044bb55a0b89158c2c6880d66dcaeb", + "eval/resolver_reworded_corrections.py": "a9054778a37b2175b46f04b674b4779e358931eee5d2ae52bbc5f953d234fb9e", + "integrations/pi/src/generated-contract.ts": "906711cba159d75b8c5440f12796be78cd2f8f4b423971e4009bb3c0d61223c6", + "integrations/prime_agent/src/engraphis_prime_agent/_contract.py": "4b868447886cc7f6708c1667b5807882c822728db2b0efb4860283ba4fac400b", + "scripts/export_mcp_contract.py": "5ddbd51fa9e1e2604b75596af532584ecb6ab775eabb39f428c7001e6580c336", + "scripts/update.py": "739bc1550aebf9209a07ab5a4ab56794adab2f4a01c3577bac2e1e08b7b17f23", + "tests/test_mcp_contract.py": "bdb06750dcbcc75f20ca106274a0df66abf7153059656a139bb2d0f6019bc36b", + "tests/test_resolve.py": "b748526cd931ddee6bb659c3dccc431b93725883af71f723f21eedd58e867c75", + "tests/test_storage_concurrency_repair.py": "5a42bc5883667695c3a8dc0b7e7e757b0576ce5ee6deb685a525f9768baa00c6", + "tests/test_update.py": "29f68743db2426df193fb1ef74e3e7f31fc4f8842f668ac2c97faea39369b092" + }, + "review": { + "coordination": "Exactly four bounded internal reviewers, one level, with parent integration. No Orca or separate user-visible task delegation.", + "result": "Scoped follow-up changes reviewed and accepted for PR submission. This is engineering review, not production verification or authorization to merge.", + "fixes": [ + "Acknowledge successful direct publication by captured generation; replay current canonical state after title edits and keep newer repairs.", + "Serialize external cleanup and canonical erasure; preserve repair on rollback and cleanup failure. Reject caller-owned external-index erase transactions before provider side effects.", + "Use an indexed repair dequeue order; existing schema-17 databases gain the index on reopen without a version bump.", + "Honor requires_rebuild on recreated same-identity external indexes, including historical vectors, and use immediate canonical fallback until ready.", + "Preserve unknown legacy editable-install capabilities through the existing extras selector, including preview and rollback.", + "Normalize only MCP description indentation across Python versions; preserve meaningful description, schema and annotation drift detection.", + "Deduplicate permutations of bare environment labels only when content is identical; retain factual title and body bindings." + ], + "title_counterexample": "An intermediate body-only environment check lost different source/destination relationships stated in titles. It was rejected before commit; final regressions and two real-write corpus cases preserve these differences." + }, + "validation": [ + { + "name": "repair, recall, service, secret hygiene, native vector and batch writes", + "command": "$env:ENGRAPHIS_EXTRACTOR='none'; $env:PYTHONPATH='C:/Users/jomie/AppData/Local/Temp/engraphis-review-sqlitevec-c37ba0e'; python -m pytest tests/test_storage_concurrency_repair.py tests/test_service.py tests/test_secret_hygiene.py tests/test_recall_recovery.py tests/test_remember_many.py tests/test_vector_sqlitevec_backend.py tests/test_recall.py tests/test_retrieval_policy.py -q -rs -o addopts= --tb=short", + "result": { + "exit_code": 0, + "passed": 281, + "skipped": 1, + "warnings": 1, + "seconds": 72.48 + }, + "skip": "tests/test_service.py:1674: symlinks not supported in this environment", + "raw_log_sha256": "f419e20dd6b4a904d0b275df88836dec2d1da9c9ec1e24e01025c45313ecc9d3" + }, + { + "name": "resolver and engine focused gate", + "command": "ENGRAPHIS_EXTRACTOR=none python -m pytest tests/test_resolve.py tests/test_resolver_acceptance.py tests/test_engine.py -q -rs -o addopts= --tb=short", + "passed": 185, + "skipped": 2, + "seconds": 7.36, + "skip_reason": "Windows symlink support unavailable in the two engine cases.", + "tool_output_chunk": "c7a3f5" + }, + { + "name": "editable updater and installation profile", + "passed": 64, + "new_behavior_cases": 18, + "before": "3 failed and 15 passed in the profile matrix", + "boundary": "Capability matrix mocks package and git mutation; no actual user installation upgraded." + }, + { + "name": "MCP contract supported-runtime comparison", + "python": [ + "3.12.10", + "3.13.13", + "3.14.7" + ], + "mcp": "1.29.0", + "pydantic": "2.13.4", + "passed_each": 7, + "byte_identical": true, + "contract_digest": "1f0b4351e3a3d10b4404a86f47e30dfa85922f3aa08a86ce723e64aba6c793a3" + }, + { + "name": "offline evaluation", + "required_gates_passed": 7, + "additional_resolver_gates_passed": 2, + "receipt": "offline-gates-review-followup.json", + "unit_rows": 44, + "real_write_rows": 12, + "false_invalidations": 0, + "false_noops": 0, + "lost_distinct_facts": 0 + }, + { + "name": "static and contract gates", + "checks": [ + "ruff check .", + "pyright", + "MCP contract --check", + "commercial manifest", + "strict-CSP external assets", + "git diff --check" + ], + "result": "passed", + "warning": "Installed MCP/Pydantic emits an existing unresolved lifespan annotation warning; typecheck reports zero errors and warnings." + } + ], + "compatibility": { + "schema_version": 17, + "additional_version_bump": false, + "migration": "The existing schema-17 migration and backup instructions still apply. The repair-order index is installed idempotently for existing version-17 databases.", + "external_erase_transaction": "An injected separate-index erasure must own its transaction. Finish a caller-opened SQLite transaction before erasing; this prevents rolling back durable compensation after provider deletion.", + "rollback": "Use the documented pre-migration backup and restoration procedure for rollout reversal. These incremental repair changes retain the schema and canonical history; reverting their fixes reintroduces the recorded defects." + }, + "boundaries": [ + "Final-head remote checks are separate from these local receipts and must be checked after push.", + "Earlier complete-suite and browser counts remain tied to their recorded commits.", + "The two backend benchmark artifacts contain twelve cells in total and remain historical measurements; Store and schema hashes changed after measurement, as recorded in pr-benchmark-source-check.json.", + "No independent user study, full 100000-memory agent workload, paid model evaluation or production recovery exercise is claimed.", + "No merge, deployment, credential rotation, stash removal or review-thread resolution was performed." + ] +} diff --git a/docs/evidence/reliability/resolver-title-unit-final.json b/docs/evidence/reliability/resolver-title-unit-final.json new file mode 100644 index 00000000..09d8c2c8 --- /dev/null +++ b/docs/evidence/reliability/resolver-title-unit-final.json @@ -0,0 +1 @@ +{"environment": {"implementation": "CPython", "machine": "AMD64", "packages": {"engraphis": "1.6", "numpy": "2.4.5", "sentence-transformers": "6.0.0", "torch": "2.13.0", "transformers": "5.15.1"}, "platform": "Windows-11-10.0.26100-SP0", "python": "3.12.10"}, "exclusions": [], "metrics": {"correction_precision": 1.0, "correction_recall": 1.0, "distinct_fact_error_rate": 0.0, "execution": "resolver_unit", "false_invalidation_ids": [], "false_invalidations": 0, "false_noop_ids": [], "false_noops": 0, "lost_distinct_fact_ids": [], "lost_distinct_facts": 0, "missed_correction_ids": [], "missed_corrections": 0, "negatives": 6, "positives": 38, "positives_superseded": 38, "similarity_injected": true, "total": 44}, "models": {}, "privacy": {"content_fingerprint_policy": "omitted", "raw_answer_policy": "omitted", "raw_context_policy": "omitted", "raw_query_policy": "omitted"}, "protocol": {"command": ["python", "-m", "eval.resolver_reworded_corrections", "--dataset", "eval/datasets/resolver_reworded_corrections.jsonl", "--json"], "config": {"end_to_end": false, "evidence_scope": "authored regression corpus; not an independent quality benchmark", "offline": true}, "n_scored": 44, "n_total": 44, "token_accounting": {"identity": "unspecified", "method": "unspecified", "revision": null, "scope": "unspecified"}}, "records": [{"question_id": "rc01"}, {"question_id": "rc02"}, {"question_id": "rc03"}, {"question_id": "rc04"}, {"question_id": "rc05"}, {"question_id": "rc06"}, {"question_id": "rc07"}, {"question_id": "rc08"}, {"question_id": "rc09"}, {"question_id": "rc10"}, {"question_id": "rc11"}, {"question_id": "rc12"}, {"question_id": "rc13"}, {"question_id": "rc14"}, {"question_id": "rc15"}, {"question_id": "rc16"}, {"question_id": "rc17"}, {"question_id": "rc18"}, {"question_id": "rc19"}, {"question_id": "rc20"}, {"question_id": "rc21"}, {"question_id": "rc22"}, {"question_id": "rc23"}, {"question_id": "rc24"}, {"question_id": "rc25"}, {"question_id": "rc26"}, {"question_id": "rc27"}, {"question_id": "rc28"}, {"question_id": "rc29"}, {"question_id": "rc30"}, {"question_id": "rc31"}, {"question_id": "rc32"}, {"question_id": "rc33"}, {"question_id": "rc34"}, {"question_id": "rc35"}, {"question_id": "rc36"}, {"question_id": "df01"}, {"question_id": "df02"}, {"question_id": "df03"}, {"question_id": "df04"}, {"question_id": "df05"}, {"question_id": "df06"}, {"question_id": "df07"}, {"question_id": "df08"}], "schema": "engraphis-benchmark/v2", "suite": {"dataset": "resolver_reworded_corrections.jsonl", "name": "resolver-unit", "sha256": "5b796d8852117b30060cf019e93fd775fea256ca492ce0fedf9b4786f0a0421b", "sources": [{"bytes": 12921, "name": "resolver_reworded_corrections.py", "sha256": "a9054778a37b2175b46f04b674b4779e358931eee5d2ae52bbc5f953d234fb9e"}, {"bytes": 45229, "name": "resolve.py", "sha256": "f01a6f55e44320ab04b97e516342f20155668863b2fc4765305e066d87586524"}]}, "system": {"config_sha256": "f332c6f681d4c59a1263fcd65b4aae2b28a1db252aff687890204859b04aaf33", "dirty_state_sha256": "6820c3be9857ae6a8f2a31f9894c018f900eaeb8d51a9ebeb51892007d13f871", "git_commit": "b48850a9cfe38d02b38b1563961700ab534372f4", "git_dirty": true}} diff --git a/docs/evidence/reliability/resolver-title-write-final.json b/docs/evidence/reliability/resolver-title-write-final.json new file mode 100644 index 00000000..24558c58 --- /dev/null +++ b/docs/evidence/reliability/resolver-title-write-final.json @@ -0,0 +1 @@ +{"environment": {"implementation": "CPython", "machine": "AMD64", "packages": {"engraphis": "1.6", "numpy": "2.4.5", "sentence-transformers": "6.0.0", "torch": "2.13.0", "transformers": "5.15.1"}, "platform": "Windows-11-10.0.26100-SP0", "python": "3.12.10"}, "exclusions": [], "metrics": {"correction_precision": 1.0, "correction_recall": 1.0, "distinct_fact_error_rate": 0.0, "execution": "production_write_path", "false_invalidation_ids": [], "false_invalidations": 0, "false_noop_ids": [], "false_noops": 0, "lost_distinct_fact_ids": [], "lost_distinct_facts": 0, "missed_correction_ids": [], "missed_corrections": 0, "negatives": 8, "positives": 4, "positives_superseded": 4, "similarity_injected": false, "total": 12}, "models": {}, "privacy": {"content_fingerprint_policy": "omitted", "raw_answer_policy": "omitted", "raw_context_policy": "omitted", "raw_query_policy": "omitted"}, "protocol": {"command": ["python", "-m", "eval.resolver_reworded_corrections", "--dataset", "eval/datasets/resolver_write_acceptance.jsonl", "--json", "--end-to-end"], "config": {"end_to_end": true, "evidence_scope": "authored regression corpus; not an independent quality benchmark", "offline": true}, "n_scored": 12, "n_total": 12, "token_accounting": {"identity": "unspecified", "method": "unspecified", "revision": null, "scope": "unspecified"}}, "records": [{"question_id": "write-ttl"}, {"question_id": "write-window"}, {"question_id": "write-reworded"}, {"question_id": "write-keyed-limit"}, {"question_id": "keep-environments"}, {"question_id": "keep-environment-binding"}, {"question_id": "keep-backup-binding"}, {"question_id": "keep-account-identity"}, {"question_id": "keep-subjects"}, {"question_id": "keep-different-attributes"}, {"question_id": "keep-titled-environment-binding"}, {"question_id": "keep-title-only-environment-binding"}], "schema": "engraphis-benchmark/v2", "suite": {"dataset": "resolver_write_acceptance.jsonl", "name": "resolver-write-acceptance", "sha256": "757eb8aa46f691d11db700c4908419dce2044bb55a0b89158c2c6880d66dcaeb", "sources": [{"bytes": 12921, "name": "resolver_reworded_corrections.py", "sha256": "a9054778a37b2175b46f04b674b4779e358931eee5d2ae52bbc5f953d234fb9e"}, {"bytes": 45229, "name": "resolve.py", "sha256": "f01a6f55e44320ab04b97e516342f20155668863b2fc4765305e066d87586524"}, {"bytes": 8756, "name": "factory.py", "sha256": "c8f0349b1c2e9b0fc389019ca3a41d36eb1bfb2230048f0db2c6f9fe9c15be61"}, {"bytes": 236692, "name": "engine.py", "sha256": "5307b6492c8c2ec7852ed490c99ef366061b4ea2c9325ff5abb1c51aac8db9b8"}, {"bytes": 468325, "name": "store.py", "sha256": "4d5670a8e1a1d13c84db572f7598a4511c7804df4a26da28bede6d587d0454c5"}, {"bytes": 40116, "name": "schema.py", "sha256": "88b2643f3905a4b27907a13dcb9379525a7cb4cf844b57aa6ef14d610b6cb312"}, {"bytes": 31040, "name": "interfaces.py", "sha256": "5f0ace8f64472100f31b591b0b3822e7fb21eae84813d69325bcb4b9108d4d22"}, {"bytes": 2349, "name": "vector_search.py", "sha256": "75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234"}, {"bytes": 1902, "name": "vector_repair.py", "sha256": "0483a25ab21880479087d0e75d3d63887e0b1bed9f439c8650b8028c1294305f"}, {"bytes": 6163, "name": "vector_numpy.py", "sha256": "c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72"}, {"bytes": 8347, "name": "embedder_deterministic.py", "sha256": "ec8b23de7e7e8273416125f5876ca96f55e4ae7881841bae55783ab0ba9130ad"}]}, "system": {"config_sha256": "ee045ea3b6856666fd62732a69814a5c737353b810a4b1a7b4a79a3186e1d101", "dirty_state_sha256": "6820c3be9857ae6a8f2a31f9894c018f900eaeb8d51a9ebeb51892007d13f871", "git_commit": "b48850a9cfe38d02b38b1563961700ab534372f4", "git_dirty": true}} diff --git a/engraphis/core/engine.py b/engraphis/core/engine.py index 64a70a80..497e0887 100644 --- a/engraphis/core/engine.py +++ b/engraphis/core/engine.py @@ -805,11 +805,16 @@ def _hydrate_separate_vector_index(self, fingerprint: str) -> None: return target = index_repair_identity(self.index, self.store) if target is not None: - self.store.register_vector_index(target) + # A durable target can outlive a recreated physical index. Its empty + # queue proves completeness only while the adapter remains healthy. + self.store.register_vector_index( + target, rebuild=getattr(self.index, "requires_rebuild", False) is True, + ) while self.store.vector_index_pending(target): repaired = self.repair_vector_index(limit=EMBEDDING_REBUILD_BATCH) if repaired["repaired"] == 0: raise RuntimeError("external vector index repair is incomplete") + self._mark_separate_vector_index_rebuild_complete() return can_skip = getattr(self.index, "can_skip_hydration", None) if callable(can_skip) and can_skip(): @@ -2958,6 +2963,9 @@ def secure_erase(self, memory_id: str, *, actor: str = "user") -> dict: ``VectorIndex`` may be an injected external backend. Request its deletion first, but do not leave the local SQLite copy intact if that backend is unavailable; the returned status explicitly reports that incomplete external cleanup. + + Separate indexes require an engine-owned transaction so a caller cannot later + roll back the durable repair debt after an irreversible provider deletion. """ with self._write_lock: repair_target = index_repair_identity(self.index, self.store) @@ -3019,11 +3027,14 @@ def delete_vectors(ids: list[str], *, in_store_transaction: bool = False) -> Non # Physical maintenance must run after the engine-owned transaction # commits; VACUUM is invalid while the erase transaction is active. result["maintenance"] = self.store.run_secure_erase_maintenance() - except BaseException: + except BaseException as erase_exc: # Provider deletion cannot roll back with SQLite. Persist work # for the restored canonical rows after the failed transaction. if repair_target is not None and attempted_ids: - self.store.queue_vector_index_repairs(repair_target, attempted_ids) + try: + self.store.queue_vector_index_repairs(repair_target, attempted_ids) + except BaseException as repair_exc: + raise repair_exc from erase_exc raise result["vector_index_cleanup"] = index_cleanup if index_cleanup in {"failed", "partial"}: diff --git a/engraphis/core/resolve.py b/engraphis/core/resolve.py index 41b62985..ba4960dc 100644 --- a/engraphis/core/resolve.py +++ b/engraphis/core/resolve.py @@ -179,6 +179,25 @@ def mentions(text: str) -> list[str]: return len(set(candidate)) > 1 and set(candidate) == set(record) and candidate != record +def _only_environment_title_order_changed(candidate_text: str, + candidate_content: Optional[str], + record: MemoryRecord) -> bool: + """Ignore permutations of bare display labels, never facts carried by titles.""" + if (not candidate_content or candidate_content != record.content + or not candidate_text.endswith(candidate_content)): + return False + # The engine supplies title + newline + content. Unrecognized caller layouts + # stay conservative; punctuation, arrows and predicate words are not labels. + prefix = candidate_text[:-len(candidate_content)] + if not prefix or not prefix[-1].isspace(): + return False + title = prefix.strip() + candidate, previous = title.casefold().split(), record.title.casefold().split() + return (len(candidate) > 1 and candidate != previous + and sorted(candidate) == sorted(previous) + and all(token in _ENV_QUALIFIERS for token in candidate)) + + _MONTHS = frozenset({ "january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december", @@ -390,7 +409,10 @@ def resolve(candidate_text: str, neighbors: list[tuple[float, MemoryRecord]], *, # envs, not a correction. env_conflict = ( _env_conflict_for_correction(candidate_text, rec_text) - or _has_reordered_environments(candidate_text, rec_text) + or (_has_reordered_environments(candidate_text, rec_text) + and not _only_environment_title_order_changed( + candidate_text, candidate_content, rec, + )) ) subject_identifier_drift = _has_subject_identifier_drift(candidate_text, rec_text) named_subject_drift = _has_named_subject_drift(candidate_text, rec_text) diff --git a/engraphis/core/store.py b/engraphis/core/store.py index b611b819..656a9c5b 100644 --- a/engraphis/core/store.py +++ b/engraphis/core/store.py @@ -5327,16 +5327,17 @@ def vector_generation(self) -> int: ).fetchone() return int(row[0]) if row is not None else 0 - def register_vector_index(self, identity: str) -> None: - """Register a durable external target and seed its initial repair backlog.""" + def register_vector_index(self, identity: str, *, rebuild: bool = False) -> None: + """Register a target; reseed canonical rows when its physical index was lost.""" with self.write_transaction(): inserted = self.conn.execute( "INSERT OR IGNORE INTO vector_index_targets(identity) VALUES (?)", (identity,) ).rowcount - if inserted: + if inserted or rebuild: self.conn.execute( "INSERT INTO vector_index_repairs(identity, memory_id, generation) " - "SELECT ?, id, ? FROM mem_vectors", + "SELECT ?, id, ? FROM mem_vectors WHERE 1 " + "ON CONFLICT(identity,memory_id) DO UPDATE SET generation=excluded.generation", (identity, self.vector_generation()), ) diff --git a/engraphis/core/vector_repair.py b/engraphis/core/vector_repair.py index 8b16a01b..20abead7 100644 --- a/engraphis/core/vector_repair.py +++ b/engraphis/core/vector_repair.py @@ -34,6 +34,9 @@ def canonical_search_required(index, store: "Store", *, identity = index_repair_identity(index, store) if identity is None: return False + # Physical loss can be reported before startup has reseeded the durable queue. + if getattr(index, "requires_rebuild", False) is True: + return True pending = store.vector_index_pending(identity) # A standalone RecallEngine may use a read-only/testing retrieval adapter # which has never participated in MemoryEngine's durable write lifecycle. diff --git a/eval/datasets/resolver_write_acceptance.jsonl b/eval/datasets/resolver_write_acceptance.jsonl index 1bec35ec..9719aea1 100644 --- a/eval/datasets/resolver_write_acceptance.jsonl +++ b/eval/datasets/resolver_write_acceptance.jsonl @@ -8,3 +8,5 @@ {"id":"keep-account-identity","neighbor":"Customer account 100 has 30 days of audit retention.","candidate":"Customer account 200 has 30 days of audit retention.","expected":"add"} {"id":"keep-subjects","neighbor":"ServiceAlpha uses the regional mirror for every production deployment.","candidate":"ServiceBeta uses the regional mirror for every production deployment.","expected":"add"} {"id":"keep-different-attributes","neighbor":"The primary worker stores source artifacts in the package registry.","candidate":"The primary worker executes integration tests in the isolated sandbox.","expected":"add"} +{"id":"keep-titled-environment-binding","neighbor":"The staging database is mirrored in production.","candidate":"The production database is mirrored in staging.","neighbor_title":"Production staging","candidate_title":"Staging production","expected":"add"} +{"id":"keep-title-only-environment-binding","neighbor":"Replication runs every 30 minutes.","candidate":"Replication runs every 30 minutes.","neighbor_title":"Staging database is mirrored in production","candidate_title":"Production database is mirrored in staging","expected":"add"} diff --git a/eval/resolver_reworded_corrections.py b/eval/resolver_reworded_corrections.py index 72295040..c234fa45 100644 --- a/eval/resolver_reworded_corrections.py +++ b/eval/resolver_reworded_corrections.py @@ -38,10 +38,10 @@ DATASET = Path(__file__).resolve().parent / "datasets" / "resolver_reworded_corrections.jsonl" -def _memory_record(text: str, record_id: str) -> MemoryRecord: +def _memory_record(text: str, record_id: str, *, title: str = "") -> MemoryRecord: return MemoryRecord( id=record_id, workspace_id="w", repo_id=None, session_id=None, - title="", content=text, mtype="semantic", scope="workspace", + title=title, content=text, mtype="semantic", scope="workspace", importance=0.0, confidence=1.0, valid_from=0.0, valid_to=None, ingested_at=0.0, expired_at=None, subject_key="", claim_kind="", keywords=(), metadata={}, @@ -54,8 +54,12 @@ def _write_pair(row: dict, engine) -> tuple[str, str, bool, bool]: repo_id = engine.store.get_or_create_repo(workspace_id, str(row["id"])) shared = {"workspace_id": workspace_id, "repo_id": repo_id} shared.update({key: row[key] for key in ("subject_key", "claim_kind") if key in row}) - before = engine.remember_with_resolution(row["neighbor"], **shared) - after = engine.remember_with_resolution(row["candidate"], **shared) + before = engine.remember_with_resolution( + row["neighbor"], title=row.get("neighbor_title", ""), **shared, + ) + after = engine.remember_with_resolution( + row["candidate"], title=row.get("candidate_title", ""), **shared, + ) old_record = engine.store.get_memory(before["id"]) new_record = engine.store.get_memory(after["id"]) old_survives = bool(old_record and old_record.valid_to is None) @@ -89,7 +93,9 @@ def evaluate(dataset: Path = DATASET, *, end_to_end: bool = False) -> dict[str, seen_ids.add(case_id) total += 1 expected = row["expected"] - neighbor = _memory_record(row["neighbor"], f"mem_{row['id']}_n") + neighbor = _memory_record( + row["neighbor"], f"mem_{row['id']}_n", title=row.get("neighbor_title", ""), + ) # Use a high similarity so the resolver's strong/rewrite gates # are exercised for every row. The labeled ground truth tells # us whether the resolver should INVALIDATE or ADD. @@ -105,9 +111,13 @@ def evaluate(dataset: Path = DATASET, *, end_to_end: bool = False) -> dict[str, finally: engine.store.close() else: + candidate = row["candidate"] + if row.get("candidate_title"): + candidate = f"{row['candidate_title']}\n{candidate}" resolution = resolve( - row["candidate"], + candidate, [(0.9, neighbor)], + candidate_content=row["candidate"], ) actual, reason = resolution.op.value, resolution.reason if expected == "invalidate": diff --git a/tests/test_resolve.py b/tests/test_resolve.py index 4485d278..5d3b50b6 100644 --- a/tests/test_resolve.py +++ b/tests/test_resolve.py @@ -662,3 +662,71 @@ def test_unkeyed_facts_with_organization_subjects_both_live(): assert eng.store.get_memory(beta["id"]).valid_to is None finally: eng.store.close() + + +def test_duplicate_content_ignores_reordered_display_environments(): + content = "Atlas stores memory in SQLite." + prior = _rec(content, title="Staging production", id="mem_display") + result = resolve( + "Production staging " + content, [(0.99, prior)], + candidate_content=content, + ) + assert result.op == ResolutionOp.NOOP + assert result.target_id == "mem_display" + + +def test_real_writes_distinguish_display_order_from_environment_bindings(): + from engraphis.core.engine import MemoryEngine + + engine = MemoryEngine.create(":memory:", auto_evolve=False) + try: + workspace = engine.store.get_or_create_workspace("title-order") + content = "The staging database is mirrored in production." + first = engine.remember_with_resolution( + content, title="Staging production", workspace_id=workspace, + ) + duplicate = engine.remember_with_resolution( + content, title="Production staging", workspace_id=workspace, + ) + assert first["op"] == "add" + assert duplicate["op"] == "noop" + assert duplicate["id"] == first["id"] + + distinct = engine.remember_with_resolution( + "The production database is mirrored in staging.", + title="Production staging", workspace_id=workspace, + ) + assert distinct["op"] == "relate" + assert distinct["id"] != first["id"] + assert engine.store.get_memory(first["id"]).valid_to is None + assert engine.store.get_memory(distinct["id"]).valid_to is None + finally: + engine.close() + + + +def test_environment_bindings_in_titles_survive_identical_content(): + from engraphis.core.engine import MemoryEngine + + title_pairs = [ + ("Staging database is mirrored in production", "Production database is mirrored in staging"), + ("Staging -> production", "Production -> staging"), + ("Staging to production", "Production to staging"), + ] + engine = MemoryEngine.create(":memory:", auto_evolve=False) + try: + for case, (old_title, new_title) in enumerate(title_pairs): + workspace = engine.store.get_or_create_workspace(f"title-fact-{case}") + first = engine.remember_with_resolution( + "Replication runs every 30 minutes.", title=old_title, workspace_id=workspace, + ) + second = engine.remember_with_resolution( + "Replication runs every 30 minutes.", title=new_title, workspace_id=workspace, + ) + assert second["op"] == "relate", (old_title, new_title, second) + assert second["id"] != first["id"] + assert engine.store.get_memory(first["id"]).valid_to is None + assert engine.store.get_memory(first["id"]).title == old_title + assert engine.store.get_memory(second["id"]).title == new_title + finally: + engine.close() diff --git a/tests/test_storage_concurrency_repair.py b/tests/test_storage_concurrency_repair.py index 2ad75caa..b2d2e36f 100644 --- a/tests/test_storage_concurrency_repair.py +++ b/tests/test_storage_concurrency_repair.py @@ -4,7 +4,7 @@ import shutil import sqlite3 import threading -from concurrent.futures import ThreadPoolExecutor +from concurrent.futures import ThreadPoolExecutor, TimeoutError import numpy as np import pytest @@ -12,6 +12,7 @@ from engraphis.core.interfaces import MemoryRecord, Scope, SearchFilter from engraphis.core.vector_repair import canonical_search_required, index_repair_identity from engraphis.factory import create_memory_engine +from engraphis.service import MemoryService class ExternalIndex: @@ -417,3 +418,338 @@ def test_repair_dequeue_uses_covering_order_index_without_sorting(): assert tuple(engine.store.conn.execute(sql, (target,)).fetchone()) == ("mem_09998", 0) finally: engine.close() + + +def test_title_publication_acknowledges_repair_and_keeps_configured_search(): + engine = create_memory_engine(auto_evolve=False) + index = ExternalIndex() + _use_external(engine, index) + service = MemoryService(engine) + workspace = engine.store.get_or_create_workspace("title-repair") + try: + memory = engine.remember("Atlas stores memory in SQLite.", workspace_id=workspace) + service.update_memory(memory, workspace="title-repair", title="Current title") + np.testing.assert_allclose(index.rows[memory], engine.store.get_vectors([memory])[memory]) + assert engine.store.vector_index_pending(index_repair_identity(index, engine.store)) == 0 + assert not canonical_search_required(index, engine.store) + finally: + engine.close() + + +@pytest.mark.parametrize("erase", [False, True]) +def test_delayed_title_publication_never_replays_old_payload(monkeypatch, erase): + engine = create_memory_engine(auto_evolve=False) + index = ExternalIndex() + _use_external(engine, index) + service = MemoryService(engine) + workspace = engine.store.get_or_create_workspace("delayed-title") + publish = service._publish_memory_index_action + actions = [] + monkeypatch.setattr(service, "_publish_memory_index_action", actions.append) + try: + memory = engine.remember("Atlas stores memory in SQLite.", workspace_id=workspace) + service.update_memory(memory, workspace="delayed-title", title="Old delayed title") + service.update_memory(memory, workspace="delayed-title", title="Latest canonical title") + if erase: + engine.secure_erase(memory) + else: + publish(actions[1]) + publications = len(index.published) + publish(actions[0]) + assert len(index.published) == publications + if erase: + assert memory not in index.rows + else: + np.testing.assert_allclose(index.rows[memory], engine.store.get_vectors([memory])[memory]) + assert engine.store.vector_index_pending(index_repair_identity(index, engine.store)) == 0 + finally: + engine.close() + + +def test_secret_title_cleanup_queues_absent_canonical_vector_until_external_retry(): + engine = create_memory_engine(auto_evolve=False) + index = ExternalIndex() + _use_external(engine, index) + service = MemoryService(engine) + workspace = engine.store.get_or_create_workspace("secret-title") + try: + memory = engine.store.add_memory(MemoryRecord( + id="", content="Private local record.", sensitivity="secret", + scope=Scope.WORKSPACE, workspace_id=workspace, + )) + # An old external copy can survive even after its canonical vector is gone. + index.rows[memory] = np.zeros(engine.embedder.dim, dtype=np.float32) + index.fail = True + service.update_memory(memory, workspace="secret-title", title="Review label") + target = index_repair_identity(index, engine.store) + assert engine.store.vector_index_pending(target) == 1 + assert memory in index.rows + index.fail = False + assert engine.repair_vector_index()["repaired"] == 1 + assert memory not in index.rows + assert engine.store.vector_index_pending(target) == 0 + finally: + engine.close() + + +@pytest.mark.parametrize("fail", [False, True]) +def test_secure_erase_acknowledges_only_confirmed_external_cleanup(fail): + engine = create_memory_engine(auto_evolve=False) + index = ExternalIndex() + _use_external(engine, index) + workspace = engine.store.get_or_create_workspace("erase-repair") + try: + memory = engine.remember("Atlas stores memory in SQLite.", workspace_id=workspace) + index.fail = fail + result = engine.secure_erase(memory) + assert result["vector_index_cleanup"] == ("failed" if fail else "deleted") + assert engine.store.get_memory(memory) is None + target = index_repair_identity(index, engine.store) + assert engine.store.vector_index_pending(target) == int(fail) + assert canonical_search_required(index, engine.store) is fail + index.fail = False + engine.repair_vector_index() + assert memory not in index.rows + assert engine.store.vector_index_pending(target) == 0 + finally: + engine.close() + + +def test_secure_erase_serializes_external_delete_against_concurrent_repair(tmp_path, monkeypatch): + path = str(tmp_path / "erase-race.db") + first = create_memory_engine(path, auto_evolve=False) + second = create_memory_engine(path, auto_evolve=False) + index = ExternalIndex() + _use_external(first, index) + _use_external(second, index) + deleted = threading.Event() + release = threading.Event() + try: + workspace = first.store.get_or_create_workspace("erase-race") + memory = first.remember("Atlas stores memory in SQLite.", workspace_id=workspace) + first.store.put_vector(memory, first.store.get_vectors([memory])[memory], model=first.embedding_space) + first.store.conn.commit() + delete = index.delete + + def pause_after_delete(ids, *, commit=True): + delete(ids, commit=commit) + deleted.set() + assert release.wait(10), "erasure synchronization timed out" + + monkeypatch.setattr(index, "delete", pause_after_delete) + with ThreadPoolExecutor(max_workers=2) as pool: + erasure = pool.submit(first.secure_erase, memory) + try: + assert deleted.wait(10), "external deletion did not run" + replay = pool.submit(second.repair_vector_index, memory_id=memory) + with pytest.raises(TimeoutError): + replay.result(timeout=0.1) + finally: + release.set() + assert erasure.result(timeout=10)["vector_index_cleanup"] == "deleted" + assert replay.result(timeout=10)["pending"] == 0 + assert memory not in index.rows + assert first.store.get_memory(memory) is None + finally: + release.set() + first.close() + second.close() + + +def test_rebuild_publication_acknowledges_its_confirmed_generation(tmp_path): + from tests.test_recall_recovery import _engine_for, _VersionedSemanticEmbedder + + path = tmp_path / "rebuild-publication.db" + index = ExternalIndex() + first = _engine_for(path, _VersionedSemanticEmbedder("A")) + _use_external(first, index) + try: + first._rebuild_versioned_embeddings() + workspace = first.store.get_or_create_workspace("rebuild-publication") + memory = first.remember("Atlas stores memory in SQLite.", workspace_id=workspace) + finally: + first.close() + second = _engine_for(path, _VersionedSemanticEmbedder("B")) + _use_external(second, index) + try: + second._rebuild_versioned_embeddings() + np.testing.assert_allclose(index.rows[memory], second.store.get_vectors([memory])[memory]) + assert second.store.vector_index_pending(index_repair_identity(index, second.store)) == 0 + finally: + second.close() + + +def test_publication_acknowledgement_preserves_newer_canonical_generation(): + engine = create_memory_engine(auto_evolve=False) + index = ExternalIndex() + _use_external(engine, index) + workspace = engine.store.get_or_create_workspace("newer-generation") + try: + memory = engine.remember("Atlas stores memory in SQLite.", workspace_id=workspace) + target = index_repair_identity(index, engine.store) + for text in ("Earlier title", "Later canonical title"): + with engine.store.write_transaction(): + engine.store.put_vector(memory, engine.embedder.embed([text])[0], model=engine.embedding_space) + if text == "Earlier title": + captured = engine.store.vector_index_repair_generations(target, [memory]) + engine.store.acknowledge_vector_index_repairs(target, captured) + assert engine.store.vector_index_pending(target) == 1 + assert engine.store.vector_index_repair_generations(target, [memory])[memory] > captured[memory] + assert canonical_search_required(index, engine.store) + assert engine.repair_vector_index()["repaired"] == 1 + np.testing.assert_allclose(index.rows[memory], engine.store.get_vectors([memory])[memory]) + assert engine.store.vector_index_pending(target) == 0 + finally: + engine.close() + + +@pytest.mark.parametrize("repair_fails", [False, True]) +def test_secure_erase_rollback_restores_repair_debt_or_reports_compensation_failure(monkeypatch, repair_fails): + engine = create_memory_engine(auto_evolve=False) + index = ExternalIndex() + _use_external(engine, index) + workspace = engine.store.get_or_create_workspace("erase-rollback") + try: + memory = engine.remember("Atlas stores memory in SQLite.", workspace_id=workspace) + erase = engine.store.secure_erase_memory + queue = engine.store.queue_vector_index_repairs + + def fail_after_erase(*args, **kwargs): + erase(*args, **kwargs) + assert engine.store.get_memory(memory) is None + raise RuntimeError("injected canonical erase failure") + + def fail_compensation(*args, **kwargs): + if not engine.store.conn.transaction_owned_by_current_thread(): + raise RuntimeError("injected repair persistence failure") + return queue(*args, **kwargs) + + monkeypatch.setattr(engine.store, "secure_erase_memory", fail_after_erase) + if repair_fails: + monkeypatch.setattr(engine.store, "queue_vector_index_repairs", fail_compensation) + expected = "injected repair persistence failure" if repair_fails else "injected canonical erase failure" + with pytest.raises(RuntimeError, match=expected) as failure: + engine.secure_erase(memory) + assert engine.store.get_memory(memory) is not None + assert memory not in index.rows + target = index_repair_identity(index, engine.store) + if repair_fails: + assert isinstance(failure.value.__cause__, RuntimeError) + assert str(failure.value.__cause__) == "injected canonical erase failure" + else: + assert engine.store.vector_index_pending(target) == 1 + assert canonical_search_required(index, engine.store) + assert engine.repair_vector_index()["repaired"] == 1 + np.testing.assert_allclose(index.rows[memory], engine.store.get_vectors([memory])[memory]) + assert engine.store.vector_index_pending(target) == 0 + finally: + engine.close() + + +def test_external_secure_erase_rejects_caller_transaction_before_provider_deletion(monkeypatch): + engine = create_memory_engine(auto_evolve=False) + index = ExternalIndex() + _use_external(engine, index) + workspace = engine.store.get_or_create_workspace("caller-erase") + try: + memory = engine.remember("Atlas stores memory in SQLite.", workspace_id=workspace) + deleted = [] + monkeypatch.setattr(index, "delete", lambda ids: deleted.extend(ids)) + engine.store.conn.execute("BEGIN IMMEDIATE") + with pytest.raises(RuntimeError, match="caller-owned transactions cannot erase"): + engine.secure_erase(memory) + assert engine.store.conn.transaction_owned_by_current_thread() + assert not deleted + assert memory in index.rows + engine.store.conn.rollback() + assert engine.store.get_memory(memory) is not None + assert engine.store.vector_index_pending(index_repair_identity(index, engine.store)) == 0 + finally: + engine.close() + + +def test_existing_v17_database_gains_repair_queue_index_without_losing_debt(tmp_path): + from engraphis.core.store import Store + + path = tmp_path / "v17-queue-index.db" + with Store(str(path)) as store: + store.queue_vector_index_repairs("upgrade-target", ["mem_cleanup"]) + with sqlite3.connect(str(path)) as previous: + previous.execute("DROP INDEX idx_vector_index_repairs_queue") + with Store(str(path)) as upgraded: + assert upgraded.schema_version == 17 + assert upgraded.vector_index_pending("upgrade-target") == 1 + plan = " ".join(str(row[3]) for row in upgraded.conn.execute( + "EXPLAIN QUERY PLAN SELECT memory_id,generation FROM vector_index_repairs " + "WHERE identity=? ORDER BY generation,memory_id LIMIT 1", ("upgrade-target",), + ).fetchall()).upper() + assert "USE TEMP B-TREE" not in plan + assert "COVERING INDEX IDX_VECTOR_INDEX_REPAIRS_QUEUE" in plan + + +@pytest.mark.parametrize("startup_state", ["healthy", "recreated", "failed_rebuild"]) +def test_external_startup_honors_rebuild_signal_with_existing_target(tmp_path, startup_state): + from tests.test_recall_recovery import _engine_for, _VersionedSemanticEmbedder + + class RestartableIndex(ExternalIndex): + requires_rebuild = False + + def __init__(self): + super().__init__() + self.completed = 0 + + def mark_rebuild_complete(self): + self.completed += 1 + self.requires_rebuild = False + + path = tmp_path / "external-startup.db" + index = RestartableIndex() + first = _engine_for(path, _VersionedSemanticEmbedder("A")) + _use_external(first, index) + try: + first._rebuild_versioned_embeddings() + workspace = first.store.get_or_create_workspace("external-startup") + historical = first.remember("Atlas stores memory in SQLite.", workspace_id=workspace) + current = first.remember("Whales sing underwater.", workspace_id=workspace) + first.store.close_validity(historical) + target = index_repair_identity(index, first.store) + assert first.store.vector_index_pending(target) == 0 + assert set(index.rows) == {historical, current} + finally: + first.close() + + if startup_state != "healthy": + index = RestartableIndex() + index.requires_rebuild = True + assert not index.rows + index.published.clear() + second = _engine_for(path, _VersionedSemanticEmbedder("A")) + _use_external(second, index) + try: + assert index_repair_identity(index, second.store) == target + assert second.store.vector_index_pending(target) == 0 + assert canonical_search_required(index, second.store) is index.requires_rebuild + if startup_state == "failed_rebuild": + index.fail = True + with pytest.raises(RuntimeError, match="external vector index repair is incomplete"): + second._rebuild_versioned_embeddings() + assert second.store.vector_index_pending(target) == 2 + assert canonical_search_required(index, second.store) + assert index.requires_rebuild + assert index.completed == 0 + index.fail = False + second._rebuild_versioned_embeddings() + assert set(index.rows) == {historical, current} + for memory in (historical, current): + np.testing.assert_allclose(index.rows[memory], second.store.get_vectors([memory])[memory]) + assert second.store.vector_index_pending(target) == 0 + assert not index.requires_rebuild + assert not canonical_search_required(index, second.store) + if startup_state == "healthy": + assert not index.published + assert index.completed == 0 + else: + assert set(index.published) == {historical, current} + assert index.completed == 1 + finally: + second.close() From e577ea33bf18e25df56c9212530d68ba0890602c Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Sat, 5 Sep 2026 07:43:06 -0400 Subject: [PATCH 10/10] fix: enforce operator veto and synchronize physics tests --- .github/workflows/ci.yml | 8 + docs/RELIABILITY_PROGRAM.md | 2 + .../reliability/browser-followup-review.json | 145 ++++++++++++++++++ docs/evidence/reliability/catalog.json | 16 +- docs/evidence/reliability/catalog.json.sha256 | 2 +- .../reliability/operator-policy-review.json | 59 +++++++ docs/evidence/reliability/validation.json | 4 +- engraphis/managed_processing.py | 2 + engraphis/routes/v2_api.py | 9 +- tests/e2e/graph-engine.spec.js | 70 +++++---- tests/test_managed_processing_policy.py | 122 +++++++++++++++ 11 files changed, 407 insertions(+), 32 deletions(-) create mode 100644 docs/evidence/reliability/browser-followup-review.json create mode 100644 docs/evidence/reliability/operator-policy-review.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0119283a..369b6212 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -261,6 +261,14 @@ jobs: run: npm audit --audit-level=high - name: Playwright desktop/mobile, keyboard, CSP, console, and axe checks run: npx playwright test + - name: Retain browser failure diagnostics + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: browser-failure-diagnostics-${{ github.run_id }}-${{ github.run_attempt }} + path: test-results/ + if-no-files-found: ignore + retention-days: 14 docker-gate: # Keeps CI fast: the docker job always runs on push to main, but on PRs only diff --git a/docs/RELIABILITY_PROGRAM.md b/docs/RELIABILITY_PROGRAM.md index 810d5b75..8c86d525 100644 --- a/docs/RELIABILITY_PROGRAM.md +++ b/docs/RELIABILITY_PROGRAM.md @@ -53,6 +53,8 @@ capacity, production readiness, or a completed user study. | R22 | Reproduced on Python 3.13/3.14: MCP export changed because docstring indentation differed | Normalize tool descriptions with `inspect.cleandoc`; preserve schema/annotation and meaningful-description drift checks | Matched MCP/Pydantic runtimes; only 32 description fields and the digest differed | | R23 | Reproduced: a recreated external index with the same identity could be treated as complete | Honor the adapter rebuild signal, reseed canonical rows and retain repair on failure; keep healthy startup incremental | Same-identity restart cases include historical vectors, failed rebuild/retry and healthy startup | | R24 | Reproduced: reordered display-title environments caused identical writes to be stored twice | Ignore only permutations of bare environment labels on identical content; preserve factual title and content bindings | Duplicate-label regressions plus real-write factual-title, arrow and body-binding protection; two titled acceptance cases | +| R25 | Reproduced: direct API enable bypassed the operator processing veto | Reject before Cloud access, recheck before submission, and reject latent approval in the service transaction; retain disable and pending acknowledgement | Five disabling values, mid-read veto, direct-service rejection and remote-disable/outage scenarios; 124 related tests passed and independent policy review passed | +| R26 | Reproduced browser-test observation races: duplicate animation steps and missed first contact; CI retained no failure artifact | Sample the existing completed-physics callback and freeze at the original budget; retain scoped failure traces in CI | Eight exact-test repetitions passed with two Chromium workers and zero retries; original assertions retained. Actual CI failure upload remains unexercised. | ## Architecture and compatibility decisions diff --git a/docs/evidence/reliability/browser-followup-review.json b/docs/evidence/reliability/browser-followup-review.json new file mode 100644 index 00000000..485b7885 --- /dev/null +++ b/docs/evidence/reliability/browser-followup-review.json @@ -0,0 +1,145 @@ +{ + "schema": "engraphis-browser-followup-review/v1", + "date": "2026-09-05T11:42:27.941990+00:00", + "base_commit": "7148b72fe3d8976951124b622a354b89e0da1f0b", + "github_failure": { + "run": 33962732785, + "job": 101297342277, + "passed": 111, + "failed": 1, + "uploaded_artifacts": 0, + "url": "https://github.com/Coding-Dev-Tools/engraphis/actions/runs/33962732785/job/101297342277" + }, + "finding": "Node-side polling duplicated a completed step and could miss the first contact correction even when live browser physics advanced correctly.", + "fix": "The single slider test opts into the existing renderer onPhysics callback. It synchronously captures completed frames and freezes at the original solver budget, retaining all original motion, contact, radius, direction and overshoot assertions.", + "changed_source_sha256": { + "tests/e2e/graph-engine.spec.js": "a3fbae8d48741c9f2bddf7141af8054e7a67d228ff3d7a6274e9c420d9574e7b", + ".github/workflows/ci.yml": "629f8f786c3a2b9c964162b8fc451a16ea22dd0a18ff8005a116f28c708460c8" + }, + "unchanged_runtime_sha256": { + "engraphis/dashboard_assets/engraphis-graph.js": "a76d482781de76bccdf8e3955fadf6e8b2de71129511e90f3832b30de78bbde0", + "playwright.config.js": "89aaaece6f36ba55fa35406983f0eb1f2695e4872e6793b44a53604120a06ee9" + }, + "before": { + "command": [ + "node", + "node_modules/@playwright/test/cli.js", + "test", + "tests/e2e/graph-engine.spec.js:3349", + "--project=chromium", + "--workers=1", + "--reporter=line" + ], + "ENGRAPHIS_PLAYWRIGHT_PORT": 58432, + "exit_code": 1, + "log_sha256": "78581ee942273f15cbbdb256cc7b8fe97f36b28bf4e417578b33b7f641c7928b" + }, + "intermediate": { + "passed": 7, + "failed": 1, + "source_sha256": "70a5177b197735599b6342d01858438218f92f990f102d0054f58f6f8d9f918c", + "cause": "Node polling missed transient first-step contact diagnostic" + }, + "after": { + "command": [ + "node", + "node_modules/@playwright/test/cli.js", + "test", + "tests/e2e/graph-engine.spec.js", + "-g", + "Galaxy sliders retain full ranges with orbital-speed and radius response", + "--project=chromium", + "--workers=2", + "--repeat-each=8", + "--reporter=json" + ], + "ENGRAPHIS_PLAYWRIGHT_PORT": 57264, + "exit_code": 0, + "log_sha256": "451af1d2f3bdb11a9f550f6cea8d01d522111e812e61a8139cce2a665d83c497", + "passed": 8, + "skipped": 0, + "failed": 0, + "retries": 0, + "workers": 2, + "seconds": 20.407974999999997 + }, + "observed_each_run": { + "gravity_baseline_and_strong_steps": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + "contact_natural_steps": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + "contact_fast_steps": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16 + ], + "first_contact": { + "naturalOrbits": { + "step": 1, + "overlaps": 1, + "correction": 13.19073521045847 + }, + "fastOrbits": { + "step": 1, + "overlaps": 1, + "correction": 12.072017095014512 + } + } + }, + "ci_diagnostics": { + "action": "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a", + "condition": "failure()", + "path": "test-results/", + "retention_days": 14, + "existing_release_infrastructure_tests_passed": 22, + "actual_failure_upload_verified": false + }, + "environment": { + "platform": "Windows AMD64", + "python": "3.12.10", + "node": "24.15.0", + "playwright": "1.62.1", + "browser": "Chromium", + "server": "isolated disposable in-memory dashboard; configured test credentials; no production data" + }, + "review": { + "worker": "approved", + "parent": "reviewed complete test and workflow diffs; verified source hashes and eight per-run step/contact observations" + }, + "limitations": [ + "Local Windows Chromium; next remote CI has not run.", + "Graph API fixture is mocked; real browser assets and renderer run.", + "GitHub prior run uploaded no artifacts; local failure traces retained.", + "Parent CI failure artifact upload statically reviewed, future failed run required to exercise actual upload." + ] +} diff --git a/docs/evidence/reliability/catalog.json b/docs/evidence/reliability/catalog.json index ca3bae07..7db840fb 100644 --- a/docs/evidence/reliability/catalog.json +++ b/docs/evidence/reliability/catalog.json @@ -5,6 +5,10 @@ "validation": "validation.json", "public_base_commit": "cb03dbe104394b7760ef402a2b1917eac5e8accc", "files": { + "browser-followup-review.json": { + "sha256": "d23954313775d96eca8816bdd3d9f2637453302dabafa99ec864373d57eb69ca", + "bytes": 4466 + }, "fts-insert-counterfactual-20260905.json": { "sha256": "b0a4603e983246790682ee5db4b5d5fab65b8ffb72dce002c85049f5112bff16", "bytes": 7397 @@ -41,6 +45,10 @@ "sha256": "aa2f2e3e0312491d57e6d6571dbb92a96570c8655f55bfe7b63a3dc6ca2fc2dc", "bytes": 3663 }, + "operator-policy-review.json": { + "sha256": "ab46e034964fa18aefa9c3088d8371ba9a4acb753dd35a20faa40b12933c84c8", + "bytes": 3298 + }, "pr-benchmark-source-check.json": { "sha256": "af6a1ecc06f933227dc07d3fb19ae184790afd40b639ca4226f77086246ed670", "bytes": 1795 @@ -102,8 +110,8 @@ "bytes": 20106 }, "validation.json": { - "sha256": "b1352fbac30794374084073ab2703cda7e3b15fbe3afe462a98b75bc670c57a4", - "bytes": 21616 + "sha256": "577d6837b21217c88cd53ad0e8a36c6c79fef013310d930018704b7383ed9e1d", + "bytes": 21689 }, "vector-scale-incomplete-baseline-20260905.json": { "sha256": "45c4f61518012e35e09b6d274dec3015d00d4948f2b8279c0f56760dec080544", @@ -176,6 +184,8 @@ }, "source_overlays": [ "public-ci-test-isolation.json", - "pr-review-followup.json" + "pr-review-followup.json", + "operator-policy-review.json", + "browser-followup-review.json" ] } diff --git a/docs/evidence/reliability/catalog.json.sha256 b/docs/evidence/reliability/catalog.json.sha256 index 7f22692a..10a8164d 100644 --- a/docs/evidence/reliability/catalog.json.sha256 +++ b/docs/evidence/reliability/catalog.json.sha256 @@ -1 +1 @@ -f29fbf50ecb672a518ea3c624888451f92a11ee000c018511f2ea23bddee6a80 catalog.json +62d59dc9e3436fb78b1113ccb5bf493d0a29d19d89e9d8227df162616c05b449 catalog.json diff --git a/docs/evidence/reliability/operator-policy-review.json b/docs/evidence/reliability/operator-policy-review.json new file mode 100644 index 00000000..06a9faaa --- /dev/null +++ b/docs/evidence/reliability/operator-policy-review.json @@ -0,0 +1,59 @@ +{ + "schema": "engraphis-operator-policy-review/v1", + "date": "2026-09-05T11:28:28.722616+00:00", + "base_commit": "7148b72fe3d8976951124b622a354b89e0da1f0b", + "finding": "https://github.com/Coding-Dev-Tools/engraphis/pull/201#discussion_r3940422778", + "source_sha256": { + "engraphis/managed_processing.py": "6d33cdfd10800d9552fcfae3c2b071d2b39fe10fb69d1a8e5d9ed6026882197e", + "engraphis/routes/v2_api.py": "0ba2c5b190af5b82a0cc0fb30ed1c688dad5a8be926151f509e576e5c96803b8", + "tests/test_managed_processing_policy.py": "ce690a56dbbe4f51b4525189e0b7e15e0fb179d37cde32fb640017e3e813e0a8", + "docs/RELIABILITY_PROGRAM.md": "e05df8656f9e6086935fbed18415dcfd95e05e903ee67e29d93b4764ace734e5" + }, + "behavior": [ + "An enable request under the operator veto receives a static 403 before Cloud client construction, credential lookup or network access.", + "The existing local-intent fence rechecks the veto after reading Cloud policy and before submitting enable; changed intent returns 409.", + "The service transaction rejects enabled approval while the operator veto is active, before persistence or audit, so removing the veto cannot reveal latent approval.", + "Explicit disable still persists local opt-out immediately and seeks remote acknowledgement; an outage retains the existing truthful pending notice." + ], + "reproduction": { + "command": "ENGRAPHIS_EXTRACTOR=none python -m pytest tests/test_managed_processing_policy.py -q -o addopts= --tb=short -k operator_optout", + "before": { + "failed": 7, + "passed": 2, + "deselected": 14 + }, + "tool_output_chunk": "47760d", + "observed": "Five disabling values and a veto appearing during the Cloud read returned HTTP 200 and allowed the cloud call. Direct local approval did not raise. The final changed-intent response uses the existing 409 contract." + }, + "validation": [ + { + "command": "ENGRAPHIS_EXTRACTOR=none python -m pytest tests/test_managed_processing_policy.py tests/test_cloud_features.py tests/test_dashboard_v2.py -q -rs -o addopts= --tb=short", + "passed": 124, + "skipped": 0, + "seconds": 16.72, + "warnings": 1, + "tool_output_chunk": "27b7d4" + }, + { + "name": "independent security review", + "passed": 23, + "review_result": "approved; no scoped security blocker; reviewer made no edits", + "test_module": "tests/test_managed_processing_policy.py" + }, + { + "name": "static and contract checks", + "result": "Ruff, Pyright, generated MCP contract, commercial manifest, strict-CSP asset check and whitespace checks passed." + } + ], + "environment": { + "python": "3.12.10", + "platform": "Windows AMD64", + "ENGRAPHIS_EXTRACTOR": "none" + }, + "limitations": [ + "Cloud responses are controlled test doubles exercising the real local HTTP route; no production cloud operation ran.", + "Changing a process environment variable does not itself acknowledge remote cancellation of already submitted work.", + "No schema migration, dependency change or change to encrypted-sync policy.", + "Final GitHub checks must be bound to the subsequent commit; the base revision had 22 passing checks and one reproduced browser sampling failure." + ] +} diff --git a/docs/evidence/reliability/validation.json b/docs/evidence/reliability/validation.json index af438890..f5f1a82f 100644 --- a/docs/evidence/reliability/validation.json +++ b/docs/evidence/reliability/validation.json @@ -535,6 +535,8 @@ }, "source_overlays": [ "public-ci-test-isolation.json", - "pr-review-followup.json" + "pr-review-followup.json", + "operator-policy-review.json", + "browser-followup-review.json" ] } diff --git a/engraphis/managed_processing.py b/engraphis/managed_processing.py index 726cc891..e2de8d8a 100644 --- a/engraphis/managed_processing.py +++ b/engraphis/managed_processing.py @@ -81,6 +81,8 @@ def set_processing_policy(service: Any, workspace: str, *, enabled: bool, if owns: conn.execute("BEGIN IMMEDIATE") previous = processing_policy(service, ws) + if enabled and previous["operator_disabled"]: + raise ValueError("managed processing is disabled by this installation's configuration") if expected_revision is not None and previous["revision"] != expected_revision: raise ProcessingPolicyChanged("Processing controls changed while Cloud was responding. Reload and retry.") value = {"schema": SCHEMA, "enabled": enabled, "confirmed": True, diff --git a/engraphis/routes/v2_api.py b/engraphis/routes/v2_api.py index 94d02453..4b810e54 100644 --- a/engraphis/routes/v2_api.py +++ b/engraphis/routes/v2_api.py @@ -1448,11 +1448,18 @@ def managed_processing_set(req: _ManagedProcessingReq): local = _run(current_service.managed_processing_policy, ws) if req.enabled and not req.confirmed: raise _invalid_request() + if req.enabled and local["operator_disabled"]: + raise HTTPException(status_code=403, detail={ + "error": "Managed processing is disabled by this installation's configuration.", + "code": "processing_operator_disabled", + }) if not req.enabled: local = _run(current_service.set_managed_processing_policy, ws, enabled=False, remote_sync_pending=True) def ensure_current_local_intent(): - if current_service.managed_processing_policy(ws)["revision"] != local["revision"]: + current = current_service.managed_processing_policy(ws) + if (current["revision"] != local["revision"] + or (req.enabled and current["operator_disabled"])): raise ProcessingPolicyChanged( "Processing controls changed while Cloud was responding. Reload and retry.") diff --git a/tests/e2e/graph-engine.spec.js b/tests/e2e/graph-engine.spec.js index 391c45b3..40983d09 100644 --- a/tests/e2e/graph-engine.spec.js +++ b/tests/e2e/graph-engine.spec.js @@ -312,7 +312,9 @@ const servedCompleteGalaxyScene = completeGalaxyScene(); * a Node harness cannot: which scripts were fetched, which CSP rules fired, and what the page * logged. Returns the recorders so each test can assert on them. */ -async function openDashboard(page, { query = '', graphScene = graphScenePayload } = {}) { +async function openDashboard(page, { + query = '', graphScene = graphScenePayload, capturePhysics = false, +} = {}) { const requested = []; const consoleErrors = []; const pageErrors = []; @@ -321,7 +323,7 @@ async function openDashboard(page, { query = '', graphScene = graphScenePayload // Report CSP violations from the page itself. A blocked inline style is not a request // failure and not a console error Playwright surfaces reliably, so the only trustworthy // source is the document event the browser fires. - await page.addInitScript(() => { + await page.addInitScript(({ capturePhysics }) => { window.__cspViolations = []; document.addEventListener('securitypolicyviolation', event => { window.__cspViolations.push({ @@ -373,6 +375,15 @@ async function openDashboard(page, { query = '', graphScene = graphScenePayload return originalNodeClick(node); } }; } + if (capturePhysics) { + const originalPhysics = args[1] && args[1].onPhysics; + args[1] = { ...args[1], onPhysics: diagnostics => { + if (typeof originalPhysics === 'function') originalPhysics(diagnostics); + if (typeof window.__sampleGalaxyFrame === 'function') { + window.__sampleGalaxyFrame(); + } + } }; + } const instance = Reflect.apply(target.create, target, args); window.__engraphisGraph = instance; return instance; @@ -383,7 +394,7 @@ async function openDashboard(page, { query = '', graphScene = graphScenePayload }); }, }); - }); + }, { capturePhysics }); page.on('request', request => requested.push(request.url())); page.on('console', message => { @@ -446,9 +457,27 @@ async function openGraphView(page) { } /* Measure the hierarchy in graph space, where zoom-to-fit cannot fake orbital motion. System - centres are evidence-mass weighted, matching the runtime force and server scene contract. */ -async function galaxySystemSnapshot(page) { - return page.evaluate(() => { + centres are evidence-mass weighted, matching the runtime force and server scene contract. + A positive stepCount collects live frame snapshots, then freezes at that step budget. */ +async function galaxySystemSnapshot(page, stepCount = 0) { + return page.evaluate(function snapshot(steps = 0) { + if (steps > 0) { + // Observe completed frames in the browser before another frame can overwrite its + // contact counters. Polling from Node can miss a frame or read the same step twice. + const samples = [snapshot()]; + return new Promise(resolve => { + window.__sampleGalaxyFrame = () => { + const sample = snapshot(); + samples.push(sample); + if (sample.diagnostics.steps >= samples[0].diagnostics.steps + steps) { + window.__sampleGalaxyFrame = null; + window.__engraphisGraph.freeze(true); + resolve(samples); + } + }; + window.__engraphisGraph.freeze(false); + }); + } const graph = window.__fg; const nodes = graph && typeof graph.graphData === 'function' ? graph.graphData().nodes.filter(node => !node.ghost) @@ -525,7 +554,7 @@ async function galaxySystemSnapshot(page) { finite: nodes.every(node => [node.x, node.y, node.vx, node.vy] .every(value => Number.isFinite(value))), }; - }); + }, stepCount); } /* Envelope clearance is a paint-space requirement: node centres can be distinct while complete @@ -936,15 +965,7 @@ async function gravityTrial(page, gravity, stepCount = 8) { localBaseline: window.EngraphisGraph._internals.galaxyLocalGravityConstant(48), localMaximum: window.EngraphisGraph._internals.galaxyLocalGravityConstant(200), })); - await page.evaluate(() => window.__engraphisGraph.freeze(false)); - const samples = [before]; - for (let step = 1; step <= stepCount; step += 1) { - await page.waitForFunction(({ start, minimum }) => - window.__engraphisGraph.physicsDiagnostics().steps >= start + minimum, - { start: before.diagnostics.steps, minimum: step }); - samples.push(await galaxySystemSnapshot(page)); - } - await page.evaluate(() => window.__engraphisGraph.freeze(true)); + const samples = await galaxySystemSnapshot(page, stepCount); const after = samples.at(-1); return { before, after, curve, @@ -991,15 +1012,7 @@ async function orbitalSeparationTrial(page, separation, stepCount = 8) { await page.waitForFunction(() => window.__fg.graphData().nodes.length === 9 && window.__engraphisGraph.physicsDiagnostics().frozen); const before = await galaxySystemSnapshot(page); - await page.evaluate(() => window.__engraphisGraph.freeze(false)); - const samples = []; - for (let step = 1; step <= stepCount; step += 1) { - await page.waitForFunction(({ start, minimum }) => - window.__engraphisGraph.physicsDiagnostics().steps >= start + minimum, - { start: before.diagnostics.steps, minimum: step }); - samples.push(await galaxySystemSnapshot(page)); - } - await page.evaluate(() => window.__engraphisGraph.freeze(true)); + const samples = (await galaxySystemSnapshot(page, stepCount)).slice(1); const after = samples.at(-1); const meanDiameter = snapshot => snapshot.systems.reduce( (sum, system) => sum + system.internalDiameter, 0, @@ -1038,6 +1051,7 @@ async function orbitalSeparationTrial(page, separation, stepCount = 8) { sample.diagnostics.lastRelationCorrectionDistance + sample.diagnostics.lastOrbitalCorrectionDistance), contactTrace: samples.map(sample => ({ + step: sample.diagnostics.steps, relation: sample.diagnostics.lastRelationCorrectionDistance, orbital: sample.diagnostics.lastOrbitalCorrectionDistance, overlaps: sample.diagnostics.lastOrbitalSeparations, @@ -3350,11 +3364,15 @@ test('Galaxy sliders retain full ranges with orbital-speed and radius response', // Use normal motion for this tuning sweep; the dedicated reduced-motion regression proves // that the same fixed solver and hierarchical orbits remain live under that preference. await page.emulateMedia({ reducedMotion: 'no-preference' }); - await openDashboard(page, { query: '?graph-engine=next' }); + await openDashboard(page, { query: '?graph-engine=next', capturePhysics: true }); await openGraphView(page); await page.waitForFunction(() => window.__engraphisGraph && window.__fg); const baseline = await gravityTrial(page, 48); const strong = await gravityTrial(page, 200); + await testInfo.attach('gravity-step-samples.json', { + body: Buffer.from(JSON.stringify({ baseline, strong }, null, 2)), + contentType: 'application/json', + }); const naturalOrbits = await orbitalSeparationTrial(page, 100); const fastOrbits = await orbitalSeparationTrial(page, 400, 16); await testInfo.attach('orbital-speed-convergence.json', { diff --git a/tests/test_managed_processing_policy.py b/tests/test_managed_processing_policy.py index 4d9a7fe7..9e4c84b1 100644 --- a/tests/test_managed_processing_policy.py +++ b/tests/test_managed_processing_policy.py @@ -165,6 +165,128 @@ def set_processing_policy(self, wid, **kwargs): build_managed_snapshot(svc, "a") +@pytest.mark.parametrize("override", ["0", "false", "no", " OFF ", "not-valid"]) +@pytest.mark.usefixtures("_http_stack") +def test_operator_optout_rejects_http_enable_before_cloud_access(svc, monkeypatch, override): + from fastapi import FastAPI + from fastapi.testclient import TestClient + from engraphis.cloud_features import CloudFeatureClient + from engraphis.routes import v2_api + + calls = [] + + class Cloud: + def get_processing_policy(self, wid): + calls.append("get") + return {"enabled": False, "revision": 1} + + def set_processing_policy(self, wid, **kwargs): + calls.append("put") + return {"enabled": kwargs["enabled"], "revision": 2} + + def configured(_): + calls.append("construct") + return Cloud() + + monkeypatch.setenv("ENGRAPHIS_MANAGED_COMPUTE_CONSENT", override) + monkeypatch.setattr(CloudFeatureClient, "from_environment", configured) + monkeypatch.setattr(v2_api, "service", lambda: svc) + before = svc.managed_processing_policy("a") + app = FastAPI() + app.include_router(v2_api.router) + with TestClient(app) as client: + response = client.post("/api/managed-processing", json={ + "workspace": "a", "enabled": True, "confirmed": True, + }) + assert response.status_code == 403 + assert response.json()["detail"]["code"] == "processing_operator_disabled" + assert calls == [] + assert svc.managed_processing_policy("a") == before + + +@pytest.mark.usefixtures("_http_stack") +def test_operator_optout_during_cloud_read_prevents_enable_submission(svc, monkeypatch): + from fastapi import FastAPI + from fastapi.testclient import TestClient + from engraphis.cloud_features import CloudFeatureClient + from engraphis.routes import v2_api + + calls = [] + + class Cloud: + def get_processing_policy(self, wid): + calls.append("get") + monkeypatch.setenv("ENGRAPHIS_MANAGED_COMPUTE_CONSENT", "0") + return {"enabled": False, "revision": 1} + + def set_processing_policy(self, wid, **kwargs): + calls.append("put") + return {"enabled": True, "revision": 2} + + monkeypatch.setattr(CloudFeatureClient, "from_environment", lambda _: Cloud()) + monkeypatch.setattr(v2_api, "service", lambda: svc) + app = FastAPI() + app.include_router(v2_api.router) + with TestClient(app) as client: + response = client.post("/api/managed-processing", json={ + "workspace": "a", "enabled": True, "confirmed": True, + }) + assert response.status_code == 409 + assert response.json()["detail"]["code"] == "processing_policy_changed" + assert calls == ["get"] + policy = svc.managed_processing_policy("a") + assert not policy["confirmed"] and policy["revision"] == 0 + + +def test_operator_optout_rejects_latent_local_approval(svc, monkeypatch): + before = svc.managed_processing_policy("a") + monkeypatch.setenv("ENGRAPHIS_MANAGED_COMPUTE_CONSENT", "0") + with pytest.raises(ValueError, match="disabled by this installation"): + svc.set_managed_processing_policy("a", enabled=True, confirmed=True, remote_revision=2) + monkeypatch.delenv("ENGRAPHIS_MANAGED_COMPUTE_CONSENT") + assert svc.managed_processing_policy("a") == before + + +@pytest.mark.parametrize("cloud_fails", [False, True]) +@pytest.mark.usefixtures("_http_stack") +def test_operator_optout_still_allows_remote_disable(svc, monkeypatch, cloud_fails): + from fastapi import FastAPI + from fastapi.testclient import TestClient + from engraphis.cloud_features import CloudFeatureClient + from engraphis.routes import v2_api + + svc.set_managed_processing_policy("a", enabled=True, confirmed=True, remote_revision=2) + monkeypatch.setenv("ENGRAPHIS_MANAGED_COMPUTE_CONSENT", "0") + calls = [] + + class Cloud: + def get_processing_policy(self, wid): + return {"enabled": True, "revision": 2} + + def set_processing_policy(self, wid, **kwargs): + calls.append(kwargs) + if cloud_fails: + raise CloudFeatureError("unavailable", status=503) + return {"enabled": False, "revision": 3} + + monkeypatch.setattr(CloudFeatureClient, "from_environment", lambda _: Cloud()) + monkeypatch.setattr(v2_api, "service", lambda: svc) + app = FastAPI() + app.include_router(v2_api.router) + with TestClient(app) as client: + response = client.post("/api/managed-processing", json={ + "workspace": "a", "enabled": False, + }) + assert response.status_code == 200 + policy = response.json() + assert not policy["enabled"] and policy["operator_disabled"] + assert policy["remote_sync_pending"] is cloud_fails + assert calls == [{"enabled": False, "confirmed": False, "revision": 2}] + monkeypatch.delenv("ENGRAPHIS_MANAGED_COMPUTE_CONSENT") + assert not svc.managed_processing_policy("a")["enabled"] + + + @pytest.mark.parametrize("delay_at", ["get", "put"]) @pytest.mark.usefixtures("_http_stack") def test_newer_optout_fences_delayed_cloud_enable(svc, monkeypatch, delay_at):