@shortlink-org/portolan 0.2.4 → 0.4.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 (291) hide show
  1. package/README.md +174 -6
  2. package/catalog/enum_test.go +46 -0
  3. package/catalog/evidence_test.go +35 -0
  4. package/catalog/model.go +1213 -0
  5. package/catalog/roundtrip_test.go +210 -0
  6. package/catalog/via_test.go +38 -0
  7. package/cli/init.test.mjs +6 -1
  8. package/cli/portolan.mjs +14 -1
  9. package/cli/portolan.test.mjs +49 -0
  10. package/go.mod +21 -0
  11. package/go.sum +34 -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 +28 -15
  35. package/plugin/describe.go +156 -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 +404 -50
  40. package/plugins/cmd/portolan-http-clients/main.go +19 -0
  41. package/plugins/extract-argocd/options.schema.json +44 -0
  42. package/plugins/extract-celery/extract.py +0 -2
  43. package/plugins/extract-celery/extract_test.py +1 -1
  44. package/plugins/extract-celery/main.py +1 -0
  45. package/plugins/extract-csharp-ddd/README.md +213 -0
  46. package/plugins/extract-csharp-ddd/options.schema.json +55 -0
  47. package/plugins/extract-django/README.md +52 -19
  48. package/plugins/extract-django/choices.py +47 -0
  49. package/plugins/extract-django/domain.py +30 -18
  50. package/plugins/extract-django/extract.py +21 -7
  51. package/plugins/extract-django/extract_test.py +68 -2
  52. package/plugins/extract-django/lifecycle.py +4 -28
  53. package/plugins/extract-django/main.py +1 -0
  54. package/plugins/extract-django/operations.py +1 -1
  55. package/plugins/extract-django/routing_test.py +109 -1
  56. package/plugins/extract-django/rules.py +154 -0
  57. package/plugins/extract-django/rules_test.py +158 -0
  58. package/plugins/extract-django/store.py +1 -1
  59. package/plugins/extract-django/transport.py +101 -55
  60. package/plugins/extract-django/verbs.py +241 -0
  61. package/plugins/extract-go/README.md +47 -0
  62. package/plugins/extract-go-sqs/options.schema.json +21 -0
  63. package/plugins/extract-http-clients/describe.go +20 -0
  64. package/plugins/extract-http-clients/describe_test.go +11 -0
  65. package/plugins/extract-http-clients/extract.go +740 -0
  66. package/plugins/extract-http-clients/extract_test.go +1561 -0
  67. package/plugins/extract-http-clients/main.go +41 -0
  68. package/plugins/extract-java/build/org/portolan/extract/Extract.class +0 -0
  69. package/plugins/extract-java/build/org/portolan/extract/Main.class +0 -0
  70. package/plugins/extract-java/build/org/portolan/extract/Protocol$Builder.class +0 -0
  71. package/plugins/extract-java/build/org/portolan/extract/Protocol$Input.class +0 -0
  72. package/plugins/extract-java/build/org/portolan/extract/Protocol$Options.class +0 -0
  73. package/plugins/extract-k8s/options.schema.json +32 -0
  74. package/plugins/extract-laravel/Cargo.lock +962 -0
  75. package/plugins/extract-laravel/Cargo.toml +20 -0
  76. package/plugins/extract-laravel/README.md +200 -0
  77. package/plugins/extract-laravel/options.schema.json +68 -0
  78. package/plugins/extract-laravel/rustfmt.toml +1 -0
  79. package/plugins/extract-php-ddd/Cargo.lock +972 -0
  80. package/plugins/extract-php-ddd/Cargo.toml +22 -0
  81. package/plugins/extract-php-ddd/README.md +141 -0
  82. package/plugins/extract-php-ddd/options.schema.json +50 -0
  83. package/plugins/extract-php-ddd/rustfmt.toml +1 -0
  84. package/plugins/extract-python-kafka/extract.py +0 -2
  85. package/plugins/extract-python-kafka/extract_test.py +1 -1
  86. package/plugins/extract-python-kafka/main.py +1 -0
  87. package/plugins/extract-terraform/options.schema.json +26 -0
  88. package/plugins/extract-ts/extract.test.ts +2 -2
  89. package/plugins/extract-ts/extract.ts +4 -5
  90. package/plugins/extract-ts/graphql.test.ts +1 -1
  91. package/plugins/extract-ts/main.ts +1 -0
  92. package/plugins/openapi/ids.go +261 -0
  93. package/plugins/openapi/ids_test.go +98 -0
  94. package/plugins/phpscan/Cargo.lock +953 -0
  95. package/plugins/phpscan/Cargo.toml +19 -0
  96. package/plugins/phpscan/rustfmt.toml +1 -0
  97. package/plugins/portolan-go.wasm +0 -0
  98. package/plugins/pyplugin/catalog.py +24 -1
  99. package/plugins/pyplugin/protocol.py +1 -5
  100. package/plugins/verify-otel/options.schema.json +12 -0
  101. package/portolan.json +132 -4
  102. package/rules/builtin.json +218 -0
  103. package/schema/portolan.schema.json +905 -4
  104. package/scripts/README.md +21 -13
  105. package/scripts/catalog-sources.mjs +6 -0
  106. package/scripts/delivery-presets.mjs +57 -14
  107. package/scripts/diff.mjs +5 -1
  108. package/scripts/django-aggregates.test.mjs +58 -0
  109. package/scripts/gen-likec4.mjs +150 -17
  110. package/scripts/gen-likec4.test.mjs +96 -0
  111. package/scripts/gen.mjs +148 -118
  112. package/scripts/gitops-example.test.mjs +108 -0
  113. package/scripts/go-discovery.test.mjs +30 -0
  114. package/scripts/history.mjs +186 -3
  115. package/scripts/history.test.mjs +1 -1
  116. package/scripts/host-plugins/fetch-argocd.mjs +338 -0
  117. package/scripts/host-plugins/fetch-argocd.options.json +49 -0
  118. package/scripts/host-plugins/fetch-argocd.test.mjs +274 -0
  119. package/scripts/host-plugins/fetch-bsr.mjs +1 -0
  120. package/scripts/host-plugins/fetch-csr.mjs +1 -0
  121. package/scripts/host-plugins/fetch-git.mjs +78 -21
  122. package/scripts/host-plugins/fetch-git.test.mjs +62 -8
  123. package/scripts/host-plugins/fetch-k8s.mjs +263 -0
  124. package/scripts/host-plugins/fetch-k8s.options.json +50 -0
  125. package/scripts/host-plugins/fetch-k8s.test.mjs +259 -0
  126. package/scripts/host-plugins/k8s-topology.mjs +183 -0
  127. package/scripts/json-format.mjs +192 -0
  128. package/scripts/json-format.test.mjs +97 -0
  129. package/scripts/local-api.mjs +287 -12
  130. package/scripts/local-api.test.mjs +150 -5
  131. package/scripts/local-discovery.mjs +94 -9
  132. package/scripts/manifest.mjs +14 -3
  133. package/scripts/manifest.test.mjs +24 -0
  134. package/scripts/output-diff.mjs +94 -0
  135. package/scripts/output-diff.test.mjs +36 -0
  136. package/scripts/package-smoke.mjs +62 -4
  137. package/scripts/plugin-host.mjs +45 -3
  138. package/scripts/plugin-host.test.mjs +9 -0
  139. package/scripts/plugin-wasm-worker.mjs +4 -1
  140. package/scripts/plugins-fresh.mjs +97 -0
  141. package/scripts/plugins-fresh.test.mjs +64 -0
  142. package/scripts/provenance.mjs +72 -0
  143. package/scripts/provenance.test.mjs +149 -0
  144. package/scripts/run-builtin.mjs +62 -7
  145. package/scripts/schema.mjs +167 -19
  146. package/scripts/trace-trials.mjs +176 -0
  147. package/scripts/trace-trials.test.mjs +142 -0
  148. package/scripts/warning-policy.mjs +167 -0
  149. package/scripts/warning-policy.test.mjs +93 -0
  150. package/src/app/Breadcrumbs.test.ts +3 -0
  151. package/src/app/Breadcrumbs.tsx +3 -0
  152. package/src/app/CatalogApp.tsx +2 -0
  153. package/src/app/Sidebar.tsx +3 -3
  154. package/src/app/SidebarFooter.tsx +20 -4
  155. package/src/catalog-docs.test.ts +64 -0
  156. package/src/catalog-docs.ts +35 -0
  157. package/src/catalog-error.test.ts +15 -0
  158. package/src/catalog-index.ts +25 -0
  159. package/src/catalog-model.ts +293 -5
  160. package/src/catalog-validation.ts +113 -2
  161. package/src/catalog.test.ts +40 -1
  162. package/src/chat/Starter.tsx +5 -11
  163. package/src/chat/tools.test.ts +27 -0
  164. package/src/chat/tools.ts +5 -9
  165. package/src/components/CatalogStamp.tsx +10 -8
  166. package/src/components/ChannelRows.test.tsx +76 -1
  167. package/src/components/ChannelRows.tsx +223 -60
  168. package/src/components/DeploymentRows.tsx +131 -0
  169. package/src/components/DocsLinks.test.tsx +27 -0
  170. package/src/components/DocsLinks.tsx +56 -0
  171. package/src/components/FieldTree.tsx +5 -2
  172. package/src/components/HTTPDestinationEvidence.test.tsx +23 -0
  173. package/src/components/HTTPDestinationEvidence.tsx +31 -0
  174. package/src/components/Integrations.tsx +1 -1
  175. package/src/components/MachineDocs.tsx +6 -5
  176. package/src/components/MethodRows.tsx +9 -2
  177. package/src/components/PluginIcon.tsx +77 -0
  178. package/src/components/ProblemRow.tsx +173 -153
  179. package/src/components/RelationEvidence.test.tsx +14 -0
  180. package/src/components/RelationEvidence.tsx +53 -0
  181. package/src/components/RuleMarks.tsx +22 -0
  182. package/src/components/ShapeRows.tsx +24 -20
  183. package/src/data.ts +46 -7
  184. package/src/enrich.test.ts +459 -4
  185. package/src/enrich.ts +308 -7
  186. package/src/er/ErCanvas.tsx +218 -12
  187. package/src/er/GroupNode.tsx +57 -0
  188. package/src/er/StoreHeader.tsx +1 -0
  189. package/src/er/layout.test.ts +85 -2
  190. package/src/er/layout.ts +140 -5
  191. package/src/er/spec.test.ts +17 -0
  192. package/src/er/spec.ts +23 -10
  193. package/src/flow/Recordings.test.tsx +52 -0
  194. package/src/flow/Recordings.tsx +236 -0
  195. package/src/flow/StepDetail.tsx +59 -0
  196. package/src/flow/TraceTrial.tsx +419 -0
  197. package/src/flow/evidence.test.ts +16 -0
  198. package/src/flow/evidence.ts +34 -0
  199. package/src/flow/examples.test.ts +33 -0
  200. package/src/flow/examples.ts +37 -0
  201. package/src/flow/outline.test.ts +28 -0
  202. package/src/flow/outline.ts +7 -2
  203. package/src/flow/trace-trial-resume.test.ts +42 -0
  204. package/src/flow/trace-trial-resume.ts +74 -0
  205. package/src/graph/elk.ts +78 -0
  206. package/src/index.css +68 -0
  207. package/src/landing/DraggableReveal.tsx +3 -2
  208. package/src/landing/EvidencePipeline.tsx +105 -0
  209. package/src/landing/LandingPage.tsx +17 -70
  210. package/src/landing/ProductTour.tsx +6 -6
  211. package/src/lib/all-problems.ts +27 -17
  212. package/src/lib/catalog-diff.ts +1 -1
  213. package/src/lib/centrality.test.ts +251 -0
  214. package/src/lib/centrality.ts +232 -0
  215. package/src/lib/confluence.test.ts +37 -0
  216. package/src/lib/confluence.ts +41 -0
  217. package/src/lib/context-color.ts +1 -1
  218. package/src/lib/deployment-drift.ts +22 -0
  219. package/src/lib/derive.ts +43 -69
  220. package/src/lib/django-aggregates.d.mts +9 -0
  221. package/src/lib/django-aggregates.mjs +36 -0
  222. package/src/lib/django-aggregates.test.ts +29 -0
  223. package/src/lib/django-aggregates.ts +5 -0
  224. package/src/lib/environments.test.ts +83 -0
  225. package/src/lib/environments.ts +57 -0
  226. package/src/lib/integration-url.test.ts +30 -0
  227. package/src/lib/integration-url.ts +63 -0
  228. package/src/lib/kafka-ui.ts +3 -45
  229. package/src/lib/local-api.ts +116 -4
  230. package/src/lib/notion.ts +13 -0
  231. package/src/lib/plugin-index.json +2765 -0
  232. package/src/lib/plugins.test.ts +68 -0
  233. package/src/lib/plugins.ts +264 -0
  234. package/src/lib/problem-flows.test.ts +61 -0
  235. package/src/lib/problem-flows.ts +78 -0
  236. package/src/lib/problem-rules-cel.d.mts +43 -0
  237. package/src/lib/problem-rules-cel.mjs +407 -0
  238. package/src/lib/problem-rules.test.ts +287 -0
  239. package/src/lib/problem-rules.ts +271 -0
  240. package/src/lib/problem-subjects.ts +737 -0
  241. package/src/lib/rule-entries.ts +39 -0
  242. package/src/lib/{data-problems.test.ts → rules-data.test.ts} +18 -16
  243. package/src/lib/rules-deploy.test.ts +133 -0
  244. package/src/lib/{problems.test.ts → rules-edges.test.ts} +8 -3
  245. package/src/lib/{proto-problems.test.ts → rules-proto.test.ts} +9 -25
  246. package/src/lib/{wire-problems.test.ts → rules-wire.test.ts} +13 -11
  247. package/src/lib/rules.test.ts +51 -0
  248. package/src/lib/rules.ts +86 -0
  249. package/src/lib/setup-info.test.ts +17 -0
  250. package/src/lib/setup-info.ts +58 -0
  251. package/src/lib/shape.test.ts +32 -0
  252. package/src/lib/shape.ts +30 -6
  253. package/src/lib/tech.ts +16 -0
  254. package/src/lib/trace-project.test.ts +34 -0
  255. package/src/lib/trace-project.ts +50 -0
  256. package/src/lib/use-problems.ts +23 -0
  257. package/src/lib/warnings.test.ts +63 -0
  258. package/src/lib/warnings.ts +260 -0
  259. package/src/likec4/ids.test.ts +6 -2
  260. package/src/likec4/ids.ts +43 -0
  261. package/src/main.tsx +23 -0
  262. package/src/map/ContextMapGraph.tsx +76 -32
  263. package/src/merge-deployments.test.ts +127 -0
  264. package/src/merge.test.ts +82 -0
  265. package/src/merge.ts +189 -18
  266. package/src/pages/AggregatePage.tsx +65 -14
  267. package/src/pages/ContextMap.tsx +45 -3
  268. package/src/pages/ContextPage.tsx +8 -5
  269. package/src/pages/EventPage.tsx +15 -5
  270. package/src/pages/FlowDetail.tsx +23 -2
  271. package/src/pages/GraphPage.tsx +40 -3
  272. package/src/pages/Overview.tsx +152 -12
  273. package/src/pages/PluginIndex.tsx +190 -0
  274. package/src/pages/Problems.tsx +396 -128
  275. package/src/pages/ServicePage.tsx +62 -5
  276. package/src/pages/Settings.tsx +217 -43
  277. package/src/pages/settings/AboutSettings.tsx +8 -1
  278. package/src/pages/settings/DjangoAggregateChoices.tsx +79 -0
  279. package/src/pages/settings/IntegrationsSettings.tsx +63 -17
  280. package/src/pages/settings/RecordingSettings.tsx +138 -0
  281. package/src/pages/settings/RulesSettings.tsx +825 -0
  282. package/src/routes.test.ts +9 -0
  283. package/src/routes.ts +22 -1
  284. package/src/selection/DetailPanel.tsx +15 -0
  285. package/src/virtual-provenance.d.ts +11 -0
  286. package/vite.config.ts +5 -0
  287. package/scripts/vendor-lock.mjs +0 -58
  288. package/scripts/vendor-lock.test.mjs +0 -69
  289. package/src/lib/data-problems.ts +0 -314
  290. package/src/lib/proto-problems.ts +0 -237
  291. package/src/lib/wire-problems.ts +0 -342
