synapse-hub 2.6.0 → 3.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 +4 -0
- package/.agents/agents/live-explorer.md +2 -0
- package/.agents/agents/orchestrator.md +16 -0
- package/.agents/agents/pom-crafter.md +10 -0
- package/.agents/agents/quality-gatekeeper.md +5 -0
- package/.agents/agents/test-runner.md +8 -0
- package/.agents/rules/git-push-protocol.md +1 -1
- package/.agents/rules/graphify.md +2 -0
- package/.agents/rules/mandatory-playwright-live.md +9 -9
- package/.agents/rules/mobile_appium_rules.md +8 -1
- package/.agents/skills/actor-qa-swarm/SKILL.md +77 -0
- package/.agents/skills/actor-qa-swarm/examples/broker_sharding_sample.json +12 -0
- package/.agents/skills/agentless-qa-repair/SKILL.md +69 -0
- package/.agents/skills/agentless-qa-repair/examples/healing_report_sample.json +17 -0
- package/.agents/skills/api-spec-to-e2e-matrix/SKILL.md +1 -1
- package/.agents/skills/clean-pom-crafter/SKILL.md +2 -2
- package/.agents/skills/db-state-verifier-and-seeder/SKILL.md +4 -4
- package/.agents/skills/event-stream-qa-orchestrator/SKILL.md +79 -0
- package/.agents/skills/event-stream-qa-orchestrator/examples/timeline_sample.jsonl +2 -0
- package/.agents/skills/failure-autopsy-and-healing/SKILL.md +2 -2
- package/.agents/skills/failure-autopsy-and-healing/scripts/triage_failures.sh +6 -21
- package/.agents/skills/katalon-cli-runner/SKILL.md +1 -1
- package/.agents/skills/lean-impact-scoreboard/SKILL.md +1 -1
- package/.agents/skills/mipro-prompt-compiler/SKILL.md +59 -0
- package/.agents/skills/mipro-prompt-compiler/examples/compiled_module_sample.json +22 -0
- package/.agents/skills/qa-aci-interface/SKILL.md +67 -0
- package/.agents/skills/qa-aci-interface/examples/surgical_edit_sample.json +12 -0
- package/.agents/skills/qa-memory-os/SKILL.md +80 -0
- package/.agents/skills/qa-memory-os/examples/archival_memory_sample.json +21 -0
- package/.agents/skills/repo-quality-audit/SKILL.md +1 -1
- package/.agents/skills/review-spec/SKILL.md +60 -0
- package/.agents/skills/review-spec/examples/sample_spec_review.md +33 -0
- package/.agents/skills/senior-code-review/SKILL.md +1 -1
- package/.agents/skills/synthetic-test-data-generator/SKILL.md +2 -2
- package/.agents/skills/technical-debt-harvester/SKILL.md +1 -1
- package/.agents/skills/tot-exploratory-testing/SKILL.md +59 -0
- package/.agents/skills/tot-exploratory-testing/examples/tot_graph_sample.json +35 -0
- package/.agents/skills/wcag-audit-patterns/SKILL.md +2 -2
- package/.agents/skills/web-agent-gym-benchmarking/SKILL.md +62 -0
- package/.agents/skills/web-agent-gym-benchmarking/examples/benchmark_report_sample.json +22 -0
- package/CLAUDE.md +2 -2
- package/README.en.md +70 -291
- package/README.md +68 -303
- package/docs/ARCHITECTURE.md +54 -8
- package/docs/CONTRIBUTING.md +1 -1
- package/docs/QUALITY_GATES.md +113 -0
- package/docs/research/EXTERNAL_AGENT_SKILLS_SURVEY.md +1062 -0
- package/package.json +1 -1
- package/scripts/package_dist.py +2 -1
- package/skills_manifest.json +92 -11
- package/synapse.config.json +1 -1
- package/templates/ai_bridges/CLAUDE.md +2 -2
- package/templates/ai_bridges/copilot-instructions.md +2 -2
- package/tools/TOOLS_RUNBOOK.md +1 -1
- package/tools/__init__.py +18 -1
- package/tools/_openapi_parser.py +158 -0
- package/tools/agent_canary.py +42 -30
- package/tools/agentless_patcher.py +417 -0
- package/tools/analyze_project.py +12 -11
- package/tools/api_drift_sentinel.py +4 -4
- package/tools/api_fuzzer.py +8 -8
- package/tools/auto_heal.py +23 -5
- package/tools/auto_heal_reflexion.py +2 -2
- package/tools/autogen_qa_broker.py +318 -0
- package/tools/browser_helpers.js +2 -2
- package/tools/browsergym_eval.py +236 -0
- package/tools/bug_reporter.py +11 -13
- package/tools/bump_version.py +75 -5
- package/tools/cache_resolver.py +78 -83
- package/tools/check_env.py +9 -1
- package/tools/check_version_sync.py +40 -3
- package/tools/ci_healing_hook.py +5 -7
- package/tools/cli.py +5 -7
- package/tools/config_decoupler.py +2 -3
- package/tools/core/__init__.py +52 -0
- package/tools/count_tokens.py +22 -20
- package/tools/dashboard_generator.py +628 -999
- package/tools/db_sandbox.py +69 -53
- package/tools/debt_harvester.py +3 -3
- package/tools/distill_log.py +3 -3
- package/tools/dspy_qa_signatures.py +3 -4
- package/tools/export_postman_collection.py +8 -8
- package/tools/flaky_detector.py +2 -2
- package/tools/fuzz_payloads.py +5 -5
- package/tools/generate_allure_report.py +1 -1
- package/tools/generate_jmx_plan.py +4 -4
- package/tools/gherkin_suite.py +1 -1
- package/tools/git_pre_commit_guard.py +2 -2
- package/tools/goldfish_review.py +2 -2
- package/tools/heal/__init__.py +28 -0
- package/tools/heal_pr_automator.py +29 -4
- package/tools/heuristic_locator_matcher.py +2 -2
- package/tools/infer_linter.py +8 -8
- package/tools/interview_story.py +0 -1
- package/tools/jtl_analyzer.py +11 -11
- package/tools/lint/__init__.py +30 -0
- package/tools/lint_ai_docs.py +42 -0
- package/tools/lookup_tc.py +1 -1
- package/tools/matrix_sharder.py +15 -13
- package/tools/mcp/__init__.py +16 -0
- package/tools/mcp_excel_server.py +2 -2
- package/tools/mcp_server.py +3 -4
- package/tools/md_to_xlsx.py +26 -26
- package/tools/micro_mutator.py +11 -12
- package/tools/mipro_assertion_tuner.py +207 -0
- package/tools/network_mock_shield.py +27 -27
- package/tools/newman_analyzer.py +2 -3
- package/tools/omni_emitter.py +5 -8
- package/tools/omni_parser.py +5 -5
- package/tools/omni_transpiler.py +5 -5
- package/tools/output_formatter.py +19 -0
- package/tools/perf/__init__.py +24 -0
- package/tools/pinpoint_failure.py +13 -1
- package/tools/project_state.py +4 -2
- package/tools/qa_event_stream.py +336 -0
- package/tools/repo_map.py +8 -8
- package/tools/report/__init__.py +32 -0
- package/tools/reversible_sandbox.py +5 -2
- package/tools/safety_firewall.py +2 -2
- package/tools/senior_code_review.py +2 -2
- package/tools/setup_project.py +10 -12
- package/tools/skill_collision_guard.py +3 -3
- package/tools/skill_vault.py +12 -3
- package/tools/skillspector.py +5 -7
- package/tools/smart_visual_masker.py +4 -6
- package/tools/spec_drift_sentinel.py +1 -2
- package/tools/swe_qa_aci.py +369 -0
- package/tools/synapse_qa_bench.py +3 -3
- package/tools/sync_agent_rules.py +18 -4
- package/tools/telemetry_correlator.py +3 -2
- package/tools/test_memory_os.py +419 -0
- package/tools/textgrad_mutation_optimizer.py +3 -3
- package/tools/tms_sync_bridge.py +5 -5
- package/tools/tot_exploratory_tester.py +239 -0
- package/tools/traffic_session_converter.py +9 -9
- package/tools/universal_ir.py +2 -3
- package/tools/update_sentinel.py +6 -9
- package/tools/verify_all.py +10 -0
- package/tools/visual_diff_canvas.py +1 -2
- package/tools/visual_som_helper.py +15 -13
- package/tools/watch_mode.py +1 -1
- package/tools/wcag_audit.py +22 -1
- package/tools/welcome_page.py +5 -6
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026 Thế Vị (@TeeVee998). All rights reserved.
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
# Project: Synapse-Hub (https://github.com/TeeVee998/Synapse-Hub)
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
Hierarchical QA Test Memory OS (Letta / MemGPT Pattern)
|
|
8
|
+
-------------------------------------------------------
|
|
9
|
+
Provides a 3-tier persistent memory operating system for test automation:
|
|
10
|
+
1. Core Memory: Fast key-value blocks for active environment context (AUT URL, auth token expiry, flags).
|
|
11
|
+
2. Recall Memory: Sequential event log of recent test execution steps and subagent actions.
|
|
12
|
+
3. Archival Memory: Long-term searchable knowledge base of locator mutations and flaky patterns,
|
|
13
|
+
version-scoped by AUT release (e.g. v2.1 vs v2.2) to eliminate memory rot.
|
|
14
|
+
4. Memory Hygiene: Garbage collection and secret scrubbing (zero plaintext tokens/passwords).
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
python tools/test_memory_os.py init --db-path .agent_memory/qa_memory.db
|
|
18
|
+
python tools/test_memory_os.py update-core --block auth_state --data '{"token_expiry": "..."}'
|
|
19
|
+
python tools/test_memory_os.py archival-insert --key LOCATOR_MUTATION --payload '{"page": "LoginPage", ...}' --aut-version "v2.1"
|
|
20
|
+
python tools/test_memory_os.py archival-search --query "Login submit button" --aut-version "v2.1" --json
|
|
21
|
+
python tools/test_memory_os.py gc --scrub-secrets --max-age-days 30
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
import sys
|
|
26
|
+
import re
|
|
27
|
+
import json
|
|
28
|
+
import sqlite3
|
|
29
|
+
import time
|
|
30
|
+
import uuid
|
|
31
|
+
import difflib
|
|
32
|
+
import argparse
|
|
33
|
+
from datetime import datetime, timezone, timedelta
|
|
34
|
+
from typing import Dict, Any, List, Optional, Tuple
|
|
35
|
+
|
|
36
|
+
# Enforce UTF-8 stdout encoding
|
|
37
|
+
if hasattr(sys.stdout, 'reconfigure'):
|
|
38
|
+
try:
|
|
39
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
40
|
+
sys.stderr.reconfigure(encoding='utf-8')
|
|
41
|
+
except Exception:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
SECRET_PATTERNS = [
|
|
46
|
+
re.compile(r'["\']?(?:password|passwd|pwd|secret|apiKey|api_key|token|bearer)["\']?\s*[:=]\s*["\']?([^"\'\s,;]{8,})["\']?', re.IGNORECASE),
|
|
47
|
+
re.compile(r'Bearer\s+([a-zA-Z0-9_\-\.]{15,})', re.IGNORECASE),
|
|
48
|
+
re.compile(r'-----BEGIN (?:RSA )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA )?PRIVATE KEY-----'),
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ManagedConnection(sqlite3.Connection):
|
|
53
|
+
"""SQLite connection that automatically commits/rollbacks and closes on context exit."""
|
|
54
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
55
|
+
try:
|
|
56
|
+
super().__exit__(exc_type, exc_val, exc_tb)
|
|
57
|
+
finally:
|
|
58
|
+
self.close()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TestMemoryOS:
|
|
62
|
+
"""3-Tier Hierarchical QA Memory OS."""
|
|
63
|
+
__test__ = False
|
|
64
|
+
|
|
65
|
+
def __init__(self, db_path: Optional[str] = None):
|
|
66
|
+
if db_path:
|
|
67
|
+
self.db_path = db_path
|
|
68
|
+
else:
|
|
69
|
+
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
70
|
+
self.db_path = os.path.join(repo_root, ".agent_memory", "qa_memory.db")
|
|
71
|
+
|
|
72
|
+
os.makedirs(os.path.dirname(os.path.abspath(self.db_path)), exist_ok=True)
|
|
73
|
+
self._init_db()
|
|
74
|
+
|
|
75
|
+
def _get_connection(self) -> ManagedConnection:
|
|
76
|
+
conn = sqlite3.connect(self.db_path, factory=ManagedConnection)
|
|
77
|
+
conn.row_factory = sqlite3.Row
|
|
78
|
+
return conn
|
|
79
|
+
|
|
80
|
+
def _init_db(self):
|
|
81
|
+
with self._get_connection() as conn:
|
|
82
|
+
# 1. Core Memory Table
|
|
83
|
+
conn.execute("""
|
|
84
|
+
CREATE TABLE IF NOT EXISTS core_memory (
|
|
85
|
+
block_key TEXT PRIMARY KEY,
|
|
86
|
+
data_json TEXT NOT NULL,
|
|
87
|
+
updated_at TEXT NOT NULL
|
|
88
|
+
)
|
|
89
|
+
""")
|
|
90
|
+
# 2. Recall Memory Table
|
|
91
|
+
conn.execute("""
|
|
92
|
+
CREATE TABLE IF NOT EXISTS recall_memory (
|
|
93
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
94
|
+
session_id TEXT NOT NULL,
|
|
95
|
+
event_type TEXT NOT NULL,
|
|
96
|
+
actor TEXT NOT NULL,
|
|
97
|
+
payload_json TEXT NOT NULL,
|
|
98
|
+
timestamp TEXT NOT NULL
|
|
99
|
+
)
|
|
100
|
+
""")
|
|
101
|
+
conn.execute("CREATE INDEX IF NOT EXISTS idx_recall_session ON recall_memory(session_id, timestamp)")
|
|
102
|
+
|
|
103
|
+
# 3. Archival Memory Table
|
|
104
|
+
conn.execute("""
|
|
105
|
+
CREATE TABLE IF NOT EXISTS archival_memory (
|
|
106
|
+
id TEXT PRIMARY KEY,
|
|
107
|
+
key TEXT NOT NULL,
|
|
108
|
+
aut_version TEXT DEFAULT 'latest',
|
|
109
|
+
payload_json TEXT NOT NULL,
|
|
110
|
+
tags TEXT DEFAULT '',
|
|
111
|
+
text_content TEXT NOT NULL,
|
|
112
|
+
created_at TEXT NOT NULL
|
|
113
|
+
)
|
|
114
|
+
""")
|
|
115
|
+
conn.execute("CREATE INDEX IF NOT EXISTS idx_archival_key_ver ON archival_memory(key, aut_version)")
|
|
116
|
+
|
|
117
|
+
# ----------------- Tier 1: Core Memory -----------------
|
|
118
|
+
|
|
119
|
+
def update_core(self, block_key: str, data: Any) -> Dict[str, Any]:
|
|
120
|
+
"""Updates or inserts a core memory block."""
|
|
121
|
+
data_str = json.dumps(data) if not isinstance(data, str) else data
|
|
122
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
123
|
+
with self._get_connection() as conn:
|
|
124
|
+
conn.execute("""
|
|
125
|
+
INSERT INTO core_memory (block_key, data_json, updated_at)
|
|
126
|
+
VALUES (?, ?, ?)
|
|
127
|
+
ON CONFLICT(block_key) DO UPDATE SET
|
|
128
|
+
data_json=excluded.data_json,
|
|
129
|
+
updated_at=excluded.updated_at
|
|
130
|
+
""", (block_key, data_str, now))
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
"status": "PASS",
|
|
134
|
+
"block_key": block_key,
|
|
135
|
+
"updated_at": now
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
def get_core(self, block_key: str) -> Optional[Dict[str, Any]]:
|
|
139
|
+
"""Retrieves a core memory block."""
|
|
140
|
+
with self._get_connection() as conn:
|
|
141
|
+
row = conn.execute("SELECT data_json, updated_at FROM core_memory WHERE block_key = ?", (block_key,)).fetchone()
|
|
142
|
+
if row:
|
|
143
|
+
try:
|
|
144
|
+
parsed = json.loads(row["data_json"])
|
|
145
|
+
except Exception:
|
|
146
|
+
parsed = row["data_json"]
|
|
147
|
+
return {
|
|
148
|
+
"block_key": block_key,
|
|
149
|
+
"data": parsed,
|
|
150
|
+
"updated_at": row["updated_at"]
|
|
151
|
+
}
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
def list_core(self) -> List[Dict[str, Any]]:
|
|
155
|
+
"""Lists all core memory blocks."""
|
|
156
|
+
with self._get_connection() as conn:
|
|
157
|
+
rows = conn.execute("SELECT block_key, data_json, updated_at FROM core_memory ORDER BY block_key").fetchall()
|
|
158
|
+
results = []
|
|
159
|
+
for r in rows:
|
|
160
|
+
try:
|
|
161
|
+
parsed = json.loads(r["data_json"])
|
|
162
|
+
except Exception:
|
|
163
|
+
parsed = r["data_json"]
|
|
164
|
+
results.append({"block_key": r["block_key"], "data": parsed, "updated_at": r["updated_at"]})
|
|
165
|
+
return results
|
|
166
|
+
|
|
167
|
+
# ----------------- Tier 2: Recall Memory -----------------
|
|
168
|
+
|
|
169
|
+
def recall_insert(self, session_id: str, event_type: str, actor: str, payload: Any) -> int:
|
|
170
|
+
"""Inserts an event into recall memory log."""
|
|
171
|
+
payload_str = json.dumps(payload) if not isinstance(payload, str) else payload
|
|
172
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
173
|
+
with self._get_connection() as conn:
|
|
174
|
+
cursor = conn.execute("""
|
|
175
|
+
INSERT INTO recall_memory (session_id, event_type, actor, payload_json, timestamp)
|
|
176
|
+
VALUES (?, ?, ?, ?, ?)
|
|
177
|
+
""", (session_id, event_type, actor, payload_str, now))
|
|
178
|
+
return cursor.lastrowid or 0
|
|
179
|
+
|
|
180
|
+
def recall_log(self, session_id: str, limit: int = 50) -> List[Dict[str, Any]]:
|
|
181
|
+
"""Retrieves recent sequential steps for a session."""
|
|
182
|
+
with self._get_connection() as conn:
|
|
183
|
+
rows = conn.execute("""
|
|
184
|
+
SELECT id, session_id, event_type, actor, payload_json, timestamp
|
|
185
|
+
FROM recall_memory
|
|
186
|
+
WHERE session_id = ?
|
|
187
|
+
ORDER BY id ASC
|
|
188
|
+
LIMIT ?
|
|
189
|
+
""", (session_id, limit)).fetchall()
|
|
190
|
+
events = []
|
|
191
|
+
for r in rows:
|
|
192
|
+
try:
|
|
193
|
+
payload = json.loads(r["payload_json"])
|
|
194
|
+
except Exception:
|
|
195
|
+
payload = r["payload_json"]
|
|
196
|
+
events.append({
|
|
197
|
+
"id": r["id"],
|
|
198
|
+
"session_id": r["session_id"],
|
|
199
|
+
"event_type": r["event_type"],
|
|
200
|
+
"actor": r["actor"],
|
|
201
|
+
"payload": payload,
|
|
202
|
+
"timestamp": r["timestamp"]
|
|
203
|
+
})
|
|
204
|
+
return events
|
|
205
|
+
|
|
206
|
+
# ----------------- Tier 3: Archival Memory -----------------
|
|
207
|
+
|
|
208
|
+
def archival_insert(self, key: str, payload: Any, aut_version: str = "latest", tags: str = "") -> str:
|
|
209
|
+
"""Inserts long-term searchable knowledge into archival memory."""
|
|
210
|
+
payload_str = json.dumps(payload) if not isinstance(payload, str) else payload
|
|
211
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
212
|
+
mem_id = f"mem_arch_{int(time.time() * 1000) % 10000000}_{uuid.uuid4().hex[:6]}"
|
|
213
|
+
|
|
214
|
+
# Flatten text content for semantic/fuzzy search
|
|
215
|
+
text_content = f"{key} {aut_version} {tags} {payload_str}".lower()
|
|
216
|
+
|
|
217
|
+
with self._get_connection() as conn:
|
|
218
|
+
conn.execute("""
|
|
219
|
+
INSERT INTO archival_memory (id, key, aut_version, payload_json, tags, text_content, created_at)
|
|
220
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
221
|
+
""", (mem_id, key, aut_version, payload_str, tags, text_content, now))
|
|
222
|
+
|
|
223
|
+
return mem_id
|
|
224
|
+
|
|
225
|
+
def archival_search(self, query: str, aut_version: Optional[str] = None, top_k: int = 3) -> Dict[str, Any]:
|
|
226
|
+
"""Searches archival memory with semantic token matching and fuzzy ratio."""
|
|
227
|
+
query_lower = query.lower()
|
|
228
|
+
tokens = set(re.findall(r'\b[a-z0-9_]{3,}\b', query_lower))
|
|
229
|
+
|
|
230
|
+
with self._get_connection() as conn:
|
|
231
|
+
if aut_version:
|
|
232
|
+
rows = conn.execute(
|
|
233
|
+
"SELECT id, key, aut_version, payload_json, tags, text_content, created_at FROM archival_memory WHERE aut_version = ?",
|
|
234
|
+
(aut_version,)
|
|
235
|
+
).fetchall()
|
|
236
|
+
else:
|
|
237
|
+
rows = conn.execute(
|
|
238
|
+
"SELECT id, key, aut_version, payload_json, tags, text_content, created_at FROM archival_memory"
|
|
239
|
+
).fetchall()
|
|
240
|
+
|
|
241
|
+
scored = []
|
|
242
|
+
for r in rows:
|
|
243
|
+
text = r["text_content"]
|
|
244
|
+
# 1. Token overlap score
|
|
245
|
+
text_tokens = set(re.findall(r'\b[a-z0-9_]{3,}\b', text))
|
|
246
|
+
if tokens:
|
|
247
|
+
token_score = len(tokens.intersection(text_tokens)) / len(tokens)
|
|
248
|
+
else:
|
|
249
|
+
token_score = 0.0
|
|
250
|
+
|
|
251
|
+
# 2. Fuzzy sequence similarity
|
|
252
|
+
fuzzy_ratio = difflib.SequenceMatcher(None, query_lower, text[:200]).ratio()
|
|
253
|
+
|
|
254
|
+
# Combined score: 60% token overlap + 40% fuzzy match
|
|
255
|
+
score = round((0.6 * token_score) + (0.4 * fuzzy_ratio), 3)
|
|
256
|
+
|
|
257
|
+
if score > 0.15 or (tokens and tokens.intersection(text_tokens)):
|
|
258
|
+
try:
|
|
259
|
+
payload = json.loads(r["payload_json"])
|
|
260
|
+
except Exception:
|
|
261
|
+
payload = r["payload_json"]
|
|
262
|
+
|
|
263
|
+
scored.append({
|
|
264
|
+
"id": r["id"],
|
|
265
|
+
"similarity_score": score,
|
|
266
|
+
"key": r["key"],
|
|
267
|
+
"aut_version": r["aut_version"],
|
|
268
|
+
"timestamp": r["created_at"],
|
|
269
|
+
"payload": payload
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
scored.sort(key=lambda x: x["similarity_score"], reverse=True)
|
|
273
|
+
results = scored[:top_k]
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
"status": "PASS",
|
|
277
|
+
"query": query,
|
|
278
|
+
"aut_version": aut_version or "all",
|
|
279
|
+
"matches_found": len(results),
|
|
280
|
+
"results": results
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
# ----------------- Hygiene & Secret Scrubbing -----------------
|
|
284
|
+
|
|
285
|
+
def scrub_string(self, text: str) -> Tuple[str, int]:
|
|
286
|
+
"""Masks detected secrets in text."""
|
|
287
|
+
scrubbed = text
|
|
288
|
+
count = 0
|
|
289
|
+
for pat in SECRET_PATTERNS:
|
|
290
|
+
matches = list(pat.finditer(scrubbed))
|
|
291
|
+
if matches:
|
|
292
|
+
count += len(matches)
|
|
293
|
+
for m in matches:
|
|
294
|
+
val = m.group(0)
|
|
295
|
+
scrubbed = scrubbed.replace(val, "[MASKED_SECRET]")
|
|
296
|
+
return scrubbed, count
|
|
297
|
+
|
|
298
|
+
def gc(self, scrub_secrets: bool = True, max_age_days: int = 30) -> Dict[str, Any]:
|
|
299
|
+
"""Performs garbage collection and secret scrubbing."""
|
|
300
|
+
scrub_count = 0
|
|
301
|
+
deleted_recall = 0
|
|
302
|
+
|
|
303
|
+
with self._get_connection() as conn:
|
|
304
|
+
# 1. Clean old recall logs
|
|
305
|
+
cutoff = (datetime.now(timezone.utc) - timedelta(days=max_age_days)).isoformat()
|
|
306
|
+
cur = conn.execute("DELETE FROM recall_memory WHERE timestamp < ?", (cutoff,))
|
|
307
|
+
deleted_recall = cur.rowcount
|
|
308
|
+
|
|
309
|
+
# 2. Scrub secrets in core memory
|
|
310
|
+
if scrub_secrets:
|
|
311
|
+
rows = conn.execute("SELECT block_key, data_json FROM core_memory").fetchall()
|
|
312
|
+
for r in rows:
|
|
313
|
+
new_val, c = self.scrub_string(r["data_json"])
|
|
314
|
+
if c > 0:
|
|
315
|
+
scrub_count += c
|
|
316
|
+
conn.execute("UPDATE core_memory SET data_json = ? WHERE block_key = ?", (new_val, r["block_key"]))
|
|
317
|
+
|
|
318
|
+
# Scrub archival memory
|
|
319
|
+
a_rows = conn.execute("SELECT id, payload_json, text_content FROM archival_memory").fetchall()
|
|
320
|
+
for ar in a_rows:
|
|
321
|
+
new_p, cp = self.scrub_string(ar["payload_json"])
|
|
322
|
+
new_t, ct = self.scrub_string(ar["text_content"])
|
|
323
|
+
if cp > 0 or ct > 0:
|
|
324
|
+
scrub_count += (cp + ct)
|
|
325
|
+
conn.execute("UPDATE archival_memory SET payload_json = ?, text_content = ? WHERE id = ?", (new_p, new_t, ar["id"]))
|
|
326
|
+
|
|
327
|
+
# Database file size check
|
|
328
|
+
db_size_mb = 0.0
|
|
329
|
+
if os.path.exists(self.db_path):
|
|
330
|
+
db_size_mb = round(os.path.getsize(self.db_path) / (1024 * 1024), 2)
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
"status": "PASS",
|
|
334
|
+
"db_path": self.db_path,
|
|
335
|
+
"db_size_mb": db_size_mb,
|
|
336
|
+
"max_size_limit_mb": 50.0,
|
|
337
|
+
"size_status": "PASS" if db_size_mb <= 50.0 else "WARNING_OVERSIZED",
|
|
338
|
+
"secrets_scrubbed": scrub_count,
|
|
339
|
+
"expired_recall_records_deleted": deleted_recall
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def main():
|
|
344
|
+
parser = argparse.ArgumentParser(description="Hierarchical QA Test Memory OS (Letta Pattern)")
|
|
345
|
+
parser.add_argument("--db-path", help="Path to SQLite database file")
|
|
346
|
+
subparsers = parser.add_subparsers(dest="subcommand", help="Subcommand to execute")
|
|
347
|
+
|
|
348
|
+
# init
|
|
349
|
+
subparsers.add_parser("init", help="Initialize memory OS database")
|
|
350
|
+
|
|
351
|
+
# update-core
|
|
352
|
+
uc_parser = subparsers.add_parser("update-core", help="Update core memory block")
|
|
353
|
+
uc_parser.add_argument("--block", required=True, help="Core memory block key")
|
|
354
|
+
uc_parser.add_argument("--data", required=True, help="JSON or text payload")
|
|
355
|
+
|
|
356
|
+
# get-core
|
|
357
|
+
gc_parser = subparsers.add_parser("get-core", help="Retrieve core memory block")
|
|
358
|
+
gc_parser.add_argument("--block", required=True, help="Core memory block key")
|
|
359
|
+
|
|
360
|
+
# archival-insert
|
|
361
|
+
ai_parser = subparsers.add_parser("archival-insert", help="Insert long-term knowledge into Archival Memory")
|
|
362
|
+
ai_parser.add_argument("--key", required=True, help="Knowledge key (e.g. LOCATOR_MUTATION)")
|
|
363
|
+
ai_parser.add_argument("--payload", required=True, help="JSON data payload")
|
|
364
|
+
ai_parser.add_argument("--aut-version", default="latest", help="AUT release version (e.g. v2.1)")
|
|
365
|
+
ai_parser.add_argument("--tags", default="", help="Comma-separated search tags")
|
|
366
|
+
|
|
367
|
+
# archival-search
|
|
368
|
+
as_parser = subparsers.add_parser("archival-search", help="Search archival memory by query")
|
|
369
|
+
as_parser.add_argument("--query", required=True, help="Search query string")
|
|
370
|
+
as_parser.add_argument("--aut-version", help="Filter by AUT release version")
|
|
371
|
+
as_parser.add_argument("--top-k", type=int, default=3, help="Max results to return")
|
|
372
|
+
as_parser.add_argument("--json", action="store_true", help="Print JSON output")
|
|
373
|
+
|
|
374
|
+
# gc
|
|
375
|
+
clean_parser = subparsers.add_parser("gc", help="Garbage collection and secret scrubbing")
|
|
376
|
+
clean_parser.add_argument("--scrub-secrets", action="store_true", default=True, help="Scrub sensitive credentials")
|
|
377
|
+
clean_parser.add_argument("--max-age-days", type=int, default=30, help="Max age for recall logs")
|
|
378
|
+
|
|
379
|
+
args = parser.parse_args()
|
|
380
|
+
mem = TestMemoryOS(db_path=args.db_path)
|
|
381
|
+
|
|
382
|
+
if args.subcommand == "init":
|
|
383
|
+
print(json.dumps({"status": "PASS", "db_path": mem.db_path, "initialized": True}, indent=2))
|
|
384
|
+
|
|
385
|
+
elif args.subcommand == "update-core":
|
|
386
|
+
try:
|
|
387
|
+
parsed_data = json.loads(args.data)
|
|
388
|
+
except Exception:
|
|
389
|
+
parsed_data = args.data
|
|
390
|
+
res = mem.update_core(args.block, parsed_data)
|
|
391
|
+
print(json.dumps(res, indent=2))
|
|
392
|
+
|
|
393
|
+
elif args.subcommand == "get-core":
|
|
394
|
+
res = mem.get_core(args.block)
|
|
395
|
+
print(json.dumps(res or {"status": "NOT_FOUND", "block": args.block}, indent=2))
|
|
396
|
+
|
|
397
|
+
elif args.subcommand == "archival-insert":
|
|
398
|
+
try:
|
|
399
|
+
parsed_payload = json.loads(args.payload)
|
|
400
|
+
except Exception:
|
|
401
|
+
parsed_payload = args.payload
|
|
402
|
+
mid = mem.archival_insert(args.key, parsed_payload, aut_version=args.aut_version, tags=args.tags)
|
|
403
|
+
print(json.dumps({"status": "PASS", "id": mid, "key": args.key, "aut_version": args.aut_version}, indent=2))
|
|
404
|
+
|
|
405
|
+
elif args.subcommand == "archival-search":
|
|
406
|
+
res = mem.archival_search(args.query, aut_version=args.aut_version, top_k=args.top_k)
|
|
407
|
+
print(json.dumps(res, indent=2))
|
|
408
|
+
|
|
409
|
+
elif args.subcommand == "gc":
|
|
410
|
+
res = mem.gc(scrub_secrets=args.scrub_secrets, max_age_days=args.max_age_days)
|
|
411
|
+
print(json.dumps(res, indent=2))
|
|
412
|
+
|
|
413
|
+
else:
|
|
414
|
+
parser.print_help()
|
|
415
|
+
sys.exit(1)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
if __name__ == "__main__":
|
|
419
|
+
main()
|
|
@@ -20,7 +20,7 @@ import sys
|
|
|
20
20
|
import re
|
|
21
21
|
import json
|
|
22
22
|
import argparse
|
|
23
|
-
from typing import List, Dict, Any, Tuple
|
|
23
|
+
from typing import List, Dict, Any, Tuple
|
|
24
24
|
|
|
25
25
|
# Enforce UTF-8 stdout encoding for Windows console compatibility
|
|
26
26
|
if hasattr(sys.stdout, 'reconfigure'):
|
|
@@ -33,10 +33,10 @@ if hasattr(sys.stdout, 'reconfigure'):
|
|
|
33
33
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
34
34
|
sys.path.insert(0, SCRIPT_DIR)
|
|
35
35
|
|
|
36
|
-
import micro_mutator
|
|
36
|
+
import micro_mutator # noqa: E402
|
|
37
37
|
|
|
38
38
|
try:
|
|
39
|
-
import textgrad
|
|
39
|
+
import textgrad # noqa: F401, E402
|
|
40
40
|
TEXTGRAD_AVAILABLE = True
|
|
41
41
|
except ImportError:
|
|
42
42
|
TEXTGRAD_AVAILABLE = False
|
package/tools/tms_sync_bridge.py
CHANGED
|
@@ -78,7 +78,7 @@ class TMSBridge:
|
|
|
78
78
|
"issue_type": "Story",
|
|
79
79
|
"status": "Ready for QA",
|
|
80
80
|
"assignee": self.user_email,
|
|
81
|
-
"description":
|
|
81
|
+
"description": "As an Enterprise Accountant, I want to reconcile invoices matching ERP records to ensure financial accuracy.",
|
|
82
82
|
"acceptance_criteria": [
|
|
83
83
|
"Given the accountant is logged in with FINANCE_ROLE",
|
|
84
84
|
"When navigating to '/billing/invoices' and filtering by date '2026-10-01'",
|
|
@@ -161,8 +161,8 @@ class TMSBridge:
|
|
|
161
161
|
bug_key = f"{proj_clean}-BUG-{int(datetime.now().timestamp()) % 10000}"
|
|
162
162
|
|
|
163
163
|
repro = reproduction_steps or [
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
"1. Launch headless browser session via Playwright Live.",
|
|
165
|
+
"2. Navigate to target application flow under test.",
|
|
166
166
|
f"3. Execute test action sequence for '{test_case_id}'.",
|
|
167
167
|
f"4. Observe failure: {error_message}"
|
|
168
168
|
]
|
|
@@ -299,9 +299,9 @@ def main():
|
|
|
299
299
|
if args.json:
|
|
300
300
|
print(json.dumps(story, indent=2))
|
|
301
301
|
else:
|
|
302
|
-
print(
|
|
302
|
+
print("\n======================================================================")
|
|
303
303
|
print(f" ENTERPRISE ALM STORY PULL: [{story.get('issue_key')}]")
|
|
304
|
-
print(
|
|
304
|
+
print("======================================================================")
|
|
305
305
|
print(f" Summary : {story.get('summary')}")
|
|
306
306
|
print(f" Status : {story.get('status')} | Assignee: {story.get('assignee')}")
|
|
307
307
|
print(f" Source : {story.get('source')}\n")
|