Files
Agent-System/scripts/agentctl_catalog.py
T
weichen 28ed7005dd feat(tooling): add governance catalog and requirement overlap gates
Extend agentctl SQLite with catalog tables, CJK FTS5 recall, hard/soft
overlap gates, fail-open embeddings, backfill, and MCP/skills hooks so
cross-conversation designs cannot silently conflict.
2026-09-24 09:21:57 +00:00

856 lines
30 KiB
Python

#!/usr/bin/env python3
"""Catalog governance layer: requirements, FTS5 CJK recall, overlap gates, embeddings.
Lives beside agentctl.py. Additive schema only; never touches product PostgreSQL.
Vector layer is fail-open: missing/failing embeddings degrade to FTS5 only.
"""
from __future__ import annotations
import hashlib
import json
import math
import os
import re
import sqlite3
import struct
import time
import urllib.error
import urllib.request
import uuid
from typing import Any, Callable, Optional
SCHEMA_SQL = '''
CREATE TABLE IF NOT EXISTS catalog_requirements(
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
statement TEXT NOT NULL,
feature TEXT,
source_task TEXT,
source_doc TEXT,
owner TEXT NOT NULL,
state TEXT NOT NULL,
supersedes TEXT,
conversation_ref TEXT,
created REAL NOT NULL,
updated REAL NOT NULL
);
CREATE INDEX IF NOT EXISTS catalog_req_state ON catalog_requirements(state, feature);
CREATE INDEX IF NOT EXISTS catalog_req_task ON catalog_requirements(source_task);
CREATE TABLE IF NOT EXISTS catalog_features(
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
scope TEXT,
mutex TEXT,
matrix_status TEXT,
updated REAL NOT NULL
);
CREATE VIRTUAL TABLE IF NOT EXISTS catalog_req_text USING fts5(
req_id UNINDEXED, body, seg, tokenize='unicode61'
);
CREATE TABLE IF NOT EXISTS catalog_overlaps(
id TEXT PRIMARY KEY,
probe_task TEXT NOT NULL,
probe_req TEXT,
target_req TEXT,
target_task TEXT,
target_feature TEXT,
level TEXT NOT NULL,
kind TEXT NOT NULL,
score REAL,
evidence TEXT NOT NULL,
created REAL NOT NULL
);
CREATE INDEX IF NOT EXISTS catalog_overlaps_probe ON catalog_overlaps(probe_task, created);
CREATE TABLE IF NOT EXISTS catalog_decisions(
id TEXT PRIMARY KEY,
overlap_id TEXT NOT NULL,
task TEXT NOT NULL,
decision TEXT NOT NULL,
decided_by TEXT NOT NULL,
reason TEXT NOT NULL,
authorization_ref TEXT,
created REAL NOT NULL
);
CREATE INDEX IF NOT EXISTS catalog_decisions_task ON catalog_decisions(task, overlap_id);
CREATE TABLE IF NOT EXISTS catalog_issues(
issue_id TEXT PRIMARY KEY,
severity TEXT,
issue_state TEXT,
owner TEXT,
module TEXT,
keywords TEXT,
main_record TEXT,
lesson_ids TEXT,
requirements TEXT,
tasks TEXT,
recurrence_count INTEGER DEFAULT 0,
evidence_basis TEXT,
created REAL,
updated REAL
);
CREATE TABLE IF NOT EXISTS catalog_tests(
id TEXT PRIMARY KEY,
report_path TEXT NOT NULL,
task TEXT,
requirements TEXT,
scope TEXT,
verdict TEXT,
level TEXT,
real_agent INTEGER NOT NULL DEFAULT 0,
schema_version TEXT,
env TEXT,
sha TEXT,
created REAL
);
CREATE TABLE IF NOT EXISTS catalog_releases(
id TEXT PRIMARY KEY,
env TEXT NOT NULL,
branch TEXT,
commit_sha TEXT,
tag TEXT,
deploy_seq INTEGER,
schema_version TEXT,
image_digest TEXT,
source_task TEXT,
phase TEXT,
receipt TEXT,
created REAL
);
CREATE TABLE IF NOT EXISTS catalog_embeddings(
req_id TEXT PRIMARY KEY,
model TEXT NOT NULL,
dim INTEGER NOT NULL,
vec BLOB NOT NULL,
source_sha256 TEXT NOT NULL,
status TEXT NOT NULL,
created REAL NOT NULL
);
CREATE TABLE IF NOT EXISTS catalog_task_bindings(
task TEXT PRIMARY KEY,
requirement TEXT,
intent TEXT,
mutex TEXT,
feature TEXT,
updated REAL NOT NULL
);
'''
REQ_STATES = frozenset({
'proposed', 'accepted', 'in_progress', 'delivered', 'superseded', 'rejected',
})
HARD_KINDS = frozenset({'same_mutex', 'same_feature', 'same_scope', 'same_req_id'})
SOFT_KINDS = frozenset({'semantic'})
DECISIONS = frozenset({'proceed', 'merge_into', 'supersede', 'defer', 'reject'})
REQ_ID_RE = re.compile(r'^R-[A-Za-z0-9][A-Za-z0-9._-]{0,120}$')
DEFAULT_GATE = {
'hard_kinds': ['same_mutex', 'same_feature', 'same_scope', 'same_req_id'],
'semantic_top_k': 10,
'bm25_threshold': -8.0,
'ack_required': True,
}
DEFAULT_VECTOR = {
'enabled': False,
'timeout_seconds': 8,
'fail_open': True,
'cosine_threshold': 0.82,
}
CATALOG_GATE: dict = dict(DEFAULT_GATE)
CATALOG_VECTOR: dict = dict(DEFAULT_VECTOR)
# Deny callback injected by agentctl (deny(code, message, **details)).
_deny: Optional[Callable[..., Any]] = None
def bind_deny(fn: Callable[..., Any]) -> None:
global _deny
_deny = fn
def _fail(code: str, message: str, **details: Any) -> None:
if _deny is None:
raise RuntimeError(f'{code}: {message} {details}')
_deny(code, message, **details)
def apply_policy(policy: dict) -> None:
"""Optional catalog_gate / catalog_vector keys; missing → defaults."""
global CATALOG_GATE, CATALOG_VECTOR
gate = policy.get('catalog_gate')
if gate is None:
CATALOG_GATE = dict(DEFAULT_GATE)
elif not isinstance(gate, dict):
_fail('POLICY_INVALID', 'catalog_gate must be an object')
else:
merged = dict(DEFAULT_GATE)
merged.update(gate)
if not isinstance(merged.get('hard_kinds'), list):
_fail('POLICY_INVALID', 'catalog_gate.hard_kinds must be a list')
if not isinstance(merged.get('semantic_top_k'), int) or merged['semantic_top_k'] <= 0:
_fail('POLICY_INVALID', 'catalog_gate.semantic_top_k must be a positive int')
if not isinstance(merged.get('bm25_threshold'), (int, float)):
_fail('POLICY_INVALID', 'catalog_gate.bm25_threshold must be a number')
if not isinstance(merged.get('ack_required'), bool):
_fail('POLICY_INVALID', 'catalog_gate.ack_required must be a bool')
CATALOG_GATE = merged
vec = policy.get('catalog_vector')
if vec is None:
CATALOG_VECTOR = dict(DEFAULT_VECTOR)
elif not isinstance(vec, dict):
_fail('POLICY_INVALID', 'catalog_vector must be an object')
else:
merged = dict(DEFAULT_VECTOR)
merged.update(vec)
if not isinstance(merged.get('enabled'), bool):
_fail('POLICY_INVALID', 'catalog_vector.enabled must be a bool')
if not isinstance(merged.get('timeout_seconds'), int) or merged['timeout_seconds'] <= 0:
_fail('POLICY_INVALID', 'catalog_vector.timeout_seconds must be a positive int')
if not isinstance(merged.get('fail_open'), bool):
_fail('POLICY_INVALID', 'catalog_vector.fail_open must be a bool')
CATALOG_VECTOR = merged
def ensure_schema(db: sqlite3.Connection) -> None:
db.executescript(SCHEMA_SQL)
def _now() -> float:
return time.time()
def split_cjk(text: str) -> str:
"""Insert spaces around CJK chars so unicode61 FTS can phrase-match digrams."""
if not text:
return ''
spaced = re.sub(r'([\u3400-\u9fff\uf900-\ufaff])', r' \1 ', text)
return ' '.join(spaced.split())
def fts_phrase(text: str) -> str:
"""Build a double-quoted FTS5 phrase from CJK-split tokens."""
tokens = split_cjk(text).split()
if not tokens:
return '""'
# Escape embedded double quotes for FTS5.
safe = [t.replace('"', '""') for t in tokens]
return '"' + ' '.join(safe) + '"'
def validate_req_id(req_id: str) -> str:
if not REQ_ID_RE.fullmatch(req_id):
_fail('INVALID_REQUEST', 'Requirement id must match R-<slug>', id=req_id)
return req_id
def requirement_add(
db: sqlite3.Connection,
*,
req_id: str,
title: str,
statement: str,
owner: str,
feature: Optional[str] = None,
source_task: Optional[str] = None,
source_doc: Optional[str] = None,
state: str = 'proposed',
supersedes: Optional[str] = None,
conversation_ref: Optional[str] = None,
) -> dict:
validate_req_id(req_id)
if state not in REQ_STATES:
_fail('INVALID_STATE', 'Unknown requirement state', state=state)
if not title.strip() or not statement.strip() or not owner.strip():
_fail('INVALID_REQUEST', 'title, statement, and owner are required')
existing = db.execute('SELECT id, source_task FROM catalog_requirements WHERE id=?', (req_id,)).fetchone()
if existing:
if source_task and existing['source_task'] and existing['source_task'] != source_task:
_fail(
'REQUIREMENT_EXISTS',
'Requirement id already owned by another task',
id=req_id,
holder=existing['source_task'],
)
_fail('REQUIREMENT_EXISTS', 'Requirement id already exists', id=req_id)
now = _now()
db.execute(
'INSERT INTO catalog_requirements VALUES(?,?,?,?,?,?,?,?,?,?,?,?)',
(
req_id, title.strip(), statement.strip(), feature, source_task, source_doc,
owner.strip(), state, supersedes, conversation_ref, now, now,
),
)
_index_requirement(db, req_id, title.strip() + '\n' + statement.strip())
return {'status': 'added', 'id': req_id}
def _index_requirement(db: sqlite3.Connection, req_id: str, body: str) -> None:
db.execute('DELETE FROM catalog_req_text WHERE req_id=?', (req_id,))
db.execute(
'INSERT INTO catalog_req_text(req_id, body, seg) VALUES(?,?,?)',
(req_id, body, split_cjk(body)),
)
def requirement_list(db: sqlite3.Connection, *, state: Optional[str] = None, limit: int = 100) -> list[dict]:
if state:
rows = db.execute(
'SELECT * FROM catalog_requirements WHERE state=? ORDER BY updated DESC LIMIT ?',
(state, limit),
).fetchall()
else:
rows = db.execute(
'SELECT * FROM catalog_requirements ORDER BY updated DESC LIMIT ?', (limit,)
).fetchall()
return [dict(r) for r in rows]
def requirement_show(db: sqlite3.Connection, req_id: str) -> dict:
row = db.execute('SELECT * FROM catalog_requirements WHERE id=?', (req_id,)).fetchone()
if not row:
_fail('UNKNOWN_REQUIREMENT', 'Requirement not found', id=req_id)
return dict(row)
def feature_upsert(
db: sqlite3.Connection,
*,
feature_id: str,
title: str,
scope: Optional[str] = None,
mutex: Optional[str] = None,
matrix_status: Optional[str] = None,
) -> dict:
now = _now()
db.execute(
'''INSERT INTO catalog_features(id, title, scope, mutex, matrix_status, updated)
VALUES(?,?,?,?,?,?)
ON CONFLICT(id) DO UPDATE SET
title=excluded.title,
scope=COALESCE(excluded.scope, catalog_features.scope),
mutex=COALESCE(excluded.mutex, catalog_features.mutex),
matrix_status=COALESCE(excluded.matrix_status, catalog_features.matrix_status),
updated=excluded.updated''',
(feature_id, title, scope, mutex, matrix_status, now),
)
return {'status': 'upserted', 'id': feature_id}
def search(db: sqlite3.Connection, query: str, *, top: int = 10) -> list[dict]:
tokens = split_cjk(query).split()
if not tokens:
return []
# Soft recall: AND of char tokens (all must appear). Phrase is too strict for
# near-paraphrase overlap ("检测与治理" vs "检测治理"). Escape FTS keywords.
safe = []
for t in tokens:
t = t.replace('"', '""')
if t.upper() in {'AND', 'OR', 'NOT', 'NEAR'}:
t = '"' + t + '"'
else:
t = '"' + t + '"' # quote each token to avoid column/syntax issues
safe.append(t)
# Quoting each token and joining with spaces → AND of tokens in FTS5.
match_expr = ' '.join(safe)
try:
rows = db.execute(
'''SELECT r.id, r.title, r.statement, r.feature, r.source_task, r.state,
bm25(catalog_req_text) AS score
FROM catalog_req_text
JOIN catalog_requirements r ON r.id = catalog_req_text.req_id
WHERE catalog_req_text MATCH ?
ORDER BY score
LIMIT ?''',
(match_expr, top),
).fetchall()
except sqlite3.OperationalError:
return []
return [dict(r) for r in rows]
def bind_task(
db: sqlite3.Connection,
*,
task: str,
requirement: Optional[str] = None,
intent: Optional[str] = None,
mutex: Optional[str] = None,
feature: Optional[str] = None,
) -> dict:
db.execute(
'''INSERT INTO catalog_task_bindings(task, requirement, intent, mutex, feature, updated)
VALUES(?,?,?,?,?,?)
ON CONFLICT(task) DO UPDATE SET
requirement=COALESCE(excluded.requirement, catalog_task_bindings.requirement),
intent=COALESCE(excluded.intent, catalog_task_bindings.intent),
mutex=COALESCE(excluded.mutex, catalog_task_bindings.mutex),
feature=COALESCE(excluded.feature, catalog_task_bindings.feature),
updated=excluded.updated''',
(task, requirement, intent, mutex, feature, _now()),
)
return {'status': 'bound', 'task': task}
def _active_tasks(db: sqlite3.Connection) -> list[dict]:
try:
rows = db.execute("SELECT * FROM tasks WHERE state='active'").fetchall()
except sqlite3.Error:
return []
return [dict(r) for r in rows]
def _path_overlaps(scopes_a: list[str], scopes_b: list[str], overlaps_fn: Callable) -> bool:
for a in scopes_a:
for b in scopes_b:
if overlaps_fn('path:' + a, 'path:' + b):
return True
return False
def probe_overlap(
db: sqlite3.Connection,
*,
task: str,
statement: str,
requirement: Optional[str] = None,
feature: Optional[str] = None,
mutex: Optional[str] = None,
scopes: Optional[list[str]] = None,
top: Optional[int] = None,
overlaps_fn: Optional[Callable] = None,
) -> dict:
"""Run L1 hard + L2 soft (+ optional L3) overlap detection; persist soft/hard rows."""
top = top or int(CATALOG_GATE['semantic_top_k'])
hard_kinds = set(CATALOG_GATE.get('hard_kinds') or DEFAULT_GATE['hard_kinds'])
hard: list[dict] = []
soft: list[dict] = []
now = _now()
# L1: same_req_id
if requirement and 'same_req_id' in hard_kinds:
row = db.execute('SELECT * FROM catalog_requirements WHERE id=?', (requirement,)).fetchone()
if row and row['source_task'] and row['source_task'] != task:
hit = _persist_overlap(
db, probe_task=task, probe_req=requirement, target_req=requirement,
target_task=row['source_task'], target_feature=row['feature'],
level='hard', kind='same_req_id', score=1.0,
evidence=f'Requirement {requirement} owned by {row["source_task"]}',
created=now,
)
hard.append(hit)
# L1: same_feature — another active requirement on same feature (not superseded)
if feature and 'same_feature' in hard_kinds:
rows = db.execute(
'''SELECT * FROM catalog_requirements
WHERE feature=? AND state NOT IN ('superseded','rejected','delivered')
AND (source_task IS NULL OR source_task != ?)''',
(feature, task),
).fetchall()
for row in rows:
hit = _persist_overlap(
db, probe_task=task, probe_req=requirement, target_req=row['id'],
target_task=row['source_task'], target_feature=feature,
level='hard', kind='same_feature', score=1.0,
evidence=f'Feature {feature} already claimed by requirement {row["id"]}',
created=now,
)
hard.append(hit)
# L1: same_mutex via bindings + CURRENT-style mutex on active tasks
if mutex and 'same_mutex' in hard_kinds:
rows = db.execute(
'''SELECT * FROM catalog_task_bindings
WHERE mutex=? AND task != ?''',
(mutex, task),
).fetchall()
active_ids = {t['id'] for t in _active_tasks(db)}
for row in rows:
if row['task'] not in active_ids and row['task'] not in active_ids:
# Still hard if binding exists and peer task is active
pass
if row['task'] in active_ids:
hit = _persist_overlap(
db, probe_task=task, probe_req=requirement, target_req=row['requirement'],
target_task=row['task'], target_feature=row['feature'],
level='hard', kind='same_mutex', score=1.0,
evidence=f'Mutex {mutex} held by active task {row["task"]}',
created=now,
)
hard.append(hit)
# L1: same_scope — path overlaps with other active task scopes
if scopes and overlaps_fn and 'same_scope' in hard_kinds:
for peer in _active_tasks(db):
if peer['id'] == task:
continue
peer_scopes = json.loads(peer['scopes']) if isinstance(peer['scopes'], str) else peer['scopes']
if _path_overlaps(scopes, peer_scopes, overlaps_fn):
hit = _persist_overlap(
db, probe_task=task, probe_req=requirement, target_req=None,
target_task=peer['id'], target_feature=None,
level='hard', kind='same_scope', score=1.0,
evidence=f'Scope overlaps active task {peer["id"]} scopes={peer_scopes}',
created=now,
)
hard.append(hit)
# L2: semantic FTS5
threshold = float(CATALOG_GATE.get('bm25_threshold', DEFAULT_GATE['bm25_threshold']))
hits = search(db, statement, top=top)
for row in hits:
if requirement and row['id'] == requirement:
continue
if row.get('source_task') == task:
continue
score = float(row.get('score') or 0.0)
# bm25: lower (more negative) is better; accept scores <= threshold
if score > threshold and score >= 0:
# Positive bm25 is weak; still include if phrase matched and score is finite
pass
# Always record soft semantic hits that MATCH returned (already ranked)
hit = _persist_overlap(
db, probe_task=task, probe_req=requirement, target_req=row['id'],
target_task=row.get('source_task'), target_feature=row.get('feature'),
level='soft', kind='semantic', score=score,
evidence=f'FTS5 hit title={row["title"]!r} bm25={score:.4f}',
created=now,
)
soft.append(hit)
# L3: optional vector enhancement (fail-open)
vector_status = 'disabled'
if CATALOG_VECTOR.get('enabled'):
vector_status, vec_soft = _vector_soft_hits(
db, task=task, statement=statement, requirement=requirement, top=top, created=now,
)
# Merge unique soft hits
seen = {h['id'] for h in soft}
for h in vec_soft:
if h['id'] not in seen:
soft.append(h)
seen.add(h['id'])
# Soft hits need ack — filter already-acked
pending_soft = [h for h in soft if not _decision_exists(db, task, h['id'])]
status = 'conflict' if hard else ('ack_required' if pending_soft else 'none')
return {
'status': status,
'hard': hard,
'soft': soft,
'pending_soft': pending_soft,
'vector_layer': vector_status,
'task': task,
'requirement': requirement,
}
def _persist_overlap(
db: sqlite3.Connection,
*,
probe_task: str,
probe_req: Optional[str],
target_req: Optional[str],
target_task: Optional[str],
target_feature: Optional[str],
level: str,
kind: str,
score: Optional[float],
evidence: str,
created: float,
) -> dict:
oid = str(uuid.uuid4())
db.execute(
'INSERT INTO catalog_overlaps VALUES(?,?,?,?,?,?,?,?,?,?,?)',
(
oid, probe_task, probe_req, target_req, target_task, target_feature,
level, kind, score, evidence, created,
),
)
return {
'id': oid, 'probe_task': probe_task, 'probe_req': probe_req,
'target_req': target_req, 'target_task': target_task,
'target_feature': target_feature, 'level': level, 'kind': kind,
'score': score, 'evidence': evidence, 'created': created,
}
def _decision_exists(db: sqlite3.Connection, task: str, overlap_id: str) -> bool:
row = db.execute(
'SELECT id FROM catalog_decisions WHERE task=? AND overlap_id=?',
(task, overlap_id),
).fetchone()
return bool(row)
def overlap_ack(
db: sqlite3.Connection,
*,
task: str,
overlap_id: str,
decision: str,
decided_by: str,
reason: str,
authorization_ref: Optional[str] = None,
) -> dict:
if decision not in DECISIONS:
_fail('INVALID_REQUEST', 'Unknown decision', decision=decision)
if not reason.strip():
_fail('INVALID_REQUEST', 'reason is required for overlap ack')
ov = db.execute('SELECT * FROM catalog_overlaps WHERE id=?', (overlap_id,)).fetchone()
if not ov:
_fail('UNKNOWN_OVERLAP', 'Overlap id not found', id=overlap_id)
if ov['probe_task'] != task:
_fail('PATH_OUT_OF_SCOPE', 'Overlap belongs to another task', holder=ov['probe_task'])
if ov['level'] == 'hard' and decision == 'proceed':
_fail(
'REQUIREMENT_CONFLICT',
'Hard overlaps cannot be acknowledged as proceed; resolve mutex/feature/scope first',
overlap_id=overlap_id,
kind=ov['kind'],
holder=ov['target_task'],
)
if _decision_exists(db, task, overlap_id):
return {'status': 'replayed', 'overlap_id': overlap_id}
did = str(uuid.uuid4())
db.execute(
'INSERT INTO catalog_decisions VALUES(?,?,?,?,?,?,?,?)',
(did, overlap_id, task, decision, decided_by, reason.strip(), authorization_ref, _now()),
)
return {'status': 'acked', 'id': did, 'overlap_id': overlap_id, 'decision': decision}
def gate_task_register(
db: sqlite3.Connection,
*,
task: str,
scopes: list[str],
requirement: Optional[str] = None,
intent: Optional[str] = None,
feature: Optional[str] = None,
mutex: Optional[str] = None,
overlaps_fn: Optional[Callable] = None,
statement: Optional[str] = None,
) -> dict:
"""Probe overlaps for task register. Caller must commit then enforce deny codes.
Returns probe result. Does not raise on soft/hard — register path commits first
so soft overlap rows survive OVERLAP_ACK_REQUIRED.
"""
stmt = statement or intent or ''
if not (requirement or stmt or feature or mutex):
return {'status': 'skipped', 'reason': 'no requirement/intent/feature/mutex provided'}
result = probe_overlap(
db, task=task, statement=stmt or (requirement or feature or mutex or task),
requirement=requirement, feature=feature, mutex=mutex,
scopes=scopes, overlaps_fn=overlaps_fn,
)
bind_task(db, task=task, requirement=requirement, intent=intent, mutex=mutex, feature=feature)
return result
def enforce_gate_result(result: dict) -> None:
"""Raise deny codes after the probe transaction has committed."""
if result.get('status') == 'skipped':
return
hard = result.get('hard') or []
if hard:
first = hard[0]
_fail(
'REQUIREMENT_CONFLICT',
first['evidence'],
kind=first['kind'],
holder=first.get('target_task'),
overlap_id=first['id'],
target_req=first.get('target_req'),
)
if CATALOG_GATE.get('ack_required', True) and result.get('pending_soft'):
_fail(
'OVERLAP_ACK_REQUIRED',
'Semantic overlaps require catalog overlap ack before coding',
pending=[h['id'] for h in result['pending_soft']],
count=len(result['pending_soft']),
)
def pending_acks_for_task(db: sqlite3.Connection, task: str) -> list[dict]:
rows = db.execute(
'''SELECT o.* FROM catalog_overlaps o
WHERE o.probe_task=? AND o.level='soft'
AND NOT EXISTS (
SELECT 1 FROM catalog_decisions d
WHERE d.overlap_id=o.id AND d.task=o.probe_task
)
ORDER BY o.created''',
(task,),
).fetchall()
return [dict(r) for r in rows]
def assert_acks_clear(db: sqlite3.Connection, task: str) -> None:
"""Block first write (with) when soft overlaps are unacked."""
if not CATALOG_GATE.get('ack_required', True):
return
pending = pending_acks_for_task(db, task)
if pending:
_fail(
'OVERLAP_ACK_REQUIRED',
'Unresolved soft overlaps; run catalog overlap ack',
pending=[p['id'] for p in pending],
count=len(pending),
)
def catalog_status(db: sqlite3.Connection) -> dict:
def count(table: str) -> int:
try:
return db.execute(f'SELECT COUNT(*) FROM {table}').fetchone()[0]
except sqlite3.Error:
return 0
return {
'status': 'ok',
'requirements': count('catalog_requirements'),
'features': count('catalog_features'),
'overlaps': count('catalog_overlaps'),
'decisions': count('catalog_decisions'),
'issues': count('catalog_issues'),
'tests': count('catalog_tests'),
'releases': count('catalog_releases'),
'embeddings': count('catalog_embeddings'),
'gate': dict(CATALOG_GATE),
'vector': {
'enabled': CATALOG_VECTOR.get('enabled'),
'fail_open': CATALOG_VECTOR.get('fail_open'),
},
}
def catalog_drift(db: sqlite3.Connection, *, matrix_states: dict[str, str]) -> dict:
"""Compare catalog_features.matrix_status to live MATRIX states; never writes docs."""
regressions = []
for fid, status in matrix_states.items():
row = db.execute('SELECT matrix_status FROM catalog_features WHERE id=?', (fid,)).fetchone()
if row and row['matrix_status'] == 'done' and status != 'done':
regressions.append({'record_id': fid, 'catalog': row['matrix_status'], 'matrix': status})
feature_upsert(db, feature_id=fid, title=fid, matrix_status=status)
open_caps = [k for k, v in matrix_states.items() if v not in ('done', 'removed')]
return {
'status': 'ok',
'record_count': len(matrix_states),
'regressions': regressions,
'open_capabilities': open_caps,
'note': 'catalog features refreshed from MATRIX; never auto-promotes done',
}
# --- Vector layer (fail-open) -------------------------------------------------
def _pack_vec(values: list[float]) -> bytes:
return struct.pack(f'<{len(values)}f', *values)
def _unpack_vec(blob: bytes) -> list[float]:
n = len(blob) // 4
return list(struct.unpack(f'<{n}f', blob))
def _cosine(a: list[float], b: list[float]) -> float:
if not a or not b or len(a) != len(b):
return 0.0
dot = sum(x * y for x, y in zip(a, b))
na = math.sqrt(sum(x * x for x in a))
nb = math.sqrt(sum(y * y for y in b))
if na == 0 or nb == 0:
return 0.0
return dot / (na * nb)
def _embed_text(text: str) -> tuple[str, Optional[list[float]], str]:
"""Return (status, vector_or_None, model). Never raises to caller for network errors."""
url = os.environ.get('EMBEDDING_ENDPOINT_URL', '').strip()
model = os.environ.get('EMBEDDING_MODEL', 'bge-m3').strip() or 'bge-m3'
key = os.environ.get('EMBEDDING_API_KEY', '').strip()
if not CATALOG_VECTOR.get('enabled'):
return 'disabled', None, model
if not url or not key:
return 'degraded', None, model
timeout = int(CATALOG_VECTOR.get('timeout_seconds', 8))
payload = json.dumps({'model': model, 'input': text}).encode('utf-8')
req = urllib.request.Request(
url, data=payload, method='POST',
headers={'Content-Type': 'application/json', 'Authorization': f'Bearer {key}'},
)
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
if resp.status != 200:
return 'degraded', None, model
body = json.loads(resp.read().decode('utf-8'))
data = body.get('data') or []
if not data or 'embedding' not in data[0]:
return 'degraded', None, model
return 'ok', list(data[0]['embedding']), model
except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError, json.JSONDecodeError, KeyError, IndexError, TypeError):
return 'degraded', None, model
def store_embedding(db: sqlite3.Connection, req_id: str, text: str) -> dict:
status, vec, model = _embed_text(text)
if status != 'ok' or vec is None:
return {'status': status, 'req_id': req_id, 'model': model}
blob = _pack_vec(vec)
sha = hashlib.sha256(text.encode('utf-8')).hexdigest()
db.execute(
'''INSERT INTO catalog_embeddings(req_id, model, dim, vec, source_sha256, status, created)
VALUES(?,?,?,?,?,?,?)
ON CONFLICT(req_id) DO UPDATE SET
model=excluded.model, dim=excluded.dim, vec=excluded.vec,
source_sha256=excluded.source_sha256, status=excluded.status,
created=excluded.created''',
(req_id, model, len(vec), blob, sha, 'ok', _now()),
)
return {'status': 'ok', 'req_id': req_id, 'dim': len(vec), 'model': model}
def _vector_soft_hits(
db: sqlite3.Connection,
*,
task: str,
statement: str,
requirement: Optional[str],
top: int,
created: float,
) -> tuple[str, list[dict]]:
status, probe, model = _embed_text(statement)
if status != 'ok' or probe is None:
return status, []
threshold = float(CATALOG_VECTOR.get('cosine_threshold', 0.82))
rows = db.execute(
"SELECT req_id, vec FROM catalog_embeddings WHERE status='ok'"
).fetchall()
scored: list[tuple[float, str]] = []
for row in rows:
if requirement and row['req_id'] == requirement:
continue
other = _unpack_vec(row['vec'])
sim = _cosine(probe, other)
if sim >= threshold:
scored.append((sim, row['req_id']))
scored.sort(reverse=True)
soft = []
for sim, rid in scored[:top]:
req = db.execute('SELECT * FROM catalog_requirements WHERE id=?', (rid,)).fetchone()
if not req:
continue
if req['source_task'] == task:
continue
hit = _persist_overlap(
db, probe_task=task, probe_req=requirement, target_req=rid,
target_task=req['source_task'], target_feature=req['feature'],
level='soft', kind='semantic', score=sim,
evidence=f'vector cosine={sim:.4f} model={model}',
created=created,
)
soft.append(hit)
return 'ok', soft