package/plugins/README.md CHANGED
@@ -18,7 +18,7 @@ the plugin itself rather than its work:
18
18
  → { "portolanVersion": "0.1.0", "kind": "describe" }
19
19
  ← { "files": [],
20
20
  "describe": { "name": "extract-go", "summary": "...",
21
- "phases": ["extract"],
21
+ "category": "code", "phases": ["extract"],
22
22
  "options": { "type": "object", "additionalProperties": false,
23
23
  "properties": { "context": { ... } } } } }
24
24
  ```
@@ -31,6 +31,16 @@ is what makes that worth having: `encoding/json` drops a field it does not
31
31
  recognise, so before this a misspelled option was no option at all and nothing
32
32
  said so.
33
33
 
34
+ `category` is what the plugin reads or makes, one of `code`, `contracts`,
35
+ `messaging`, `data`, `infrastructure`, `repository`, `documents`, `evidence`,
36
+ `sources` and `exports` (the constants in `plugin/describe.go`). A phase says
37
+ where in a run a plugin goes; the category says what kind of fact it is after,
38
+ which is how the site's `/plugins` page groups it. `npm run schema` also writes
39
+ `src/lib/plugin-index.json` - every declared plugin as it describes itself,
40
+ with how the manifest runs it - and that page and the landing page's list of
41
+ inputs are rendered from it, so a plugin added to `portolan.json` appears on
42
+ both without being written down twice.
43
+
34
44
  A descriptor may also carry `needs`: what the host must put in the request
35
45
  beyond the tree, because a sandboxed module cannot reach it. The one need so
36
46
  far is `history` (portolan.0007) - when each file under the root was first
@@ -54,7 +64,10 @@ Three obligations, and they are the whole of it:
54
64
  unsafe filename or invalid response fails the run. Non-fatal extraction
55
65
  notes go to stderr; there is no advisory response property a caller may
56
66
  accidentally ignore. A note that opens with `warning: ` is kept beside the
57
- step in `.portolan/build-report.json` and listed on the Settings page.
67
+ step in `.portolan/build-report.json`. The Settings page classifies and
68
+ groups those notes by plugin, stable rule and severity. Typed CEL policies
69
+ in `portolan.json` can suppress reviewed diagnostics only with an explicit
70
+ reason; the host evaluates them and records the decision in the build report.
58
71
 
59
72
  The repository enforces those obligations with schema/field coverage tests,
60
73
  byte-for-byte permutation tests, generated-link and anchor checks, Mermaid
@@ -78,12 +91,14 @@ either be rendered or be explicitly acknowledged by the relevant exporter.
78
91
  `go:embed` and returned in the descriptor. `schematest.Check` in a test keeps
79
92
  it from drifting from the options struct: a field renamed on one side and not
80
93
  the other fails, and so does an option with no description.
81
- 3. Build it. A built-in Go plugin is a library package with
94
+ 3. Build it. A sandboxed built-in Go plugin is a library package with
82
95
  `Serve(io.Reader, io.Writer) error`; add it to the map in
83
96
  `plugins/cmd/portolan-go/main.go`, and `plugins:build` in `package.json`
84
97
  puts it in `plugins/portolan-go.wasm` with the rest (a test keeps the map
85
- and `portolan.json` in step). A plugin of your own is its own module:
86
- `GOOS=wasip1 GOARCH=wasm go build`.
98
+ and `portolan.json` in step). A built-in that genuinely needs the Go
99
+ toolchain gets a small command under `plugins/cmd/` and is declared as a
100
+ `process`; `http-clients` is the example. A plugin of your own is its own
101
+ module: `GOOS=wasip1 GOARCH=wasm go build`.
87
102
  4. Declare it in `portolan.json`, under `plugins` (how to run it) and
88
103
  `generate` (what to run it on), then run `npm run schema` so the manifest
89
104
  schema learns its options.
@@ -149,7 +164,14 @@ a value's name is the constant's literal, because that is what the wire
149
164
  carries, and the constant's own name only for an iota. A `Deprecated:`
150
165
  paragraph in the doc marks the value. In Rust it is a `pub enum` whose every
151
166
  variant is a bare name, the literal an `as_str` answers standing in for the
152
- variant. In Java it is a top-level enum in the aggregate's package. In proto,
167
+ variant. In Java it is a top-level enum in the aggregate's package. In PHP it
168
+ is a backed `enum` anywhere in the module, and on an Eloquent model the
169
+ constants that share a prefix - `STATUS_PENDING = 'pending'` beside
170
+ `STATUS_CLOSED = 'closed'` - with `@deprecated` in a constant's docblock
171
+ marking the value; see `extract-laravel/README.md`. In a PHP tree laid out
172
+ by bounded context (`extract-php-ddd`) it is a backed `enum` under a module's
173
+ `Domain/`. In a .NET modular monolith (`extract-csharp-ddd`) it is a C# `enum`
174
+ in the aggregate's directory, `[Obsolete]` marking a value. In proto,
153
175
  the enums the messages reach through their fields sit on the interface as
154
176
  `enums`, with the numbers the wire uses.
155
177
 
@@ -179,10 +201,23 @@ the original collection layout for TypeScript, Rust, Java, or custom trees and
179
201
  may also point directly at one feature repository.
180
202
 
181
203
  `extract-river` is another independent enrichment for Go repositories. It
182
- joins a job argument's `Kind()` to `Client.Insert`/`InsertTx`, the selected
183
- queue, `Worker[Args].Work`, and `river.AddWorker`. The result is a work-queue
184
- channel plus a two-hop enqueue/dispatch flow, with payload fields and source
185
- lines. It does not need aggregates and does not treat a job as a domain event.
204
+ joins a job argument's `Kind()` to `Client.Insert`/`InsertTx` and the
205
+ `InsertMany*` batches, the selected queue, `Worker[Args].Work`, and
206
+ `river.AddWorker`. The result is a work-queue channel plus a two-hop
207
+ enqueue/dispatch flow, with payload fields and source lines. It does not need
208
+ aggregates and does not treat a job as a domain event. The job type is
209
+ followed through locals, constructors' results and parameters up to the
210
+ callers, so an `Enqueue(ctx, args river.JobArgs, opts)` wrapper is read once
211
+ per concrete job passed to it, and a worker registered as
212
+ `river.AddWorker(workers, NewSendWorker(deps))` is registered. The queue is
213
+ the `InsertOpts.Queue` at the call (a literal, a constant, a config `default:`
214
+ tag, or what the callers pass), else the args type's own `InsertOpts()`, else
215
+ `default`. What no Insert in the tree feeds is not dropped: a registered
216
+ worker's queue is the one its type chooses or the one queue `river.Config`
217
+ lists, and with several and no choice the worker is kept as a flow whose
218
+ receive is unresolved. The diagnostics keep the two cases apart - an Insert
219
+ whose job type or queue the reader could not follow is said at the call, a
220
+ worker with no Insert in the tree is said at the worker.
186
221
 
187
222
  `extract-redis` finds runtime construction of go-redis, rueidis and redigo
188
223
  clients in non-test Go source. That source evidence adds a service-owned Redis
@@ -193,16 +228,27 @@ key patterns with their read/write/delete use, TTL, value type and source. The
193
228
  patterns remain Redis keyspaces rather than being presented as SQL tables.
194
229
 
195
230
  `extract-watermill` reads Watermill `Router.AddHandler` and
196
- `AddNoPublisherHandler` registrations. It resolves literal and constant topics,
197
- plus defaults on env-config structs, follows direct `Publisher.Publish` calls
198
- and one-hop publishing helpers, and traces JSON marshal/unmarshal values back to
199
- their Go payload structs. It carries enclosing `if`/`else` conditions and early
200
- returns through the handler's control flow: proven alternatives become one
201
- catalog `alt`; publications whose relationship cannot be proven remain separate
202
- possible routes. The channels merge normally with AsyncAPI declarations by address. Generic
203
- `NewEventHandler[T]` and `NewCommandHandler[T]` registrations on Watermill CQRS
204
- processors are also extracted; fixed topic generators and the standard
205
- event/command-name generator form are resolved from source.
231
+ `AddNoPublisherHandler` registrations, and `Subscriber.Subscribe` calls made
232
+ without a router. It resolves literal and constant topics - typed constants
233
+ through `string(...)`, constants concatenated with `+`, single-return
234
+ helpers - plus defaults on env-config structs, nested (`cfg.Kafka.Orders`) or
235
+ not; a topic, a name or a handler that a registering function takes as a
236
+ parameter is filled in once per caller, so a `handle(r, name, topic, fn)`
237
+ wrapper yields one handler per call. The handler value may be a function
238
+ literal, a function or method of the tree (`h.OnOrder`, `handlers.OnShipped`)
239
+ or a constructor returning a literal. It follows direct `Publisher.Publish`
240
+ calls and one-hop publishing helpers, records what the service publishes
241
+ outside any handler as a `send` on the topic, and traces JSON marshal/unmarshal
242
+ values back to their Go payload structs. It carries enclosing `if`/`else`
243
+ conditions and early returns through the handler's control flow: proven
244
+ alternatives become one catalog `alt`; publications whose relationship cannot
245
+ be proven remain separate possible routes. A handler whose topic does not
246
+ resolve is kept, with its receive step unresolved and no channel claimed, and
247
+ named in a diagnostic. The channels merge normally with AsyncAPI declarations
248
+ by address. Generic `NewEventHandler[T]` and `NewCommandHandler[T]`
249
+ registrations on Watermill CQRS processors are also extracted; fixed topic
250
+ generators and the standard event/command-name generator form are resolved
251
+ from source.
206
252
 
207
253
  `extract-go-nats` reads nats.go and JetStream calls into the subjects a service
208
254
  listens on and publishes to. A call is known by the type it is made on -
@@ -219,6 +265,61 @@ channel's doc. A subject read off a database row is a warning at the call,
219
265
  not a channel. Consumer configs give the filter subject and the durable name;
220
266
  streams, wildcard subjects and work-queue retention are not read yet.
221
267
 
268
+ `extract-go-sqs` is its twin for Amazon SQS through aws-sdk-go-v2. A call is
269
+ known by being made on `*sqs.Client` - `SendMessage`, `SendMessageBatch`,
270
+ `ReceiveMessage` - and the queue is the `QueueUrl` of the input struct,
271
+ written inline or built into a local first. `aws.String` and `aws.ToString`
272
+ are taken off, and the `QueueUrl` of a `GetQueueUrl` result is read as the
273
+ `QueueName` that was asked for. The queue is then followed like a subject: to
274
+ a literal, a constant, a config default, or a parameter up to two hops through
275
+ the callers, and when it sits in a field of the receiver, to the constructor
276
+ that filled the field and on to that constructor's callers - the worker's
277
+ shape, where the queue comes in at `New` and is read at `Run`. A URL becomes
278
+ the queue's name, its last path segment. Channels are `message` streams, not
279
+ domain events: a queue is point to point, and the event model is not asked to
280
+ match it. A port that takes exactly one other string beside the queue names
281
+ the message; a direct call names none. `DeleteMessage`, visibility changes and
282
+ `CreateQueue` say nothing about direction and are not read; SNS is not read.
283
+
284
+ `extract-terraform` reads the other half: the infrastructure the code runs
285
+ on, from the `*.tf` files of the directory given as `dir` - or the root, or
286
+ the one directory under it that holds any. Nothing is evaluated; the files
287
+ are read as syntax with `hashicorp/hcl`, and a name is followed the way a
288
+ subject is - to a literal, a variable's default, a local, the argument a
289
+ calling `module` block passed in, or a child module's output - and left out
290
+ with a warning when none of those reaches it, never taken from the label.
291
+ Local modules (`source = "./..."`) are read where they are called. A registry
292
+ module the reader knows - `terraform-aws-modules/lambda`, `sqs`, `sns`,
293
+ `s3-bucket` with its `notification` submodule, `dynamodb-table`, whether
294
+ named through the registry or as the git repository behind it with a ref -
295
+ is read as the resource it wraps: its inputs are that resource's attributes
296
+ under the module's names (`environment_variables`, `attributes = [...]`,
297
+ `event_source_mapping = {...}`, `subscriptions = {...}`, `create_dlq`), and
298
+ its outputs (`lambda_function_arn`, `queue_arn` or `sqs_queue_arn`,
299
+ `dead_letter_queue_arn`, `dynamodb_table_stream_arn`) are references to it,
300
+ so `module.queue` reads as `aws_sqs_queue` would; the dead-letter queue the
301
+ queue module makes beside itself is `module.queue.dlq`, and a layer the
302
+ lambda module makes is not a function. Any other registry module is noted
303
+ and skipped. A reference from one resource to another
304
+ (`aws_sqs_queue.orders.arn`) is the edge itself and needs no value. What
305
+ comes out: an `aws_sqs_queue` is a `message` channel and an `aws_sns_topic`
306
+ an `event` channel, each with `messages: []` and the facts in the doc - FIFO,
307
+ the dead-letter queue and its receive count from `redrive_policy`, the SNS
308
+ subscription that fills a queue, the S3 notification that fills either; an
309
+ `aws_lambda_function` is a component of kind `function` in the same context,
310
+ its runtime among its technologies - or the service itself when its name
311
+ slugs to the `service` option; an `aws_lambda_event_source_mapping` puts the
312
+ queue on the function as received, and a topic reaching the function through
313
+ a subscribed queue is listed on it too; a value in the function's
314
+ `environment` that reaches a queue, topic, table or bucket lists that on the
315
+ function with the variable's name, and without a direction, because
316
+ Terraform does not know one. `aws_s3_bucket`, `aws_dynamodb_table` (keys and
317
+ secondary indexes as the table), `aws_db_instance` and `aws_rds_cluster`
318
+ (kind from `engine`) are stores at the root, owned by the `service` option;
319
+ a queue or topic nothing in the module touches is listed on that service as
320
+ well, and says so. Kinesis, EventBridge, API Gateway, Step Functions,
321
+ ElastiCache, MSK and ECS resources are named once as not read yet.
322
+
222
323
  `extract-python-kafka` is the framework-independent Kafka enrichment for
223
324
  Python. It recognizes confluent-kafka, kafka-python and aiokafka by their
224
325
  imported client types, follows literal topics through constants, environment
@@ -260,16 +361,19 @@ literals, direct assignments, or setters. A standalone flow says whether no
260
361
  source caller exists or callers exist but no inbound/asynchronous root was
261
362
  proved, so the UI exposes the missing evidence instead of implying a complete
262
363
  business path.
263
- When a provider branch still ends before its transport, the extractor loads
264
- the module with `go/packages`, builds SSA, and uses `x/tools` VTA to resolve
364
+ When a provider branch still ends before its transport, the extractor runs as
365
+ a workspace-local native sidecar, loads the module with `go/packages`, builds
366
+ SSA, and uses `x/tools` VTA to resolve
265
367
  calls through interface parameters, function values, return values, and
266
368
  interface-typed struct fields. Typed edges are followed only after a concrete
267
369
  factory branch is selected: applying context-insensitive VTA to a shared
268
370
  dispatcher would otherwise attach every request implementation to every
269
371
  endpoint. Factory conditions and HTTP/SOAP meaning continue to come from the
270
- source extractor. Module loading is read-only and bounded; unavailable private
271
- dependencies, type errors, or a timeout produce a warning and retain the
272
- syntax-only result rather than failing generation.
372
+ source extractor. The sidecar is built from the Portolan package through the
373
+ project's Go toolchain and runs with the project as its working directory.
374
+ Module loading is read-only and bounded; unavailable private dependencies,
375
+ type errors, or a timeout produce a warning and retain the syntax-only result
376
+ rather than failing generation.
273
377
  Routes without a provider factory are also joined to their outbound calls,
274
378
  including handlers invoked from closures and methods on locally constructed
275
379
  values. A Swagger `@Router` annotation is medium-confidence root evidence for
@@ -454,9 +558,10 @@ far side.
454
558
  The demo estate's org-wide and context-wide records live in `data/adr`, and are
455
559
  read by a step that points at that directory with a glob of its own. Root
456
560
  `docs/` is where `gen-markdown` writes, so nothing hand-written can live there.
457
- `in` is the directory of records rather than `data` itself: a step's fragment is
458
- only left out of its own stamp when the output is *inside* the input root, and
459
- `in: data` with `out: data` would be stamped from the file it writes.
561
+ `in` is the directory of records rather than `data` itself: what `gen --check`
562
+ lists as changed inputs is the root less the output directories inside it, and
563
+ `in: data` with `out: data` would list the file the step writes among the
564
+ reasons it was rewritten.
460
565
 
461
566
  ```json
