@caddis/cli 0.4.13 → 0.4.15
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/bundles/antigravity-plugin/caddis_statusline.py +30 -3
- package/bundles/antigravity-plugin/plugin.json +1 -1
- package/bundles/antigravity-plugin/repo_freshness.py +146 -0
- package/bundles/antigravity-plugin/scripts/caddis-run.ps1 +11 -0
- package/bundles/antigravity-plugin/scripts/caddis-run.sh +9 -0
- package/bundles/antigravity-plugin/scripts/caddis_gate.py +24 -3
- package/bundles/antigravity-plugin/scripts/caddis_governor.py +542 -0
- package/bundles/antigravity-plugin/scripts/caddis_host_guard.py +689 -0
- package/bundles/antigravity-plugin/scripts/caddis_lanes.py +1632 -0
- package/bundles/antigravity-plugin/scripts/caddis_statusline.py +30 -3
- package/bundles/antigravity-plugin/scripts/install-host-guard.ps1 +393 -0
- package/bundles/antigravity-plugin/skills/_registry.md +2 -0
- package/bundles/antigravity-plugin/skills/confluence-hla/SKILL.md +375 -0
- package/bundles/antigravity-plugin/skills/confluence-hla/scripts/convert_hla_to_confluence.py +1164 -0
- package/bundles/antigravity-plugin/skills/confluence-hla/scripts/discover_db_lineage.py +970 -0
- package/bundles/antigravity-plugin/skills/feature-plan/SKILL.md +30 -4
- package/bundles/antigravity-plugin/skills/implement/SKILL.md +48 -1
- package/bundles/antigravity-plugin/skills/pr-merge/SKILL.md +155 -0
- package/bundles/antigravity-plugin/warm_start_agy.py +38 -4
- package/bundles/codex/.codex/skills/docs/confluence-hla/SKILL.md +375 -0
- package/bundles/codex/.codex/skills/docs/confluence-hla/scripts/convert_hla_to_confluence.py +1164 -0
- package/bundles/codex/.codex/skills/docs/confluence-hla/scripts/discover_db_lineage.py +970 -0
- package/bundles/manifest.json +2 -2
- package/dist/claude-deepseek.js +50 -10
- package/dist/claude-glm.js +50 -10
- package/dist/claude-oss.js +50 -10
- package/dist/cli.js +173 -63
- package/package.json +1 -1
|
@@ -0,0 +1,970 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Database Lineage Discovery Tool for Fleet Applications.
|
|
3
|
+
|
|
4
|
+
Discovers, verifies, and maps the end-to-end database lineage across:
|
|
5
|
+
Host[Server] -> Database[DB] -> Schema.Table[Table/View]
|
|
6
|
+
|
|
7
|
+
Active Discovery Protocol:
|
|
8
|
+
1. Inspects configuration files (config/.env*, .env, settings.py).
|
|
9
|
+
- Priority 1: User credentials found in config (default user 'LinkedUser').
|
|
10
|
+
- Priority 2: Windows Integrated Authentication (Trusted_Connection=yes).
|
|
11
|
+
2. Queries live database catalogs:
|
|
12
|
+
- sys.servers (Linked Servers)
|
|
13
|
+
- INFORMATION_SCHEMA.TABLES / sys.tables (Active tables and schemas)
|
|
14
|
+
- INFORMATION_SCHEMA.ROUTINES / sys.procedures (Active Stored Procedures)
|
|
15
|
+
- INFORMATION_SCHEMA.VIEWS (Views and joined dependencies)
|
|
16
|
+
3. Analyzes application code (ORM models, background workers, ETL scripts).
|
|
17
|
+
4. Emits a structured 7-stage lineage table and horizontal stacked diagrams
|
|
18
|
+
(Mermaid for Markdown, PlantUML for Confluence XHTML).
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
python scripts/discover_db_lineage.py [--target-dir <path>] [--output <file>] [--format md|html|json]
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import re
|
|
28
|
+
import sys
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Any, Dict, List, Optional
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def find_env_files(target_dir: Path) -> List[Path]:
|
|
34
|
+
"""Locate all .env files in config/ and root of the target directory."""
|
|
35
|
+
candidates = []
|
|
36
|
+
config_dir = target_dir / "config"
|
|
37
|
+
if config_dir.is_dir():
|
|
38
|
+
for p in config_dir.glob(".env*"):
|
|
39
|
+
if p.is_file():
|
|
40
|
+
candidates.append(p)
|
|
41
|
+
for p in target_dir.glob(".env*"):
|
|
42
|
+
if p.is_file() and p not in candidates:
|
|
43
|
+
candidates.append(p)
|
|
44
|
+
return candidates
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def parse_env_file(path: Path) -> Dict[str, str]:
|
|
48
|
+
"""Extract key-value pairs from a .env file, supporting UTF-8, UTF-16, and CP1252."""
|
|
49
|
+
kv = {}
|
|
50
|
+
try:
|
|
51
|
+
raw = path.read_bytes()
|
|
52
|
+
text = None
|
|
53
|
+
for enc in ["utf-8-sig", "utf-8", "utf-16", "utf-16-le", "cp1252"]:
|
|
54
|
+
try:
|
|
55
|
+
decoded = raw.decode(enc)
|
|
56
|
+
if "\x00" not in decoded:
|
|
57
|
+
text = decoded
|
|
58
|
+
break
|
|
59
|
+
except Exception:
|
|
60
|
+
continue
|
|
61
|
+
if text is None:
|
|
62
|
+
text = raw.decode("utf-8", errors="replace").replace("\x00", "")
|
|
63
|
+
|
|
64
|
+
for line in text.splitlines():
|
|
65
|
+
line = line.strip()
|
|
66
|
+
if not line or line.startswith("#"):
|
|
67
|
+
continue
|
|
68
|
+
if "=" in line:
|
|
69
|
+
k, v = line.split("=", 1)
|
|
70
|
+
kv[k.strip()] = v.strip().strip('"').strip("'")
|
|
71
|
+
except Exception as e:
|
|
72
|
+
print(f"Warning: could not read {path}: {e}", file=sys.stderr)
|
|
73
|
+
return kv
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def extract_database_targets_from_env(cfg: Dict[str, str], target_name: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
77
|
+
"""Discover all database targets declared in an environment config."""
|
|
78
|
+
targets: List[Dict[str, Any]] = []
|
|
79
|
+
|
|
80
|
+
# 1. Standard DB_HOST & DB_NAME
|
|
81
|
+
if cfg.get("DB_HOST") and cfg.get("DB_NAME"):
|
|
82
|
+
targets.append({
|
|
83
|
+
"host": cfg["DB_HOST"],
|
|
84
|
+
"db": cfg["DB_NAME"],
|
|
85
|
+
"user": cfg.get("DB_USER", "LinkedUser"),
|
|
86
|
+
"password": cfg.get("DB_PASSWORD"),
|
|
87
|
+
"driver": cfg.get("DB_DRIVER", "ODBC Driver 18 for SQL Server"),
|
|
88
|
+
"trust_cert": cfg.get("DB_TRUST_SERVER_CERTIFICATE", "yes"),
|
|
89
|
+
"port": cfg.get("DB_PORT"),
|
|
90
|
+
"label": "Primary Database",
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
# 2. DERBY_SERVER & DERBY_DATABASE
|
|
94
|
+
if cfg.get("DERBY_SERVER") and cfg.get("DERBY_DATABASE"):
|
|
95
|
+
targets.append({
|
|
96
|
+
"host": cfg["DERBY_SERVER"],
|
|
97
|
+
"db": cfg["DERBY_DATABASE"],
|
|
98
|
+
"user": cfg.get("DERBY_USER", "LinkedUser"),
|
|
99
|
+
"password": cfg.get("DERBY_PASSWORD"),
|
|
100
|
+
"driver": cfg.get("DERBY_DRIVER", "ODBC Driver 18 for SQL Server"),
|
|
101
|
+
"trust_cert": cfg.get("DERBY_TRUST_SERVER_CERTIFICATE", "yes"),
|
|
102
|
+
"port": cfg.get("DERBY_PORT"),
|
|
103
|
+
"label": "Derby / Telemetry Database",
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
# 3. Dynamic Prefix discovery: e.g. EXTERNAL_DB__SERVER, SERVSIGHT_DB__SERVER, ITDEV_DB__SERVER, CERILLION_DB__SERVER
|
|
107
|
+
server_keys = [k for k in cfg.keys() if k.endswith("_SERVER") or k.endswith("__SERVER")]
|
|
108
|
+
for s_key in server_keys:
|
|
109
|
+
if s_key.endswith("__SERVER"):
|
|
110
|
+
prefix = s_key[:-8]
|
|
111
|
+
db_key = f"{prefix}__DATABASE"
|
|
112
|
+
user_key = f"{prefix}__USERNAME"
|
|
113
|
+
pwd_key = f"{prefix}__PASSWORD"
|
|
114
|
+
driver_key = f"{prefix}__DRIVER"
|
|
115
|
+
else:
|
|
116
|
+
prefix = s_key[:-7]
|
|
117
|
+
db_key = f"{prefix}_DATABASE"
|
|
118
|
+
user_key = f"{prefix}_USER"
|
|
119
|
+
pwd_key = f"{prefix}_PASSWORD"
|
|
120
|
+
driver_key = f"{prefix}_DRIVER"
|
|
121
|
+
|
|
122
|
+
host = cfg.get(s_key)
|
|
123
|
+
db = cfg.get(db_key)
|
|
124
|
+
if host and db:
|
|
125
|
+
if target_name and target_name.lower() in ("customer-insights", "customer-service"):
|
|
126
|
+
if db.lower() == "customer_feedback_jit" or prefix.upper() in ("EXTERNAL_DB", "EXTERNAL"):
|
|
127
|
+
continue
|
|
128
|
+
targets.append({
|
|
129
|
+
"host": host,
|
|
130
|
+
"db": db,
|
|
131
|
+
"user": cfg.get(user_key, "LinkedUser"),
|
|
132
|
+
"password": cfg.get(pwd_key),
|
|
133
|
+
"driver": cfg.get(driver_key, "ODBC Driver 18 for SQL Server"),
|
|
134
|
+
"trust_cert": "yes",
|
|
135
|
+
"port": None,
|
|
136
|
+
"label": prefix.replace("__", " ").replace("_", " ").title(),
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
return targets
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def inspect_database_metadata(
|
|
143
|
+
host: str,
|
|
144
|
+
db: str,
|
|
145
|
+
user: Optional[str] = None,
|
|
146
|
+
password: Optional[str] = None,
|
|
147
|
+
driver: str = "ODBC Driver 18 for SQL Server",
|
|
148
|
+
trust_cert: str = "yes",
|
|
149
|
+
port: Optional[str] = None,
|
|
150
|
+
) -> Dict[str, Any]:
|
|
151
|
+
"""Query live database metadata using pyodbc with LinkedUser or Windows Auth fallback."""
|
|
152
|
+
results: Dict[str, Any] = {
|
|
153
|
+
"connected": False,
|
|
154
|
+
"auth_method": None,
|
|
155
|
+
"tables": [],
|
|
156
|
+
"procedures": [],
|
|
157
|
+
"linked_servers": [],
|
|
158
|
+
"views": [],
|
|
159
|
+
"error": None,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
import pyodbc
|
|
164
|
+
except ImportError:
|
|
165
|
+
results["error"] = "pyodbc not installed in current Python environment"
|
|
166
|
+
return results
|
|
167
|
+
|
|
168
|
+
if not driver.startswith("{"):
|
|
169
|
+
driver_str = f"{{{driver}}}"
|
|
170
|
+
else:
|
|
171
|
+
driver_str = driver
|
|
172
|
+
|
|
173
|
+
server_spec = f"{host},{port}" if port and port != "1433" else host
|
|
174
|
+
|
|
175
|
+
# Connection attempts: 1. User/password (e.g. LinkedUser), 2. Windows Auth
|
|
176
|
+
conn_attempts = []
|
|
177
|
+
if user and password:
|
|
178
|
+
conn_attempts.append((
|
|
179
|
+
f"DRIVER={driver_str};SERVER={server_spec};DATABASE={db};UID={user};PWD={password};TrustServerCertificate={trust_cert};LoginTimeout=4;",
|
|
180
|
+
f"SQL User ({user})",
|
|
181
|
+
))
|
|
182
|
+
|
|
183
|
+
conn_attempts.append((
|
|
184
|
+
f"DRIVER={driver_str};SERVER={server_spec};DATABASE={db};Trusted_Connection=yes;TrustServerCertificate={trust_cert};LoginTimeout=4;",
|
|
185
|
+
"Windows Integrated Authentication",
|
|
186
|
+
))
|
|
187
|
+
|
|
188
|
+
conn = None
|
|
189
|
+
auth_used = None
|
|
190
|
+
for conn_str, auth_label in conn_attempts:
|
|
191
|
+
try:
|
|
192
|
+
conn = pyodbc.connect(conn_str)
|
|
193
|
+
auth_used = auth_label
|
|
194
|
+
break
|
|
195
|
+
except Exception as exc:
|
|
196
|
+
results["error"] = str(exc)
|
|
197
|
+
|
|
198
|
+
if not conn:
|
|
199
|
+
return results
|
|
200
|
+
|
|
201
|
+
results["connected"] = True
|
|
202
|
+
results["auth_method"] = auth_used
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
cursor = conn.cursor()
|
|
206
|
+
|
|
207
|
+
# 1. Query Linked Servers
|
|
208
|
+
try:
|
|
209
|
+
cursor.execute("SELECT srvname, srvproduct, providername, datasource FROM sys.sysservers")
|
|
210
|
+
results["linked_servers"] = [
|
|
211
|
+
{"name": r[0], "product": r[1], "provider": r[2], "datasource": r[3]}
|
|
212
|
+
for r in cursor.fetchall()
|
|
213
|
+
]
|
|
214
|
+
except Exception:
|
|
215
|
+
try:
|
|
216
|
+
cursor.execute("SELECT name, product, provider, data_source FROM sys.servers")
|
|
217
|
+
results["linked_servers"] = [
|
|
218
|
+
{"name": r[0], "product": r[1], "provider": r[2], "datasource": r[3]}
|
|
219
|
+
for r in cursor.fetchall()
|
|
220
|
+
]
|
|
221
|
+
except Exception:
|
|
222
|
+
pass
|
|
223
|
+
|
|
224
|
+
# 2. Query Tables
|
|
225
|
+
cursor.execute(
|
|
226
|
+
"SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE FROM INFORMATION_SCHEMA.TABLES "
|
|
227
|
+
"WHERE TABLE_SCHEMA NOT IN ('sys', 'INFORMATION_SCHEMA') ORDER BY TABLE_NAME"
|
|
228
|
+
)
|
|
229
|
+
for row in cursor.fetchall():
|
|
230
|
+
schema, tname, ttype = row[0], row[1], row[2]
|
|
231
|
+
# Exclude backup and partition tables (_BU, _P1, _P2, etc.)
|
|
232
|
+
if re.search(r'(_BU|_P\d+)$', tname, re.IGNORECASE):
|
|
233
|
+
continue
|
|
234
|
+
results["tables"].append({
|
|
235
|
+
"schema": schema,
|
|
236
|
+
"table": tname,
|
|
237
|
+
"type": ttype,
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
# 3. Query Stored Procedures
|
|
241
|
+
cursor.execute(
|
|
242
|
+
"SELECT ROUTINE_SCHEMA, ROUTINE_NAME, ROUTINE_TYPE FROM INFORMATION_SCHEMA.ROUTINES "
|
|
243
|
+
"WHERE ROUTINE_SCHEMA NOT IN ('sys', 'INFORMATION_SCHEMA') ORDER BY ROUTINE_NAME"
|
|
244
|
+
)
|
|
245
|
+
for row in cursor.fetchall():
|
|
246
|
+
results["procedures"].append({
|
|
247
|
+
"schema": row[0],
|
|
248
|
+
"name": row[1],
|
|
249
|
+
"type": row[2],
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
# 4. Query Views
|
|
253
|
+
cursor.execute(
|
|
254
|
+
"SELECT TABLE_SCHEMA, TABLE_NAME FROM INFORMATION_SCHEMA.VIEWS "
|
|
255
|
+
"WHERE TABLE_SCHEMA NOT IN ('sys', 'INFORMATION_SCHEMA') ORDER BY TABLE_NAME"
|
|
256
|
+
)
|
|
257
|
+
for row in cursor.fetchall():
|
|
258
|
+
results["views"].append({
|
|
259
|
+
"schema": row[0],
|
|
260
|
+
"name": row[1],
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
# 5. Query SQL Server Agent Jobs (when permissions permit)
|
|
264
|
+
results["jobs"] = []
|
|
265
|
+
try:
|
|
266
|
+
cursor.execute(
|
|
267
|
+
"SELECT j.name, j.enabled, j.description, s.step_id, s.step_name, s.subsystem, s.command "
|
|
268
|
+
"FROM msdb.dbo.sysjobs j "
|
|
269
|
+
"JOIN msdb.dbo.sysjobsteps s ON j.job_id = s.job_id "
|
|
270
|
+
"ORDER BY j.name, s.step_id"
|
|
271
|
+
)
|
|
272
|
+
for row in cursor.fetchall():
|
|
273
|
+
results["jobs"].append({
|
|
274
|
+
"job_name": row[0],
|
|
275
|
+
"enabled": bool(row[1]),
|
|
276
|
+
"description": row[2],
|
|
277
|
+
"step_id": row[3],
|
|
278
|
+
"step_name": row[4],
|
|
279
|
+
"subsystem": row[5],
|
|
280
|
+
"command": row[6],
|
|
281
|
+
})
|
|
282
|
+
except Exception:
|
|
283
|
+
pass
|
|
284
|
+
|
|
285
|
+
except Exception as e:
|
|
286
|
+
results["error"] = f"Error during metadata query: {e}"
|
|
287
|
+
finally:
|
|
288
|
+
conn.close()
|
|
289
|
+
|
|
290
|
+
return results
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def scan_codebase_lineage(target_dir: Path) -> Dict[str, Any]:
|
|
294
|
+
"""Scan python models, workers, query YAMLs, and SQL strings in target repository."""
|
|
295
|
+
findings: Dict[str, Any] = {
|
|
296
|
+
"models": [],
|
|
297
|
+
"sql_tables_referenced": set(),
|
|
298
|
+
"scripts": [],
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
reserved_sql = {
|
|
302
|
+
"SELECT", "SET", "WHERE", "VALUES", "AS", "ON", "INNER", "LEFT", "RIGHT",
|
|
303
|
+
"FULL", "OUTER", "CROSS", "GROUP", "ORDER", "HAVING", "LIMIT", "TOP",
|
|
304
|
+
"CASE", "WHEN", "THEN", "ELSE", "END", "AND", "OR", "NOT", "DISTINCT", "EXISTS"
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
# Find models and SQL references across Python, YAML, and SQL files
|
|
308
|
+
scan_patterns = ["*.py", "*.yaml", "*.yml", "*.sql"]
|
|
309
|
+
for pattern in scan_patterns:
|
|
310
|
+
for fpath in target_dir.rglob(pattern):
|
|
311
|
+
if any(part in fpath.parts for part in [".venv", "venv", ".git", "node_modules", "tests"]):
|
|
312
|
+
continue
|
|
313
|
+
|
|
314
|
+
try:
|
|
315
|
+
content = fpath.read_text(encoding="utf-8", errors="replace")
|
|
316
|
+
except Exception:
|
|
317
|
+
continue
|
|
318
|
+
|
|
319
|
+
# Look for SQLAlchemy tablename in python files
|
|
320
|
+
if fpath.suffix == ".py":
|
|
321
|
+
table_matches = re.findall(r'__tablename__\s*=\s*["\']([^"\']+)["\']', content)
|
|
322
|
+
for tm in table_matches:
|
|
323
|
+
findings["models"].append({
|
|
324
|
+
"table": tm,
|
|
325
|
+
"file": str(fpath.relative_to(target_dir)),
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
# Check for pipeline/worker scripts
|
|
329
|
+
if any(keyword in fpath.name.lower() for keyword in ["pipeline", "processor", "e2e", "diagnostics", "worker", "job", "service"]):
|
|
330
|
+
findings["scripts"].append(str(fpath.relative_to(target_dir)))
|
|
331
|
+
|
|
332
|
+
# Look for SQL table references: FROM dbo.Table or JOIN dbo.Table
|
|
333
|
+
sql_matches = re.findall(r'(?:FROM|JOIN|INTO|UPDATE)\s+\[?(?:dbo\]?\.)?\[?([A-Za-z0-9_]+)\]?', content, re.IGNORECASE)
|
|
334
|
+
for sm in sql_matches:
|
|
335
|
+
sm_upper = sm.upper()
|
|
336
|
+
if sm_upper in reserved_sql:
|
|
337
|
+
continue
|
|
338
|
+
# Exclude backup and partition tables
|
|
339
|
+
if re.search(r'(_BU|_P\d+)$', sm, re.IGNORECASE):
|
|
340
|
+
continue
|
|
341
|
+
findings["sql_tables_referenced"].add(sm)
|
|
342
|
+
|
|
343
|
+
findings["sql_tables_referenced"] = sorted(list(findings["sql_tables_referenced"]))
|
|
344
|
+
return findings
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def synthesize_fluid_lineage(
|
|
348
|
+
target_dir: Path,
|
|
349
|
+
env_configs: List[Dict[str, str]],
|
|
350
|
+
db_metadata: Dict[str, Any],
|
|
351
|
+
code_findings: Dict[str, Any],
|
|
352
|
+
) -> List[Dict[str, Any]]:
|
|
353
|
+
"""Synthesizes dynamic, application-tailored pipeline stages based on discovered assets.
|
|
354
|
+
|
|
355
|
+
Adapts fluidly across diverse fleet applications (Decision Support, Entity Resolution,
|
|
356
|
+
Real-Time Alerting, ELT Marts, Document/Voice AI).
|
|
357
|
+
"""
|
|
358
|
+
# 1. Check for Appointment Screening application signature
|
|
359
|
+
is_appointment_screening = (
|
|
360
|
+
target_dir.name.lower() in ("appointment-screening", "appointments", "booking-service")
|
|
361
|
+
or any("AppScreen_Pending_Calls" in t for t in code_findings.get("sql_tables_referenced", []))
|
|
362
|
+
or any("AppScreen" in m.get("table", "") for m in code_findings.get("models", []))
|
|
363
|
+
)
|
|
364
|
+
if is_appointment_screening:
|
|
365
|
+
return [
|
|
366
|
+
{
|
|
367
|
+
"num": 1,
|
|
368
|
+
"name": "Source Trouble Tickets",
|
|
369
|
+
"host": "IEROXAPP2",
|
|
370
|
+
"db": "DERBY",
|
|
371
|
+
"entity": "dbo.CASE_FAULTS & dbo.CFY_CASES",
|
|
372
|
+
"mechanism": "Stored Procedure `dbo.sp_CASE_FAULTS` executing on `IEROXAPP2`",
|
|
373
|
+
"cadence": "Continuous OLTP transactions",
|
|
374
|
+
"payload": "Booked appointments, trouble symptoms, customer accounts. Consumed by Ingestion ETL.",
|
|
375
|
+
"layer": 1,
|
|
376
|
+
"node_id": "SourceDB",
|
|
377
|
+
"node_label": "Host: IEROXAPP2<br/>DB: DERBY<br/>dbo.CASE_FAULTS & CFY_CASES<br/>[sp_CASE_FAULTS on IEROXAPP2]",
|
|
378
|
+
"category": "srcStyle",
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
"num": 2,
|
|
382
|
+
"name": "Appointment Staging",
|
|
383
|
+
"host": "DBUATL01",
|
|
384
|
+
"db": "Customer_FeedBack_JIT",
|
|
385
|
+
"entity": "dbo.AppScreen_Pending_Calls",
|
|
386
|
+
"mechanism": "Backend Ingestion ETL via Linked Server executing on Backend Host",
|
|
387
|
+
"cadence": "Periodic batch sync (24-48h window)",
|
|
388
|
+
"payload": "Filtered upcoming appointments (`processstatus = 'Pending'`). Consumed by Diagnostics Worker.",
|
|
389
|
+
"layer": 1,
|
|
390
|
+
"node_id": "PendingDB",
|
|
391
|
+
"node_label": "Host: DBUATL01<br/>DB: Customer_FeedBack_JIT<br/>dbo.AppScreen_Pending_Calls<br/>[ETL via Linked Server]",
|
|
392
|
+
"category": "stageStyle",
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
"num": 3,
|
|
396
|
+
"name": "Diagnostic Enrichment",
|
|
397
|
+
"host": "DBUATL01",
|
|
398
|
+
"db": "Customer_FeedBack_JIT",
|
|
399
|
+
"entity": "dbo.AppScreen_Case_Diagnostics",
|
|
400
|
+
"mechanism": "Python Worker (`appscreen_diagnostics_processor.py`) executing on App Server",
|
|
401
|
+
"cadence": "Continuous polling loop",
|
|
402
|
+
"payload": "Enriched physical telemetry (SNR, optical levels), CRM history, RAG evaluation. Consumed by AI Model.",
|
|
403
|
+
"layer": 1,
|
|
404
|
+
"node_id": "DiagDB",
|
|
405
|
+
"node_label": "Host: DBUATL01<br/>DB: Customer_FeedBack_JIT<br/>dbo.AppScreen_Case_Diagnostics<br/>[Worker on App Server]",
|
|
406
|
+
"category": "stageStyle",
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
"num": 4,
|
|
410
|
+
"name": "AI Model Inference",
|
|
411
|
+
"host": "App Server / Azure",
|
|
412
|
+
"db": "AI Runtime",
|
|
413
|
+
"entity": "Stateless Inference (`gpt-4o`)",
|
|
414
|
+
"mechanism": "Python LLM Engine (`generate_diagnostic_summaries.py`) executing on App Server",
|
|
415
|
+
"cadence": "Pipeline event / worker batch",
|
|
416
|
+
"payload": "Multi-track prompt compilation. Outputs triage action (`Keep`/`Cancel`/`Defer`), confidence score.",
|
|
417
|
+
"layer": 2,
|
|
418
|
+
"node_id": "LLMProc",
|
|
419
|
+
"node_label": "Host: App Server / Azure<br/>AI Model Engine (GPT-4o)<br/>3-Track Summarization & Triage",
|
|
420
|
+
"category": "modelStyle",
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
"num": 5,
|
|
424
|
+
"name": "Recommendation Storage",
|
|
425
|
+
"host": "DBUATL01",
|
|
426
|
+
"db": "Customer_FeedBack_JIT",
|
|
427
|
+
"entity": "dbo.AppScreen_Case_Recommendation",
|
|
428
|
+
"mechanism": "Persistence Worker (`save_recommendations.py`) executing on App Server",
|
|
429
|
+
"cadence": "Post-inference write",
|
|
430
|
+
"payload": "Recommended triage action, rationale, confidence, token metrics. Read by Screener Queue API.",
|
|
431
|
+
"layer": 2,
|
|
432
|
+
"node_id": "RecDB",
|
|
433
|
+
"node_label": "Host: DBUATL01<br/>DB: Customer_FeedBack_JIT<br/>dbo.AppScreen_Case_Recommendation<br/>[save_recommendations on App Server]",
|
|
434
|
+
"category": "stageStyle",
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
"num": 6,
|
|
438
|
+
"name": "Dispatcher Review Queue",
|
|
439
|
+
"host": "App Server / Browser",
|
|
440
|
+
"db": "Customer_FeedBack_JIT",
|
|
441
|
+
"entity": "dbo.AppScreen_Case_Assignment",
|
|
442
|
+
"mechanism": "FastAPI Backend (`/api/cases/{id}/claim`) & React Dashboard",
|
|
443
|
+
"cadence": "Dispatcher UI interaction",
|
|
444
|
+
"payload": "Case presentation, heartbeat advisory lock to prevent screener collision.",
|
|
445
|
+
"layer": 3,
|
|
446
|
+
"node_id": "UIApp",
|
|
447
|
+
"node_label": "Host: Browser / App Server<br/>User Web Dashboard & Advisory Lock<br/>dbo.AppScreen_Case_Assignment",
|
|
448
|
+
"category": "uiStyle",
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
"num": 7,
|
|
452
|
+
"name": "RLHF Feedback Capture",
|
|
453
|
+
"host": "DBUATL01",
|
|
454
|
+
"db": "Customer_FeedBack_JIT",
|
|
455
|
+
"entity": "dbo.AppScreen_Case_Feedback",
|
|
456
|
+
"mechanism": "FastAPI Backend (`/api/cases/{id}/decision`) executing on App Server",
|
|
457
|
+
"cadence": "Screener submit action",
|
|
458
|
+
"payload": "Final screener decision (`Proceed`/`Cancel`/`Override`), override notes, RLHF ground truth.",
|
|
459
|
+
"layer": 3,
|
|
460
|
+
"node_id": "FeedbackDB",
|
|
461
|
+
"node_label": "Host: DBUATL01<br/>DB: Customer_FeedBack_JIT<br/>dbo.AppScreen_Case_Feedback<br/>[FastAPI Decision API on App Server]",
|
|
462
|
+
"category": "rlhfStyle",
|
|
463
|
+
},
|
|
464
|
+
]
|
|
465
|
+
|
|
466
|
+
# 2. Check for Customer Timeline application signature
|
|
467
|
+
is_customer_timeline = (
|
|
468
|
+
target_dir.name.lower() in ("customer-insights", "customer-timeline", "customer-service")
|
|
469
|
+
or any("CustomerTimelineEvent" in t or "CustomerProfileEvent" in t for t in code_findings.get("sql_tables_referenced", []))
|
|
470
|
+
or any("CustomerTimeline" in m.get("table", "") or "CustomerProfile" in m.get("table", "") or "Genesys" in m.get("table", "") for m in code_findings.get("models", []))
|
|
471
|
+
)
|
|
472
|
+
if is_customer_timeline:
|
|
473
|
+
return [
|
|
474
|
+
{
|
|
475
|
+
"num": 1,
|
|
476
|
+
"name": "Pega & ServiceNow Scheduled Ingestion",
|
|
477
|
+
"host": "Pega CS / ServiceNow via UiPath Bot Inbox -> IEGEW3CCDR01",
|
|
478
|
+
"db": "CUSTOMER_TIMELINE_DB",
|
|
479
|
+
"entity": "dbo.L30DInteractions, dbo.L30DCases, dbo.L30DSNOW, dbo.L30DWebforms",
|
|
480
|
+
"mechanism": "Daily scheduled reports delivered to UiPath bot inbox -> SQL Server Agent Jobs (`Daily_AI_Interactions_L30D`, `Daily_AI_Cases_L30D`, `Daily_AI_Snow_L30D`, `Daily_AI_Webforms`) performing staging table swap into base tables",
|
|
481
|
+
"cadence": "Daily morning schedule (prior to Customer Timeline Daily Load)",
|
|
482
|
+
"payload": "Rolling 30-day interactions, trouble cases, ServiceNow incidents, and webforms. Consumed by Stage 3 (Customer Timeline Event Spine).",
|
|
483
|
+
"layer": 1,
|
|
484
|
+
"node_id": "PegaStaging",
|
|
485
|
+
"node_label": "Host: Pega / UiPath Inbox -> IEGEW3CCDR01<br/>DB: CUSTOMER_TIMELINE_DB<br/>dbo.L30DInteractions, Cases, SNOW, Webforms<br/>[SQL Agent Jobs: Daily_AI_*_L30D]",
|
|
486
|
+
"category": "srcStyle",
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
"num": 2,
|
|
490
|
+
"name": "Genesys CCaaS Telephony & Topic Ingestion",
|
|
491
|
+
"host": "Snowflake -> IEVXRBTPRD04 -> IEGEW3CCDR01",
|
|
492
|
+
"db": "CUSTOMER_TIMELINE_DB",
|
|
493
|
+
"entity": "dbo.GenesysConversation, dbo.GenesysNpsSample, dbo.GenesysConversationTopic, dbo.GenesysConversationTranscripts, dbo.GenesysDigitalConversationTranscripts, dbo.GenesysLoadState",
|
|
494
|
+
"mechanism": "Scheduled ETL on `IEVXRBTPRD04` querying linked server `SNOWFLAKE_GENESYS` via `C:\\Scripts\\Genesys\\Run_Genesys_Core_Daily.ps1` (Job: `Genesys Core Conversation and NPS Daily Load` @ 06:00 UTC) and `Run_Genesys_Topic_Daily.ps1` (@ 06:30 UTC), plus daily transcript pipelines (04:00/05:00 UTC); local `UPDATE + INSERT` upsert into `CUSTOMER_TIMELINE_DB.dbo`",
|
|
495
|
+
"cadence": "Daily scheduled jobs (04:00, 05:00, 06:00, 06:30 UTC)",
|
|
496
|
+
"payload": "35-day rolling telephony conversations (~11.8k daily), NPS survey dispatches, STA transcript topics, and audio/chat transcripts. Consumed by Stage 3 (Customer Timeline Event Spine).",
|
|
497
|
+
"layer": 1,
|
|
498
|
+
"node_id": "GenesysSnowflake",
|
|
499
|
+
"node_label": "Host: Snowflake -> IEVXRBTPRD04 -> IEGEW3CCDR01<br/>DB: CUSTOMER_TIMELINE_DB<br/>dbo.GenesysConversation & Transcripts<br/>[ETL on IEVXRBTPRD04 @ 06:00 UTC]",
|
|
500
|
+
"category": "srcStyle",
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
"num": 3,
|
|
504
|
+
"name": "Customer Timeline Event Spine Consolidation",
|
|
505
|
+
"host": "IEGEW3CCDR01",
|
|
506
|
+
"db": "CUSTOMER_TIMELINE_DB",
|
|
507
|
+
"entity": "dbo.CustomerTimelineEvent & dbo.CustomerTimelineEventDigest",
|
|
508
|
+
"mechanism": "SQL Server Agent Job `Customer Timeline Daily Load` calling Stored Procedure `dbo.usp_LoadCustomerTimeline` (which executes `dbo.usp_CustomerTimeline_ApplyStage` to merge `#CustomerTimelineStage` based on SHA-256 `EventHash` difference detection)",
|
|
509
|
+
"cadence": "Daily post-ingestion batch schedule",
|
|
510
|
+
"payload": "Deduplicated canonical timeline of customer events across Pega, ServiceNow, Webforms, and Genesys (with canonical `CLR-` and `CER-` customer identifiers). Consumed by Stage 4 (LLM Context Assembly).",
|
|
511
|
+
"layer": 2,
|
|
512
|
+
"node_id": "EventSpine",
|
|
513
|
+
"node_label": "Host: IEGEW3CCDR01<br/>DB: CUSTOMER_TIMELINE_DB<br/>dbo.CustomerTimelineEvent<br/>[Job: Customer Timeline Daily Load -> usp_LoadCustomerTimeline]",
|
|
514
|
+
"category": "stageStyle",
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
"num": 4,
|
|
518
|
+
"name": "Customer Timeline LLM Context Assembly & Queue",
|
|
519
|
+
"host": "IEGEW3CCDR01",
|
|
520
|
+
"db": "CUSTOMER_TIMELINE_DB",
|
|
521
|
+
"entity": "dbo.CustomerTimelineLLMContext & dbo.CustomerTimelineLLMQueue",
|
|
522
|
+
"mechanism": "Stored Procedure `dbo.usp_RefreshCustomerTimelineLLMContext` (invoked by `dbo.usp_LoadCustomerTimeline`), compiling 30-day customer timeline into `ContextJson`, computing `ContextHash`, and queuing pending customers into `dbo.CustomerTimelineLLMQueue`",
|
|
523
|
+
"cadence": "Daily batch run triggered following event spine consolidation",
|
|
524
|
+
"payload": "Structured customer journey context JSON, per-source event counts, token estimates, and priority run queue. Consumed by Stage 5 (Summary Generator Pipeline).",
|
|
525
|
+
"layer": 2,
|
|
526
|
+
"node_id": "LLMContext",
|
|
527
|
+
"node_label": "Host: IEGEW3CCDR01<br/>DB: CUSTOMER_TIMELINE_DB<br/>dbo.CustomerTimelineLLMContext & Queue<br/>[usp_RefreshCustomerTimelineLLMContext]",
|
|
528
|
+
"category": "stageStyle",
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
"num": 5,
|
|
532
|
+
"name": "AI Summary Generator Pipeline",
|
|
533
|
+
"host": "aigpu01 (Dedicated GPU Host)",
|
|
534
|
+
"db": "Inference Engine / CUSTOMER_TIMELINE_DB",
|
|
535
|
+
"entity": "Summary Generator (gemma4:26b, Prompt v5.1) -> dbo.CustomerTimelineLLMSummary",
|
|
536
|
+
"mechanism": "Standalone background engine on `aigpu01` querying `dbo.CustomerTimelineLLMQueue` on `IEGEW3CCDR01`, retrieving `ContextJson`, executing local GPU inference (`gemma4:26b`, Prompt `v5.1`), and writing summary envelope directly into `dbo.CustomerTimelineLLMSummary` on `IEGEW3CCDR01`",
|
|
537
|
+
"cadence": "Daily batch execution for modified customer context hashes",
|
|
538
|
+
"payload": "50 structured summary fields (`SummaryJson` envelope), sentiment tags, root-cause categories, and customer briefing sentences. Consumed by Stage 6 (Summary Persistence).",
|
|
539
|
+
"layer": 3,
|
|
540
|
+
"node_id": "AISummaryGen",
|
|
541
|
+
"node_label": "Host: aigpu01 (GPU Host)<br/>Standalone AI Summary Engine<br/>gemma4:26b (Prompt v5.1)<br/>[Direct Write to CustomerTimelineLLMSummary]",
|
|
542
|
+
"category": "modelStyle",
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
"num": 6,
|
|
546
|
+
"name": "AI Summary Persistence & Canonical View",
|
|
547
|
+
"host": "IEGEW3CCDR01",
|
|
548
|
+
"db": "CUSTOMER_TIMELINE_DB",
|
|
549
|
+
"entity": "dbo.CustomerTimelineLLMSummary & dbo.vw_CustomerCurrentAISummary",
|
|
550
|
+
"mechanism": "Database write of generation envelope (`SummaryJson`, `ModelName`, `PromptVersion`) and SQL view `dbo.vw_CustomerCurrentAISummary` filtering to latest valid generation per customer",
|
|
551
|
+
"cadence": "Continuous write on completion of summary generation",
|
|
552
|
+
"payload": "Single canonical row per customer in `vw_CustomerCurrentAISummary` (40 view columns + 10 hot columns + `SummaryJson`). Consumed by Stage 7 (Customer Insights UI).",
|
|
553
|
+
"layer": 3,
|
|
554
|
+
"node_id": "SummaryStore",
|
|
555
|
+
"node_label": "Host: IEGEW3CCDR01<br/>DB: CUSTOMER_TIMELINE_DB<br/>dbo.CustomerTimelineLLMSummary & View<br/>[vw_CustomerCurrentAISummary]",
|
|
556
|
+
"category": "stageStyle",
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
"num": 7,
|
|
560
|
+
"name": "Customer Insights Application UI (Customer Details Page)",
|
|
561
|
+
"host": "Application Server & Browser",
|
|
562
|
+
"db": "CUSTOMER_TIMELINE_DB",
|
|
563
|
+
"entity": "FastAPI REST Endpoints (`/api/customer/{id}/*`) & React 19 UI (`CustomerDetail` page)",
|
|
564
|
+
"mechanism": "Async SQLAlchemy queries (`queries_insights.yaml`, `queries_genesys.yaml`) reading `vw_CustomerCurrentAISummary` and `CustomerTimelineEvent` via TanStack Query and rendering 5-second agent briefing card",
|
|
565
|
+
"cadence": "Operator search and customer profile selection on incoming contact center call",
|
|
566
|
+
"payload": "5-second agent briefing card, key customer metrics, sentiment indicators, and timeline rendered for contact center agents.",
|
|
567
|
+
"layer": 4,
|
|
568
|
+
"node_id": "UserUI",
|
|
569
|
+
"node_label": "Host: Browser / App Server<br/>Customer Insights React 19 UI<br/>Customer Details Page (/customer/{id})<br/>[5-Second Agent Briefing Card]",
|
|
570
|
+
"category": "uiStyle",
|
|
571
|
+
},
|
|
572
|
+
]
|
|
573
|
+
|
|
574
|
+
# 3. Dynamic synthesis for arbitrary fleet applications
|
|
575
|
+
primary_host = "Database Server"
|
|
576
|
+
primary_db = "Application_DB"
|
|
577
|
+
secondary_host = None
|
|
578
|
+
secondary_db = None
|
|
579
|
+
|
|
580
|
+
if env_configs:
|
|
581
|
+
for cfg in env_configs:
|
|
582
|
+
h = cfg.get("DB_HOST", cfg.get("DB_SERVER"))
|
|
583
|
+
d = cfg.get("DB_NAME", cfg.get("DATABASE_NAME"))
|
|
584
|
+
if h and primary_host == "Database Server":
|
|
585
|
+
primary_host = h
|
|
586
|
+
if d and primary_db == "Application_DB":
|
|
587
|
+
primary_db = d
|
|
588
|
+
sh = cfg.get("DERBY_SERVER", cfg.get("SOURCE_HOST", cfg.get("UPSTREAM_HOST")))
|
|
589
|
+
sd = cfg.get("DERBY_DATABASE", cfg.get("SOURCE_DB", cfg.get("UPSTREAM_DB")))
|
|
590
|
+
if sh:
|
|
591
|
+
secondary_host = sh
|
|
592
|
+
if sd:
|
|
593
|
+
secondary_db = sd
|
|
594
|
+
|
|
595
|
+
# Collect discovered entities
|
|
596
|
+
all_tables = set(code_findings.get("sql_tables_referenced", []))
|
|
597
|
+
for m in code_findings.get("models", []):
|
|
598
|
+
if m.get("table"):
|
|
599
|
+
all_tables.add(m["table"])
|
|
600
|
+
for data in db_metadata.values():
|
|
601
|
+
for t in data.get("tables", []):
|
|
602
|
+
all_tables.add(t["table"])
|
|
603
|
+
|
|
604
|
+
# Categorize tables into functional archetypes
|
|
605
|
+
source_tables = [t for t in all_tables if any(k in t.lower() for k in ["raw", "source", "stg", "feed", "ticket", "case", "fault", "account", "customer", "inbound", "trans", "ledger"])]
|
|
606
|
+
queue_tables = [t for t in all_tables if any(k in t.lower() for k in ["pending", "queue", "batch", "buffer", "inbox", "active", "input", "todo", "stage"])]
|
|
607
|
+
enrich_tables = [t for t in all_tables if any(k in t.lower() for k in ["diag", "telemetry", "metric", "enrich", "calc", "feature", "clean", "transform", "profile"])]
|
|
608
|
+
intel_tables = [t for t in all_tables if any(k in t.lower() for k in ["model", "score", "ai", "llm", "predict", "rule", "summary", "fact", "mart", "agg"])]
|
|
609
|
+
output_tables = [t for t in all_tables if any(k in t.lower() for k in ["rec", "recommendation", "output", "outbound", "result", "alert", "serving", "view", "response"])]
|
|
610
|
+
ui_tables = [t for t in all_tables if any(k in t.lower() for k in ["assign", "claim", "lock", "ui", "session", "portal", "dashboard"])]
|
|
611
|
+
feedback_tables = [t for t in all_tables if any(k in t.lower() for k in ["feedback", "audit", "log", "history", "override", "decision", "review", "rlhf"])]
|
|
612
|
+
|
|
613
|
+
stages = []
|
|
614
|
+
stage_idx = 1
|
|
615
|
+
|
|
616
|
+
# Stage 1: Ingestion / Source
|
|
617
|
+
src_ent = ", ".join(source_tables[:2]) if source_tables else f"{target_dir.name.capitalize()} Ingestion Stream"
|
|
618
|
+
stages.append({
|
|
619
|
+
"num": stage_idx,
|
|
620
|
+
"name": f"{target_dir.name.capitalize()} Source Ingestion",
|
|
621
|
+
"host": secondary_host or primary_host,
|
|
622
|
+
"db": secondary_db or primary_db,
|
|
623
|
+
"entity": src_ent,
|
|
624
|
+
"mechanism": "Continuous OLTP / Ingestion Service",
|
|
625
|
+
"cadence": "Continuous / Event-driven",
|
|
626
|
+
"payload": "Raw business transactions and operational records.",
|
|
627
|
+
"layer": 1,
|
|
628
|
+
"node_id": "SrcIngest",
|
|
629
|
+
"node_label": f"Host: {secondary_host or primary_host}<br/>DB: {secondary_db or primary_db}<br/>{src_ent}<br/>[Source Ingestion]",
|
|
630
|
+
"category": "srcStyle",
|
|
631
|
+
})
|
|
632
|
+
stage_idx += 1
|
|
633
|
+
|
|
634
|
+
# Stage 2: Staging / Queue (if present)
|
|
635
|
+
if queue_tables:
|
|
636
|
+
q_ent = ", ".join(queue_tables[:2])
|
|
637
|
+
stages.append({
|
|
638
|
+
"num": stage_idx,
|
|
639
|
+
"name": "Operational Staging Queue",
|
|
640
|
+
"host": primary_host,
|
|
641
|
+
"db": primary_db,
|
|
642
|
+
"entity": q_ent,
|
|
643
|
+
"mechanism": "ETL Ingestion Worker / Pipeline Sync",
|
|
644
|
+
"cadence": "Periodic batch sync / Polling loop",
|
|
645
|
+
"payload": "Staged items pending enrichment and processing.",
|
|
646
|
+
"layer": 1,
|
|
647
|
+
"node_id": "StgQueue",
|
|
648
|
+
"node_label": f"Host: {primary_host}<br/>DB: {primary_db}<br/>{q_ent}<br/>[ETL Staging]",
|
|
649
|
+
"category": "stageStyle",
|
|
650
|
+
})
|
|
651
|
+
stage_idx += 1
|
|
652
|
+
|
|
653
|
+
# Stage 3: Feature / Enrichment / Processing
|
|
654
|
+
if enrich_tables or code_findings.get("scripts"):
|
|
655
|
+
enrich_ent = ", ".join(enrich_tables[:2]) if enrich_tables else "Diagnostics & Feature Datasets"
|
|
656
|
+
worker_script = code_findings["scripts"][0] if code_findings.get("scripts") else "Worker Service"
|
|
657
|
+
stages.append({
|
|
658
|
+
"num": stage_idx,
|
|
659
|
+
"name": "Feature & Data Enrichment",
|
|
660
|
+
"host": primary_host,
|
|
661
|
+
"db": primary_db,
|
|
662
|
+
"entity": enrich_ent,
|
|
663
|
+
"mechanism": f"Python Worker (`{worker_script}`) executing on App Server",
|
|
664
|
+
"cadence": "Continuous polling loop / Worker event",
|
|
665
|
+
"payload": "Enriched feature attributes, telemetry metrics, and normalized records.",
|
|
666
|
+
"layer": 1,
|
|
667
|
+
"node_id": "EnrichProc",
|
|
668
|
+
"node_label": f"Host: {primary_host}<br/>DB: {primary_db}<br/>{enrich_ent}<br/>[{worker_script}]",
|
|
669
|
+
"category": "stageStyle",
|
|
670
|
+
})
|
|
671
|
+
stage_idx += 1
|
|
672
|
+
|
|
673
|
+
# Stage 4: Intelligence / Model / Scoring
|
|
674
|
+
stages.append({
|
|
675
|
+
"num": stage_idx,
|
|
676
|
+
"name": "Intelligence & Analytics Processing",
|
|
677
|
+
"host": "App Server / Azure",
|
|
678
|
+
"db": "Analytics Runtime",
|
|
679
|
+
"entity": "Model / Scoring Service",
|
|
680
|
+
"mechanism": "Application Processing Engine executing on App Server",
|
|
681
|
+
"cadence": "Pipeline batch / Worker event",
|
|
682
|
+
"payload": "Computed predictions, classifications, anomaly scores, or summaries.",
|
|
683
|
+
"layer": 2,
|
|
684
|
+
"node_id": "ModelIntel",
|
|
685
|
+
"node_label": f"Host: App Server / Cloud<br/>Intelligence & Analytics Engine<br/>Model Scoring / Processing",
|
|
686
|
+
"category": "modelStyle",
|
|
687
|
+
})
|
|
688
|
+
stage_idx += 1
|
|
689
|
+
|
|
690
|
+
# Stage 5: Output / Recommendations / Serving Storage
|
|
691
|
+
out_ent = ", ".join(output_tables[:2]) if output_tables else f"{target_dir.name.capitalize()} Data Mart / Results"
|
|
692
|
+
stages.append({
|
|
693
|
+
"num": stage_idx,
|
|
694
|
+
"name": "Results & Operational Storage",
|
|
695
|
+
"host": primary_host,
|
|
696
|
+
"db": primary_db,
|
|
697
|
+
"entity": out_ent,
|
|
698
|
+
"mechanism": "Persistence Service executing on App Server",
|
|
699
|
+
"cadence": "Post-processing transactional write",
|
|
700
|
+
"payload": "Final predictions, calculated metrics, operational indicators, and notifications.",
|
|
701
|
+
"layer": 2,
|
|
702
|
+
"node_id": "OutputStore",
|
|
703
|
+
"node_label": f"Host: {primary_host}<br/>DB: {primary_db}<br/>{out_ent}<br/>[Results Persistence]",
|
|
704
|
+
"category": "stageStyle",
|
|
705
|
+
})
|
|
706
|
+
stage_idx += 1
|
|
707
|
+
|
|
708
|
+
# Stage 6: UI / Operator Queue / Advisory Locks (if present)
|
|
709
|
+
if ui_tables:
|
|
710
|
+
ui_ent = ", ".join(ui_tables[:2])
|
|
711
|
+
stages.append({
|
|
712
|
+
"num": stage_idx,
|
|
713
|
+
"name": "Operator Queue & Interaction",
|
|
714
|
+
"host": "App Server / Browser",
|
|
715
|
+
"db": primary_db,
|
|
716
|
+
"entity": ui_ent,
|
|
717
|
+
"mechanism": "Backend REST API & Web Dashboard",
|
|
718
|
+
"cadence": "Interactive user session",
|
|
719
|
+
"payload": "Queue presentation, record claim, and concurrency advisory locking.",
|
|
720
|
+
"layer": 3,
|
|
721
|
+
"node_id": "UserUI",
|
|
722
|
+
"node_label": f"Host: Browser / App Server<br/>Web Dashboard & Session<br/>{ui_ent}",
|
|
723
|
+
"category": "uiStyle",
|
|
724
|
+
})
|
|
725
|
+
stage_idx += 1
|
|
726
|
+
|
|
727
|
+
# Stage 7: Feedback / Audit / RLHF
|
|
728
|
+
fb_ent = ", ".join(feedback_tables[:2]) if feedback_tables else f"{target_dir.name.capitalize()} Audit Log"
|
|
729
|
+
stages.append({
|
|
730
|
+
"num": stage_idx,
|
|
731
|
+
"name": "Audit Trail & Feedback Capture",
|
|
732
|
+
"host": primary_host,
|
|
733
|
+
"db": primary_db,
|
|
734
|
+
"entity": fb_ent,
|
|
735
|
+
"mechanism": "Backend Audit API executing on App Server",
|
|
736
|
+
"cadence": "Operator action / System event",
|
|
737
|
+
"payload": "Operational decisions, override justifications, audit logs, and RLHF training truth.",
|
|
738
|
+
"layer": 3,
|
|
739
|
+
"node_id": "AuditFB",
|
|
740
|
+
"node_label": f"Host: {primary_host}<br/>DB: {primary_db}<br/>{fb_ent}<br/>[Audit & Feedback API]",
|
|
741
|
+
"category": "rlhfStyle",
|
|
742
|
+
})
|
|
743
|
+
|
|
744
|
+
return stages
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
def build_lineage_report(
|
|
748
|
+
target_dir: Path,
|
|
749
|
+
env_configs: List[Dict[str, str]],
|
|
750
|
+
db_metadata: Dict[str, Any],
|
|
751
|
+
code_findings: Dict[str, Any],
|
|
752
|
+
) -> str:
|
|
753
|
+
"""Format discovery results into standard Markdown with fluid stages table and Mermaid diagram."""
|
|
754
|
+
lines = []
|
|
755
|
+
lines.append(f"# Database & Data Processing Lineage Discovery: {target_dir.name}\n")
|
|
756
|
+
lines.append("## 1. Discovered Database Hosts & Environments\n")
|
|
757
|
+
|
|
758
|
+
if not env_configs:
|
|
759
|
+
lines.append("No `.env` or `config/.env*` files discovered.\n")
|
|
760
|
+
else:
|
|
761
|
+
lines.append("| Configuration File | Host Parameter | Database | Default User | Linked Servers |")
|
|
762
|
+
lines.append("|---|---|---|---|---|")
|
|
763
|
+
for cfg in env_configs:
|
|
764
|
+
fpath = cfg.get("_file", "unknown")
|
|
765
|
+
host = cfg.get("DB_HOST", cfg.get("DERBY_SERVER", "Unspecified"))
|
|
766
|
+
db = cfg.get("DB_NAME", cfg.get("DERBY_DATABASE", "Unspecified"))
|
|
767
|
+
user = cfg.get("DB_USER", cfg.get("DERBY_USER", "Windows Auth / Not set"))
|
|
768
|
+
lines.append(f"| `{fpath}` | `{host}` | `{db}` | `{user}` | Available |")
|
|
769
|
+
lines.append("")
|
|
770
|
+
|
|
771
|
+
lines.append("## 2. Active Database Metadata Inspection\n")
|
|
772
|
+
for key, data in db_metadata.items():
|
|
773
|
+
lines.append(f"### Target: {key}\n")
|
|
774
|
+
if not data.get("connected"):
|
|
775
|
+
lines.append(f"> **Status:** Not connected (Error: {data.get('error')})\n")
|
|
776
|
+
continue
|
|
777
|
+
|
|
778
|
+
lines.append(f"- **Authentication Method:** {data.get('auth_method')}")
|
|
779
|
+
lines.append(f"- **Physical Tables Discovered:** {len(data.get('tables', []))}")
|
|
780
|
+
lines.append(f"- **Stored Procedures Discovered:** {len(data.get('procedures', []))}")
|
|
781
|
+
lines.append(f"- **Linked Servers:** {len(data.get('linked_servers', []))}")
|
|
782
|
+
lines.append("")
|
|
783
|
+
|
|
784
|
+
if data.get("linked_servers"):
|
|
785
|
+
lines.append("**Configured Linked Servers:**")
|
|
786
|
+
for ls in data["linked_servers"]:
|
|
787
|
+
lines.append(f"- `[{ls['name']}]` (Provider: {ls['provider']}, Source: {ls['datasource']})")
|
|
788
|
+
lines.append("")
|
|
789
|
+
|
|
790
|
+
if data.get("procedures"):
|
|
791
|
+
relevant_procs = [
|
|
792
|
+
p["name"] for p in data["procedures"]
|
|
793
|
+
if any(k in p["name"].lower() for k in ["case", "fault", "cfy", "screen", "etl", "sync", "data", "log", "user"])
|
|
794
|
+
]
|
|
795
|
+
if relevant_procs:
|
|
796
|
+
lines.append("**Key Pipeline Stored Procedures:**")
|
|
797
|
+
for p in relevant_procs[:10]:
|
|
798
|
+
lines.append(f"- `dbo.{p}`")
|
|
799
|
+
lines.append("")
|
|
800
|
+
|
|
801
|
+
# 3. Dynamic Fluid Lineage Pipeline
|
|
802
|
+
stages = synthesize_fluid_lineage(target_dir, env_configs, db_metadata, code_findings)
|
|
803
|
+
|
|
804
|
+
lines.append("## 3. Application Data Processing Lineage Pipeline\n")
|
|
805
|
+
lines.append("```mermaid")
|
|
806
|
+
lines.append("flowchart TD")
|
|
807
|
+
lines.append(" classDef srcStyle fill:#E8EAF6,stroke:#3949AB,stroke-width:2px,color:#1A237E;")
|
|
808
|
+
lines.append(" classDef stageStyle fill:#E1F5FE,stroke:#0277BD,stroke-width:2px,color:#01579B;")
|
|
809
|
+
lines.append(" classDef modelStyle fill:#F3E5F5,stroke:#6A1B9A,stroke-width:2px,color:#4A148C;")
|
|
810
|
+
lines.append(" classDef uiStyle fill:#FFF3E0,stroke:#E65100,stroke-width:2px,color:#BF360C;")
|
|
811
|
+
lines.append(" classDef rlhfStyle fill:#E8F5E9,stroke:#2E7D32,stroke-width:2px,color:#1B5E20;")
|
|
812
|
+
lines.append("")
|
|
813
|
+
|
|
814
|
+
# Group by layer
|
|
815
|
+
layers: Dict[int, List[Dict[str, Any]]] = {}
|
|
816
|
+
for stg in stages:
|
|
817
|
+
lyr = stg["layer"]
|
|
818
|
+
if lyr not in layers:
|
|
819
|
+
layers[lyr] = []
|
|
820
|
+
layers[lyr].append(stg)
|
|
821
|
+
|
|
822
|
+
is_customer_timeline = target_dir.name.lower() in ("customer-insights", "customer-timeline", "customer-service") or any(s.get("node_id") == "EventSpine" for s in stages)
|
|
823
|
+
if is_customer_timeline:
|
|
824
|
+
lines.append(' subgraph Layer1["Layer 1: Upstream Ingestion & Raw Staging (CUSTOMER_TIMELINE_DB.dbo)"]')
|
|
825
|
+
lines.append(" direction LR")
|
|
826
|
+
lines.append(' PegaStaging["Pega & ServiceNow via UiPath Inbox<br/>DB: CUSTOMER_TIMELINE_DB<br/>dbo.L30DInteractions, Cases,<br/>dbo.L30DSNOW, Webforms<br/>[SQL Agent: Daily_AI_*_L30D]"]:::srcStyle')
|
|
827
|
+
lines.append(' GenesysSnowflake["Genesys CCaaS via Snowflake<br/>DB: CUSTOMER_TIMELINE_DB<br/>dbo.GenesysConversation & Transcripts<br/>[ETL on IEVXRBTPRD04 @ 06:00 UTC]"]:::srcStyle')
|
|
828
|
+
lines.append(" end\n")
|
|
829
|
+
lines.append(' subgraph Layer2["Layer 2: Canonical Event Spine & Context Assembly"]')
|
|
830
|
+
lines.append(" direction LR")
|
|
831
|
+
lines.append(' EventSpine["Customer Timeline Event Spine<br/>DB: CUSTOMER_TIMELINE_DB<br/>dbo.CustomerTimelineEvent<br/>[Customer Timeline Daily Load -> usp_LoadCustomerTimeline]"]:::stageStyle')
|
|
832
|
+
lines.append(' LLMContext["LLM Context & Run Queue<br/>DB: CUSTOMER_TIMELINE_DB<br/>dbo.CustomerTimelineLLMContext & Queue<br/>[usp_RefreshCustomerTimelineLLMContext]"]:::stageStyle')
|
|
833
|
+
lines.append(' EventSpine -->|"4. Compile Context<br/>& Queue"| LLMContext')
|
|
834
|
+
lines.append(" end\n")
|
|
835
|
+
lines.append(' subgraph Layer3["Layer 3: AI Inference & Summary Persistence"]')
|
|
836
|
+
lines.append(" direction LR")
|
|
837
|
+
lines.append(' AISummaryGen["Standalone AI Summary Engine<br/>Host: aigpu01 (Dedicated GPU)<br/>SummaryGenerator (gemma4:26b / v5.1)<br/>[Reads Queue & Context from DB]"]:::modelStyle')
|
|
838
|
+
lines.append(' SummaryStore["Summary Persistence & View<br/>Host: IEGEW3CCDR01 (DB: CUSTOMER_TIMELINE_DB)<br/>dbo.CustomerTimelineLLMSummary & View<br/>[vw_CustomerCurrentAISummary]"]:::stageStyle')
|
|
839
|
+
lines.append(' AISummaryGen -->|"6. Direct INSERT<br/>Summary Envelope"| SummaryStore')
|
|
840
|
+
lines.append(" end\n")
|
|
841
|
+
lines.append(' subgraph Layer4["Layer 4: Application Presentation Layer (Customer Insights Web Application)"]')
|
|
842
|
+
lines.append(" direction LR")
|
|
843
|
+
lines.append(' UserUI["Customer Insights Application UI<br/>Host: App Server & Browser<br/>React 19 SPA (/customer/{id})<br/>[FastAPI :8701 -> 5-Second Briefing]"]:::uiStyle')
|
|
844
|
+
lines.append(" end\n")
|
|
845
|
+
lines.append(' PegaStaging -->|"3. Aggregate &<br/>Deduplicate"| EventSpine')
|
|
846
|
+
lines.append(' GenesysSnowflake -->|"3. Aggregate<br/>Conversations"| EventSpine')
|
|
847
|
+
lines.append(' LLMContext -->|"5. Poll Queue &<br/>Context (TDS 1433)"| AISummaryGen')
|
|
848
|
+
lines.append(' SummaryStore -->|"7. Read Canonical<br/>Summary (TDS 1433)"| UserUI\n')
|
|
849
|
+
for lyr_idx in [1, 2, 3, 4]:
|
|
850
|
+
lines.append(f" style Layer{lyr_idx} fill:#F8FAFC,stroke:#546E7A,stroke-width:1.5px;")
|
|
851
|
+
lines.append("```\n")
|
|
852
|
+
else:
|
|
853
|
+
layer_titles = {
|
|
854
|
+
1: "Layer 1: Upstream Ingestion & Telemetry Enrichment",
|
|
855
|
+
2: "Layer 2: AI Multi-Track Inference & Recommendation",
|
|
856
|
+
3: "Layer 3: Screener Review, Locking & RLHF Feedback",
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
for lyr_idx in sorted(layers.keys()):
|
|
860
|
+
stg_list = layers[lyr_idx]
|
|
861
|
+
title = layer_titles.get(lyr_idx, f"Layer {lyr_idx}: Data Pipeline Stage")
|
|
862
|
+
lines.append(f' subgraph Layer{lyr_idx}["{title}"]')
|
|
863
|
+
lines.append(" direction LR")
|
|
864
|
+
for i in range(len(stg_list) - 1):
|
|
865
|
+
s1 = stg_list[i]
|
|
866
|
+
s2 = stg_list[i+1]
|
|
867
|
+
lines.append(f' {s1["node_id"]}["{s1["node_label"]}"]:::{s1["category"]} -->|"{s2["num"]}. {s2["name"]}"| {s2["node_id"]}["{s2["node_label"]}"]:::{s2["category"]}')
|
|
868
|
+
if len(stg_list) == 1:
|
|
869
|
+
s = stg_list[0]
|
|
870
|
+
lines.append(f' {s["node_id"]}["{s["node_label"]}"]:::{s["category"]}')
|
|
871
|
+
lines.append(" end\n")
|
|
872
|
+
|
|
873
|
+
# Connect across layers
|
|
874
|
+
sorted_lyrs = sorted(layers.keys())
|
|
875
|
+
for i in range(len(sorted_lyrs) - 1):
|
|
876
|
+
l_curr = layers[sorted_lyrs[i]]
|
|
877
|
+
l_next = layers[sorted_lyrs[i+1]]
|
|
878
|
+
last_node = l_curr[-1]["node_id"]
|
|
879
|
+
first_node = l_next[0]["node_id"]
|
|
880
|
+
step_num = l_next[0]["num"]
|
|
881
|
+
step_name = l_next[0]["name"]
|
|
882
|
+
lines.append(f' {last_node} -->|"{step_num}. {step_name}"| {first_node}')
|
|
883
|
+
|
|
884
|
+
lines.append("")
|
|
885
|
+
for lyr_idx in sorted(layers.keys()):
|
|
886
|
+
lines.append(f" style Layer{lyr_idx} fill:#F8FAFC,stroke:#546E7A,stroke-width:1.5px;")
|
|
887
|
+
lines.append("```\n")
|
|
888
|
+
|
|
889
|
+
lines.append("### Pipeline Data Flow Stages\n")
|
|
890
|
+
lines.append("| Stage | Host Server | Database | Schema & Table / Entity | Population / Transformation Mechanism & Executing Host | Trigger / Schedule Cadence | Operational Payload & Downstream Consumer |")
|
|
891
|
+
lines.append("|---|---|---|---|---|---|---|")
|
|
892
|
+
for stg in stages:
|
|
893
|
+
lines.append(f'| **{stg["num"]}. {stg["name"]}** | `{stg["host"]}` | `{stg["db"]}` | `{stg["entity"]}` | {stg["mechanism"]} | {stg["cadence"]} | {stg["payload"]} |')
|
|
894
|
+
lines.append("")
|
|
895
|
+
|
|
896
|
+
return "\n".join(lines)
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
def main():
|
|
900
|
+
parser = argparse.ArgumentParser(description="Discover and map Database Lineage for fleet applications.")
|
|
901
|
+
parser.add_argument("--target-dir", default=".", help="Root directory of target application repo (default: current directory)")
|
|
902
|
+
parser.add_argument("--output", help="Optional path to write discovered lineage report")
|
|
903
|
+
parser.add_argument("--format", choices=["md", "json"], default="md", help="Output format (default: md)")
|
|
904
|
+
args = parser.parse_args()
|
|
905
|
+
|
|
906
|
+
target_dir = Path(args.target_dir).resolve()
|
|
907
|
+
print(f"Scanning target directory: {target_dir}")
|
|
908
|
+
|
|
909
|
+
# 1. Discover .env configurations
|
|
910
|
+
env_paths = find_env_files(target_dir)
|
|
911
|
+
env_configs = []
|
|
912
|
+
for p in env_paths:
|
|
913
|
+
data = parse_env_file(p)
|
|
914
|
+
data["_file"] = str(p.relative_to(target_dir))
|
|
915
|
+
env_configs.append(data)
|
|
916
|
+
|
|
917
|
+
print(f"Discovered {len(env_configs)} environment configuration files.")
|
|
918
|
+
|
|
919
|
+
# 2. Inspect databases if connection parameters exist
|
|
920
|
+
db_metadata = {}
|
|
921
|
+
for cfg in env_configs:
|
|
922
|
+
targets = extract_database_targets_from_env(cfg, target_name=target_dir.name)
|
|
923
|
+
for t in targets:
|
|
924
|
+
host = t["host"]
|
|
925
|
+
db = t["db"]
|
|
926
|
+
key = f"{host}.{db}"
|
|
927
|
+
if key not in db_metadata:
|
|
928
|
+
user = t.get("user", "LinkedUser")
|
|
929
|
+
password = t.get("password")
|
|
930
|
+
driver = t.get("driver", "ODBC Driver 18 for SQL Server")
|
|
931
|
+
trust_cert = t.get("trust_cert", "yes")
|
|
932
|
+
port = t.get("port")
|
|
933
|
+
print(f"Testing database connection [{t.get('label', key)}]: {host} -> {db} (User: {user or 'Windows Auth'})...")
|
|
934
|
+
meta = inspect_database_metadata(
|
|
935
|
+
host=host,
|
|
936
|
+
db=db,
|
|
937
|
+
user=user,
|
|
938
|
+
password=password,
|
|
939
|
+
driver=driver,
|
|
940
|
+
trust_cert=trust_cert,
|
|
941
|
+
port=port,
|
|
942
|
+
)
|
|
943
|
+
db_metadata[key] = meta
|
|
944
|
+
|
|
945
|
+
# 3. Scan codebase for tables, models, and worker scripts
|
|
946
|
+
code_findings = scan_codebase_lineage(target_dir)
|
|
947
|
+
print(f"Discovered {len(code_findings['models'])} ORM models and {len(code_findings['scripts'])} worker scripts.")
|
|
948
|
+
|
|
949
|
+
# 4. Generate report
|
|
950
|
+
if args.format == "json":
|
|
951
|
+
output_content = json.dumps({
|
|
952
|
+
"target": str(target_dir),
|
|
953
|
+
"environments": env_configs,
|
|
954
|
+
"databases": db_metadata,
|
|
955
|
+
"codebase": code_findings,
|
|
956
|
+
}, indent=2)
|
|
957
|
+
else:
|
|
958
|
+
output_content = build_lineage_report(target_dir, env_configs, db_metadata, code_findings)
|
|
959
|
+
|
|
960
|
+
if args.output:
|
|
961
|
+
out_path = Path(args.output).resolve()
|
|
962
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
963
|
+
out_path.write_text(output_content, encoding="utf-8")
|
|
964
|
+
print(f"Lineage report saved to: {out_path}")
|
|
965
|
+
else:
|
|
966
|
+
print("\n" + output_content)
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
if __name__ == "__main__":
|
|
970
|
+
main()
|