forge-doctor-api 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 (268) hide show
  1. forge_doctor_api/__init__.py +69 -0
  2. forge_doctor_api/__main__.py +3 -0
  3. forge_doctor_api/_graphstudio.py +376 -0
  4. forge_doctor_api/_graphview.py +181 -0
  5. forge_doctor_api/analyzers/__init__.py +0 -0
  6. forge_doctor_api/analyzers/asyncapi/__init__.py +39 -0
  7. forge_doctor_api/analyzers/asyncapi/compat.py +274 -0
  8. forge_doctor_api/analyzers/asyncapi/graph.py +116 -0
  9. forge_doctor_api/analyzers/asyncapi/knowledge.py +60 -0
  10. forge_doctor_api/analyzers/asyncapi/model.py +143 -0
  11. forge_doctor_api/analyzers/asyncapi/parser.py +512 -0
  12. forge_doctor_api/analyzers/cache/__init__.py +17 -0
  13. forge_doctor_api/analyzers/cache/graph.py +256 -0
  14. forge_doctor_api/analyzers/cache/model.py +66 -0
  15. forge_doctor_api/analyzers/cache/scan.py +192 -0
  16. forge_doctor_api/analyzers/clients/__init__.py +19 -0
  17. forge_doctor_api/analyzers/clients/graph.py +57 -0
  18. forge_doctor_api/analyzers/clients/graphql_doc.py +105 -0
  19. forge_doctor_api/analyzers/clients/java.py +139 -0
  20. forge_doctor_api/analyzers/clients/javascript.py +214 -0
  21. forge_doctor_api/analyzers/clients/model.py +83 -0
  22. forge_doctor_api/analyzers/clients/python.py +337 -0
  23. forge_doctor_api/analyzers/clients/scan.py +72 -0
  24. forge_doctor_api/analyzers/gateway/__init__.py +25 -0
  25. forge_doctor_api/analyzers/gateway/model.py +128 -0
  26. forge_doctor_api/analyzers/gateway/parser.py +572 -0
  27. forge_doctor_api/analyzers/graphql/__init__.py +49 -0
  28. forge_doctor_api/analyzers/graphql/client.py +164 -0
  29. forge_doctor_api/analyzers/graphql/compat.py +228 -0
  30. forge_doctor_api/analyzers/graphql/graph.py +105 -0
  31. forge_doctor_api/analyzers/graphql/model.py +180 -0
  32. forge_doctor_api/analyzers/graphql/parser.py +423 -0
  33. forge_doctor_api/analyzers/graphql/shape.py +149 -0
  34. forge_doctor_api/analyzers/grpc/__init__.py +51 -0
  35. forge_doctor_api/analyzers/grpc/compat.py +288 -0
  36. forge_doctor_api/analyzers/grpc/graph.py +84 -0
  37. forge_doctor_api/analyzers/grpc/model.py +150 -0
  38. forge_doctor_api/analyzers/grpc/parser.py +406 -0
  39. forge_doctor_api/analyzers/grpc/reliability.py +130 -0
  40. forge_doctor_api/analyzers/iac/__init__.py +21 -0
  41. forge_doctor_api/analyzers/iac/cloudformation.py +152 -0
  42. forge_doctor_api/analyzers/iac/graph.py +65 -0
  43. forge_doctor_api/analyzers/iac/hcl.py +184 -0
  44. forge_doctor_api/analyzers/iac/model.py +81 -0
  45. forge_doctor_api/analyzers/iac/parser.py +216 -0
  46. forge_doctor_api/analyzers/openapi/__init__.py +76 -0
  47. forge_doctor_api/analyzers/openapi/detection.py +63 -0
  48. forge_doctor_api/analyzers/openapi/graph.py +171 -0
  49. forge_doctor_api/analyzers/openapi/identity.py +44 -0
  50. forge_doctor_api/analyzers/openapi/knowledge.py +59 -0
  51. forge_doctor_api/analyzers/openapi/loader.py +290 -0
  52. forge_doctor_api/analyzers/openapi/model.py +355 -0
  53. forge_doctor_api/analyzers/openapi/parser.py +952 -0
  54. forge_doctor_api/analyzers/openapi/refs.py +151 -0
  55. forge_doctor_api/analyzers/openapi/shape.py +81 -0
  56. forge_doctor_api/analyzers/routes/__init__.py +33 -0
  57. forge_doctor_api/analyzers/routes/adapter.py +291 -0
  58. forge_doctor_api/analyzers/routes/fastapi.py +971 -0
  59. forge_doctor_api/analyzers/routes/graph.py +101 -0
  60. forge_doctor_api/analyzers/routes/javascript.py +977 -0
  61. forge_doctor_api/analyzers/routes/model.py +122 -0
  62. forge_doctor_api/analyzers/routes/spring.py +541 -0
  63. forge_doctor_api/analyzers/routes/textscan.py +102 -0
  64. forge_doctor_api/analyzers/runtime/__init__.py +53 -0
  65. forge_doctor_api/analyzers/runtime/accesslog.py +68 -0
  66. forge_doctor_api/analyzers/runtime/adapter.py +32 -0
  67. forge_doctor_api/analyzers/runtime/aggregate.py +224 -0
  68. forge_doctor_api/analyzers/runtime/execution.py +408 -0
  69. forge_doctor_api/analyzers/runtime/graph.py +93 -0
  70. forge_doctor_api/analyzers/runtime/history.py +103 -0
  71. forge_doctor_api/analyzers/runtime/loader.py +283 -0
  72. forge_doctor_api/analyzers/runtime/model.py +100 -0
  73. forge_doctor_api/analyzers/runtime/otlp.py +211 -0
  74. forge_doctor_api/analyzers/runtime/stream.py +301 -0
  75. forge_doctor_api/analyzers/version/__init__.py +19 -0
  76. forge_doctor_api/analyzers/version/detect.py +242 -0
  77. forge_doctor_api/analyzers/version/model.py +71 -0
  78. forge_doctor_api/change/__init__.py +41 -0
  79. forge_doctor_api/change/baseline.py +51 -0
  80. forge_doctor_api/change/blast.py +178 -0
  81. forge_doctor_api/change/events.py +372 -0
  82. forge_doctor_api/change/model.py +125 -0
  83. forge_doctor_api/change/pr.py +56 -0
  84. forge_doctor_api/checks/__init__.py +1 -0
  85. forge_doctor_api/checks/apisec/__init__.py +5 -0
  86. forge_doctor_api/checks/apisec/catalog.py +172 -0
  87. forge_doctor_api/checks/apisec/engine.py +549 -0
  88. forge_doctor_api/checks/asyncapi/__init__.py +14 -0
  89. forge_doctor_api/checks/asyncapi/catalog.py +87 -0
  90. forge_doctor_api/checks/asyncapi/engine.py +374 -0
  91. forge_doctor_api/checks/client/__init__.py +16 -0
  92. forge_doctor_api/checks/client/catalog.py +41 -0
  93. forge_doctor_api/checks/client/engine.py +215 -0
  94. forge_doctor_api/checks/compat/__init__.py +27 -0
  95. forge_doctor_api/checks/compat/catalog.py +125 -0
  96. forge_doctor_api/checks/compat/engine.py +982 -0
  97. forge_doctor_api/checks/compat/fingerprint.py +116 -0
  98. forge_doctor_api/checks/drift/__init__.py +38 -0
  99. forge_doctor_api/checks/drift/catalog.py +150 -0
  100. forge_doctor_api/checks/drift/engine.py +608 -0
  101. forge_doctor_api/checks/drift/error_contract.py +127 -0
  102. forge_doctor_api/checks/graphql/__init__.py +15 -0
  103. forge_doctor_api/checks/graphql/catalog.py +150 -0
  104. forge_doctor_api/checks/graphql/engine.py +300 -0
  105. forge_doctor_api/checks/grpc/__init__.py +15 -0
  106. forge_doctor_api/checks/grpc/catalog.py +115 -0
  107. forge_doctor_api/checks/grpc/engine.py +217 -0
  108. forge_doctor_api/checks/oas/__init__.py +6 -0
  109. forge_doctor_api/checks/oas/catalog.py +271 -0
  110. forge_doctor_api/checks/oas/engine.py +766 -0
  111. forge_doctor_api/checks/observability/__init__.py +6 -0
  112. forge_doctor_api/checks/observability/catalog.py +75 -0
  113. forge_doctor_api/checks/observability/engine.py +159 -0
  114. forge_doctor_api/checks/perf/__init__.py +5 -0
  115. forge_doctor_api/checks/perf/catalog.py +104 -0
  116. forge_doctor_api/checks/perf/engine.py +299 -0
  117. forge_doctor_api/checks/relapi/__init__.py +5 -0
  118. forge_doctor_api/checks/relapi/catalog.py +124 -0
  119. forge_doctor_api/checks/relapi/engine.py +418 -0
  120. forge_doctor_api/cli/__init__.py +1596 -0
  121. forge_doctor_api/cli/catalog.py +131 -0
  122. forge_doctor_api/contracts/__init__.py +90 -0
  123. forge_doctor_api/contracts/adapters.py +336 -0
  124. forge_doctor_api/contracts/models.py +658 -0
  125. forge_doctor_api/contracts/schemas.py +244 -0
  126. forge_doctor_api/contracts/validate.py +114 -0
  127. forge_doctor_api/contracts/version.py +73 -0
  128. forge_doctor_api/core/__init__.py +1 -0
  129. forge_doctor_api/core/cache.py +115 -0
  130. forge_doctor_api/core/context.py +105 -0
  131. forge_doctor_api/core/discovery.py +317 -0
  132. forge_doctor_api/core/evidence_store.py +138 -0
  133. forge_doctor_api/core/export.py +53 -0
  134. forge_doctor_api/core/gate.py +101 -0
  135. forge_doctor_api/core/graph.py +387 -0
  136. forge_doctor_api/core/models.py +682 -0
  137. forge_doctor_api/core/plan.py +186 -0
  138. forge_doctor_api/core/redaction.py +106 -0
  139. forge_doctor_api/core/report.py +26 -0
  140. forge_doctor_api/core/stats.py +94 -0
  141. forge_doctor_api/diagnose/__init__.py +23 -0
  142. forge_doctor_api/diagnose/engine.py +328 -0
  143. forge_doctor_api/diagnose/model.py +87 -0
  144. forge_doctor_api/fleet/__init__.py +32 -0
  145. forge_doctor_api/fleet/collect.py +156 -0
  146. forge_doctor_api/fleet/model.py +130 -0
  147. forge_doctor_api/fleet/report.py +503 -0
  148. forge_doctor_api/graphview.py +84 -0
  149. forge_doctor_api/handoff/__init__.py +31 -0
  150. forge_doctor_api/handoff/boundary.py +215 -0
  151. forge_doctor_api/handoff/bundle.py +202 -0
  152. forge_doctor_api/handoff/context.py +281 -0
  153. forge_doctor_api/handoff/decision.py +72 -0
  154. forge_doctor_api/handoff/delta.py +197 -0
  155. forge_doctor_api/handoff/mcp.py +259 -0
  156. forge_doctor_api/handoff/mcp_server.py +274 -0
  157. forge_doctor_api/handoff/model.py +217 -0
  158. forge_doctor_api/handoff/protocol.py +336 -0
  159. forge_doctor_api/handoff/routing.py +84 -0
  160. forge_doctor_api/integrations/__init__.py +0 -0
  161. forge_doctor_api/knowledge/__init__.py +77 -0
  162. forge_doctor_api/knowledge/asyncapi/__init__.py +1 -0
  163. forge_doctor_api/knowledge/asyncapi/versions.yaml +38 -0
  164. forge_doctor_api/knowledge/capability.py +314 -0
  165. forge_doctor_api/knowledge/frameworks/__init__.py +1 -0
  166. forge_doctor_api/knowledge/frameworks/express.yaml +17 -0
  167. forge_doctor_api/knowledge/frameworks/fastapi.yaml +17 -0
  168. forge_doctor_api/knowledge/frameworks/spring.yaml +17 -0
  169. forge_doctor_api/knowledge/gateways/__init__.py +1 -0
  170. forge_doctor_api/knowledge/gateways/aws-api-gateway.yaml +16 -0
  171. forge_doctor_api/knowledge/gateways/capability-map.yaml +41 -0
  172. forge_doctor_api/knowledge/gateways/envoy.yaml +17 -0
  173. forge_doctor_api/knowledge/gateways/kong.yaml +15 -0
  174. forge_doctor_api/knowledge/gateways/nginx.yaml +15 -0
  175. forge_doctor_api/knowledge/graphql/__init__.py +1 -0
  176. forge_doctor_api/knowledge/graphql/spec.yaml +30 -0
  177. forge_doctor_api/knowledge/grpc/__init__.py +1 -0
  178. forge_doctor_api/knowledge/grpc/behaviors.yaml +69 -0
  179. forge_doctor_api/knowledge/http/__init__.py +1 -0
  180. forge_doctor_api/knowledge/http/semantics.yaml +27 -0
  181. forge_doctor_api/knowledge/jsonschema/__init__.py +1 -0
  182. forge_doctor_api/knowledge/jsonschema/dialects.yaml +30 -0
  183. forge_doctor_api/knowledge/loader.py +131 -0
  184. forge_doctor_api/knowledge/manifest.py +229 -0
  185. forge_doctor_api/knowledge/model.py +122 -0
  186. forge_doctor_api/knowledge/openapi/__init__.py +1 -0
  187. forge_doctor_api/knowledge/openapi/versions.yaml +54 -0
  188. forge_doctor_api/knowledge/security/__init__.py +0 -0
  189. forge_doctor_api/knowledge/security/owasp_api_top10.yaml +98 -0
  190. forge_doctor_api/lab/__init__.py +42 -0
  191. forge_doctor_api/lab/capabilities.py +81 -0
  192. forge_doctor_api/lab/loader.py +114 -0
  193. forge_doctor_api/lab/model.py +246 -0
  194. forge_doctor_api/lab/report.py +283 -0
  195. forge_doctor_api/lab/runner.py +299 -0
  196. forge_doctor_api/migrate/__init__.py +41 -0
  197. forge_doctor_api/migrate/engine.py +885 -0
  198. forge_doctor_api/migrate/graph.py +263 -0
  199. forge_doctor_api/migrate/model.py +124 -0
  200. forge_doctor_api/output/__init__.py +17 -0
  201. forge_doctor_api/output/writers.py +140 -0
  202. forge_doctor_api/perf/__init__.py +100 -0
  203. forge_doctor_api/perf/baseline.py +186 -0
  204. forge_doctor_api/perf/budget.py +100 -0
  205. forge_doctor_api/perf/capacity.py +244 -0
  206. forge_doctor_api/perf/correlation.py +91 -0
  207. forge_doctor_api/perf/criticalpath.py +140 -0
  208. forge_doctor_api/perf/experiments.py +215 -0
  209. forge_doctor_api/perf/fanout.py +144 -0
  210. forge_doctor_api/perf/payload.py +72 -0
  211. forge_doctor_api/perf/signals.py +152 -0
  212. forge_doctor_api/perf/stats.py +70 -0
  213. forge_doctor_api/plugins/__init__.py +51 -0
  214. forge_doctor_api/plugins/conformance.py +221 -0
  215. forge_doctor_api/plugins/manifest.py +181 -0
  216. forge_doctor_api/plugins/registry.py +129 -0
  217. forge_doctor_api/plugins/sdk.py +96 -0
  218. forge_doctor_api/plugins/trust.py +92 -0
  219. forge_doctor_api/plugins/validation.py +197 -0
  220. forge_doctor_api/policy/__init__.py +37 -0
  221. forge_doctor_api/policy/engine.py +112 -0
  222. forge_doctor_api/policy/loader.py +208 -0
  223. forge_doctor_api/policy/model.py +116 -0
  224. forge_doctor_api/policy/ownership.py +140 -0
  225. forge_doctor_api/policy/rules.py +255 -0
  226. forge_doctor_api/py.typed +1 -0
  227. forge_doctor_api/reliability/__init__.py +50 -0
  228. forge_doctor_api/reliability/config.py +661 -0
  229. forge_doctor_api/reliability/idempotency.py +46 -0
  230. forge_doctor_api/reliability/model.py +180 -0
  231. forge_doctor_api/reliability/retry.py +93 -0
  232. forge_doctor_api/reliability/slo.py +94 -0
  233. forge_doctor_api/reliability/timeout.py +84 -0
  234. forge_doctor_api/report.py +83 -0
  235. forge_doctor_api/safefix/__init__.py +27 -0
  236. forge_doctor_api/safefix/classify.py +52 -0
  237. forge_doctor_api/safefix/model.py +74 -0
  238. forge_doctor_api/safefix/rules.py +123 -0
  239. forge_doctor_api/scan.py +854 -0
  240. forge_doctor_api/sdk.py +205 -0
  241. forge_doctor_api/security/__init__.py +43 -0
  242. forge_doctor_api/security/knowledge.py +29 -0
  243. forge_doctor_api/security/model.py +257 -0
  244. forge_doctor_api/security/scan.py +1012 -0
  245. forge_doctor_api/service_graph.py +48 -0
  246. forge_doctor_api/temporal.py +247 -0
  247. forge_doctor_api/twin/__init__.py +25 -0
  248. forge_doctor_api/twin/assemble.py +266 -0
  249. forge_doctor_api/twin/drift.py +364 -0
  250. forge_doctor_api/twin/model.py +117 -0
  251. forge_doctor_api/ui/__init__.py +28 -0
  252. forge_doctor_api/ui/app.py +529 -0
  253. forge_doctor_api/ui/home.py +175 -0
  254. forge_doctor_api/ui/i18n.py +143 -0
  255. forge_doctor_api/ui/kit.py +689 -0
  256. forge_doctor_api/ui/screen.py +648 -0
  257. forge_doctor_api/ui/tui.py +124 -0
  258. forge_doctor_api/ui/wizard.py +309 -0
  259. forge_doctor_api/workspace/__init__.py +36 -0
  260. forge_doctor_api/workspace/impact.py +134 -0
  261. forge_doctor_api/workspace/links.py +319 -0
  262. forge_doctor_api/workspace/loader.py +123 -0
  263. forge_doctor_api/workspace/model.py +105 -0
  264. forge_doctor_api-0.2.0.dist-info/METADATA +209 -0
  265. forge_doctor_api-0.2.0.dist-info/RECORD +268 -0
  266. forge_doctor_api-0.2.0.dist-info/WHEEL +4 -0
  267. forge_doctor_api-0.2.0.dist-info/entry_points.txt +3 -0
  268. forge_doctor_api-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,69 @@
