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.
- graphify/__init__.py +30 -0
- graphify/__main__.py +757 -0
- graphify/_minhash.py +107 -0
- graphify/affected.py +318 -0
- graphify/always_on/agents-md.md +12 -0
- graphify/always_on/antigravity-rules.md +14 -0
- graphify/always_on/claude-md.md +9 -0
- graphify/always_on/gemini-md.md +9 -0
- graphify/always_on/kiro-steering.md +5 -0
- graphify/always_on/vscode-instructions.md +17 -0
- graphify/analyze.py +769 -0
- graphify/benchmark.py +152 -0
- graphify/build.py +2300 -0
- graphify/cache.py +1746 -0
- graphify/callflow_html.py +2051 -0
- graphify/cargo_introspect.py +109 -0
- graphify/cli.py +4745 -0
- graphify/cluster.py +409 -0
- graphify/command-kilo.md +15 -0
- graphify/cross_repo_calls.py +216 -0
- graphify/cross_repo_types.py +75 -0
- graphify/csharp_dispatch.py +154 -0
- graphify/dedup.py +1213 -0
- graphify/detect.py +2566 -0
- graphify/diagnostics.py +406 -0
- graphify/export.py +1349 -0
- graphify/exporters/__init__.py +1 -0
- graphify/exporters/base.py +14 -0
- graphify/exporters/graphdb.py +173 -0
- graphify/exporters/html.py +637 -0
- graphify/extract.py +7856 -0
- graphify/extractors/MIGRATION.md +107 -0
- graphify/extractors/__init__.py +66 -0
- graphify/extractors/apex.py +215 -0
- graphify/extractors/base.py +85 -0
- graphify/extractors/bash.py +579 -0
- graphify/extractors/blade.py +53 -0
- graphify/extractors/commonlisp.py +540 -0
- graphify/extractors/csharp.py +448 -0
- graphify/extractors/dart.py +564 -0
- graphify/extractors/dm.py +494 -0
- graphify/extractors/elixir.py +241 -0
- graphify/extractors/engine.py +6509 -0
- graphify/extractors/fortran.py +311 -0
- graphify/extractors/go.py +527 -0
- graphify/extractors/json_config.py +240 -0
- graphify/extractors/julia.py +289 -0
- graphify/extractors/markdown.py +408 -0
- graphify/extractors/models.py +131 -0
- graphify/extractors/objc.py +566 -0
- graphify/extractors/ocaml.py +289 -0
- graphify/extractors/pascal.py +688 -0
- graphify/extractors/pascal_forms.py +196 -0
- graphify/extractors/powershell.py +522 -0
- graphify/extractors/razor.py +192 -0
- graphify/extractors/resolution.py +3584 -0
- graphify/extractors/robot.py +296 -0
- graphify/extractors/rust.py +470 -0
- graphify/extractors/sln.py +92 -0
- graphify/extractors/sql.py +720 -0
- graphify/extractors/terraform.py +181 -0
- graphify/extractors/verilog.py +329 -0
- graphify/extractors/zig.py +181 -0
- graphify/file_slice.py +246 -0
- graphify/global_graph.py +194 -0
- graphify/google_workspace.py +237 -0
- graphify/hooks.py +933 -0
- graphify/ids.py +93 -0
- graphify/ingest.py +358 -0
- graphify/install.py +2366 -0
- graphify/llm.py +3544 -0
- graphify/manifest.py +4 -0
- graphify/manifest_ingest.py +311 -0
- graphify/mcp_ingest.py +386 -0
- graphify/multigraph_compat.py +212 -0
- graphify/pascal_resolution.py +129 -0
- graphify/paths.py +436 -0
- graphify/pg_introspect.py +165 -0
- graphify/prs.py +770 -0
- graphify/querylog.py +80 -0
- graphify/reflect.py +882 -0
- graphify/report.py +346 -0
- graphify/resolver_registry.py +85 -0
- graphify/ruby_resolution.py +242 -0
- graphify/scip_ingest.py +363 -0
- graphify/security.py +460 -0
- graphify/semantic_cleanup.py +336 -0
- graphify/serve.py +2608 -0
- graphify/skill-agents.md +710 -0
- graphify/skill-aider.md +1283 -0
- graphify/skill-amp.md +710 -0
- graphify/skill-claw.md +713 -0
- graphify/skill-codex.md +710 -0
- graphify/skill-copilot.md +713 -0
- graphify/skill-devin.md +1410 -0
- graphify/skill-droid.md +710 -0
- graphify/skill-kilo.md +722 -0
- graphify/skill-kiro.md +713 -0
- graphify/skill-opencode.md +705 -0
- graphify/skill-pi.md +713 -0
- graphify/skill-trae.md +711 -0
- graphify/skill-vscode.md +709 -0
- graphify/skill-windows.md +755 -0
- graphify/skill.md +713 -0
- graphify/skills/agents/references/add-watch.md +56 -0
- graphify/skills/agents/references/exports.md +87 -0
- graphify/skills/agents/references/extraction-spec.md +70 -0
- graphify/skills/agents/references/github-and-merge.md +46 -0
- graphify/skills/agents/references/hooks.md +33 -0
- graphify/skills/agents/references/query.md +311 -0
- graphify/skills/agents/references/transcribe.md +52 -0
- graphify/skills/agents/references/update.md +210 -0
- graphify/skills/amp/references/add-watch.md +56 -0
- graphify/skills/amp/references/exports.md +87 -0
- graphify/skills/amp/references/extraction-spec.md +70 -0
- graphify/skills/amp/references/github-and-merge.md +46 -0
- graphify/skills/amp/references/hooks.md +33 -0
- graphify/skills/amp/references/query.md +311 -0
- graphify/skills/amp/references/transcribe.md +52 -0
- graphify/skills/amp/references/update.md +210 -0
- graphify/skills/claude/references/add-watch.md +56 -0
- graphify/skills/claude/references/exports.md +87 -0
- graphify/skills/claude/references/extraction-spec.md +70 -0
- graphify/skills/claude/references/github-and-merge.md +46 -0
- graphify/skills/claude/references/hooks.md +33 -0
- graphify/skills/claude/references/query.md +311 -0
- graphify/skills/claude/references/transcribe.md +52 -0
- graphify/skills/claude/references/update.md +210 -0
- graphify/skills/claw/references/add-watch.md +56 -0
- graphify/skills/claw/references/exports.md +87 -0
- graphify/skills/claw/references/extraction-spec.md +31 -0
- graphify/skills/claw/references/github-and-merge.md +46 -0
- graphify/skills/claw/references/hooks.md +33 -0
- graphify/skills/claw/references/query.md +311 -0
- graphify/skills/claw/references/transcribe.md +52 -0
- graphify/skills/claw/references/update.md +210 -0
- graphify/skills/codex/references/add-watch.md +56 -0
- graphify/skills/codex/references/exports.md +87 -0
- graphify/skills/codex/references/extraction-spec.md +31 -0
- graphify/skills/codex/references/github-and-merge.md +46 -0
- graphify/skills/codex/references/hooks.md +33 -0
- graphify/skills/codex/references/query.md +311 -0
- graphify/skills/codex/references/transcribe.md +52 -0
- graphify/skills/codex/references/update.md +210 -0
- graphify/skills/copilot/references/add-watch.md +56 -0
- graphify/skills/copilot/references/exports.md +87 -0
- graphify/skills/copilot/references/extraction-spec.md +70 -0
- graphify/skills/copilot/references/github-and-merge.md +46 -0
- graphify/skills/copilot/references/hooks.md +33 -0
- graphify/skills/copilot/references/query.md +311 -0
- graphify/skills/copilot/references/transcribe.md +52 -0
- graphify/skills/copilot/references/update.md +210 -0
- graphify/skills/droid/references/add-watch.md +56 -0
- graphify/skills/droid/references/exports.md +87 -0
- graphify/skills/droid/references/extraction-spec.md +70 -0
- graphify/skills/droid/references/github-and-merge.md +46 -0
- graphify/skills/droid/references/hooks.md +33 -0
- graphify/skills/droid/references/query.md +311 -0
- graphify/skills/droid/references/transcribe.md +52 -0
- graphify/skills/droid/references/update.md +210 -0
- graphify/skills/kilo/references/add-watch.md +56 -0
- graphify/skills/kilo/references/exports.md +87 -0
- graphify/skills/kilo/references/extraction-spec.md +70 -0
- graphify/skills/kilo/references/github-and-merge.md +46 -0
- graphify/skills/kilo/references/hooks.md +33 -0
- graphify/skills/kilo/references/query.md +311 -0
- graphify/skills/kilo/references/transcribe.md +52 -0
- graphify/skills/kilo/references/update.md +210 -0
- graphify/skills/kiro/references/add-watch.md +56 -0
- graphify/skills/kiro/references/exports.md +87 -0
- graphify/skills/kiro/references/extraction-spec.md +31 -0
- graphify/skills/kiro/references/github-and-merge.md +46 -0
- graphify/skills/kiro/references/hooks.md +33 -0
- graphify/skills/kiro/references/query.md +311 -0
- graphify/skills/kiro/references/transcribe.md +52 -0
- graphify/skills/kiro/references/update.md +210 -0
- graphify/skills/opencode/references/add-watch.md +56 -0
- graphify/skills/opencode/references/exports.md +87 -0
- graphify/skills/opencode/references/extraction-spec.md +70 -0
- graphify/skills/opencode/references/github-and-merge.md +46 -0
- graphify/skills/opencode/references/hooks.md +33 -0
- graphify/skills/opencode/references/query.md +311 -0
- graphify/skills/opencode/references/transcribe.md +52 -0
- graphify/skills/opencode/references/update.md +210 -0
- graphify/skills/pi/references/add-watch.md +56 -0
- graphify/skills/pi/references/exports.md +87 -0
- graphify/skills/pi/references/extraction-spec.md +31 -0
- graphify/skills/pi/references/github-and-merge.md +46 -0
- graphify/skills/pi/references/hooks.md +33 -0
- graphify/skills/pi/references/query.md +311 -0
- graphify/skills/pi/references/transcribe.md +52 -0
- graphify/skills/pi/references/update.md +210 -0
- graphify/skills/trae/references/add-watch.md +56 -0
- graphify/skills/trae/references/exports.md +87 -0
- graphify/skills/trae/references/extraction-spec.md +70 -0
- graphify/skills/trae/references/github-and-merge.md +46 -0
- graphify/skills/trae/references/hooks.md +35 -0
- graphify/skills/trae/references/query.md +311 -0
- graphify/skills/trae/references/transcribe.md +52 -0
- graphify/skills/trae/references/update.md +210 -0
- graphify/skills/vscode/references/add-watch.md +56 -0
- graphify/skills/vscode/references/exports.md +87 -0
- graphify/skills/vscode/references/extraction-spec.md +70 -0
- graphify/skills/vscode/references/github-and-merge.md +46 -0
- graphify/skills/vscode/references/hooks.md +33 -0
- graphify/skills/vscode/references/query.md +311 -0
- graphify/skills/vscode/references/transcribe.md +52 -0
- graphify/skills/vscode/references/update.md +210 -0
- graphify/skills/windows/references/add-watch.md +56 -0
- graphify/skills/windows/references/exports.md +87 -0
- graphify/skills/windows/references/extraction-spec.md +70 -0
- graphify/skills/windows/references/github-and-merge.md +46 -0
- graphify/skills/windows/references/hooks.md +33 -0
- graphify/skills/windows/references/query.md +311 -0
- graphify/skills/windows/references/transcribe.md +52 -0
- graphify/skills/windows/references/update.md +210 -0
- graphify/symbol_resolution.py +556 -0
- graphify/transcribe.py +186 -0
- graphify/tree_html.py +603 -0
- graphify/validate.py +95 -0
- graphify/watch.py +2280 -0
- graphify/wiki.py +405 -0
- graphitect/__init__.py +28 -0
- graphitect/__main__.py +4 -0
- graphitect/_vendor/__init__.py +2 -0
- graphitect/_vendor/archify/LICENSE +22 -0
- graphitect/_vendor/archify/SKILL.md +137 -0
- graphitect/_vendor/archify/THIRD_PARTY_NOTICES.md +69 -0
- graphitect/_vendor/archify/assets/JetBrainsMono-OFL.txt +93 -0
- graphitect/_vendor/archify/assets/template.html +14935 -0
- graphitect/_vendor/archify/bin/archify.mjs +2091 -0
- graphitect/_vendor/archify/bin/open-artifact.mjs +86 -0
- graphitect/_vendor/archify/bin/preview.mjs +653 -0
- graphitect/_vendor/archify/bin/visual-check.mjs +829 -0
- graphitect/_vendor/archify/brand-marks/README.md +31 -0
- graphitect/_vendor/archify/brand-marks/catalog.json +131 -0
- graphitect/_vendor/archify/delta/architecture-delta.mjs +1221 -0
- graphitect/_vendor/archify/examples/agent-run.lifecycle.json +60 -0
- graphitect/_vendor/archify/examples/agent-tool-call.workflow.json +94 -0
- graphitect/_vendor/archify/examples/async-job-roundtrip.sequence.json +61 -0
- graphitect/_vendor/archify/examples/brand-aware-delivery.architecture.json +47 -0
- graphitect/_vendor/archify/examples/cache-miss-request.sequence.json +82 -0
- graphitect/_vendor/archify/examples/checkout-platform.base.architecture.json +31 -0
- graphitect/_vendor/archify/examples/checkout-platform.head.architecture.json +31 -0
- graphitect/_vendor/archify/examples/dataflow-product-analytics.html +15045 -0
- graphitect/_vendor/archify/examples/deployment-release.lifecycle.json +49 -0
- graphitect/_vendor/archify/examples/event-stream.dataflow.json +57 -0
- graphitect/_vendor/archify/examples/incident-response.workflow.json +64 -0
- graphitect/_vendor/archify/examples/lifecycle-agent-run.html +14980 -0
- graphitect/_vendor/archify/examples/product-analytics.dataflow.json +76 -0
- graphitect/_vendor/archify/examples/production-deployment.architecture.json +71 -0
- graphitect/_vendor/archify/examples/release-delivery.workflow.json +62 -0
- graphitect/_vendor/archify/examples/sequence-cache-miss-request.html +15060 -0
- graphitect/_vendor/archify/examples/web-app-rendered.html +15009 -0
- graphitect/_vendor/archify/examples/web-app.architecture.json +46 -0
- graphitect/_vendor/archify/examples/workflow-agent-tool-call-rendered.html +15051 -0
- graphitect/_vendor/archify/migrations/workflow-v2.mjs +279 -0
- graphitect/_vendor/archify/package-lock.json +149 -0
- graphitect/_vendor/archify/package.json +39 -0
- graphitect/_vendor/archify/recipes/scenarios.mjs +391 -0
- graphitect/_vendor/archify/references/authoring-contract.md +243 -0
- graphitect/_vendor/archify/references/brand-marks.md +65 -0
- graphitect/_vendor/archify/references/delivery-contract.md +120 -0
- graphitect/_vendor/archify/references/viewer-runtime.md +45 -0
- graphitect/_vendor/archify/renderers/architecture/grid.mjs +62 -0
- graphitect/_vendor/archify/renderers/architecture/render-architecture.mjs +1078 -0
- graphitect/_vendor/archify/renderers/dataflow/README.md +104 -0
- graphitect/_vendor/archify/renderers/dataflow/render-dataflow.mjs +483 -0
- graphitect/_vendor/archify/renderers/lifecycle/README.md +115 -0
- graphitect/_vendor/archify/renderers/lifecycle/render-lifecycle.mjs +561 -0
- graphitect/_vendor/archify/renderers/sequence/README.md +114 -0
- graphitect/_vendor/archify/renderers/sequence/render-sequence.mjs +464 -0
- graphitect/_vendor/archify/renderers/shared/brand-marks.mjs +563 -0
- graphitect/_vendor/archify/renderers/shared/cli.mjs +218 -0
- graphitect/_vendor/archify/renderers/shared/desktop-readability.mjs +26 -0
- graphitect/_vendor/archify/renderers/shared/diagnostics.mjs +127 -0
- graphitect/_vendor/archify/renderers/shared/engineering-profiles.mjs +157 -0
- graphitect/_vendor/archify/renderers/shared/generated-brand-marks.mjs +2003 -0
- graphitect/_vendor/archify/renderers/shared/generated-validators.mjs +13 -0
- graphitect/_vendor/archify/renderers/shared/geometry.mjs +1423 -0
- graphitect/_vendor/archify/renderers/shared/i18n.mjs +595 -0
- graphitect/_vendor/archify/renderers/shared/layout-report.mjs +40 -0
- graphitect/_vendor/archify/renderers/shared/legend.mjs +217 -0
- graphitect/_vendor/archify/renderers/shared/output-path.mjs +340 -0
- graphitect/_vendor/archify/renderers/shared/repository-evidence.mjs +238 -0
- graphitect/_vendor/archify/renderers/shared/repository-location.mjs +58 -0
- graphitect/_vendor/archify/renderers/shared/text-fit.mjs +49 -0
- graphitect/_vendor/archify/renderers/shared/utils.mjs +232 -0
- graphitect/_vendor/archify/renderers/shared/validator.mjs +86 -0
- graphitect/_vendor/archify/renderers/workflow/README.md +223 -0
- graphitect/_vendor/archify/renderers/workflow/render-workflow.mjs +35 -0
- graphitect/_vendor/archify/renderers/workflow/workflow-compiler.mjs +4400 -0
- graphitect/_vendor/archify/renderers/workflow/workflow-migration-geometry.mjs +144 -0
- graphitect/_vendor/archify/schemas/README.md +211 -0
- graphitect/_vendor/archify/schemas/architecture.schema.json +178 -0
- graphitect/_vendor/archify/schemas/common.schema.json +115 -0
- graphitect/_vendor/archify/schemas/dataflow.schema.json +243 -0
- graphitect/_vendor/archify/schemas/lifecycle.schema.json +266 -0
- graphitect/_vendor/archify/schemas/sequence.schema.json +223 -0
- graphitect/_vendor/archify/schemas/workflow.schema.json +428 -0
- graphitect/_vendor/archify/scripts/check-render-output.mjs +836 -0
- graphitect/_vendor/archify/scripts/check-update.mjs +1667 -0
- graphitect/_vendor/archify/scripts/generate-brand-marks.mjs +141 -0
- graphitect/_vendor/archify/scripts/generate-validators.mjs +66 -0
- graphitect/_vendor/archify/scripts/render-examples.mjs +26 -0
- graphitect/_vendor/archify/scripts/update-contract.mjs +182 -0
- graphitect/_vendor/archify/skill-release.json +10 -0
- graphitect/cli.py +981 -0
- graphitect/deliver/__init__.py +5 -0
- graphitect/deliver/archify_adapter.py +1877 -0
- graphitect/deliver/archify_ir.py +160 -0
- graphitect/deliver/archify_repair.py +135 -0
- graphitect/deliver/doc_compiler.py +916 -0
- graphitect/ground/__init__.py +5 -0
- graphitect/ground/describe_source.py +27 -0
- graphitect/ground/fullread_source.py +56 -0
- graphitect/ground/graphify_source.py +107 -0
- graphitect/models.py +118 -0
- graphitect/skill/SKILL.md +80 -0
- graphitect/skill/agents/openai.yaml +4 -0
- graphitect/synthesize/__init__.py +5 -0
- graphitect/synthesize/engine.py +281 -0
- graphitect/synthesize/llm_backend.py +331 -0
- graphitect/synthesize/questions.py +139 -0
- graphitect/synthesize/rubric.py +104 -0
- graphitect-0.2.0.dist-info/METADATA +284 -0
- graphitect-0.2.0.dist-info/RECORD +336 -0
- graphitect-0.2.0.dist-info/WHEEL +5 -0
- graphitect-0.2.0.dist-info/entry_points.txt +2 -0
- graphitect-0.2.0.dist-info/licenses/LICENSE +21 -0
- graphitect-0.2.0.dist-info/licenses/LICENSE-ARCHIFY-MIT +22 -0
- graphitect-0.2.0.dist-info/licenses/LICENSE-GRAPHIFY-APACHE-2.0 +202 -0
- graphitect-0.2.0.dist-info/licenses/LICENSE-GRAPHIFY-MIT +21 -0
- graphitect-0.2.0.dist-info/licenses/NOTICE-ARCHIFY-THIRD-PARTY.md +69 -0
- graphitect-0.2.0.dist-info/licenses/NOTICE-GRAPHIFY +8 -0
- 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 " · " + "; ".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 & 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
|
+
"""
|