@shortlink-org/portolan 0.2.3 → 0.3.0

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 (184) hide show
  1. package/README.md +23 -0
  2. package/catalog/enum_test.go +46 -0
  3. package/catalog/evidence_test.go +35 -0
  4. package/catalog/model.go +1066 -0
  5. package/catalog/roundtrip_test.go +203 -0
  6. package/catalog/via_test.go +38 -0
  7. package/cli/init.test.mjs +6 -1
  8. package/cli/portolan.mjs +8 -0
  9. package/cli/portolan.test.mjs +49 -0
  10. package/go.mod +14 -0
  11. package/go.sum +20 -0
  12. package/internal/gocall/README.md +19 -0
  13. package/internal/gocall/analyze.go +189 -0
  14. package/internal/gocall/analyze_test.go +107 -0
  15. package/internal/gohttp/analyze.go +2562 -0
  16. package/internal/gohttp/destination.go +373 -0
  17. package/internal/gohttp/endpoints.go +1067 -0
  18. package/internal/gohttp/roots.go +320 -0
  19. package/internal/gohttp/typed.go +96 -0
  20. package/internal/goscan/constants.go +85 -0
  21. package/internal/goscan/goscan_test.go +227 -0
  22. package/internal/goscan/index.go +629 -0
  23. package/internal/goscan/index_test.go +66 -0
  24. package/internal/goscan/names.go +52 -0
  25. package/internal/goscan/parse_test.go +11 -0
  26. package/internal/goscan/source.go +37 -0
  27. package/internal/goscan/tree.go +284 -0
  28. package/internal/goscan/types.go +99 -0
  29. package/internal/wsdl/ids.go +127 -0
  30. package/internal/wsdl/ids_test.go +21 -0
  31. package/internal/wsdl/model.go +70 -0
  32. package/internal/wsdl/parse.go +949 -0
  33. package/internal/wsdl/parse_test.go +170 -0
  34. package/package.json +22 -10
  35. package/plugin/describe.go +118 -0
  36. package/plugin/describe_test.go +114 -0
  37. package/plugin/protocol.go +141 -0
  38. package/plugin/schematest/schematest.go +126 -0
  39. package/plugins/README.md +114 -46
  40. package/plugins/cmd/portolan-http-clients/main.go +19 -0
  41. package/plugins/extract-celery/extract.py +0 -2
  42. package/plugins/extract-celery/extract_test.py +1 -1
  43. package/plugins/extract-django/README.md +39 -17
  44. package/plugins/extract-django/domain.py +28 -17
  45. package/plugins/extract-django/extract.py +21 -7
  46. package/plugins/extract-django/extract_test.py +55 -2
  47. package/plugins/extract-django/lifecycle.py +2 -0
  48. package/plugins/extract-django/operations.py +1 -1
  49. package/plugins/extract-django/routing_test.py +109 -1
  50. package/plugins/extract-django/store.py +1 -1
  51. package/plugins/extract-django/transport.py +101 -55
  52. package/plugins/extract-django/verbs.py +241 -0
  53. package/plugins/extract-go/README.md +47 -0
  54. package/plugins/extract-http-clients/describe.go +19 -0
  55. package/plugins/extract-http-clients/describe_test.go +11 -0
  56. package/plugins/extract-http-clients/extract.go +740 -0
  57. package/plugins/extract-http-clients/extract_test.go +1561 -0
  58. package/plugins/extract-http-clients/main.go +41 -0
  59. package/plugins/extract-java/build/org/portolan/extract/Extract.class +0 -0
  60. package/plugins/extract-java/build/org/portolan/extract/Protocol$Builder.class +0 -0
  61. package/plugins/extract-java/build/org/portolan/extract/Protocol$Input.class +0 -0
  62. package/plugins/extract-java/build/org/portolan/extract/Protocol$Options.class +0 -0
  63. package/plugins/extract-python-kafka/README.md +6 -0
  64. package/plugins/extract-python-kafka/extract.py +2 -4
  65. package/plugins/extract-python-kafka/extract_test.py +18 -2
  66. package/plugins/extract-ts/extract.test.ts +2 -2
  67. package/plugins/extract-ts/extract.ts +4 -5
  68. package/plugins/extract-ts/graphql.test.ts +1 -1
  69. package/plugins/openapi/ids.go +261 -0
  70. package/plugins/openapi/ids_test.go +98 -0
  71. package/plugins/portolan-go.wasm +0 -0
  72. package/plugins/pyplugin/catalog.py +12 -1
  73. package/plugins/pyplugin/kafka.py +74 -3
  74. package/plugins/pyplugin/protocol.py +1 -5
  75. package/portolan.json +3 -2
  76. package/schema/portolan.schema.json +34 -0
  77. package/scripts/README.md +18 -12
  78. package/scripts/catalog-sources.mjs +6 -0
  79. package/scripts/delivery-presets.mjs +21 -11
  80. package/scripts/diff.mjs +5 -1
  81. package/scripts/django-aggregates.test.mjs +58 -0
  82. package/scripts/gen-likec4.mjs +79 -21
  83. package/scripts/gen-likec4.test.mjs +25 -2
  84. package/scripts/gen.mjs +118 -115
  85. package/scripts/go-discovery.test.mjs +30 -0
  86. package/scripts/history.mjs +186 -3
  87. package/scripts/history.test.mjs +1 -1
  88. package/scripts/host-plugins/fetch-git.mjs +77 -21
  89. package/scripts/host-plugins/fetch-git.test.mjs +62 -8
  90. package/scripts/local-api.mjs +71 -4
  91. package/scripts/local-api.test.mjs +63 -4
  92. package/scripts/local-discovery.mjs +82 -9
  93. package/scripts/manifest.mjs +5 -3
  94. package/scripts/manifest.test.mjs +24 -0
  95. package/scripts/output-diff.mjs +94 -0
  96. package/scripts/output-diff.test.mjs +36 -0
  97. package/scripts/package-smoke.mjs +62 -4
  98. package/scripts/plugin-host.mjs +22 -2
  99. package/scripts/plugin-host.test.mjs +9 -0
  100. package/scripts/plugin-wasm-worker.mjs +4 -1
  101. package/scripts/provenance.mjs +72 -0
  102. package/scripts/provenance.test.mjs +149 -0
  103. package/scripts/run-builtin.mjs +39 -5
  104. package/scripts/schema.mjs +29 -0
  105. package/scripts/warning-policy.mjs +161 -0
  106. package/scripts/warning-policy.test.mjs +56 -0
  107. package/src/app/Breadcrumbs.test.ts +4 -0
  108. package/src/app/Breadcrumbs.tsx +1 -0
  109. package/src/app/Sidebar.tsx +3 -3
  110. package/src/catalog-docs.test.ts +64 -0
  111. package/src/catalog-docs.ts +35 -0
  112. package/src/catalog-error.test.ts +15 -0
  113. package/src/catalog-model.ts +70 -6
  114. package/src/catalog-stores.test.ts +17 -0
  115. package/src/catalog-validation.ts +52 -2
  116. package/src/catalog.test.ts +13 -2
  117. package/src/chat/Starter.tsx +5 -11
  118. package/src/chat/tools.test.ts +27 -0
  119. package/src/chat/tools.ts +5 -9
  120. package/src/components/CatalogStamp.tsx +10 -8
  121. package/src/components/ChannelRows.messagepack.test.tsx +28 -0
  122. package/src/components/ChannelRows.test.tsx +54 -0
  123. package/src/components/ChannelRows.tsx +57 -10
  124. package/src/components/HTTPDestinationEvidence.test.tsx +23 -0
  125. package/src/components/HTTPDestinationEvidence.tsx +31 -0
  126. package/src/components/Integrations.tsx +1 -1
  127. package/src/components/LifecycleDiagram.tsx +28 -12
  128. package/src/components/MachineDocs.tsx +6 -5
  129. package/src/components/MethodRows.tsx +9 -2
  130. package/src/components/ProblemRow.tsx +4 -0
  131. package/src/components/RelationEvidence.test.tsx +14 -0
  132. package/src/components/RelationEvidence.tsx +53 -0
  133. package/src/components/WhatLinksHere.tsx +6 -4
  134. package/src/data.ts +25 -7
  135. package/src/enrich.test.ts +336 -6
  136. package/src/enrich.ts +206 -3
  137. package/src/flow/StepDetail.tsx +104 -54
  138. package/src/flow/answers.test.ts +18 -1
  139. package/src/flow/answers.ts +37 -8
  140. package/src/flow/evidence.test.ts +16 -0
  141. package/src/flow/evidence.ts +34 -0
  142. package/src/index.css +44 -0
  143. package/src/landing/DraggableReveal.tsx +3 -2
  144. package/src/landing/EvidencePipeline.tsx +105 -0
  145. package/src/landing/LandingPage.tsx +2 -59
  146. package/src/lib/backlinks.test.ts +16 -1
  147. package/src/lib/backlinks.ts +20 -0
  148. package/src/lib/catalog-diff.test.ts +18 -0
  149. package/src/lib/catalog-diff.ts +20 -2
  150. package/src/lib/derive.ts +1 -0
  151. package/src/lib/django-aggregates.d.mts +9 -0
  152. package/src/lib/django-aggregates.mjs +36 -0
  153. package/src/lib/django-aggregates.test.ts +29 -0
  154. package/src/lib/django-aggregates.ts +5 -0
  155. package/src/lib/kafka-ui.test.ts +87 -0
  156. package/src/lib/kafka-ui.ts +105 -0
  157. package/src/lib/local-api.ts +20 -2
  158. package/src/lib/setup-info.test.ts +17 -0
  159. package/src/lib/setup-info.ts +58 -0
  160. package/src/lib/warnings.test.ts +54 -0
  161. package/src/lib/warnings.ts +260 -0
  162. package/src/lib/wire-problems.test.ts +21 -0
  163. package/src/lib/wire-problems.ts +62 -1
  164. package/src/likec4/FlowView.tsx +2 -6
  165. package/src/likec4/flow-edges.test.ts +64 -1
  166. package/src/likec4/flow-edges.ts +43 -7
  167. package/src/likec4/view-index.ts +8 -2
  168. package/src/map/ContextMapGraph.tsx +76 -32
  169. package/src/merge.test.ts +23 -0
  170. package/src/merge.ts +33 -10
  171. package/src/pages/AggregatePage.tsx +8 -7
  172. package/src/pages/CatalogFailure.tsx +2 -2
  173. package/src/pages/ContextPage.tsx +6 -5
  174. package/src/pages/ServicePage.tsx +4 -3
  175. package/src/pages/Settings.tsx +200 -44
  176. package/src/pages/settings/DjangoAggregateChoices.tsx +79 -0
  177. package/src/pages/settings/IntegrationsSettings.tsx +117 -0
  178. package/src/routes.test.ts +2 -0
  179. package/src/routes.ts +2 -1
  180. package/src/selection/DetailPanel.tsx +61 -1
  181. package/src/virtual-provenance.d.ts +11 -0
  182. package/vite.config.ts +5 -0
  183. package/scripts/vendor-lock.mjs +0 -58
  184. package/scripts/vendor-lock.test.mjs +0 -69
