Skip to content

feat(backend): Mermaid diagrams + migrate all inline LLM prompts to backend/app/prompts/ - #347

Merged
arozumenko merged 15 commits into
mainfrom
pr-346
May 22, 2026
Merged

feat(backend): Mermaid diagrams + migrate all inline LLM prompts to backend/app/prompts/#347
arozumenko merged 15 commits into
mainfrom
pr-346

Conversation

@arozumenko

@arozumenko arozumenko commented May 22, 2026

Copy link
Copy Markdown
Owner

Summary

Mermaid diagram generation (previous commits)

  • _select_nodes now filters out document-type nodes — doc-only clusters produce no architecture diagram instead of a diagram full of README nodes
  • _build_class_diagram normalizes dotted parent_symbol values so methods/fields now appear in class bodies
  • _is_data_model only fires on real DB constructs (SQLAlchemy, TypeORM, GORM) — Pydantic BaseModel and @dataclass no longer trigger erDiagram
  • _inject_cluster_diagrams rewritten with agentic LLM-based discovery; _llm_class_diagram and _llm_data_model_diagram produce validated Mermaid blocks from real code
  • _validate_mermaid accepts classDiagram and erDiagram headers

Prompt migration (this commit)

  • Created backend/app/prompts/__init__.py with a load_prompt(relative_path) loader backed by functools.lru_cache (zero repeated I/O, raises FileNotFoundError with a useful message on missing paths)
  • Extracted all LLM system/user prompt strings from 13 Python source files into 31 Markdown files under backend/app/prompts/, grouped by subsystem: writer/, planner/, ask/, research/, extractors/, repo/, and wiki/
  • Updated every modified Python module to call load_prompt() instead of embedding inline string literals; template prompts with {variable} placeholders are loaded once and .format(**kwargs) is called at the call site
  • The four actively imported constants in wiki_prompts_enhanced.py are now overridden at the bottom of the file with _load_prompt() calls so all callers get the MD-backed version without an import change

New prompt files (31 MD files)

writer/system.md, writer/user_template.md, writer/budget_exhausted.md, writer/chapter_index_budget_exhausted.md, planner/system.md, planner/chaptered_system.md, planner/refiner_system.md, ask/workflow_instructions.md, ask/tool_instructions.md, ask/output_instructions.md, ask/query_optimization_system.md, ask/query_optimization_user.md, ask/answer_system.md, ask/answer_user.md, research/workflow_instructions.md, research/tool_instructions.md, research/stopping_criteria.md, research/output_format.md, extractors/image_describe.md, extractors/pdf_page_describe.md, repo/explorer_system.md, repo/explorer_budget_exhausted.md, repo/overview_system.md, wiki/surgical_edit_system.md, wiki/surgical_edit_user.md, wiki/page_format.md, wiki/content_generation_v3_tone.md, wiki/repo_analysis_structured.md, wiki/repo_analysis_enhanced.md, wiki/wiki_structure.md

Test plan

  • 45 new tests in tests/unit/test_prompt_loader.py covering loader behaviour, all 31 migrated files, template placeholders, and integration checks that Python module constants match their MD files
  • pytest tests/unit/ — 4559+ passed, 3 skipped (no regressions)
  • All modified files pass python -m py_compile

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings May 22, 2026 07:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves Mermaid diagram generation in the backend wiki content writer by fixing several deterministic diagram bugs and adding LLM-driven (source-based) class/data-model diagram injection into chapter index pages, with validation and graceful fallback when LLM isn’t configured.

Changes:

  • Fixes architecture diagram node selection to exclude document-type nodes and expands Mermaid header validation to include classDiagram/erDiagram.
  • Improves deterministic diagram fidelity (parent symbol normalization for class members; stricter DB-model detection for ER diagrams).
  • Adds agentic, file-reading LLM diagram generation and injection into chapter index markdown, plus new/updated unit tests.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.