462
567
  {
@@ -621,6 +726,178 @@ line with its description, and as entity links of type `command`, each
621
726
  leading to the line of the runner file it was read from, when `sourceBaseUrl`
622
727
  says where the repository is.
623
728
 
729
+ ## Where it answers, what it dials: the manifests, and the cluster
730
+
731
+ A catalog that knows what a service provides and what it consumes still has
732
+ holes where the two do not meet by route alone: two services answer
733
+ `POST /foo`, and the caller's code says only that it posts to `$LEDGER_URL`.
734
+ What that variable holds is not in the code. It is in the Deployment, the
735
+ ConfigMap it reads, and the Service and Ingress objects that give the far end
736
+ a name. Two plugins read those - `extract-k8s` from the manifests in a
737
+ repository, `fetch-k8s` from a live cluster - and put two lists on the
738
+ service:
739
+
740
+ - **Hosts** - the names it answers on: a Kubernetes Service's name in its
741
+ short, namespaced, `svc` and fully qualified forms, and the hosts of the
742
+ Ingress or Gateway API HTTPRoute in front of it, from a rule whose backend
743
+ is that Service.
744
+ - **Dials** - the in-cluster names its workload is configured to reach, cut
745
+ out of its containers' `env` and the ConfigMaps they read.
746
+
747
+ The merge then has both facts in one place. A call that names a host, or a
748
+ caller whose manifests dial one, resolves against the providers of its route
749
+ when exactly one of them answers on that name, and the evidence says
750
+ `kubernetes-host` as the basis. A host only ever decides between providers of
751
+ the route; it never conjures a provider that does not have it.
752
+
753
+ ### What is never written
754
+
755
+ A manifest holds configuration, and configuration is where the passwords are.
756
+ The rule that keeps them out is a rule of shape, not a list of words:
757
+
758
+ - A `Secret`, a `SealedSecret`, an `ExternalSecret`, or any document with
759
+ `stringData` is passed over at its `kind`, before its body is decoded. A
760
+ `secretKeyRef` or `secretRef` is not followed.
761
+ - A value from `env` or a ConfigMap is reduced to the host it names, or to
762
+ nothing. A URL gives up its hostname and keeps nothing else - not the
763
+ scheme, the port, the path, the query, nor the user and password in front
764
+ of the host. A bare `host[:port]` gives up the port. `info`, `true`, a
765
+ token, a DSN written as `key=value` pairs: none of these is a host, and
766
+ none passes.
767
+ - What is left qualifies only when the cluster resolves it: a form the tree's
768
+ own Services answer to, or `<name>.<namespace>.svc[.cluster.local]`, the
769
+ one shape no name outside a cluster has. A bare `cart` from another
770
+ repository's manifests is not taken, because nothing distinguishes it from
771
+ a word.
772
+ - Warnings name files and directories, never values, because the build
773
+ report is committed.
774
+
775
+ The fragment carries the variable's name no more than its value: the
776
+ `environmentVariable` the HTTP client extractor records stays what it was, and
777
+ no value is attached to it.
778
+
779
+ ### The manifests: extract-k8s
780
+
781
+ Every `*.yaml` and `*.yml` under the root, or under the `paths` named in the
782
+ options; anything without `apiVersion` and `kind` - a compose file, a
783
+ workflow - is passed over silently. Multi-document files are split. A file
784
+ holding `{{` is a template and not YAML until rendered, so a directory of
785
+ Helm templates is passed over with one warning for the directory; render the
786
+ chart into the tree, or point `paths` at plain manifests, to have it read.
787
+ Kustomize is read without running it: the base and the overlays are all under
788
+ the root, and two documents of one kind and name - a Deployment and the patch
789
+ laid over it - fold into one, their env and labels unioned. `namespace`
790
+ narrows the read to one namespace; an object naming none is read either way.
791
+
792
+ The workload the step is about is the Deployment, StatefulSet, DaemonSet,
793
+ ReplicaSet, Job or CronJob named like the service, or labelled so with
794
+ `app.kubernetes.io/name` or `app`, or the only one in the tree. A Job or
795
+ CronJob makes the service's kind `job`; a Deployment says nothing about kind,
796
+ because a webapp and a worker both deploy that way. Several workloads and
797
+ none named is a warning, not a guess.
798
+
799
+ ### The cluster: fetch-k8s
800
+
801
+ The manifests say what should run; the cluster says what does, and for an
802
+ estate deployed by Helm or Argo the rendered objects are in no repository at
803
+ all. `fetch-k8s` asks `kubectl` for the workloads, Services, Ingresses,
804
+ Gateway API routes and ConfigMaps of the namespaces named - never for a
805
+ Secret - and answers with one fragment for every workload it saw, placed
806
+ into contexts and services by label. It runs in the host (portolan.0008)
807
+ because it needs the binary, the socket and kubectl's own credential, which
808
+ comes from the kubeconfig and is never in the manifest.
809
+
810
+ ```json
811
+ { "name": "k8s-cluster", "host": "fetch-k8s" }
812
+
813
+ {
814
+ "plugin": "k8s-cluster",
815
+ "in": ".",
816
+ "out": "data",
817
+ "options": {
818
+ "kubeContext": "prod-eu",
819
+ "namespaces": ["shop", "auth", "payments"],
820
+ "labels": { "context": "app.kubernetes.io/part-of", "service": "app.kubernetes.io/name" },
821
+ "namespaceContexts": { "shop-jobs": "shop" },
822
+ "cache": "data",
823
+ "out": "k8s-cluster.json"
824
+ }
825
+ }
826
+ ```
827
+
828
+ The context is the value of the `app.kubernetes.io/part-of` label and the
829
+ service the value of `app.kubernetes.io/name`; an estate that labels
830
+ differently names its own keys under `labels`. A workload without the
831
+ context label falls back to `namespaceContexts`; one with neither is counted
832
+ in a warning for its namespace and left out, not guessed at. A workload
833
+ without the service label is named after itself. Several workloads landing
834
+ on one service - a Deployment and the CronJob beside it - fold into one; a
835
+ service that is only jobs is a `job`, one with a Deployment beside them is
836
+ whatever its code says.
837
+
838
+ The same offline rule as fetch-git: with `PORTOLAN_OFFLINE` set, or in CI,
839
+ the fragment committed at `cache` is used unchanged; when the cluster cannot
840
+ be reached it is used with a warning that names the failed command and its
841
+ exit status, not the server; with no committed copy, that is an error and
842
+ not a short answer. What kubectl itself said on stderr goes to the terminal,
843
+ where the person at the keyboard can read the connection error, and not into
844
+ the build report, which is committed. Nothing but the fragment is written to
845
+ disk: the objects live in the process for the length of one call.
846
+
847
+ portolan.0011 records why both readers keep names and nothing else.
848
+
849
+ ## Where it runs: the GitOps tree, and the deployer
850
+
851
+ A service's manifests say what it is; what deploys them says where it
852
+ stands - which environment, cluster and namespace, from which directory of
853
+ which repository, tracking which revision. Two plugins read that, and the
854
+ merge lays one under the other (portolan.0012, portolan.0013):
855
+
856
+ - **extract-argocd** reads the Argo CD `Application` and `ApplicationSet`
857
+ objects in a GitOps tree, as wasm over the workspace, and answers with
858
+ what *should* run: one deployment per Application, basis `manifest`. An
859
+ ApplicationSet is expanded as far as a tree allows - `list` generators
860
+ whole, `git` directories and files by walking this repository, `matrix`
861
+ over those, the template rendered as Go `text/template` or fasttemplate
862
+ as it declared. A `clusters`, `pullRequest`, `scmProvider`, `merge` or
863
+ `plugin` generator, or a `git` one pointing at another repository, is a
864
+ warning naming the ApplicationSet and no rows. When the source path holds
865
+ a `kustomization.yaml`, the tool is kustomize and the images the overlay
866
+ pins are the declared images.
867
+ - **fetch-argocd** asks the Argo CD API for what *does* run - the revision
868
+ synced, the images running, the link - and commits it as a snapshot
869
+ beside a lock, basis `api` (see the fetchers above).
870
+
871
+ ```json
872
+ { "name": "argocd-gitops", "wasm": { "url": "file://plugins/portolan-go.wasm" } }
873
+
874
+ {
875
+ "plugin": "argocd-gitops",
876
+ "in": "examples/gitops",
877
+ "out": "examples/gitops/portolan",
878
+ "options": {
879
+ "repo": "github.com/shortlink-org/portolan",
880
+ "paths": ["bootstrap", "appsets"],
881
+ "labels": { "context": "app.kubernetes.io/part-of", "service": "app.kubernetes.io/name" },
882
+ "out": "argocd.json"
883
+ }
884
+ }
885
+ ```
886
+
887
+ An Application is placed on a service by the labels it carries -
888
+ `app.kubernetes.io/part-of` and `app.kubernetes.io/name`, the same pair
889
+ fetch-k8s reads off a workload, or whichever the manifest names under
890
+ `labels` - and, without them, by deploying from the service's repository
891
+ inside the service's directory. One that matches neither way is listed on
892
+ the Problems page as unclaimed rather than guessed at.
893
+
894
+ Where both plugins spoke for one Application the merge folds the two into
895
+ one row, basis `both`: the deployer's shape, the tree filling what the
896
+ deployer did not say, and the fields the two disagree on kept as the tree's
897
+ word under `drift` - a `deployment-drift` problem, and a chip on the
898
+ service page that says the difference in words. A row only the tree spoke
899
+ for wears `declared`.
900
+
624
901
  ## Outside the estate: an external with a contract
625
902
 
626
903
  A service calls things nobody here builds - a card network, a tax API, a
@@ -744,13 +1021,60 @@ called" - and `unresolved` is never raised, because a trace does not put the
744
1021
  far end in the catalog. A consumer span inside a trace opens a flow of its own
745
1022
  and is matched the same way, so one password change verifies both the
746
1023
  request's flow and the policy's. A root no flow opens is written down as
747
- `observed-<service>-<route>`, once per shape, with a summary saying how many
1024
+ `observed-<service>-<route>`, once per opening, with a summary saying how many
748
1025
  traces showed it.
749
1026
 
1027
+ Every recording that opened a flow is laid over it. A declared step carries
1028
+ `seen: { traces }`, how many recordings showed it. An rpc or an event a
1029
+ recording showed that the code does not declare is put into the flow after
1030
+ the declared step it followed, as a step with an id the code never gave
1031
+ (`seen1`, `seen2`) and a note saying in how many recordings; a lane such a
1032
+ hop needed is added after the declared ones. A store call is never added -
1033
+ the code's word on which repository method ran is the better one - and a
1034
+ consumer met on the way belongs to the flow it opens.
1035
+
1036
+ Where recordings part, the flow says so. What every recording showed after
1037
+ a step is a run of plain steps; where they went different ways is an `alt`
1038
+ (`seen-alt1`, or `alt1` in an observed flow) with a branch for each way,
1039
+ titled by the hop it starts with and counting the recordings that went it,
1040
+ each branch laid over the same way inside; what they all showed again
1041
+ after parting comes after the frame, once. A recording that went no
1042
+ further where others went on is the branch titled `otherwise`, with no
1043
+ steps and its count, so that a reader sees the hop is not always taken.
1044
+ Two observed recordings that open the same way are one flow built this
1045
+ way, so a happy path and a refusal read as one sequence that parts. The
1046
+ merge takes all of this from a second declaration of a flow because it
1047
+ says where it came from: `seen` on a step the first declaration does not
1048
+ have, or a frame whose every step and branch carries it.
1049
+
1050
+ A recording is also kept as an example of each flow it showed, under
1051
+ `examples` on the flow: the recording and trace it came from, when it ran,
1052
+ how long it took, and for each step the span's name, its length and an
1053
+ allowlist of its attributes - `http.route`, `http.request.method`,
1054
+ `http.response.status_code`, `rpc.service`, `rpc.method`, `db.operation.name`,
1055
+ `db.collection.name`, `messaging.destination.name`, `event.name`,
1056
+ `server.address` and a few more of that kind. A query text, a header, a full
1057
+ URL or a path with an id in it is somebody's data and is never carried; the
1058
+ list is closed in `examples.go`, and a new attribute is added there with the
1059
+ argument for it. `examples` in the options says how many recordings a flow
1060
+ keeps, the ones that show the most of it first: five when unset, zero for
1061
+ none.
1062
+
1063
+ A recording added from the page lands under the project's
1064
+ `telemetry/recordings/` and widens (or adds) the project's `otel` verify step
1065
+ to read that directory, so every run after reads it too; the page runs the
1066
+ generator over a copy of the workspace first and shows which flows the
1067
+ recording showed before anything is written (portolan.0014).
1068
+
750
1069
  `service.name` is matched to the one service whose slug it is, `event.name` to
751
1070
  the one event whose `wire.name` it is, or failing that to the one event of the
752
1071
  publisher's with that last segment; `services` and `events` in the options say
753
- otherwise where an estate's names differ. A publish span whose
1072
+ otherwise where an estate's names differ. A server span's route is matched to
1073
+ the operation whose `http` verb and path template it fits; `routes` in the
1074
+ options names an operation for a route the estate spells another way - a
1075
+ prefix a gateway adds, a path the document writes differently - as
1076
+ `{ "POST /api/v1/sessions": "login" }`, and the name has to be one the
1077
+ service's interface declares. A publish span whose
754
1078
  `messaging.destination.name` is not the event's `wire.channel` is a warning:
755
1079
  the event went out, but not where the code says it does.
756
1080
 
@@ -775,9 +1099,9 @@ enforces: a team that owns a directory is a team that gets the pull request.
775
1099
  ```
776
1100
 
777
1101
  Point `in` at the directory the file is in, not at the repository root: the
778
- host dates a fragment from the last commit to touch the step's input, and the
779
- subject of this one is the `CODEOWNERS` file. Rooted at the repository, it
780
- would be restamped by every commit ever made. Left with no `file`, the three
1102
+ subject of this step is the `CODEOWNERS` file, and `gen --check` explains a
1103
+ changed fragment by what moved under the step's input. Rooted at the
1104
+ repository, every commit ever made would be named. Left with no `file`, the three
781
1105
  places a forge looks are tried in order - `CODEOWNERS`, `.github/CODEOWNERS`,
782
1106
  `docs/CODEOWNERS` - and a `file` that names something absent fails the run,
783
1107
  because answering "nobody owns anything" to a typo is only noticed a month
@@ -822,21 +1146,23 @@ the forge looks.
822
1146
  `wasm` is the default and should stay that way. The module gets no network, no
823
1147
  environment and no way to start a process. A generator gets no filesystem
824
1148
  either. An extract or verify step gets the workspace preopened as `/`
825
- (portolan.0006), which is how the built-in Go extractors read a tree without a
826
- Go toolchain on the machine: every one of them, and the three generators, is
827
- the single module `plugins/portolan-go.wasm`, which answers to the plugin name
828
- the host passes as `argv[0]`. WASI preopens read-write, so an extractor is
1149
+ (portolan.0006), which is how syntax-only built-in Go extractors read a tree
1150
+ without a Go toolchain on the machine: those extractors and the three generators
1151
+ are the single module `plugins/portolan-go.wasm`, which answers to the plugin
1152
+ name the host passes as `argv[0]`. WASI preopens read-write, so an extractor is
829
1153
  trusted not to write the tree it reads, the same trust a process plugin has
830
1154
  today; a `sha256` pins that trust to a build.
831
1155
 
832
- `process` is the escape hatch for a plugin that needs a toolchain: the Rust,
833
- Java, Python and TypeScript extractors run in their own runtimes, and
834
- `fetch-bsr` still talks to its registry from Go. It gets the same protocol
1156
+ `process` is the escape hatch for a plugin that needs a toolchain: the typed Go
1157
+ HTTP client analyzer and the Rust, Java, Python, PHP and TypeScript extractors run in
1158
+ their own runtimes. It gets the same protocol
835
1159
  and none of the sandbox, which is the trade being made and the reason it is
836
1160
  not the default. It declares `command` and an `args` array; the host never
837
- feeds a command string through a shell. A built-in Go plugin that still runs
838
- as a process is the same code reached as
839
- `go run ./plugins/cmd/portolan-go <name>`.
1161
+ feeds a command string through a shell. For a built-in declared as `go run`,
1162
+ the adapter compiles a workspace-local native sidecar from the package and
1163
+ then runs it with the scanned workspace as its current directory. This keeps
1164
+ module resolution in Portolan's shipped source and project loading in the
1165
+ project being analyzed.
840
1166
 
841
1167
  `host` is for Portolan's own code that needs what only the host has - a git
842
1168
  binary, a socket - and so runs inside the host process (portolan.0008):
@@ -859,12 +1185,18 @@ written.
859
1185
  `fetch-git` is `fetch-bsr` for a repository rather than a registry, and it
860
1186
  lives by the same four rules. A pin is a repository, a commit and the paths
861
1187
  actually read; the step fetches exactly those directories at exactly that
862
- commit and hands them back as files, so the host writes them into the tree
1188
+ commit and hands their text files back, so the host writes them into the tree
863
1189
  beside a `git.lock.json` naming the commit and the digest of every file. The
864
1190
  paths inside the copy are the repository's own, which is the point: the
865
1191
  extract step that follows points its `in` at the vendored service and reads
866
1192
  it exactly as it would read that service's checkout.
867
1193
 
1194
+ Binary files are not part of the extractor input. Known binary extensions are
1195
+ discarded before their blobs are read; files without a known extension are
1196
+ sniffed for NUL bytes and invalid UTF-8 before the response is assembled. The
1197
+ lock records only their path, byte size and skip reason, never Base64 content,
1198
+ so online fetch and offline replay produce the same bounded text-only copy.
1199
+
868
1200
  It runs inside the host (`scripts/host-plugins/fetch-git.mjs`,
869
1201
  portolan.0008) rather than as a module, because it needs a git binary and a
870
1202
  socket, and a manifest names it with `host` rather than `wasm` or `process`.
@@ -919,11 +1251,12 @@ Nothing else can say it. A service says which repository it lives in, and an
919
1251
  extractor reads a directory as a pure function of what is on disk; neither has
920
1252
  any idea which commit somebody fetched. Without that line, every source path
921
1253
  of every vendored service is dead text on the page - the file and the line are
922
- known, and there is nowhere to send a reader - and every fragment read out of
923
- the copy is stamped with the commit that VENDORED it, so the service looks
924
- fresh whenever the fetch is re-run and unchanged when its own repository
925
- moves. With it, `sourceHref` links the line at the commit it was read at and
926
- `stampFor` dates the fragment from the code rather than the vendoring.
1254
+ known, and there is nowhere to send a reader. With it, `sourceHref` links the
1255
+ line at the commit it was read at. The fragments read out of the copy are
1256
+ dated, like every other source, by the commit of THIS repository that last
1257
+ changed them (portolan.0010) - which is the fetch that brought the change in,
1258
+ and is what "how fresh is this" means to the estate that vendored it; the pin
1259
+ is what says which upstream commit that was.
927
1260
 
928
1261
  One more line is needed for the app itself: `SOURCE_GLOBS` in `src/data.ts`,
929
1262
  where the same patterns are written out a second time because
@@ -1020,7 +1353,8 @@ workflow already does.
1020
1353
 
1021
1354
  Four rules govern what happens when a fetch does not:
1022
1355
 
1023
- 1. Fetch succeeded → the fetched files and a regenerated lock.
1356
+ 1. Fetch succeeded → the fetched text files, binary-skip metadata and a
1357
+ regenerated lock.
1024
1358
  2. Skipped or failed, cache complete and matching its digests → the cached files
1025
1359
  byte-identically, plus a warning. Output unchanged, so `--check` stays clean.
1026
1360
  3. Failed **and** no usable cache → a non-zero exit, never a short file list. The
@@ -1202,3 +1536,23 @@ it, the call before the decorator before `task_routes` before the default,
1202
1536
  and `transaction.on_commit(...)` around an enqueue is a note on the step,
1203
1537
  which is the one fact about *when* a message leaves that the code states
1204
1538
  plainly.
1539
+
1540
+ ### HTTP destination provenance
1541
+
1542
+ `extract-http-clients` emits structured `destination` evidence on HTTP consumes
1543
+ and flow steps: the request call site and endpoint expression, local path,
1544
+ base URL expression, config field and environment variable, source locations,
1545
+ and a full path when source proves the join. Functional options that assign a
1546
+ URL field to a returned client are followed through adapter constructors.
1547
+ Bindings belong to the adapter's client field; conflicting construction sites
1548
+ remain unresolved instead of sharing another client's settings.
1549
+
1550
+ A Go `default` struct tag is recorded as `config-default`, not as the effective
1551
+ runtime configuration. For example, `WithBaseURL(cfg.SettingAddr)` with
1552
+ `default:"http://localhost:8000/settings"` and a concatenated
1553
+ `/get-admin-settings` yields `/settings/get-admin-settings`. The merge matches
1554
+ that full path exactly and never falls back to a suffix if it is absent.
1555
+ Legacy suffix matches remain explicitly labeled `unique-suffix` in the evidence
1556
+ shown in RPC step details. Later runtime URL modifiers are listed separately;
1557
+ the recovered full path describes the static join. Unreadable options may
1558
+ leave only the local route available.
@@ -0,0 +1,19 @@
1
+ // Command portolan-http-clients is the native sidecar for typed Go call-graph
2
+ // analysis. It intentionally does not compile for WASI: go/packages needs the
3
+ // project Go toolchain to load build tags and dependencies before SSA/VTA can
4
+ // resolve interface calls.
5
+ package main
6
+
7
+ import (
8
+ "fmt"
9
+ "os"
10
+
11
+ extracthttpclients "github.com/shortlink-org/portolan/plugins/extract-http-clients"
12
+ )
13
+
14
+ func main() {
15
+ if err := extracthttpclients.Serve(os.Stdin, os.Stdout); err != nil {
16
+ fmt.Fprintln(os.Stderr, "portolan-http-clients:", err)
17
+ os.Exit(1)
18
+ }
19
+ }