opencode-arch 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. opencode_arch/__init__.py +3 -0
  2. opencode_arch/artifacts/__init__.py +48 -0
  3. opencode_arch/artifacts/context.py +451 -0
  4. opencode_arch/artifacts/diagrams.py +451 -0
  5. opencode_arch/artifacts/selector.py +331 -0
  6. opencode_arch/artifacts/templates.py +444 -0
  7. opencode_arch/cli/__init__.py +1 -0
  8. opencode_arch/cli/bench.py +25 -0
  9. opencode_arch/cli/calibrate.py +208 -0
  10. opencode_arch/cli/confidence.py +66 -0
  11. opencode_arch/cli/docs.py +333 -0
  12. opencode_arch/cli/docs_validator.py +295 -0
  13. opencode_arch/cli/export_data.py +133 -0
  14. opencode_arch/cli/extract.py +93 -0
  15. opencode_arch/cli/gap_analyzer.py +107 -0
  16. opencode_arch/cli/generate.py +68 -0
  17. opencode_arch/cli/launch.py +264 -0
  18. opencode_arch/cli/main.py +360 -0
  19. opencode_arch/cli/metrics.py +186 -0
  20. opencode_arch/cli/prompts.py +20 -0
  21. opencode_arch/cli/regen_loop.py +1028 -0
  22. opencode_arch/context/__init__.py +29 -0
  23. opencode_arch/context/formatter.py +492 -0
  24. opencode_arch/context/pipeline_bridge.py +201 -0
  25. opencode_arch/extract/__init__.py +8 -0
  26. opencode_arch/extract/constraint_detector.py +398 -0
  27. opencode_arch/extract/from_artifacts.py +837 -0
  28. opencode_arch/extract/from_code.py +646 -0
  29. opencode_arch/extract/route_detector.py +400 -0
  30. opencode_arch/extract/table_parser.py +177 -0
  31. opencode_arch/learning/__init__.py +19 -0
  32. opencode_arch/learning/adapter.py +157 -0
  33. opencode_arch/learning/assessor.py +170 -0
  34. opencode_arch/learning/classifier.py +144 -0
  35. opencode_arch/learning/lessons.py +139 -0
  36. opencode_arch/learning/maintainer.py +281 -0
  37. opencode_arch/learning/patterns.py +51 -0
  38. opencode_arch/mcp/__init__.py +1 -0
  39. opencode_arch/mcp/__main__.py +8 -0
  40. opencode_arch/mcp/server.py +183 -0
  41. opencode_arch/mcp/tools/__init__.py +1 -0
  42. opencode_arch/mcp/tools/check.py +159 -0
  43. opencode_arch/mcp/tools/extract.py +107 -0
  44. opencode_arch/mcp/tools/feedback.py +65 -0
  45. opencode_arch/mcp/tools/generate.py +104 -0
  46. opencode_arch/mcp/tools/group.py +62 -0
  47. opencode_arch/mcp/tools/ingest.py +101 -0
  48. opencode_arch/mcp/tools/require.py +77 -0
  49. opencode_arch/mcp/tools/scan.py +53 -0
  50. opencode_arch/mcp/tools/slice.py +235 -0
  51. opencode_arch/mcp/tools/validate.py +59 -0
  52. opencode_arch/prompts/__init__.py +1 -0
  53. opencode_arch/prompts/regen.py +36 -0
  54. opencode_arch/runner/__init__.py +5 -0
  55. opencode_arch/runner/base.py +21 -0
  56. opencode_arch/runner/opencode.py +66 -0
  57. opencode_arch/telemetry/__init__.py +6 -0
  58. opencode_arch/telemetry/collector.py +40 -0
  59. opencode_arch/telemetry/recorder.py +12 -0
  60. opencode_arch/telemetry/store.py +537 -0
  61. opencode_arch-1.0.0.dist-info/METADATA +247 -0
  62. opencode_arch-1.0.0.dist-info/RECORD +65 -0
  63. opencode_arch-1.0.0.dist-info/WHEEL +4 -0
  64. opencode_arch-1.0.0.dist-info/entry_points.txt +2 -0
  65. opencode_arch-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,537 @@
