synapse-hub 0.0.0-stage → 2.0.0
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.
- package/.agents/agents/failure-analyst.md +47 -0
- package/.agents/agents/live-explorer.md +41 -0
- package/.agents/agents/orchestrator.md +43 -0
- package/.agents/agents/pom-crafter.md +81 -0
- package/.agents/agents/quality-gatekeeper.md +47 -0
- package/.agents/agents/test-runner.md +43 -0
- package/.agents/rules/context-engineering.md +88 -0
- package/.agents/rules/git-push-protocol.md +101 -0
- package/.agents/rules/graphify.md +14 -0
- package/.agents/rules/lessons-learned.md +23 -0
- package/.agents/rules/mandatory-playwright-live.md +29 -0
- package/.agents/rules/skill-precedence.md +42 -0
- package/.agents/rules/systematic-debugging.md +99 -0
- package/.agents/skills/agent-orchestration-multi-agent-optimize/SKILL.md +239 -0
- package/.agents/skills/antigravity-test-sentinel/SKILL.md +64 -0
- package/.agents/skills/api-network-mocking-interception/SKILL.md +142 -0
- package/.agents/skills/api-spec-to-e2e-matrix/SKILL.md +70 -0
- package/.agents/skills/architecture-decision-records/SKILL.md +441 -0
- package/.agents/skills/auth-session-and-cookie-manager/SKILL.md +72 -0
- package/.agents/skills/backend-load-and-stress-patterns/SKILL.md +214 -0
- package/.agents/skills/bats-testing-patterns/SKILL.md +100 -0
- package/.agents/skills/bats-testing-patterns/resources/implementation-playbook.md +614 -0
- package/.agents/skills/clean-pom-crafter/SKILL.md +75 -0
- package/.agents/skills/clean-pom-crafter/examples/standard_test_case.groovy +28 -0
- package/.agents/skills/clean-pom-crafter/references/pom_patterns.groovy +138 -0
- package/.agents/skills/code-refactoring-refactor-clean/SKILL.md +64 -0
- package/.agents/skills/code-refactoring-refactor-clean/resources/implementation-playbook.md +879 -0
- package/.agents/skills/db-state-verifier-and-seeder/SKILL.md +123 -0
- package/.agents/skills/e2e-testing-patterns/SKILL.md +103 -0
- package/.agents/skills/e2e-testing-patterns/resources/implementation-playbook.md +522 -0
- package/.agents/skills/failure-autopsy-and-healing/SKILL.md +54 -0
- package/.agents/skills/failure-autopsy-and-healing/references/healing_matrix.md +64 -0
- package/.agents/skills/failure-autopsy-and-healing/references/qa_investigation_protocol.md +56 -0
- package/.agents/skills/failure-autopsy-and-healing/scripts/triage_failures.sh +98 -0
- package/.agents/skills/file-and-report-validator/SKILL.md +90 -0
- package/.agents/skills/flaky-test-sentinel-and-quarantine/SKILL.md +114 -0
- package/.agents/skills/github-actions-templates/SKILL.md +334 -0
- package/.agents/skills/graphify-impact-analyzer/SKILL.md +78 -0
- package/.agents/skills/katalon-cli-runner/SKILL.md +76 -0
- package/.agents/skills/katalon-cli-runner/references/groovy-patterns.md +209 -0
- package/.agents/skills/katalon-cli-runner/references/katalon-api.md +155 -0
- package/.agents/skills/katalon-cli-runner/references/runner_commands.md +67 -0
- package/.agents/skills/lam-script-generator/SKILL.md +83 -0
- package/.agents/skills/playwright-live-interaction/SKILL.md +86 -0
- package/.agents/skills/playwright-live-interaction/references/advanced_superpowers.md +160 -0
- package/.agents/skills/playwright-live-interaction/references/compact_accessibility_snapshots.md +75 -0
- package/.agents/skills/playwright-live-interaction/references/recipes.md +388 -0
- package/.agents/skills/playwright-live-interaction/references/swarm_orchestration.md +134 -0
- package/.agents/skills/security-dast-sentinel/SKILL.md +176 -0
- package/.agents/skills/self-healing-locator/SKILL.md +73 -0
- package/.agents/skills/synthetic-test-data-generator/SKILL.md +135 -0
- package/.agents/skills/token-and-context-optimizer/SKILL.md +70 -0
- package/.agents/skills/visual-regression-sentinel/SKILL.md +62 -0
- package/.agents/skills/wcag-audit-patterns/SKILL.md +77 -0
- package/.agents/skills/wcag-audit-patterns/resources/implementation-playbook.md +541 -0
- package/.agents/skills/web-vitals-and-performance-profiler/SKILL.md +65 -0
- package/.agents/templates/completion_report.md +57 -0
- package/.agents/workflows/graphify.md +10 -0
- package/.agents/workflows/test-impact.md +15 -0
- package/.agents/workflows/verify.md +34 -0
- package/AGENTS.md +115 -0
- package/README.md +394 -2
- package/bin/agent-qa.js +90 -0
- package/package.json +59 -4
- package/skills_manifest.json +262 -0
- package/templates/PROJECT_AI_GUIDE_TEMPLATE.md +97 -0
- package/templates/PROJECT_STATE_TEMPLATE.md +20 -0
- package/tools/TOOLS_RUNBOOK.md +298 -0
- package/tools/agent_canary.py +501 -0
- package/tools/analyze_project.py +242 -0
- package/tools/api_drift_sentinel.py +208 -0
- package/tools/auto_heal.py +189 -0
- package/tools/browser_helpers.js +593 -0
- package/tools/bug_reporter.py +224 -0
- package/tools/cache_resolver.py +846 -0
- package/tools/check_env.py +288 -0
- package/tools/ci_healing_hook.py +128 -0
- package/tools/cli.py +402 -0
- package/tools/config_decoupler.py +253 -0
- package/tools/db_sandbox.py +341 -0
- package/tools/distill_log.py +152 -0
- package/tools/e2e_flow_prober.ps1 +65 -0
- package/tools/flaky_detector.py +222 -0
- package/tools/fuzz_payloads.py +197 -0
- package/tools/generate_allure_report.ps1 +30 -0
- package/tools/generate_api_matrix.py +193 -0
- package/tools/generate_tc_from_story.py +182 -0
- package/tools/generate_test_reports.ps1 +135 -0
- package/tools/git_pre_commit_guard.py +203 -0
- package/tools/heal_pr_automator.py +195 -0
- package/tools/heuristic_locator_matcher.py +202 -0
- package/tools/infer_linter.py +238 -0
- package/tools/lint_ai_docs.py +254 -0
- package/tools/lookup_tc.py +165 -0
- package/tools/lookup_xpath.py +167 -0
- package/tools/matrix_sharder.py +146 -0
- package/tools/mcp_excel_server.ps1 +251 -0
- package/tools/mcp_server.py +490 -0
- package/tools/micro_mutator.py +309 -0
- package/tools/network_mock_shield.py +170 -0
- package/tools/omni_emitter.py +500 -0
- package/tools/omni_parser.py +435 -0
- package/tools/omni_transpiler.py +168 -0
- package/tools/pinpoint_failure.py +88 -0
- package/tools/project_state.py +169 -0
- package/tools/repo_map.json +12 -0
- package/tools/repo_map.py +277 -0
- package/tools/repo_map.txt +7 -0
- package/tools/safety_firewall.py +245 -0
- package/tools/setup_project.py +265 -0
- package/tools/smart_visual_masker.py +134 -0
- package/tools/test_impact.py +321 -0
- package/tools/tms_sync_bridge.py +342 -0
- package/tools/trace_data_flow.py +240 -0
- package/tools/traffic_session_converter.py +256 -0
- package/tools/universal_ir.py +480 -0
- package/tools/validate_groovy_scripts.py +173 -0
- package/tools/verify_all.py +496 -0
- package/tools/verify_all.sh +4 -0
- package/tools/visual_diff_canvas.py +142 -0
- package/tools/visual_som_helper.py +418 -0
- package/tools/wcag_audit.py +214 -0
|
@@ -0,0 +1,846 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Universal Local Semantic Memory Cache & Self-Healing Resolver
|
|
4
|
+
-------------------------------------------------------------
|
|
5
|
+
Provides 100% Offline, Zero-Token diagnostic resolution for known test failures.
|
|
6
|
+
Saves solutions directly into an SQLite database (tools/memory_cache.db).
|
|
7
|
+
When identical failures occur, returns pre-tested patches with 0 LLM Tokens.
|
|
8
|
+
|
|
9
|
+
Features:
|
|
10
|
+
1. Positive Memory Cache (healing_cache): Pre-tested, verified code patches.
|
|
11
|
+
2. Negative Memory Cache (failed_fixes): Blacklist tracking incorrect patches,
|
|
12
|
+
preventing AI Agents from falling into Blind Retry Loops.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import sys
|
|
16
|
+
import os
|
|
17
|
+
import sqlite3
|
|
18
|
+
import json
|
|
19
|
+
import argparse
|
|
20
|
+
import re
|
|
21
|
+
import math
|
|
22
|
+
import time
|
|
23
|
+
import functools
|
|
24
|
+
from collections import Counter
|
|
25
|
+
from datetime import datetime
|
|
26
|
+
|
|
27
|
+
# Enforce UTF-8 stdout encoding for Windows console compatibility
|
|
28
|
+
if hasattr(sys.stdout, 'reconfigure'):
|
|
29
|
+
try:
|
|
30
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
31
|
+
sys.stderr.reconfigure(encoding='utf-8')
|
|
32
|
+
except Exception:
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
GLOBAL_DIR = os.path.expanduser("~/.agent_memory")
|
|
36
|
+
GLOBAL_DB = os.path.join(GLOBAL_DIR, "global_cache.db")
|
|
37
|
+
LOCAL_DB = os.path.join(os.path.dirname(os.path.abspath(__file__)), "memory_cache.db")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def resolve_db_path(use_local=False):
|
|
41
|
+
"""Returns the SQLite database path. Defaults to Global Central Memory."""
|
|
42
|
+
if use_local:
|
|
43
|
+
return LOCAL_DB
|
|
44
|
+
os.makedirs(GLOBAL_DIR, exist_ok=True)
|
|
45
|
+
return GLOBAL_DB
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
DB_FILE = resolve_db_path()
|
|
49
|
+
|
|
50
|
+
SEED_SOLUTIONS = [
|
|
51
|
+
{
|
|
52
|
+
"error_signature": "ElementClickInterceptedException",
|
|
53
|
+
"module_name": "GLOBAL",
|
|
54
|
+
"symptom": "Modal backdrop or PrimeNG overlay (.p-component-overlay) blocks button click.",
|
|
55
|
+
"root_cause": "Angular PrimeNG animation fade-out latency.",
|
|
56
|
+
"groovy_patch": "((JavascriptExecutor) driver).executeScript(\"arguments[0].click();\", element);"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"error_signature": "Save_Button_Disabled",
|
|
60
|
+
"module_name": "GLOBAL",
|
|
61
|
+
"symptom": "Save button [ Submit ] remains dimmed/disabled.",
|
|
62
|
+
"root_cause": "Angular Reactive Form missing synthetic input/change event or required field empty.",
|
|
63
|
+
"groovy_patch": "WebElement el = findElement(By.cssSelector(\"input[formcontrolname='code']\"));\njsSetInput(el, uniqueCode);"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"error_signature": "StaleElementReferenceException",
|
|
67
|
+
"module_name": "GLOBAL",
|
|
68
|
+
"symptom": "Element is not attached to the DOM.",
|
|
69
|
+
"root_cause": "Angular re-rendered DOM node after async API response.",
|
|
70
|
+
"groovy_patch": "for (int i = 0; i < 3; i++) {\n try {\n findElement(by).click();\n break;\n } catch (StaleElementReferenceException e) {\n sleep(500);\n }\n}"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"error_signature": "Duplicate_Code_Error",
|
|
74
|
+
"module_name": "GLOBAL",
|
|
75
|
+
"symptom": "Toast alert: 'Code already exists' or 'Data already exists'.",
|
|
76
|
+
"root_cause": "Hardcoded or non-unique test identifier collided with existing record.",
|
|
77
|
+
"groovy_patch": "String code = BasePage.uniqueCode(\"AUTO_\");"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"error_signature": "PrimeNG_Dropdown_Empty",
|
|
81
|
+
"module_name": "GLOBAL",
|
|
82
|
+
"symptom": "Dropdown item not clicked or panel not visible.",
|
|
83
|
+
"root_cause": "Overlay panel (.p-dropdown-panel) has not rendered when click dispatched.",
|
|
84
|
+
"groovy_patch": "waitForElementVisible(By.cssSelector(\".p-dropdown-panel\"), 5);\nfindElement(By.xpath(\"//li[contains(., '\" + text + \"')]\")).click();"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"error_signature": "BlockUI_TimeoutException",
|
|
88
|
+
"module_name": "GLOBAL",
|
|
89
|
+
"symptom": "Timeout waiting for table or form interaction.",
|
|
90
|
+
"root_cause": "PrimeNG loading mask (.p-blockui, .p-progress-spinner) still active.",
|
|
91
|
+
"groovy_patch": "waitForElementNotVisible(By.cssSelector(\".p-blockui, .p-progress-spinner\"), 10);"
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
SEED_LESSONS_LEARNED = [
|
|
96
|
+
{
|
|
97
|
+
"page_context": "Checkout / Payment",
|
|
98
|
+
"error_signature": "ElementClickInterceptedException",
|
|
99
|
+
"root_cause": "Debounce 300ms hoặc PrimeNG overlay (.p-component-overlay) đang mờ dần.",
|
|
100
|
+
"failed_attempt": "findElement(By.id('confirm-btn')).click();",
|
|
101
|
+
"successful_fix": "waitForElementNotVisible('.p-component-overlay', 5); jsClick(btn);",
|
|
102
|
+
"lesson_summary": "Tại Checkout/Payment, button Confirm bị debounce hoặc overlay animation; không click trực tiếp mà phải đợi overlay biến mất hoặc dùng clickJS/clickSafe."
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"page_context": "Auth / Reactive Forms",
|
|
106
|
+
"error_signature": "Save_Button_Disabled / FormInvalid",
|
|
107
|
+
"root_cause": "Angular Reactive Form không nhận native DOM value do thiếu Zone.js synthetic input event.",
|
|
108
|
+
"failed_attempt": "input.sendKeys(uniqueCode); submitBtn.click();",
|
|
109
|
+
"successful_fix": "jsSetInput(input, uniqueCode); qa.audit();",
|
|
110
|
+
"lesson_summary": "Tại các form Angular/React, sendKeys chay không trigger validity; bắt buộc dispatch synthetic input/change event hoặc dùng qa.audit."
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"page_context": "Data Mining / Async Table",
|
|
114
|
+
"error_signature": "StaleElementReferenceException",
|
|
115
|
+
"root_cause": "Bảng dữ liệu re-render DOM node sau khi nhận API response bất đồng bộ.",
|
|
116
|
+
"failed_attempt": "tableRows.get(0).click();",
|
|
117
|
+
"successful_fix": "waitForElementNotVisible('.p-blockui, .p-progress-spinner', 10); retryClick(row);",
|
|
118
|
+
"lesson_summary": "Tại bảng dữ liệu async, tuyệt đối không click row ngay lập tức; phải đợi BlockUI/Spinner biến mất hoàn toàn để tránh Stale Element."
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
SEED_FAILED_FIXES = [
|
|
123
|
+
{
|
|
124
|
+
"error_signature": "StaleElementReferenceException",
|
|
125
|
+
"attempted_patch": "Thread.sleep(3000); findElement(by).click();",
|
|
126
|
+
"failure_reason": "Causes pipeline flakiness; fails whenever network lag or server response exceeds static sleep duration.",
|
|
127
|
+
"agent_role": "pom-crafter"
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"error_signature": "ElementClickInterceptedException",
|
|
131
|
+
"attempted_patch": "new Actions(driver).moveToElement(el).click().perform();",
|
|
132
|
+
"failure_reason": "Still blocked by modal backdrop because backdrop element sits physically above target element in z-index.",
|
|
133
|
+
"agent_role": "failure-analyst"
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"error_signature": "Save_Button_Disabled",
|
|
137
|
+
"attempted_patch": "el.sendKeys(uniqueCode);",
|
|
138
|
+
"failure_reason": "Standard Selenium sendKeys does not fire Angular Zone.js synthetic input/change events, leaving formControl invalid.",
|
|
139
|
+
"agent_role": "pom-crafter"
|
|
140
|
+
}
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def retry_on_locked(max_retries=5, initial_delay=0.05, backoff=2.0):
|
|
145
|
+
"""Decorator to retry SQLite operations if database is locked or busy."""
|
|
146
|
+
def decorator(func):
|
|
147
|
+
@functools.wraps(func)
|
|
148
|
+
def wrapper(*args, **kwargs):
|
|
149
|
+
delay = initial_delay
|
|
150
|
+
last_err = None
|
|
151
|
+
for attempt in range(max_retries):
|
|
152
|
+
try:
|
|
153
|
+
return func(*args, **kwargs)
|
|
154
|
+
except sqlite3.OperationalError as e:
|
|
155
|
+
last_err = e
|
|
156
|
+
err_msg = str(e).lower()
|
|
157
|
+
if "locked" in err_msg or "busy" in err_msg:
|
|
158
|
+
time.sleep(delay)
|
|
159
|
+
delay *= backoff
|
|
160
|
+
else:
|
|
161
|
+
raise
|
|
162
|
+
if last_err:
|
|
163
|
+
raise last_err
|
|
164
|
+
return wrapper
|
|
165
|
+
return decorator
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def get_db(db_path=None, timeout=30.0):
|
|
169
|
+
"""Returns a resilient SQLite connection configured with WAL mode and busy timeout."""
|
|
170
|
+
target = db_path or DB_FILE
|
|
171
|
+
conn = sqlite3.connect(target, timeout=timeout)
|
|
172
|
+
conn.row_factory = sqlite3.Row
|
|
173
|
+
try:
|
|
174
|
+
conn.execute("PRAGMA journal_mode=WAL;")
|
|
175
|
+
conn.execute("PRAGMA synchronous=NORMAL;")
|
|
176
|
+
conn.execute("PRAGMA busy_timeout=30000;")
|
|
177
|
+
except Exception:
|
|
178
|
+
pass
|
|
179
|
+
return conn
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@retry_on_locked()
|
|
183
|
+
def init_db(seed=True, target_db=None):
|
|
184
|
+
current_db = target_db or DB_FILE
|
|
185
|
+
with get_db(current_db) as conn:
|
|
186
|
+
cursor = conn.cursor()
|
|
187
|
+
# 1. Positive Memory Table (Verified Working Patches)
|
|
188
|
+
cursor.execute("""
|
|
189
|
+
CREATE TABLE IF NOT EXISTS healing_cache (
|
|
190
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
191
|
+
error_signature TEXT NOT NULL,
|
|
192
|
+
module_name TEXT DEFAULT 'GLOBAL',
|
|
193
|
+
symptom TEXT,
|
|
194
|
+
root_cause TEXT,
|
|
195
|
+
groovy_patch TEXT NOT NULL,
|
|
196
|
+
hit_count INTEGER DEFAULT 0,
|
|
197
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
198
|
+
UNIQUE(error_signature, module_name)
|
|
199
|
+
)
|
|
200
|
+
""")
|
|
201
|
+
|
|
202
|
+
# 2. Negative Memory Table (Blacklist of Failed Fix Attempts)
|
|
203
|
+
cursor.execute("""
|
|
204
|
+
CREATE TABLE IF NOT EXISTS failed_fixes (
|
|
205
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
206
|
+
error_signature TEXT NOT NULL,
|
|
207
|
+
attempted_patch TEXT NOT NULL,
|
|
208
|
+
failure_reason TEXT NOT NULL,
|
|
209
|
+
agent_role TEXT DEFAULT 'pom-crafter',
|
|
210
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
211
|
+
)
|
|
212
|
+
""")
|
|
213
|
+
|
|
214
|
+
# 3. Episodic Reflexion Table (Historical Lessons Learned)
|
|
215
|
+
cursor.execute("""
|
|
216
|
+
CREATE TABLE IF NOT EXISTS lessons_learned (
|
|
217
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
218
|
+
page_context TEXT DEFAULT 'GLOBAL',
|
|
219
|
+
error_signature TEXT NOT NULL,
|
|
220
|
+
root_cause TEXT,
|
|
221
|
+
failed_attempt TEXT,
|
|
222
|
+
successful_fix TEXT,
|
|
223
|
+
lesson_summary TEXT NOT NULL,
|
|
224
|
+
hit_count INTEGER DEFAULT 0,
|
|
225
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
226
|
+
)
|
|
227
|
+
""")
|
|
228
|
+
conn.commit()
|
|
229
|
+
|
|
230
|
+
# Migrate existing solutions from local DB if initializing global DB for the first time
|
|
231
|
+
if current_db == GLOBAL_DB and os.path.exists(LOCAL_DB):
|
|
232
|
+
try:
|
|
233
|
+
local_conn = get_db(LOCAL_DB)
|
|
234
|
+
rows = local_conn.execute("SELECT * FROM healing_cache").fetchall()
|
|
235
|
+
for r in rows:
|
|
236
|
+
cursor.execute("""
|
|
237
|
+
INSERT OR IGNORE INTO healing_cache
|
|
238
|
+
(error_signature, module_name, symptom, root_cause, groovy_patch, hit_count)
|
|
239
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
240
|
+
""", (r["error_signature"], r["module_name"], r["symptom"], r["root_cause"], r["groovy_patch"], r["hit_count"]))
|
|
241
|
+
conn.commit()
|
|
242
|
+
local_conn.close()
|
|
243
|
+
except Exception:
|
|
244
|
+
pass
|
|
245
|
+
|
|
246
|
+
if seed:
|
|
247
|
+
for s in SEED_SOLUTIONS:
|
|
248
|
+
cursor.execute("""
|
|
249
|
+
INSERT OR IGNORE INTO healing_cache
|
|
250
|
+
(error_signature, module_name, symptom, root_cause, groovy_patch)
|
|
251
|
+
VALUES (?, ?, ?, ?, ?)
|
|
252
|
+
""", (s["error_signature"], s["module_name"], s["symptom"], s["root_cause"], s["groovy_patch"]))
|
|
253
|
+
|
|
254
|
+
for f in SEED_FAILED_FIXES:
|
|
255
|
+
cursor.execute("""
|
|
256
|
+
SELECT COUNT(*) as cnt FROM failed_fixes
|
|
257
|
+
WHERE error_signature = ? AND attempted_patch = ?
|
|
258
|
+
""", (f["error_signature"], f["attempted_patch"]))
|
|
259
|
+
if cursor.fetchone()["cnt"] == 0:
|
|
260
|
+
cursor.execute("""
|
|
261
|
+
INSERT INTO failed_fixes (error_signature, attempted_patch, failure_reason, agent_role)
|
|
262
|
+
VALUES (?, ?, ?, ?)
|
|
263
|
+
""", (f["error_signature"], f["attempted_patch"], f["failure_reason"], f["agent_role"]))
|
|
264
|
+
|
|
265
|
+
for l in SEED_LESSONS_LEARNED:
|
|
266
|
+
cursor.execute("""
|
|
267
|
+
SELECT COUNT(*) as cnt FROM lessons_learned
|
|
268
|
+
WHERE error_signature = ? AND page_context = ?
|
|
269
|
+
""", (l["error_signature"], l["page_context"]))
|
|
270
|
+
if cursor.fetchone()["cnt"] == 0:
|
|
271
|
+
cursor.execute("""
|
|
272
|
+
INSERT INTO lessons_learned
|
|
273
|
+
(page_context, error_signature, root_cause, failed_attempt, successful_fix, lesson_summary)
|
|
274
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
275
|
+
""", (l["page_context"], l["error_signature"], l["root_cause"], l["failed_attempt"], l["successful_fix"], l["lesson_summary"]))
|
|
276
|
+
conn.commit()
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# Stop-words list filtering generic stacktrace, code runtime & English grammar noise
|
|
280
|
+
STOP_WORDS = {
|
|
281
|
+
# English stop words
|
|
282
|
+
"a", "about", "above", "after", "again", "against", "all", "am", "an", "and",
|
|
283
|
+
"any", "are", "aren't", "as", "at", "be", "because", "been", "before", "being",
|
|
284
|
+
"below", "between", "both", "but", "by", "can", "cannot", "could", "couldn't",
|
|
285
|
+
"did", "didn't", "do", "does", "doesn't", "doing", "don't", "down", "during",
|
|
286
|
+
"each", "few", "for", "from", "further", "had", "hadn't", "has", "hasn't",
|
|
287
|
+
"have", "haven't", "having", "he", "her", "here", "hers", "herself", "him",
|
|
288
|
+
"himself", "his", "how", "i", "if", "in", "into", "is", "isn't", "it", "it's",
|
|
289
|
+
"its", "itself", "let's", "me", "more", "most", "mustn't", "my", "myself",
|
|
290
|
+
"no", "nor", "not", "of", "off", "on", "once", "only", "or", "other", "ought",
|
|
291
|
+
"our", "ours", "ourselves", "out", "over", "own", "same", "shan't", "she",
|
|
292
|
+
"should", "shouldn't", "so", "some", "such", "than", "that", "that's", "the",
|
|
293
|
+
"their", "theirs", "them", "themselves", "then", "there", "there's", "these",
|
|
294
|
+
"they", "they'd", "they'll", "they're", "they've", "this", "those", "through",
|
|
295
|
+
"to", "too", "under", "until", "up", "very", "was", "wasn't", "we", "we'd",
|
|
296
|
+
"we'll", "we're", "we've", "were", "weren't", "what", "what's", "when", "where",
|
|
297
|
+
"which", "while", "who", "whom", "why", "with", "won't", "would", "wouldn't",
|
|
298
|
+
"you", "you'd", "you'll", "you're", "you've", "your", "yours", "yourself",
|
|
299
|
+
|
|
300
|
+
# Java / Groovy / Selenium / Katalon Stacktrace & Runtime Boilerplate Noise
|
|
301
|
+
"org", "com", "net", "io", "java", "lang", "util", "reflect", "sun", "native",
|
|
302
|
+
"impl", "accessor", "unknown", "source", "line", "method", "thread", "run",
|
|
303
|
+
"call", "invoke", "invoker", "kms", "katalon", "core", "webui", "keyword",
|
|
304
|
+
"builtin", "driver", "webdriver", "remote", "http", "internal", "client",
|
|
305
|
+
"server", "session", "step", "tc", "test", "tests", "case", "suite", "runner",
|
|
306
|
+
"element", "elements", "locator", "find", "finding", "found", "selenium",
|
|
307
|
+
"openqa", "chrome", "firefox", "edge", "safari", "webkit",
|
|
308
|
+
|
|
309
|
+
# Generic Error Noise (present in almost all failure traces)
|
|
310
|
+
"exception", "error", "failed", "failure", "caused", "cause", "trace",
|
|
311
|
+
"stacktrace", "message", "warning", "info", "debug", "null", "object"
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def tokenize(text):
|
|
316
|
+
"""
|
|
317
|
+
Extracts tokens with CamelCase decomposition and Stop-words stripping.
|
|
318
|
+
Preserves specific domain identifiers while eliminating generic stacktrace noise.
|
|
319
|
+
"""
|
|
320
|
+
if not text:
|
|
321
|
+
return []
|
|
322
|
+
words = re.findall(r'[a-zA-Z0-9_\-]+', text)
|
|
323
|
+
tokens = []
|
|
324
|
+
for w in words:
|
|
325
|
+
w_lower = w.lower().strip("-_")
|
|
326
|
+
# Filter Katalon / Framework built-in keyword class noise (e.g. ClickKeyword, SetTextKeyword)
|
|
327
|
+
if w_lower.endswith("keyword"):
|
|
328
|
+
continue
|
|
329
|
+
if len(w_lower) >= 2 and w_lower not in STOP_WORDS:
|
|
330
|
+
tokens.append(w_lower)
|
|
331
|
+
# Split CamelCase: "ElementClickIntercepted" -> "click", "intercepted"
|
|
332
|
+
camel_parts = re.findall(r'[A-Z]?[a-z]+|[A-Z]+(?=[A-Z][a-z]|\d|\W|$)|\d+', w)
|
|
333
|
+
if len(camel_parts) > 1:
|
|
334
|
+
for cp in camel_parts:
|
|
335
|
+
cp_lower = cp.lower().strip("-_")
|
|
336
|
+
if len(cp_lower) >= 2 and cp_lower not in STOP_WORDS and not cp_lower.endswith("keyword"):
|
|
337
|
+
tokens.append(cp_lower)
|
|
338
|
+
return tokens
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def compute_tfidf_similarities(query, documents):
|
|
342
|
+
"""
|
|
343
|
+
Computes TF-IDF weighted cosine similarity between a query and a candidate document corpus.
|
|
344
|
+
Applies inverse document frequency to dampen frequent boilerplate words and amplify
|
|
345
|
+
rare diagnostic markers (e.g. 'stale', 'intercepted', 'ng-invalid').
|
|
346
|
+
"""
|
|
347
|
+
query_tokens = tokenize(query)
|
|
348
|
+
if not query_tokens or not documents:
|
|
349
|
+
return [0.0] * len(documents)
|
|
350
|
+
|
|
351
|
+
doc_tokens_list = [tokenize(doc) for doc in documents]
|
|
352
|
+
all_docs = [query_tokens] + doc_tokens_list
|
|
353
|
+
total_docs = len(all_docs)
|
|
354
|
+
|
|
355
|
+
# Document Frequency across the combined corpus
|
|
356
|
+
df = Counter()
|
|
357
|
+
for doc in all_docs:
|
|
358
|
+
for token in set(doc):
|
|
359
|
+
df[token] += 1
|
|
360
|
+
|
|
361
|
+
# Smoothed IDF: ln(1 + N / (1 + df)) + 1
|
|
362
|
+
idf = {t: math.log(1.0 + total_docs / (1.0 + freq)) + 1.0 for t, freq in df.items()}
|
|
363
|
+
|
|
364
|
+
def get_tfidf_vec(tokens):
|
|
365
|
+
tf = Counter(tokens)
|
|
366
|
+
vec = {}
|
|
367
|
+
for t, count in tf.items():
|
|
368
|
+
vec[t] = (1.0 + math.log(count)) * idf.get(t, 1.0)
|
|
369
|
+
return vec
|
|
370
|
+
|
|
371
|
+
def norm(vec):
|
|
372
|
+
return math.sqrt(sum(val ** 2 for val in vec.values()))
|
|
373
|
+
|
|
374
|
+
query_vec = get_tfidf_vec(query_tokens)
|
|
375
|
+
query_norm = norm(query_vec)
|
|
376
|
+
if not query_norm:
|
|
377
|
+
return [0.0] * len(documents)
|
|
378
|
+
|
|
379
|
+
scores = []
|
|
380
|
+
for doc_tokens in doc_tokens_list:
|
|
381
|
+
if not doc_tokens:
|
|
382
|
+
scores.append(0.0)
|
|
383
|
+
continue
|
|
384
|
+
doc_vec = get_tfidf_vec(doc_tokens)
|
|
385
|
+
doc_norm = norm(doc_vec)
|
|
386
|
+
if not doc_norm:
|
|
387
|
+
scores.append(0.0)
|
|
388
|
+
continue
|
|
389
|
+
dot = sum(query_vec[t] * doc_vec[t] for t in set(query_vec.keys()) & set(doc_vec.keys()))
|
|
390
|
+
scores.append(float(dot) / (query_norm * doc_norm))
|
|
391
|
+
|
|
392
|
+
return scores
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def compute_cosine_similarity(text1, text2):
|
|
396
|
+
"""
|
|
397
|
+
Computes tokenized Cosine Similarity between two texts with stop-words filtration.
|
|
398
|
+
"""
|
|
399
|
+
tokens1 = tokenize(text1)
|
|
400
|
+
tokens2 = tokenize(text2)
|
|
401
|
+
if not tokens1 or not tokens2:
|
|
402
|
+
return 0.0
|
|
403
|
+
vec1 = Counter(tokens1)
|
|
404
|
+
vec2 = Counter(tokens2)
|
|
405
|
+
intersection = set(vec1.keys()) & set(vec2.keys())
|
|
406
|
+
if not intersection:
|
|
407
|
+
return 0.0
|
|
408
|
+
numerator = sum([vec1[x] * vec2[x] for x in intersection])
|
|
409
|
+
sum1 = sum([val**2 for val in vec1.values()])
|
|
410
|
+
sum2 = sum([val**2 for val in vec2.values()])
|
|
411
|
+
denominator = math.sqrt(sum1) * math.sqrt(sum2)
|
|
412
|
+
if not denominator:
|
|
413
|
+
return 0.0
|
|
414
|
+
return float(numerator) / denominator
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def get_failed_fixes(error_signature):
|
|
418
|
+
"""Retrieves all previously recorded failed fix attempts for this error signature."""
|
|
419
|
+
init_db(seed=True)
|
|
420
|
+
with get_db() as conn:
|
|
421
|
+
cursor = conn.cursor()
|
|
422
|
+
cursor.execute("""
|
|
423
|
+
SELECT * FROM failed_fixes
|
|
424
|
+
WHERE error_signature = ? OR error_signature LIKE ?
|
|
425
|
+
ORDER BY created_at DESC
|
|
426
|
+
""", (error_signature, f"%{error_signature}%"))
|
|
427
|
+
return [dict(r) for r in cursor.fetchall()]
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
@retry_on_locked()
|
|
431
|
+
def record_failed_fix(error_signature, attempted_patch, failure_reason, agent_role="pom-crafter"):
|
|
432
|
+
"""Records a failed bug fix attempt into negative memory so AI agents will never repeat it."""
|
|
433
|
+
init_db(seed=True)
|
|
434
|
+
with get_db() as conn:
|
|
435
|
+
cursor = conn.cursor()
|
|
436
|
+
cursor.execute("""
|
|
437
|
+
INSERT INTO failed_fixes (error_signature, attempted_patch, failure_reason, agent_role)
|
|
438
|
+
VALUES (?, ?, ?, ?)
|
|
439
|
+
""", (error_signature, attempted_patch, failure_reason, agent_role))
|
|
440
|
+
conn.commit()
|
|
441
|
+
return True
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def query_solution(error_signature, module_name="GLOBAL", threshold=0.35):
|
|
445
|
+
"""
|
|
446
|
+
Two-Way Query:
|
|
447
|
+
1. Returns verified working patch from positive memory.
|
|
448
|
+
2. Attaches blacklist of previously failed fixes from negative memory.
|
|
449
|
+
"""
|
|
450
|
+
init_db(seed=True)
|
|
451
|
+
with get_db() as conn:
|
|
452
|
+
cursor = conn.cursor()
|
|
453
|
+
# 1. Exact match with module
|
|
454
|
+
cursor.execute("""
|
|
455
|
+
SELECT * FROM healing_cache
|
|
456
|
+
WHERE error_signature = ? AND module_name = ?
|
|
457
|
+
""", (error_signature, module_name))
|
|
458
|
+
row = cursor.fetchone()
|
|
459
|
+
match_type = "EXACT_MODULE"
|
|
460
|
+
|
|
461
|
+
# 2. Fallback to GLOBAL match
|
|
462
|
+
if not row:
|
|
463
|
+
cursor.execute("""
|
|
464
|
+
SELECT * FROM healing_cache
|
|
465
|
+
WHERE error_signature = ? AND module_name = 'GLOBAL'
|
|
466
|
+
""", (error_signature,))
|
|
467
|
+
row = cursor.fetchone()
|
|
468
|
+
match_type = "GLOBAL_MATCH"
|
|
469
|
+
|
|
470
|
+
# 3. Fuzzy search on symptom/error
|
|
471
|
+
if not row:
|
|
472
|
+
cursor.execute("""
|
|
473
|
+
SELECT * FROM healing_cache
|
|
474
|
+
WHERE error_signature LIKE ? OR symptom LIKE ?
|
|
475
|
+
""", (f"%{error_signature}%", f"%{error_signature}%"))
|
|
476
|
+
row = cursor.fetchone()
|
|
477
|
+
match_type = "FUZZY_SQL"
|
|
478
|
+
|
|
479
|
+
# 4. Offline Semantic Vector TF-IDF Cosine Similarity
|
|
480
|
+
confidence = 1.0
|
|
481
|
+
if not row:
|
|
482
|
+
cursor.execute("SELECT * FROM healing_cache")
|
|
483
|
+
all_rows = cursor.fetchall()
|
|
484
|
+
if all_rows:
|
|
485
|
+
candidates_corpus = [
|
|
486
|
+
f"{c['error_signature']} {c['symptom']} {c['root_cause']}"
|
|
487
|
+
for c in all_rows
|
|
488
|
+
]
|
|
489
|
+
scores = compute_tfidf_similarities(error_signature, candidates_corpus)
|
|
490
|
+
best_idx = -1
|
|
491
|
+
best_score = 0.0
|
|
492
|
+
for idx, score in enumerate(scores):
|
|
493
|
+
if score > best_score:
|
|
494
|
+
best_score = score
|
|
495
|
+
best_idx = idx
|
|
496
|
+
|
|
497
|
+
if best_idx >= 0 and best_score >= threshold:
|
|
498
|
+
row = all_rows[best_idx]
|
|
499
|
+
match_type = f"SEMANTIC_TFIDF ({best_score*100:.1f}%)"
|
|
500
|
+
confidence = best_score
|
|
501
|
+
|
|
502
|
+
# Query negative memory for this error
|
|
503
|
+
failed_fixes = get_failed_fixes(error_signature)
|
|
504
|
+
|
|
505
|
+
if row:
|
|
506
|
+
cursor.execute("""
|
|
507
|
+
UPDATE healing_cache
|
|
508
|
+
SET hit_count = hit_count + 1
|
|
509
|
+
WHERE id = ?
|
|
510
|
+
""", (row["id"],))
|
|
511
|
+
conn.commit()
|
|
512
|
+
result = dict(row)
|
|
513
|
+
result["match_type"] = match_type
|
|
514
|
+
result["confidence"] = confidence
|
|
515
|
+
result["failed_fixes"] = failed_fixes
|
|
516
|
+
return result
|
|
517
|
+
elif failed_fixes:
|
|
518
|
+
# No solution yet, but we have failed fixes recorded!
|
|
519
|
+
return {
|
|
520
|
+
"error_signature": error_signature,
|
|
521
|
+
"module_name": module_name,
|
|
522
|
+
"symptom": "Unknown solution",
|
|
523
|
+
"root_cause": "Unknown",
|
|
524
|
+
"groovy_patch": None,
|
|
525
|
+
"match_type": "NEGATIVE_MEMORY_ONLY",
|
|
526
|
+
"confidence": 0.0,
|
|
527
|
+
"failed_fixes": failed_fixes
|
|
528
|
+
}
|
|
529
|
+
return None
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
@retry_on_locked()
|
|
533
|
+
def learn_solution(error_signature, module_name, symptom, root_cause, groovy_patch):
|
|
534
|
+
"""Saves a verified solution into positive memory."""
|
|
535
|
+
init_db(seed=True)
|
|
536
|
+
with get_db() as conn:
|
|
537
|
+
cursor = conn.cursor()
|
|
538
|
+
cursor.execute("""
|
|
539
|
+
INSERT INTO healing_cache (error_signature, module_name, symptom, root_cause, groovy_patch)
|
|
540
|
+
VALUES (?, ?, ?, ?, ?)
|
|
541
|
+
ON CONFLICT(error_signature, module_name) DO UPDATE SET
|
|
542
|
+
symptom = excluded.symptom,
|
|
543
|
+
root_cause = excluded.root_cause,
|
|
544
|
+
groovy_patch = excluded.groovy_patch
|
|
545
|
+
""", (error_signature, module_name or "GLOBAL", symptom, root_cause, groovy_patch))
|
|
546
|
+
conn.commit()
|
|
547
|
+
return True
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
@retry_on_locked()
|
|
551
|
+
def sync_db():
|
|
552
|
+
"""Bidirectional synchronization between Global Central Memory and Local Project Cache."""
|
|
553
|
+
global DB_FILE
|
|
554
|
+
# Initialize schemas for both Global and Local stores prior to sync
|
|
555
|
+
DB_FILE = resolve_db_path(use_local=False)
|
|
556
|
+
init_db(seed=True, target_db=GLOBAL_DB)
|
|
557
|
+
DB_FILE = resolve_db_path(use_local=True)
|
|
558
|
+
init_db(seed=False, target_db=LOCAL_DB)
|
|
559
|
+
|
|
560
|
+
g_conn = get_db(GLOBAL_DB)
|
|
561
|
+
l_conn = get_db(LOCAL_DB)
|
|
562
|
+
|
|
563
|
+
# 1. Sync positive cache
|
|
564
|
+
g_rows = g_conn.execute("SELECT * FROM healing_cache").fetchall()
|
|
565
|
+
for r in g_rows:
|
|
566
|
+
l_conn.execute("""
|
|
567
|
+
INSERT INTO healing_cache (error_signature, module_name, symptom, root_cause, groovy_patch, hit_count)
|
|
568
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
569
|
+
ON CONFLICT(error_signature, module_name) DO UPDATE SET
|
|
570
|
+
symptom = excluded.symptom,
|
|
571
|
+
root_cause = excluded.root_cause,
|
|
572
|
+
groovy_patch = excluded.groovy_patch
|
|
573
|
+
""", (r["error_signature"], r["module_name"], r["symptom"], r["root_cause"], r["groovy_patch"], r["hit_count"]))
|
|
574
|
+
l_conn.commit()
|
|
575
|
+
|
|
576
|
+
l_rows = l_conn.execute("SELECT * FROM healing_cache").fetchall()
|
|
577
|
+
for r in l_rows:
|
|
578
|
+
g_conn.execute("""
|
|
579
|
+
INSERT INTO healing_cache (error_signature, module_name, symptom, root_cause, groovy_patch, hit_count)
|
|
580
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
581
|
+
ON CONFLICT(error_signature, module_name) DO UPDATE SET
|
|
582
|
+
symptom = excluded.symptom,
|
|
583
|
+
root_cause = excluded.root_cause,
|
|
584
|
+
groovy_patch = excluded.groovy_patch
|
|
585
|
+
""", (r["error_signature"], r["module_name"], r["symptom"], r["root_cause"], r["groovy_patch"], r["hit_count"]))
|
|
586
|
+
g_conn.commit()
|
|
587
|
+
|
|
588
|
+
# 2. Sync negative failed fixes
|
|
589
|
+
g_failed = g_conn.execute("SELECT * FROM failed_fixes").fetchall()
|
|
590
|
+
for f in g_failed:
|
|
591
|
+
l_conn.execute("""
|
|
592
|
+
INSERT OR IGNORE INTO failed_fixes (error_signature, attempted_patch, failure_reason, agent_role)
|
|
593
|
+
VALUES (?, ?, ?, ?)
|
|
594
|
+
""", (f["error_signature"], f["attempted_patch"], f["failure_reason"], f["agent_role"]))
|
|
595
|
+
l_conn.commit()
|
|
596
|
+
|
|
597
|
+
# 3. Sync lessons learned
|
|
598
|
+
g_lessons = g_conn.execute("SELECT * FROM lessons_learned").fetchall()
|
|
599
|
+
for r in g_lessons:
|
|
600
|
+
l_conn.execute("""
|
|
601
|
+
INSERT OR IGNORE INTO lessons_learned
|
|
602
|
+
(page_context, error_signature, root_cause, failed_attempt, successful_fix, lesson_summary, hit_count)
|
|
603
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
604
|
+
""", (r["page_context"], r["error_signature"], r["root_cause"], r["failed_attempt"], r["successful_fix"], r["lesson_summary"], r["hit_count"]))
|
|
605
|
+
l_conn.commit()
|
|
606
|
+
|
|
607
|
+
l_conn.close()
|
|
608
|
+
g_conn.close()
|
|
609
|
+
return len(g_rows)
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
@retry_on_locked()
|
|
613
|
+
def record_lesson(page_context, error_signature, root_cause, failed_attempt, successful_fix, lesson_summary):
|
|
614
|
+
"""
|
|
615
|
+
Episodic Reflexion Memory:
|
|
616
|
+
Records a concise lesson learned (maximum 2 lines) into SQLite for cross-session reflection.
|
|
617
|
+
"""
|
|
618
|
+
init_db(seed=True)
|
|
619
|
+
with get_db() as conn:
|
|
620
|
+
cursor = conn.cursor()
|
|
621
|
+
cursor.execute("""
|
|
622
|
+
INSERT INTO lessons_learned
|
|
623
|
+
(page_context, error_signature, root_cause, failed_attempt, successful_fix, lesson_summary)
|
|
624
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
625
|
+
""", (page_context or "GLOBAL", error_signature, root_cause, failed_attempt, successful_fix, lesson_summary.strip()))
|
|
626
|
+
conn.commit()
|
|
627
|
+
return True
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def query_lessons(context_or_keyword, limit=2):
|
|
631
|
+
"""
|
|
632
|
+
Episodic Reflexion Memory Query:
|
|
633
|
+
Retrieves the top N (default: 2) most relevant historical lessons learned for a given page, flow, or error.
|
|
634
|
+
Returns: List[Dict[str, Any]]
|
|
635
|
+
"""
|
|
636
|
+
init_db(seed=True)
|
|
637
|
+
if not context_or_keyword:
|
|
638
|
+
context_or_keyword = "GLOBAL"
|
|
639
|
+
|
|
640
|
+
with get_db() as conn:
|
|
641
|
+
cursor = conn.cursor()
|
|
642
|
+
rows = cursor.execute("SELECT * FROM lessons_learned").fetchall()
|
|
643
|
+
if not rows:
|
|
644
|
+
return []
|
|
645
|
+
|
|
646
|
+
scored = []
|
|
647
|
+
q_lower = context_or_keyword.lower()
|
|
648
|
+
for r in rows:
|
|
649
|
+
combined = f"{r['page_context']} {r['error_signature']} {r['lesson_summary']} {r['root_cause'] or ''}"
|
|
650
|
+
sim_score = compute_cosine_similarity(context_or_keyword, combined)
|
|
651
|
+
|
|
652
|
+
# Boost score for explicit matches in page_context or error_signature
|
|
653
|
+
if r["page_context"] and r["page_context"].lower() in q_lower:
|
|
654
|
+
sim_score += 0.40
|
|
655
|
+
if r["error_signature"] and r["error_signature"].lower() in q_lower:
|
|
656
|
+
sim_score += 0.35
|
|
657
|
+
|
|
658
|
+
scored.append((sim_score, dict(r)))
|
|
659
|
+
|
|
660
|
+
scored.sort(key=lambda x: x[0], reverse=True)
|
|
661
|
+
top_results = [item[1] for item in scored[:limit]]
|
|
662
|
+
|
|
663
|
+
for item in top_results:
|
|
664
|
+
cursor.execute("UPDATE lessons_learned SET hit_count = hit_count + 1 WHERE id = ?", (item["id"],))
|
|
665
|
+
conn.commit()
|
|
666
|
+
|
|
667
|
+
return top_results
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
def get_stats():
|
|
671
|
+
init_db(seed=True)
|
|
672
|
+
with get_db() as conn:
|
|
673
|
+
cursor = conn.cursor()
|
|
674
|
+
cursor.execute("SELECT COUNT(*) as total_solutions, SUM(hit_count) as total_hits FROM healing_cache")
|
|
675
|
+
row = cursor.fetchone()
|
|
676
|
+
total_sol = row["total_solutions"] or 0
|
|
677
|
+
total_hits = row["total_hits"] or 0
|
|
678
|
+
saved_tokens = total_hits * 2000
|
|
679
|
+
|
|
680
|
+
cursor.execute("SELECT COUNT(*) as total_failed FROM failed_fixes")
|
|
681
|
+
total_failed = cursor.fetchone()["total_failed"] or 0
|
|
682
|
+
|
|
683
|
+
cursor.execute("SELECT COUNT(*) as total_lessons FROM lessons_learned")
|
|
684
|
+
total_lessons = cursor.fetchone()["total_lessons"] or 0
|
|
685
|
+
|
|
686
|
+
cursor.execute("SELECT error_signature, module_name, hit_count FROM healing_cache ORDER BY hit_count DESC")
|
|
687
|
+
top_solutions = [dict(r) for r in cursor.fetchall()]
|
|
688
|
+
|
|
689
|
+
return {
|
|
690
|
+
"total_solutions": total_sol,
|
|
691
|
+
"total_hits": total_hits,
|
|
692
|
+
"total_failed_fixes": total_failed,
|
|
693
|
+
"total_lessons_learned": total_lessons,
|
|
694
|
+
"estimated_tokens_saved": saved_tokens,
|
|
695
|
+
"solutions": top_solutions
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
def main():
|
|
700
|
+
global DB_FILE
|
|
701
|
+
parser = argparse.ArgumentParser(description="Enterprise Central Agent Memory & Self-Healing Resolver")
|
|
702
|
+
parser.add_argument("--local", action="store_true", help="Force local project memory_cache.db instead of Global Central Memory")
|
|
703
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
704
|
+
|
|
705
|
+
# Query command
|
|
706
|
+
query_parser = subparsers.add_parser("query", help="Query solution & anti-patches for an error (Zero Token)")
|
|
707
|
+
query_parser.add_argument("--error", required=True, help="Error signature or keyword")
|
|
708
|
+
query_parser.add_argument("--module", default="GLOBAL", help="Module or Page Class name")
|
|
709
|
+
|
|
710
|
+
# Learn command (Positive Memory)
|
|
711
|
+
learn_parser = subparsers.add_parser("learn", help="Save verified solution into positive memory")
|
|
712
|
+
learn_parser.add_argument("--error", required=True, help="Error signature")
|
|
713
|
+
learn_parser.add_argument("--module", default="GLOBAL", help="Module or Page Class name")
|
|
714
|
+
learn_parser.add_argument("--symptom", required=True, help="Symptom description")
|
|
715
|
+
learn_parser.add_argument("--cause", required=True, help="Root cause")
|
|
716
|
+
learn_parser.add_argument("--patch", required=True, help="Working patch code")
|
|
717
|
+
|
|
718
|
+
# Record-Failed command (Negative Memory)
|
|
719
|
+
failed_parser = subparsers.add_parser("record-failed", help="Record a failed fix attempt into negative memory blacklist")
|
|
720
|
+
failed_parser.add_argument("--error", required=True, help="Error signature being diagnosed")
|
|
721
|
+
failed_parser.add_argument("--patch", required=True, help="The failed patch code or attempted action")
|
|
722
|
+
failed_parser.add_argument("--reason", required=True, help="Why this fix failed or regression caused")
|
|
723
|
+
failed_parser.add_argument("--role", default="pom-crafter", help="Agent role that attempted the fix")
|
|
724
|
+
|
|
725
|
+
# Record-Lesson command (Episodic Reflexion Memory)
|
|
726
|
+
lesson_record_parser = subparsers.add_parser("record-lesson", help="Record a 2-line lesson learned into episodic memory")
|
|
727
|
+
lesson_record_parser.add_argument("--context", default="GLOBAL", help="Page name or workflow context (e.g., 'Checkout', 'Login')")
|
|
728
|
+
lesson_record_parser.add_argument("--error", required=True, help="Error signature or breaking symptom")
|
|
729
|
+
lesson_record_parser.add_argument("--cause", default="", help="Root cause explanation")
|
|
730
|
+
lesson_record_parser.add_argument("--failed", default="", help="Failed attempt or anti-pattern")
|
|
731
|
+
lesson_record_parser.add_argument("--fix", default="", help="Successful fix pattern")
|
|
732
|
+
lesson_record_parser.add_argument("--lesson", required=True, help="Short lesson summary (maximum 2 lines)")
|
|
733
|
+
|
|
734
|
+
# Get-Lessons command (Episodic Reflexion Memory Query)
|
|
735
|
+
lesson_get_parser = subparsers.add_parser("get-lessons", help="Retrieve top 2 historical lessons learned for context")
|
|
736
|
+
lesson_get_parser.add_argument("--context", required=True, help="Page, flow, or error keyword to match")
|
|
737
|
+
lesson_get_parser.add_argument("--limit", type=int, default=2, help="Number of lessons to retrieve (default: 2)")
|
|
738
|
+
|
|
739
|
+
# Stats command
|
|
740
|
+
subparsers.add_parser("stats", help="View memory cache statistics (Positive & Negative)")
|
|
741
|
+
|
|
742
|
+
# Sync command
|
|
743
|
+
subparsers.add_parser("sync", help="Synchronize Global Central Memory with Local Project Cache")
|
|
744
|
+
|
|
745
|
+
# Init command
|
|
746
|
+
subparsers.add_parser("init", help="Initialize and seed database")
|
|
747
|
+
|
|
748
|
+
args = parser.parse_args()
|
|
749
|
+
if args.local:
|
|
750
|
+
DB_FILE = resolve_db_path(use_local=True)
|
|
751
|
+
else:
|
|
752
|
+
DB_FILE = resolve_db_path(use_local=False)
|
|
753
|
+
|
|
754
|
+
if args.command == "query":
|
|
755
|
+
sol = query_solution(args.error, args.module)
|
|
756
|
+
if sol:
|
|
757
|
+
print("==================================================")
|
|
758
|
+
if sol.get("groovy_patch"):
|
|
759
|
+
print(f"✅ [CACHE HIT - 0 TOKENS] Found pre-tested solution!")
|
|
760
|
+
print(f"Database Scope : {'LOCAL' if args.local else 'GLOBAL CENTRAL (~/.agent_memory)'}")
|
|
761
|
+
print(f"Match Strategy : {sol.get('match_type', 'EXACT')}")
|
|
762
|
+
print(f"Error Signature : {sol['error_signature']}")
|
|
763
|
+
print(f"Module Scope : {sol['module_name']}")
|
|
764
|
+
print(f"Root Cause : {sol['root_cause']}")
|
|
765
|
+
print(f"Hit Count : {sol.get('hit_count', 0)}")
|
|
766
|
+
print("--------------------------------------------------")
|
|
767
|
+
print("💡 Recommended Healing Patch:")
|
|
768
|
+
print(sol["groovy_patch"])
|
|
769
|
+
else:
|
|
770
|
+
print(f"⚠️ [NO VERIFIED SOLUTION YET] for error: '{args.error}'")
|
|
771
|
+
|
|
772
|
+
failed_fixes = sol.get("failed_fixes", [])
|
|
773
|
+
if failed_fixes:
|
|
774
|
+
print("--------------------------------------------------")
|
|
775
|
+
print(f"🚫 FORBIDDEN ANTI-PATCHES ({len(failed_fixes)} previously failed - DO NOT REPEAT):")
|
|
776
|
+
for idx, ff in enumerate(failed_fixes, 1):
|
|
777
|
+
print(f" {idx}. [FAILED ATTEMPT by {ff.get('agent_role', 'agent')}]:")
|
|
778
|
+
print(f" Patch : {ff.get('attempted_patch')}")
|
|
779
|
+
print(f" Reason: {ff.get('failure_reason')}")
|
|
780
|
+
print("==================================================")
|
|
781
|
+
else:
|
|
782
|
+
print(f"❌ [CACHE MISS] No cached solution found for: '{args.error}'. Triggering LLM reasoning.")
|
|
783
|
+
sys.exit(1)
|
|
784
|
+
|
|
785
|
+
elif args.command == "learn":
|
|
786
|
+
ok = learn_solution(args.error, args.module, args.symptom, args.cause, args.patch)
|
|
787
|
+
if ok:
|
|
788
|
+
print(f"✅ [POSITIVE MEMORY LEARNED] Solution for '{args.error}' ({args.module}) cached successfully.")
|
|
789
|
+
|
|
790
|
+
elif args.command == "record-failed":
|
|
791
|
+
ok = record_failed_fix(args.error, args.patch, args.reason, args.role)
|
|
792
|
+
if ok:
|
|
793
|
+
print(f"🚫 [NEGATIVE MEMORY RECORDED] Failed fix attempt for '{args.error}' added to blacklist.")
|
|
794
|
+
print(f" Agent Role : {args.role}")
|
|
795
|
+
print(f" Reason : {args.reason}")
|
|
796
|
+
|
|
797
|
+
elif args.command == "record-lesson":
|
|
798
|
+
ok = record_lesson(args.context, args.error, args.cause, args.failed, args.fix, args.lesson)
|
|
799
|
+
if ok:
|
|
800
|
+
print(f"🧠 [EPISODIC REFLEXION RECORDED] Lesson for context '{args.context}' cached successfully.")
|
|
801
|
+
print(f" Error Signature : {args.error}")
|
|
802
|
+
print(f" Lesson (2 lines): {args.lesson.strip()}")
|
|
803
|
+
|
|
804
|
+
elif args.command == "get-lessons":
|
|
805
|
+
lessons = query_lessons(args.context, limit=args.limit)
|
|
806
|
+
print("==================================================")
|
|
807
|
+
print(f"🧠 [EPISODIC REFLEXION MEMORY] Top {len(lessons)} Lessons for: '{args.context}'")
|
|
808
|
+
print("==================================================")
|
|
809
|
+
if not lessons:
|
|
810
|
+
print(" (Zero relevant lessons in cache yet. Proceeding with clean reasoning.)")
|
|
811
|
+
else:
|
|
812
|
+
for idx, l in enumerate(lessons, 1):
|
|
813
|
+
print(f"📌 [Lesson {idx}] Context: {l['page_context']} | Trigger: {l['error_signature']}")
|
|
814
|
+
print(f" 💡 Rule: {l['lesson_summary']}")
|
|
815
|
+
if l.get("failed_attempt") or l.get("successful_fix"):
|
|
816
|
+
print(f" ⚠️ Anti-Pattern: {l.get('failed_attempt') or 'N/A'}")
|
|
817
|
+
print(f" ✅ Proven Fix : {l.get('successful_fix') or 'N/A'}")
|
|
818
|
+
print("==================================================")
|
|
819
|
+
|
|
820
|
+
elif args.command == "stats":
|
|
821
|
+
stats = get_stats()
|
|
822
|
+
print("==================================================")
|
|
823
|
+
print(f"📊 AGENT MEMORY CACHE STATISTICS ({'LOCAL' if args.local else 'GLOBAL CENTRAL'})")
|
|
824
|
+
print(f"Database Path : {DB_FILE}")
|
|
825
|
+
print(f"Verified Solutions (✅) : {stats['total_solutions']}")
|
|
826
|
+
print(f"Failed Fixes Blacklist (🚫): {stats['total_failed_fixes']}")
|
|
827
|
+
print(f"Lessons Learned (🧠) : {stats.get('total_lessons_learned', 0)}")
|
|
828
|
+
print(f"Total Cache Hits : {stats['total_hits']}")
|
|
829
|
+
print(f"Estimated Tokens Saved : ~{stats['estimated_tokens_saved']:,} tokens")
|
|
830
|
+
print("--------------------------------------------------")
|
|
831
|
+
print("Top Solutions by Hit Count:")
|
|
832
|
+
for s in stats["solutions"]:
|
|
833
|
+
print(f" - [{s['hit_count']} hits] {s['error_signature']} ({s['module_name']})")
|
|
834
|
+
print("==================================================")
|
|
835
|
+
|
|
836
|
+
elif args.command == "sync":
|
|
837
|
+
count = sync_db()
|
|
838
|
+
print(f"✅ [MEMORY SYNC] Bidirectional sync completed ({count} solutions synchronized between Global & Local).")
|
|
839
|
+
|
|
840
|
+
elif args.command == "init" or args.command is None:
|
|
841
|
+
init_db(seed=True)
|
|
842
|
+
print(f"✅ Central Agent Memory initialized at: {DB_FILE}")
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
if __name__ == "__main__":
|
|
846
|
+
main()
|