1
+ """Forge Doctor API: deterministic API architecture intelligence.
2
+
3
+ Public surface (stable): `Doctor` facade, `DoctorReport`, core models
4
+ and the handoff bundle. Everything else is internal and may move —
5
+ import from here, not from deep module paths.
6
+ """
7
+
8
+ from forge_doctor_api.core.context import ProjectContext
9
+ from forge_doctor_api.core.discovery import ArtifactInventory
10
+ from forge_doctor_api.core.models import (
11
+ Confidence,
12
+ Evidence,
13
+ EvidenceKind,
14
+ Finding,
15
+ Model,
16
+ Severity,
17
+ SourceLocation,
18
+ UnknownFact,
19
+ )
20
+ from forge_doctor_api.core.plan import AnalysisPlan
21
+ from forge_doctor_api.core.report import DomainSummary
22
+ from forge_doctor_api.handoff.bundle import assemble_bundle
23
+ from forge_doctor_api.handoff.model import ApiHandoffBundle
24
+ from forge_doctor_api.handoff.protocol import (
25
+ ForgeHandoff,
26
+ ForgeReceipt,
27
+ ForgeRef,
28
+ ForgeRequest,
29
+ ForgeResult,
30
+ )
31
+ from forge_doctor_api.report import DoctorReport
32
+ from forge_doctor_api.sdk import (
33
+ SDK_VERSION,
34
+ Doctor,
35
+ DoctorError,
36
+ FindingNotFoundError,
37
+ ProjectUnreadableError,
38
+ )
39
+
40
+ __version__ = "0.2.0"
41
+
42
+ __all__ = [
43
+ "SDK_VERSION",
44
+ "AnalysisPlan",
45
+ "ApiHandoffBundle",
46
+ "ArtifactInventory",
47
+ "Confidence",
48
+ "Doctor",
49
+ "DoctorError",
50
+ "DoctorReport",
51
+ "DomainSummary",
52
+ "Evidence",
53
+ "EvidenceKind",
54
+ "Finding",
55
+ "FindingNotFoundError",
56
+ "ForgeHandoff",
57
+ "ForgeReceipt",
58
+ "ForgeRef",
59
+ "ForgeRequest",
60
+ "ForgeResult",
61
+ "Model",
62
+ "ProjectContext",
63
+ "ProjectUnreadableError",
64
+ "Severity",
65
+ "SourceLocation",
66
+ "UnknownFact",
67
+ "__version__",
68
+ "assemble_bundle",
69
+ ]
@@ -0,0 +1,3 @@
1
+ from forge_doctor_api.cli import main
2
+
3
+ main()
@@ -0,0 +1,376 @@
1
+ """Forge Graph Studio — local-first, read-only graph explorer.
2
+
3
+ A zero-dependency stdlib server (``http.server`` on 127.0.0.1) plus a
4
+ single-file HTML/canvas frontend embedded below. No Node, no CDN, no
5
+ build step — the whole app ships as Python package source.
6
+
7
+ GET-only by design: exploring a graph can never modify the project
8
+ (§4.4). ``serve()`` never opens a browser unless asked; ``--no-browser``
9
+ prints the URL for SSH port-forwarding.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import threading
16
+ import webbrowser
17
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
18
+ from pathlib import Path
19
+ from typing import Any
20
+
21
+ STUDIO_HTML = r"""<!doctype html>
22
+ <html lang="en">
23
+ <head>
24
+ <meta charset="utf-8">
25
+ <title>Forge Graph Studio</title>
26
+ <meta name="viewport" content="width=device-width,initial-scale=1">
27
+ <style>
28
+ :root{--bg:#0d1117;--panel:#161b22;--fg:#e6edf3;--dim:#8b949e;--line:#30363d;--acc:#58a6ff}
29
+ *{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--fg);
30
+ font:13px/1.4 ui-monospace,Consolas,monospace;display:grid;
31
+ grid-template-columns:220px 1fr 280px;grid-template-rows:44px 1fr 26px;height:100vh}
32
+ header{grid-column:1/4;display:flex;gap:12px;align-items:center;padding:0 12px;
33
+ background:var(--panel);border-bottom:1px solid var(--line)}
34
+ header b{color:var(--acc)}select,input{background:var(--bg);color:var(--fg);
35
+ border:1px solid var(--line);border-radius:4px;padding:4px 6px}
36
+ #side,#insp{background:var(--panel);padding:10px;overflow:auto}
37
+ #side{border-right:1px solid var(--line)}#insp{border-left:1px solid var(--line)}
38
+ #wrap{position:relative;overflow:hidden}canvas{display:block;width:100%;height:100%;cursor:grab}
39
+ footer{grid-column:1/4;background:var(--panel);border-top:1px solid var(--line);
40
+ display:flex;gap:16px;padding:4px 12px;color:var(--dim)}
41
+ .k{word-break:break-all}.dim{color:var(--dim)}h4{margin:10px 0 4px;color:var(--dim);
42
+ text-transform:uppercase;font-size:11px;letter-spacing:.05em}
43
+ label.f{display:block;margin:2px 0;cursor:pointer}
44
+ #insp .pill{display:inline-block;border:1px solid var(--line);border-radius:10px;
45
+ padding:0 8px;margin:1px 2px;font-size:11px}
46
+ .ev{color:var(--acc);font-size:11px;word-break:break-all}
47
+ </style></head>
48
+ <body>
49
+ <header><b>FORGE GRAPH STUDIO</b>
50
+ <select id="gsel"></select>
51
+ <input id="q" placeholder="search…" size="20">
52
+ <span id="meta" class="dim"></span></header>
53
+ <div id="side"><h4>Layers</h4><div id="layers"></div>
54
+ <h4>Edge kinds</h4><div id="ekinds"></div>
55
+ <h4>Legend</h4><div id="legend" class="dim"></div></div>
56
+ <div id="wrap"><canvas id="cv"></canvas></div>
57
+ <div id="insp"><h4>Inspector</h4><div id="ibody" class="dim">select a node or edge</div></div>
58
+ <footer><span id="stats"></span><span id="prov"></span>
59
+ <span class="dim">scroll=zoom · drag=pan · click=inspect · n=neighbors ·
60
+ e=export · d=diff · s=snapshot · esc=reset</span></footer>
61
+ <script>
62
+ let G={nodes:[],edges:[]},L={},view={x:0,y:0,s:1},sel=null,hover=null,
63
+ kindsOn={},ekindsOn={},PAL={},running=true;
64
+ const cv=document.getElementById('cv'),cx=cv.getContext('2d');
65
+ const KCOLORS=['#58a6ff','#f78166','#7ee787','#ffa657','#d2a8ff','#76e3ea','#ff9bce','#e3b341'];
66
+ const ESTYLE={observed:[],declared:[6,3],inferred:[3,4],planned:[2,4],desired:[8,3],unknown:[1,5]};
67
+ function pal(k){if(!(k in PAL))PAL[k]=KCOLORS[Object.keys(PAL).length%KCOLORS.length];return PAL[k]}
68
+ async function loadGraphs(){const r=await fetch('/api/graphs');const d=await r.json();
69
+ const s=document.getElementById('gsel');s.innerHTML='';
70
+ d.graphs.forEach(g=>{const o=document.createElement('option');o.value=g.graph_id;
71
+ o.textContent=`${g.provider_id} · ${g.graph_id}`+
72
+ ` (${g.node_count}/${g.edge_count})`;s.appendChild(o)});
73
+ if(d.graphs.length)loadGraph(d.graphs[0].graph_id);
74
+ s.onchange=e=>loadGraph(e.target.value)}
75
+ async function loadGraph(id){const r=await fetch('/api/graph/'+encodeURIComponent(id));
76
+ const d=await r.json();sel=null;L={};
77
+ G.nodes=(d.nodes||[]).map(n=>({...n,x:Math.random()*600-300,y:Math.random()*600-300,vx:0,vy:0,deg:0}));
78
+ G.edges=(d.edges||[]);
79
+ const byId={};G.nodes.forEach(n=>{byId[n.id]=n});
80
+ G.edges.forEach(e=>{if(byId[e.source])byId[e.source].deg++;if(byId[e.target])byId[e.target].deg++});
81
+ G.edges=G.edges.filter(e=>byId[e.source]&&byId[e.target]);
82
+ kindsOn={};G.nodes.forEach(n=>kindsOn[n.kind]=true);
83
+ ekindsOn={};G.edges.forEach(e=>ekindsOn[e.kind]=true);
84
+ sidePanel();document.getElementById('meta').textContent=
85
+ `${d.descriptor.provider_id} · ${d.descriptor.domain} · ${d.descriptor.generated_at||''}`;
86
+ document.getElementById('prov').textContent=
87
+ (d.descriptor.limitations||[]).join(' | ').slice(0,120);
88
+ running=true;tick()}
89
+ function sidePanel(){const L=document.getElementById('layers'),E=document.getElementById('ekinds');
90
+ L.innerHTML='';E.innerHTML='';
91
+ Object.keys(kindsOn).sort().forEach(k=>{L.innerHTML+=
92
+ `<label class="f"><input type="checkbox" checked data-k="${k}">`+
93
+ ` <span style="color:${pal(k)}">●</span> ${k}</label>`});
94
+ Object.keys(ekindsOn).sort().forEach(k=>{E.innerHTML+=
95
+ `<label class="f"><input type="checkbox" checked data-ek="${k}"> ${k}</label>`});
96
+ L.querySelectorAll('input').forEach(i=>i.onchange=e=>{kindsOn[e.target.dataset.k]=e.target.checked});
97
+ E.querySelectorAll('input').forEach(i=>i.onchange=e=>{ekindsOn[e.target.dataset.ek]=e.target.checked});
98
+ document.getElementById('legend').innerHTML=
99
+ Object.keys(ESTYLE).map(s=>`<div>─ ${s}</div>`).join('')}
100
+ function tick(){let n=0;const byId={};G.nodes.forEach(x=>byId[x.id]=x);
101
+ for(let i=0;i<G.nodes.length;i++)for(let j=i+1;j<G.nodes.length;j++){
102
+ const a=G.nodes[i],b=G.nodes[j];let dx=a.x-b.x,dy=a.y-b.y,d=dx*dx+dy*dy+40;
103
+ const f=Math.min(2000/d,40);a.vx+=dx/d*f;a.vy+=dy/d*f;b.vx-=dx/d*f;b.vy-=dy/d*f}
104
+ G.edges.forEach(e=>{const a=byId[e.source],b=byId[e.target];
105
+ const dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1,f=(d-120)*0.02;
106
+ a.vx+=dx/d*f*d/120;a.vy+=dy/d*f*d/120;b.vx-=dx/d*f*d/120;b.vy-=dy/d*f*d/120});
107
+ G.nodes.forEach(a=>{a.vx-=a.x*0.001;a.vy-=a.y*0.001;a.vx*=0.85;a.vy*=0.85;
108
+ a.x+=a.vx;a.y+=a.vy;if(Math.abs(a.vx)+Math.abs(a.vy)>0.1)n++});
109
+ draw();if(n>0&&running)requestAnimationFrame(tick)}
110
+ function toS(x,y){const r=cv.getBoundingClientRect();
111
+ return[(x*view.s)+r.width/2+view.x,(y*view.s)+r.height/2+view.y]}
112
+ function draw(){const r=cv.getBoundingClientRect();
113
+ if(cv.width!==r.width*devicePixelRatio){cv.width=r.width*devicePixelRatio;
114
+ cv.height=r.height*devicePixelRatio}
115
+ cx.setTransform(devicePixelRatio,0,0,devicePixelRatio,0,0);
116
+ cx.clearRect(0,0,r.width,r.height);
117
+ const q=(document.getElementById('q').value||'').toLowerCase();
118
+ const vis={};let vc=0,ec=0;
119
+ G.nodes.forEach(n=>{const s=(n.label||'')+' '+n.id;
120
+ vis[n.id]=kindsOn[n.kind]&&(!q||s.toLowerCase().includes(q))&&
121
+ (!focusSet||focusSet.has(n.id));
122
+ if(vis[n.id])vc++});
123
+ G.edges.forEach(e=>{if(!vis[e.source]||!vis[e.target]||!ekindsOn[e.kind])return;ec++;
124
+ const a=G.nodes.find(n=>n.id===e.source),b=G.nodes.find(n=>n.id===e.target);
125
+ const[x1,y1]=toS(a.x,a.y),[x2,y2]=toS(b.x,b.y);
126
+ cx.strokeStyle=e===sel?'#f0f6fc':'#444c56';cx.lineWidth=e===sel?2:1;
127
+ cx.setLineDash(ESTYLE[e.epistemic_state]||ESTYLE.unknown);
128
+ cx.beginPath();cx.moveTo(x1,y1);cx.lineTo(x2,y2);cx.stroke();
129
+ // arrowhead
130
+ const an=Math.atan2(y2-y1,x2-x1);cx.setLineDash([]);cx.beginPath();
131
+ cx.moveTo(x2,y2);cx.lineTo(x2-8*Math.cos(an-0.5),y2-8*Math.sin(an-0.5));
132
+ cx.lineTo(x2-8*Math.cos(an+0.5),y2-8*Math.sin(an+0.5));cx.fillStyle=cx.strokeStyle;cx.fill()});
133
+ cx.setLineDash([]);
134
+ G.nodes.forEach(n=>{if(!vis[n.id])return;const[x,y]=toS(n.x,n.y);
135
+ const rad=4+Math.min(n.deg,12);
136
+ cx.beginPath();cx.arc(x,y,rad,0,7);
137
+ const dcol=drawDiffColors&&DC[drawDiffColors[n.id]];
138
+ cx.fillStyle=n===sel?'#f0f6fc':(n===hover?'#fff':(dcol||pal(n.kind)));cx.fill();
139
+ if(n===sel){cx.strokeStyle='#58a6ff';cx.lineWidth=2;cx.stroke()}
140
+ if(view.s>0.5||n.deg>4){cx.fillStyle='#8b949e';cx.font='10px monospace';
141
+ cx.fillText((n.label||n.id).slice(0,24),x+rad+2,y+3)}});
142
+ document.getElementById('stats').textContent=`${vc} nodes · ${ec} edges visible`}
143
+ cv.onwheel=e=>{e.preventDefault();const r=cv.getBoundingClientRect();
144
+ const mx=e.clientX-r.left-r.width/2,my=e.clientY-r.top-r.height/2;
145
+ const f=e.deltaY<0?1.15:0.87;view.x=(view.x-mx)*f+mx;view.y=(view.y-my)*f+my;
146
+ view.s*=f;draw()};
147
+ let drag=null;cv.onmousedown=e=>{const p=pick(e);drag={e,node:p.node,mx:view.x,my:view.y}};
148
+ cv.onmousemove=e=>{if(drag){const r=cv.getBoundingClientRect();
149
+ if(drag.node){drag.node.x=(e.clientX-r.left-r.width/2-view.x)/view.s;
150
+ drag.node.y=(e.clientY-r.top-r.height/2-view.y)/view.s}else{
151
+ view.x=drag.mx+e.clientX-drag.e.clientX;view.y=drag.my+e.clientY-drag.e.clientY}
152
+ draw()}else{const p=pick(e);hover=p.node;draw()}};
153
+ cv.onmouseup=e=>{const p=pick(e);
154
+ if(!drag||(Math.abs(e.clientX-drag.e.clientX)<4&&Math.abs(e.clientY-drag.e.clientY)<4)){
155
+ sel=p.node||p.edge||null;inspect()}
156
+ drag=null;draw()};
157
+ function pick(e){const r=cv.getBoundingClientRect();
158
+ const wx=(e.clientX-r.left-r.width/2-view.x)/view.s,
159
+ wy=(e.clientY-r.top-r.height/2-view.y)/view.s;
160
+ for(const n of G.nodes)if(Math.hypot(n.x-wx,n.y-wy)<12/view.s+4)return{node:n};
161
+ for(const ed of G.edges){const a=G.nodes.find(n=>n.id===ed.source),
162
+ b=G.nodes.find(n=>n.id===ed.target);if(!a||!b)continue;
163
+ const L2=(b.x-a.x)**2+(b.y-a.y)**2||1;
164
+ const t=Math.max(0,Math.min(1,((wx-a.x)*(b.x-a.x)+(wy-a.y)*(b.y-a.y))/L2));
165
+ if(Math.hypot(wx-(a.x+t*(b.x-a.x)),wy-(a.y+t*(b.y-a.y)))<6/view.s)return{edge:ed}}
166
+ return{}}
167
+ function inspect(){const b=document.getElementById('ibody');if(!sel){
168
+ b.innerHTML='<span class="dim">select a node or edge</span>';return}
169
+ let h='';
170
+ if(sel.kind&&sel.label!==undefined&&!sel.source){
171
+ h=`<div class="k"><b>${sel.label||sel.id}</b></div>
172
+ <span class="pill">${sel.kind}</span><span class="pill">${sel.epistemic_state}</span>
173
+ <span class="pill">${sel.source_provider||''}</span>
174
+ <h4>Attributes</h4><div class="k">${JSON.stringify(sel.attributes||{},null,1)}</div>
175
+ <h4>Evidence</h4>${(sel.evidence_refs||[]).map(e=>`<div class="ev">${e}</div>`)
176
+ .join('')||'<span class="dim">none</span>'}
177
+ <h4>Edges (${G.edges.filter(e=>e.source===sel.id||e.target===sel.id).length})</h4>`;
178
+ }else{h=`<div class="k"><b>${sel.kind}</b></div>
179
+ <div class="k dim">${sel.source} → ${sel.target}</div>
180
+ <span class="pill">${sel.epistemic_state}</span><span class="pill">${sel.provenance||''}</span>
181
+ ${sel.confidence!=null?`<span class="pill">conf ${sel.confidence}</span>`:''}
182
+ <h4>Evidence</h4>${(sel.evidence_refs||[]).map(e=>`<div class="ev">${e}</div>`)
183
+ .join('')||'<span class="dim">none</span>'}
184
+ <h4>Temporal</h4><div class="k">${Object.keys(sel.temporal||{}).length?
185
+ JSON.stringify(sel.temporal):'none'}</div>`}
186
+ b.innerHTML=h}
187
+ document.getElementById('q').oninput=draw;
188
+ // --- premium interactions (Cycle 4): neighbors / export / diff / snapshot ---
189
+ let focusSet=null; // BFS subgraph when 'n' pressed
190
+ function neighbors(seed){const adj={};G.edges.forEach(e=>{
191
+ (adj[e.source]=adj[e.source]||new Set()).add(e.target);
192
+ (adj[e.target]=adj[e.target]||new Set()).add(e.source)});
193
+ const seen=new Set([seed]);let fr=[seed];
194
+ for(let d=0;d<2&&fr.length;d++){const nx=[];fr.forEach(id=>{
195
+ (adj[id]||[]).forEach(m=>{if(!seen.has(m)){seen.add(m);nx.push(m)}})});fr=nx}
196
+ return seen}
197
+ async function exportVisible(){const q=(document.getElementById('q').value||'').toLowerCase();
198
+ const vis={};G.nodes.forEach(n=>{const s=(n.label||'')+' '+n.id;
199
+ vis[n.id]=kindsOn[n.kind]&&(!q||s.toLowerCase().includes(q))&&(!focusSet||focusSet.has(n.id))});
200
+ const doc={exported_at:new Date().toISOString(),
201
+ nodes:G.nodes.filter(n=>vis[n.id]).map(n=>({id:n.id,kind:n.kind,label:n.label,
202
+ epistemic_state:n.epistemic_state,attributes:n.attributes,evidence_refs:n.evidence_refs})),
203
+ edges:G.edges.filter(e=>vis[e.source]&&vis[e.target]&&ekindsOn[e.kind])};
204
+ const a=document.createElement('a');a.href=URL.createObjectURL(
205
+ new Blob([JSON.stringify(doc,null,1)],{type:'application/json'}));
206
+ a.download='forge-graph-subgraph.json';a.click()}
207
+ let diffRef=null;
208
+ async function diffMode(){const r=await fetch('/api/graphs');const d=await r.json();
209
+ if(d.graphs.length<2){document.getElementById('prov').textContent=
210
+ 'diff needs ≥2 graphs loaded';return}
211
+ const other=d.graphs.map(g=>g.graph_id).find(id=>id!==curId)||d.graphs[0].graph_id;
212
+ const rr=await fetch('/api/graph/'+encodeURIComponent(other));const o=await rr.json();
213
+ const on={};(o.nodes||[]).forEach(n=>on[n.id]=n);
214
+ const oe={};(o.edges||[]).forEach(e=>oe[e.source+'>'+e.target+':'+e.kind]=1);
215
+ const eadded=G.edges.filter(e=>!oe[e.source+'>'+e.target+':'+e.kind]).length;
216
+ let added=0,removed=0,changed=0;const cls={};
217
+ G.nodes.forEach(n=>{if(!on[n.id]){cls[n.id]='added';added++}else if(
218
+ JSON.stringify(on[n.id].attributes)!==JSON.stringify(n.attributes)||
219
+ on[n.id].epistemic_state!==n.epistemic_state){cls[n.id]='changed';changed++}else cls[n.id]='same'});
220
+ removed=(o.nodes||[]).filter(n=>!G.nodes.find(x=>x.id===n.id)).length;
221
+ diffRef={other,cls};
222
+ drawDiffColors=cls;
223
+ document.getElementById('prov').textContent=
224
+ `diff vs ${other}: +${added} ~${changed} −${removed} nodes · +${eadded} edges (esc clears)`;
225
+ draw()}
226
+ let drawDiffColors=null;
227
+ const DC={added:'#7ee787',removed:'#f78166',changed:'#ffa657',same:null};
228
+ let curId=null;
229
+ const _loadGraph=loadGraph;
230
+ loadGraph=async function(id){curId=id;diffRef=null;drawDiffColors=null;
231
+ await _loadGraph(id)}
232
+ function snapshotSave(){try{localStorage.setItem('forge-graph-snap',
233
+ JSON.stringify({graph:curId,view,q:document.getElementById('q').value,
234
+ kinds:kindsOn,ekinds:ekindsOn,when:Date.now()}));
235
+ document.getElementById('prov').textContent='snapshot saved (s restores)'}catch(e){}}
236
+ function snapshotLoad(){try{const s=JSON.parse(localStorage.getItem('forge-graph-snap')||'null');
237
+ if(!s)return;view=s.view;document.getElementById('q').value=s.q||'';
238
+ Object.assign(kindsOn,s.kinds||{});Object.assign(ekindsOn,s.ekinds||{});draw()}catch(e){}}
239
+ document.onkeydown=e=>{
240
+ if(e.key==='Escape'){view={x:0,y:0,s:1};sel=null;focusSet=null;diffRef=null;
241
+ drawDiffColors=null;inspect();draw()}
242
+ else if(e.key==='n'&&sel&&sel.id!==undefined){focusSet=focusSet?null:neighbors(sel.id);draw()}
243
+ else if(e.key==='e')exportVisible()
244
+ else if(e.key==='d')diffMode()
245
+ else if(e.key==='s'){localStorage.getItem('forge-graph-snap')?snapshotLoad():snapshotSave()}};
246
+ loadGraphs();
247
+ </script></body></html>
248
+ """
249
+
250
+
251
+ def _handler(views: dict[str, dict[str, Any]]) -> type[BaseHTTPRequestHandler]:
252
+ class StudioHandler(BaseHTTPRequestHandler):
253
+ def log_message(self, fmt: str, *args: object) -> None: # quiet
254
+ pass
255
+
256
+ def _send(self, code: int, body: bytes, ctype: str) -> None:
257
+ self.send_response(code)
258
+ self.send_header("Content-Type", ctype)
259
+ self.send_header("Content-Length", str(len(body)))
260
+ self.send_header("Cache-Control", "no-store")
261
+ self.end_headers()
262
+ self.wfile.write(body)
263
+
264
+ def _json(self, code: int, doc: object) -> None:
265
+ self._send(code, json.dumps(doc, default=str).encode(), "application/json")
266
+
267
+ def do_GET(self) -> None: # noqa: N802 - stdlib naming
268
+ path = self.path.split("?")[0].rstrip("/") or "/"
269
+ if path == "/":
270
+ self._send(200, STUDIO_HTML.encode(), "text/html; charset=utf-8")
271
+ return
272
+ if path == "/api/health":
273
+ self._json(200, {"status": "ok", "graphs": len(views)})
274
+ return
275
+ if path == "/api/graphs":
276
+ self._json(200, {"graphs": [v["descriptor"] for v in views.values()]})
277
+ return
278
+ if path.startswith("/api/graph/"):
279
+ gid = path[len("/api/graph/"):]
280
+ view = views.get(gid)
281
+ if view is None:
282
+ self._json(404, {"error": f"unknown graph {gid!r}"})
283
+ return
284
+ self._json(200, view)
285
+ return
286
+ self._json(404, {"error": "not found"})
287
+
288
+ def do_POST(self) -> None: # read-only by design (§4.4)
289
+ self._json(405, {"error": "Graph Studio is read-only"})
290
+
291
+ do_PUT = do_DELETE = do_PATCH = do_POST
292
+
293
+ return StudioHandler
294
+
295
+
296
+ def serve(
297
+ views: list[dict[str, Any]],
298
+ *,
299
+ host: str = "127.0.0.1",
300
+ port: int = 0,
301
+ open_browser: bool = True,
302
+ out: Any = None,
303
+ ) -> int:
304
+ """Serve the studio until Ctrl+C. ``views`` are ForgeGraphView dicts.
305
+
306
+ Binds loopback only — never exposes graphs on the network. With
307
+ ``open_browser=False`` (``--no-browser``, remote/SSH) the URL is
308
+ printed for port-forwarding instead.
309
+ """
310
+ out = out or __import__("sys").stdout
311
+ by_id = {v["descriptor"]["graph_id"]: v for v in views}
312
+ httpd = ThreadingHTTPServer((host, port), _handler(by_id))
313
+ bound = httpd.server_address[1]
314
+ url = f"http://{host}:{bound}/"
315
+ if open_browser:
316
+ threading.Timer(0.3, lambda: webbrowser.open(url)).start()
317
+ print(f"Graph Studio → {url} (Ctrl+C to stop)", file=out)
318
+ else:
319
+ print(f"Graph Studio listening on {url}", file=out)
320
+ print(f" remote? forward with: ssh -L {bound}:127.0.0.1:{bound} <host>", file=out)
321
+ try:
322
+ httpd.serve_forever()
323
+ except KeyboardInterrupt:
324
+ pass
325
+ finally:
326
+ httpd.server_close()
327
+ return 0
328
+
329
+
330
+ def graph_studio_enabled(
331
+ root: Path | str | None = None,
332
+ *,
333
+ state_rel: str = ".forge/install",
334
+ user_state_rel: str = "~/.forge/install",
335
+ ) -> bool:
336
+ """Was Graph Studio declined at install time?
337
+
338
+ ``components.json`` under the forge's state dir records the optional-
339
+ component selection (``graph_studio: false``). Absence means the
340
+ default — enabled — so checkouts and pre-component installs keep
341
+ working.
342
+ """
343
+ import json
344
+
345
+ candidates: list[Path] = []
346
+ if root:
347
+ candidates.append(Path(root) / state_rel / "components.json")
348
+ candidates.append(Path(user_state_rel).expanduser() / "components.json")
349
+ for p in candidates:
350
+ if not p.is_file():
351
+ continue
352
+ try:
353
+ doc = json.loads(p.read_text(encoding="utf-8"))
354
+ except Exception:
355
+ continue
356
+ if "graph_studio" in doc:
357
+ return bool(doc["graph_studio"])
358
+ return True
359
+
360
+
361
+ def open_studio(
362
+ views: list[Any],
363
+ *,
364
+ open_browser: bool = True,
365
+ port: int = 0,
366
+ out: Any = None,
367
+ ) -> int:
368
+ """Entry for ``graph ui``: accept views or view objects, then serve."""
369
+ docs = [v.to_dict() if hasattr(v, "to_dict") else v for v in views]
370
+ if not docs:
371
+ print(
372
+ "no graphs available — produce one first (see `graph build`/`graph` docs)",
373
+ file=out,
374
+ )
375
+ return 2
376
+ return serve(docs, open_browser=open_browser, port=port, out=out)
@@ -0,0 +1,181 @@
1
+ """ForgeGraphView/v1 — the visualization contract for Graph Studio.
2
+
3
+ A read-only, rendering-ready projection of a forge's real graph. The
4
+ graph engines stay authoritative; this contract only *carries* their
5
+ data to a viewer without losing provenance, epistemic state or evidence
6
+ references. Layout coordinates are deliberately absent — they belong to
7
+ the visualization layer, not the model.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import time
13
+ import uuid
14
+ from dataclasses import asdict, dataclass, field
15
+ from typing import Any
16
+
17
+ GRAPHVIEW_SCHEMA = "forge/ForgeGraphView/v1"
18
+ GRAPH_DESCRIPTOR_SCHEMA = "forge/GraphDescriptor/v1"
19
+
20
+ # Epistemic states a view may carry. Engines map their own vocabulary
21
+ # onto these explicitly; unmapped states stay "unknown", never upgraded.
22
+ EPISTEMIC_STATES = (
23
+ "observed",
24
+ "declared",
25
+ "planned",
26
+ "desired",
27
+ "inferred",
28
+ "unknown",
29
+ )
30
+
31
+ # Capabilities an adapter may declare — only real ones.
32
+ GRAPH_CAPABILITIES = (
33
+ "snapshots",
34
+ "node_inspect",
35
+ "edge_inspect",
36
+ "neighbors",
37
+ "paths",
38
+ "dependency_traversal",
39
+ "impact_analysis",
40
+ "snapshot_diff",
41
+ "temporal",
42
+ "search",
43
+ "filter",
44
+ "export",
45
+ )
46
+
47
+
48
+ # Engine vocabulary → view vocabulary (explicit, reversible): a state an
49
+ # engine uses that has a view equivalent is translated; anything else
50
+ # stays "unknown" — never silently upgraded to observed/declared.
51
+ EPISTEMIC_TRANSLATION = {
52
+ "explicit": "declared",
53
+ "asserted": "declared",
54
+ "actual": "observed",
55
+ }
56
+
57
+
58
+ def _check(state: str) -> str:
59
+ if state in EPISTEMIC_STATES:
60
+ return state
61
+ return EPISTEMIC_TRANSLATION.get(state, "unknown")
62
+
63
+
64
+ @dataclass(frozen=True, kw_only=True)
65
+ class GraphDescriptor:
66
+ """Summary of one graph a provider can expose (§1.2)."""
67
+
68
+ graph_id: str
69
+ provider_id: str
70
+ domain: str
71
+ graph_schema: str = GRAPH_DESCRIPTOR_SCHEMA
72
+ snapshot_id: str = ""
73
+ generated_at: str = ""
74
+ node_count: int = 0
75
+ edge_count: int = 0
76
+ available_layers: tuple[str, ...] = ()
77
+ available_queries: tuple[str, ...] = ()
78
+ capabilities: tuple[str, ...] = ()
79
+ limitations: tuple[str, ...] = ()
80
+
81
+ def to_dict(self) -> dict[str, Any]:
82
+ return asdict(self)
83
+
84
+
85
+ @dataclass(frozen=True, kw_only=True)
86
+ class GraphNodeView:
87
+ """One node, with engine provenance preserved (§1.3)."""
88
+
89
+ id: str
90
+ kind: str
91
+ label: str
92
+ domain: str = ""
93
+ source_provider: str = ""
94
+ attributes: dict[str, Any] = field(default_factory=dict)
95
+ epistemic_state: str = "unknown"
96
+ evidence_refs: tuple[str, ...] = ()
97
+ snapshot_ref: str = ""
98
+
99
+ def __post_init__(self) -> None:
100
+ object.__setattr__(self, "epistemic_state", _check(self.epistemic_state))
101
+
102
+ def to_dict(self) -> dict[str, Any]:
103
+ return asdict(self)
104
+
105
+
106
+ @dataclass(frozen=True, kw_only=True)
107
+ class GraphEdgeView:
108
+ """One directed edge; direction and semantics preserved (§1.4)."""
109
+
110
+ id: str
111
+ source: str
112
+ target: str
113
+ kind: str
114
+ direction: str = "directed"
115
+ provenance: str = "unknown"
116
+ epistemic_state: str = "unknown"
117
+ evidence_refs: tuple[str, ...] = ()
118
+ confidence: float | None = None
119
+ temporal: dict[str, Any] = field(default_factory=dict)
120
+ attributes: dict[str, Any] = field(default_factory=dict)
121
+
122
+ def __post_init__(self) -> None:
123
+ object.__setattr__(self, "epistemic_state", _check(self.epistemic_state))
124
+
125
+ def to_dict(self) -> dict[str, Any]:
126
+ d = asdict(self)
127
+ if self.confidence is None:
128
+ d.pop("confidence")
129
+ return d
130
+
131
+
132
+ @dataclass(frozen=True, kw_only=True)
133
+ class ForgeGraphView:
134
+ """A complete view document: descriptor + nodes + edges."""
135
+
136
+ descriptor: GraphDescriptor
137
+ nodes: tuple[GraphNodeView, ...] = ()
138
+ edges: tuple[GraphEdgeView, ...] = ()
139
+ schema: str = GRAPHVIEW_SCHEMA
140
+
141
+ def to_dict(self) -> dict[str, Any]:
142
+ return {
143
+ "schema": self.schema,
144
+ "descriptor": self.descriptor.to_dict(),
145
+ "nodes": [n.to_dict() for n in self.nodes],
146
+ "edges": [e.to_dict() for e in self.edges],
147
+ }
148
+
149
+ @staticmethod
150
+ def from_dict(doc: dict[str, Any]) -> ForgeGraphView:
151
+ d = doc["descriptor"]
152
+ descriptor = GraphDescriptor(
153
+ **{k: tuple(v) if isinstance(v, list) else v for k, v in d.items()}
154
+ )
155
+ nodes = tuple(
156
+ GraphNodeView(
157
+ **{k: tuple(v) if isinstance(v, list) else v for k, v in n.items()}
158
+ )
159
+ for n in doc.get("nodes", [])
160
+ )
161
+ edges = tuple(
162
+ GraphEdgeView(
163
+ **{k: tuple(v) if isinstance(v, list) else v for k, v in e.items()}
164
+ )
165
+ for e in doc.get("edges", [])
166
+ )
167
+ return ForgeGraphView(descriptor=descriptor, nodes=nodes, edges=edges)
168
+
169
+
170
+ def new_descriptor(*, provider_id: str, domain: str, graph_id: str | None = None,
171
+ capabilities: tuple[str, ...] = (),
172
+ limitations: tuple[str, ...] = ()) -> GraphDescriptor:
173
+ """A descriptor with real provenance stamps."""
174
+ return GraphDescriptor(
175
+ graph_id=graph_id or f"graph-{uuid.uuid4().hex[:8]}",
176
+ provider_id=provider_id,
177
+ domain=domain,
178
+ generated_at=time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
179
+ capabilities=tuple(c for c in capabilities if c in GRAPH_CAPABILITIES),
180
+ limitations=limitations,
181
+ )
File without changes
@@ -0,0 +1,39 @@
1
+ """AsyncAPI semantic model (§20)."""
2
+
3
+ from forge_doctor_api.analyzers.asyncapi.compat import diff_asyncapi_models
4
+ from forge_doctor_api.analyzers.asyncapi.graph import async_graph
5
+ from forge_doctor_api.analyzers.asyncapi.model import (
6
+ ASYNCAPI_MODEL_SCHEMA_VERSION,
7
+ AsyncAction,
8
+ AsyncApiChannel,
9
+ AsyncApiDocument,
10
+ AsyncApiIssue,
11
+ AsyncApiMessage,
12
+ AsyncApiOperation,
13
+ AsyncApiProjectModel,
14
+ AsyncApiSchema,
15
+ AsyncApiSecurityRequirement,
16
+ AsyncApiServer,
17
+ AsyncDocumentStatus,
18
+ AsyncIssueCode,
19
+ )
20
+ from forge_doctor_api.analyzers.asyncapi.parser import load_asyncapi_project
21
+
22
+ __all__ = [
23
+ "ASYNCAPI_MODEL_SCHEMA_VERSION",
24
+ "AsyncAction",
25
+ "AsyncApiChannel",
26
+ "AsyncApiDocument",
27
+ "AsyncApiIssue",
28
+ "AsyncApiMessage",
29
+ "AsyncApiOperation",
30
+ "AsyncApiProjectModel",
31
+ "AsyncApiSchema",
32
+ "AsyncApiSecurityRequirement",
33
+ "AsyncApiServer",
34
+ "AsyncDocumentStatus",
35
+ "AsyncIssueCode",
36
+ "async_graph",
37
+ "diff_asyncapi_models",
38
+ "load_asyncapi_project",
39
+ ]