File Description
backend/app/core/wiki_content_writer/writer_agent.py Adds cluster diagram injection plus LLM-based class/ER diagram generation from gathered source files.
backend/app/core/wiki_content_writer/diagram_generator.py Extends Mermaid validation headers and adds deterministic helpers for class/ER diagrams and cluster-context diagrams.
backend/tests/unit/wiki_content_writer/test_writer_diagrams.py New unit tests covering source gathering, LLM diagram generation, injection behavior, and Mermaid header validation.
backend/tests/unit/wiki_content_writer/test_diagram_generator.py Updates/adds tests for doc-node filtering, parent symbol normalization, stricter data-model detection, and cluster-context diagrams.

"""
from __future__ import annotations

import re
Comment on lines +991 to +1001
text = response.content if hasattr(response, "content") else str(response)
if not text or "EMPTY" in text:
return ""
import re as _re # noqa: PLC0415
m = _re.search(r"```mermaid\n.*?```", text, _re.DOTALL)
if not m:
return ""
from .diagram_generator import _validate_mermaid # noqa: PLC0415
candidate = m.group(0)
ok, _ = _validate_mermaid(candidate)
return candidate if ok else ""
Comment on lines +1040 to +1050
text = response.content if hasattr(response, "content") else str(response)
if not text or "EMPTY" in text:
return ""
import re as _re # noqa: PLC0415
m = _re.search(r"```mermaid\n.*?```", text, _re.DOTALL)
if not m:
return ""
from .diagram_generator import _validate_mermaid # noqa: PLC0415
candidate = m.group(0)
ok, _ = _validate_mermaid(candidate)
return candidate if ok else ""
Comment on lines +929 to +932
_DOC_TYPES = {
"config_document", "markdown_document", "toml_document",
"module_doc", "file_doc", "rst_document", "html_document",
}
Comment on lines 50 to 54
Returns (is_valid, error_reason). Checks:
- Fenced code block with mermaid tag
- Valid graph/flowchart header
- Valid diagram header (graph/flowchart, classDiagram, or erDiagram)
- All node-declaration lines use quoted labels
- No obvious unclosed quotes
from typing import Any
from unittest.mock import MagicMock, patch

import pytest
Adds three Mermaid diagrams to every cluster _index.md:

## Architecture — deterministic graph LR
Cluster's own architectural nodes in a subgraph, with cross-cluster
edges to neighbouring clusters when available. Doc-type nodes
(markdown_document, config_document, etc.) are excluded from
_select_nodes so only real code symbols appear.

## Class Structure — LLM-based, agentic
_gather_cluster_source reads the cluster's top source files (up to 5,
scored by is_architectural count). _llm_class_diagram sends those
files to the LLM with a structured prompt; the LLM returns a full
classDiagram with stereotypes, visibility prefixes, member signatures,
and inheritance arrows. Falls back to empty on LLM absence or error.

## Data Model — LLM-based, agentic
_llm_data_model_diagram detects true database/persistence models only
(SQLAlchemy Column/relationship/DeclarativeBase, TypeORM @entity,
GORM gorm: tags). Pydantic response schemas and plain dataclasses are
explicitly excluded. Emits an erDiagram with fields, PK/FK markers,
and entity relationships. Silent no-op when fewer than 2 DB models.

Bug fixes vs. earlier stub:
- parent_symbol normalised via rsplit(".", 1)[-1] before member
  matching (was "module.ClassName", never matched "ClassName")
- _is_data_model tightened: BaseModel/@DataClass removed as triggers
- _validate_mermaid extended to accept classDiagram and erDiagram

All diagrams are injected after ## Overview and before ## Key
Components / ## Sub-pages; any failure returns the original markdown
unchanged. 79 unit tests across test_diagram_generator.py and the
new test_writer_diagrams.py.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@arozumenko arozumenko changed the title fix(backend): fix Mermaid diagram bugs and add agentic LLM-based diagrams feat(backend): Mermaid diagrams for cluster chapter index pages May 22, 2026
@arozumenko
arozumenko requested a review from Copilot May 22, 2026 08:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.

Comment on lines +11 to +12
import re
from typing import Any
Comment on lines +263 to +272
def test_invalid_graph_header_rejected(self):
"""classDiagram with wrong header should fail validation and return empty."""
bad_diagram = "```mermaid\ngraph LR\n A --> B\n```"
writer = self._writer_with_llm(bad_diagram)
result = writer._llm_class_diagram("code", "Title")
# A graph LR block returned from _llm_class_diagram should still pass if valid
# BUT we want classDiagram specifically — however the method extracts any mermaid block
# and validates it. A valid graph LR passes _validate_mermaid.
# The test here confirms no crash occurs.
assert isinstance(result, str)
Comment on lines +837 to +840
# ── 4. Inject cluster diagrams into the chapter index ─────────
chapter_index_md = self._inject_cluster_diagrams(
chapter_index_md, chapter_spec.cluster_id, chapter_spec.chapter_title
)
Comment on lines +736 to +739
ext_title = cluster_titles.get(ext_cid) or f"cluster_{ext_cid}"
ext_safe = _safe_id(ext_title[:_MAX_LABEL_CHARS])
ext_label = _sanitize_label(ext_title[:_MAX_LABEL_CHARS])
lines.append(f' {ext_safe}["{ext_label}"]')
Comment on lines +752 to +755
src_safe = _safe_id(src_name)
ext_title = cluster_titles.get(ext_cid) or f"cluster_{ext_cid}"
ext_safe = _safe_id(ext_title[:_MAX_LABEL_CHARS])
lines.append(f" {src_safe} --> {ext_safe}")
Comment on lines +637 to +643
sig = (member.get("signature") or "").strip()
if sig:
# Strip the leading 'def ' / 'func ' / 'fn ' keyword if present.
sig = re.sub(r"^(def|func|fn|function)\s+", "", sig)
# Strip leading path prefix from the signature name as well.
sig = re.sub(r"^[A-Za-z0-9_.]+\.", "", sig)
return f"{visibility}{_sanitize_label(sig)}"
from typing import Any
from unittest.mock import MagicMock, patch

import pytest
- Remove unused `re` and `pytest` imports in test_writer_diagrams.py
- Update _validate_mermaid docstring (remove stale quoted-label bullet)
- Add _sanitize_member() that preserves -> return arrows; use it in
  _method_signature_line() instead of _sanitize_label() which strips >
- Fix external cluster node ID collision: build cid→safe_id map upfront,
  append _{cid} suffix on collision with internal or other external nodes;
  reuse same map for edge declarations so both sides always match
- Add asciidoc_document to _gather_cluster_source doc-type filter
- Replace "EMPTY" in text substring check with text.strip() == "EMPTY"
  exact sentinel to avoid dropping valid diagrams containing that word
- Enforce expected diagram type header: _llm_class_diagram rejects
  non-classDiagram blocks; _llm_data_model_diagram rejects non-erDiagram
- Wrap _inject_cluster_diagrams in run_in_executor to avoid blocking
  the event loop with sync disk IO and LLM calls
- Replace misleading test_invalid_graph_header_rejected with
  test_non_classdiagram_header_rejected that actually asserts result == ""

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

Comment on lines +47 to +58
def _sanitize_member(text: str) -> str:
"""Sanitize a classDiagram member line.