1
+ """SQLite-backed telemetry store for tool invocations."""
2
+ from __future__ import annotations
3
+
4
+ import sqlite3
5
+ import time
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+
11
+ class TelemetryStore:
12
+ """Records tool invocations and outcomes for optimization."""
13
+
14
+ def __init__(self, db_path: Path | None = None):
15
+ if db_path is None:
16
+ db_path = Path.home() / ".opencode-arch" / "telemetry.db"
17
+ self.db_path = db_path
18
+ self.db_path.parent.mkdir(parents=True, exist_ok=True)
19
+ self._init_db()
20
+
21
+ def _init_db(self):
22
+ conn = sqlite3.connect(self.db_path)
23
+ conn.execute("""
24
+ CREATE TABLE IF NOT EXISTS invocations (
25
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
26
+ timestamp REAL NOT NULL,
27
+ tool TEXT NOT NULL,
28
+ repo TEXT,
29
+ context_tokens INTEGER DEFAULT 0,
30
+ output_quality INTEGER DEFAULT 0,
31
+ iterations INTEGER DEFAULT 1,
32
+ metadata TEXT
33
+ )
34
+ """)
35
+ conn.execute("""
36
+ CREATE TABLE IF NOT EXISTS regen_outcomes (
37
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
38
+ repo TEXT NOT NULL,
39
+ subsystem TEXT NOT NULL,
40
+ iteration INTEGER NOT NULL,
41
+ constant_count INTEGER DEFAULT 0,
42
+ signature_count INTEGER DEFAULT 0,
43
+ contract_count INTEGER DEFAULT 0,
44
+ pass_rate REAL DEFAULT 0.0,
45
+ time_seconds REAL DEFAULT 0.0,
46
+ prompt_tokens INTEGER DEFAULT 0,
47
+ source_equivalent_tokens INTEGER DEFAULT 0,
48
+ compression_ratio REAL DEFAULT 0.0,
49
+ mode TEXT DEFAULT 'normal',
50
+ timestamp TEXT
51
+ )
52
+ """)
53
+ # Migration: add token columns if they don't exist (for existing DBs)
54
+ try:
55
+ conn.execute("ALTER TABLE regen_outcomes ADD COLUMN prompt_tokens INTEGER DEFAULT 0")
56
+ conn.execute("ALTER TABLE regen_outcomes ADD COLUMN source_equivalent_tokens INTEGER DEFAULT 0")
57
+ conn.execute("ALTER TABLE regen_outcomes ADD COLUMN compression_ratio REAL DEFAULT 0.0")
58
+ conn.execute("ALTER TABLE regen_outcomes ADD COLUMN mode TEXT DEFAULT 'normal'")
59
+ except sqlite3.OperationalError:
60
+ pass # columns already exist
61
+
62
+ conn.execute("""
63
+ CREATE TABLE IF NOT EXISTS learning_curve (
64
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
65
+ repo TEXT NOT NULL,
66
+ repo_sequence INTEGER NOT NULL,
67
+ mode TEXT NOT NULL DEFAULT 'normal',
68
+ total_subsystems INTEGER DEFAULT 0,
69
+ converged_subsystems INTEGER DEFAULT 0,
70
+ avg_pass_rate REAL DEFAULT 0.0,
71
+ avg_iterations REAL DEFAULT 0.0,
72
+ avg_prompt_tokens REAL DEFAULT 0.0,
73
+ avg_source_equivalent REAL DEFAULT 0.0,
74
+ avg_compression_ratio REAL DEFAULT 0.0,
75
+ total_time_seconds REAL DEFAULT 0.0,
76
+ timestamp TEXT
77
+ )
78
+ """)
79
+ conn.execute("""
80
+ CREATE TABLE IF NOT EXISTS lessons (
81
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
82
+ lesson_id TEXT UNIQUE NOT NULL,
83
+ discovered_repo TEXT NOT NULL,
84
+ category TEXT NOT NULL,
85
+ description TEXT NOT NULL,
86
+ evidence TEXT DEFAULT '{}',
87
+ applied_to TEXT DEFAULT '[]',
88
+ impact REAL,
89
+ timestamp TEXT
90
+ )
91
+ """)
92
+ conn.execute("""
93
+ CREATE TABLE IF NOT EXISTS report_cards (
94
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
95
+ repo TEXT NOT NULL,
96
+ mode TEXT NOT NULL,
97
+ grade TEXT NOT NULL,
98
+ fidelity REAL,
99
+ compression_ratio REAL,
100
+ failure_patterns TEXT DEFAULT '{}',
101
+ novel_patterns INTEGER DEFAULT 0,
102
+ improvement_actions TEXT DEFAULT '[]',
103
+ timestamp TEXT
104
+ )
105
+ """)
106
+ conn.execute("""
107
+ CREATE TABLE IF NOT EXISTS drift_flags (
108
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
109
+ file TEXT NOT NULL,
110
+ issue TEXT NOT NULL,
111
+ severity TEXT NOT NULL,
112
+ auto_fixable INTEGER DEFAULT 0,
113
+ suggested_fix TEXT DEFAULT '',
114
+ resolved INTEGER DEFAULT 0,
115
+ timestamp TEXT
116
+ )
117
+ """)
118
+ conn.execute("""
119
+ CREATE TABLE IF NOT EXISTS function_metrics (
120
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
121
+ timestamp REAL NOT NULL,
122
+ tool TEXT NOT NULL,
123
+ function TEXT NOT NULL,
124
+ module TEXT NOT NULL DEFAULT '',
125
+ repo TEXT DEFAULT '',
126
+ time_ms REAL DEFAULT 0.0,
127
+ quality_scores TEXT DEFAULT '{}',
128
+ input_metrics TEXT DEFAULT '{}',
129
+ output_metrics TEXT DEFAULT '{}'
130
+ )
131
+ """)
132
+ conn.commit()
133
+ conn.close()
134
+
135
+ def record(self, tool: str, repo: str = "", context_tokens: int = 0,
136
+ output_quality: int = 0, iterations: int = 1, metadata: str = ""):
137
+ conn = sqlite3.connect(self.db_path)
138
+ conn.execute(
139
+ "INSERT INTO invocations (timestamp, tool, repo, context_tokens, output_quality, iterations, metadata) "
140
+ "VALUES (?, ?, ?, ?, ?, ?, ?)",
141
+ (time.time(), tool, repo, context_tokens, output_quality, iterations, metadata),
142
+ )
143
+ conn.commit()
144
+ conn.close()
145
+
146
+ def query(self, tool: str | None = None, limit: int = 100) -> list[dict[str, Any]]:
147
+ conn = sqlite3.connect(self.db_path)
148
+ conn.row_factory = sqlite3.Row
149
+ if tool:
150
+ rows = conn.execute(
151
+ "SELECT * FROM invocations WHERE tool = ? ORDER BY timestamp DESC LIMIT ?",
152
+ (tool, limit),
153
+ ).fetchall()
154
+ else:
155
+ rows = conn.execute(
156
+ "SELECT * FROM invocations ORDER BY timestamp DESC LIMIT ?",
157
+ (limit,),
158
+ ).fetchall()
159
+ conn.close()
160
+ return [dict(row) for row in rows]
161
+
162
+ def averages(self, tool: str) -> dict[str, float]:
163
+ conn = sqlite3.connect(self.db_path)
164
+ row = conn.execute(
165
+ "SELECT AVG(context_tokens), AVG(output_quality), AVG(iterations) "
166
+ "FROM invocations WHERE tool = ?",
167
+ (tool,),
168
+ ).fetchone()
169
+ conn.close()
170
+ return {
171
+ "avg_context_tokens": row[0] or 0,
172
+ "avg_output_quality": row[1] or 0,
173
+ "avg_iterations": row[2] or 0,
174
+ }
175
+
176
+ # ------------------------------------------------------------------
177
+ # Regen-loop learning store
178
+ # ------------------------------------------------------------------
179
+
180
+ def log_regen_outcome(
181
+ self,
182
+ repo: str,
183
+ subsystem: str,
184
+ iteration: int,
185
+ features: dict[str, int],
186
+ pass_rate: float,
187
+ time_seconds: float,
188
+ prompt_tokens: int = 0,
189
+ source_equivalent_tokens: int = 0,
190
+ compression_ratio: float = 0.0,
191
+ mode: str = "normal",
192
+ ):
193
+ """Log a regeneration attempt outcome.
194
+
195
+ Args:
196
+ repo: Repository name.
197
+ subsystem: Subsystem name.
198
+ iteration: Which iteration converged (or max if didn't).
199
+ features: Dict with constant_count, signature_count, contract_count.
200
+ pass_rate: Final pass rate achieved.
201
+ time_seconds: Wall-clock time for this subsystem.
202
+ prompt_tokens: Tokens used in the prompt.
203
+ source_equivalent_tokens: Tokens agent would need without extension.
204
+ compression_ratio: source_equivalent / prompt_tokens.
205
+ mode: Regen mode ('normal' or 'blind').
206
+ """
207
+ conn = sqlite3.connect(self.db_path)
208
+ conn.execute(
209
+ "INSERT INTO regen_outcomes "
210
+ "(repo, subsystem, iteration, constant_count, signature_count, contract_count, "
211
+ "pass_rate, time_seconds, prompt_tokens, source_equivalent_tokens, "
212
+ "compression_ratio, mode, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
213
+ (
214
+ repo,
215
+ subsystem,
216
+ iteration,
217
+ features.get("constant_count", 0),
218
+ features.get("signature_count", 0),
219
+ features.get("contract_count", 0),
220
+ pass_rate,
221
+ time_seconds,
222
+ prompt_tokens,
223
+ source_equivalent_tokens,
224
+ compression_ratio,
225
+ mode,
226
+ datetime.now(timezone.utc).isoformat(),
227
+ ),
228
+ )
229
+ conn.commit()
230
+ conn.close()
231
+
232
+ def get_patterns(self, repo_category: str | None = None) -> list[dict[str, Any]]:
233
+ """Retrieve learned patterns from regen outcomes.
234
+
235
+ Returns aggregated stats per subsystem showing average pass rates,
236
+ iteration counts, and feature correlations.
237
+
238
+ Args:
239
+ repo_category: Optional filter by repo name pattern.
240
+
241
+ Returns:
242
+ List of dicts with pattern information.
243
+ """
244
+ conn = sqlite3.connect(self.db_path)
245
+ conn.row_factory = sqlite3.Row
246
+
247
+ if repo_category:
248
+ rows = conn.execute(
249
+ "SELECT subsystem, "
250
+ "AVG(pass_rate) as avg_pass_rate, "
251
+ "AVG(iteration) as avg_iterations, "
252
+ "AVG(constant_count) as avg_constants, "
253
+ "AVG(signature_count) as avg_signatures, "
254
+ "AVG(contract_count) as avg_contracts, "
255
+ "COUNT(*) as attempts "
256
+ "FROM regen_outcomes WHERE repo LIKE ? "
257
+ "GROUP BY subsystem ORDER BY avg_pass_rate DESC",
258
+ (f"%{repo_category}%",),
259
+ ).fetchall()
260
+ else:
261
+ rows = conn.execute(
262
+ "SELECT subsystem, "
263
+ "AVG(pass_rate) as avg_pass_rate, "
264
+ "AVG(iteration) as avg_iterations, "
265
+ "AVG(constant_count) as avg_constants, "
266
+ "AVG(signature_count) as avg_signatures, "
267
+ "AVG(contract_count) as avg_contracts, "
268
+ "COUNT(*) as attempts "
269
+ "FROM regen_outcomes "
270
+ "GROUP BY subsystem ORDER BY avg_pass_rate DESC",
271
+ ).fetchall()
272
+
273
+ conn.close()
274
+ return [dict(row) for row in rows]
275
+
276
+ def query_regen_outcomes(self, repo: str | None = None, limit: int = 50) -> list[dict[str, Any]]:
277
+ """Query raw regen outcome records.
278
+
279
+ Args:
280
+ repo: Optional filter by repo name.
281
+ limit: Max records to return.
282
+
283
+ Returns:
284
+ List of outcome records as dicts.
285
+ """
286
+ conn = sqlite3.connect(self.db_path)
287
+ conn.row_factory = sqlite3.Row
288
+
289
+ if repo:
290
+ rows = conn.execute(
291
+ "SELECT * FROM regen_outcomes WHERE repo = ? ORDER BY timestamp DESC LIMIT ?",
292
+ (repo, limit),
293
+ ).fetchall()
294
+ else:
295
+ rows = conn.execute(
296
+ "SELECT * FROM regen_outcomes ORDER BY timestamp DESC LIMIT ?",
297
+ (limit,),
298
+ ).fetchall()
299
+
300
+ conn.close()
301
+ return [dict(row) for row in rows]
302
+
303
+ # ------------------------------------------------------------------
304
+ # Learning curve tracking
305
+ # ------------------------------------------------------------------
306
+
307
+ def record_learning_curve(
308
+ self,
309
+ repo: str,
310
+ mode: str,
311
+ total_subsystems: int,
312
+ converged_subsystems: int,
313
+ avg_pass_rate: float,
314
+ avg_iterations: float,
315
+ avg_prompt_tokens: float,
316
+ avg_source_equivalent: float,
317
+ avg_compression_ratio: float,
318
+ total_time_seconds: float,
319
+ ):
320
+ """Record per-repo summary for learning curve trend analysis.
321
+
322
+ repo_sequence is auto-computed as the count of previous entries + 1.
323
+ This allows tracking: does the system get better with each new repo?
324
+ """
325
+ conn = sqlite3.connect(self.db_path)
326
+ # Auto-compute sequence number
327
+ row = conn.execute(
328
+ "SELECT COALESCE(MAX(repo_sequence), 0) FROM learning_curve"
329
+ ).fetchone()
330
+ seq = (row[0] or 0) + 1
331
+
332
+ conn.execute(
333
+ "INSERT INTO learning_curve "
334
+ "(repo, repo_sequence, mode, total_subsystems, converged_subsystems, "
335
+ "avg_pass_rate, avg_iterations, avg_prompt_tokens, avg_source_equivalent, "
336
+ "avg_compression_ratio, total_time_seconds, timestamp) "
337
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
338
+ (repo, seq, mode, total_subsystems, converged_subsystems,
339
+ avg_pass_rate, avg_iterations, avg_prompt_tokens, avg_source_equivalent,
340
+ avg_compression_ratio, total_time_seconds,
341
+ datetime.now(timezone.utc).isoformat()),
342
+ )
343
+ conn.commit()
344
+ conn.close()
345
+
346
+ def get_learning_curve(self, mode: str | None = None) -> list[dict[str, Any]]:
347
+ """Get learning curve data ordered by repo sequence.
348
+
349
+ Shows how metrics improve with each successive repo processed.
350
+ Key metrics that should IMPROVE (go down):
351
+ - avg_iterations: fewer attempts needed
352
+ - avg_prompt_tokens: more efficient prompts
353
+
354
+ Key metrics that should IMPROVE (go up):
355
+ - avg_pass_rate: higher fidelity
356
+ - avg_compression_ratio: better token arbitrage
357
+ - converged_subsystems / total_subsystems: higher success rate
358
+ """
359
+ conn = sqlite3.connect(self.db_path)
360
+ conn.row_factory = sqlite3.Row
361
+
362
+ if mode:
363
+ rows = conn.execute(
364
+ "SELECT * FROM learning_curve WHERE mode = ? ORDER BY repo_sequence ASC",
365
+ (mode,),
366
+ ).fetchall()
367
+ else:
368
+ rows = conn.execute(
369
+ "SELECT * FROM learning_curve ORDER BY repo_sequence ASC"
370
+ ).fetchall()
371
+
372
+ conn.close()
373
+ return [dict(row) for row in rows]
374
+
375
+ # ------------------------------------------------------------------
376
+ # Report cards and lessons
377
+ # ------------------------------------------------------------------
378
+
379
+ def record_report_card(self, repo: str, mode: str, grade: str, fidelity: float,
380
+ compression_ratio: float, failure_patterns: str,
381
+ novel_patterns: int, improvement_actions: str):
382
+ """Record a report card for a repo run."""
383
+ conn = sqlite3.connect(self.db_path)
384
+ conn.execute(
385
+ "INSERT INTO report_cards (repo, mode, grade, fidelity, compression_ratio, "
386
+ "failure_patterns, novel_patterns, improvement_actions, timestamp) "
387
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
388
+ (repo, mode, grade, fidelity, compression_ratio, failure_patterns,
389
+ novel_patterns, improvement_actions,
390
+ datetime.now(timezone.utc).isoformat()),
391
+ )
392
+ conn.commit()
393
+ conn.close()
394
+
395
+ def get_report_cards(self, repo: str | None = None, limit: int = 20) -> list[dict[str, Any]]:
396
+ """Query report cards, optionally filtered by repo."""
397
+ conn = sqlite3.connect(self.db_path)
398
+ conn.row_factory = sqlite3.Row
399
+ if repo:
400
+ rows = conn.execute(
401
+ "SELECT * FROM report_cards WHERE repo = ? ORDER BY timestamp DESC LIMIT ?",
402
+ (repo, limit),
403
+ ).fetchall()
404
+ else:
405
+ rows = conn.execute(
406
+ "SELECT * FROM report_cards ORDER BY timestamp DESC LIMIT ?",
407
+ (limit,),
408
+ ).fetchall()
409
+ conn.close()
410
+ return [dict(row) for row in rows]
411
+
412
+ def record_lesson(self, lesson_id: str, discovered_repo: str, category: str,
413
+ description: str, evidence: str = "{}"):
414
+ """Record a new lesson learned. Ignores duplicates (same lesson_id)."""
415
+ conn = sqlite3.connect(self.db_path)
416
+ try:
417
+ conn.execute(
418
+ "INSERT OR IGNORE INTO lessons (lesson_id, discovered_repo, category, "
419
+ "description, evidence, timestamp) VALUES (?, ?, ?, ?, ?, ?)",
420
+ (lesson_id, discovered_repo, category, description, evidence,
421
+ datetime.now(timezone.utc).isoformat()),
422
+ )
423
+ conn.commit()
424
+ finally:
425
+ conn.close()
426
+
427
+ def get_lessons(self, category: str | None = None) -> list[dict[str, Any]]:
428
+ """Query all lessons, optionally filtered by category."""
429
+ conn = sqlite3.connect(self.db_path)
430
+ conn.row_factory = sqlite3.Row
431
+ if category:
432
+ rows = conn.execute(
433
+ "SELECT * FROM lessons WHERE category = ? ORDER BY timestamp DESC",
434
+ (category,),
435
+ ).fetchall()
436
+ else:
437
+ rows = conn.execute("SELECT * FROM lessons ORDER BY timestamp DESC").fetchall()
438
+ conn.close()
439
+ return [dict(row) for row in rows]
440
+
441
+ def mark_lesson_applied(self, lesson_id: str, repo: str):
442
+ """Mark a lesson as applied to a specific repo."""
443
+ conn = sqlite3.connect(self.db_path)
444
+ # Get current applied_to
445
+ row = conn.execute(
446
+ "SELECT applied_to FROM lessons WHERE lesson_id = ?", (lesson_id,)
447
+ ).fetchone()
448
+ if row:
449
+ import json
450
+ current = json.loads(row[0] or "[]")
451
+ if repo not in current:
452
+ current.append(repo)
453
+ conn.execute(
454
+ "UPDATE lessons SET applied_to = ? WHERE lesson_id = ?",
455
+ (json.dumps(current), lesson_id),
456
+ )
457
+ conn.commit()
458
+ conn.close()
459
+
460
+ # ------------------------------------------------------------------
461
+ # Drift flags
462
+ # ------------------------------------------------------------------
463
+
464
+ def record_drift_flag(self, file: str, issue: str, severity: str,
465
+ auto_fixable: bool = False, suggested_fix: str = ""):
466
+ """Record a documentation drift flag."""
467
+ conn = sqlite3.connect(self.db_path)
468
+ conn.execute(
469
+ "INSERT INTO drift_flags (file, issue, severity, auto_fixable, suggested_fix, "
470
+ "resolved, timestamp) VALUES (?, ?, ?, ?, ?, 0, ?)",
471
+ (file, issue, severity, int(auto_fixable), suggested_fix,
472
+ datetime.now(timezone.utc).isoformat()),
473
+ )
474
+ conn.commit()
475
+ conn.close()
476
+
477
+ def get_drift_flags(self, resolved: bool = False) -> list[dict[str, Any]]:
478
+ """Get drift flags, optionally filtered by resolution status."""
479
+ conn = sqlite3.connect(self.db_path)
480
+ conn.row_factory = sqlite3.Row
481
+ rows = conn.execute(
482
+ "SELECT * FROM drift_flags WHERE resolved = ? ORDER BY timestamp DESC",
483
+ (int(resolved),),
484
+ ).fetchall()
485
+ conn.close()
486
+ return [dict(row) for row in rows]
487
+
488
+ def resolve_drift_flag(self, flag_id: int):
489
+ """Mark a drift flag as resolved."""
490
+ conn = sqlite3.connect(self.db_path)
491
+ conn.execute("UPDATE drift_flags SET resolved = 1 WHERE id = ?", (flag_id,))
492
+ conn.commit()
493
+ conn.close()
494
+
495
+ # ------------------------------------------------------------------
496
+ # Function metrics
497
+ # ------------------------------------------------------------------
498
+
499
+ def record_function_metric(self, tool: str, function: str, module: str = "",
500
+ repo: str = "", time_ms: float = 0.0,
501
+ quality_scores: str = "{}", input_metrics: str = "{}",
502
+ output_metrics: str = "{}") -> None:
503
+ """Record a function-level metric."""
504
+ conn = sqlite3.connect(self.db_path)
505
+ conn.execute(
506
+ """INSERT INTO function_metrics
507
+ (timestamp, tool, function, module, repo, time_ms, quality_scores, input_metrics, output_metrics)
508
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
509
+ (time.time(), tool, function, module, repo, time_ms, quality_scores, input_metrics, output_metrics),
510
+ )
511
+ conn.commit()
512
+ conn.close()
513
+
514
+ def get_function_metrics(self, *, repo: str = None, function: str = None,
515
+ tool: str = None, last: int = None) -> list[dict]:
516
+ """Query function metrics with optional filters."""
517
+ conn = sqlite3.connect(self.db_path)
518
+ query = "SELECT * FROM function_metrics WHERE 1=1"
519
+ params: list = []
520
+ if repo:
521
+ query += " AND repo = ?"
522
+ params.append(repo)
523
+ if function:
524
+ query += " AND function = ?"
525
+ params.append(function)
526
+ if tool:
527
+ query += " AND tool = ?"
528
+ params.append(tool)
529
+ query += " ORDER BY timestamp DESC"
530
+ if last:
531
+ query += " LIMIT ?"
532
+ params.append(last)
533
+ cursor = conn.execute(query, params)
534
+ columns = [desc[0] for desc in cursor.description]
535
+ rows = [dict(zip(columns, row)) for row in cursor.fetchall()]
536
+ conn.close()
537
+ return rows