@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
@@ -0,0 +1,22 @@
1
+ [package]
2
+ name = "portolan-extract-php-ddd"
3
+ version = "0.1.0"
4
+ edition = "2024"
5
+ description = "A PHP service laid out by bounded context, module and hexagonal layer in, a catalog fragment out: the PHP twin of extract-go and extract-rust, on the same protocol."
6
+ publish = false
7
+
8
+ [lib]
9
+ name = "portolan_extract_php_ddd"
10
+ path = "src/lib.rs"
11
+
12
+ [[bin]]
13
+ name = "portolan-extract-php-ddd"
14
+ path = "src/main.rs"
15
+
16
+ [dependencies]
17
+ # The PHP tree as syntax, shared with extract-laravel.
18
+ phpscan = { path = "../phpscan" }
19
+ # Doctrine mappings are XML; roxmltree reads them without a build step.
20
+ roxmltree = "0.21"
21
+ serde = { version = "1", features = ["derive"] }
22
+ serde_json = { version = "1", features = ["preserve_order"] }
@@ -0,0 +1,141 @@
1
+ # extract-php-ddd
2
+
3
+ A PHP tree laid out by bounded context, module and hexagonal layer in, a
4
+ catalog fragment out - three, when the route files prove an HTTP contract
5
+ and the Doctrine mappings a database. The PHP twin of `extract-go`,
6
+ `extract-ts` and `extract-rust`: like them, this reads a layout rather than
7
+ a framework, and the layout is the claim. Nothing is annotated for the
8
+ catalog; the directory a class sits in and the base it extends say what it
9
+ is.
10
+
11
+ The layout is the one [CodelyTV's php-ddd-example](https://github.com/CodelyTV/php-ddd-example)
12
+ made common in PHP - `src/<Context>/<Module>/{Domain,Application,Infrastructure}`
13
+ with the deployables under `apps/<context>/<app>` - and it is that project the
14
+ site ships as a profile, read by this extractor alone.
15
+
16
+ Written in Rust and run as a process plugin, `cargo run --quiet
17
+ --manifest-path plugins/extract-php-ddd/Cargo.toml`, on the same reader as
18
+ `extract-laravel`: `plugins/phpscan`, over [Mago](https://github.com/carthage-software/mago)'s
19
+ `mago-syntax`. **The application is never run.** No container, no `composer
20
+ install`: everything is read from syntax, names are resolved by namespace and
21
+ `use` line, and a file that does not parse is read as far as it parsed and
22
+ reported.
23
+
24
+ ## The layout it reads
25
+
26
+ ```
27
+ src/<Context>/<Module>/Domain the model: root, entities, value objects, events, ports
28
+ src/<Context>/<Module>/Application the use cases: commands, queries, their handlers, subscribers
29
+ src/<Context>/<Module>/Infrastructure the adapters: repositories, *.orm.xml Doctrine mappings
30
+ src/Shared, src/<Context>/Shared the shared kernel: bases, ids; not a context, not a module
31
+ apps/<context>/<app>/config/routes/*.yaml the HTTP edge of one deployable
32
+ apps/<context>/<app>/config/services*.yaml which contexts' code the deployable loads
33
+ apps/<context>/<app>/src/Controller what answers a route
34
+ ```
35
+
36
+ `source` and `apps` in the options rename `src/` and `apps/`. `tests/`,
37
+ `vendor/` and everything under an application but `src/` are not read.
38
+
39
+ ## What becomes what
40
+
41
+ **Context.** A directory under `src/` other than `Shared`, with its slug as
42
+ id. `classification` in the options applies to every context the tree
43
+ declares; nothing in the tree says which is core.
44
+
45
+ **Service.** Each application under `apps/<context>/` is a service of that
46
+ context - `mooc.backend`, `backoffice.frontend` - with the route files it
47
+ carries as its HTTP interfaces, one per file, `mooc.backend.courses`. The
48
+ context's model is filed under its `backend` application, or its first one
49
+ when it has none of that name. A context with no application at all gets a
50
+ service named after itself, `analytics.analytics`, and a warning: the code
51
+ is there, nothing deploys it.
52
+
53
+ **Aggregate.** A module whose `Domain/` has a class extending
54
+ `AggregateRoot`. The root's constructor parameters are its fields, typed as
55
+ written, `id: CourseId`; a class extending the root is an entity of it, the
56
+ joined subclasses of an abstract `Step`. A value object is a class that wraps
57
+ one value - it extends a `*ValueObject` base or `Uuid`, or answers `value()` -
58
+ with its fields read up the base chain, so `CourseName extends
59
+ StringValueObject` has the parent's `value: string`. Exceptions, collections
60
+ and domain services are not shape and are skipped. A module with no root but
61
+ with use cases - `Auth`, with a command and no aggregate - is a `model-group`
62
+ aggregate; one with neither is a warning and nothing else.
63
+
64
+ **Event.** A class extending `DomainEvent`. Its wire name is what
65
+ `eventName()` answers, `course.created`; its fields are the keys of
66
+ `toPrimitives()`, typed by the constructor parameter of the same name; its
67
+ doc the class docblock. The id is the class name with `DomainEvent` taken
68
+ off, `mooc.backend.courses.CourseCreated`. Every event's `wire.channel` is the
69
+ RabbitMQ exchange, `exchange` in the options, because the code reads it from
70
+ the environment.
71
+
72
+ **Operation.** A class implementing `CommandHandler` or `QueryHandler`; the
73
+ `__invoke` parameter names the message and the operation is the message
74
+ without its suffix, `create-course` for `CreateCourseCommand`. The doc is the
75
+ handler's, or the use case's it holds (`CourseCreator`), or the message's.
76
+ Its `fields` are the message's constructor parameters, typed as written -
77
+ what the caller hands in, and an empty list for a query that takes nothing;
78
+ its `source` is the handler's `__invoke`. `exposedBy` names the routes of
79
+ the operation's own service that dispatch or ask it; a route in another
80
+ application is a call across contexts and lives in the flow instead. A
81
+ class named `*CommandHandler` that implements neither interface is reported:
82
+ the bus never reaches it.
83
+
84
+ **Subscriber.** A class implementing `DomainEventSubscriber`; `subscribedTo()`
85
+ says which events, and `DomainEvent::class` there means every one. Each
86
+ subscriber is a consumer on the events it names, a flow from the bus in, and
87
+ a channel of its own: the RabbitMQ queue `RabbitMqQueueNameFormatter` would
88
+ name for it, `codelytv.mooc.courses_counter.increment_courses_counter_on_course_created`.
89
+ A subscriber in a context no application's `services.yaml` loads is reported:
90
+ declared, wired nowhere.
91
+
92
+ **Store.** The tables the `*.orm.xml` mappings under a context declare, as
93
+ one `mysql` store per context (`storeKind` in the options), owned by the
94
+ context's service: `<id>`, `<field>` and `<embedded>` become columns, the
95
+ embeddable's own mapping supplying the column names; a `JOINED` subclass gets
96
+ its own table keyed to the parent's. Each table `persists` the aggregate its
97
+ entity is the root or an entity of, and its columns `map` to the entity's
98
+ fields. A `*Repository` interface under `Domain/` is a port; the adapter
99
+ implementing it says what answers it - `Doctrine*`/`MySql*` the database,
100
+ `Elasticsearch*` an index, kept as a second store of kind `other` with one
101
+ table per index shaped like the root it keeps; `InMemory*` and `File*`
102
+ nothing, and a warning. Every `$this->repository->save(...)` on a port is an
103
+ access on the port's table, `write CourseRepository.save`, whether or not a
104
+ flow reaches it.
105
+
106
+ **Flow.** One per route whose controller the tree has: the request in, what
107
+ the controller `dispatch`es or `ask`s, then what the handler does, followed
108
+ through the classes it holds by their constructor types - `$this->creator->__invoke(...)`,
109
+ `Course::create(...)`, `apply($this->incrementer, [...])`, a call on a local
110
+ variable tried against the module's root. On the way, `record(new Event)`
111
+ and `publish(new Event)` are event steps to the bus and a call on a port a
112
+ step to its store. When a controller dispatches a command whose handler lives
113
+ in another context - the backoffice front end creating a Mooc course - the
114
+ flow crosses to that context's service with a `call` step and a note: in
115
+ process, over an in-memory bus, but a boundary all the same. One flow per
116
+ subscriber does the same from the queue in.
117
+
118
+ **HTTP.** One OpenAPI 3.1 document per application with routes,
119
+ `openapi.<context>-<app>.yaml` (`openapiOut` in the options, `{service}`
120
+ where the name goes), one operation per route and verb, the path's `{id}`
121
+ as parameters and the controller's docblock as the summary; a route to a
122
+ controller not in the tree is kept and reported.
123
+
124
+ ## Options
125
+
126
+ See `options.schema.json`. `repo` defaults to composer.json's
127
+ `support.source` or `homepage`; `exchange` to `domain_events`; `storeKind`
128
+ to `mysql`; `classification` to nothing.
129
+
130
+ ## Tests
131
+
132
+ ```bash
133
+ cargo test --manifest-path plugins/extract-php-ddd/Cargo.toml
134
+ ```
135
+
136
+ `testdata/mooc` is a small tree in the layout: three contexts with
137
+ applications and two without, a joined-subclass aggregate, an Elasticsearch
138
+ read model, a subscriber to every event, and a front end dispatching another
139
+ context's command. `expected.json`, `expected-stores.json` and the
140
+ `openapi.*.yaml` beside it are what the extractor writes for it; the fixture
141
+ tests explain the rules, the goldens catch what nobody asserted.
@@ -0,0 +1,50 @@
1
+ {
2
+ "type": "object",
3
+ "additionalProperties": false,
4
+ "properties": {
5
+ "source": {
6
+ "type": "string",
7
+ "description": "The directory the bounded contexts are in, relative to the input root: one directory per context, one per module under it, `Domain`, `Application` and `Infrastructure` under that. A `Shared` directory at either level is the shared kernel and is not a context or a module.",
8
+ "default": "src"
9
+ },
10
+ "apps": {
11
+ "type": "string",
12
+ "description": "The directory the deployable applications are in, relative to the input root: `<apps>/<context>/<app>`, each with `config/routes/*.yaml` and `src/Controller/`. Each application is a service of its context; a context with none gets one service named after itself.",
13
+ "default": "apps"
14
+ },
15
+ "repo": {
16
+ "type": "string",
17
+ "description": "Where the source lives. Defaults to composer.json's support.source or homepage when either names a repository.",
18
+ "examples": ["github.com/CodelyTV/php-ddd-example"]
19
+ },
20
+ "exchange": {
21
+ "type": "string",
22
+ "description": "The RabbitMQ exchange domain events are published to. The code reads it from the environment, so the manifest says.",
23
+ "default": "domain_events"
24
+ },
25
+ "storeKind": {
26
+ "enum": ["postgres", "mysql", "sqlite", "other"],
27
+ "description": "What the Doctrine-mapped tables live in.",
28
+ "default": "mysql"
29
+ },
30
+ "classification": {
31
+ "enum": ["core", "supporting", "generic"],
32
+ "description": "Whether the contexts are core to the business, support it, or are generic and could be bought. One value for every context the tree declares."
33
+ },
34
+ "out": {
35
+ "type": "string",
36
+ "description": "Name of the fragment file, inside the step's output directory.",
37
+ "default": "domain.json"
38
+ },
39
+ "openapiOut": {
40
+ "type": "string",
41
+ "description": "Name of the inferred OpenAPI 3.1 document, inside the step's output directory. Written when the route files expose HTTP operations.",
42
+ "default": "openapi.inferred.yaml"
43
+ },
44
+ "storesOut": {
45
+ "type": "string",
46
+ "description": "Name of the store fragment, inside the step's output directory. Written when a Doctrine mapping or a repository adapter names a store.",
47
+ "default": "stores.json"
48
+ }
49
+ }
50
+ }
@@ -0,0 +1 @@
1
+ max_width = 160
@@ -62,8 +62,6 @@ def extract(input_: Input, opts: Options, b: Builder, cwd: str = "") -> None:
62
62
  flows.sort(key=lambda flow: flow["slug"])
63
63
 
64
64
  fragment: Dict[str, Any] = {
65
- "generatedAt": input_.generated_at,
66
- "commit": input_.commit,
67
65
  "contexts": [
68
66
  {
69
67
  "id": context,
@@ -23,7 +23,7 @@ class PythonKafka(unittest.TestCase):
23
23
  fixture = os.path.join(HERE, "testdata", "service")
24
24
  builder = Builder()
25
25
  extract(
26
- Input(root=os.path.relpath(fixture, ROOT), commit="abc1234", generated_at="2026-09-08T00:00:00Z"),
26
+ Input(root=os.path.relpath(fixture, ROOT)),
27
27
  Options.of({"context": "shop", "service": "orders", "settings": "config.settings"}),
28
28
  builder,
29
29
  cwd=ROOT,
@@ -18,6 +18,7 @@ HERE = os.path.dirname(os.path.abspath(__file__))
18
18
  DESCRIPTOR = {
19
19
  "name": "extract-python-kafka",
20
20
  "summary": "Reads confluent-kafka, kafka-python and aiokafka producers and consumers into message channels and source-backed flows.",
21
+ "category": "messaging",
21
22
  "phases": ["extract"],
22
23
  }
23
24
 
@@ -0,0 +1,26 @@
1
+ {
2
+ "type": "object",
3
+ "additionalProperties": false,
4
+ "properties": {
5
+ "context": {
6
+ "type": "string",
7
+ "description": "Slug of the bounded context the module deploys into. Defaults to the name of the input directory.",
8
+ "examples": ["shop"]
9
+ },
10
+ "service": {
11
+ "type": "string",
12
+ "description": "Slug of the service the module sits beside. Queues, topics and stores nothing in the module claims are listed on it, and it owns every store. A Lambda function whose name slugs to this becomes the service itself; any other becomes a component of kind function in the same context. Defaults to the name of the input directory.",
13
+ "examples": ["fulfillment"]
14
+ },
15
+ "dir": {
16
+ "type": "string",
17
+ "description": "Directory of *.tf files to read, relative to the input root. Left out, the root is read when it holds *.tf files, and otherwise the one directory under it that does - when there are several, the first by name, and the rest are named in a warning. Local modules (source = ./...) are followed from wherever they are called; registry modules are not read.",
18
+ "examples": ["deploy/terraform"]
19
+ },
20
+ "out": {
21
+ "type": "string",
22
+ "description": "Name of the fragment file, inside the step's output directory.",
23
+ "default": "terraform.json"
24
+ }
25
+ }
26
+ }
@@ -8,7 +8,7 @@ const ROOT = "plugins/extract-ts/testdata/cart";
8
8
  const options = { context: "shop", service: "cart", store: "pg", peers: { "auth.v1": "auth.auth", "shop.v1": "shop.pricing" } };
9
9
 
10
10
  function run() {
11
- return extract({ root: ROOT, commit: "abc1234", generatedAt: "2026-09-04T00:00:00Z" }, options);
11
+ return extract({ root: ROOT }, options);
12
12
  }
13
13
 
14
14
  function fragment() {
@@ -200,7 +200,7 @@ describe("the JavaScript twin", () => {
200
200
  );
201
201
 
202
202
  it("reads to the fragment the TypeScript tree reads to", () => {
203
- const twin = extract({ root: JS, commit: "abc1234", generatedAt: "2026-09-04T00:00:00Z" }, options);
203
+ const twin = extract({ root: JS }, options);
204
204
  const golden = JSON.parse(readFileSync(`${ROOT}/expected.json`, "utf8"));
205
205
  expect(sameShape(JSON.parse(twin.files[0]!.contents))).toEqual(sameShape(golden));
206
206
  expect(twin.warnings.map((d) => d.ref)).toEqual(["getBasket"]);
@@ -5,6 +5,9 @@
5
5
  import { existsSync, readFileSync, realpathSync } from "node:fs";
6
6
  import { basename, join, relative, resolve } from "node:path";
7
7
  import type { Catalog, Service } from "../../src/catalog.ts";
8
+
9
+ /** What an extractor writes: a catalog less the stamps, which are the history's to give (portolan.0010). */
10
+ type Fragment = Omit<Catalog, "generatedAt" | "commit">;
8
11
  import { readAggregates, type WarningSink } from "./domain.ts";
9
12
  import { operationOf, readUseCases } from "./operations.ts";
10
13
  import { readBindings } from "./wiring.ts";
@@ -39,8 +42,6 @@ export interface Options {
39
42
  export interface Input {
40
43
  root: string;
41
44
  output?: string;
42
- commit: string;
43
- generatedAt: string;
44
45
  }
45
46
 
46
47
  export interface Warning {
@@ -151,9 +152,7 @@ export function extract(input: Input, opts: Options, cwd = process.cwd()): Respo
151
152
  for (const e of source.errors) b.warn(rel(source.path), `${rel(e.at)}: ${e.message}; the file is read only up to here`);
152
153
  }
153
154
 
154
- const fragment: Catalog = {
155
- generatedAt: input.generatedAt,
156
- commit: input.commit,
155
+ const fragment: Fragment = {
157
156
  contexts: [
158
157
  {
159
158
  id: context,
@@ -13,7 +13,7 @@ const ROOT = "plugins/extract-ts/testdata/storefront";
13
13
  const options = { context: "storefront", service: "bff", graphql: "src/schema", peers: { "auth.v1": "auth.auth" } };
14
14
 
15
15
  function run() {
16
- return extract({ root: ROOT, commit: "abc1234", generatedAt: "2026-09-05T00:00:00Z" }, options);
16
+ return extract({ root: ROOT }, options);
17
17
  }
18
18
 
19
19
  function fragment() {
@@ -11,6 +11,7 @@ const schema = JSON.parse(readFileSync(new URL("./options.schema.json", import.m
11
11
  export const descriptor = {
12
12
  name: "extract-ts",
13
13
  summary: "Reads a TypeScript service by its layout - aggregates, events, use cases, endpoints, policies, clients - into a catalog fragment.",
14
+ category: "code",
14
15
  phases: ["extract"],
15
16
  options: schema,
16
17
  };
@@ -0,0 +1,261 @@
1
+ // Package openapi is what two extractors agree on about an OpenAPI document:
2
+ // how its interfaces are named in the catalog, and which operation answers on
3
+ // which route. The server side reads the document to say what a service
4
+ // provides; the client side reads the copy vendored beside a generated client
5
+ // to say what a service calls. Both have to spell `auth.v1.Sessions/login` the
6
+ // same way, or the call would never resolve to the method, and one place is
7
+ // how they do.
8
+ package openapi
9
+
10
+ import (
11
+ "fmt"
12
+ "os"
13
+ "strings"
14
+
15
+ "gopkg.in/yaml.v3"
16
+ )
17
+
18
+ // APIID is the document's title and major version: `auth` 1.0.0 gives
19
+ // `auth.v1`. It prefixes every interface the document declares.
20
+ func APIID(title, version string) string {
21
+ name := title
22
+ if name == "" {
23
+ name = "api"
24
+ }
25
+ name = strings.ReplaceAll(strings.ToLower(name), " ", "-")
26
+
27
+ if major, _, ok := strings.Cut(version, "."); ok && major != "" {
28
+ return name + ".v" + major
29
+ }
30
+
31
+ return name
32
+ }
33
+
34
+ // DocumentAPIID is the id a document says it goes by in the estate, or the one
35
+ // built from its title and version when it says nothing.
36
+ //
37
+ // `x-portolan-api` in `info` is for a copy vendored from outside the estate:
38
+ // Stripe's document is titled "Stripe API" and versioned "2026-08-26.dahlia",
39
+ // and `stripe-api.v2026-08-26` on every arrow would be the document's words
40
+ // where the estate wants its own. The copy is the consumer's translation
41
+ // boundary already, so it is the one place the estate's name may be written,
42
+ // and both sides of a call read it from there rather than from two manifests.
43
+ func DocumentAPIID(declared, title, version string) string {
44
+ if declared = strings.TrimSpace(declared); declared != "" {
45
+ return declared
46
+ }
47
+
48
+ return APIID(title, version)
49
+ }
50
+
51
+ // ExternalID is the id a system outside the estate goes by when nothing but
52
+ // its own document names it: the document's title as a slug, with a trailing
53
+ // "API" dropped because the system is the thing, not its interface. "Gordian
54
+ // Flights & Ancillaries API" is gordian-flights-ancillaries; "Stripe API" is
55
+ // stripe. Both sides of a call - the extractor that reads the vendored copy
56
+ // for what the system answers on, and the one that reads the client beside it
57
+ // for what this service calls - derive it here, so neither needs the manifest
58
+ // to say it.
59
+ func ExternalID(title string) string {
60
+ var b strings.Builder
61
+ dash := false
62
+ for _, r := range strings.ToLower(strings.TrimSpace(title)) {
63
+ switch {
64
+ case r >= 'a' && r <= 'z', r >= '0' && r <= '9':
65
+ b.WriteRune(r)
66
+ dash = false
67
+ case !dash && b.Len() > 0:
68
+ b.WriteByte('-')
69
+ dash = true
70
+ }
71
+ }
72
+ id := strings.TrimRight(b.String(), "-")
73
+ if trimmed := strings.TrimSuffix(id, "-api"); trimmed != "" {
74
+ id = trimmed
75
+ }
76
+
77
+ return id
78
+ }
79
+
80
+ // Title is the human form of a tag: users becomes Users, price_list becomes
81
+ // PriceList, because it sits in an id beside a proto-shaped service name.
82
+ func Title(name string) string {
83
+ var b strings.Builder
84
+ for _, word := range strings.FieldsFunc(name, func(r rune) bool { return r == '_' || r == '-' || r == ' ' }) {
85
+ runes := []rune(word)
86
+ if runes[0] >= 'a' && runes[0] <= 'z' {
87
+ runes[0] = runes[0] - 'a' + 'A'
88
+ }
89
+ b.WriteString(string(runes))
90
+ }
91
+
92
+ return b.String()
93
+ }
94
+
95
+ // InterfaceID is the contract declared by the OpenAPI document. Tags organise
96
+ // operations inside that contract; they do not create separate interfaces.
97
+ func InterfaceID(api, _ string) string { return api }
98
+
99
+ // Verbs, in the order the paths object lists them by convention.
100
+ var Verbs = []string{"get", "put", "post", "delete", "options", "head", "patch", "trace"}
101
+
102
+ // Operation is one route of the document and the name it goes by.
103
+ type Operation struct {
104
+ // ID is the operationId, or `VERB /path` when the document has none.
105
+ ID string
106
+ // Tag is the first tag, retained as document metadata. It does not change
107
+ // the identity of the contract.
108
+ Tag string
109
+ // Verb is upper case: POST.
110
+ Verb string
111
+ // Path is the template as written: /v1/users/{userId}.
112
+ Path string
113
+ }
114
+
115
+ // Interface is the id of the interface the operation belongs to.
116
+ func (o Operation) Interface(api string) string { return InterfaceID(api, o.Tag) }
117
+
118
+ // CallID is what a call to this operation is known by in the catalog.
119
+ func (o Operation) CallID(api string) string { return o.Interface(api) + "/" + o.ID }
120
+
121
+ // Spec is the part of a document these extractors read.
122
+ type Spec struct {
123
+ API string
124
+ Operations []Operation
125
+
126
+ // Title, Description and DocsURL are what the document says about the
127
+ // system it describes, for naming that system when the manifest does not.
128
+ Title string
129
+ Description string
130
+ DocsURL string
131
+ }
132
+
133
+ // Read loads a document and lists its operations. The api id may be
134
+ // overridden by whoever calls, since the manifest may name one.
135
+ func Read(path string) (*Spec, error) {
136
+ contents, err := os.ReadFile(path)
137
+ if err != nil {
138
+ return nil, err
139
+ }
140
+ var root yaml.Node
141
+ if err := yaml.Unmarshal(contents, &root); err != nil {
142
+ return nil, fmt.Errorf("%s: %w", path, err)
143
+ }
144
+ node := &root
145
+ if node.Kind == yaml.DocumentNode && len(node.Content) > 0 {
146
+ node = node.Content[0]
147
+ }
148
+
149
+ info := child(node, "info")
150
+ spec := &Spec{
151
+ API: DocumentAPIID(text(child(info, "x-portolan-api")), text(child(info, "title")), text(child(info, "version"))),
152
+ Title: text(child(info, "title")),
153
+ Description: text(child(info, "description")),
154
+ DocsURL: text(child(node, "externalDocs", "url")),
155
+ }
156
+ for _, p := range entries(child(node, "paths")) {
157
+ for _, verb := range Verbs {
158
+ operation := child(p.value, verb)
159
+ if operation == nil {
160
+ continue
161
+ }
162
+ id := text(child(operation, "operationId"))
163
+ if id == "" {
164
+ id = strings.ToUpper(verb) + " " + p.key
165
+ }
166
+ tags := list(child(operation, "tags"))
167
+ tag := ""
168
+ if len(tags) > 0 {
169
+ tag = tags[0]
170
+ }
171
+ spec.Operations = append(spec.Operations, Operation{ID: id, Tag: tag, Verb: strings.ToUpper(verb), Path: p.key})
172
+ }
173
+ }
174
+
175
+ return spec, nil
176
+ }
177
+
178
+ // Find looks up an operation by verb and route. Both sides may spell a path
179
+ // parameter differently - `{userId}` in the document, `%s` in a generated
180
+ // client - so parameters are compared by position, not by name.
181
+ func (s *Spec) Find(verb, path string) (Operation, bool) {
182
+ want := strings.ToUpper(verb) + " " + shape(path)
183
+ for _, op := range s.Operations {
184
+ if op.Verb+" "+shape(op.Path) == want {
185
+ return op, true
186
+ }
187
+ }
188
+
189
+ return Operation{}, false
190
+ }
191
+
192
+ // shape replaces every parameter, however spelled, with one marker.
193
+ func shape(path string) string {
194
+ var b strings.Builder
195
+ for _, segment := range strings.Split(strings.TrimSuffix(path, "/"), "/") {
196
+ if strings.HasPrefix(segment, "{") || segment == "%s" || segment == "%v" || segment == "%d" {
197
+ b.WriteString("/*")
198
+
199
+ continue
200
+ }
201
+ b.WriteString("/" + segment)
202
+ }
203
+
204
+ return b.String()
205
+ }
206
+
207
+ type entry struct {
208
+ key string
209
+ value *yaml.Node
210
+ }
211
+
212
+ func entries(node *yaml.Node) []entry {
213
+ if node == nil || node.Kind != yaml.MappingNode {
214
+ return nil
215
+ }
216
+ var out []entry
217
+ for i := 0; i+1 < len(node.Content); i += 2 {
218
+ out = append(out, entry{key: node.Content[i].Value, value: node.Content[i+1]})
219
+ }
220
+
221
+ return out
222
+ }
223
+
224
+ func child(node *yaml.Node, keys ...string) *yaml.Node {
225
+ for _, key := range keys {
226
+ if node == nil || node.Kind != yaml.MappingNode {
227
+ return nil
228
+ }
229
+ var next *yaml.Node
230
+ for i := 0; i+1 < len(node.Content); i += 2 {
231
+ if node.Content[i].Value == key {
232
+ next = node.Content[i+1]
233
+
234
+ break
235
+ }
236
+ }
237
+ node = next
238
+ }
239
+
240
+ return node
241
+ }
242
+
243
+ func text(node *yaml.Node) string {
244
+ if node == nil || node.Kind != yaml.ScalarNode {
245
+ return ""
246
+ }
247
+
248
+ return strings.TrimSpace(node.Value)
249
+ }
250
+
251
+ func list(node *yaml.Node) []string {
252
+ if node == nil || node.Kind != yaml.SequenceNode {
253
+ return nil
254
+ }
255
+ var out []string
256
+ for _, item := range node.Content {
257
+ out = append(out, text(item))
258
+ }
259
+
260
+ return out
261
+ }