Preserves ``->`` return-type arrows (only strips bare ``<``/``>`` that
would break Mermaid's parser, not the ``->`` sequence used for return types).
"""
s = text.replace('"', "'")
# Replace bare < and > only when not part of ->
s = re.sub(r"(?<!-)<|>(?!>)", "", s)
s = re.sub(r"[{}|\\]", "", s)
s = re.sub(r"\s+", " ", s).strip()
return s or text
Comment on lines 578 to 592
def _select_nodes(nodes: list[dict]) -> list[dict]:
"""Filter to ≤ 15 nodes; prefer architectural ones when > 15 candidates."""
if len(nodes) <= _MAX_NODES:
return nodes
arch = [n for n in nodes if n.get("is_architectural")]
"""Filter to ≤ 15 non-document nodes; prefer architectural ones when > 15."""
# Bug 1 fix: exclude document-type nodes from architecture diagrams.
code_nodes = [
n for n in nodes
if (n.get("symbol_type") or n.get("kind", "")) not in _DOC_TYPES
]
if not code_nodes:
return []
if len(code_nodes) <= _MAX_NODES:
return code_nodes
arch = [n for n in code_nodes if n.get("is_architectural")]
if len(arch) <= _MAX_NODES:
return arch
return arch[:_MAX_NODES]
Comment on lines +683 to +685
# Storage returns different nodes for different cluster IDs
call_count = {"count": 0}
def _get_nodes_by_cluster(cluster_id):
github-actions Bot and others added 3 commits May 22, 2026 12:56
Root causes fixed (all verified in container against real DB):

1. Skeleton cluster_id ≠ DB macro_cluster: structure_skeleton.py uses
   enumerate(start=1) while Leiden assigns 0-based macro_cluster values.
   _resolve_db_cluster now probes storage with exact file paths from
   evidence slices, counting macro_cluster votes only for nodes whose
   rel_path exactly matches a hint — avoids sibling-subdirectory
   contamination (e.g. workers/processors outvoting workers/queue.py).

2. LLM has WIKI_TOOL_SCHEMAS pre-bound so it calls read_file instead of
   generating diagrams. _diagram_llm_invoke binds tool_choice='none'
   at the API level and prepends a SystemMessage forbidding tool calls.
   Verified: Bedrock Claude now outputs classDiagram directly.

3. response.content is a Bedrock content-blocks list, not a string.
   _extract_llm_text normalises both list and string responses.

4. _gather_cluster_source prefers file_hints over cluster DB lookup,
   eliminating the dependency on the broken cluster_id mapping entirely
   for source context gathering.

5. get_nodes_by_path_prefix uses LIKE 'prefix/%' (directory queries
   only); exact file paths returned 0 nodes. Fixed by stripping filename
   and filtering results to exact hint paths.

Removed all temporary debug INFO logs added during investigation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…h to TD layout

graph LR with 15 nodes produces an unreadable horizontal strip. Fix:
- Only render nodes that participate in at least one edge; isolated nodes
  convey no relationships and just add clutter
- Return empty when fewer than 2 connected nodes (nothing useful to show)
- Switch from graph LR to graph TD — top-down layout scales better and
  avoids the horizontal squash
- Reduce _MAX_NODES from 15 to 8

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…files

All three diagrams (Architecture, Class Structure, Data Model) now use an
agentic tool loop: the LLM receives the list of available source files and
calls read_file on whichever it chooses (budget 6 calls) before generating
the diagram. No more static pre-selection of top-N files.

backend/app/prompts/ — new prompt directory:
  diagrams/architecture.md  — system prompt for graph TD architecture diagram
  diagrams/class_structure.md — system prompt for classDiagram
  diagrams/data_model.md    — system prompt for erDiagram
  __init__.py               — load_prompt(relative_path) loader with lru_cache

writer_agent.py changes:
  _DIAGRAM_PROMPT_FILES replaces inline _DIAGRAM_SYSTEM dict
  _agentic_diagram() — tool loop: give LLM file list, execute read_file calls,
    extract validated Mermaid block from final response
  _list_cluster_files() — returns available files for the cluster (prefers
    evidence file_hints, falls back to DB query)
  Removed _gather_cluster_source, _llm_architecture_diagram,
    _llm_class_diagram, _llm_data_model_diagram (all replaced by _agentic_diagram)
  Removed _diagram_llm_invoke (tool_choice=none no longer needed since the
    agentic loop explicitly controls when to stop calling tools)

diagram_generator.py: _render_cluster_context now only includes connected
  nodes (participants in at least one edge), switches to graph TD layout.
  _MAX_NODES reduced to 8 (no longer used for architecture — kept for
  the class diagram DB fallback path).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@arozumenko arozumenko changed the title feat(backend): Mermaid diagrams for cluster chapter index pages feat(backend): Mermaid diagrams + migrate all inline LLM prompts to backend/app/prompts/ May 22, 2026
github-actions Bot and others added 2 commits May 22, 2026 13:41
…pts/

All LLM system/user prompt strings have been extracted from Python source
files into Markdown files under backend/app/prompts/, grouped by subsystem:
writer/, planner/, ask/, research/, extractors/, repo/, and wiki/.

A new load_prompt() loader (app/prompts/__init__.py) reads files relative to
the package directory with lru_cache for zero repeated I/O cost. Python modules
are updated to call load_prompt() instead of embedding text literals, making
prompts independently versionable and reviewable without touching application
logic.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove _MAX_NODES: _select_nodes now returns all non-doc nodes,
  _select_class_nodes is a pass-through
- TestManyNodes: assert all 20 nodes appear (no cap); assert all code
  nodes included regardless of is_architectural flag
- TestGenerateClusterContextDiagram: connected-only behavior — add edges
  to tests expecting nodes; add test confirming isolated nodes → empty;
  header changed from graph LR to graph TD
- TestLlmClassDiagram / TestLlmDataModelDiagram → TestAgenticDiagram
  (methods replaced by _agentic_diagram)
- TestInjectClusterDiagrams: mock _agentic_diagram directly instead of
  patching DiagramGenerator

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
github-actions Bot and others added 4 commits May 22, 2026 13:47
…ure flows

Prompt upgrades:
- architecture.md: switched from graph TD to stateDiagram-v2 — captures
  functional flows (states, transitions, error paths) instead of a node
  graph that just lists components. Includes full stateDiagram-v2 syntax
  reference with examples.
- class_structure.md: added full classDiagram syntax reference (visibility,
  stereotypes, relationships, generics, abstract methods, static members,
  multiplicity). Instructs LLM to use grep/get_signature aggressively.
- data_model.md: added full erDiagram syntax reference (entity attributes,
  PK/FK/UK markers, all cardinality notations). Instructs LLM to grep for
  ORM patterns before deciding which entities to include.
- references/: full Mermaid reference docs from mermaid-skill repo stored
  alongside the prompts for context.

_agentic_diagram prompts now instruct LLM to use all available tools
(read_file, grep, get_signature, get_callers, get_callees) to collect
enough data before generating each diagram type.

_validate_mermaid: added stateDiagram and stateDiagram-v2 to accepted
headers. _DIAGRAM_EXPECTED_HEADER updated to match stateDiagram for
the architecture diagram type.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds hover controls (zoom in/out, fullscreen) to every MermaidDiagram
card and a fullscreen Dialog with drag-to-pan, scroll-to-zoom, GitHub-
style 3×3 control pad, zoom % indicator, and reset-to-fit. Single file
change; no new dependencies.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Modal SVG invisible: canvas used height:100% inside overflow:hidden
  Paper — switched to position:absolute inset:0 so it always fills the
  dialog regardless of Paper's overflow setting
- Inline zoom scaled the wrapper box (transform:scale, layout-neutral)
  not the diagram — switched to zoom CSS property on the SVG element
  which changes actual layout dimensions; card overflow:auto scrolls
- Duplicate fullscreen buttons: replaced second FitScreenIcon with a
  reset-zoom button (disabled at scale=1) — zoom-out / zoom-in /
  reset / fullscreen is now the correct four-action pill
- Max scale raised to 1000% (10×) for both modal and inline; inline
  min scale lowered to 25%

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 55 out of 55 changed files in this pull request and generated 9 comments.

Comment on lines +11 to +13
from typing import Any
from unittest.mock import MagicMock, patch

Comment on lines +52 to +55
s = text.replace('"', "'")
# Replace bare < and > only when not part of ->
s = re.sub(r"(?<!-)<|>(?!>)", "", s)
s = re.sub(r"[{}|\\]", "", s)
Comment thread backend/app/prompts/__init__.py Outdated
Comment on lines +50 to +56
path = _PROMPTS_ROOT / relative_path
if not path.exists():
raise FileNotFoundError(
f"Prompt file not found: {path} "
f"(resolved from relative_path={relative_path!r})"
)
return path.read_text(encoding="utf-8").strip()
Comment on lines +217 to +237
<Tooltip title="Zoom out" placement="bottom">
<IconButton
size="small"
onClick={handleInlineZoomOut}
sx={{ color: pillColor, p: 0.5 }}
disabled={scale <= INLINE_MIN_SCALE}
>
<ZoomOutIcon fontSize="small" />
</IconButton>
</Tooltip>

<Tooltip title="Zoom in" placement="bottom">
<IconButton
size="small"
onClick={handleInlineZoomIn}
sx={{ color: pillColor, p: 0.5 }}
disabled={scale >= INLINE_MAX_SCALE}
>
<ZoomInIcon fontSize="small" />
</IconButton>
</Tooltip>
Comment on lines +239 to +258
<Tooltip title="Fullscreen" placement="bottom">
<IconButton
size="small"
onClick={handleOpenFullscreen}
sx={{ color: pillColor, p: 0.5 }}
>
<FullscreenIcon fontSize="small" />
</IconButton>
</Tooltip>

<Tooltip title="Reset zoom" placement="bottom">
<IconButton
size="small"
onClick={(e) => { e.stopPropagation(); setScale(1); }}
sx={{ color: pillColor, p: 0.5 }}
disabled={scale === 1}
>
<FitScreenIcon fontSize="small" />
</IconButton>
</Tooltip>
Comment on lines +277 to +287
<Tooltip title="Close">
<IconButton
onClick={closeModal}
sx={{
bgcolor: pillBg,
color: '#fff',
'&:hover': { bgcolor: 'rgba(0,0,0,0.75)' },
}}
>
<CloseIcon />
</IconButton>
Comment on lines +365 to +379
<Tooltip title="Pan up">
<IconButton size="small" onClick={handlePanUp} sx={{ color: pillColor, p: 0.5 }}>
<ArrowUpwardIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Zoom in">
<IconButton
size="small"
onClick={handleModalZoomIn}
sx={{ color: pillColor, p: 0.5 }}
disabled={modalScale >= MAX_SCALE}
>
<ZoomInIcon fontSize="small" />
</IconButton>
</Tooltip>
Comment on lines +382 to +396
<Tooltip title="Pan left">
<IconButton size="small" onClick={handlePanLeft} sx={{ color: pillColor, p: 0.5 }}>
<ArrowBackIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Reset view">
<IconButton size="small" onClick={handleModalReset} sx={{ color: pillColor, p: 0.5 }}>
<FitScreenIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Pan right">
<IconButton size="small" onClick={handlePanRight} sx={{ color: pillColor, p: 0.5 }}>
<ArrowForwardIcon fontSize="small" />
</IconButton>
</Tooltip>
Comment on lines +400 to +414
<Tooltip title="Pan down">
<IconButton size="small" onClick={handlePanDown} sx={{ color: pillColor, p: 0.5 }}>
<ArrowDownwardIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Zoom out">
<IconButton
size="small"
onClick={handleModalZoomOut}
sx={{ color: pillColor, p: 0.5 }}
disabled={modalScale <= MIN_SCALE}
>
<ZoomOutIcon fontSize="small" />
</IconButton>
</Tooltip>
github-actions Bot and others added 2 commits May 22, 2026 14:59
backend:
- _sanitize_member: fix regex — (?<!-)> preserves -> arrows (old >(?!>)
  incorrectly stripped the > in ->)
- _build_diagram: add early return when _select_nodes yields empty list;
  previously produced a fenced but empty graph LR block for doc-only clusters
- test_diagram_generator: remove unused call_count variable (Ruff F841)
- test_writer_diagrams: remove unused patch import (Ruff F401)
- load_prompt: resolve path and assert it stays within _PROMPTS_ROOT
  to prevent path traversal (e.g. load_prompt("../../etc/passwd"))

web:
- MermaidDiagram: add aria-label to every icon-only IconButton in the
  hover pill and modal control pad so screen readers announce the action

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pages are served directly from artifact files via list_artifacts, not
from the DB. index_wiki_pages already cleaned up stale DB records but
the .md files on disk were never removed, so old pages accumulated
alongside new ones after every refresh.

Before uploading new pages, snapshot the existing wiki_pages/*.md keys.
After all new pages are written, delete any .md files in the old set
that are not in the new generation's page set. Non-page artifacts
(analysis JSON, etc.) are untouched.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 56 out of 56 changed files in this pull request and generated 5 comments.

Comment on lines +186 to +190
'& .mermaid-svg-wrap svg': {
display: 'block',
zoom: scale,
},
}}
Comment thread backend/app/services/wiki_service.py Outdated
Comment on lines +931 to +947
# Snapshot existing page files BEFORE writing new ones so we can
# remove any that the new generation no longer produces. Only
# targets wiki_pages/ .md files — analysis artefacts are kept.
new_page_keys: set[str] = {
f"{invocation.wiki_id}/{page_id}.md" for page_id in generated_pages
}
try:
existing_artifacts = await self.storage.list_artifacts(
"wiki_artifacts", prefix=invocation.wiki_id
)
stale_page_files = [
a for a in existing_artifacts
if a.endswith(".md") and "wiki_pages" in a and a not in new_page_keys
]
except Exception as _e:
logger.warning("Could not list existing artifacts for stale cleanup: %s", _e)
stale_page_files = []
messages.append(HumanMessage(content=load_prompt("writer/budget_exhausted.md")))
try:
response = effective_llm.invoke(messages)
final_content = response.content if isinstance(response.content, str) else str(response.content)
Comment on lines +375 to +383
"""Generate a Mermaid graph LR showing the cluster as a subgraph.

The cluster's own architectural nodes are rendered inside a ``subgraph``
block. When *all_cluster_ids* and *cluster_titles* are provided,
cross-cluster edges are detected and neighbouring clusters are rendered
as plain external nodes.

Returns a fenced ``graph LR`` block, or ``''`` on empty cluster /
storage error.
Comment thread backend/app/prompts/__init__.py Outdated
Comment on lines +56 to +61
if not path.exists():
raise FileNotFoundError(
f"Prompt file not found: {path} "
f"(resolved from relative_path={relative_path!r})"
)
return path.read_text(encoding="utf-8").strip()
wiki_service.py — stale page cleanup:
  The previous fix compared generated_pages integer keys against
  wiki_pages/*.md artifact paths, which never matched, causing ALL
  existing wiki_pages/*.md to be treated as stale and deleted before
  the new files were written. Rewritten: snapshot existing wiki_pages/*.md
  BEFORE any writes; upload generated_pages then artifacts; collect new
  wiki_pages/*.md keys from artifact upload; delete stale files only AFTER
  all new files are safely written.

writer_agent.py:
  Final write fallback at line 362 used isinstance(response.content, str)
  check that fails for Bedrock content-block lists. Replaced with
  _extract_llm_text() which handles both string and block-list responses.

diagram_generator.py:
  generate_cluster_context_diagram docstring said "graph LR" but the
  renderer emits "graph TD" since the layout switch. Updated.

prompts/__init__.py:
  path.exists() → path.is_file() so directories produce a clear
  FileNotFoundError instead of IsADirectoryError from read_text().

MermaidDiagram.tsx:
  zoom CSS property is not supported in Firefox. Replaced with standard
  width-based scaling: strip Mermaid's inline max-width after render,
  store naturalWidth, set svg width = naturalWidth * scale in CSS.
  width changes actual layout dimensions in all browsers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@arozumenko
arozumenko merged commit a788d51 into main May 22, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants