graphitect 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (336) hide show
  1. graphify/__init__.py +30 -0
  2. graphify/__main__.py +757 -0
  3. graphify/_minhash.py +107 -0
  4. graphify/affected.py +318 -0
  5. graphify/always_on/agents-md.md +12 -0
  6. graphify/always_on/antigravity-rules.md +14 -0
  7. graphify/always_on/claude-md.md +9 -0
  8. graphify/always_on/gemini-md.md +9 -0
  9. graphify/always_on/kiro-steering.md +5 -0
  10. graphify/always_on/vscode-instructions.md +17 -0
  11. graphify/analyze.py +769 -0
  12. graphify/benchmark.py +152 -0
  13. graphify/build.py +2300 -0
  14. graphify/cache.py +1746 -0
  15. graphify/callflow_html.py +2051 -0
  16. graphify/cargo_introspect.py +109 -0
  17. graphify/cli.py +4745 -0
  18. graphify/cluster.py +409 -0
  19. graphify/command-kilo.md +15 -0
  20. graphify/cross_repo_calls.py +216 -0
  21. graphify/cross_repo_types.py +75 -0
  22. graphify/csharp_dispatch.py +154 -0
  23. graphify/dedup.py +1213 -0
  24. graphify/detect.py +2566 -0
  25. graphify/diagnostics.py +406 -0
  26. graphify/export.py +1349 -0
  27. graphify/exporters/__init__.py +1 -0
  28. graphify/exporters/base.py +14 -0
  29. graphify/exporters/graphdb.py +173 -0
  30. graphify/exporters/html.py +637 -0
  31. graphify/extract.py +7856 -0
  32. graphify/extractors/MIGRATION.md +107 -0
  33. graphify/extractors/__init__.py +66 -0
  34. graphify/extractors/apex.py +215 -0
  35. graphify/extractors/base.py +85 -0
  36. graphify/extractors/bash.py +579 -0
  37. graphify/extractors/blade.py +53 -0
  38. graphify/extractors/commonlisp.py +540 -0
  39. graphify/extractors/csharp.py +448 -0
  40. graphify/extractors/dart.py +564 -0
  41. graphify/extractors/dm.py +494 -0
  42. graphify/extractors/elixir.py +241 -0
  43. graphify/extractors/engine.py +6509 -0
  44. graphify/extractors/fortran.py +311 -0
  45. graphify/extractors/go.py +527 -0
  46. graphify/extractors/json_config.py +240 -0
  47. graphify/extractors/julia.py +289 -0
  48. graphify/extractors/markdown.py +408 -0
  49. graphify/extractors/models.py +131 -0
  50. graphify/extractors/objc.py +566 -0
  51. graphify/extractors/ocaml.py +289 -0
  52. graphify/extractors/pascal.py +688 -0
  53. graphify/extractors/pascal_forms.py +196 -0
  54. graphify/extractors/powershell.py +522 -0
  55. graphify/extractors/razor.py +192 -0
  56. graphify/extractors/resolution.py +3584 -0
  57. graphify/extractors/robot.py +296 -0
  58. graphify/extractors/rust.py +470 -0
  59. graphify/extractors/sln.py +92 -0
  60. graphify/extractors/sql.py +720 -0
  61. graphify/extractors/terraform.py +181 -0
  62. graphify/extractors/verilog.py +329 -0
  63. graphify/extractors/zig.py +181 -0
  64. graphify/file_slice.py +246 -0
  65. graphify/global_graph.py +194 -0
  66. graphify/google_workspace.py +237 -0
  67. graphify/hooks.py +933 -0
  68. graphify/ids.py +93 -0
  69. graphify/ingest.py +358 -0
  70. graphify/install.py +2366 -0
  71. graphify/llm.py +3544 -0
  72. graphify/manifest.py +4 -0
  73. graphify/manifest_ingest.py +311 -0
  74. graphify/mcp_ingest.py +386 -0
  75. graphify/multigraph_compat.py +212 -0
  76. graphify/pascal_resolution.py +129 -0
  77. graphify/paths.py +436 -0
  78. graphify/pg_introspect.py +165 -0
  79. graphify/prs.py +770 -0
  80. graphify/querylog.py +80 -0
  81. graphify/reflect.py +882 -0
  82. graphify/report.py +346 -0
  83. graphify/resolver_registry.py +85 -0
  84. graphify/ruby_resolution.py +242 -0
  85. graphify/scip_ingest.py +363 -0
  86. graphify/security.py +460 -0
  87. graphify/semantic_cleanup.py +336 -0
  88. graphify/serve.py +2608 -0
  89. graphify/skill-agents.md +710 -0
  90. graphify/skill-aider.md +1283 -0
  91. graphify/skill-amp.md +710 -0
  92. graphify/skill-claw.md +713 -0
  93. graphify/skill-codex.md +710 -0
  94. graphify/skill-copilot.md +713 -0
  95. graphify/skill-devin.md +1410 -0
  96. graphify/skill-droid.md +710 -0
  97. graphify/skill-kilo.md +722 -0
  98. graphify/skill-kiro.md +713 -0
  99. graphify/skill-opencode.md +705 -0
  100. graphify/skill-pi.md +713 -0
  101. graphify/skill-trae.md +711 -0
  102. graphify/skill-vscode.md +709 -0
  103. graphify/skill-windows.md +755 -0
  104. graphify/skill.md +713 -0
  105. graphify/skills/agents/references/add-watch.md +56 -0
  106. graphify/skills/agents/references/exports.md +87 -0
  107. graphify/skills/agents/references/extraction-spec.md +70 -0
  108. graphify/skills/agents/references/github-and-merge.md +46 -0
  109. graphify/skills/agents/references/hooks.md +33 -0
  110. graphify/skills/agents/references/query.md +311 -0
  111. graphify/skills/agents/references/transcribe.md +52 -0
  112. graphify/skills/agents/references/update.md +210 -0
  113. graphify/skills/amp/references/add-watch.md +56 -0
  114. graphify/skills/amp/references/exports.md +87 -0
  115. graphify/skills/amp/references/extraction-spec.md +70 -0
  116. graphify/skills/amp/references/github-and-merge.md +46 -0
  117. graphify/skills/amp/references/hooks.md +33 -0
  118. graphify/skills/amp/references/query.md +311 -0
  119. graphify/skills/amp/references/transcribe.md +52 -0
  120. graphify/skills/amp/references/update.md +210 -0
  121. graphify/skills/claude/references/add-watch.md +56 -0
  122. graphify/skills/claude/references/exports.md +87 -0
  123. graphify/skills/claude/references/extraction-spec.md +70 -0
  124. graphify/skills/claude/references/github-and-merge.md +46 -0
  125. graphify/skills/claude/references/hooks.md +33 -0
  126. graphify/skills/claude/references/query.md +311 -0
  127. graphify/skills/claude/references/transcribe.md +52 -0
  128. graphify/skills/claude/references/update.md +210 -0
  129. graphify/skills/claw/references/add-watch.md +56 -0
  130. graphify/skills/claw/references/exports.md +87 -0
  131. graphify/skills/claw/references/extraction-spec.md +31 -0
  132. graphify/skills/claw/references/github-and-merge.md +46 -0
  133. graphify/skills/claw/references/hooks.md +33 -0
  134. graphify/skills/claw/references/query.md +311 -0
  135. graphify/skills/claw/references/transcribe.md +52 -0
  136. graphify/skills/claw/references/update.md +210 -0
  137. graphify/skills/codex/references/add-watch.md +56 -0
  138. graphify/skills/codex/references/exports.md +87 -0
  139. graphify/skills/codex/references/extraction-spec.md +31 -0
  140. graphify/skills/codex/references/github-and-merge.md +46 -0
  141. graphify/skills/codex/references/hooks.md +33 -0
  142. graphify/skills/codex/references/query.md +311 -0
  143. graphify/skills/codex/references/transcribe.md +52 -0
  144. graphify/skills/codex/references/update.md +210 -0
  145. graphify/skills/copilot/references/add-watch.md +56 -0
  146. graphify/skills/copilot/references/exports.md +87 -0
  147. graphify/skills/copilot/references/extraction-spec.md +70 -0
  148. graphify/skills/copilot/references/github-and-merge.md +46 -0
  149. graphify/skills/copilot/references/hooks.md +33 -0
  150. graphify/skills/copilot/references/query.md +311 -0
  151. graphify/skills/copilot/references/transcribe.md +52 -0
  152. graphify/skills/copilot/references/update.md +210 -0
  153. graphify/skills/droid/references/add-watch.md +56 -0
  154. graphify/skills/droid/references/exports.md +87 -0
  155. graphify/skills/droid/references/extraction-spec.md +70 -0
  156. graphify/skills/droid/references/github-and-merge.md +46 -0
  157. graphify/skills/droid/references/hooks.md +33 -0
  158. graphify/skills/droid/references/query.md +311 -0
  159. graphify/skills/droid/references/transcribe.md +52 -0
  160. graphify/skills/droid/references/update.md +210 -0
  161. graphify/skills/kilo/references/add-watch.md +56 -0
  162. graphify/skills/kilo/references/exports.md +87 -0
  163. graphify/skills/kilo/references/extraction-spec.md +70 -0
  164. graphify/skills/kilo/references/github-and-merge.md +46 -0
  165. graphify/skills/kilo/references/hooks.md +33 -0
  166. graphify/skills/kilo/references/query.md +311 -0
  167. graphify/skills/kilo/references/transcribe.md +52 -0
  168. graphify/skills/kilo/references/update.md +210 -0
  169. graphify/skills/kiro/references/add-watch.md +56 -0
  170. graphify/skills/kiro/references/exports.md +87 -0
  171. graphify/skills/kiro/references/extraction-spec.md +31 -0
  172. graphify/skills/kiro/references/github-and-merge.md +46 -0
  173. graphify/skills/kiro/references/hooks.md +33 -0
  174. graphify/skills/kiro/references/query.md +311 -0
  175. graphify/skills/kiro/references/transcribe.md +52 -0
  176. graphify/skills/kiro/references/update.md +210 -0
  177. graphify/skills/opencode/references/add-watch.md +56 -0
  178. graphify/skills/opencode/references/exports.md +87 -0
  179. graphify/skills/opencode/references/extraction-spec.md +70 -0
  180. graphify/skills/opencode/references/github-and-merge.md +46 -0
  181. graphify/skills/opencode/references/hooks.md +33 -0
  182. graphify/skills/opencode/references/query.md +311 -0
  183. graphify/skills/opencode/references/transcribe.md +52 -0
  184. graphify/skills/opencode/references/update.md +210 -0
  185. graphify/skills/pi/references/add-watch.md +56 -0
  186. graphify/skills/pi/references/exports.md +87 -0
  187. graphify/skills/pi/references/extraction-spec.md +31 -0
  188. graphify/skills/pi/references/github-and-merge.md +46 -0
  189. graphify/skills/pi/references/hooks.md +33 -0
  190. graphify/skills/pi/references/query.md +311 -0
  191. graphify/skills/pi/references/transcribe.md +52 -0
  192. graphify/skills/pi/references/update.md +210 -0
  193. graphify/skills/trae/references/add-watch.md +56 -0
  194. graphify/skills/trae/references/exports.md +87 -0
  195. graphify/skills/trae/references/extraction-spec.md +70 -0
  196. graphify/skills/trae/references/github-and-merge.md +46 -0
  197. graphify/skills/trae/references/hooks.md +35 -0
  198. graphify/skills/trae/references/query.md +311 -0
  199. graphify/skills/trae/references/transcribe.md +52 -0
  200. graphify/skills/trae/references/update.md +210 -0
  201. graphify/skills/vscode/references/add-watch.md +56 -0
  202. graphify/skills/vscode/references/exports.md +87 -0
  203. graphify/skills/vscode/references/extraction-spec.md +70 -0
  204. graphify/skills/vscode/references/github-and-merge.md +46 -0
  205. graphify/skills/vscode/references/hooks.md +33 -0
  206. graphify/skills/vscode/references/query.md +311 -0
  207. graphify/skills/vscode/references/transcribe.md +52 -0
  208. graphify/skills/vscode/references/update.md +210 -0
  209. graphify/skills/windows/references/add-watch.md +56 -0
  210. graphify/skills/windows/references/exports.md +87 -0
  211. graphify/skills/windows/references/extraction-spec.md +70 -0
  212. graphify/skills/windows/references/github-and-merge.md +46 -0
  213. graphify/skills/windows/references/hooks.md +33 -0
  214. graphify/skills/windows/references/query.md +311 -0
  215. graphify/skills/windows/references/transcribe.md +52 -0
  216. graphify/skills/windows/references/update.md +210 -0
  217. graphify/symbol_resolution.py +556 -0
  218. graphify/transcribe.py +186 -0
  219. graphify/tree_html.py +603 -0
  220. graphify/validate.py +95 -0
  221. graphify/watch.py +2280 -0
  222. graphify/wiki.py +405 -0
  223. graphitect/__init__.py +28 -0
  224. graphitect/__main__.py +4 -0
  225. graphitect/_vendor/__init__.py +2 -0
  226. graphitect/_vendor/archify/LICENSE +22 -0
  227. graphitect/_vendor/archify/SKILL.md +137 -0
  228. graphitect/_vendor/archify/THIRD_PARTY_NOTICES.md +69 -0
  229. graphitect/_vendor/archify/assets/JetBrainsMono-OFL.txt +93 -0
  230. graphitect/_vendor/archify/assets/template.html +14935 -0
  231. graphitect/_vendor/archify/bin/archify.mjs +2091 -0
  232. graphitect/_vendor/archify/bin/open-artifact.mjs +86 -0
  233. graphitect/_vendor/archify/bin/preview.mjs +653 -0
  234. graphitect/_vendor/archify/bin/visual-check.mjs +829 -0
  235. graphitect/_vendor/archify/brand-marks/README.md +31 -0
  236. graphitect/_vendor/archify/brand-marks/catalog.json +131 -0
  237. graphitect/_vendor/archify/delta/architecture-delta.mjs +1221 -0
  238. graphitect/_vendor/archify/examples/agent-run.lifecycle.json +60 -0
  239. graphitect/_vendor/archify/examples/agent-tool-call.workflow.json +94 -0
  240. graphitect/_vendor/archify/examples/async-job-roundtrip.sequence.json +61 -0
  241. graphitect/_vendor/archify/examples/brand-aware-delivery.architecture.json +47 -0
  242. graphitect/_vendor/archify/examples/cache-miss-request.sequence.json +82 -0
  243. graphitect/_vendor/archify/examples/checkout-platform.base.architecture.json +31 -0
  244. graphitect/_vendor/archify/examples/checkout-platform.head.architecture.json +31 -0
  245. graphitect/_vendor/archify/examples/dataflow-product-analytics.html +15045 -0
  246. graphitect/_vendor/archify/examples/deployment-release.lifecycle.json +49 -0
  247. graphitect/_vendor/archify/examples/event-stream.dataflow.json +57 -0
  248. graphitect/_vendor/archify/examples/incident-response.workflow.json +64 -0
  249. graphitect/_vendor/archify/examples/lifecycle-agent-run.html +14980 -0
  250. graphitect/_vendor/archify/examples/product-analytics.dataflow.json +76 -0
  251. graphitect/_vendor/archify/examples/production-deployment.architecture.json +71 -0
  252. graphitect/_vendor/archify/examples/release-delivery.workflow.json +62 -0
  253. graphitect/_vendor/archify/examples/sequence-cache-miss-request.html +15060 -0
  254. graphitect/_vendor/archify/examples/web-app-rendered.html +15009 -0
  255. graphitect/_vendor/archify/examples/web-app.architecture.json +46 -0
  256. graphitect/_vendor/archify/examples/workflow-agent-tool-call-rendered.html +15051 -0
  257. graphitect/_vendor/archify/migrations/workflow-v2.mjs +279 -0
  258. graphitect/_vendor/archify/package-lock.json +149 -0
  259. graphitect/_vendor/archify/package.json +39 -0
  260. graphitect/_vendor/archify/recipes/scenarios.mjs +391 -0
  261. graphitect/_vendor/archify/references/authoring-contract.md +243 -0
  262. graphitect/_vendor/archify/references/brand-marks.md +65 -0
  263. graphitect/_vendor/archify/references/delivery-contract.md +120 -0
  264. graphitect/_vendor/archify/references/viewer-runtime.md +45 -0
  265. graphitect/_vendor/archify/renderers/architecture/grid.mjs +62 -0
  266. graphitect/_vendor/archify/renderers/architecture/render-architecture.mjs +1078 -0
  267. graphitect/_vendor/archify/renderers/dataflow/README.md +104 -0
  268. graphitect/_vendor/archify/renderers/dataflow/render-dataflow.mjs +483 -0
  269. graphitect/_vendor/archify/renderers/lifecycle/README.md +115 -0
  270. graphitect/_vendor/archify/renderers/lifecycle/render-lifecycle.mjs +561 -0
  271. graphitect/_vendor/archify/renderers/sequence/README.md +114 -0
  272. graphitect/_vendor/archify/renderers/sequence/render-sequence.mjs +464 -0
  273. graphitect/_vendor/archify/renderers/shared/brand-marks.mjs +563 -0
  274. graphitect/_vendor/archify/renderers/shared/cli.mjs +218 -0
  275. graphitect/_vendor/archify/renderers/shared/desktop-readability.mjs +26 -0
  276. graphitect/_vendor/archify/renderers/shared/diagnostics.mjs +127 -0
  277. graphitect/_vendor/archify/renderers/shared/engineering-profiles.mjs +157 -0
  278. graphitect/_vendor/archify/renderers/shared/generated-brand-marks.mjs +2003 -0
  279. graphitect/_vendor/archify/renderers/shared/generated-validators.mjs +13 -0
  280. graphitect/_vendor/archify/renderers/shared/geometry.mjs +1423 -0
  281. graphitect/_vendor/archify/renderers/shared/i18n.mjs +595 -0
  282. graphitect/_vendor/archify/renderers/shared/layout-report.mjs +40 -0
  283. graphitect/_vendor/archify/renderers/shared/legend.mjs +217 -0
  284. graphitect/_vendor/archify/renderers/shared/output-path.mjs +340 -0
  285. graphitect/_vendor/archify/renderers/shared/repository-evidence.mjs +238 -0
  286. graphitect/_vendor/archify/renderers/shared/repository-location.mjs +58 -0
  287. graphitect/_vendor/archify/renderers/shared/text-fit.mjs +49 -0
  288. graphitect/_vendor/archify/renderers/shared/utils.mjs +232 -0
  289. graphitect/_vendor/archify/renderers/shared/validator.mjs +86 -0
  290. graphitect/_vendor/archify/renderers/workflow/README.md +223 -0
  291. graphitect/_vendor/archify/renderers/workflow/render-workflow.mjs +35 -0
  292. graphitect/_vendor/archify/renderers/workflow/workflow-compiler.mjs +4400 -0
  293. graphitect/_vendor/archify/renderers/workflow/workflow-migration-geometry.mjs +144 -0
  294. graphitect/_vendor/archify/schemas/README.md +211 -0
  295. graphitect/_vendor/archify/schemas/architecture.schema.json +178 -0
  296. graphitect/_vendor/archify/schemas/common.schema.json +115 -0
  297. graphitect/_vendor/archify/schemas/dataflow.schema.json +243 -0
  298. graphitect/_vendor/archify/schemas/lifecycle.schema.json +266 -0
  299. graphitect/_vendor/archify/schemas/sequence.schema.json +223 -0
  300. graphitect/_vendor/archify/schemas/workflow.schema.json +428 -0
  301. graphitect/_vendor/archify/scripts/check-render-output.mjs +836 -0
  302. graphitect/_vendor/archify/scripts/check-update.mjs +1667 -0
  303. graphitect/_vendor/archify/scripts/generate-brand-marks.mjs +141 -0
  304. graphitect/_vendor/archify/scripts/generate-validators.mjs +66 -0
  305. graphitect/_vendor/archify/scripts/render-examples.mjs +26 -0
  306. graphitect/_vendor/archify/scripts/update-contract.mjs +182 -0
  307. graphitect/_vendor/archify/skill-release.json +10 -0
  308. graphitect/cli.py +981 -0
  309. graphitect/deliver/__init__.py +5 -0
  310. graphitect/deliver/archify_adapter.py +1877 -0
  311. graphitect/deliver/archify_ir.py +160 -0
  312. graphitect/deliver/archify_repair.py +135 -0
  313. graphitect/deliver/doc_compiler.py +916 -0
  314. graphitect/ground/__init__.py +5 -0
  315. graphitect/ground/describe_source.py +27 -0
  316. graphitect/ground/fullread_source.py +56 -0
  317. graphitect/ground/graphify_source.py +107 -0
  318. graphitect/models.py +118 -0
  319. graphitect/skill/SKILL.md +80 -0
  320. graphitect/skill/agents/openai.yaml +4 -0
  321. graphitect/synthesize/__init__.py +5 -0
  322. graphitect/synthesize/engine.py +281 -0
  323. graphitect/synthesize/llm_backend.py +331 -0
  324. graphitect/synthesize/questions.py +139 -0
  325. graphitect/synthesize/rubric.py +104 -0
  326. graphitect-0.2.0.dist-info/METADATA +284 -0
  327. graphitect-0.2.0.dist-info/RECORD +336 -0
  328. graphitect-0.2.0.dist-info/WHEEL +5 -0
  329. graphitect-0.2.0.dist-info/entry_points.txt +2 -0
  330. graphitect-0.2.0.dist-info/licenses/LICENSE +21 -0
  331. graphitect-0.2.0.dist-info/licenses/LICENSE-ARCHIFY-MIT +22 -0
  332. graphitect-0.2.0.dist-info/licenses/LICENSE-GRAPHIFY-APACHE-2.0 +202 -0
  333. graphitect-0.2.0.dist-info/licenses/LICENSE-GRAPHIFY-MIT +21 -0
  334. graphitect-0.2.0.dist-info/licenses/NOTICE-ARCHIFY-THIRD-PARTY.md +69 -0
  335. graphitect-0.2.0.dist-info/licenses/NOTICE-GRAPHIFY +8 -0
  336. graphitect-0.2.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,916 @@
