feat(backend): Mermaid diagrams + migrate all inline LLM prompts to backend/app/prompts/ - #347
Merged
Conversation
Contributor
There was a problem hiding this comment.
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 |
3 tasks
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>
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>
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): |
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>
…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>
…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>
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 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> |
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>
…iles, not imported
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>
Comment on lines
+186
to
+190
| '& .mermaid-svg-wrap svg': { | ||
| display: 'block', | ||
| zoom: scale, | ||
| }, | ||
| }} |
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 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Mermaid diagram generation (previous commits)
_select_nodesnow filters out document-type nodes — doc-only clusters produce no architecture diagram instead of a diagram full of README nodes_build_class_diagramnormalizes dottedparent_symbolvalues so methods/fields now appear in class bodies_is_data_modelonly fires on real DB constructs (SQLAlchemy, TypeORM, GORM) — PydanticBaseModeland@dataclassno longer trigger erDiagram_inject_cluster_diagramsrewritten with agentic LLM-based discovery;_llm_class_diagramand_llm_data_model_diagramproduce validated Mermaid blocks from real code_validate_mermaidacceptsclassDiagramanderDiagramheadersPrompt migration (this commit)
backend/app/prompts/__init__.pywith aload_prompt(relative_path)loader backed byfunctools.lru_cache(zero repeated I/O, raisesFileNotFoundErrorwith a useful message on missing paths)backend/app/prompts/, grouped by subsystem:writer/,planner/,ask/,research/,extractors/,repo/, andwiki/load_prompt()instead of embedding inline string literals; template prompts with{variable}placeholders are loaded once and.format(**kwargs)is called at the call sitewiki_prompts_enhanced.pyare now overridden at the bottom of the file with_load_prompt()calls so all callers get the MD-backed version without an import changeNew 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.mdTest plan
tests/unit/test_prompt_loader.pycovering loader behaviour, all 31 migrated files, template placeholders, and integration checks that Python module constants match their MD filespytest tests/unit/— 4559+ passed, 3 skipped (no regressions)python -m py_compile🤖 Generated with Claude Code