@@ -0,0 +1,241 @@
1
+ """Which HTTP verb a mounted handler answers, read from what the code declares.
2
+
3
+ A URLConf can mount any callable — ``path("planet/fetch", Planet.fetch)`` —
4
+ and then the route table proves the path but says nothing about the verb.
5
+ The verb is still written down somewhere in the handler, in one of the ways
6
+ Django and DRF offer for it, and this module reads them in the order a
7
+ reviewer would trust them:
8
+
9
+ 1. a decorator on the handler: DRF's ``@action(methods=…)`` and ``@api_view``,
10
+ Django's ``@require_http_methods([…])``, ``@require_GET``, ``@require_POST``
11
+ and ``@require_safe``, also through ``method_decorator(…)``;
12
+ 2. the same decorators on the class, ``@method_decorator(…, name="dispatch")``;
13
+ 3. the class's ``http_method_names``;
14
+ 4. a branch on ``request.method`` in the handler body;
15
+ 5. a project wrapper — a decorator or a function the handler hands ``request``
16
+ to — whose own body does one of the above, followed a bounded number of
17
+ levels deep.
18
+
19
+ The first tier that speaks decides. Nothing here is guessed: a handler none
20
+ of the tiers describes has no verb, and the caller says so rather than
21
+ inventing one.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import ast
27
+ from dataclasses import dataclass
28
+ from typing import List, Optional, Set, Tuple
29
+
30
+ from source import Module, Project, const_str, dotted, keyword, methods, node_name
31
+
32
+ HTTP_VERBS = ("GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "TRACE")
33
+
34
+ # What a framework answers for every route regardless of the handler; listing
35
+ # them never says what the handler is for, so they are dropped when any other
36
+ # verb is named beside them.
37
+ IMPLICIT = {"HEAD", "OPTIONS", "TRACE"}
38
+
39
+ SHORTHAND = {"require_GET": ("GET",), "require_POST": ("POST",), "require_safe": ("GET",)}
40
+ LISTED = ("require_http_methods", "action", "api_view")
41
+
42
+ # How far a project wrapper is followed before the reader stops. Two levels
43
+ # cover a decorator that wraps a decorator; anything deeper is a project that
44
+ # should declare its verb where the handler is.
45
+ DEPTH = 3
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class Evidence:
50
+ verbs: Tuple[str, ...]
51
+ rule: str # "decorator", "class decorator", "http_method_names", "request.method", "wrapper <name>"
52
+ source: str # file:line of what was read
53
+
54
+
55
+ class Reader:
56
+ def __init__(self, project: Optional[Project]):
57
+ self.project = project
58
+
59
+ # --- the tiers -----------------------------------------------------------
60
+
61
+ def for_handler(self, module: Module, handler: ast.AST, owner: Optional[ast.ClassDef] = None) -> Optional[Evidence]:
62
+ found = self.decorators(module, handler, handler_name=getattr(handler, "name", ""))
63
+ if found is None and owner is not None:
64
+ found = self.decorators(module, owner, handler_name=getattr(handler, "name", ""), rule="class decorator")
65
+ if found is None and owner is not None:
66
+ found = self.method_names(module, owner)
67
+ if found is None:
68
+ found = self.branches(module, handler)
69
+ if found is None:
70
+ found = self.wrappers(module, handler, owner, DEPTH, set())
71
+ return found
72
+
73
+ def decorators(self, module: Module, node: ast.AST, handler_name: str = "", rule: str = "decorator") -> Optional[Evidence]:
74
+ for dec in getattr(node, "decorator_list", []) or []:
75
+ inner = dec
76
+ # ``method_decorator(require_POST)`` on a method, or on the class
77
+ # with ``name=`` saying which method; ``dispatch`` means every one.
78
+ if isinstance(inner, ast.Call) and dotted(inner.func).split(".")[-1] == "method_decorator" and inner.args:
79
+ applies_to = const_str(keyword(inner, "name"))
80
+ if rule == "class decorator" and applies_to not in ("", "dispatch", handler_name):
81
+ continue
82
+ inner = inner.args[0]
83
+ verbs = listed_verbs(inner)
84
+ if verbs:
85
+ return Evidence(verbs, rule, module.where(dec))
86
+ return None
87
+
88
+ def method_names(self, module: Module, owner: ast.ClassDef) -> Optional[Evidence]:
89
+ for stmt in owner.body:
90
+ if not isinstance(stmt, (ast.Assign, ast.AnnAssign)):
91
+ continue
92
+ targets = stmt.targets if isinstance(stmt, ast.Assign) else [stmt.target]
93
+ if not any(isinstance(target, ast.Name) and target.id == "http_method_names" for target in targets):
94
+ continue
95
+ verbs = verbs_in(stmt.value)
96
+ if verbs:
97
+ return Evidence(verbs, "http_method_names", module.where(stmt))
98
+ return None
99
+
100
+ def branches(self, module: Module, node: ast.AST) -> Optional[Evidence]:
101
+ found: List[str] = []
102
+ where = ""
103
+ for compare in ast.walk(node):
104
+ if not isinstance(compare, ast.Compare):
105
+ continue
106
+ sides = [compare.left] + list(compare.comparators)
107
+ if not any(is_request_method(side) for side in sides):
108
+ continue
109
+ for side in sides:
110
+ for verb in verbs_in(side, filter_implicit=False):
111
+ if verb not in found:
112
+ found.append(verb)
113
+ where = where or module.where(compare)
114
+ verbs = normalise(found)
115
+ return Evidence(verbs, "request.method", where) if verbs else None
116
+
117
+ def wrappers(self, module: Module, handler: ast.AST, owner: Optional[ast.ClassDef], depth: int, seen: Set[Tuple[str, str]]) -> Optional[Evidence]:
118
+ if depth <= 0 or self.project is None:
119
+ return None
120
+ candidates: List[Tuple[Module, ast.AST, Optional[ast.ClassDef], str]] = []
121
+ for dec in getattr(handler, "decorator_list", []) or []:
122
+ target = dec.func if isinstance(dec, ast.Call) else dec
123
+ resolved = self.callable_target(module, owner, target)
124
+ if resolved is not None:
125
+ candidates.append(resolved + (dotted(target),))
126
+ for call in ast.walk(handler):
127
+ if not isinstance(call, ast.Call) or not passes_request(call):
128
+ continue
129
+ resolved = self.callable_target(module, owner, call.func)
130
+ if resolved is not None:
131
+ candidates.append(resolved + (dotted(call.func),))
132
+ for target_module, target, target_owner, name in candidates:
133
+ key = (target_module.dotted, node_name(target) if not target_owner else target_owner.name + "." + node_name(target))
134
+ if key in seen:
135
+ continue
136
+ seen.add(key)
137
+ found = self.decorators(target_module, target) or self.calls(target_module, target) or self.branches(target_module, target)
138
+ if found is None:
139
+ found = self.wrappers(target_module, target, target_owner, depth - 1, seen)
140
+ if found is not None:
141
+ return Evidence(found.verbs, "wrapper %s" % name, found.source)
142
+ return None
143
+
144
+ def calls(self, module: Module, node: ast.AST) -> Optional[Evidence]:
145
+ """``return require_POST(view)`` inside a project decorator: the
146
+ framework decorator applied by hand rather than with ``@``."""
147
+ for call in ast.walk(node):
148
+ if not isinstance(call, ast.Call):
149
+ continue
150
+ for candidate in (call, call.func):
151
+ verbs = listed_verbs(candidate)
152
+ if verbs:
153
+ return Evidence(verbs, "decorator", module.where(call))
154
+ return None
155
+
156
+ # --- names ---------------------------------------------------------------
157
+
158
+ def callable_target(self, module: Module, owner: Optional[ast.ClassDef], node: ast.AST) -> Optional[Tuple[Module, ast.AST, Optional[ast.ClassDef]]]:
159
+ """A project function or method this name refers to; nothing for a
160
+ framework name, a builtin or anything the imports do not explain."""
161
+ name = dotted(node)
162
+ if not name or self.project is None:
163
+ return None
164
+ parts = name.split(".")
165
+ if len(parts) == 2 and parts[0] in ("self", "cls") and owner is not None:
166
+ for item in methods(owner):
167
+ if item.name == parts[1]:
168
+ return module, item, owner
169
+ return None
170
+ if len(parts) == 1:
171
+ hit = self.project.resolve(module, name)
172
+ if hit is None:
173
+ return None
174
+ target = function_in(hit[0], hit[1])
175
+ return (hit[0], target, None) if target is not None else None
176
+ imported = module.imports.get(parts[0])
177
+ if imported is None:
178
+ return None
179
+ target_module = self.project.module(".".join([imported.module] + ([] if imported.name == "*" else [imported.name]) + parts[1:-1]))
180
+ if target_module is None:
181
+ return None
182
+ target = function_in(target_module, parts[-1])
183
+ return (target_module, target, None) if target is not None else None
184
+
185
+
186
+ def function_in(module: Module, name: str) -> Optional[ast.AST]:
187
+ for node in module.tree.body:
188
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == name:
189
+ return node
190
+ return None
191
+
192
+
193
+ def listed_verbs(node: ast.AST) -> Tuple[str, ...]:
194
+ """The verbs a framework decorator names, however it was imported:
195
+ ``@require_http_methods(["GET", "POST"])``, ``@action(methods=["post"])``,
196
+ ``@api_view(["GET"])``, or the ``require_POST`` shorthand."""
197
+ name = dotted(node.func if isinstance(node, ast.Call) else node).split(".")[-1]
198
+ if name in SHORTHAND:
199
+ return SHORTHAND[name]
200
+ if not isinstance(node, ast.Call) or name not in LISTED:
201
+ return ()
202
+ listed = keyword(node, "methods") or keyword(node, "request_method_list") or keyword(node, "http_method_names")
203
+ if listed is None and node.args:
204
+ listed = node.args[0]
205
+ if listed is None and name in ("action", "api_view"):
206
+ # DRF's documented default when the decorator names no methods.
207
+ return ("GET",)
208
+ return verbs_in(listed)
209
+
210
+
211
+ def verbs_in(node: Optional[ast.AST], filter_implicit: bool = True) -> Tuple[str, ...]:
212
+ """The HTTP verbs a literal spells, upper-cased; anything that is not one
213
+ is left out, so ``["get", "frobnicate"]`` reads as GET alone."""
214
+ if node is None:
215
+ return ()
216
+ items = node.elts if isinstance(node, (ast.List, ast.Tuple, ast.Set)) else [node]
217
+ found = [const_str(item).upper() for item in items if const_str(item)]
218
+ return normalise(found) if filter_implicit else tuple(verb for verb in found if verb in HTTP_VERBS)
219
+
220
+
221
+ def normalise(found: List[str]) -> Tuple[str, ...]:
222
+ verbs = [verb for verb in found if verb in HTTP_VERBS]
223
+ explicit = [verb for verb in verbs if verb not in IMPLICIT]
224
+ out: List[str] = []
225
+ for verb in explicit or verbs:
226
+ if verb not in out:
227
+ out.append(verb)
228
+ return tuple(out)
229
+
230
+
231
+ def is_request_method(node: ast.AST) -> bool:
232
+ """``request.method``, ``self.request.method``, or either through
233
+ ``.upper()``/``.lower()``."""
234
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr in ("upper", "lower") and not node.args:
235
+ node = node.func.value
236
+ return dotted(node).endswith("request.method")
237
+
238
+
239
+ def passes_request(call: ast.Call) -> bool:
240
+ values = list(call.args) + [kw.value for kw in call.keywords]
241
+ return any(dotted(value) in ("request", "self.request") for value in values)
@@ -0,0 +1,47 @@
1
+ # Go domain and execution-flow extraction
2
+
3
+ The syntax extractor reads aggregates, events and use cases in the supported
4
+ DDD layouts. It also discovers registered HTTP execution roots without requiring
5
+ an aggregate, a `UseCase.Handle` method, an `internal` directory or a `scope`.
6
+
7
+ For conventional services, supported registrations include `net/http.HandleFunc`,
8
+ `ServeMux.HandleFunc`, and `ServeMux.Handle` with a `http.HandlerFunc` conversion.
9
+ The handler can be a function or a same-package method on an explicitly typed
10
+ parameter, receiver or local composite literal. Router verb registrations use
11
+ recognized router imports/types; the existing `http` package convention remains
12
+ available for syntax-only legacy fixtures. Concrete service methods and their
13
+ repository calls are followed by the existing flow walker. An unregistered
14
+ method does not become an HTTP root just because it resembles a handler.
15
+
16
+ `scope` still limits ownership to `internal/<scope>` in a shared module. Imported
17
+ local application methods can be followed across that boundary. Nested modules,
18
+ vendor, node_modules and testdata are excluded from package discovery. When a
19
+ DDD flow already covers the same handler, its richer domain flow is retained.
20
+
21
+ Assembly must not select the first of several use cases. Competing providers,
22
+ multiple adapter targets and unreadable multi-use-case providers leave the call
23
+ `unresolved`, with an `ambiguous-binding` warning and the candidate use cases.
24
+ A per-method adapter binding is retained only when all candidate providers agree.
25
+
26
+ This extractor remains syntax-only and runs in WASI. The shared native typed
27
+ analysis API lives in `internal/gocall`; its first consumer is the HTTP client
28
+ sidecar. Adopting that API in additional extractors is a separate migration,
29
+ without imposing a Go toolchain on this syntax path.
30
+
31
+ Go-domain, HTTP analysis and Go SQL readers now share `internal/goscan` for
32
+ source selection, imports, constants and package lookup. Each invocation reuses
33
+ its parsed AST across passes. Generated declarations remain available for
34
+ contract lookup but are marked separately. The index respects GOOS/GOARCH,
35
+ build constraints and nested module boundaries; it is not a cache shared across
36
+ plugin processes. Native type analysis still uses the Go package loader.
37
+
38
+ Flow steps carry structured `evidence`: source expressions, enclosing functions,
39
+ provider signatures and unresolved candidates. Provider signatures are marked as
40
+ inference, and static paths do not claim that a call ran. The detail panel also
41
+ explains older fragments from their recorded source and contract references.
42
+
43
+ For a service with plain SQL migrations, combine `go-domain` and `sql` with the
44
+ same context, service and store. SQL discovers `migrations`/`migration` directories
45
+ outside `internal` too. Without a matching domain root or an explicit
46
+ `-- aggregate: <id>` annotation, tables and views remain independent of aggregates;
47
+ their schema, foreign keys and SQL accesses are retained.
@@ -0,0 +1,19 @@
1
+ package extracthttpclients
2
+
3
+ import (
4
+ _ "embed"
5
+
6
+ "github.com/shortlink-org/portolan/plugin"
7
+ )
8
+
9
+ //go:embed options.schema.json
10
+ var optionsSchema []byte
11
+
12
+ func descriptor() plugin.Descriptor {
13
+ return plugin.Descriptor{
14
+ Name: "extract-http-clients",
15
+ Summary: "Reads outbound HTTP/SOAP calls and joins Go routes, handlers, factories, and providers into source-backed flows.",
16
+ Phases: []string{plugin.PhaseExtract},
17
+ Options: optionsSchema,
18
+ }
19
+ }
@@ -0,0 +1,11 @@
1
+ package extracthttpclients
2
+
3
+ import (
4
+ "testing"
5
+
6
+ "github.com/shortlink-org/portolan/plugin/schematest"
7
+ )
8
+
9
+ func TestOptionsSchemaMatchesStruct(t *testing.T) {
10
+ schematest.Check(t, optionsSchema, Options{})
11
+ }