@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.
Files changed (28) hide show
  1. package/bundles/antigravity-plugin/caddis_statusline.py +30 -3
  2. package/bundles/antigravity-plugin/plugin.json +1 -1
  3. package/bundles/antigravity-plugin/repo_freshness.py +146 -0
  4. package/bundles/antigravity-plugin/scripts/caddis-run.ps1 +11 -0
  5. package/bundles/antigravity-plugin/scripts/caddis-run.sh +9 -0
  6. package/bundles/antigravity-plugin/scripts/caddis_gate.py +24 -3
  7. package/bundles/antigravity-plugin/scripts/caddis_governor.py +542 -0
  8. package/bundles/antigravity-plugin/scripts/caddis_host_guard.py +689 -0
  9. package/bundles/antigravity-plugin/scripts/caddis_lanes.py +1632 -0
  10. package/bundles/antigravity-plugin/scripts/caddis_statusline.py +30 -3
  11. package/bundles/antigravity-plugin/scripts/install-host-guard.ps1 +393 -0
  12. package/bundles/antigravity-plugin/skills/_registry.md +2 -0
  13. package/bundles/antigravity-plugin/skills/confluence-hla/SKILL.md +375 -0
  14. package/bundles/antigravity-plugin/skills/confluence-hla/scripts/convert_hla_to_confluence.py +1164 -0
  15. package/bundles/antigravity-plugin/skills/confluence-hla/scripts/discover_db_lineage.py +970 -0
  16. package/bundles/antigravity-plugin/skills/feature-plan/SKILL.md +30 -4
  17. package/bundles/antigravity-plugin/skills/implement/SKILL.md +48 -1
  18. package/bundles/antigravity-plugin/skills/pr-merge/SKILL.md +155 -0
  19. package/bundles/antigravity-plugin/warm_start_agy.py +38 -4
  20. package/bundles/codex/.codex/skills/docs/confluence-hla/SKILL.md +375 -0
  21. package/bundles/codex/.codex/skills/docs/confluence-hla/scripts/convert_hla_to_confluence.py +1164 -0
  22. package/bundles/codex/.codex/skills/docs/confluence-hla/scripts/discover_db_lineage.py +970 -0
  23. package/bundles/manifest.json +2 -2
  24. package/dist/claude-deepseek.js +50 -10
  25. package/dist/claude-glm.js +50 -10
  26. package/dist/claude-oss.js +50 -10
  27. package/dist/cli.js +173 -63
  28. 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()