1
+ """Renders a GroundedUnderstanding to Markdown (secondary export) or a
2
+ designed HTML page (primary export, plan.md §05) - real tables for tech
3
+ choices/tradeoffs, a dynamically computed confirmed/inferred bar chart (not
4
+ hand-tuned per doc, unlike the earlier hand-authored artifacts this borrows
5
+ its visual language from), and an inline diagram sharing node IDs with the
6
+ surrounding prose via a hover-highlight script.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import html as html_lib
12
+ import json
13
+ import re
14
+
15
+ from ..models import Claim, Confidence, DesignDocSection, GroundedUnderstanding, Tradeoff
16
+
17
+ # One-line explanations of what each canonical section (llm_backend's
18
+ # _CANONICAL_SECTIONS) is actually for - Naman's feedback (12 Sep 2026):
19
+ # "Limitations & future work... I don't understand what the section is."
20
+ # A bare heading over a couple of terse claims doesn't tell an unfamiliar
21
+ # reader what kind of content to expect there or why it's separate from,
22
+ # say, "Tradeoffs" - especially true for this one, since "future work" can
23
+ # read as a roadmap commitment rather than what it actually is here: this
24
+ # tool's own inferred suggestions, not something the author has committed
25
+ # to. Not derived from the heading text at runtime - a fixed, hand-written
26
+ # subtitle per canonical heading, shown regardless of what the LLM actually
27
+ # wrote for that section. A non-canonical (hand-authored) heading simply
28
+ # gets no subtitle rather than a guessed one.
29
+ _SECTION_DESCRIPTIONS: dict[str, str] = {
30
+ "Overview": "What this system is and what problem it solves.",
31
+ "Components & responsibilities": "The system's major parts and what each one actually does.",
32
+ "Technology choices & why": "Specific tools and frameworks chosen, and the reasoning behind each.",
33
+ "Tradeoffs & alternatives considered": "Decisions weighed against real alternatives, with their pros and cons.",
34
+ "Key workflows": "How the system behaves end-to-end for its main use cases.",
35
+ "Limitations & future work": (
36
+ "Known gaps or weak points in the current implementation, and what graphitect "
37
+ "suggests addressing next - its own inferred recommendations, not commitments the author has made."
38
+ ),
39
+ }
40
+
41
+
42
+ def _all_claims(section: DesignDocSection) -> list[Claim]:
43
+ """A section's flat claims plus every tradeoff's decision/pros/cons -
44
+ used everywhere a confirmed/inferred count needs to reflect everything
45
+ actually shown for a section, not just the flat list. Without this, the
46
+ stat line and bar chart would silently undercount once tradeoffs
47
+ (12 Sep 2026) added a second place claims live.
48
+ """
49
+ claims = list(section.claims)
50
+ for tradeoff in section.tradeoffs:
51
+ claims.append(tradeoff.decision)
52
+ claims.extend(tradeoff.pros)
53
+ claims.extend(tradeoff.cons)
54
+ return claims
55
+
56
+
57
+ def _cite_bits(claim: Claim) -> list[str]:
58
+ """The raw (unescaped, unwrapped) citation strings for a claim - shared
59
+ by both the Markdown and HTML renderers so the "collapse a redundant
60
+ user citation" rule (below) can't drift between the two formats.
61
+ """
62
+ bits = []
63
+ for c in claim.cites:
64
+ # A user answer's note is often verbatim the claim text itself
65
+ # (apply_answer sets both from the same free-text response) -
66
+ # repeating it in the citation adds nothing, so just say "user" then.
67
+ if c.source == "user" and c.note and c.note.strip() == claim.text.strip():
68
+ bits.append("user")
69
+ else:
70
+ bits.append(c.note or c.file or c.source)
71
+ return [b for b in bits if b]
72
+
73
+
74
+ def _md_cite_str(claim: Claim) -> str:
75
+ bits = _cite_bits(claim)
76
+ return f" ({'; '.join(bits)})" if bits else ""
77
+
78
+
79
+ def _md_claim_line(claim: Claim, *, prefix: str = "- ") -> str:
80
+ return f"{prefix}{claim.text} `{claim.confidence.value}`{_md_cite_str(claim)}"
81
+
82
+
83
+ def _md_tradeoff_block(tradeoff: Tradeoff) -> list[str]:
84
+ decision = tradeoff.decision
85
+ lines = [f"**{decision.text}** `{decision.confidence.value}`{_md_cite_str(decision)}"]
86
+ if tradeoff.alternatives_considered:
87
+ lines.append("")
88
+ lines.append(f"*Alternatives considered: {', '.join(tradeoff.alternatives_considered)}*")
89
+ lines.append("")
90
+ pros_cell = "<br>".join(_md_claim_line(c, prefix="") for c in tradeoff.pros) or "—"
91
+ cons_cell = "<br>".join(_md_claim_line(c, prefix="") for c in tradeoff.cons) or "—"
92
+ lines.append("| Pros | Cons |")
93
+ lines.append("|---|---|")
94
+ lines.append(f"| {pros_cell} | {cons_cell} |")
95
+ lines.append("")
96
+ return lines
97
+
98
+
99
+ def to_markdown(understanding: GroundedUnderstanding, title: str) -> str:
100
+ lines = [f"# {title}", ""]
101
+ confirmed_count = 0
102
+ inferred_count = 0
103
+
104
+ for section in understanding.doc:
105
+ lines.append(f"## {section.heading}")
106
+ description = _SECTION_DESCRIPTIONS.get(section.heading)
107
+ if description:
108
+ lines.append(f"*{description}*")
109
+ lines.append("")
110
+ for claim in section.claims:
111
+ lines.append(_md_claim_line(claim))
112
+ lines.append("")
113
+
114
+ for tradeoff in section.tradeoffs:
115
+ lines.extend(_md_tradeoff_block(tradeoff))
116
+
117
+ for claim in _all_claims(section):
118
+ if claim.confidence == Confidence.CONFIRMED:
119
+ confirmed_count += 1
120
+ else:
121
+ inferred_count += 1
122
+
123
+ total = confirmed_count + inferred_count
124
+ if total:
125
+ lines.append("---")
126
+ lines.append(
127
+ f"**{confirmed_count} of {total}** claims confirmed against code, README, "
128
+ f"git log, docs, or a direct answer. The rest are labeled reasoned judgment, not fact."
129
+ )
130
+
131
+ if understanding.pending_questions:
132
+ lines.append("")
133
+ lines.append(
134
+ f"**{len(understanding.pending_questions)} question(s) pending** - "
135
+ "answer them and re-run with `--answers` to upgrade the affected claims."
136
+ )
137
+
138
+ return "\n".join(lines)
139
+
140
+
141
+ # ---------------------------------------------------------------------------
142
+ # HTML export
143
+ # ---------------------------------------------------------------------------
144
+
145
+ _CSS = """\
146
+ :root{
147
+ --paper:#f6f2e8; --surface:#fffcf4; --ink:#201d17; --ink-muted:#6d6656; --ink-faint:#948c76;
148
+ --line:#ddd4bf; --confirmed:#1f6f5a; --confirmed-soft:#e1efe9; --inferred:#8a611a;
149
+ --inferred-soft:#f3ead7; --gap:#a53f27; --gap-soft:#f5e2da; --focus:#1f6f5a;
150
+ }
151
+ @media (prefers-color-scheme: dark){
152
+ :root:not([data-theme="light"]){
153
+ --paper:#16140f; --surface:#1d1a14; --ink:#ede7d8; --ink-muted:#a39a84; --ink-faint:#786f5b;
154
+ --line:#3a3527; --confirmed:#5fbf9c; --confirmed-soft:#17332a; --inferred:#dba94e;
155
+ --inferred-soft:#362c16; --gap:#e2795a; --gap-soft:#3a2019; --focus:#5fbf9c;
156
+ }
157
+ }
158
+ :root[data-theme="dark"]{
159
+ --paper:#16140f; --surface:#1d1a14; --ink:#ede7d8; --ink-muted:#a39a84; --ink-faint:#786f5b;
160
+ --line:#3a3527; --confirmed:#5fbf9c; --confirmed-soft:#17332a; --inferred:#dba94e;
161
+ --inferred-soft:#362c16; --gap:#e2795a; --gap-soft:#3a2019; --focus:#5fbf9c;
162
+ }
163
+ *{box-sizing:border-box;}
164
+ body{background:var(--paper); color:var(--ink); font-family:'Segoe UI',Arial,sans-serif; line-height:1.62; margin:0;}
165
+ a{color:var(--confirmed);}
166
+ .page{width:min(2400px,calc(100vw - clamp(24px,6vw,160px))); margin:0 auto; padding-block:clamp(32px,4vh,56px) 100px;}
167
+ header,.doc-nav,.explanation-content,footer{max-width:760px; margin-inline:auto;}
168
+ h1,h2{font-family:Georgia,serif; font-weight:600; text-wrap:balance; color:var(--ink);}
169
+ h1{font-size:clamp(1.9rem,4.4vw,2.5rem); line-height:1.14; margin:8px 0 10px;}
170
+ h2{font-size:1.44rem; margin:0 0 6px;}
171
+ p{margin:0 0 14px; max-width:62ch;}
172
+ p.lede{font-size:1.06rem; color:var(--ink-muted); max-width:60ch;}
173
+ strong{color:var(--ink); font-weight:600;}
174
+ .doc-nav{display:flex; flex-wrap:wrap; gap:2px 18px; font-family:Consolas,monospace; font-size:10.5px; letter-spacing:.05em; text-transform:uppercase; color:var(--ink-faint); margin-bottom:44px;}
175
+ .doc-nav a{color:var(--ink-muted); text-decoration:none; border-bottom:1px solid transparent;}
176
+ .doc-nav a:hover{color:var(--confirmed); border-color:var(--confirmed);}
177
+ .report-actions{display:flex; align-items:center; flex-wrap:wrap; gap:9px 12px; margin-top:18px;}
178
+ .print-button{appearance:none; cursor:pointer; border:1px solid var(--confirmed); border-radius:5px; padding:7px 11px; color:var(--surface); background:var(--confirmed); font:600 11px Consolas,monospace; letter-spacing:.02em;}
179
+ .print-button:hover{filter:brightness(.94);}
180
+ .print-hint{font-size:12.5px; color:var(--ink-muted); margin:0;}
181
+ section{margin-top:52px; scroll-margin-top:16px;}
182
+ .kicker{font-family:Consolas,monospace; font-size:11px; letter-spacing:.08em; text-transform:uppercase; color:var(--ink-faint); margin-bottom:6px;}
183
+ .section-desc{font-size:13.5px; color:var(--ink-muted); font-style:italic; margin:-4px 0 16px; max-width:58ch;}
184
+ .explanation-intro{background:var(--surface); border:1px solid var(--line); border-radius:8px; padding:14px 16px; margin:18px 0 24px;}
185
+ .explanation-intro p{font-size:14px; color:var(--ink-muted); margin:0;}
186
+ .node-refs{display:flex; flex-wrap:wrap; gap:6px; margin-bottom:14px;}
187
+ .node-ref{font-family:Consolas,monospace; font-size:10.5px; color:var(--ink-muted); background:var(--surface); border:1px solid var(--line); border-radius:3px; padding:2px 7px; cursor:default;}
188
+ .node-ref.active{color:var(--confirmed); border-color:var(--confirmed); background:var(--confirmed-soft);}
189
+ .ev{display:inline-flex; align-items:baseline; gap:5px; font-family:Consolas,monospace; font-size:10.5px; padding:1.5px 7px 2px; border-radius:3px; white-space:nowrap; margin:0 2px; border:1px solid transparent;}
190
+ .ev.confirmed{background:var(--confirmed-soft); color:var(--confirmed); border-color:color-mix(in srgb, var(--confirmed) 35%, transparent);}
191
+ .ev.inferred{background:var(--inferred-soft); color:var(--inferred); border-color:color-mix(in srgb, var(--inferred) 35%, transparent);}
192
+ .claim-list{list-style:none; margin:0 0 8px; padding:0; display:flex; flex-direction:column; gap:14px;}
193
+ .claim-list li{padding-left:0; border-top:1px solid var(--line); padding-top:14px;}
194
+ .claim-list li:first-child{border-top:none; padding-top:0;}
195
+ code.inline{font-family:Consolas,monospace; font-size:.86em; background:var(--surface); border:1px solid var(--line); border-radius:4px; padding:.08em .38em;}
196
+ .tbl-wrap{overflow-x:auto; margin:18px 0 8px; border:1px solid var(--line); border-radius:8px;}
197
+ table{width:100%; border-collapse:collapse; font-size:13.6px; min-width:480px;}
198
+ th,td{text-align:left; padding:10px 13px; border-bottom:1px solid var(--line); vertical-align:top;}
199
+ th{font-family:Consolas,monospace; font-size:10.5px; letter-spacing:.04em; text-transform:uppercase; color:var(--ink-faint); font-weight:500; background:var(--surface);}
200
+ tr:last-child td{border-bottom:none;}
201
+ .tradeoff{margin:22px 0 6px; padding-top:18px; border-top:1px dashed var(--line);}
202
+ .tradeoff-label{font:10.5px Consolas,monospace; color:var(--ink-faint); letter-spacing:.06em; text-transform:uppercase; margin:0 0 6px;}
203
+ .tradeoff-decision{font-weight:600; margin-bottom:6px;}
204
+ .tradeoff-alts{font-size:12.5px; color:var(--ink-muted); font-style:italic; margin-bottom:4px;}
205
+ .tradeoff-table td{width:50%;}
206
+ .tradeoff-table .claim-list{gap:10px;}
207
+ .tradeoff-table .claim-list li{border-top:none; padding-top:0;}
208
+ .tradeoff-none{color:var(--ink-faint); font-style:italic; list-style:none;}
209
+ .output-section{margin-top:52px; width:100%;}
210
+ .output-heading{max-width:760px; margin-inline:auto;}
211
+ .diagram-controls{display:flex; flex-wrap:wrap; gap:8px; margin:18px 0 0;}
212
+ .diagram-mode-button{appearance:none; cursor:pointer; font:11px Consolas,monospace; letter-spacing:.03em; color:var(--ink-muted); background:var(--surface); border:1px solid var(--line); border-radius:999px; padding:7px 11px;}
213
+ .diagram-mode-button:hover{color:var(--ink); border-color:var(--ink-muted);}
214
+ .diagram-mode-button[aria-selected="true"]{color:var(--surface); background:var(--confirmed); border-color:var(--confirmed);}
215
+ .diagram-mode-note{font-size:12.5px; color:var(--ink-muted); max-width:none; margin:10px 0 -4px;}
216
+ .diagram-mode[hidden]{display:none;}
217
+ .diagram-frame{background:var(--surface); border:1px solid var(--line); border-radius:12px; padding:0; overflow:hidden; margin:18px 0; min-height:clamp(620px,74vh,820px); height:min(78vh,980px);}
218
+ .diagram-frame iframe{display:block; width:100%; height:100%; border:0; background:#07111d;}
219
+ .diagram-canvas{max-width:100%; overflow:auto; scrollbar-gutter:stable both-edges; margin:18px 0; border-radius:12px;}
220
+ .diagram-canvas .diagram-frame{margin:0;}
221
+ .diagram-frame--full{width:1520px; min-width:1520px;}
222
+ .diagram-frame .empty{padding:24px; text-align:center; color:var(--ink-faint); font-family:Consolas,monospace; font-size:12.5px;}
223
+ .print-diagram-wrap{display:none;}
224
+ .legend{display:flex; gap:20px; align-items:center; font-size:12.5px; color:var(--ink-muted); margin-bottom:14px; flex-wrap:wrap;}
225
+ .legend .sw{display:inline-flex; align-items:center; gap:6px;}
226
+ .legend .sw i{width:11px; height:11px; border-radius:2.5px; display:inline-block;}
227
+ .stat-line{font-family:Consolas,monospace; font-size:13px; color:var(--ink-muted); margin-top:10px;}
228
+ .stat-line b{color:var(--ink); font-weight:600;}
229
+ .callout{background:var(--surface); border:1px solid var(--line); border-left:3px solid var(--gap); border-radius:0 8px 8px 0; padding:15px 18px; font-size:14px; margin:22px 0;}
230
+ .callout .lbl-top{font-family:Consolas,monospace; font-size:10.5px; text-transform:uppercase; letter-spacing:.06em; color:var(--gap); display:block; margin-bottom:6px;}
231
+ footer{margin-top:64px; padding-top:20px; border-top:1px solid var(--line); font-size:12.5px; color:var(--ink-faint); font-family:Consolas,monospace;}
232
+ @media(max-width:800px){.page{width:calc(100vw - 32px);padding-block-start:28px}.diagram-frame{min-height:520px;height:72vh}.diagram-frame--full{width:1260px;min-width:1260px}}
233
+ @page{size:A4; margin:14mm;}
234
+ @media print{
235
+ :root{color-scheme:light;}
236
+ *{-webkit-print-color-adjust:exact; print-color-adjust:exact;}
237
+ html,body{background:#fff!important; color:#201d17!important;}
238
+ body{font-size:10pt; line-height:1.48; padding:0;}
239
+ .page{max-width:none; padding:0;}
240
+ header,.explanation-content,footer{max-width:none;}
241
+ .doc-nav,.report-actions,.diagram-modes{display:none!important;}
242
+ .output-section{margin-top:24px;}
243
+ h1{font-size:25pt;}
244
+ h2{font-size:16pt; break-after:avoid-page;}
245
+ .kicker{font-size:8.5pt;}
246
+ .print-diagram-wrap{display:block!important; margin:12px 0 0; break-inside:avoid-page;}
247
+ .print-diagram-caption{color:#6d6656; font-size:9pt; margin:0 0 7px;}
248
+ .print-diagram{border:1px solid #ddd4bf; border-radius:6px; overflow:hidden;}
249
+ .tbl-wrap{overflow:visible;}
250
+ table{min-width:0; font-size:9.1pt;}
251
+ th,td{padding:7px 8px;}
252
+ .tradeoff{break-inside:avoid-page;}
253
+ .node-refs{display:none;}
254
+ footer{margin-top:30px;}
255
+ }
256
+ """
257
+
258
+ _HIGHLIGHT_SCRIPT = """\
259
+ (function(){
260
+ function activeFrame(){
261
+ return document.querySelector('.diagram-mode:not([hidden]) iframe') || document.getElementById('graphitect-diagram-frame');
262
+ }
263
+ function selectMode(mode){
264
+ document.querySelectorAll('[data-diagram-mode]').forEach(function(panel){
265
+ panel.hidden = panel.getAttribute('data-diagram-mode') !== mode;
266
+ });
267
+ document.querySelectorAll('[data-diagram-mode-button]').forEach(function(button){
268
+ button.setAttribute('aria-selected', String(button.getAttribute('data-diagram-mode-button') === mode));
269
+ });
270
+ var note = document.querySelector('[data-diagram-mode-note]');
271
+ if (note) note.textContent = mode === 'sequence'
272
+ ? 'Each arrow is a direct extracted Graphify calls/invokes edge. This is not runtime request telemetry, timing data, or a captured trace.'
273
+ : mode === 'story'
274
+ ? 'Each chapter follows directly observed component relationships. Present story only changes the stage; enable Live and Play story when you are ready for the paced walkthrough.'
275
+ : mode === 'full'
276
+ ? 'All detected relationships between the rendered components. On large codebases, those components are Graphify community rollups; this pane scrolls horizontally when needed.'
277
+ : 'A readable structural subset. Switch to a Sequence trace, guided Workflow story, or Full architecture rollup for more detail.';
278
+ }
279
+ document.querySelectorAll('[data-diagram-mode-button]').forEach(function(button){
280
+ button.addEventListener('click', function(){ selectMode(button.getAttribute('data-diagram-mode-button')); });
281
+ });
282
+ function useStoryFrame(callback){
283
+ var frame = document.getElementById('graphitect-story-diagram-frame');
284
+ if (!frame) return;
285
+ function run(){
286
+ try { if (frame.contentDocument) callback(frame.contentDocument); }
287
+ catch (_) { /* Never break the report if a browser blocks iframe access. */ }
288
+ }
289
+ if (frame.contentDocument && frame.contentDocument.readyState === 'complete') run();
290
+ else frame.addEventListener('load', run, {once:true});
291
+ }
292
+ function chooseStoryView(doc, id){
293
+ if (!id) return;
294
+ Array.prototype.slice.call(doc.querySelectorAll('[data-guided-view-id]')).some(function(button){
295
+ if (button.getAttribute('data-guided-view-id') !== id) return false;
296
+ button.click();
297
+ return true;
298
+ });
299
+ }
300
+ function runStory(options){
301
+ selectMode('story');
302
+ useStoryFrame(function(doc){
303
+ chooseStoryView(doc, options.view);
304
+ var present = doc.getElementById('btn-present');
305
+ if (options.present && present && doc.documentElement.getAttribute('data-present') !== 'true') present.click();
306
+ var motion = doc.getElementById('btn-motion');
307
+ if (options.play && motion && motion.getAttribute('aria-pressed') !== 'true') motion.click();
308
+ var play = doc.getElementById('guided-view-play');
309
+ if (options.play && play && play.getAttribute('aria-pressed') !== 'true') play.click();
310
+ });
311
+ }
312
+ var presentStory = document.querySelector('[data-present-story]');
313
+ if (presentStory) presentStory.addEventListener('click', function(){ runStory({present:true, play:false}); });
314
+ var query = new URLSearchParams(window.location.search);
315
+ var hash = new URLSearchParams(window.location.hash.replace(/^#/, ''));
316
+ if (document.getElementById('graphitect-story-diagram-frame') &&
317
+ (query.get('present') === '1' || query.get('play') === '1' || hash.get('view'))) {
318
+ runStory({present:query.get('present') === '1', play:query.get('play') === '1', view:hash.get('view')});
319
+ }
320
+ var printButton = document.querySelector('[data-print-report]');
321
+ if (printButton) printButton.addEventListener('click', function(){ window.print(); });
322
+ var printHost = document.getElementById('graphitect-print-diagram');
323
+ var printTemplate = document.getElementById('graphitect-print-diagram-template');
324
+ if (printHost && printTemplate) {
325
+ var printRoot = printHost.attachShadow ? printHost.attachShadow({mode:'open'}) : printHost;
326
+ printRoot.appendChild(printTemplate.content.cloneNode(true));
327
+ }
328
+ function hoverDetails(doc){
329
+ var element = doc.getElementById('graphitect-hover-details');
330
+ if (!element) return {};
331
+ try { return JSON.parse(element.textContent || '{}'); }
332
+ catch (_) { return {}; }
333
+ }
334
+ function installDiagramHovers(frame){
335
+ function install(doc){
336
+ if (!doc || doc.getElementById('graphitect-node-tooltip')) return;
337
+ var details = hoverDetails(doc);
338
+ var tooltip = doc.createElement('div');
339
+ tooltip.id = 'graphitect-node-tooltip';
340
+ tooltip.setAttribute('role', 'tooltip');
341
+ tooltip.style.cssText = 'position:fixed;z-index:2147483647;display:none;max-width:320px;padding:10px 12px;border:1px solid #5eead4;border-radius:8px;background:#061827;color:#e6fffb;box-shadow:0 8px 28px rgba(0,0,0,.42);font:12px/1.45 ui-monospace,SFMono-Regular,Consolas,monospace;pointer-events:none;';
342
+ doc.body.appendChild(tooltip);
343
+ function nodeLabel(id){
344
+ var node = doc.querySelector('[data-node-id="' + CSS.escape(id) + '"]');
345
+ return node ? (node.getAttribute('data-node-label') || id) : id;
346
+ }
347
+ function linksFor(id, direction){
348
+ var selector = direction === 'incoming' ? '[data-edge-to="' : '[data-edge-from="';
349
+ var attribute = direction === 'incoming' ? 'data-edge-from' : 'data-edge-to';
350
+ return Array.prototype.slice.call(doc.querySelectorAll(selector + CSS.escape(id) + '"]'))
351
+ .slice(0, 3)
352
+ .map(function(edge){
353
+ var relation = edge.getAttribute('data-edge-label') || 'relates to';
354
+ return relation + ' ' + nodeLabel(edge.getAttribute(attribute) || 'component');
355
+ });
356
+ }
357
+ function addLine(text, strong){
358
+ var line = doc.createElement('div');
359
+ line.textContent = text;
360
+ if (strong) line.style.fontWeight = '700';
361
+ tooltip.appendChild(line);
362
+ }
363
+ function show(node, event){
364
+ var id = node.getAttribute('data-node-id') || '';
365
+ var detail = details[id] || {};
366
+ tooltip.replaceChildren();
367
+ addLine(node.getAttribute('data-node-label') || id, true);
368
+ if (detail.summary) addLine(detail.summary);
369
+ if (detail.symbol) addLine('Code symbol: ' + detail.symbol);
370
+ var source = detail.source || node.getAttribute('data-node-sublabel');
371
+ if (source) addLine('Source: ' + source);
372
+ var incoming = linksFor(id, 'incoming');
373
+ var outgoing = linksFor(id, 'outgoing');
374
+ if (incoming.length) addLine('Incoming: ' + incoming.join('; '));
375
+ if (outgoing.length) addLine('Outgoing: ' + outgoing.join('; '));
376
+ var point = event && typeof event.clientX === 'number'
377
+ ? {x:event.clientX, y:event.clientY}
378
+ : (function(){ var box = node.getBoundingClientRect(); return {x:box.left, y:box.bottom}; })();
379
+ tooltip.style.left = Math.max(8, Math.min(point.x + 14, doc.defaultView.innerWidth - 332)) + 'px';
380
+ tooltip.style.top = Math.max(8, Math.min(point.y + 14, doc.defaultView.innerHeight - 180)) + 'px';
381
+ tooltip.style.display = 'block';
382
+ }
383
+ function hide(){ tooltip.style.display = 'none'; }
384
+ Array.prototype.slice.call(doc.querySelectorAll('[data-node-id]')).forEach(function(node){
385
+ node.addEventListener('pointerenter', function(event){ show(node, event); });
386
+ node.addEventListener('pointermove', function(event){ show(node, event); });
387
+ node.addEventListener('pointerleave', hide);
388
+ node.addEventListener('focus', function(){ show(node, null); });
389
+ node.addEventListener('blur', hide);
390
+ node.addEventListener('keydown', function(event){ if (event.key === 'Escape') hide(); });
391
+ });
392
+ }
393
+ try {
394
+ if (frame.contentDocument && frame.contentDocument.readyState === 'complete') install(frame.contentDocument);
395
+ else frame.addEventListener('load', function(){ install(frame.contentDocument); }, {once:true});
396
+ } catch (_) { /* Browsers may block iframe access; the report still works. */ }
397
+ }
398
+ document.querySelectorAll('.diagram-mode iframe').forEach(installDiagramHovers);
399
+ document.querySelectorAll('[data-node-ref]').forEach(function(el){
400
+ var id = el.getAttribute('data-node-ref');
401
+ function target(){
402
+ var frame = activeFrame();
403
+ try { return frame && frame.contentDocument.querySelector('[id="' + id + '"]'); }
404
+ catch (_) { return null; }
405
+ }
406
+ el.addEventListener('mouseenter', function(){
407
+ var node = target();
408
+ if (!node) return;
409
+ el.classList.add('active');
410
+ node.style.filter = 'drop-shadow(0 0 8px #5fbf9c)';
411
+ });
412
+ el.addEventListener('mouseleave', function(){
413
+ var node = target();
414
+ el.classList.remove('active');
415
+ if (node) node.style.removeProperty('filter');
416
+ });
417
+ });
418
+ })();
419
+ """
420
+
421
+
422
+ def _embed_hover_details(viewer_html: str, details: dict[str, dict[str, str]] | None) -> str:
423
+ """Attach report-only, escaped hover metadata to an Archify viewer copy."""
424
+ if not details:
425
+ return viewer_html
426
+ payload = json.dumps(details, ensure_ascii=False, separators=(",", ":")).replace("</", "<\\/")
427
+ marker = f'<script id="graphitect-hover-details" type="application/json">{payload}</script>'
428
+ if "</body>" in viewer_html.lower():
429
+ return re.sub(r"</body>", marker + "</body>", viewer_html, count=1, flags=re.IGNORECASE)
430
+ return viewer_html + marker
431
+
432
+
433
+ def _present_embedded_viewer(viewer_html: str) -> str:
434
+ """Start a complete Archify viewer in its responsive presentation stage.
435
+
436
+ The report still embeds the original viewer HTML in ``iframe.srcdoc``.
437
+ Presentation Stage only gives that live viewer the iframe's viewport; its
438
+ pan/zoom, theme, route, story, and export controls remain its own runtime.
439
+ """
440
+
441
+ def replace_html_tag(match: re.Match[str]) -> str:
442
+ attrs = re.sub(
443
+ r'\sdata-present(?=\s|=|$)(?:\s*=\s*(?:"[^"]*"|\'[^\']*\'|[^\s>]+))?',
444
+ "",
445
+ match.group(1),
446
+ flags=re.IGNORECASE,
447
+ )
448
+ return f'<html{attrs} data-present="true">'
449
+
450
+ return re.sub(r"<html\b([^>]*)>", replace_html_tag, viewer_html, count=1, flags=re.IGNORECASE)
451
+
452
+
453
+ def _esc(text: str) -> str:
454
+ return html_lib.escape(text, quote=True)
455
+
456
+
457
+ def _slug(text: str) -> str:
458
+ return "".join(c if c.isalnum() else "-" for c in text.lower()).strip("-")
459
+
460
+
461
+ def _cite_str(claim: Claim) -> str:
462
+ bits = _cite_bits(claim)
463
+ return " &middot; " + "; ".join(_esc(b) for b in bits) if bits else ""
464
+
465
+
466
+ def _render_section_description(heading: str) -> str:
467
+ description = _SECTION_DESCRIPTIONS.get(heading)
468
+ return f'<p class="section-desc">{_esc(description)}</p>' if description else ""
469
+
470
+
471
+ def _render_node_refs(section: DesignDocSection, node_id_remap: dict[str, str] | None = None) -> str:
472
+ if not section.related_node_ids:
473
+ return ""
474
+ remap = node_id_remap or {}
475
+ # The pill's visible text always stays the real raw node id - that's the
476
+ # actual citation and is meaningful to a reader. data-node-ref is what
477
+ # _HIGHLIGHT_SCRIPT looks up in the rendered diagram, though, and once a
478
+ # large graph's diagram is aggregated into community boxes those raw ids
479
+ # no longer exist as SVG element ids at all - the highlight silently
480
+ # matched nothing. remap.get(nid, nid) points the lookup at whatever id
481
+ # the node actually renders as (unchanged when no aggregation happened).
482
+ pills = "".join(
483
+ f'<span class="node-ref" data-node-ref="{_esc(remap.get(nid, nid))}">{_esc(nid)}</span>'
484
+ for nid in section.related_node_ids
485
+ )
486
+ return f'<div class="node-refs">{pills}</div>'
487
+
488
+
489
+ def _render_claim_items(claims: list[Claim]) -> str:
490
+ """The <li> markup shared by _render_claim_list and the tradeoff
491
+ pros/cons table cells - a claim's text + confidence badge + citation.
492
+ """
493
+ items = []
494
+ for claim in claims:
495
+ tag = claim.confidence.value
496
+ items.append(
497
+ f'<li><span>{_esc(claim.text)}</span> '
498
+ f'<span class="ev {tag}">{tag}</span>{_cite_str(claim)}</li>'
499
+ )
500
+ return "".join(items)
501
+
502
+
503
+ def _render_claim_list(claims: list[Claim]) -> str:
504
+ return f'<ul class="claim-list">{_render_claim_items(claims)}</ul>'
505
+
506
+
507
+ def _render_tradeoffs(section: DesignDocSection) -> str:
508
+ """A real pros/cons comparison per significant decision (Naman's
509
+ request, 12 Sep 2026) - not just the flat claim list above. Every piece
510
+ of text here is still a confidence-tagged, cited Claim (decision, and
511
+ each pro/con) so a table row can't read as more certain than the rest
512
+ of the doc allows.
513
+ """
514
+ if not section.tradeoffs:
515
+ return ""
516
+ blocks = []
517
+ for tradeoff in section.tradeoffs:
518
+ decision = tradeoff.decision
519
+ tag = decision.confidence.value
520
+ alts_html = ""
521
+ if tradeoff.alternatives_considered:
522
+ alt_text = ", ".join(_esc(a) for a in tradeoff.alternatives_considered)
523
+ alts_html = f'<div class="tradeoff-alts">Alternatives considered: {alt_text}</div>'
524
+ pros_html = _render_claim_items(tradeoff.pros) or '<li class="tradeoff-none">None noted</li>'
525
+ cons_html = _render_claim_items(tradeoff.cons) or '<li class="tradeoff-none">None noted</li>'
526
+ blocks.append(
527
+ '<div class="tradeoff">'
528
+ '<p class="tradeoff-label">Decision analysis</p>'
529
+ f'<div class="tradeoff-decision"><span>{_esc(decision.text)}</span> '
530
+ f'<span class="ev {tag}">{tag}</span>{_cite_str(decision)}</div>'
531
+ f"{alts_html}"
532
+ '<div class="tbl-wrap"><table class="tradeoff-table">'
533
+ "<thead><tr><th>Pros</th><th>Cons</th></tr></thead>"
534
+ f'<tbody><tr><td><ul class="claim-list">{pros_html}</ul></td>'
535
+ f'<td><ul class="claim-list">{cons_html}</ul></td></tr></tbody>'
536
+ "</table></div>"
537
+ "</div>"
538
+ )
539
+ return "".join(blocks)
540
+
541
+
542
+ def _render_claim_table(claims: list[Claim]) -> str:
543
+ rows = []
544
+ for claim in claims:
545
+ tag = claim.confidence.value
546
+ rows.append(
547
+ f"<tr><td>{_esc(claim.text)}</td>"
548
+ f'<td><span class="ev {tag}">{tag}</span>{_cite_str(claim)}</td></tr>'
549
+ )
550
+ return (
551
+ '<div class="tbl-wrap choice-table"><table><thead><tr><th>Choice &amp; reasoning</th><th>Evidence</th></tr></thead>'
552
+ f"<tbody>{''.join(rows)}</tbody></table></div>"
553
+ )
554
+
555
+ # Sections with these headings render as a table (claim + confidence side by
556
+ # side reads better for a list of discrete choices); everything else renders
557
+ # as the claim-list bullet style - matches the visual convention of prior
558
+ # hand-authored graphitect docs (see the git-resume-agent design doc).
559
+ _TABLE_SECTIONS = {"Technology choices & why"}
560
+
561
+
562
+ def _render_bar_chart(understanding: GroundedUnderstanding) -> str:
563
+ """A confirmed/inferred bar per section, computed from the actual claim
564
+ data - not hand-tuned per document the way earlier prototype docs were.
565
+ """
566
+ rows = []
567
+ for section in understanding.doc:
568
+ all_claims = _all_claims(section)
569
+ confirmed = sum(1 for c in all_claims if c.confidence == Confidence.CONFIRMED)
570
+ inferred = sum(1 for c in all_claims if c.confidence == Confidence.INFERRED)
571
+ if confirmed or inferred:
572
+ rows.append((section.heading, confirmed, inferred))
573
+ if not rows:
574
+ return ""
575
+
576
+ row_h, gap, bar_x, px_per_claim = 26, 14, 160, 34
577
+ height = len(rows) * (row_h + gap) + gap
578
+ max_total = max(c + i for _, c, i in rows) or 1
579
+ width = bar_x + max_total * px_per_claim + 60
580
+
581
+ svg_rows = []
582
+ for idx, (heading, confirmed, inferred) in enumerate(rows):
583
+ y = gap + idx * (row_h + gap)
584
+ svg_rows.append(
585
+ f'<text x="{bar_x - 10}" y="{y + row_h * 0.7:.0f}" text-anchor="end" '
586
+ f'font-size="11.5" fill="var(--ink-muted)">{_esc(heading)}</text>'
587
+ )
588
+ x = bar_x
589
+ if confirmed:
590
+ w = confirmed * px_per_claim
591
+ svg_rows.append(
592
+ f'<rect x="{x}" y="{y}" width="{w}" height="{row_h}" rx="4" fill="var(--confirmed)"/>'
593
+ f'<text x="{x + w/2:.0f}" y="{y + row_h*0.68:.0f}" text-anchor="middle" '
594
+ f'font-size="10.5" fill="#fff" font-weight="600">{confirmed}</text>'
595
+ )
596
+ x += w
597
+ if inferred:
598
+ w = inferred * px_per_claim
599
+ svg_rows.append(
600
+ f'<rect x="{x}" y="{y}" width="{w}" height="{row_h}" rx="4" fill="var(--inferred)"/>'
601
+ f'<text x="{x + w/2:.0f}" y="{y + row_h*0.68:.0f}" text-anchor="middle" '
602
+ f'font-size="10.5" fill="var(--ink)" font-weight="600">{inferred}</text>'
603
+ )
604
+
605
+ return (
606
+ f'<div class="diagram-frame" style="padding:16px;">'
607
+ f'<svg viewBox="0 0 {width} {height}" role="img" aria-label="Confirmed versus inferred claim counts by section.">'
608
+ f'<g font-family="Source Sans 3, sans-serif">{"".join(svg_rows)}</g></svg></div>'
609
+ )
610
+
611
+
612
+ def _extract_diagram_assets(diagram_html_or_svg: str) -> tuple[str, str]:
613
+ """Extract archify's `<svg>` plus whichever of its `<style>` blocks the
614
+ SVG actually depends on, from a full rendered HTML page.
615
+
616
+ Confirmed live (12 Sep 2026) that a bare svg-markup extraction isn't
617
+ enough: archify's diagram styles its elements entirely through CSS
618
+ classes (`c-backend`, `m-security`, etc.) defined in the page's own
619
+ `<style>` blocks, not inline attributes - embedding the SVG alone
620
+ rendered as an unstyled, visually broken pattern-fill mess instead of a
621
+ real diagram. The style block(s) get returned separately (not spliced
622
+ into the svg itself) because they must be mounted in a shadow root, not
623
+ the light DOM - archify's stylesheet defines global `body {}` and
624
+ `:root` rules that would otherwise leak out and collide with this page's
625
+ own styling (also confirmed live, not theoretical).
626
+
627
+ Only style blocks that actually reference a class used inside the
628
+ extracted SVG are kept - archify's page also ships a large embedded
629
+ webfont block that has nothing to do with rendering the diagram
630
+ correctly and would otherwise roughly double the file size for no
631
+ visual benefit.
632
+
633
+ Falls back to (input, "") unchanged if it's already a bare SVG (starts
634
+ with `<svg`, no surrounding page to pull styles from).
635
+ """
636
+ lower = diagram_html_or_svg.lower()
637
+ start = lower.find("<svg")
638
+ if start == -1:
639
+ return diagram_html_or_svg, ""
640
+ end = lower.rfind("</svg>")
641
+ if end == -1:
642
+ return diagram_html_or_svg, ""
643
+ svg = diagram_html_or_svg[start : end + len("</svg>")]
644
+
645
+ used_classes = set(re.findall(r'class="([^"]+)"', svg))
646
+ class_tokens = {tok for group in used_classes for tok in group.split()}
647
+ if not class_tokens:
648
+ return svg, ""
649
+
650
+ style_blocks = re.findall(r"<style[^>]*>(.*?)</style>", diagram_html_or_svg, re.DOTALL)
651
+ relevant = [
652
+ block for block in style_blocks if any(f".{tok}" in block for tok in class_tokens)
653
+ ]
654
+ combined = "\n".join(relevant)
655
+ # :root only ever matches the real document root, never a shadow tree -
656
+ # confirmed live: the theme CSS custom properties (--bg, --text, ...)
657
+ # archify defines on :root silently resolved to nothing inside the
658
+ # shadow root, rendering as a solid black box with invisible text
659
+ # instead of the intended dark-themed diagram with visible labels.
660
+ # :host is the shadow-DOM equivalent - rewriting makes those variables
661
+ # actually resolve within the mounted tree.
662
+ combined = re.sub(r":root\b", ":host", combined)
663
+ return svg, combined
664
+
665
+
666
+ def to_html(
667
+ understanding: GroundedUnderstanding,
668
+ title: str,
669
+ *,
670
+ diagram_svg: str | None = None,
671
+ story_diagram_svg: str | None = None,
672
+ story_hover_details: dict[str, dict[str, str]] | None = None,
673
+ sequence_diagram_svg: str | None = None,
674
+ full_diagram_svg: str | None = None,
675
+ story_chapter_count: int | None = None,
676
+ sequence_message_count: int | None = None,
677
+ overview_connection_count: int | None = None,
678
+ full_connection_count: int | None = None,
679
+ node_id_remap: dict[str, str] | None = None,
680
+ explanation_available: bool = True,
681
+ diagram_opted_out: bool = False,
682
+ ) -> str:
683
+ """Render one HTML report containing the complete Archify viewer.
684
+
685
+ ``diagram_svg`` keeps its old public name for compatibility, but now
686
+ accepts the complete Archify HTML artifact. ``iframe.srcdoc`` starts its
687
+ responsive presentation stage while preserving themes, navigation, route
688
+ tools, guided views, and exports without a second output file.
689
+ """
690
+ visible_doc = understanding.doc if explanation_available else []
691
+ nav_items = [("Diagram", "interactive-diagram"), ("Explanation", "project-explanation")]
692
+ nav_items.extend((s.heading, _slug(s.heading)) for s in visible_doc)
693
+ nav_html = "".join(f'<a href="#{slug}">{_esc(h)}</a>' for h, slug in nav_items)
694
+
695
+ sections_html = []
696
+ confirmed_total = inferred_total = 0
697
+ for section in visible_doc:
698
+ for c in _all_claims(section):
699
+ if c.confidence == Confidence.CONFIRMED:
700
+ confirmed_total += 1
701
+ else:
702
+ inferred_total += 1
703
+ body = (
704
+ _render_claim_table(section.claims)
705
+ if section.heading in _TABLE_SECTIONS
706
+ else _render_claim_list(section.claims)
707
+ )
708
+ sections_html.append(
709
+ f'<section id="{_slug(section.heading)}">'
710
+ f'<h2>{_esc(section.heading)}</h2>'
711
+ f"{_render_section_description(section.heading)}"
712
+ f"{_render_node_refs(section, node_id_remap)}"
713
+ f"{body}"
714
+ f"{_render_tradeoffs(section)}"
715
+ f"</section>"
716
+ )
717
+
718
+ if diagram_svg:
719
+ viewer_srcdoc = html_lib.escape(_present_embedded_viewer(diagram_svg), quote=True)
720
+ printable_svg, printable_styles = _extract_diagram_assets(diagram_svg)
721
+ print_diagram_block = (
722
+ '<div class="print-diagram-wrap">'
723
+ '<p class="print-diagram-caption">Printable structural overview. Open the HTML report to explore the full graph.</p>'
724
+ '<div class="print-diagram" id="graphitect-print-diagram"></div>'
725
+ '<template id="graphitect-print-diagram-template">'
726
+ '<style>:host{display:block;-webkit-print-color-adjust:exact;print-color-adjust:exact;}'
727
+ ':host svg{display:block;width:100%!important;height:auto!important;}</style>'
728
+ f'<style>{printable_styles}</style>{printable_svg}</template></div>'
729
+ )
730
+ if sequence_diagram_svg or story_diagram_svg or full_diagram_svg:
731
+ overview_label = "Overview"
732
+ if overview_connection_count is not None:
733
+ overview_label += f" · {overview_connection_count} key links"
734
+ full_label = "Full architecture rollup"
735
+ if full_connection_count is not None:
736
+ full_label += f" · {full_connection_count} relationships"
737
+ sequence_label = "Sequence trace"
738
+ if sequence_message_count is not None:
739
+ sequence_label += f" · {sequence_message_count} direct calls"
740
+ story_label = "Workflow story"
741
+ if story_chapter_count is not None:
742
+ story_label += f" · {story_chapter_count} chapters"
743
+
744
+ # A direct static call chain is the clearest non-jumpy starting
745
+ # point. Fall back to the workflow, then the structural overview.
746
+ active_mode = (
747
+ "sequence"
748
+ if sequence_diagram_svg
749
+ else "story"
750
+ if story_diagram_svg
751
+ else "overview"
752
+ )
753
+
754
+ def mode_button(mode: str, label: str) -> str:
755
+ return (
756
+ f'<button type="button" class="diagram-mode-button" '
757
+ f'data-diagram-mode-button="{mode}" '
758
+ f'aria-selected="{str(mode == active_mode).lower()}">{_esc(label)}</button>'
759
+ )
760
+
761
+ def mode_panel(mode: str, frame_id: str, frame_title: str, srcdoc: str) -> str:
762
+ hidden = " hidden" if mode != active_mode else ""
763
+ return (
764
+ f'<div class="diagram-mode" data-diagram-mode="{mode}"{hidden}>'
765
+ '<div class="diagram-frame">'
766
+ f'<iframe id="{frame_id}" title="{frame_title}" srcdoc="{srcdoc}"></iframe>'
767
+ '</div></div>'
768
+ )
769
+
770
+ controls = []
771
+ panels = []
772
+ if sequence_diagram_svg:
773
+ sequence_viewer_srcdoc = html_lib.escape(
774
+ _present_embedded_viewer(sequence_diagram_svg), quote=True
775
+ )
776
+ controls.append(mode_button("sequence", sequence_label))
777
+ panels.append(
778
+ mode_panel(
779
+ "sequence",
780
+ "graphitect-sequence-diagram-frame",
781
+ "Evidence-gated Archify static call sequence",
782
+ sequence_viewer_srcdoc,
783
+ )
784
+ )
785
+ if story_diagram_svg:
786
+ story_viewer_srcdoc = html_lib.escape(
787
+ _present_embedded_viewer(
788
+ _embed_hover_details(story_diagram_svg, story_hover_details)
789
+ ),
790
+ quote=True,
791
+ )
792
+ controls.append(mode_button("story", story_label))
793
+ panels.append(
794
+ mode_panel(
795
+ "story",
796
+ "graphitect-story-diagram-frame",
797
+ "Guided Archify workflow story",
798
+ story_viewer_srcdoc,
799
+ )
800
+ )
801
+ controls.append(mode_button("overview", overview_label))
802
+ panels.append(
803
+ mode_panel(
804
+ "overview",
805
+ "graphitect-diagram-frame",
806
+ "Overview Archify system diagram",
807
+ viewer_srcdoc,
808
+ )
809
+ )
810
+ if full_diagram_svg:
811
+ full_viewer_srcdoc = html_lib.escape(
812
+ _present_embedded_viewer(full_diagram_svg), quote=True
813
+ )
814
+ controls.append(mode_button("full", full_label))
815
+ full_hidden = " hidden" if active_mode != "full" else ""
816
+ panels.append(
817
+ f'<div class="diagram-mode" data-diagram-mode="full"{full_hidden}>'
818
+ '<div class="diagram-canvas" aria-label="Scrollable full relationship graph">'
819
+ '<div class="diagram-frame diagram-frame--full">'
820
+ '<iframe id="graphitect-full-diagram-frame" title="Full Archify architecture rollup" '
821
+ f'srcdoc="{full_viewer_srcdoc}"></iframe></div></div></div>'
822
+ )
823
+ if story_diagram_svg:
824
+ controls.append(
825
+ '<button type="button" class="diagram-mode-button" data-present-story>Present story</button>'
826
+ )
827
+
828
+ initial_note = (
829
+ 'Each arrow is a direct extracted Graphify calls/invokes edge. This is not runtime request telemetry, timing data, or a captured trace.'
830
+ if active_mode == "sequence"
831
+ else 'The workflow follows one direct code path in order. Present story only changes the stage; enable Live and Play story when you are ready for the paced walkthrough.'
832
+ if active_mode == "story"
833
+ else 'A readable structural subset. Switch modes to inspect the evidence-gated sequence, workflow, or full relationship rollup.'
834
+ )
835
+ diagram_block = (
836
+ '<div class="diagram-modes">'
837
+ '<div class="diagram-controls" role="tablist" aria-label="Diagram detail">'
838
+ f'{"".join(controls)}</div>'
839
+ '<p class="diagram-mode-note" data-diagram-mode-note aria-live="polite">'
840
+ f'{initial_note}</p>{"".join(panels)}</div>'
841
+ )
842
+ else:
843
+ diagram_block = (
844
+ '<div class="diagram-frame">'
845
+ '<iframe id="graphitect-diagram-frame" title="Interactive Archify system diagram" '
846
+ f'srcdoc="{viewer_srcdoc}"></iframe>'
847
+ '</div>'
848
+ )
849
+ else:
850
+ print_diagram_block = ""
851
+ empty_message = (
852
+ "Diagram generation was explicitly opted out."
853
+ if diagram_opted_out
854
+ else "The bundled Archify renderer could not produce a diagram."
855
+ )
856
+ diagram_block = (
857
+ '<div class="diagram-frame"><div class="empty">'
858
+ f"{_esc(empty_message)}</div></div>"
859
+ )
860
+
861
+ total = confirmed_total + inferred_total
862
+ stat_line = (
863
+ f'<p class="stat-line"><b>{confirmed_total} of {total}</b> claims confirmed against '
864
+ "code, README, git log, docs, or a direct answer. The rest are labeled reasoned "
865
+ "judgment, not fact.</p>"
866
+ if total
867
+ else ""
868
+ )
869
+ pending_callout = (
870
+ f'<div class="callout"><span class="lbl-top">Pending</span>'
871
+ f"<p><b>{len(understanding.pending_questions)}</b> question(s) still unanswered - "
872
+ "answer them and re-run with <code class=\"inline\">--answers</code> to upgrade the "
873
+ "affected claims.</p></div>"
874
+ if explanation_available and understanding.pending_questions
875
+ else ""
876
+ )
877
+
878
+ if explanation_available:
879
+ explanation_block = (
880
+ '<div class="explanation-intro"><p>Each statement explains what the repository shows, why a choice matters here, and what it costs. '
881
+ 'Source-backed facts are marked confirmed; reasoned analysis is marked inferred.</p></div>'
882
+ '<div class="legend">'
883
+ '<span class="sw"><i style="background:var(--confirmed)"></i> confirmed</span>'
884
+ '<span class="sw"><i style="background:var(--inferred)"></i> inferred</span>'
885
+ '</div>'
886
+ f"{''.join(sections_html)}{_render_bar_chart(understanding)}{stat_line}{pending_callout}"
887
+ )
888
+ else:
889
+ explanation_block = (
890
+ '<div class="callout"><span class="lbl-top">LLM required</span>'
891
+ '<p>Project rationale, technology choices, trade-offs, and pros and cons were not '
892
+ 'generated because no LLM API key was available.</p></div>'
893
+ )
894
+
895
+ return f"""<!doctype html>
896
+ <html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
897
+ <title>{_esc(title)}</title>
898
+ <style>{_CSS}</style>
899
+ </head><body><div class="page">
900
+ <header><p class="kicker">graphitect</p><h1>{_esc(title)}</h1>
901
+ <div class="report-actions"><button type="button" class="print-button" data-print-report>Print / save PDF</button>
902
+ <p class="print-hint">Uses your browser's Save as PDF option.</p></div></header>
903
+ <nav class="doc-nav">{nav_html}</nav>
904
+ <section class="output-section" id="interactive-diagram">
905
+ <div class="output-heading"><p class="kicker">Section 1</p><h2>Interactive system diagram</h2></div>
906
+ {diagram_block}{print_diagram_block}
907
+ </section>
908
+ <section class="output-section explanation-content" id="project-explanation">
909
+ <p class="kicker">Section 2</p><h2>Project explanation</h2>
910
+ {explanation_block}
911
+ </section>
912
+ <footer>Generated by graphitect - graph(ify) + (arch)itect.</footer>
913
+ </div>
914
+ <script>{_HIGHLIGHT_SCRIPT}</script>
915
+ </body></html>
916
+ """