@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,1164 @@
|
|
|
1
|
+
"""Convert High-Level Architecture (HLA) Markdown documents into Confluence Storage Format XHTML.
|
|
2
|
+
|
|
3
|
+
This script parses an HLA Markdown document, strips YAML frontmatter, metadata notices,
|
|
4
|
+
and reference sections, converts Markdown structure and tables to native Confluence XHTML,
|
|
5
|
+
and embeds self-contained PlantUML macros with corporate Material Design styling.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python scripts/convert_hla_to_confluence.py <path-to-hla.md> [--output <path-to-output.html>]
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
import xml.etree.ElementTree as ET
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Dict, List, Optional
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def strip_frontmatter(content: str) -> str:
|
|
20
|
+
"""Remove YAML frontmatter from the beginning of markdown content."""
|
|
21
|
+
pattern = r"^---\s*\n(.*?)\n---\s*\n"
|
|
22
|
+
return re.sub(pattern, "", content, flags=re.DOTALL)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def strip_references_and_ai_metadata(content: str) -> str:
|
|
26
|
+
"""Remove references section, document notices, and any AI/agent-specific metadata."""
|
|
27
|
+
# 1. Remove Reference Documents subsection up to next header or end
|
|
28
|
+
pattern_ref = r"###\s*\d+\.\d+\s+Reference Documents.*?(?=(^##|\Z))"
|
|
29
|
+
content = re.sub(pattern_ref, "", content, flags=re.DOTALL | re.MULTILINE)
|
|
30
|
+
|
|
31
|
+
# 2. Filter out document notices, STE statements, and AI tool/model metadata
|
|
32
|
+
banned_keywords = [
|
|
33
|
+
"document notice",
|
|
34
|
+
"simplified technical english",
|
|
35
|
+
"asd-ste100",
|
|
36
|
+
"active voice is used",
|
|
37
|
+
"sentences are short",
|
|
38
|
+
"explanations are direct",
|
|
39
|
+
"caddis",
|
|
40
|
+
"creating model",
|
|
41
|
+
"creation-agent",
|
|
42
|
+
"claude code",
|
|
43
|
+
"codex",
|
|
44
|
+
"antigravity",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
lines = []
|
|
48
|
+
for line in content.splitlines():
|
|
49
|
+
lower = line.lower()
|
|
50
|
+
if any(keyword in lower for keyword in banned_keywords):
|
|
51
|
+
continue
|
|
52
|
+
lines.append(line)
|
|
53
|
+
return "\n".join(lines)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def sanitize_ai_slop_and_punctuation(content: str) -> str:
|
|
57
|
+
"""Normalize punctuation, eliminate em-dashes/en-dashes, and purge AI slop.
|
|
58
|
+
|
|
59
|
+
1. Replaces em-dashes (—) and en-dashes (–) with colons, parentheses, or ASCII hyphens.
|
|
60
|
+
2. Converts smart/curly quotes (“ ” ‘ ’) to straight ASCII quotes.
|
|
61
|
+
3. Replaces unicode bullet characters (•) with ASCII dashes (-).
|
|
62
|
+
4. Eliminates AI buzzwords, purple prose, and meta-commentary framing.
|
|
63
|
+
"""
|
|
64
|
+
# 1. Normalize em-dashes and en-dashes in titles and bullet definitions
|
|
65
|
+
content = re.sub(r'^(#+\s+[^—\n]+?)\s*[—–]\s*(.+)$', r'\1: \2', content, flags=re.MULTILINE)
|
|
66
|
+
content = re.sub(r'\*\*([^*—–\n]+?)\s*[—–]\s*([^*—–\n]+?)\*\*', r'**\1 (\2)**', content)
|
|
67
|
+
content = content.replace("—", " - ").replace("–", "-")
|
|
68
|
+
content = re.sub(r'[ \t]+-[ \t]+', ' - ', content)
|
|
69
|
+
|
|
70
|
+
# 2. Smart quotes to straight ASCII
|
|
71
|
+
content = content.replace("“", '"').replace("”", '"').replace("‘", "'").replace("’", "'")
|
|
72
|
+
|
|
73
|
+
# 3. Unicode bullet characters to ASCII hyphen
|
|
74
|
+
content = content.replace("•", "-")
|
|
75
|
+
|
|
76
|
+
# 4. Filter AI slop buzzwords and purple prose
|
|
77
|
+
ai_buzzword_replacements = [
|
|
78
|
+
(r'\bdelve into\b', 'inspect'),
|
|
79
|
+
(r'\bdelve\b', 'investigate'),
|
|
80
|
+
(r'\bpivotal\b', 'primary'),
|
|
81
|
+
(r'\btestament to\b', 'evidence of'),
|
|
82
|
+
(r'\btapestry\b', 'architecture'),
|
|
83
|
+
(r'\bbeacon of\b', 'model for'),
|
|
84
|
+
(r'\bgame-changer\b', 'significant improvement'),
|
|
85
|
+
(r'\brevolutionize\b', 'transform'),
|
|
86
|
+
(r'\bcornerstone of\b', 'core element of'),
|
|
87
|
+
(r'\bseamlessly\b', 'directly'),
|
|
88
|
+
(r'\bseamless\b', 'direct'),
|
|
89
|
+
(r'\bholistic\b', 'end-to-end'),
|
|
90
|
+
(r'\bfoster\b', 'support'),
|
|
91
|
+
(r'\bharnessing\b', 'using'),
|
|
92
|
+
(r'\bleverage\b', 'use'),
|
|
93
|
+
(r'\bleveraging\b', 'using'),
|
|
94
|
+
(r'\bleverages\b', 'uses'),
|
|
95
|
+
(r'\bleveraged\b', 'used'),
|
|
96
|
+
(r'\bmeticulous\b', 'detailed'),
|
|
97
|
+
(r'\bcrucial\b', 'critical'),
|
|
98
|
+
(r'\bcutting-edge\b', 'modern'),
|
|
99
|
+
(r'\brobust\b', 'reliable'),
|
|
100
|
+
]
|
|
101
|
+
for pattern, repl in ai_buzzword_replacements:
|
|
102
|
+
content = re.sub(pattern, repl, content, flags=re.IGNORECASE)
|
|
103
|
+
|
|
104
|
+
return content
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def format_inline_markdown(text: str) -> str:
|
|
108
|
+
"""Safely escape XML characters and convert inline markdown (bold, italic, code)."""
|
|
109
|
+
# 0. Sanitize AI slop punctuation (em-dashes, en-dashes, smart quotes, bullets)
|
|
110
|
+
text = text.replace("—", " - ").replace("–", "-")
|
|
111
|
+
text = text.replace("“", '"').replace("”", '"').replace("‘", "'").replace("’", "'")
|
|
112
|
+
text = text.replace("•", "-")
|
|
113
|
+
|
|
114
|
+
# 1. Escape XML characters
|
|
115
|
+
text = re.sub(r"&(?!(amp|lt|gt|quot|apos);)", "&", text)
|
|
116
|
+
text = text.replace("<", "<").replace(">", ">")
|
|
117
|
+
|
|
118
|
+
# 2. Extract and protect code spans with placeholders so wildcards (*) are not parsed as italics
|
|
119
|
+
code_spans = []
|
|
120
|
+
|
|
121
|
+
def save_code(m):
|
|
122
|
+
code_spans.append(f"<code>{m.group(1)}</code>")
|
|
123
|
+
return f"__INLINE_CODE_SPAN_{len(code_spans)-1}__"
|
|
124
|
+
|
|
125
|
+
text = re.sub(r"`([^`]+)`", save_code, text)
|
|
126
|
+
|
|
127
|
+
# 3. Bold **bold** -> <strong>bold</strong>
|
|
128
|
+
text = re.sub(r"\*\*([^*]+)\*\*", r"<strong>\1</strong>", text)
|
|
129
|
+
|
|
130
|
+
# 4. Italic *italic* -> <em>italic</em>
|
|
131
|
+
text = re.sub(r"(?<!\*)\*([^*]+)\*(?!\*)", r"<em>\1</em>", text)
|
|
132
|
+
|
|
133
|
+
# 5. Restore code spans
|
|
134
|
+
for idx, span in enumerate(code_spans):
|
|
135
|
+
text = text.replace(f"__INLINE_CODE_SPAN_{idx}__", span)
|
|
136
|
+
|
|
137
|
+
return text
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def convert_table(md_table_lines: list[str]) -> str:
|
|
141
|
+
"""Convert Markdown table lines to Confluence wrapped table."""
|
|
142
|
+
if len(md_table_lines) < 2:
|
|
143
|
+
return ""
|
|
144
|
+
|
|
145
|
+
header_line = md_table_lines[0].strip()
|
|
146
|
+
data_lines = md_table_lines[2:]
|
|
147
|
+
|
|
148
|
+
headers = [format_inline_markdown(c.strip()) for c in header_line.split("|")[1:-1]]
|
|
149
|
+
num_cols = len(headers)
|
|
150
|
+
|
|
151
|
+
cols_html = "".join("<col/>" for _ in range(num_cols))
|
|
152
|
+
colgroup = f"<colgroup>{cols_html}</colgroup>"
|
|
153
|
+
|
|
154
|
+
thead_cells = "".join(f"<th><p>{h}</p></th>" for h in headers)
|
|
155
|
+
thead = f"<thead><tr>{thead_cells}</tr></thead>"
|
|
156
|
+
|
|
157
|
+
rows_html = []
|
|
158
|
+
for row in data_lines:
|
|
159
|
+
row_str = row.strip()
|
|
160
|
+
if not row_str or not row_str.startswith("|"):
|
|
161
|
+
continue
|
|
162
|
+
cells = [format_inline_markdown(c.strip()) for c in row_str.split("|")[1:-1]]
|
|
163
|
+
while len(cells) < num_cols:
|
|
164
|
+
cells.append("")
|
|
165
|
+
cell_elements = [f"<td><p>{c}</p></td>" for c in cells]
|
|
166
|
+
row_html = "".join(cell_elements)
|
|
167
|
+
rows_html.append(f"<tr>{row_html}</tr>")
|
|
168
|
+
|
|
169
|
+
tbody = f"<tbody>{''.join(rows_html)}</tbody>"
|
|
170
|
+
return f'<table class="wrapped">{colgroup}{thead}{tbody}</table>'
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _parse_mermaid_flowchart(code: str) -> Optional[str]:
|
|
174
|
+
"""Dynamically converts any Mermaid flowchart into Confluence PlantUML with Material palette."""
|
|
175
|
+
lines = code.strip().splitlines()
|
|
176
|
+
subgraphs = []
|
|
177
|
+
sg_stack = []
|
|
178
|
+
top_connections = []
|
|
179
|
+
top_nodes = {}
|
|
180
|
+
|
|
181
|
+
subgraph_start_re = re.compile(r'^\s*subgraph\s+([A-Za-z0-9_]+)(?:\["([^"]+)"\]|\[([^\]]+)\])?', re.IGNORECASE)
|
|
182
|
+
direction_re = re.compile(r'^\s*direction\s+(LR|RL|TB|TD)', re.IGNORECASE)
|
|
183
|
+
node_def_re = re.compile(r'([A-Za-z0-9_]+)\["([^"]+)"\](?::::([A-Za-z0-9_]+))?|([A-Za-z0-9_]+)\[([^\]]+)\](?::::([A-Za-z0-9_]+))?')
|
|
184
|
+
|
|
185
|
+
has_flowchart = False
|
|
186
|
+
for line in lines:
|
|
187
|
+
stripped = line.strip()
|
|
188
|
+
if not stripped or stripped.startswith("%%") or stripped.startswith("classDef") or stripped.startswith("style "):
|
|
189
|
+
continue
|
|
190
|
+
if stripped.lower().startswith("flowchart") or stripped.lower().startswith("graph"):
|
|
191
|
+
has_flowchart = True
|
|
192
|
+
continue
|
|
193
|
+
|
|
194
|
+
sg_match = subgraph_start_re.match(stripped)
|
|
195
|
+
if sg_match:
|
|
196
|
+
sg_id = sg_match.group(1)
|
|
197
|
+
sg_title = sg_match.group(2) or sg_match.group(3) or sg_id
|
|
198
|
+
new_sg = {
|
|
199
|
+
"id": sg_id,
|
|
200
|
+
"title": sg_title,
|
|
201
|
+
"direction": "TB",
|
|
202
|
+
"nodes": {},
|
|
203
|
+
"edges": [],
|
|
204
|
+
"children": []
|
|
205
|
+
}
|
|
206
|
+
if sg_stack:
|
|
207
|
+
sg_stack[-1]["children"].append(new_sg)
|
|
208
|
+
else:
|
|
209
|
+
subgraphs.append(new_sg)
|
|
210
|
+
sg_stack.append(new_sg)
|
|
211
|
+
continue
|
|
212
|
+
|
|
213
|
+
if stripped.lower() == "end":
|
|
214
|
+
if sg_stack:
|
|
215
|
+
sg_stack.pop()
|
|
216
|
+
continue
|
|
217
|
+
|
|
218
|
+
dir_match = direction_re.match(stripped)
|
|
219
|
+
if dir_match and sg_stack:
|
|
220
|
+
sg_stack[-1]["direction"] = dir_match.group(1).upper()
|
|
221
|
+
continue
|
|
222
|
+
|
|
223
|
+
# 1. Extract node definitions
|
|
224
|
+
for match in node_def_re.finditer(stripped):
|
|
225
|
+
n_id = match.group(1) or match.group(4)
|
|
226
|
+
n_lbl = match.group(2) or match.group(5)
|
|
227
|
+
n_cls = match.group(3) or match.group(6) or ""
|
|
228
|
+
node_info = {"id": n_id, "label": n_lbl, "class": n_cls}
|
|
229
|
+
if sg_stack:
|
|
230
|
+
sg_stack[-1]["nodes"][n_id] = node_info
|
|
231
|
+
else:
|
|
232
|
+
top_nodes[n_id] = node_info
|
|
233
|
+
|
|
234
|
+
# 2. Clean line for edge extraction
|
|
235
|
+
def repl_node(m):
|
|
236
|
+
return m.group(1) or m.group(4)
|
|
237
|
+
clean_edge_line = node_def_re.sub(repl_node, stripped)
|
|
238
|
+
|
|
239
|
+
# 3. Extract edges
|
|
240
|
+
edge_tokens = re.split(r'(\s*(?:<-->|-->|---|->)\s*(?:\|"[^"]*"\|\s*|\|[^\|]*\|\s*)?)', clean_edge_line)
|
|
241
|
+
if len(edge_tokens) >= 3:
|
|
242
|
+
for i in range(0, len(edge_tokens) - 2, 2):
|
|
243
|
+
src = edge_tokens[i].strip()
|
|
244
|
+
arrow_token = edge_tokens[i+1].strip()
|
|
245
|
+
dst = edge_tokens[i+2].strip()
|
|
246
|
+
|
|
247
|
+
lbl_match = re.search(r'\|"?([^"\|]*)"?\|', arrow_token)
|
|
248
|
+
lbl = lbl_match.group(1).strip() if lbl_match else ""
|
|
249
|
+
|
|
250
|
+
if src and dst and re.match(r'^[A-Za-z0-9_]+$', src) and re.match(r'^[A-Za-z0-9_]+$', dst):
|
|
251
|
+
edge_obj = {"src": src, "dst": dst, "label": lbl}
|
|
252
|
+
if sg_stack:
|
|
253
|
+
sg_stack[-1]["edges"].append(edge_obj)
|
|
254
|
+
else:
|
|
255
|
+
top_connections.append(edge_obj)
|
|
256
|
+
|
|
257
|
+
# If no subgraphs or nodes discovered, return None to trigger fallback
|
|
258
|
+
if not subgraphs and not top_nodes:
|
|
259
|
+
return None
|
|
260
|
+
|
|
261
|
+
def get_node_type(node_id: str, label: str, cls: str) -> tuple:
|
|
262
|
+
id_low = node_id.lower()
|
|
263
|
+
lbl_low = label.lower()
|
|
264
|
+
cls_low = cls.lower()
|
|
265
|
+
|
|
266
|
+
# Specific classDef and archetype hints
|
|
267
|
+
if "rlhf" in cls_low or "feedback" in id_low:
|
|
268
|
+
return "database", "rlhf_db"
|
|
269
|
+
if "src" in cls_low or "source" in id_low or "derby" in lbl_low or "case_fault" in lbl_low:
|
|
270
|
+
return "database", "source_db"
|
|
271
|
+
if id_low.endswith("db") or "_db" in id_low:
|
|
272
|
+
return "database", "staging_db"
|
|
273
|
+
if "model" in cls_low or "synth" in cls_low or "llm" in id_low or "ai model" in lbl_low or "gpt" in lbl_low or "gemma" in lbl_low:
|
|
274
|
+
return "rectangle", "ai_model"
|
|
275
|
+
if "ui" in cls_low or "browser" in id_low or "dashboard" in lbl_low or "screener" in id_low or "client" in id_low or "user" in id_low:
|
|
276
|
+
return "rectangle", "user_ui"
|
|
277
|
+
if "gateway" in id_low or "apigee" in id_low or "gateway" in cls_low:
|
|
278
|
+
return "queue", "gateway"
|
|
279
|
+
if "ext" in cls_low or "telemetry" in id_low or any(k in lbl_low for k in ["telemetry", "nxt", "fcaps", "cdn"]):
|
|
280
|
+
return "rectangle", "telemetry"
|
|
281
|
+
if any(k in id_low for k in ["recoutput", "decision", "action"]) or "outstyle" in cls_low:
|
|
282
|
+
return "rectangle", "outcome"
|
|
283
|
+
|
|
284
|
+
# Check if explicitly a database/table
|
|
285
|
+
is_db = any(k in lbl_low for k in ["db:", "dbo.", "database", "table", "store", "sqlite", "sql server", "[sp_"]) or \
|
|
286
|
+
any(k in id_low for k in ["db", "table", "store"]) or \
|
|
287
|
+
any(k in cls_low for k in ["stage", "datastyle"])
|
|
288
|
+
if is_db:
|
|
289
|
+
return "database", "staging_db"
|
|
290
|
+
|
|
291
|
+
return "rectangle", "service"
|
|
292
|
+
|
|
293
|
+
out = []
|
|
294
|
+
out.append("@startuml")
|
|
295
|
+
out.append("skinparam shadowing false")
|
|
296
|
+
out.append('skinparam defaultFontName "Segoe UI"')
|
|
297
|
+
out.append("skinparam defaultFontSize 11")
|
|
298
|
+
out.append("skinparam roundCorner 6")
|
|
299
|
+
out.append("skinparam linetype ortho")
|
|
300
|
+
out.append("skinparam nodesep 80")
|
|
301
|
+
out.append("skinparam ranksep 45\n")
|
|
302
|
+
|
|
303
|
+
out.append("skinparam package {")
|
|
304
|
+
out.append(" BorderColor #546E7A")
|
|
305
|
+
out.append(" BackgroundColor #F8FAFC")
|
|
306
|
+
out.append(" FontColor #263238")
|
|
307
|
+
out.append(" FontStyle bold")
|
|
308
|
+
out.append("}\n")
|
|
309
|
+
|
|
310
|
+
out.append("skinparam database<<source_db>> {")
|
|
311
|
+
out.append(" BorderColor #3949AB")
|
|
312
|
+
out.append(" BackgroundColor #E8EAF6")
|
|
313
|
+
out.append(" FontColor #1A237E")
|
|
314
|
+
out.append("}")
|
|
315
|
+
out.append("skinparam database<<staging_db>> {")
|
|
316
|
+
out.append(" BorderColor #0277BD")
|
|
317
|
+
out.append(" BackgroundColor #E1F5FE")
|
|
318
|
+
out.append(" FontColor #01579B")
|
|
319
|
+
out.append("}")
|
|
320
|
+
out.append("skinparam rectangle<<ai_model>> {")
|
|
321
|
+
out.append(" BorderColor #6A1B9A")
|
|
322
|
+
out.append(" BackgroundColor #F3E5F5")
|
|
323
|
+
out.append(" FontColor #4A148C")
|
|
324
|
+
out.append("}")
|
|
325
|
+
out.append("skinparam rectangle<<user_ui>> {")
|
|
326
|
+
out.append(" BorderColor #E65100")
|
|
327
|
+
out.append(" BackgroundColor #FFF3E0")
|
|
328
|
+
out.append(" FontColor #BF360C")
|
|
329
|
+
out.append("}")
|
|
330
|
+
out.append("skinparam database<<rlhf_db>> {")
|
|
331
|
+
out.append(" BorderColor #2E7D32")
|
|
332
|
+
out.append(" BackgroundColor #E8F5E9")
|
|
333
|
+
out.append(" FontColor #1B5E20")
|
|
334
|
+
out.append("}")
|
|
335
|
+
out.append("skinparam rectangle<<service>> {")
|
|
336
|
+
out.append(" BorderColor #00695C")
|
|
337
|
+
out.append(" BackgroundColor #E0F2F1")
|
|
338
|
+
out.append(" FontColor #004D40")
|
|
339
|
+
out.append("}")
|
|
340
|
+
out.append("skinparam queue<<gateway>> {")
|
|
341
|
+
out.append(" BorderColor #E65100")
|
|
342
|
+
out.append(" BackgroundColor #FFF3E0")
|
|
343
|
+
out.append(" FontColor #BF360C")
|
|
344
|
+
out.append("}")
|
|
345
|
+
out.append("skinparam rectangle<<telemetry>> {")
|
|
346
|
+
out.append(" BorderColor #C2185B")
|
|
347
|
+
out.append(" BackgroundColor #FCE4EC")
|
|
348
|
+
out.append(" FontColor #880E4F")
|
|
349
|
+
out.append("}")
|
|
350
|
+
out.append("skinparam rectangle<<outcome>> {")
|
|
351
|
+
out.append(" BorderColor #2E7D32")
|
|
352
|
+
out.append(" BackgroundColor #E8F5E9")
|
|
353
|
+
out.append(" FontColor #1B5E20")
|
|
354
|
+
out.append("}\n")
|
|
355
|
+
|
|
356
|
+
def render_subgraph(sg, indent=""):
|
|
357
|
+
res = [f'{indent}package "{sg["title"]}" as {sg["id"]} {{']
|
|
358
|
+
for child in sg.get("children", []):
|
|
359
|
+
res.extend(render_subgraph(child, indent + " "))
|
|
360
|
+
for n_id, n_info in sg["nodes"].items():
|
|
361
|
+
formatted_lbl = (
|
|
362
|
+
n_info["label"]
|
|
363
|
+
.replace("<br/>", "\\n")
|
|
364
|
+
.replace("<br>", "\\n")
|
|
365
|
+
.replace("•", "-")
|
|
366
|
+
.replace('"', "'")
|
|
367
|
+
)
|
|
368
|
+
elem_type, stereotype = get_node_type(n_id, n_info["label"], n_info["class"])
|
|
369
|
+
stereo_suffix = f" <<{stereotype}>>" if stereotype else ""
|
|
370
|
+
res.append(f'{indent} {elem_type} "{formatted_lbl}" as {n_id}{stereo_suffix}')
|
|
371
|
+
|
|
372
|
+
if sg["edges"]:
|
|
373
|
+
res.append("")
|
|
374
|
+
arrow = "-right->" if sg["direction"] == "LR" else "-down->"
|
|
375
|
+
for e in sg["edges"]:
|
|
376
|
+
lbl_clean = (
|
|
377
|
+
e["label"]
|
|
378
|
+
.replace("<br/>", "\\n")
|
|
379
|
+
.replace("<br>", "\\n")
|
|
380
|
+
.replace("•", "-")
|
|
381
|
+
.replace('"', "'")
|
|
382
|
+
if e["label"]
|
|
383
|
+
else ""
|
|
384
|
+
)
|
|
385
|
+
lbl_suffix = f' : {lbl_clean}' if lbl_clean else ""
|
|
386
|
+
res.append(f'{indent} {e["src"]} {arrow} {e["dst"]}{lbl_suffix}')
|
|
387
|
+
res.append(f"{indent}}}\n")
|
|
388
|
+
return res
|
|
389
|
+
|
|
390
|
+
for sg in subgraphs:
|
|
391
|
+
out.extend(render_subgraph(sg))
|
|
392
|
+
|
|
393
|
+
for n_id, n_info in top_nodes.items():
|
|
394
|
+
formatted_lbl = (
|
|
395
|
+
n_info["label"]
|
|
396
|
+
.replace("<br/>", "\\n")
|
|
397
|
+
.replace("<br>", "\\n")
|
|
398
|
+
.replace("•", "-")
|
|
399
|
+
.replace('"', "'")
|
|
400
|
+
)
|
|
401
|
+
elem_type, stereotype = get_node_type(n_id, n_info["label"], n_info["class"])
|
|
402
|
+
stereo_suffix = f" <<{stereotype}>>" if stereotype else ""
|
|
403
|
+
out.append(f'{elem_type} "{formatted_lbl}" as {n_id}{stereo_suffix}')
|
|
404
|
+
if top_nodes:
|
|
405
|
+
out.append("")
|
|
406
|
+
|
|
407
|
+
for i in range(len(subgraphs) - 1):
|
|
408
|
+
out.append(f'{subgraphs[i]["id"]} -[hidden]down- {subgraphs[i+1]["id"]}')
|
|
409
|
+
if subgraphs:
|
|
410
|
+
out.append("")
|
|
411
|
+
|
|
412
|
+
for e in top_connections:
|
|
413
|
+
lbl_clean = (
|
|
414
|
+
e["label"]
|
|
415
|
+
.replace("<br/>", "\\n")
|
|
416
|
+
.replace("<br>", "\\n")
|
|
417
|
+
.replace("•", "-")
|
|
418
|
+
.replace('"', "'")
|
|
419
|
+
if e["label"]
|
|
420
|
+
else ""
|
|
421
|
+
)
|
|
422
|
+
lbl_suffix = f' : {lbl_clean}' if lbl_clean else ""
|
|
423
|
+
out.append(f'{e["src"]} -down-> {e["dst"]}{lbl_suffix}')
|
|
424
|
+
|
|
425
|
+
out.append("\n@enduml")
|
|
426
|
+
return "\n".join(out)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _parse_mermaid_sequence_diagram(code: str) -> Optional[str]:
|
|
430
|
+
"""Dynamically parse a Mermaid sequenceDiagram into Corporate PlantUML."""
|
|
431
|
+
lines = [line.strip() for line in code.splitlines() if line.strip()]
|
|
432
|
+
if not any("sequenceDiagram" in l for l in lines):
|
|
433
|
+
return None
|
|
434
|
+
|
|
435
|
+
out = []
|
|
436
|
+
out.append("@startuml")
|
|
437
|
+
out.append("autonumber")
|
|
438
|
+
out.append("skinparam shadowing false")
|
|
439
|
+
out.append('skinparam defaultFontName "Segoe UI"')
|
|
440
|
+
out.append("skinparam defaultFontSize 11")
|
|
441
|
+
out.append("skinparam roundCorner 6\n")
|
|
442
|
+
out.append("skinparam actor {")
|
|
443
|
+
out.append(" BorderColor #3949AB")
|
|
444
|
+
out.append(" BackgroundColor #E8EAF6")
|
|
445
|
+
out.append(" FontColor #1A237E")
|
|
446
|
+
out.append("}")
|
|
447
|
+
out.append("skinparam participant {")
|
|
448
|
+
out.append(" BorderColor #455A64")
|
|
449
|
+
out.append(" BackgroundColor #F8FAFC")
|
|
450
|
+
out.append(" FontColor #263238")
|
|
451
|
+
out.append("}")
|
|
452
|
+
out.append("skinparam database {")
|
|
453
|
+
out.append(" BorderColor #0277BD")
|
|
454
|
+
out.append(" BackgroundColor #E1F5FE")
|
|
455
|
+
out.append(" FontColor #01579B")
|
|
456
|
+
out.append("}")
|
|
457
|
+
out.append("skinparam queue {")
|
|
458
|
+
out.append(" BorderColor #E65100")
|
|
459
|
+
out.append(" BackgroundColor #FFF3E0")
|
|
460
|
+
out.append(" FontColor #BF360C")
|
|
461
|
+
out.append("}")
|
|
462
|
+
out.append("skinparam sequence {")
|
|
463
|
+
out.append(" ArrowColor #1E88E5")
|
|
464
|
+
out.append(" LifeLineBorderColor #90A4AE")
|
|
465
|
+
out.append(" LifeLineBackgroundColor #ECEFF1")
|
|
466
|
+
out.append(" DividerBorderColor #B0BEC5")
|
|
467
|
+
out.append(" DividerBackgroundColor #ECEFF1")
|
|
468
|
+
out.append(" DividerFontColor #263238")
|
|
469
|
+
out.append("}\n")
|
|
470
|
+
|
|
471
|
+
for line in lines:
|
|
472
|
+
if line.startswith("%%") or line == "sequenceDiagram" or line == "autonumber":
|
|
473
|
+
continue
|
|
474
|
+
|
|
475
|
+
# Check for actor / participant / database / queue
|
|
476
|
+
part_match = re.match(
|
|
477
|
+
r'^(actor|participant|database|queue)\s+([A-Za-z0-9_]+)(?:\s+as\s+(.*))?$',
|
|
478
|
+
line,
|
|
479
|
+
re.IGNORECASE,
|
|
480
|
+
)
|
|
481
|
+
if part_match:
|
|
482
|
+
kind = part_match.group(1).lower()
|
|
483
|
+
p_id = part_match.group(2)
|
|
484
|
+
label = part_match.group(3) or p_id
|
|
485
|
+
label_clean = (
|
|
486
|
+
label.strip('"')
|
|
487
|
+
.strip("'")
|
|
488
|
+
.replace("<br/>", "\\n")
|
|
489
|
+
.replace("<br>", "\\n")
|
|
490
|
+
)
|
|
491
|
+
# Smart element type inference
|
|
492
|
+
if kind == "participant":
|
|
493
|
+
if (
|
|
494
|
+
p_id.lower() == "db"
|
|
495
|
+
or "database" in label_clean.lower()
|
|
496
|
+
or "sql server" in label_clean.lower()
|
|
497
|
+
):
|
|
498
|
+
kind = "database"
|
|
499
|
+
elif "queue" in label_clean.lower() or "gateway" in label_clean.lower():
|
|
500
|
+
kind = "queue"
|
|
501
|
+
out.append(f'{kind} "{label_clean}" as {p_id}')
|
|
502
|
+
continue
|
|
503
|
+
|
|
504
|
+
# Check for Note over A,B: Text OR Note over A: Text
|
|
505
|
+
note_match = re.match(
|
|
506
|
+
r'^[Nn]ote\s+(over|left of|right of)\s+([A-Za-z0-9_,\s]+):\s*(.*)$',
|
|
507
|
+
line,
|
|
508
|
+
)
|
|
509
|
+
if note_match:
|
|
510
|
+
placement = note_match.group(1).lower()
|
|
511
|
+
targets = note_match.group(2).strip()
|
|
512
|
+
text = note_match.group(3).strip()
|
|
513
|
+
text_clean = text.replace("<br/>", "\\n").replace("<br>", "\\n")
|
|
514
|
+
|
|
515
|
+
# Major phase dividers vs in-place notes
|
|
516
|
+
if re.match(r'^\d+\.\s+', text_clean):
|
|
517
|
+
out.append(f"\n== {text_clean} ==")
|
|
518
|
+
else:
|
|
519
|
+
out.append(f"note {placement} {targets} : {text_clean}")
|
|
520
|
+
continue
|
|
521
|
+
|
|
522
|
+
# Check for message arrows
|
|
523
|
+
msg_match = re.match(
|
|
524
|
+
r'^([A-Za-z0-9_]+)\s*(-->>|->>|-->|->)\s*([A-Za-z0-9_]+)\s*:\s*(.*)$',
|
|
525
|
+
line,
|
|
526
|
+
)
|
|
527
|
+
if msg_match:
|
|
528
|
+
src = msg_match.group(1)
|
|
529
|
+
arrow_token = msg_match.group(2)
|
|
530
|
+
dst = msg_match.group(3)
|
|
531
|
+
msg_text = msg_match.group(4).strip()
|
|
532
|
+
msg_clean = msg_text.replace("<br/>", "\\n").replace("<br>", "\\n")
|
|
533
|
+
arrow = "-->" if "--" in arrow_token else "->"
|
|
534
|
+
out.append(f"{src} {arrow} {dst} : {msg_clean}")
|
|
535
|
+
continue
|
|
536
|
+
|
|
537
|
+
out.append("\n@enduml")
|
|
538
|
+
return "\n".join(out)
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def get_plantuml_code(code_clean: str, feature_slug: str) -> str:
|
|
542
|
+
"""Return matching clean PlantUML code with corporate Material Design styling."""
|
|
543
|
+
# If the markdown block already contains valid PlantUML, pass it through directly
|
|
544
|
+
if code_clean.startswith("@startuml"):
|
|
545
|
+
return code_clean
|
|
546
|
+
|
|
547
|
+
# 1. Dynamic Mermaid Sequence Diagram parsing
|
|
548
|
+
if "sequenceDiagram" in code_clean:
|
|
549
|
+
parsed_seq = _parse_mermaid_sequence_diagram(code_clean)
|
|
550
|
+
if parsed_seq:
|
|
551
|
+
return parsed_seq
|
|
552
|
+
# Fallback to legacy static sequence diagram
|
|
553
|
+
return """@startuml
|
|
554
|
+
autonumber
|
|
555
|
+
skinparam shadowing false
|
|
556
|
+
skinparam defaultFontName "Segoe UI"
|
|
557
|
+
skinparam defaultFontSize 12
|
|
558
|
+
skinparam roundCorner 6
|
|
559
|
+
|
|
560
|
+
skinparam actor {
|
|
561
|
+
BorderColor #3949AB
|
|
562
|
+
BackgroundColor #E8EAF6
|
|
563
|
+
FontColor #1A237E
|
|
564
|
+
}
|
|
565
|
+
skinparam participant {
|
|
566
|
+
BorderColor #455A64
|
|
567
|
+
BackgroundColor #F8FAFC
|
|
568
|
+
FontColor #263238
|
|
569
|
+
}
|
|
570
|
+
skinparam sequence {
|
|
571
|
+
ArrowColor #1E88E5
|
|
572
|
+
LifeLineBorderColor #90A4AE
|
|
573
|
+
LifeLineBackgroundColor #ECEFF1
|
|
574
|
+
DividerBorderColor #B0BEC5
|
|
575
|
+
DividerBackgroundColor #ECEFF1
|
|
576
|
+
DividerFontColor #263238
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
actor "Screener / Operations User" as User
|
|
580
|
+
participant "User Web Browser\\n(React)" as UI
|
|
581
|
+
participant "Backend Service\\n(FastAPI)" as API
|
|
582
|
+
participant "Diagnostics Worker" as Worker
|
|
583
|
+
database "SQL Server Database" as DB
|
|
584
|
+
queue "Apigee API Gateway" as Apigee
|
|
585
|
+
participant "Network & Outage APIs" as Telemetry
|
|
586
|
+
participant "Language Model Service" as Model
|
|
587
|
+
|
|
588
|
+
== 1. Automated Telemetry Ingestion & Analysis ==
|
|
589
|
+
Worker -> DB: Query scheduled appointments & customer history
|
|
590
|
+
DB --> Worker: Return pending appointment records
|
|
591
|
+
Worker -> Apigee: Request device metrics & outage data (OAuth2)
|
|
592
|
+
Apigee -> Telemetry: Call SAA NXT, FCAPS, CDN, Outage APIs
|
|
593
|
+
Telemetry --> Apigee: Return raw JSON telemetry
|
|
594
|
+
Apigee --> Worker: Return authenticated telemetry payloads
|
|
595
|
+
Worker -> Worker: Evaluate physical threshold rules
|
|
596
|
+
Worker -> Worker: Compile Network, Case & Account Summaries
|
|
597
|
+
Worker -> Model: Send combined summaries for recommendation
|
|
598
|
+
Model --> Worker: Return structured triage recommendation
|
|
599
|
+
Worker -> DB: Store diagnostics, summaries & recommendations
|
|
600
|
+
|
|
601
|
+
== 2. Screener Review & Queue Management ==
|
|
602
|
+
User -> UI: Open appointment screening queue
|
|
603
|
+
UI -> API: GET /api/cases (with date & filter criteria)
|
|
604
|
+
API -> DB: Query screened cases, health indicators & summaries
|
|
605
|
+
DB --> API: Return case datasets and diagnostics
|
|
606
|
+
API --> UI: Return cases, health scores, and summaries to queue view
|
|
607
|
+
|
|
608
|
+
== 3. Decision Capture via Feedback Service ==
|
|
609
|
+
User -> UI: Select case, review recommendation & decide action
|
|
610
|
+
User -> UI: Submit decision (Keep, Cancel, Override, Defer) with notes
|
|
611
|
+
UI -> API: POST /api/cases/{id}/decision (Feedback Service)
|
|
612
|
+
API -> DB: Record screener decision, notes & audit metadata into AppScreen_Case_Feedback
|
|
613
|
+
DB --> API: Confirm write operation
|
|
614
|
+
API --> UI: Return updated case state and confirmation
|
|
615
|
+
UI --> User: Display updated status and audit record
|
|
616
|
+
|
|
617
|
+
@enduml"""
|
|
618
|
+
|
|
619
|
+
# 2. Dynamic Mermaid Flowchart parsing with Orthogonal Routing & Material Design
|
|
620
|
+
parsed_uml = _parse_mermaid_flowchart(code_clean)
|
|
621
|
+
if parsed_uml:
|
|
622
|
+
return parsed_uml
|
|
623
|
+
|
|
624
|
+
# 3. End-to-End Database and Data Processing Pipeline Diagram (Static Fallback)
|
|
625
|
+
elif "CASE_FAULTS" in code_clean or "Pipeline" in code_clean or "rlhfStyle" in code_clean:
|
|
626
|
+
return """@startuml
|
|
627
|
+
skinparam shadowing false
|
|
628
|
+
skinparam defaultFontName "Segoe UI"
|
|
629
|
+
skinparam defaultFontSize 11
|
|
630
|
+
skinparam roundCorner 6
|
|
631
|
+
skinparam linetype ortho
|
|
632
|
+
skinparam nodesep 80
|
|
633
|
+
skinparam ranksep 45
|
|
634
|
+
|
|
635
|
+
skinparam package {
|
|
636
|
+
BorderColor #546E7A
|
|
637
|
+
BackgroundColor #F8FAFC
|
|
638
|
+
FontColor #263238
|
|
639
|
+
FontStyle bold
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
skinparam database<<source_db>> {
|
|
643
|
+
BorderColor #3949AB
|
|
644
|
+
BackgroundColor #E8EAF6
|
|
645
|
+
FontColor #1A237E
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
skinparam database<<staging_db>> {
|
|
649
|
+
BorderColor #0277BD
|
|
650
|
+
BackgroundColor #E1F5FE
|
|
651
|
+
FontColor #01579B
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
skinparam rectangle<<ai_model>> {
|
|
655
|
+
BorderColor #6A1B9A
|
|
656
|
+
BackgroundColor #F3E5F5
|
|
657
|
+
FontColor #4A148C
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
skinparam rectangle<<user_ui>> {
|
|
661
|
+
BorderColor #E65100
|
|
662
|
+
BackgroundColor #FFF3E0
|
|
663
|
+
FontColor #BF360C
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
skinparam database<<rlhf_db>> {
|
|
667
|
+
BorderColor #2E7D32
|
|
668
|
+
BackgroundColor #E8F5E9
|
|
669
|
+
FontColor #1B5E20
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
package "Layer 1: Upstream Ingestion & Telemetry Enrichment" as L1 {
|
|
673
|
+
database "**Host: IEROXAPP2**\\n**DB: DERBY**\\n`dbo.CASE_FAULTS & CFY_CASES`\\nClarify Trouble Tickets\\n[sp_CASE_FAULTS on IEROXAPP2]" as SourceDB <<source_db>>
|
|
674
|
+
database "**Host: DBUATL01**\\n**DB: Customer_FeedBack_JIT**\\n`dbo.AppScreen_Pending_Calls`\\nPending Appointments Queue\\n[Ingestion ETL via Linked Server]" as PendingDB <<staging_db>>
|
|
675
|
+
database "**Host: DBUATL01**\\n**DB: Customer_FeedBack_JIT**\\n`dbo.AppScreen_Case_Diagnostics`\\nEnriched Telemetry & RAG Status\\n[Diagnostics Worker on App Host]" as DiagDB <<staging_db>>
|
|
676
|
+
|
|
677
|
+
SourceDB -right-> PendingDB : 1. Ingest
|
|
678
|
+
PendingDB -right-> DiagDB : 2. Enrich
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
package "Layer 2: AI Multi-Track Inference & Recommendation" as L2 {
|
|
682
|
+
rectangle "**Host: App Server / Azure**\\n**AI Model Engine (GPT-4o)**\\n3-Track Summarization & Triage\\n[LLM Service on App Host]" as LLM <<ai_model>>
|
|
683
|
+
database "**Host: DBUATL01**\\n**DB: Customer_FeedBack_JIT**\\n`dbo.AppScreen_Case_Recommendation`\\nRecommendations & Confidence\\n[save_recommendations on App Host]" as RecDB <<staging_db>>
|
|
684
|
+
|
|
685
|
+
LLM -right-> RecDB : 4. Predict
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
package "Layer 3: Screener Review, Locking & RLHF Feedback" as L3 {
|
|
689
|
+
rectangle "**Host: Browser / App Server**\\n**Dispatcher Web Dashboard**\\nScreener Review & Advisory Locks\\n[`dbo.AppScreen_Case_Assignment`]" as UI <<user_ui>>
|
|
690
|
+
database "**Host: DBUATL01**\\n**DB: Customer_FeedBack_JIT**\\n`dbo.AppScreen_Case_Feedback`\\nScreener Decisions & RLHF Ground Truth\\n[FastAPI /api/cases/{id}/decision]" as FeedbackDB <<rlhf_db>>
|
|
691
|
+
|
|
692
|
+
UI -right-> FeedbackDB : 6. Feedback
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
L1 -[hidden]down- L2
|
|
696
|
+
L2 -[hidden]down- L3
|
|
697
|
+
|
|
698
|
+
DiagDB -down-> LLM : 3. Prompt Features
|
|
699
|
+
RecDB -down-> UI : 5. Present to Queue
|
|
700
|
+
|
|
701
|
+
@enduml"""
|
|
702
|
+
|
|
703
|
+
# 3. Summarization and Recommendation Pipeline Diagram
|
|
704
|
+
elif "Summarization" in code_clean or "Synthesis" in code_clean:
|
|
705
|
+
return """@startuml
|
|
706
|
+
skinparam shadowing false
|
|
707
|
+
skinparam defaultFontName "Segoe UI"
|
|
708
|
+
skinparam defaultFontSize 11
|
|
709
|
+
skinparam roundCorner 6
|
|
710
|
+
skinparam linetype ortho
|
|
711
|
+
skinparam nodesep 50
|
|
712
|
+
skinparam ranksep 35
|
|
713
|
+
|
|
714
|
+
skinparam package {
|
|
715
|
+
BorderColor #546E7A
|
|
716
|
+
BackgroundColor #F8FAFC
|
|
717
|
+
FontColor #263238
|
|
718
|
+
FontStyle bold
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
skinparam rectangle<<track1>> {
|
|
722
|
+
BorderColor #00695C
|
|
723
|
+
BackgroundColor #E0F2F1
|
|
724
|
+
FontColor #004D40
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
skinparam rectangle<<track2>> {
|
|
728
|
+
BorderColor #E65100
|
|
729
|
+
BackgroundColor #FFF3E0
|
|
730
|
+
FontColor #BF360C
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
skinparam rectangle<<track3>> {
|
|
734
|
+
BorderColor #3949AB
|
|
735
|
+
BackgroundColor #E8EAF6
|
|
736
|
+
FontColor #1A237E
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
skinparam rectangle<<synthesis>> {
|
|
740
|
+
BorderColor #6A1B9A
|
|
741
|
+
BackgroundColor #F3E5F5
|
|
742
|
+
FontColor #4A148C
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
skinparam rectangle<<outcome>> {
|
|
746
|
+
BorderColor #2E7D32
|
|
747
|
+
BackgroundColor #E8F5E9
|
|
748
|
+
FontColor #1B5E20
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
package "Stage 1: Multi-Track Data Analysis" as Inputs {
|
|
752
|
+
package "Track A: Network Health" as TrackA {
|
|
753
|
+
rectangle "1. SAA NXT, FCAPS, CDN Telemetry" as N1 <<track1>>
|
|
754
|
+
rectangle "2. Threshold Rules & Outage Check" as N2 <<track1>>
|
|
755
|
+
rectangle "**Network Health Summary**" as NSum <<track1>>
|
|
756
|
+
N1 -right-> N2
|
|
757
|
+
N2 -right-> NSum
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
package "Track B: Case History" as TrackB {
|
|
761
|
+
rectangle "1. 30-Day CRM Interactions" as C1 <<track2>>
|
|
762
|
+
rectangle "2. Repeat Fault Clustering" as C2 <<track2>>
|
|
763
|
+
rectangle "**Customer Case Summary**" as CSum <<track2>>
|
|
764
|
+
C1 -right-> C2
|
|
765
|
+
C2 -right-> CSum
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
package "Track C: Account Profile" as TrackC {
|
|
769
|
+
rectangle "1. Equipment Inventory (CPE)" as A1 <<track3>>
|
|
770
|
+
rectangle "2. Subscribed Services & Plans" as A2 <<track3>>
|
|
771
|
+
rectangle "**Account Profile Summary**" as ASum <<track3>>
|
|
772
|
+
A1 -right-> A2
|
|
773
|
+
A2 -right-> ASum
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
package "Stage 2: AI Synthesis" as Synthesis {
|
|
778
|
+
rectangle "**Language Model Synthesis Engine**\\nEvaluates Network, Case, and Account Summaries" as LLMEngine <<synthesis>>
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
package "Stage 3: Decision Support" as Decision {
|
|
782
|
+
rectangle "**Action Recommendation & Justification**\\nKeep / Cancel / Defer / Tech Visit" as RecOutput <<outcome>>
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
TrackA -[hidden]down- TrackB
|
|
786
|
+
TrackB -[hidden]down- TrackC
|
|
787
|
+
|
|
788
|
+
NSum -right-> LLMEngine : Line Quality
|
|
789
|
+
CSum -right-> LLMEngine : Case History
|
|
790
|
+
ASum -right-> LLMEngine : Equipment
|
|
791
|
+
|
|
792
|
+
LLMEngine -right-> RecOutput : Recommended Action
|
|
793
|
+
|
|
794
|
+
@enduml"""
|
|
795
|
+
|
|
796
|
+
# 4. Architecture Topology Diagram
|
|
797
|
+
else:
|
|
798
|
+
return """@startuml
|
|
799
|
+
skinparam shadowing false
|
|
800
|
+
skinparam defaultFontName "Segoe UI"
|
|
801
|
+
skinparam defaultFontSize 11
|
|
802
|
+
skinparam roundCorner 6
|
|
803
|
+
skinparam linetype ortho
|
|
804
|
+
skinparam nodesep 60
|
|
805
|
+
skinparam ranksep 50
|
|
806
|
+
|
|
807
|
+
skinparam package {
|
|
808
|
+
BorderColor #455A64
|
|
809
|
+
BackgroundColor #F8FAFC
|
|
810
|
+
FontColor #263238
|
|
811
|
+
FontStyle bold
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
skinparam rectangle<<user>> {
|
|
815
|
+
BorderColor #3949AB
|
|
816
|
+
BackgroundColor #E8EAF6
|
|
817
|
+
FontColor #1A237E
|
|
818
|
+
}
|
|
819
|
+
skinparam rectangle<<proxy>> {
|
|
820
|
+
BorderColor #455A64
|
|
821
|
+
BackgroundColor #ECEFF1
|
|
822
|
+
FontColor #263238
|
|
823
|
+
}
|
|
824
|
+
skinparam rectangle<<service>> {
|
|
825
|
+
BorderColor #00695C
|
|
826
|
+
BackgroundColor #E0F2F1
|
|
827
|
+
FontColor #004D40
|
|
828
|
+
}
|
|
829
|
+
skinparam queue<<gateway>> {
|
|
830
|
+
BorderColor #E65100
|
|
831
|
+
BackgroundColor #FFF3E0
|
|
832
|
+
FontColor #BF360C
|
|
833
|
+
}
|
|
834
|
+
skinparam database<<db>> {
|
|
835
|
+
BorderColor #0277BD
|
|
836
|
+
BackgroundColor #E1F5FE
|
|
837
|
+
FontColor #01579B
|
|
838
|
+
}
|
|
839
|
+
skinparam rectangle<<telemetry>> {
|
|
840
|
+
BorderColor #C2185B
|
|
841
|
+
BackgroundColor #FCE4EC
|
|
842
|
+
FontColor #880E4F
|
|
843
|
+
}
|
|
844
|
+
skinparam rectangle<<model>> {
|
|
845
|
+
BorderColor #6A1B9A
|
|
846
|
+
BackgroundColor #F3E5F5
|
|
847
|
+
FontColor #4A148C
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
package "User Access Layer" as UserLayer {
|
|
851
|
+
rectangle "**User Web Browser**\\nDispatch & Screening Operations" as Browser <<user>>
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
package "Application Server (Internal Windows Server Host)" as HostLayer {
|
|
855
|
+
rectangle "**IIS Web Server**\\nReverse Proxy & Auth Gate" as IIS <<proxy>>
|
|
856
|
+
rectangle "**Static Web Assets**\\nCompiled React / Vite SPA" as UI <<service>>
|
|
857
|
+
rectangle "**FastAPI Backend Service**\\nREST APIs & Feedback Service (:8203)" as API <<service>>
|
|
858
|
+
rectangle "**Diagnostics Worker Engine**\\nScheduled Polling & Pipeline Rules" as Worker <<service>>
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
package "Artificial Intelligence Services" as ModelLayer {
|
|
862
|
+
rectangle "**Language Model Service**\\nSummarization & Triage Inference" as LLM <<model>>
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
package "Integration & Middleware Layer" as MidLayer {
|
|
866
|
+
database "**Configuration Store**\\nSQLite" as SQLite <<db>>
|
|
867
|
+
database "**SQL Server Database**\\nCases & Feedback Tables" as SQL <<db>>
|
|
868
|
+
queue "**Apigee API Gateway**\\nOAuth2 Token Management & Routing" as Apigee <<gateway>>
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
package "Core Telemetry APIs (via Apigee Gateway)" as TelemetryLayer {
|
|
872
|
+
rectangle "**SAA NXT APIs**\\nCable Modem HFC Diagnostics" as SAA <<telemetry>>
|
|
873
|
+
rectangle "**FCAPS Health API**\\nFiber ONT & IPTV Metrics" as FCAPS <<telemetry>>
|
|
874
|
+
rectangle "**Device Mgmt v3 API**\\nSet-Top Box Diagnostics" as CDN <<telemetry>>
|
|
875
|
+
rectangle "**Outage Checker APIs**\\nIE MSP & ServiceNow Incidents" as Outage <<telemetry>>
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
Browser -down-> IIS : HTTPS / Win Auth
|
|
879
|
+
IIS -right-> UI
|
|
880
|
+
IIS -down-> API : /api/*
|
|
881
|
+
|
|
882
|
+
API -down-> SQLite : Config DB
|
|
883
|
+
API -down-> SQL : Read / Feedback
|
|
884
|
+
|
|
885
|
+
Worker -down-> SQL : Poll Pending & Store
|
|
886
|
+
Worker -right-> LLM : HTTPS
|
|
887
|
+
Worker -down-> Apigee : Telemetry Auth
|
|
888
|
+
|
|
889
|
+
Apigee -down-> SAA : HFC Metrics
|
|
890
|
+
Apigee -down-> FCAPS : Fiber Metrics
|
|
891
|
+
Apigee -down-> CDN : CDM Metrics
|
|
892
|
+
Apigee -down-> Outage : Incident Records
|
|
893
|
+
|
|
894
|
+
@enduml"""
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
def convert_diagram_to_plantuml_macro(code: str, feature_slug: str) -> str:
|
|
898
|
+
"""Wrap PlantUML diagram in native Confluence storage format macro."""
|
|
899
|
+
code = sanitize_ai_slop_and_punctuation(code)
|
|
900
|
+
puml = get_plantuml_code(code.strip(), feature_slug)
|
|
901
|
+
puml = sanitize_ai_slop_and_punctuation(puml)
|
|
902
|
+
return (
|
|
903
|
+
f'<p><ac:structured-macro ac:name="plantuml" ac:schema-version="1">\n'
|
|
904
|
+
f'<ac:parameter ac:name="atlassian-macro-output-type">INLINE</ac:parameter>\n'
|
|
905
|
+
f'<ac:plain-text-body><![CDATA[{puml}]]></ac:plain-text-body>\n'
|
|
906
|
+
f'</ac:structured-macro></p>'
|
|
907
|
+
)
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def markdown_to_confluence_xhtml(
|
|
911
|
+
md_text: str,
|
|
912
|
+
feature_slug: str = "system",
|
|
913
|
+
) -> str:
|
|
914
|
+
"""Convert full Markdown text to Confluence Storage Format XHTML."""
|
|
915
|
+
cleaned = strip_frontmatter(md_text)
|
|
916
|
+
cleaned = strip_references_and_ai_metadata(cleaned)
|
|
917
|
+
cleaned = sanitize_ai_slop_and_punctuation(cleaned)
|
|
918
|
+
|
|
919
|
+
lines = cleaned.splitlines()
|
|
920
|
+
output = []
|
|
921
|
+
i = 0
|
|
922
|
+
in_diagram = False
|
|
923
|
+
diagram_lines = []
|
|
924
|
+
|
|
925
|
+
in_table = False
|
|
926
|
+
table_lines = []
|
|
927
|
+
|
|
928
|
+
list_stack = [] # stack of (indent_level, list_tag) e.g. (0, 'ul')
|
|
929
|
+
|
|
930
|
+
def close_lists_to_indent(target_indent: int):
|
|
931
|
+
while list_stack and list_stack[-1][0] >= target_indent:
|
|
932
|
+
_, tag = list_stack.pop()
|
|
933
|
+
output.append(f"</{tag}>")
|
|
934
|
+
|
|
935
|
+
def close_all_lists():
|
|
936
|
+
while list_stack:
|
|
937
|
+
_, tag = list_stack.pop()
|
|
938
|
+
output.append(f"</{tag}>")
|
|
939
|
+
|
|
940
|
+
while i < len(lines):
|
|
941
|
+
line = lines[i]
|
|
942
|
+
stripped = line.strip()
|
|
943
|
+
|
|
944
|
+
# Handle Diagram code fence (mermaid or plantuml)
|
|
945
|
+
if stripped.startswith("```mermaid") or stripped.startswith("```plantuml"):
|
|
946
|
+
close_all_lists()
|
|
947
|
+
in_diagram = True
|
|
948
|
+
diagram_lines = []
|
|
949
|
+
i += 1
|
|
950
|
+
continue
|
|
951
|
+
elif in_diagram:
|
|
952
|
+
if stripped.startswith("```"):
|
|
953
|
+
in_diagram = False
|
|
954
|
+
diagram_code = "\n".join(diagram_lines)
|
|
955
|
+
converted_macro = convert_diagram_to_plantuml_macro(diagram_code, feature_slug)
|
|
956
|
+
output.append(converted_macro)
|
|
957
|
+
else:
|
|
958
|
+
diagram_lines.append(line)
|
|
959
|
+
i += 1
|
|
960
|
+
continue
|
|
961
|
+
|
|
962
|
+
# Handle regular code block
|
|
963
|
+
if stripped.startswith("```"):
|
|
964
|
+
close_all_lists()
|
|
965
|
+
code_block = []
|
|
966
|
+
i += 1
|
|
967
|
+
while i < len(lines) and not lines[i].strip().startswith("```"):
|
|
968
|
+
code_block.append(lines[i])
|
|
969
|
+
i += 1
|
|
970
|
+
i += 1
|
|
971
|
+
code_content = "\n".join(code_block)
|
|
972
|
+
output.append(
|
|
973
|
+
f'<ac:structured-macro ac:name="code" ac:schema-version="1">'
|
|
974
|
+
f"<ac:plain-text-body><![CDATA[{code_content}]]></ac:plain-text-body>"
|
|
975
|
+
f"</ac:structured-macro>"
|
|
976
|
+
)
|
|
977
|
+
continue
|
|
978
|
+
|
|
979
|
+
# Handle Markdown table
|
|
980
|
+
if stripped.startswith("|") and stripped.endswith("|"):
|
|
981
|
+
close_all_lists()
|
|
982
|
+
in_table = True
|
|
983
|
+
table_lines.append(stripped)
|
|
984
|
+
i += 1
|
|
985
|
+
continue
|
|
986
|
+
elif in_table:
|
|
987
|
+
in_table = False
|
|
988
|
+
output.append(convert_table(table_lines))
|
|
989
|
+
table_lines = []
|
|
990
|
+
|
|
991
|
+
# Empty lines
|
|
992
|
+
if not stripped:
|
|
993
|
+
i += 1
|
|
994
|
+
continue
|
|
995
|
+
|
|
996
|
+
# Horizontal rule
|
|
997
|
+
if stripped in ("---", "___", "***"):
|
|
998
|
+
close_all_lists()
|
|
999
|
+
output.append("<hr/>")
|
|
1000
|
+
i += 1
|
|
1001
|
+
continue
|
|
1002
|
+
|
|
1003
|
+
# Table of contents placeholder
|
|
1004
|
+
if re.match(r"^#{1,3}\s+Table of Contents\b", stripped, re.IGNORECASE):
|
|
1005
|
+
close_all_lists()
|
|
1006
|
+
output.append("<h2>Table of Contents</h2>")
|
|
1007
|
+
output.append(
|
|
1008
|
+
'<p><ac:structured-macro ac:name="toc" ac:schema-version="1"/></p>'
|
|
1009
|
+
)
|
|
1010
|
+
output.append("<hr/>")
|
|
1011
|
+
# Skip subsequent lines until next header
|
|
1012
|
+
i += 1
|
|
1013
|
+
while i < len(lines) and not re.match(r"^(#{1,6})\s+", lines[i].strip()):
|
|
1014
|
+
i += 1
|
|
1015
|
+
continue
|
|
1016
|
+
|
|
1017
|
+
# Headers
|
|
1018
|
+
header_match = re.match(r"^(#{1,6})\s+(.*)$", stripped)
|
|
1019
|
+
if header_match:
|
|
1020
|
+
close_all_lists()
|
|
1021
|
+
level = len(header_match.group(1))
|
|
1022
|
+
htext = format_inline_markdown(header_match.group(2).strip())
|
|
1023
|
+
if level == 1:
|
|
1024
|
+
output.append(f"<h1>{htext}</h1>")
|
|
1025
|
+
else:
|
|
1026
|
+
output.append(f"<h{level}>{htext}</h{level}>")
|
|
1027
|
+
i += 1
|
|
1028
|
+
continue
|
|
1029
|
+
|
|
1030
|
+
# Blockquote / Notice (check for banned notices)
|
|
1031
|
+
if stripped.startswith(">"):
|
|
1032
|
+
close_all_lists()
|
|
1033
|
+
raw_notice = stripped.lstrip(">").strip()
|
|
1034
|
+
lower_notice = raw_notice.lower()
|
|
1035
|
+
if any(k in lower_notice for k in ["document notice", "simplified technical english", "asd-ste100", "active voice"]):
|
|
1036
|
+
i += 1
|
|
1037
|
+
continue
|
|
1038
|
+
notice_text = format_inline_markdown(raw_notice)
|
|
1039
|
+
output.append(f"<p><em>{notice_text}</em></p>")
|
|
1040
|
+
i += 1
|
|
1041
|
+
continue
|
|
1042
|
+
|
|
1043
|
+
# List items (unordered or ordered)
|
|
1044
|
+
indent = len(line) - len(line.lstrip(" "))
|
|
1045
|
+
ul_match = re.match(r"^[-*]\s+(.*)$", stripped)
|
|
1046
|
+
ol_match = re.match(r"^\d+\.\s+(.*)$", stripped)
|
|
1047
|
+
|
|
1048
|
+
if ul_match or ol_match:
|
|
1049
|
+
list_tag = "ul" if ul_match else "ol"
|
|
1050
|
+
item_text = format_inline_markdown((ul_match or ol_match).group(1).strip())
|
|
1051
|
+
|
|
1052
|
+
# Manage list nesting
|
|
1053
|
+
if not list_stack or indent > list_stack[-1][0]:
|
|
1054
|
+
list_stack.append((indent, list_tag))
|
|
1055
|
+
output.append(f"<{list_tag}>")
|
|
1056
|
+
elif indent < list_stack[-1][0]:
|
|
1057
|
+
close_lists_to_indent(indent)
|
|
1058
|
+
if not list_stack or list_stack[-1][0] < indent:
|
|
1059
|
+
list_stack.append((indent, list_tag))
|
|
1060
|
+
output.append(f"<{list_tag}>")
|
|
1061
|
+
|
|
1062
|
+
output.append(f" <li>{item_text}</li>")
|
|
1063
|
+
i += 1
|
|
1064
|
+
continue
|
|
1065
|
+
else:
|
|
1066
|
+
close_all_lists()
|
|
1067
|
+
|
|
1068
|
+
# Regular paragraph
|
|
1069
|
+
p_text = format_inline_markdown(stripped)
|
|
1070
|
+
output.append(f"<p>{p_text}</p>")
|
|
1071
|
+
i += 1
|
|
1072
|
+
|
|
1073
|
+
close_all_lists()
|
|
1074
|
+
if in_table:
|
|
1075
|
+
output.append(convert_table(table_lines))
|
|
1076
|
+
|
|
1077
|
+
return "\n".join(output)
|
|
1078
|
+
|
|
1079
|
+
|
|
1080
|
+
def validate_confluence_xhtml(xhtml_content: str) -> bool:
|
|
1081
|
+
"""Validate that the XHTML is well-formed XML compatible with Confluence."""
|
|
1082
|
+
wrapped = (
|
|
1083
|
+
f'<root xmlns:ac="http://www.atlassian.com/schema/confluence/4/ac/" '
|
|
1084
|
+
f'xmlns:ri="http://www.atlassian.com/schema/confluence/4/ri/">'
|
|
1085
|
+
f"{xhtml_content}"
|
|
1086
|
+
f"</root>"
|
|
1087
|
+
)
|
|
1088
|
+
try:
|
|
1089
|
+
ET.fromstring(wrapped)
|
|
1090
|
+
return True
|
|
1091
|
+
except ET.ParseError as e:
|
|
1092
|
+
print(f"XHTML validation error: {e}", file=sys.stderr)
|
|
1093
|
+
return False
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
def main():
|
|
1097
|
+
parser = argparse.ArgumentParser(
|
|
1098
|
+
description="Convert HLA Markdown to Confluence Storage Format XHTML with PlantUML macros"
|
|
1099
|
+
)
|
|
1100
|
+
parser.add_argument("input_file", help="Path to input HLA Markdown file")
|
|
1101
|
+
parser.add_argument("--output", "-o", help="Path to output HTML file (optional)")
|
|
1102
|
+
parser.add_argument(
|
|
1103
|
+
"--slug",
|
|
1104
|
+
"-s",
|
|
1105
|
+
default="system",
|
|
1106
|
+
help="Feature slug for diagram names (e.g. appointment-service)",
|
|
1107
|
+
)
|
|
1108
|
+
parser.add_argument(
|
|
1109
|
+
"--lineage-only",
|
|
1110
|
+
action="store_true",
|
|
1111
|
+
help="Extract and convert only Section 4.5 (Database and Data Processing Pipeline Lineage)",
|
|
1112
|
+
)
|
|
1113
|
+
args = parser.parse_args()
|
|
1114
|
+
|
|
1115
|
+
input_path = Path(args.input_file)
|
|
1116
|
+
if not input_path.exists():
|
|
1117
|
+
print(f"Error: File not found: {input_path}", file=sys.stderr)
|
|
1118
|
+
sys.exit(1)
|
|
1119
|
+
|
|
1120
|
+
content = input_path.read_text(encoding="utf-8")
|
|
1121
|
+
|
|
1122
|
+
if args.lineage_only:
|
|
1123
|
+
# Match Section 4.5 (or any heading matching Pipeline Lineage) up to next section or end
|
|
1124
|
+
match = re.search(
|
|
1125
|
+
r"(###\s*4\.5\s+.*?)(?=(^###\s*4\.6|^##\s*5|\Z))",
|
|
1126
|
+
content,
|
|
1127
|
+
flags=re.DOTALL | re.MULTILINE,
|
|
1128
|
+
)
|
|
1129
|
+
if not match:
|
|
1130
|
+
print(
|
|
1131
|
+
"Error: Section 4.5 (Database Pipeline Lineage) not found in input document.",
|
|
1132
|
+
file=sys.stderr,
|
|
1133
|
+
)
|
|
1134
|
+
sys.exit(1)
|
|
1135
|
+
content = match.group(1)
|
|
1136
|
+
|
|
1137
|
+
slug = (
|
|
1138
|
+
args.slug if args.slug != "system" else input_path.stem.replace("-hla", "")
|
|
1139
|
+
)
|
|
1140
|
+
|
|
1141
|
+
xhtml = markdown_to_confluence_xhtml(
|
|
1142
|
+
content,
|
|
1143
|
+
feature_slug=slug,
|
|
1144
|
+
)
|
|
1145
|
+
|
|
1146
|
+
# Validate XML
|
|
1147
|
+
if not validate_confluence_xhtml(xhtml):
|
|
1148
|
+
print("Warning: Generated XHTML has XML parsing issues.", file=sys.stderr)
|
|
1149
|
+
else:
|
|
1150
|
+
print("XHTML validation successful: Content is well-formed XML.")
|
|
1151
|
+
|
|
1152
|
+
if args.output:
|
|
1153
|
+
output_path = Path(args.output)
|
|
1154
|
+
elif args.lineage_only:
|
|
1155
|
+
output_path = input_path.with_name(f"{input_path.stem}-lineage.html")
|
|
1156
|
+
else:
|
|
1157
|
+
output_path = input_path.with_suffix(".html")
|
|
1158
|
+
|
|
1159
|
+
output_path.write_text(xhtml, encoding="utf-8")
|
|
1160
|
+
print(f"Confluence XHTML successfully written to: {output_path}")
|
|
1161
|
+
|
|
1162
|
+
|
|
1163
|
+
if __name__ == "__main__":
|
|
1164
|
+
main()
|