@shortlink-org/portolan 0.1.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 (826) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +387 -0
  3. package/catalog/enum_test.go +46 -0
  4. package/catalog/model.go +932 -0
  5. package/catalog/roundtrip_test.go +185 -0
  6. package/catalog/via_test.go +38 -0
  7. package/cli/portolan.mjs +368 -0
  8. package/go.mod +14 -0
  9. package/go.sum +14 -0
  10. package/index.html +13 -0
  11. package/internal/commands/cargo.go +154 -0
  12. package/internal/commands/commands.go +143 -0
  13. package/internal/commands/commands_test.go +323 -0
  14. package/internal/commands/gradle.go +77 -0
  15. package/internal/commands/justfile.go +82 -0
  16. package/internal/commands/makefile.go +106 -0
  17. package/internal/commands/maven.go +93 -0
  18. package/internal/commands/packagejson.go +159 -0
  19. package/internal/commands/pyproject.go +169 -0
  20. package/internal/commands/taskfile.go +101 -0
  21. package/internal/commands/testdata/estate/.cargo/config.toml +7 -0
  22. package/internal/commands/testdata/estate/Makefile +33 -0
  23. package/internal/commands/testdata/estate/Taskfile.yml +21 -0
  24. package/internal/commands/testdata/estate/build.gradle.kts +21 -0
  25. package/internal/commands/testdata/estate/justfile +20 -0
  26. package/internal/commands/testdata/estate/package.json +13 -0
  27. package/internal/commands/testdata/estate/pom.xml +29 -0
  28. package/internal/commands/testdata/estate/pyproject.toml +27 -0
  29. package/internal/commands/testdata/estate/xtask/src/main.rs +27 -0
  30. package/internal/commands/testdata/golden/commands.json +305 -0
  31. package/internal/gohttp/analyze.go +2500 -0
  32. package/internal/gohttp/endpoints.go +1067 -0
  33. package/internal/gohttp/roots.go +320 -0
  34. package/internal/gohttp/typed.go +143 -0
  35. package/internal/goscan/constants.go +85 -0
  36. package/internal/goscan/goscan_test.go +227 -0
  37. package/internal/goscan/names.go +52 -0
  38. package/internal/goscan/parse_test.go +11 -0
  39. package/internal/goscan/source.go +36 -0
  40. package/internal/goscan/tree.go +155 -0
  41. package/internal/goscan/types.go +99 -0
  42. package/internal/wsdl/ids.go +127 -0
  43. package/internal/wsdl/ids_test.go +21 -0
  44. package/internal/wsdl/model.go +62 -0
  45. package/internal/wsdl/parse.go +920 -0
  46. package/internal/wsdl/parse_test.go +133 -0
  47. package/package.json +117 -0
  48. package/plugin/describe.go +107 -0
  49. package/plugin/describe_test.go +114 -0
  50. package/plugin/protocol.go +117 -0
  51. package/plugin/schematest/schematest.go +126 -0
  52. package/plugins/README.md +1160 -0
  53. package/plugins/extract-adr/describe.go +19 -0
  54. package/plugins/extract-adr/describe_test.go +11 -0
  55. package/plugins/extract-adr/extract.go +153 -0
  56. package/plugins/extract-adr/extract_test.go +350 -0
  57. package/plugins/extract-adr/history.go +99 -0
  58. package/plugins/extract-adr/main.go +65 -0
  59. package/plugins/extract-adr/options.schema.json +26 -0
  60. package/plugins/extract-adr/parse.go +642 -0
  61. package/plugins/extract-adr/parse_test.go +405 -0
  62. package/plugins/extract-asyncapi/describe.go +19 -0
  63. package/plugins/extract-asyncapi/describe_test.go +11 -0
  64. package/plugins/extract-asyncapi/extract.go +315 -0
  65. package/plugins/extract-asyncapi/extract_test.go +197 -0
  66. package/plugins/extract-asyncapi/main.go +48 -0
  67. package/plugins/extract-asyncapi/options.schema.json +26 -0
  68. package/plugins/extract-asyncapi/spec.go +151 -0
  69. package/plugins/extract-celery/README.md +169 -0
  70. package/plugins/extract-celery/extract.py +346 -0
  71. package/plugins/extract-celery/extract_test.py +192 -0
  72. package/plugins/extract-celery/main.py +66 -0
  73. package/plugins/extract-celery/options.py +31 -0
  74. package/plugins/extract-celery/options.schema.json +31 -0
  75. package/plugins/extract-celery/producers.py +156 -0
  76. package/plugins/extract-celery/routes_test.py +106 -0
  77. package/plugins/extract-commands/describe.go +19 -0
  78. package/plugins/extract-commands/describe_test.go +11 -0
  79. package/plugins/extract-commands/extract.go +79 -0
  80. package/plugins/extract-commands/extract_test.go +92 -0
  81. package/plugins/extract-commands/main.go +55 -0
  82. package/plugins/extract-commands/options.schema.json +20 -0
  83. package/plugins/extract-csr/avro.go +258 -0
  84. package/plugins/extract-csr/describe.go +19 -0
  85. package/plugins/extract-csr/describe_test.go +11 -0
  86. package/plugins/extract-csr/extract.go +338 -0
  87. package/plugins/extract-csr/extract_test.go +374 -0
  88. package/plugins/extract-csr/jsonschema.go +343 -0
  89. package/plugins/extract-csr/lock.go +25 -0
  90. package/plugins/extract-csr/main.go +84 -0
  91. package/plugins/extract-csr/options.schema.json +43 -0
  92. package/plugins/extract-csr/subject.go +102 -0
  93. package/plugins/extract-django/README.md +386 -0
  94. package/plugins/extract-django/apps.py +100 -0
  95. package/plugins/extract-django/auth.py +302 -0
  96. package/plugins/extract-django/auth_test.py +188 -0
  97. package/plugins/extract-django/clients.py +121 -0
  98. package/plugins/extract-django/contracts.py +458 -0
  99. package/plugins/extract-django/database.py +119 -0
  100. package/plugins/extract-django/domain.py +318 -0
  101. package/plugins/extract-django/events.py +86 -0
  102. package/plugins/extract-django/extract.py +601 -0
  103. package/plugins/extract-django/extract_test.py +397 -0
  104. package/plugins/extract-django/flows.py +1278 -0
  105. package/plugins/extract-django/flows_test.py +338 -0
  106. package/plugins/extract-django/ids.py +42 -0
  107. package/plugins/extract-django/ids_test.py +30 -0
  108. package/plugins/extract-django/lifecycle.py +222 -0
  109. package/plugins/extract-django/lifecycle_test.py +76 -0
  110. package/plugins/extract-django/main.py +66 -0
  111. package/plugins/extract-django/openapi.py +229 -0
  112. package/plugins/extract-django/openapi_test.py +60 -0
  113. package/plugins/extract-django/operations.py +96 -0
  114. package/plugins/extract-django/options.py +61 -0
  115. package/plugins/extract-django/options.schema.json +126 -0
  116. package/plugins/extract-django/routing.py +214 -0
  117. package/plugins/extract-django/routing_test.py +198 -0
  118. package/plugins/extract-django/serializers.py +558 -0
  119. package/plugins/extract-django/store.py +434 -0
  120. package/plugins/extract-django/transport.py +242 -0
  121. package/plugins/extract-flows/describe.go +19 -0
  122. package/plugins/extract-flows/describe_test.go +11 -0
  123. package/plugins/extract-flows/extract.go +91 -0
  124. package/plugins/extract-flows/main.go +45 -0
  125. package/plugins/extract-flows/options.schema.json +16 -0
  126. package/plugins/extract-flows/parse.go +593 -0
  127. package/plugins/extract-flows/parse_test.go +204 -0
  128. package/plugins/extract-glossary/describe.go +19 -0
  129. package/plugins/extract-glossary/describe_test.go +11 -0
  130. package/plugins/extract-glossary/extract.go +115 -0
  131. package/plugins/extract-glossary/extract_test.go +220 -0
  132. package/plugins/extract-glossary/main.go +59 -0
  133. package/plugins/extract-glossary/options.schema.json +21 -0
  134. package/plugins/extract-glossary/parse.go +214 -0
  135. package/plugins/extract-glossary/parse_test.go +203 -0
  136. package/plugins/extract-go/aggregate.go +214 -0
  137. package/plugins/extract-go/client.go +409 -0
  138. package/plugins/extract-go/client_test.go +266 -0
  139. package/plugins/extract-go/describe.go +19 -0
  140. package/plugins/extract-go/describe_test.go +11 -0
  141. package/plugins/extract-go/enum.go +195 -0
  142. package/plugins/extract-go/enum_test.go +82 -0
  143. package/plugins/extract-go/event.go +99 -0
  144. package/plugins/extract-go/extract.go +191 -0
  145. package/plugins/extract-go/extract_test.go +261 -0
  146. package/plugins/extract-go/flow.go +1441 -0
  147. package/plugins/extract-go/flow_test.go +609 -0
  148. package/plugins/extract-go/httpclient.go +174 -0
  149. package/plugins/extract-go/httpclient_test.go +296 -0
  150. package/plugins/extract-go/ids.go +92 -0
  151. package/plugins/extract-go/layout.go +225 -0
  152. package/plugins/extract-go/layout_test.go +125 -0
  153. package/plugins/extract-go/lifecycle.go +324 -0
  154. package/plugins/extract-go/lifecycle_test.go +108 -0
  155. package/plugins/extract-go/main.go +79 -0
  156. package/plugins/extract-go/operation.go +157 -0
  157. package/plugins/extract-go/options.schema.json +66 -0
  158. package/plugins/extract-go/source.go +316 -0
  159. package/plugins/extract-go/transport.go +321 -0
  160. package/plugins/extract-go/transport_test.go +145 -0
  161. package/plugins/extract-go/wiring.go +434 -0
  162. package/plugins/extract-go-nats/describe.go +19 -0
  163. package/plugins/extract-go-nats/describe_test.go +11 -0
  164. package/plugins/extract-go-nats/extract.go +177 -0
  165. package/plugins/extract-go-nats/extract_test.go +389 -0
  166. package/plugins/extract-go-nats/index.go +379 -0
  167. package/plugins/extract-go-nats/main.go +42 -0
  168. package/plugins/extract-go-nats/options.schema.json +21 -0
  169. package/plugins/extract-go-nats/resolve.go +161 -0
  170. package/plugins/extract-go-nats/sites.go +224 -0
  171. package/plugins/extract-graphql/describe.go +19 -0
  172. package/plugins/extract-graphql/describe_test.go +11 -0
  173. package/plugins/extract-graphql/extract.go +433 -0
  174. package/plugins/extract-graphql/extract_test.go +256 -0
  175. package/plugins/extract-graphql/ids.go +49 -0
  176. package/plugins/extract-graphql/lex.go +237 -0
  177. package/plugins/extract-graphql/main.go +51 -0
  178. package/plugins/extract-graphql/options.schema.json +31 -0
  179. package/plugins/extract-graphql/parse.go +621 -0
  180. package/plugins/extract-graphql/parse_test.go +122 -0
  181. package/plugins/extract-http-clients/describe.go +19 -0
  182. package/plugins/extract-http-clients/describe_test.go +11 -0
  183. package/plugins/extract-http-clients/extract.go +705 -0
  184. package/plugins/extract-http-clients/extract_test.go +1263 -0
  185. package/plugins/extract-http-clients/main.go +42 -0
  186. package/plugins/extract-http-clients/options.schema.json +33 -0
  187. package/plugins/extract-java/README.md +190 -0
  188. package/plugins/extract-java/build/org/portolan/extract/Catalog.class +0 -0
  189. package/plugins/extract-java/build/org/portolan/extract/Clients$1.class +0 -0
  190. package/plugins/extract-java/build/org/portolan/extract/Clients$2.class +0 -0
  191. package/plugins/extract-java/build/org/portolan/extract/Clients$Call.class +0 -0
  192. package/plugins/extract-java/build/org/portolan/extract/Clients$Client.class +0 -0
  193. package/plugins/extract-java/build/org/portolan/extract/Clients$Route.class +0 -0
  194. package/plugins/extract-java/build/org/portolan/extract/Clients.class +0 -0
  195. package/plugins/extract-java/build/org/portolan/extract/Domain$Aggregate.class +0 -0
  196. package/plugins/extract-java/build/org/portolan/extract/Domain.class +0 -0
  197. package/plugins/extract-java/build/org/portolan/extract/Events$Found.class +0 -0
  198. package/plugins/extract-java/build/org/portolan/extract/Events.class +0 -0
  199. package/plugins/extract-java/build/org/portolan/extract/Extract.class +0 -0
  200. package/plugins/extract-java/build/org/portolan/extract/Flows$1.class +0 -0
  201. package/plugins/extract-java/build/org/portolan/extract/Flows$Binding.class +0 -0
  202. package/plugins/extract-java/build/org/portolan/extract/Flows$Draft.class +0 -0
  203. package/plugins/extract-java/build/org/portolan/extract/Flows$Frame.class +0 -0
  204. package/plugins/extract-java/build/org/portolan/extract/Flows$Options.class +0 -0
  205. package/plugins/extract-java/build/org/portolan/extract/Flows.class +0 -0
  206. package/plugins/extract-java/build/org/portolan/extract/Ids.class +0 -0
  207. package/plugins/extract-java/build/org/portolan/extract/Json$Reader.class +0 -0
  208. package/plugins/extract-java/build/org/portolan/extract/Json.class +0 -0
  209. package/plugins/extract-java/build/org/portolan/extract/Lifecycle$1.class +0 -0
  210. package/plugins/extract-java/build/org/portolan/extract/Lifecycle$Move.class +0 -0
  211. package/plugins/extract-java/build/org/portolan/extract/Lifecycle.class +0 -0
  212. package/plugins/extract-java/build/org/portolan/extract/Main.class +0 -0
  213. package/plugins/extract-java/build/org/portolan/extract/OpenApi$Operation.class +0 -0
  214. package/plugins/extract-java/build/org/portolan/extract/OpenApi$Spec.class +0 -0
  215. package/plugins/extract-java/build/org/portolan/extract/OpenApi.class +0 -0
  216. package/plugins/extract-java/build/org/portolan/extract/Operations$1.class +0 -0
  217. package/plugins/extract-java/build/org/portolan/extract/Operations$UseCase.class +0 -0
  218. package/plugins/extract-java/build/org/portolan/extract/Operations.class +0 -0
  219. package/plugins/extract-java/build/org/portolan/extract/Proto$Rpc.class +0 -0
  220. package/plugins/extract-java/build/org/portolan/extract/Proto$Service.class +0 -0
  221. package/plugins/extract-java/build/org/portolan/extract/Proto.class +0 -0
  222. package/plugins/extract-java/build/org/portolan/extract/Protocol$Builder.class +0 -0
  223. package/plugins/extract-java/build/org/portolan/extract/Protocol$Input.class +0 -0
  224. package/plugins/extract-java/build/org/portolan/extract/Protocol$Options.class +0 -0
  225. package/plugins/extract-java/build/org/portolan/extract/Protocol.class +0 -0
  226. package/plugins/extract-java/build/org/portolan/extract/Source$Project.class +0 -0
  227. package/plugins/extract-java/build/org/portolan/extract/Source$Unit.class +0 -0
  228. package/plugins/extract-java/build/org/portolan/extract/Source.class +0 -0
  229. package/plugins/extract-java/build/org/portolan/extract/Transport$Endpoint.class +0 -0
  230. package/plugins/extract-java/build/org/portolan/extract/Transport.class +0 -0
  231. package/plugins/extract-java/build/org/portolan/extract/Yaml$Line.class +0 -0
  232. package/plugins/extract-java/build/org/portolan/extract/Yaml$Reader.class +0 -0
  233. package/plugins/extract-java/build/org/portolan/extract/Yaml.class +0 -0
  234. package/plugins/extract-java/options.schema.json +70 -0
  235. package/plugins/extract-openapi/describe.go +19 -0
  236. package/plugins/extract-openapi/describe_test.go +11 -0
  237. package/plugins/extract-openapi/discover.go +243 -0
  238. package/plugins/extract-openapi/extract.go +526 -0
  239. package/plugins/extract-openapi/extract_test.go +545 -0
  240. package/plugins/extract-openapi/main.go +75 -0
  241. package/plugins/extract-openapi/options.schema.json +62 -0
  242. package/plugins/extract-openapi/spec.go +350 -0
  243. package/plugins/extract-project/README.md +22 -0
  244. package/plugins/extract-project/describe.go +19 -0
  245. package/plugins/extract-project/describe_test.go +11 -0
  246. package/plugins/extract-project/extract.go +221 -0
  247. package/plugins/extract-project/extract_test.go +109 -0
  248. package/plugins/extract-project/main.go +41 -0
  249. package/plugins/extract-project/options.schema.json +48 -0
  250. package/plugins/extract-proto/ast.go +125 -0
  251. package/plugins/extract-proto/consumes.go +77 -0
  252. package/plugins/extract-proto/describe.go +19 -0
  253. package/plugins/extract-proto/extract.go +293 -0
  254. package/plugins/extract-proto/extract_test.go +459 -0
  255. package/plugins/extract-proto/ids.go +89 -0
  256. package/plugins/extract-proto/ids_test.go +57 -0
  257. package/plugins/extract-proto/lex.go +285 -0
  258. package/plugins/extract-proto/main.go +100 -0
  259. package/plugins/extract-proto/module.go +120 -0
  260. package/plugins/extract-proto/options.schema.json +51 -0
  261. package/plugins/extract-proto/parse.go +720 -0
  262. package/plugins/extract-proto/parse_test.go +307 -0
  263. package/plugins/extract-proto/provides.go +236 -0
  264. package/plugins/extract-proto/resolve.go +222 -0
  265. package/plugins/extract-proto/resolve_test.go +119 -0
  266. package/plugins/extract-python-kafka/README.md +36 -0
  267. package/plugins/extract-python-kafka/extract.py +211 -0
  268. package/plugins/extract-python-kafka/extract_test.py +68 -0
  269. package/plugins/extract-python-kafka/main.py +59 -0
  270. package/plugins/extract-python-kafka/options.py +29 -0
  271. package/plugins/extract-python-kafka/options.schema.json +31 -0
  272. package/plugins/extract-redis/README.md +11 -0
  273. package/plugins/extract-redis/describe.go +19 -0
  274. package/plugins/extract-redis/describe_test.go +11 -0
  275. package/plugins/extract-redis/extract.go +183 -0
  276. package/plugins/extract-redis/extract_test.go +168 -0
  277. package/plugins/extract-redis/keyspaces.go +469 -0
  278. package/plugins/extract-redis/main.go +44 -0
  279. package/plugins/extract-redis/options.schema.json +30 -0
  280. package/plugins/extract-river/describe.go +19 -0
  281. package/plugins/extract-river/describe_test.go +11 -0
  282. package/plugins/extract-river/extract.go +507 -0
  283. package/plugins/extract-river/extract_test.go +132 -0
  284. package/plugins/extract-river/main.go +42 -0
  285. package/plugins/extract-river/options.schema.json +21 -0
  286. package/plugins/extract-rust/Cargo.lock +133 -0
  287. package/plugins/extract-rust/Cargo.toml +23 -0
  288. package/plugins/extract-rust/README.md +204 -0
  289. package/plugins/extract-rust/options.schema.json +65 -0
  290. package/plugins/extract-rust/rustfmt.toml +1 -0
  291. package/plugins/extract-sql/ddl.go +893 -0
  292. package/plugins/extract-sql/ddl_test.go +401 -0
  293. package/plugins/extract-sql/describe.go +19 -0
  294. package/plugins/extract-sql/describe_test.go +11 -0
  295. package/plugins/extract-sql/layout.go +221 -0
  296. package/plugins/extract-sql/layout_test.go +55 -0
  297. package/plugins/extract-sql/lineage.go +107 -0
  298. package/plugins/extract-sql/main.go +141 -0
  299. package/plugins/extract-sql/maps.go +564 -0
  300. package/plugins/extract-sql/maps_java.go +117 -0
  301. package/plugins/extract-sql/maps_rust.go +333 -0
  302. package/plugins/extract-sql/maps_rust_test.go +70 -0
  303. package/plugins/extract-sql/maps_test.go +204 -0
  304. package/plugins/extract-sql/maps_ts.go +398 -0
  305. package/plugins/extract-sql/maps_ts_test.go +136 -0
  306. package/plugins/extract-sql/options.schema.json +53 -0
  307. package/plugins/extract-sql/projection.go +70 -0
  308. package/plugins/extract-sql/projection_test.go +50 -0
  309. package/plugins/extract-sql/store.go +420 -0
  310. package/plugins/extract-sql/store_test.go +233 -0
  311. package/plugins/extract-sql/view.go +295 -0
  312. package/plugins/extract-ts/README.md +306 -0
  313. package/plugins/extract-ts/ast.test.ts +178 -0
  314. package/plugins/extract-ts/ast.ts +648 -0
  315. package/plugins/extract-ts/clients.ts +185 -0
  316. package/plugins/extract-ts/domain.ts +164 -0
  317. package/plugins/extract-ts/extract.test.ts +208 -0
  318. package/plugins/extract-ts/extract.ts +200 -0
  319. package/plugins/extract-ts/flows.ts +783 -0
  320. package/plugins/extract-ts/graphql.test.ts +73 -0
  321. package/plugins/extract-ts/graphql.ts +181 -0
  322. package/plugins/extract-ts/ids.test.ts +21 -0
  323. package/plugins/extract-ts/ids.ts +52 -0
  324. package/plugins/extract-ts/lifecycle.ts +138 -0
  325. package/plugins/extract-ts/main.ts +48 -0
  326. package/plugins/extract-ts/openapi.test.ts +64 -0
  327. package/plugins/extract-ts/openapi.ts +99 -0
  328. package/plugins/extract-ts/operations.ts +93 -0
  329. package/plugins/extract-ts/options.schema.json +96 -0
  330. package/plugins/extract-ts/source.test.ts +174 -0
  331. package/plugins/extract-ts/source.ts +465 -0
  332. package/plugins/extract-ts/transport.ts +147 -0
  333. package/plugins/extract-ts/tsconfig.json +13 -0
  334. package/plugins/extract-ts/wiring.ts +190 -0
  335. package/plugins/extract-watermill/describe.go +19 -0
  336. package/plugins/extract-watermill/describe_test.go +11 -0
  337. package/plugins/extract-watermill/extract.go +1228 -0
  338. package/plugins/extract-watermill/extract_test.go +234 -0
  339. package/plugins/extract-watermill/main.go +41 -0
  340. package/plugins/extract-watermill/options.schema.json +21 -0
  341. package/plugins/extract-wsdl/describe.go +19 -0
  342. package/plugins/extract-wsdl/describe_test.go +11 -0
  343. package/plugins/extract-wsdl/extract.go +148 -0
  344. package/plugins/extract-wsdl/extract_test.go +45 -0
  345. package/plugins/extract-wsdl/main.go +53 -0
  346. package/plugins/extract-wsdl/options.schema.json +59 -0
  347. package/plugins/fetch-bsr/auth.go +114 -0
  348. package/plugins/fetch-bsr/bsr.go +240 -0
  349. package/plugins/fetch-bsr/cache.go +66 -0
  350. package/plugins/fetch-bsr/describe.go +19 -0
  351. package/plugins/fetch-bsr/fetch.go +192 -0
  352. package/plugins/fetch-bsr/fetch_test.go +465 -0
  353. package/plugins/fetch-bsr/lock.go +71 -0
  354. package/plugins/fetch-bsr/main.go +117 -0
  355. package/plugins/fetch-bsr/options.schema.json +42 -0
  356. package/plugins/fetch-csr/auth.go +95 -0
  357. package/plugins/fetch-csr/cache.go +69 -0
  358. package/plugins/fetch-csr/describe.go +19 -0
  359. package/plugins/fetch-csr/describe_test.go +11 -0
  360. package/plugins/fetch-csr/fetch.go +254 -0
  361. package/plugins/fetch-csr/fetch_test.go +494 -0
  362. package/plugins/fetch-csr/lock.go +95 -0
  363. package/plugins/fetch-csr/main.go +122 -0
  364. package/plugins/fetch-csr/options.schema.json +42 -0
  365. package/plugins/fetch-csr/registry.go +209 -0
  366. package/plugins/fetch-git/cache.go +65 -0
  367. package/plugins/fetch-git/describe.go +19 -0
  368. package/plugins/fetch-git/describe_test.go +11 -0
  369. package/plugins/fetch-git/fetch.go +181 -0
  370. package/plugins/fetch-git/fetch_test.go +332 -0
  371. package/plugins/fetch-git/git.go +169 -0
  372. package/plugins/fetch-git/lock.go +72 -0
  373. package/plugins/fetch-git/main.go +127 -0
  374. package/plugins/fetch-git/offline.go +39 -0
  375. package/plugins/fetch-git/options.schema.json +42 -0
  376. package/plugins/fetch-git/pin.go +90 -0
  377. package/plugins/fetch-git/pin_test.go +74 -0
  378. package/plugins/gen-backstage/describe.go +17 -0
  379. package/plugins/gen-backstage/main.go +22 -0
  380. package/plugins/gen-backstage/options.schema.json +30 -0
  381. package/plugins/gen-backstage/plugin.go +473 -0
  382. package/plugins/gen-backstage/plugin_test.go +145 -0
  383. package/plugins/gen-backstage.wasm +0 -0
  384. package/plugins/gen-markdown/adr.go +162 -0
  385. package/plugins/gen-markdown/aggregate.go +362 -0
  386. package/plugins/gen-markdown/canonical.go +93 -0
  387. package/plugins/gen-markdown/context.go +90 -0
  388. package/plugins/gen-markdown/coverage_test.go +89 -0
  389. package/plugins/gen-markdown/describe.go +19 -0
  390. package/plugins/gen-markdown/describe_test.go +11 -0
  391. package/plugins/gen-markdown/external.go +71 -0
  392. package/plugins/gen-markdown/flow.go +277 -0
  393. package/plugins/gen-markdown/glossary.go +76 -0
  394. package/plugins/gen-markdown/glossary_test.go +148 -0
  395. package/plugins/gen-markdown/llms.go +302 -0
  396. package/plugins/gen-markdown/main.go +22 -0
  397. package/plugins/gen-markdown/markdown.go +254 -0
  398. package/plugins/gen-markdown/markdown_test.go +100 -0
  399. package/plugins/gen-markdown/module.go +107 -0
  400. package/plugins/gen-markdown/options.schema.json +16 -0
  401. package/plugins/gen-markdown/plugin.go +39 -0
  402. package/plugins/gen-markdown/quality_test.go +176 -0
  403. package/plugins/gen-markdown/redis_test.go +38 -0
  404. package/plugins/gen-markdown/render.go +344 -0
  405. package/plugins/gen-markdown/render_test.go +186 -0
  406. package/plugins/gen-markdown/service.go +456 -0
  407. package/plugins/gen-markdown/source.go +177 -0
  408. package/plugins/gen-markdown/store.go +201 -0
  409. package/plugins/gen-markdown.wasm +0 -0
  410. package/plugins/gen-mermaid/describe.go +17 -0
  411. package/plugins/gen-mermaid/main.go +22 -0
  412. package/plugins/gen-mermaid/options.schema.json +10 -0
  413. package/plugins/gen-mermaid/plugin.go +104 -0
  414. package/plugins/gen-mermaid/plugin_test.go +33 -0
  415. package/plugins/gen-mermaid.wasm +0 -0
  416. package/plugins/openapi/ids.go +261 -0
  417. package/plugins/openapi/ids_test.go +98 -0
  418. package/plugins/pyplugin/catalog.py +209 -0
  419. package/plugins/pyplugin/celery_conf.py +436 -0
  420. package/plugins/pyplugin/celery_tasks.py +76 -0
  421. package/plugins/pyplugin/kafka.py +570 -0
  422. package/plugins/pyplugin/names.py +38 -0
  423. package/plugins/pyplugin/protocol.py +79 -0
  424. package/plugins/pyplugin/source.py +285 -0
  425. package/plugins/verify-codeowners/describe.go +19 -0
  426. package/plugins/verify-codeowners/describe_test.go +11 -0
  427. package/plugins/verify-codeowners/main.go +75 -0
  428. package/plugins/verify-codeowners/match.go +85 -0
  429. package/plugins/verify-codeowners/match_test.go +47 -0
  430. package/plugins/verify-codeowners/options.schema.json +15 -0
  431. package/plugins/verify-codeowners/owners.go +164 -0
  432. package/plugins/verify-codeowners/owners_test.go +225 -0
  433. package/plugins/verify-codeowners/parse.go +90 -0
  434. package/plugins/verify-codeowners/parse_test.go +62 -0
  435. package/plugins/verify-otel/describe.go +19 -0
  436. package/plugins/verify-otel/describe_test.go +11 -0
  437. package/plugins/verify-otel/main.go +53 -0
  438. package/plugins/verify-otel/match.go +336 -0
  439. package/plugins/verify-otel/options.schema.json +30 -0
  440. package/plugins/verify-otel/otlp.go +200 -0
  441. package/plugins/verify-otel/verify.go +734 -0
  442. package/plugins/verify-otel/verify_test.go +460 -0
  443. package/portolan.json +1073 -0
  444. package/public/favicon.svg +12 -0
  445. package/schema/portolan.schema.json +2558 -0
  446. package/scripts/README.md +53 -0
  447. package/scripts/build-report.mjs +60 -0
  448. package/scripts/build-report.test.mjs +44 -0
  449. package/scripts/builtin-plugins.mjs +49 -0
  450. package/scripts/catalog-sources.mjs +67 -0
  451. package/scripts/diff.mjs +315 -0
  452. package/scripts/diff.test.mjs +151 -0
  453. package/scripts/forge-comment.mjs +144 -0
  454. package/scripts/forge-comment.test.mjs +151 -0
  455. package/scripts/forge-release.mjs +138 -0
  456. package/scripts/forge-release.test.mjs +106 -0
  457. package/scripts/forge.mjs +61 -0
  458. package/scripts/gen-likec4.mjs +854 -0
  459. package/scripts/gen-likec4.test.mjs +258 -0
  460. package/scripts/gen.mjs +603 -0
  461. package/scripts/generated-docs.test.mjs +66 -0
  462. package/scripts/local-api.mjs +1192 -0
  463. package/scripts/local-api.test.mjs +387 -0
  464. package/scripts/manifest.mjs +214 -0
  465. package/scripts/manifest.test.mjs +148 -0
  466. package/scripts/output-path.mjs +79 -0
  467. package/scripts/output-path.test.mjs +45 -0
  468. package/scripts/package-smoke.mjs +37 -0
  469. package/scripts/plugin-host.mjs +419 -0
  470. package/scripts/plugin-host.test.mjs +109 -0
  471. package/scripts/plugin-wasm-worker.mjs +34 -0
  472. package/scripts/run-builtin.mjs +81 -0
  473. package/scripts/schema.mjs +357 -0
  474. package/scripts/site-docs.mjs +295 -0
  475. package/scripts/site-docs.test.mjs +181 -0
  476. package/scripts/vendor-lock.mjs +58 -0
  477. package/scripts/vendor-lock.test.mjs +69 -0
  478. package/src/README.md +38 -0
  479. package/src/app/App.tsx +448 -0
  480. package/src/app/BranchPicker.test.ts +46 -0
  481. package/src/app/BranchPicker.tsx +153 -0
  482. package/src/app/Breadcrumbs.test.ts +38 -0
  483. package/src/app/Breadcrumbs.tsx +223 -0
  484. package/src/app/BuildStamp.tsx +146 -0
  485. package/src/app/CommandPalette.tsx +292 -0
  486. package/src/app/HashScroll.tsx +45 -0
  487. package/src/app/Sidebar.tsx +1742 -0
  488. package/src/app/TopBar.tsx +393 -0
  489. package/src/app/density.tsx +64 -0
  490. package/src/app/forge-access.test.ts +27 -0
  491. package/src/app/forge-access.tsx +80 -0
  492. package/src/app/panels.tsx +141 -0
  493. package/src/app/pins.tsx +242 -0
  494. package/src/app/query-client.ts +24 -0
  495. package/src/app/responsive.ts +55 -0
  496. package/src/app/shortcuts.test.ts +39 -0
  497. package/src/app/shortcuts.tsx +282 -0
  498. package/src/app/theme.tsx +51 -0
  499. package/src/app/title.test.ts +14 -0
  500. package/src/app/title.ts +30 -0
  501. package/src/app/toast.tsx +59 -0
  502. package/src/app/tree-keys.test.ts +80 -0
  503. package/src/app/tree-keys.ts +87 -0
  504. package/src/app/ui-store.ts +52 -0
  505. package/src/catalog-error.test.ts +105 -0
  506. package/src/catalog-profile.test.ts +59 -0
  507. package/src/catalog-profile.ts +101 -0
  508. package/src/catalog-stores.test.ts +431 -0
  509. package/src/catalog.test.ts +1215 -0
  510. package/src/catalog.ts +3125 -0
  511. package/src/chat/Cards.tsx +408 -0
  512. package/src/chat/ChatPanel.tsx +79 -0
  513. package/src/chat/ChatSettings.tsx +219 -0
  514. package/src/chat/Composer.tsx +99 -0
  515. package/src/chat/Conversation.tsx +281 -0
  516. package/src/chat/Header.tsx +46 -0
  517. package/src/chat/Message.tsx +49 -0
  518. package/src/chat/Notice.tsx +195 -0
  519. package/src/chat/Starter.tsx +67 -0
  520. package/src/chat/Steps.tsx +45 -0
  521. package/src/chat/Waiting.tsx +87 -0
  522. package/src/chat/flags.test.ts +59 -0
  523. package/src/chat/flags.ts +155 -0
  524. package/src/chat/linkify.test.ts +30 -0
  525. package/src/chat/linkify.ts +45 -0
  526. package/src/chat/prefs.ts +56 -0
  527. package/src/chat/prompt.test.ts +43 -0
  528. package/src/chat/prompt.ts +157 -0
  529. package/src/chat/store.ts +35 -0
  530. package/src/chat/tools.ts +58 -0
  531. package/src/chat/transport.ts +70 -0
  532. package/src/components/AdrRow.tsx +29 -0
  533. package/src/components/AnchorLink.tsx +67 -0
  534. package/src/components/ApiReference.tsx +134 -0
  535. package/src/components/AsyncApiReference.tsx +120 -0
  536. package/src/components/CatalogStamp.tsx +155 -0
  537. package/src/components/ChannelRows.tsx +132 -0
  538. package/src/components/CommandRows.tsx +139 -0
  539. package/src/components/DiagramSkeleton.tsx +71 -0
  540. package/src/components/EditorLink.tsx +85 -0
  541. package/src/components/FieldTree.tsx +305 -0
  542. package/src/components/FlowTrigger.tsx +34 -0
  543. package/src/components/Ident.tsx +89 -0
  544. package/src/components/Integrations.tsx +243 -0
  545. package/src/components/LifecycleDiagram.tsx +110 -0
  546. package/src/components/MachineDocs.tsx +42 -0
  547. package/src/components/Markdown.tsx +173 -0
  548. package/src/components/MarkdownTable.tsx +206 -0
  549. package/src/components/Mermaid.tsx +211 -0
  550. package/src/components/MethodRows.tsx +309 -0
  551. package/src/components/Overlay.tsx +125 -0
  552. package/src/components/PageHeader.tsx +189 -0
  553. package/src/components/RowActions.tsx +97 -0
  554. package/src/components/SchemaDocument.tsx +89 -0
  555. package/src/components/Select.tsx +126 -0
  556. package/src/components/ShapeRows.tsx +118 -0
  557. package/src/components/SourceDoc.tsx +82 -0
  558. package/src/components/SourcePreview.tsx +441 -0
  559. package/src/components/TabRow.tsx +182 -0
  560. package/src/components/TechIcon.tsx +34 -0
  561. package/src/components/Toc.tsx +132 -0
  562. package/src/components/WhatLinksHere.tsx +273 -0
  563. package/src/components/WsdlReference.tsx +84 -0
  564. package/src/components/ddd-icons.tsx +107 -0
  565. package/src/components/kind.tsx +157 -0
  566. package/src/components/logo.tsx +61 -0
  567. package/src/components/primitives.tsx +264 -0
  568. package/src/data.test.ts +18 -0
  569. package/src/data.ts +202 -0
  570. package/src/enrich.test.ts +1150 -0
  571. package/src/enrich.ts +672 -0
  572. package/src/er/ErCanvas.tsx +533 -0
  573. package/src/er/RedisSchema.tsx +74 -0
  574. package/src/er/RelationCard.tsx +267 -0
  575. package/src/er/StoreHeader.tsx +155 -0
  576. package/src/er/TableNode.tsx +84 -0
  577. package/src/er/ViewNode.tsx +83 -0
  578. package/src/er/layout.test.ts +115 -0
  579. package/src/er/layout.ts +77 -0
  580. package/src/er/lineage.test.ts +140 -0
  581. package/src/er/lineage.ts +107 -0
  582. package/src/er/markers.tsx +79 -0
  583. package/src/er/spec.test.ts +344 -0
  584. package/src/er/spec.ts +426 -0
  585. package/src/flow/ChainList.tsx +162 -0
  586. package/src/flow/FlowTable.tsx +123 -0
  587. package/src/flow/FlowToolbar.tsx +372 -0
  588. package/src/flow/StepDetail.tsx +380 -0
  589. package/src/flow/StepRail.tsx +428 -0
  590. package/src/flow/answers.test.ts +50 -0
  591. package/src/flow/answers.ts +58 -0
  592. package/src/flow/chain.test.ts +383 -0
  593. package/src/flow/chain.ts +353 -0
  594. package/src/flow/chapters.test.ts +185 -0
  595. package/src/flow/chapters.ts +225 -0
  596. package/src/flow/continues.test.ts +102 -0
  597. package/src/flow/continues.ts +65 -0
  598. package/src/flow/cross-context.test.ts +97 -0
  599. package/src/flow/cross-context.ts +35 -0
  600. package/src/flow/mermaid.test.ts +22 -0
  601. package/src/flow/mermaid.ts +141 -0
  602. package/src/flow/outline.test.ts +212 -0
  603. package/src/flow/outline.ts +170 -0
  604. package/src/flow/paths.test.ts +129 -0
  605. package/src/flow/paths.ts +147 -0
  606. package/src/flow/prefs.test.ts +80 -0
  607. package/src/flow/prefs.ts +104 -0
  608. package/src/graph/DependencyGraph.tsx +283 -0
  609. package/src/graph/DependencyNodes.tsx +162 -0
  610. package/src/graph/EdgeLabel.tsx +61 -0
  611. package/src/graph/FocusedEventGraph.tsx +211 -0
  612. package/src/graph/GraphToolbar.tsx +187 -0
  613. package/src/graph/RoutedEdge.tsx +80 -0
  614. package/src/graph/dependency-layout.test.ts +360 -0
  615. package/src/graph/dependency-layout.ts +444 -0
  616. package/src/graph/elk.test.ts +70 -0
  617. package/src/graph/elk.ts +287 -0
  618. package/src/graph/nodes.tsx +97 -0
  619. package/src/graph/theme.ts +81 -0
  620. package/src/graph/useElkFlow.ts +125 -0
  621. package/src/index.css +1547 -0
  622. package/src/language/InLanguage.tsx +40 -0
  623. package/src/language/TermCard.tsx +132 -0
  624. package/src/language/cards.test.ts +114 -0
  625. package/src/language/cards.ts +111 -0
  626. package/src/lib/admonition.test.ts +49 -0
  627. package/src/lib/admonition.ts +48 -0
  628. package/src/lib/adr.test.ts +131 -0
  629. package/src/lib/adr.ts +68 -0
  630. package/src/lib/api.test.ts +122 -0
  631. package/src/lib/api.ts +135 -0
  632. package/src/lib/backlinks.test.ts +330 -0
  633. package/src/lib/backlinks.ts +567 -0
  634. package/src/lib/branch-compare.ts +16 -0
  635. package/src/lib/build-info.test.ts +76 -0
  636. package/src/lib/build-info.ts +80 -0
  637. package/src/lib/catalog-diff.test.ts +275 -0
  638. package/src/lib/catalog-diff.ts +655 -0
  639. package/src/lib/clipboard.ts +43 -0
  640. package/src/lib/comparison-memory.test.ts +40 -0
  641. package/src/lib/comparison-memory.ts +39 -0
  642. package/src/lib/context-color.ts +27 -0
  643. package/src/lib/context-map.test.ts +148 -0
  644. package/src/lib/context-map.ts +399 -0
  645. package/src/lib/data-model.test.ts +245 -0
  646. package/src/lib/data-model.ts +399 -0
  647. package/src/lib/data-problems.test.ts +178 -0
  648. package/src/lib/data-problems.ts +314 -0
  649. package/src/lib/derive.test.ts +251 -0
  650. package/src/lib/derive.ts +515 -0
  651. package/src/lib/editor-link.test.ts +125 -0
  652. package/src/lib/editor-link.ts +96 -0
  653. package/src/lib/editor-prefs.ts +27 -0
  654. package/src/lib/event-graph.test.ts +237 -0
  655. package/src/lib/event-graph.ts +327 -0
  656. package/src/lib/export-canvas.ts +37 -0
  657. package/src/lib/export-file.test.ts +30 -0
  658. package/src/lib/export-file.ts +20 -0
  659. package/src/lib/flow-tree.test.ts +177 -0
  660. package/src/lib/flow-tree.ts +160 -0
  661. package/src/lib/forge-refs.test.ts +50 -0
  662. package/src/lib/forge-refs.ts +60 -0
  663. package/src/lib/format.test.ts +35 -0
  664. package/src/lib/format.ts +64 -0
  665. package/src/lib/github-catalog.test.ts +301 -0
  666. package/src/lib/github-catalog.ts +610 -0
  667. package/src/lib/integrations.test.ts +119 -0
  668. package/src/lib/integrations.ts +133 -0
  669. package/src/lib/kinds.test.ts +91 -0
  670. package/src/lib/kinds.ts +204 -0
  671. package/src/lib/lifecycle.test.ts +93 -0
  672. package/src/lib/lifecycle.ts +165 -0
  673. package/src/lib/local-api.ts +194 -0
  674. package/src/lib/motion-features.ts +11 -0
  675. package/src/lib/motion.test.ts +120 -0
  676. package/src/lib/motion.tsx +269 -0
  677. package/src/lib/palette-recent.test.ts +80 -0
  678. package/src/lib/palette-recent.ts +90 -0
  679. package/src/lib/palette.test.ts +365 -0
  680. package/src/lib/palette.ts +593 -0
  681. package/src/lib/pins.test.ts +139 -0
  682. package/src/lib/pins.ts +130 -0
  683. package/src/lib/problems.test.ts +112 -0
  684. package/src/lib/project-preview.ts +5 -0
  685. package/src/lib/proto-problems.test.ts +121 -0
  686. package/src/lib/proto-problems.ts +65 -0
  687. package/src/lib/queries.test.ts +113 -0
  688. package/src/lib/queries.ts +131 -0
  689. package/src/lib/registry.test.ts +219 -0
  690. package/src/lib/registry.ts +208 -0
  691. package/src/lib/scenarios.ts +547 -0
  692. package/src/lib/setup-info.test.ts +199 -0
  693. package/src/lib/setup-info.ts +415 -0
  694. package/src/lib/shape.test.ts +289 -0
  695. package/src/lib/shape.ts +454 -0
  696. package/src/lib/sidebar-prefs.test.ts +32 -0
  697. package/src/lib/sidebar-prefs.ts +75 -0
  698. package/src/lib/source-code.test.ts +159 -0
  699. package/src/lib/source-code.ts +286 -0
  700. package/src/lib/source-doc.test.ts +131 -0
  701. package/src/lib/source-doc.ts +103 -0
  702. package/src/lib/source-highlight.test.ts +29 -0
  703. package/src/lib/source-highlight.ts +24 -0
  704. package/src/lib/source-link.test.ts +153 -0
  705. package/src/lib/source-link.ts +201 -0
  706. package/src/lib/spec-files.test.ts +24 -0
  707. package/src/lib/spec-files.ts +23 -0
  708. package/src/lib/table-directive.test.ts +139 -0
  709. package/src/lib/table-directive.ts +106 -0
  710. package/src/lib/tech.test.ts +120 -0
  711. package/src/lib/tech.ts +421 -0
  712. package/src/lib/terms.test.ts +67 -0
  713. package/src/lib/terms.ts +179 -0
  714. package/src/lib/wire-problems.test.ts +263 -0
  715. package/src/lib/wire-problems.ts +281 -0
  716. package/src/likec4/C4View.tsx +55 -0
  717. package/src/likec4/CanvasBridge.tsx +187 -0
  718. package/src/likec4/FlowView.tsx +235 -0
  719. package/src/likec4/InteractiveView.tsx +144 -0
  720. package/src/likec4/canvas-viewport.test.ts +149 -0
  721. package/src/likec4/canvas-viewport.ts +135 -0
  722. package/src/likec4/flow-edges.test.ts +51 -0
  723. package/src/likec4/flow-edges.ts +89 -0
  724. package/src/likec4/frame-theme.test.ts +76 -0
  725. package/src/likec4/frame-theme.ts +101 -0
  726. package/src/likec4/highlight-css.test.ts +49 -0
  727. package/src/likec4/highlight-css.ts +52 -0
  728. package/src/likec4/ids.test.ts +151 -0
  729. package/src/likec4/ids.ts +115 -0
  730. package/src/likec4/levels.tsx +70 -0
  731. package/src/likec4/mapping.test.ts +66 -0
  732. package/src/likec4/mapping.ts +54 -0
  733. package/src/likec4/reserved.json +141 -0
  734. package/src/likec4/view-index.ts +78 -0
  735. package/src/main.tsx +13 -0
  736. package/src/map/ContextMapGraph.tsx +274 -0
  737. package/src/map/ContextNode.tsx +70 -0
  738. package/src/map/FloatingEdge.tsx +86 -0
  739. package/src/merge.test.ts +1038 -0
  740. package/src/merge.ts +1004 -0
  741. package/src/pages/AdrDetail.tsx +242 -0
  742. package/src/pages/AdrIndex.tsx +114 -0
  743. package/src/pages/AggregatePage.tsx +727 -0
  744. package/src/pages/BlockPage.tsx +478 -0
  745. package/src/pages/CatalogFailure.tsx +111 -0
  746. package/src/pages/Changes.tsx +313 -0
  747. package/src/pages/ContextMap.tsx +534 -0
  748. package/src/pages/ContextPage.tsx +346 -0
  749. package/src/pages/EnumPage.tsx +282 -0
  750. package/src/pages/EventPage.tsx +686 -0
  751. package/src/pages/ExternalPage.tsx +204 -0
  752. package/src/pages/FlowDetail.tsx +663 -0
  753. package/src/pages/FlowIndex.tsx +303 -0
  754. package/src/pages/GraphPage.tsx +217 -0
  755. package/src/pages/Language.tsx +257 -0
  756. package/src/pages/ModulePage.tsx +366 -0
  757. package/src/pages/NotFound.tsx +54 -0
  758. package/src/pages/Overview.tsx +344 -0
  759. package/src/pages/Problems.tsx +360 -0
  760. package/src/pages/RegistryIndex.tsx +118 -0
  761. package/src/pages/ServicePage.tsx +746 -0
  762. package/src/pages/Settings.tsx +975 -0
  763. package/src/pages/StorePage.tsx +114 -0
  764. package/src/peek/PeekCard.test.ts +35 -0
  765. package/src/peek/PeekCard.tsx +118 -0
  766. package/src/peek/PeekLayer.tsx +147 -0
  767. package/src/peek/model.test.ts +156 -0
  768. package/src/peek/model.ts +253 -0
  769. package/src/routes.test.ts +222 -0
  770. package/src/routes.ts +449 -0
  771. package/src/selection/DetailPanel.tsx +1407 -0
  772. package/src/selection/hash.test.ts +73 -0
  773. package/src/selection/hash.ts +58 -0
  774. package/src/selection/model.test.ts +199 -0
  775. package/src/selection/model.ts +473 -0
  776. package/src/selection/pages.test.ts +115 -0
  777. package/src/selection/pages.ts +268 -0
  778. package/src/selection/store.test.ts +62 -0
  779. package/src/selection/store.ts +46 -0
  780. package/src/selection/sync.tsx +99 -0
  781. package/src/table/ColumnsMenu.tsx +72 -0
  782. package/src/table/DataTable.tsx +780 -0
  783. package/src/table/Facets.tsx +54 -0
  784. package/src/table/GroupMenu.tsx +69 -0
  785. package/src/table/Toolbar.tsx +190 -0
  786. package/src/table/cells.tsx +117 -0
  787. package/src/table/compare.test.ts +117 -0
  788. package/src/table/compare.ts +196 -0
  789. package/src/table/export.test.ts +88 -0
  790. package/src/table/export.ts +66 -0
  791. package/src/table/facet-groups.test.ts +74 -0
  792. package/src/table/facet-groups.ts +56 -0
  793. package/src/table/grouping.test.ts +84 -0
  794. package/src/table/grouping.ts +82 -0
  795. package/src/table/infer.test.ts +57 -0
  796. package/src/table/infer.ts +50 -0
  797. package/src/table/persist.test.ts +76 -0
  798. package/src/table/persist.ts +112 -0
  799. package/src/table/sort-url.test.ts +75 -0
  800. package/src/table/sort-url.ts +52 -0
  801. package/src/table/types.ts +80 -0
  802. package/src/table/useTableState.ts +220 -0
  803. package/src/test-catalog.ts +7 -0
  804. package/src/testing/estate/data__catalog.json +2847 -0
  805. package/src/testing/estate/data__flows.json +1724 -0
  806. package/src/testing/estate/examples__auth__portolan__api.json +212 -0
  807. package/src/testing/estate/examples__auth__portolan__domain.json +1204 -0
  808. package/src/testing/estate/examples__auth__portolan__glossary.json +226 -0
  809. package/src/testing/estate/examples__auth__portolan__observed.json +757 -0
  810. package/src/testing/estate/examples__auth__portolan__stores.json +195 -0
  811. package/src/testing/estate/examples__shop__cart__portolan__api.json +234 -0
  812. package/src/testing/estate/examples__shop__cart__portolan__commands.json +86 -0
  813. package/src/testing/estate/examples__shop__cart__portolan__domain.json +966 -0
  814. package/src/testing/estate/examples__shop__cart__portolan__observed.json +498 -0
  815. package/src/testing/estate/examples__shop__cart__portolan__stores.json +208 -0
  816. package/src/testing/estate/examples__shop__oms__portolan__glossary.json +98 -0
  817. package/src/testing/estate/examples__shop__oms__portolan__proto.json +186 -0
  818. package/src/testing/estate.ts +49 -0
  819. package/src/trail/Trail.tsx +81 -0
  820. package/src/trail/model.test.ts +157 -0
  821. package/src/trail/model.ts +189 -0
  822. package/src/trail/record.tsx +32 -0
  823. package/src/trail/store.test.ts +66 -0
  824. package/src/trail/store.ts +102 -0
  825. package/tsconfig.json +35 -0
  826. package/vite.config.ts +171 -0
@@ -0,0 +1,1278 @@
1
+ """What happens when somebody calls in, or when an event arrives.
2
+
3
+ Statements are read in source order. A call into the ORM is a hop to the store,
4
+ an event handed to anything is a hop to the bus, a call on a vendored client is
5
+ a hop to the peer. An `if` becomes an alt whose branch is terminal when it ends
6
+ in a return or a raise; a loop, a transaction and an except are a note on the
7
+ steps inside them. Every step is `declared`: this reads code, and code is a
8
+ claim about behaviour, not a record of it.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import ast
14
+ from dataclasses import dataclass, field as dc_field
15
+ from typing import Dict, List, Optional, Tuple
16
+ from urllib.parse import urlparse
17
+
18
+ import catalog
19
+ from clients import Client
20
+ from domain import Aggregate, ModelDef
21
+ from ids import pascal, slug
22
+ from operations import UseCase
23
+ import celery_conf
24
+ import celery_tasks
25
+ import kafka
26
+ from source import Module, Project, assigned, doc, dotted, keyword, keyword_str, methods
27
+
28
+ LANE_CLIENT = "client"
29
+ LANE_BUS = "bus"
30
+
31
+ # What an enqueue looks like: the call, and the signature it may go through.
32
+ # The queue it lands on is decided the way Celery decides it, by the same
33
+ # reader `extract-celery` uses, so the lane here is the one its flow draws.
34
+ ENQUEUE = {"delay", "apply_async"}
35
+ SIGNATURE = {"s", "si", "signature", "subtask"}
36
+
37
+ # A call on the ORM that goes to the database. `objects.<anything>` does too,
38
+ # and is caught by the manager rather than by this list.
39
+ STORE_METHODS = {"save", "delete", "refresh_from_db", "update", "full_clean_and_save"}
40
+ # What a publish looks like. A signal's `send` is one; so is anything handed an
41
+ # event, which is the rule that catches a project's own `publish()` helper.
42
+ SEND_METHODS = {"send", "send_robust", "publish"}
43
+ # A publish with an address: the topic, subject or channel goes first, and is
44
+ # read as far as syntax carries it - a literal, a module constant, a setting.
45
+ # `KafkaProducer.send`, confluent's `produce`, NATS and Redis `publish`,
46
+ # Channels' `group_send`.
47
+ PRODUCE_METHODS = {"send", "produce", "publish", "group_send"}
48
+ # Django's own model signals: a hook on the row, not an event. A receiver on
49
+ # one is reported, not drawn.
50
+ ORM_SIGNALS = {"pre_init", "post_init", "pre_save", "post_save", "pre_delete", "post_delete", "m2m_changed", "pre_migrate", "post_migrate"}
51
+ HTTP_LIBRARIES = {"requests", "httpx", "aiohttp"}
52
+ HTTP_METHODS = {"get", "post", "put", "patch", "delete", "head", "options"}
53
+ MAX_CALL_DEPTH = 5
54
+
55
+
56
+ def sender_of(decorator: ast.Call) -> str:
57
+ """`sender=Invoice`, or the models listed, or every model when none is named."""
58
+ value = keyword(decorator, "sender")
59
+ if value is None:
60
+ return "every model"
61
+ if isinstance(value, (ast.List, ast.Tuple)):
62
+ return ", ".join(dotted(v) for v in value.elts if dotted(v)) or "every model"
63
+ return dotted(value) or "every model"
64
+
65
+
66
+ @dataclass
67
+ class Options:
68
+ context: str
69
+ svc_id: str
70
+ service: str
71
+ store: str
72
+ stores: Dict[str, str] = dc_field(default_factory=dict)
73
+ peers: Dict[str, str] = dc_field(default_factory=dict)
74
+ events: Dict[str, str] = dc_field(default_factory=dict)
75
+ flow_wrappers: Dict[str, object] = dc_field(default_factory=dict)
76
+ settings: str = "" # the Django settings module Celery is configured from
77
+
78
+
79
+ class Draft:
80
+ """One flow being built: the lanes in the order they are first used, and
81
+ the steps, which nest when a branch is open."""
82
+
83
+ def __init__(self) -> None:
84
+ self.lanes: List[Dict[str, object]] = []
85
+ self.steps: List[Dict[str, object]] = []
86
+ self.sinks: List[List[Dict[str, object]]] = []
87
+ self.n = 0
88
+ self.notes: List[str] = []
89
+ self.technical_wrappers: Dict[str, Tuple[Dict[str, object], int]] = {}
90
+
91
+ def lane(self, id_: str, kind: str, context: Optional[str], label: str = "") -> str:
92
+ if not any(l["id"] == id_ for l in self.lanes):
93
+ self.lanes.append(catalog.participant(id_, kind, context, label))
94
+ return id_
95
+
96
+ def sink(self) -> List[Dict[str, object]]:
97
+ return self.sinks[-1] if self.sinks else self.steps
98
+
99
+ def push(self) -> None:
100
+ self.sinks.append([])
101
+
102
+ def pop(self) -> List[Dict[str, object]]:
103
+ return self.sinks.pop() if self.sinks else []
104
+
105
+ def enter(self, note: str) -> None:
106
+ self.notes.append(note)
107
+
108
+ def leave(self) -> None:
109
+ self.notes.pop()
110
+
111
+ def note(self, own: str) -> str:
112
+ outer = []
113
+ for note in self.notes:
114
+ if note and note not in outer:
115
+ outer.append(note)
116
+ prefix = ", ".join(outer) + "." if outer else ""
117
+ return (prefix + " " + own).strip()
118
+
119
+ def add(
120
+ self,
121
+ from_: str,
122
+ to: str,
123
+ kind: str,
124
+ label: str,
125
+ status: str = catalog.DECLARED,
126
+ ref: str = "",
127
+ note: str = "",
128
+ line: str = "",
129
+ continues_at: str = "",
130
+ handoff: Optional[Dict[str, str]] = None,
131
+ ) -> None:
132
+ self.n += 1
133
+ self.sink().append(
134
+ catalog.step(
135
+ "s%d" % self.n,
136
+ from_,
137
+ to,
138
+ kind,
139
+ label,
140
+ status,
141
+ ref,
142
+ self.note(note),
143
+ line,
144
+ continues_at,
145
+ handoff,
146
+ )
147
+ )
148
+
149
+ def add_alt(self, branches: List[Dict[str, object]]) -> None:
150
+ self.n += 1
151
+ self.sink().append(catalog.alt("alt%d" % self.n, branches))
152
+
153
+ def add_wrapper(self, key: str, label: str, target: str, technical: bool, service: str, line: str) -> None:
154
+ """Keep a configured project boundary semantic and compact repeated
155
+ infrastructure calls without discarding the fact that they happen."""
156
+ if technical and key in self.technical_wrappers:
157
+ step, count = self.technical_wrappers[key]
158
+ count += 1
159
+ self.technical_wrappers[key] = (step, count)
160
+ step["label"] = "%s ×%d" % (label, count)
161
+ step["note"] = "Project wrapper `%s`; %d call sites are collapsed in this flow." % (key, count)
162
+ return
163
+ to = target or service
164
+ kind = "rpc" if target and target != service else "call"
165
+ self.add(
166
+ service,
167
+ to,
168
+ kind,
169
+ label,
170
+ note="Project wrapper `%s`.%s" % (key, " Technical calls are collapsed." if technical else ""),
171
+ line=line,
172
+ )
173
+ if technical:
174
+ self.technical_wrappers[key] = (self.sink()[-1], 1)
175
+
176
+
177
+ def sentence(text: str) -> str:
178
+ words = text.replace("-", " ").replace("_", " ").strip()
179
+ return words[0].upper() + words[1:] if words else ""
180
+
181
+
182
+ def condition(node: ast.AST) -> str:
183
+ try:
184
+ text = ast.unparse(node) # type: ignore[attr-defined]
185
+ except Exception:
186
+ return ""
187
+ text = " ".join(text.split())
188
+ return text if len(text) <= 72 else text[:69] + "..."
189
+
190
+
191
+ @dataclass(frozen=True)
192
+ class ClassTarget:
193
+ module: Module
194
+ node: ast.ClassDef
195
+
196
+
197
+ @dataclass(frozen=True)
198
+ class CallableTarget:
199
+ module: Module
200
+ node: ast.AST
201
+ owner: Optional[ClassTarget] = None
202
+
203
+ @property
204
+ def key(self) -> Tuple[str, str, str]:
205
+ return (self.module.dotted, self.owner.node.name if self.owner is not None else "", getattr(self.node, "name", ""))
206
+
207
+ @property
208
+ def qualified(self) -> str:
209
+ parts = [self.module.dotted]
210
+ if self.owner is not None:
211
+ parts.append(self.owner.node.name)
212
+ parts.append(getattr(self.node, "name", ""))
213
+ return ".".join(part for part in parts if part)
214
+
215
+
216
+ class Frame:
217
+ """What a name means inside one body being walked."""
218
+
219
+ def __init__(
220
+ self,
221
+ module: Module,
222
+ variables: Optional[Dict[str, Tuple[str, object]]] = None,
223
+ owner: Optional[ClassTarget] = None,
224
+ active: Tuple[Tuple[str, str, str], ...] = (),
225
+ ):
226
+ self.module = module
227
+ self.vars: Dict[str, Tuple[str, object]] = dict(variables or {})
228
+ self.returned: Optional[Tuple[str, object]] = None
229
+ self.owner = owner
230
+ self.active = active
231
+
232
+
233
+ class FlowReader:
234
+ def __init__(self, opts: Options, project: Project, aggregates: List[Aggregate], models: List[ModelDef], use_cases: List[UseCase], clients: List[Client], events: Dict[str, object], rel, b):
235
+ self.opts = opts
236
+ self.project = project
237
+ self.aggregates = aggregates
238
+ self.rel = rel
239
+ self.b = b
240
+ self.calls: Dict[str, Dict[str, object]] = {}
241
+ self.referenced = set()
242
+ self.models: Dict[str, ModelDef] = {}
243
+ self.models_by_name: Dict[str, List[ModelDef]] = {}
244
+ self.emitters: Dict[Tuple[str, str], str] = {} # (model, method) -> event id
245
+ seen_models = set()
246
+ for model in models:
247
+ key = (model.module.dotted, model.name)
248
+ if key in seen_models:
249
+ continue
250
+ seen_models.add(key)
251
+ self.models_by_name.setdefault(model.name, []).append(model)
252
+ for agg in aggregates:
253
+ for model in agg.models:
254
+ key = (model.module.dotted, model.name)
255
+ if key in seen_models:
256
+ continue
257
+ seen_models.add(key)
258
+ self.models_by_name.setdefault(model.name, []).append(model)
259
+ for name, candidates in self.models_by_name.items():
260
+ self.models[name] = candidates[0]
261
+ self.use_cases: Dict[str, UseCase] = {}
262
+ for use_case in use_cases:
263
+ self.use_cases[use_case.key] = use_case
264
+ self.clients: Dict[str, Client] = {c.name: c for c in clients}
265
+ self.events = events # class or signal name -> EventDef
266
+ self._warned_store = False
267
+ self._warned_peers = set()
268
+ self._celery: Optional[Tuple[celery_conf.Config, Dict[Tuple[str, str], celery_tasks.Task]]] = None
269
+ self.produced: Dict[str, List[Tuple[str, str]]] = {} # event id -> (address, line) it was put on the wire at
270
+ self._settings: Optional[Module] = None
271
+ self._warned_kafka_topics = set()
272
+ self.classes: Dict[Tuple[str, str], ClassTarget] = {}
273
+ self.functions: Dict[Tuple[str, str], CallableTarget] = {}
274
+ for module in project.modules.values():
275
+ for node in module.classes():
276
+ target = ClassTarget(module, node)
277
+ self.classes[(module.dotted, node.name)] = target
278
+ for node in module.functions():
279
+ self.functions[(module.dotted, node.name)] = CallableTarget(module, node)
280
+
281
+ # --- lanes ---------------------------------------------------------------
282
+
283
+ def service_lane(self, d: Draft) -> str:
284
+ return d.lane(self.opts.svc_id, "service", self.opts.context)
285
+
286
+ def store_lane(self, d: Draft, alias: str = "default") -> str:
287
+ store = self.opts.stores.get(alias, self.opts.store if alias == "default" else "")
288
+ if not store:
289
+ if not self._warned_store:
290
+ self.b.warn(self.opts.svc_id, "no readable database configuration or `store` option, so calls into the ORM stay on the service's own lane")
291
+ self._warned_store = True
292
+ return self.opts.svc_id
293
+ label = "" if alias == "default" else alias
294
+ return d.lane("%s-%s" % (self.opts.service, store), "store", self.opts.context, label)
295
+
296
+ def peer_lane(self, d: Draft, pkg: str) -> Tuple[str, str, str]:
297
+ service = self.opts.peers.get(pkg, "")
298
+ if service:
299
+ context = service.split(".")[0]
300
+ return d.lane(service, "service", context), service, catalog.DECLARED
301
+ if pkg not in self._warned_peers:
302
+ self._warned_peers.add(pkg)
303
+ self.b.warn(
304
+ self.opts.svc_id,
305
+ "calls %s and the manifest names no peer for it; add it under `peers` to say which service answers, until then the calls are unresolved" % pkg,
306
+ )
307
+ return d.lane(pkg.replace(".", "-"), "unknown", None, pkg), pkg, catalog.UNRESOLVED
308
+
309
+ def consumes(self) -> List[Dict[str, object]]:
310
+ return [self.calls[key] for key in sorted(self.calls)]
311
+
312
+ # --- the two openings ----------------------------------------------------
313
+
314
+ def endpoint_flow(self, endpoint, serializer=None, model: Optional[ModelDef] = None) -> Optional[Dict[str, object]]:
315
+ d = Draft()
316
+ d.lane(LANE_CLIENT, "actor", None)
317
+ self.service_lane(d)
318
+ d.add(LANE_CLIENT, self.opts.svc_id, "rpc", endpoint.id, line=endpoint.module.where(endpoint.node))
319
+ ran: List[UseCase] = []
320
+ if isinstance(endpoint.node, ast.ClassDef):
321
+ self.inherited_endpoint(d, endpoint, serializer, model)
322
+ else:
323
+ owner = self.class_target(endpoint.module, endpoint.view) if endpoint.view else None
324
+ frame = Frame(endpoint.module, self.attributes(endpoint), owner=owner)
325
+ self.walk(d, frame, endpoint.node.body, 0, ran)
326
+ endpoint.use_cases = [u.key for u in ran]
327
+ name = slug(endpoint.id)
328
+ ident = "%s-%s" % (self.opts.service, name)
329
+ return catalog.flow(
330
+ "flow." + ident,
331
+ ident,
332
+ sentence(name),
333
+ ran[-1].doc if ran else endpoint.doc,
334
+ endpoint.module.rel,
335
+ self.opts.context,
336
+ d.lanes,
337
+ d.steps,
338
+ )
339
+
340
+ def inherited_endpoint(self, d: Draft, endpoint, serializer, model: Optional[ModelDef]) -> None:
341
+ """Materialise the behaviour DRF supplies for inherited CRUD actions.
342
+
343
+ The action itself is known from the concrete generic base. Persistence
344
+ is drawn only when ``queryset`` or serializer metadata proves a model;
345
+ the extractor never invents a table from the URL or view name.
346
+ """
347
+ action = endpoint.action
348
+ serializer_name = getattr(serializer, "name", "")
349
+ framework = next(
350
+ (
351
+ dotted(base).split(".")[-1]
352
+ for base in endpoint.node.bases
353
+ if dotted(base).split(".")[-1].endswith(("APIView", "ViewSet"))
354
+ ),
355
+ "generic view",
356
+ )
357
+ source = endpoint.module.where(endpoint.node)
358
+ model_note = "Model resolved from queryset or serializer metadata." if model is not None else "No model is declared by queryset or serializer metadata."
359
+ note = "Supplied by DRF %s. %s" % (framework, model_note)
360
+
361
+ if action in ("retrieve", "update", "partial_update", "destroy") and model is not None:
362
+ d.add(self.opts.svc_id, self.store_lane(d), "call", "%s.objects.get" % model.name, note=note, line=source)
363
+ elif action == "list" and model is not None:
364
+ d.add(self.opts.svc_id, self.store_lane(d), "call", "%s.objects.all" % model.name, note=note, line=source)
365
+
366
+ if action in ("create", "update", "partial_update") and serializer_name:
367
+ suffix = " (partial)" if action == "partial_update" else ""
368
+ d.add(self.opts.svc_id, self.opts.svc_id, "call", "Validate %s%s" % (serializer_name, suffix), note=note, line=source)
369
+
370
+ if model is not None:
371
+ if action == "create":
372
+ d.add(self.opts.svc_id, self.store_lane(d), "call", "%s.objects.create" % model.name, note=note, line=source)
373
+ elif action in ("update", "partial_update"):
374
+ d.add(self.opts.svc_id, self.store_lane(d), "call", "%s.save" % model.name, note=note, line=source)
375
+ elif action == "destroy":
376
+ d.add(self.opts.svc_id, self.store_lane(d), "call", "%s.delete" % model.name, note=note, line=source)
377
+
378
+ if action in ("list", "retrieve") and serializer_name:
379
+ suffix = " collection" if action == "list" else ""
380
+ d.add(self.opts.svc_id, self.opts.svc_id, "call", "Serialize %s%s" % (serializer_name, suffix), note=note, line=source)
381
+
382
+ def policy_flow(self, agg: Aggregate, module: Module, node: ast.AST, decorator: ast.Call) -> Optional[Dict[str, object]]:
383
+ signal = dotted(decorator.args[0]).split(".")[-1] if decorator.args else ""
384
+ if signal in ORM_SIGNALS:
385
+ # Not a policy on an event: a hook on the row. It says nothing
386
+ # about what happened, and it fires for every save - migrations,
387
+ # fixtures and the admin included - so it is reported rather than
388
+ # drawn as a flow nothing in the domain triggers.
389
+ self.b.warn(
390
+ module.where(node),
391
+ "%s runs on %s of %s: a policy hanging on a persistence hook rather than a domain event - nothing says what happened, and it fires for any save, migrations and fixtures included"
392
+ % (node.name, signal, sender_of(decorator)),
393
+ )
394
+ return None
395
+ trigger = self.trigger(module, decorator)
396
+ if trigger is None:
397
+ self.b.warn(
398
+ self.opts.svc_id,
399
+ "%s: %s receives a signal this repository does not declare and the manifest's `events` does not place; the flow is left out" % (module.rel, node.name),
400
+ )
401
+ return None
402
+ ref, label, status = trigger
403
+ d = Draft()
404
+ d.lane(LANE_BUS, "broker", None)
405
+ self.service_lane(d)
406
+ d.add(LANE_BUS, self.opts.svc_id, "event", label, status, ref if status == catalog.DECLARED else "", line=module.where(node))
407
+ if status == catalog.DECLARED:
408
+ self.referenced.add(ref)
409
+ self.walk(d, Frame(module, self.module_env(module)), node.body, 0, [])
410
+ ident = "%s-%s" % (self.opts.service, slug(node.name))
411
+ return catalog.flow(
412
+ "flow." + ident,
413
+ ident,
414
+ sentence(slug(node.name)),
415
+ doc(node),
416
+ module.rel,
417
+ self.opts.context,
418
+ d.lanes,
419
+ d.steps,
420
+ )
421
+
422
+ def task_flows(self) -> List[Dict[str, object]]:
423
+ """Source-backed continuations for Celery worker bodies.
424
+
425
+ The Celery extractor owns transport truth. These fragments start at a
426
+ shared source seam, so enrichment appends observable task work after
427
+ the worker receive without replaying the enqueue.
428
+ """
429
+ out = []
430
+ for task in celery_tasks.read_tasks(self.project):
431
+ d = Draft()
432
+ self.service_lane(d)
433
+ self.walk(d, Frame(task.module, self.module_env(task.module)), task.node.body, 0, [])
434
+ if not d.steps:
435
+ continue
436
+ ident = "%s-celery-body-%s" % (self.opts.service, slug(task.name))
437
+ out.append(
438
+ catalog.flow(
439
+ "flow." + ident,
440
+ ident,
441
+ sentence(task.short) + " work",
442
+ (task.doc + " Observable work performed by the Celery task.").strip(),
443
+ task.module.rel,
444
+ self.opts.context,
445
+ d.lanes,
446
+ d.steps,
447
+ entrypoint=task.entrypoint,
448
+ )
449
+ )
450
+ return out
451
+
452
+ def trigger(self, module: Module, decorator: ast.Call) -> Optional[Tuple[str, str, str]]:
453
+ """What a receiver reacts to: the signal it is given, resolved to an
454
+ event of this service, or placed by the manifest's `events`."""
455
+ if not decorator.args:
456
+ return None
457
+ name = dotted(decorator.args[0]).split(".")[-1]
458
+ local = self.events.get(name)
459
+ if local is not None:
460
+ return local.id, local.name, catalog.DECLARED
461
+ imported = module.imports.get(name)
462
+ if imported is not None:
463
+ for prefix, aggregate in sorted(self.opts.events.items()):
464
+ if imported.module == prefix or imported.module.startswith(prefix + "."):
465
+ return "%s.%s" % (aggregate, pascal(name)), pascal(name), catalog.DECLARED
466
+ return None
467
+
468
+ def attributes(self, endpoint) -> Dict[str, Tuple[str, object]]:
469
+ """`self.pricing = PricingClient()` in the view, and the same at module
470
+ level: what a handler can reach without being handed it."""
471
+ out: Dict[str, Tuple[str, object]] = self.module_env(endpoint.module)
472
+ for node in endpoint.module.classes():
473
+ if node.name != endpoint.view:
474
+ continue
475
+ for name, value, _ in assigned(node):
476
+ binding = self.constructed(endpoint.module, value)
477
+ if binding is not None:
478
+ out["self." + name] = binding
479
+ init = None
480
+ for m in methods(node):
481
+ if m.name == "__init__":
482
+ init = m
483
+ if init is not None:
484
+ for stmt in ast.walk(init):
485
+ if isinstance(stmt, ast.Assign) and len(stmt.targets) == 1 and isinstance(stmt.targets[0], ast.Attribute):
486
+ binding = self.constructed(endpoint.module, stmt.value)
487
+ if binding is not None:
488
+ out["self." + stmt.targets[0].attr] = binding
489
+ return out
490
+
491
+ def constructed(self, module: Module, value: ast.AST, variables=None) -> Optional[Tuple[str, object]]:
492
+ if not isinstance(value, ast.Call):
493
+ return None
494
+ name = dotted(value.func).split(".")[-1]
495
+ if name in self.clients:
496
+ return ("client", self.clients[name])
497
+ external = self.external_name(module, dotted(value.func))
498
+ if external in ("requests.Session", "httpx.Client", "httpx.AsyncClient", "aiohttp.ClientSession"):
499
+ return ("http", external.split(".", 1)[0])
500
+ kafka_client = kafka.constructed(self.project, module, value, self.opts.settings, variables)
501
+ if kafka_client is not None:
502
+ return ("kafka", kafka_client)
503
+ target = self.resolve_class(module, dotted(value.func))
504
+ if target is not None:
505
+ return ("object", target)
506
+ return None
507
+
508
+ # --- the walk ------------------------------------------------------------
509
+
510
+ def walk(self, d: Draft, frame: Frame, body: List[ast.AST], depth: int, ran: List[UseCase]) -> None:
511
+ for stmt in body:
512
+ self.statement(d, frame, stmt, depth, ran)
513
+
514
+ def statement(self, d: Draft, frame: Frame, stmt: ast.AST, depth: int, ran: List[UseCase]) -> None:
515
+ if isinstance(stmt, ast.Assign):
516
+ binding = self.value(d, frame, stmt.value, depth, ran)
517
+ for target in stmt.targets:
518
+ self.bind(frame, target, binding)
519
+ elif isinstance(stmt, ast.AnnAssign) and stmt.value is not None:
520
+ self.bind(frame, stmt.target, self.value(d, frame, stmt.value, depth, ran))
521
+ elif isinstance(stmt, ast.Expr):
522
+ self.value(d, frame, stmt.value, depth, ran)
523
+ elif isinstance(stmt, ast.Return):
524
+ if stmt.value is not None:
525
+ frame.returned = self.value(d, frame, stmt.value, depth, ran)
526
+ elif isinstance(stmt, ast.If):
527
+ self.choice(d, frame, stmt, depth, ran)
528
+ elif isinstance(stmt, (ast.For, ast.AsyncFor)):
529
+ # Creating or advancing an iterable may itself perform the
530
+ # observable work: project wrappers commonly return generators
531
+ # around streaming HTTP responses.
532
+ self.value(d, frame, stmt.iter, depth, ran)
533
+ d.enter("for each %s" % condition(stmt.target))
534
+ self.walk(d, frame, stmt.body, depth, ran)
535
+ d.leave()
536
+ elif isinstance(stmt, ast.While):
537
+ d.enter("while %s" % condition(stmt.test))
538
+ self.walk(d, frame, stmt.body, depth, ran)
539
+ d.leave()
540
+ elif isinstance(stmt, (ast.With, ast.AsyncWith)):
541
+ atomic = any(dotted(item.context_expr).split(".")[-1] == "atomic" for item in stmt.items)
542
+ d.enter("in one transaction" if atomic else "")
543
+ self.walk(d, frame, stmt.body, depth, ran)
544
+ d.leave()
545
+ elif isinstance(stmt, ast.Try):
546
+ self.walk(d, frame, stmt.body, depth, ran)
547
+ for handler in stmt.handlers:
548
+ d.enter("on %s" % (condition(handler.type) if handler.type is not None else "failure"))
549
+ self.walk(d, frame, handler.body, depth, ran)
550
+ d.leave()
551
+ self.walk(d, frame, stmt.orelse, depth, ran)
552
+ self.walk(d, frame, stmt.finalbody, depth, ran)
553
+ elif isinstance(stmt, ast.Raise):
554
+ return
555
+ elif isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef)):
556
+ frame.vars[stmt.name] = ("callable", CallableTarget(frame.module, stmt, frame.owner))
557
+
558
+ def choice(self, d: Draft, frame: Frame, stmt: ast.If, depth: int, ran: List[UseCase]) -> None:
559
+ branches = []
560
+ current: Optional[ast.AST] = stmt
561
+ while isinstance(current, ast.If):
562
+ d.push()
563
+ self.walk(d, frame, current.body, depth, ran)
564
+ steps = d.pop()
565
+ branches.append(catalog.branch(condition(current.test), steps, terminal(current.body)))
566
+ rest = current.orelse
567
+ if len(rest) == 1 and isinstance(rest[0], ast.If):
568
+ current = rest[0]
569
+ continue
570
+ if rest:
571
+ d.push()
572
+ self.walk(d, frame, rest, depth, ran)
573
+ branches.append(catalog.branch("otherwise", d.pop(), terminal(rest)))
574
+ current = None
575
+ if any(branch["steps"] for branch in branches):
576
+ if len(branches) == 1:
577
+ branches.append(catalog.branch("otherwise", []))
578
+ d.add_alt(branches)
579
+
580
+ def bind(self, frame: Frame, target: ast.AST, binding) -> None:
581
+ if binding is None:
582
+ return
583
+ if isinstance(target, ast.Name):
584
+ frame.vars[target.id] = binding
585
+ elif isinstance(target, ast.Attribute) and dotted(target):
586
+ frame.vars[dotted(target)] = binding
587
+ elif isinstance(target, (ast.Tuple, ast.List)) and isinstance(binding, tuple) and binding[0] == "many":
588
+ for element, one in zip(target.elts, binding[1]):
589
+ self.bind(frame, element, one)
590
+
591
+ def lookup(self, frame: Frame, node: ast.AST):
592
+ name = dotted(node)
593
+ if not name:
594
+ return None
595
+ if name in frame.vars:
596
+ return frame.vars[name]
597
+ base = name.split(".")[0]
598
+ found = frame.vars.get(base)
599
+ if found is not None:
600
+ return found
601
+ model = self.model_for(frame.module, name)
602
+ return ("model", model) if model is not None else None
603
+
604
+ def value(self, d: Draft, frame: Frame, node: ast.AST, depth: int, ran: List[UseCase]):
605
+ """What an expression holds, and every hop it makes on the way."""
606
+ if isinstance(node, ast.Await):
607
+ return self.value(d, frame, node.value, depth, ran)
608
+ if isinstance(node, (ast.Tuple, ast.List)):
609
+ return ("many", [self.value(d, frame, element, depth, ran) for element in node.elts])
610
+ if isinstance(node, (ast.Name, ast.Attribute)):
611
+ binding = self.lookup(frame, node)
612
+ if binding is not None:
613
+ return binding
614
+ text = self.string_value(frame.module, node)
615
+ return ("string", text) if text else None
616
+ if not isinstance(node, ast.Call):
617
+ text = self.string_value(frame.module, node)
618
+ if text:
619
+ return ("string", text)
620
+ resolved = kafka.value(self.project, frame.module, node, self.opts.settings, frame.vars)
621
+ return ("value", resolved) if resolved is not None else None
622
+
623
+ # The receiver first, then the arguments: a chain is read the way it is
624
+ # written, so `Invoice.objects.filter(...).first()` makes its query
625
+ # where the queryset is built rather than nowhere at all.
626
+ holder = None
627
+ if isinstance(node.func, ast.Attribute):
628
+ inner = node.func.value
629
+ holder = self.value(d, frame, inner, depth, ran) if isinstance(inner, ast.Call) else self.lookup(frame, inner)
630
+ name = dotted(node.func)
631
+ last = name.split(".")[-1]
632
+ line = frame.module.where(node)
633
+
634
+ # A Kafka call is recognized from the constructed client type, not the
635
+ # method name alone. Handle it before walking serialization arguments:
636
+ # those expressions describe the payload and are not extra publishes.
637
+ if holder is not None and holder[0] == "kafka":
638
+ client: kafka.Client = holder[1]
639
+ if client.role == "producer" and last in kafka.PUBLISH_METHODS.get(client.library, set()):
640
+ topic_node = kafka.topic_node(node)
641
+ address = kafka.value(self.project, frame.module, topic_node, self.opts.settings, frame.vars)
642
+ if isinstance(address, str) and address:
643
+ payload = kafka.payload_node(node)
644
+ event_id = ""
645
+ if payload is not None:
646
+ for child in ast.walk(payload):
647
+ if not isinstance(child, (ast.Name, ast.Attribute)):
648
+ continue
649
+ binding = self.lookup(frame, child)
650
+ if binding is not None and binding[0] == "event":
651
+ event_id = binding[1]
652
+ break
653
+ message = event_id.rsplit(".", 1)[-1] if event_id else kafka.payload_name(payload, frame.vars)
654
+ lane = d.lane("kafka-" + slug(address), "broker", None, "Kafka · " + address)
655
+ detail = [kafka.config_note(client)]
656
+ if payload is not None:
657
+ detail.append("payload `%s`" % kafka.expression(payload))
658
+ key = keyword(node, "key")
659
+ headers = keyword(node, "headers")
660
+ if key is not None:
661
+ detail.append("key `%s`" % kafka.expression(key))
662
+ if headers is not None:
663
+ detail.append("headers `%s`" % kafka.expression(headers))
664
+ d.add(
665
+ self.opts.svc_id,
666
+ lane,
667
+ "event" if event_id else "call",
668
+ "publish " + message,
669
+ ref=event_id,
670
+ note="; ".join(detail),
671
+ line=line,
672
+ handoff={
673
+ "kind": "message",
674
+ "transport": "kafka",
675
+ "channel": address,
676
+ "message": message,
677
+ "direction": "send",
678
+ },
679
+ )
680
+ if event_id:
681
+ self.referenced.add(event_id)
682
+ self.produced.setdefault(event_id, []).append((address, line))
683
+ elif line not in self._warned_kafka_topics:
684
+ self._warned_kafka_topics.add(line)
685
+ self.b.warn(line, "Kafka topic is dynamic%s, so no flow step is invented" % ((": `%s`" % kafka.expression(topic_node)) if topic_node is not None else ""))
686
+ return None
687
+
688
+ positional = [self.value(d, frame, arg, depth, ran) for arg in node.args]
689
+ named = {kw.arg: self.value(d, frame, kw.value, depth, ran) for kw in node.keywords if kw.arg}
690
+ args = positional + list(named.values())
691
+
692
+ if self.external_name(frame.module, name) == "urllib.parse.urljoin" and len(node.args) >= 2:
693
+ left = binding_string(positional[0]) or self.string_value(frame.module, node.args[0])
694
+ right = binding_string(positional[1]) or self.string_value(frame.module, node.args[1])
695
+ text = left.rstrip("/") + "/" + right.lstrip("/") if left or right else ""
696
+ if text:
697
+ return ("string", text)
698
+
699
+ # A use case of this service: its steps are these steps. Looked for
700
+ # first, so a use case handed an event is followed rather than read as
701
+ # a publish of it.
702
+ use_case = self.use_case(frame.module, name)
703
+ if use_case is not None:
704
+ return self.inline(d, use_case, positional, named, depth, ran, line)
705
+
706
+ # A publish with an address, or a project's own helper that makes one
707
+ # when handed an event: the event leaves for the bus, and the step
708
+ # says where. Read before the plain event rule so the address is
709
+ # not lost to it.
710
+ event = next((a for a in args if isinstance(a, tuple) and a[0] == "event"), None)
711
+ if holder is None or holder[0] not in ("client", "model", "model-store"):
712
+ address = ""
713
+ if last in PRODUCE_METHODS and node.args:
714
+ address = self.address_of(frame.module, node.args[0])
715
+ if not address and event is not None:
716
+ address = self.helper_address(frame.module, name)
717
+ if address:
718
+ self.produce(d, address, event[1] if event is not None else "", line)
719
+ return None
720
+
721
+ # Work handed to Celery is a hop off the request. `on_commit` around
722
+ # it is the one fact about when the message leaves that the code
723
+ # states plainly, so the lambda or partial it holds is read inside a
724
+ # note rather than skipped.
725
+ if last == "on_commit":
726
+ d.enter("after the transaction commits")
727
+ for arg in node.args:
728
+ self.deferred(d, frame, arg, depth, ran)
729
+ d.leave()
730
+ return None
731
+ if last in ENQUEUE and isinstance(node.func, ast.Attribute):
732
+ task = self.task_of(frame.module, node.func.value)
733
+ if task is not None:
734
+ self.enqueue(d, task, keyword_str(node, "queue"), line)
735
+ return None
736
+
737
+ # A project helper is evidence stronger than its call shape. Follow it
738
+ # before the generic "an event was handed to something" fallback, so a
739
+ # formatter or validator receiving an event is not invented as a
740
+ # publish operation.
741
+ called = self.callable_target(frame, node.func, holder)
742
+ if called is not None and (holder is None or holder[0] == "object"):
743
+ wrapper = self.wrapper(called)
744
+ if wrapper is not None:
745
+ self.wrapper_step(d, called, wrapper, line)
746
+ return None
747
+ return self.inline_callable(d, frame, called, positional, named, depth, ran)
748
+
749
+ # An event handed to anything is the event leaving for the bus, which
750
+ # is the rule that catches a project's own `publish()` helper as well
751
+ # as a signal's `send`. A list it is being collected into is not one.
752
+ event = next((a for a in args if isinstance(a, tuple) and a[0] == "event"), None)
753
+ if event is not None and last not in ("append", "extend"):
754
+ self.publish(d, event[1], line)
755
+ return None
756
+ if last in SEND_METHODS and "." in name:
757
+ found = self.events.get(name.rsplit(".", 1)[0].split(".")[-1])
758
+ if found is not None:
759
+ self.publish(d, found.id, line)
760
+ return None
761
+
762
+ # An event, a model or a client, constructed.
763
+ if last in self.events and isinstance(node.func, (ast.Name, ast.Attribute)):
764
+ return ("event", self.events[last].id)
765
+ model = self.model_for(frame.module, name)
766
+ if model is not None and ".objects." not in name:
767
+ return ("model", model)
768
+ if last in self.clients:
769
+ return ("client", self.clients[last])
770
+ constructed = self.constructed(frame.module, node, frame.vars)
771
+ if constructed is not None:
772
+ return constructed
773
+
774
+ # The ORM: a manager on a model class, or a write on an instance.
775
+ parts = name.split(".")
776
+ manager_model = self.model_for(frame.module, ".".join(parts[:-2])) if len(parts) >= 3 and parts[-2] == "objects" else None
777
+ if manager_model is not None:
778
+ if last in ("using", "db_manager"):
779
+ alias = binding_string(positional[0]) if positional else ""
780
+ return ("model-store", (manager_model, alias or "default"))
781
+ d.add(self.opts.svc_id, self.store_lane(d), "call", "%s.objects.%s" % (parts[-3], last), line=line)
782
+ return ("model", manager_model)
783
+ if holder is not None and holder[0] == "model-store":
784
+ model, alias = holder[1]
785
+ d.add(self.opts.svc_id, self.store_lane(d, alias), "call", "%s.objects.%s" % (model.name, last), line=line)
786
+ return ("model", model)
787
+ if holder is not None and holder[0] == "model":
788
+ model: ModelDef = holder[1]
789
+ if last in STORE_METHODS or (len(parts) >= 2 and parts[-2] == "objects"):
790
+ alias = binding_string(named.get("using")) or "default"
791
+ d.add(self.opts.svc_id, self.store_lane(d, alias), "call", "%s.%s" % (model.name, last), line=line)
792
+ return holder
793
+ emitted = self.emits(model, last)
794
+ if emitted:
795
+ return ("event", emitted)
796
+ if holder is not None and holder[0] == "client":
797
+ return self.rpc(d, holder[1], last, line)
798
+
799
+ http = self.http_call(frame.module, node, holder, positional)
800
+ if http is not None:
801
+ method, target = http
802
+ self.http_step(d, method, target, line)
803
+ return None
804
+
805
+ called = self.callable_target(frame, node.func, holder)
806
+ if called is not None:
807
+ wrapper = self.wrapper(called)
808
+ if wrapper is not None:
809
+ self.wrapper_step(d, called, wrapper, line)
810
+ return None
811
+ return self.inline_callable(d, frame, called, positional, named, depth, ran)
812
+
813
+ if holder is not None and holder[0] == "model":
814
+ return holder
815
+
816
+ return None
817
+
818
+ def inline(self, d: Draft, use_case: UseCase, positional, named, depth: int, ran: List[UseCase], line: str):
819
+ """A use case runs here, so its steps are drawn here - two deep at
820
+ most, past which the call itself is the step."""
821
+ if use_case not in ran:
822
+ ran.append(use_case)
823
+ if depth >= 2:
824
+ d.add(self.opts.svc_id, self.opts.svc_id, "call", use_case.id, note="Runs the use case, whose steps are not drawn again here.", line=line)
825
+ return None
826
+ inner = Frame(use_case.module, self.module_env(use_case.module))
827
+ self.bind_arguments(inner, use_case.node, positional, named, False)
828
+ self.walk(d, inner, use_case.node.body, depth + 1, ran)
829
+ return inner.returned
830
+
831
+ def inline_callable(self, d: Draft, caller: Frame, target: CallableTarget, positional, named, depth: int, ran: List[UseCase]):
832
+ """Follow a project function or method until its observable effects."""
833
+ if depth >= MAX_CALL_DEPTH or target.key in caller.active:
834
+ return None
835
+ variables = self.module_env(target.module)
836
+ if target.module is caller.module:
837
+ variables.update(caller.vars)
838
+ if target.owner is not None:
839
+ variables.update(self.class_env(target.owner))
840
+ variables.update({name: value for name, value in caller.vars.items() if name.startswith("self.")})
841
+ inner = Frame(target.module, variables, owner=target.owner, active=caller.active + (target.key,))
842
+ self.bind_arguments(inner, target.node, positional, named, target.owner is not None)
843
+ self.walk(d, inner, getattr(target.node, "body", []), depth + 1, ran)
844
+ return inner.returned
845
+
846
+ def wrapper(self, target: CallableTarget) -> Optional[Dict[str, object]]:
847
+ configured = self.opts.flow_wrappers.get(target.qualified)
848
+ return configured if isinstance(configured, dict) else None
849
+
850
+ def wrapper_step(self, d: Draft, target: CallableTarget, wrapper: Dict[str, object], line: str) -> None:
851
+ configured_label = wrapper.get("label")
852
+ label = configured_label if isinstance(configured_label, str) and configured_label else sentence(getattr(target.node, "name", ""))
853
+ configured_target = wrapper.get("target")
854
+ target_id = configured_target if isinstance(configured_target, str) else ""
855
+ if target_id:
856
+ context = target_id.split(".", 1)[0] if "." in target_id else None
857
+ d.lane(target_id, "service", context)
858
+ d.add_wrapper(
859
+ target.qualified,
860
+ label,
861
+ target_id,
862
+ wrapper.get("technical") is True,
863
+ self.opts.svc_id,
864
+ line,
865
+ )
866
+
867
+ def bind_arguments(self, frame: Frame, node: ast.AST, positional, named, method: bool) -> None:
868
+ args = getattr(node, "args", None)
869
+ if args is None:
870
+ return
871
+ parameters = list(getattr(args, "posonlyargs", [])) + list(getattr(args, "args", []))
872
+ if method and parameters and parameters[0].arg in ("self", "cls"):
873
+ parameters = parameters[1:]
874
+ for parameter, binding in zip(parameters, positional):
875
+ if binding is not None:
876
+ frame.vars[parameter.arg] = binding
877
+ for parameter in parameters:
878
+ binding = named.get(parameter.arg)
879
+ if binding is not None:
880
+ frame.vars[parameter.arg] = binding
881
+
882
+ def deferred(self, d: Draft, frame: Frame, node: ast.AST, depth: int, ran: List[UseCase]) -> None:
883
+ """What `on_commit` was handed: a lambda, whose body is read where the
884
+ lambda is; `partial(task.delay, ...)`, which is the enqueue it binds;
885
+ or anything else, read as a value."""
886
+ if isinstance(node, ast.Lambda):
887
+ self.value(d, frame, node.body, depth, ran)
888
+ return
889
+ if isinstance(node, ast.Call) and dotted(node.func).split(".")[-1] == "partial" and node.args:
890
+ bound = node.args[0]
891
+ if isinstance(bound, ast.Attribute) and bound.attr in ENQUEUE:
892
+ task = self.task_of(frame.module, bound.value)
893
+ if task is not None:
894
+ self.enqueue(d, task, "", frame.module.where(node))
895
+ return
896
+ self.value(d, frame, node, depth, ran)
897
+
898
+ def celery(self) -> Tuple[celery_conf.Config, Dict[Tuple[str, str], celery_tasks.Task]]:
899
+ """The tasks of the tree and the configuration that places them, read
900
+ once, the first time an enqueue is met."""
901
+ if self._celery is None:
902
+ by_key, _ = celery_tasks.index(celery_tasks.read_tasks(self.project))
903
+ self._celery = (celery_conf.read_config(self.project, self.opts.settings), by_key)
904
+ return self._celery
905
+
906
+ def task_of(self, module: Module, receiver: ast.AST) -> Optional[celery_tasks.Task]:
907
+ """The task `.delay` was called on, when the name resolves by import to
908
+ a function decorated as one; None for anything else."""
909
+ if isinstance(receiver, ast.Call) and isinstance(receiver.func, ast.Attribute) and receiver.func.attr in SIGNATURE:
910
+ receiver = receiver.func.value
911
+ name = dotted(receiver)
912
+ if not name:
913
+ return None
914
+ parts = name.split(".")
915
+ if len(parts) == 1:
916
+ hit = self.project.resolve(module, name)
917
+ if hit is None:
918
+ return None
919
+ target, local = hit
920
+ key = (celery_tasks.package_of(target), local)
921
+ else:
922
+ imported = module.imports.get(parts[0])
923
+ if imported is None:
924
+ return None
925
+ package = imported.module if imported.name == "*" else imported.module + "." + imported.name
926
+ key = (".".join([package] + parts[1:-1]), parts[-1])
927
+ _, by_key = self.celery()
928
+ return by_key.get(key)
929
+
930
+ def enqueue(self, d: Draft, task: celery_tasks.Task, at_call: str, line: str) -> None:
931
+ """One lane per queue, named as `extract-celery` names it, so the two
932
+ flows meet on the same participant."""
933
+ cfg, _ = self.celery()
934
+ address, _ = celery_conf.queue_for(task.name, at_call, task.queue, cfg)
935
+ lane = d.lane("celery-" + slug(address), "broker", None, "Celery · " + address)
936
+ d.add(
937
+ self.opts.svc_id,
938
+ lane,
939
+ "call",
940
+ "enqueue " + task.short,
941
+ line=line,
942
+ handoff={
943
+ "kind": "job",
944
+ "transport": "celery",
945
+ "channel": address,
946
+ "message": task.name,
947
+ "direction": "send",
948
+ },
949
+ )
950
+
951
+ def address_of(self, module: Module, node: ast.AST) -> str:
952
+ """The topic a producer was handed, as far as syntax carries it: the
953
+ literal, a module-level constant, or `settings.X` read out of the
954
+ settings module. A name in the settings module follows once more."""
955
+ text = celery_conf.str_value(node, module)
956
+ if text:
957
+ return text
958
+ name = dotted(node)
959
+ if name.startswith("settings.") and name.count(".") == 1:
960
+ if self._settings is None:
961
+ found = self.project.module(self.opts.settings or celery_conf.settings_module_name(self.project))
962
+ self._settings = found if found is not None else False # type: ignore[assignment]
963
+ if self._settings:
964
+ return celery_conf.str_value(ast.Name(id=name.split(".")[1]), self._settings)
965
+ return ""
966
+
967
+ def helper_address(self, module: Module, name: str) -> str:
968
+ """`bus.publish(event)`: a function of this project, one hop away,
969
+ whose body puts what it was handed on an address. Followed one call
970
+ further - a sync wrapper around an async publish - and no more."""
971
+ parts = name.split(".")
972
+ target: Optional[Module] = None
973
+ local = parts[-1]
974
+ if len(parts) == 1:
975
+ hit = self.project.resolve(module, name)
976
+ if hit is not None:
977
+ target, local = hit
978
+ else:
979
+ imported = module.imports.get(parts[0])
980
+ if imported is not None:
981
+ package = imported.module if imported.name == "*" else imported.module + "." + imported.name
982
+ target = self.project.module(".".join([package] + parts[1:-1]))
983
+ if target is None or target.dotted.split(".")[-1] == "services":
984
+ return ""
985
+ return self.produced_in(target, local, 0)
986
+
987
+ def produced_in(self, module: Module, function: str, depth: int) -> str:
988
+ fn = next((f for f in module.functions() if f.name == function), None)
989
+ if fn is None or depth > 1:
990
+ return ""
991
+ for node in ast.walk(fn):
992
+ if isinstance(node, ast.Call) and dotted(node.func).split(".")[-1] in PRODUCE_METHODS and node.args:
993
+ address = self.address_of(module, node.args[0])
994
+ if address:
995
+ return address
996
+ for node in ast.walk(fn):
997
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
998
+ address = self.produced_in(module, node.func.id, depth + 1)
999
+ if address:
1000
+ return address
1001
+ return ""
1002
+
1003
+ def produce(self, d: Draft, address: str, event_id: str, line: str) -> None:
1004
+ lane = d.lane(LANE_BUS, "broker", None)
1005
+ if event_id:
1006
+ d.add(self.opts.svc_id, lane, "event", event_id.rsplit(".", 1)[-1], ref=event_id, note="on " + address, line=line)
1007
+ self.referenced.add(event_id)
1008
+ self.produced.setdefault(event_id, []).append((address, line))
1009
+ else:
1010
+ d.add(self.opts.svc_id, lane, "call", "publish on " + address, line=line)
1011
+
1012
+ def publish(self, d: Draft, event_id: str, line: str) -> None:
1013
+ d.add(self.opts.svc_id, d.lane(LANE_BUS, "broker", None), "event", event_id.rsplit(".", 1)[-1], ref=event_id, line=line)
1014
+ self.referenced.add(event_id)
1015
+
1016
+ def emits(self, model: ModelDef, method: str) -> str:
1017
+ """A method of the root hands back the event of the move it made."""
1018
+ for node in methods(model.node):
1019
+ if node.name != method:
1020
+ continue
1021
+ annotation = getattr(node, "returns", None)
1022
+ if annotation is None:
1023
+ return ""
1024
+ for child in ast.walk(annotation):
1025
+ if isinstance(child, ast.Name) and child.id in self.events:
1026
+ return self.events[child.id].id
1027
+ if isinstance(child, ast.Constant) and isinstance(child.value, str) and child.value in self.events:
1028
+ return self.events[child.value].id
1029
+ return ""
1030
+
1031
+ def rpc(self, d: Draft, client: Client, method: str, line: str):
1032
+ call = client.calls.get(method)
1033
+ if call is None:
1034
+ return None
1035
+ lane, peer, status = self.peer_lane(d, call.pkg)
1036
+ d.add(self.opts.svc_id, lane, "rpc", call.label, status, call.id if status == catalog.DECLARED else "", line=line)
1037
+ if status == catalog.DECLARED:
1038
+ self.calls[call.id] = catalog.rpc_call(call.id, peer, catalog.DECLARED, call.source)
1039
+ else:
1040
+ self.calls[call.id] = catalog.rpc_call(call.id, peer, catalog.UNRESOLVED, call.source)
1041
+ return None
1042
+
1043
+ # --- project calls and standard HTTP clients ----------------------------
1044
+
1045
+ def class_target(self, module: Module, name: str) -> Optional[ClassTarget]:
1046
+ return self.classes.get((module.dotted, name))
1047
+
1048
+ def resolve_class(self, module: Module, name: str) -> Optional[ClassTarget]:
1049
+ if not name:
1050
+ return None
1051
+ if "." not in name:
1052
+ hit = self.project.resolve(module, name)
1053
+ if hit is not None:
1054
+ return self.classes.get((hit[0].dotted, hit[1]))
1055
+ return self.classes.get((module.dotted, name))
1056
+ parts = name.split(".")
1057
+ imported = module.imports.get(parts[0])
1058
+ if imported is None:
1059
+ return None
1060
+ if imported.name == "*":
1061
+ target_module = self.project.module(".".join([imported.module] + parts[1:-1]))
1062
+ return self.classes.get((target_module.dotted, parts[-1])) if target_module is not None else None
1063
+ direct = self.classes.get((imported.module, imported.name))
1064
+ if direct is not None and len(parts) == 1:
1065
+ return direct
1066
+ module_name = imported.module + "." + imported.name
1067
+ target_module = self.project.module(".".join([module_name] + parts[1:-1]))
1068
+ return self.classes.get((target_module.dotted, parts[-1])) if target_module is not None else None
1069
+
1070
+ def callable_target(self, frame: Frame, func: ast.AST, holder) -> Optional[CallableTarget]:
1071
+ name = dotted(func)
1072
+ if not name:
1073
+ return None
1074
+ local = frame.vars.get(name)
1075
+ if local is not None and local[0] == "callable":
1076
+ return local[1]
1077
+ if isinstance(func, ast.Attribute):
1078
+ if isinstance(func.value, ast.Call) and dotted(func.value.func) == "super":
1079
+ return self.super_method(frame.owner, func.attr)
1080
+ receiver = dotted(func.value)
1081
+ if receiver in ("self", "cls") and frame.owner is not None:
1082
+ return self.method_target(frame.owner, func.attr)
1083
+ if holder is not None and holder[0] == "object":
1084
+ return self.method_target(holder[1], func.attr)
1085
+ owner = self.resolve_class(frame.module, receiver)
1086
+ if owner is not None:
1087
+ return self.method_target(owner, func.attr)
1088
+ return self.resolve_function(frame.module, name)
1089
+
1090
+ def resolve_function(self, module: Module, name: str) -> Optional[CallableTarget]:
1091
+ parts = name.split(".")
1092
+ if len(parts) == 1:
1093
+ hit = self.project.resolve(module, name)
1094
+ if hit is None:
1095
+ return None
1096
+ return self.functions.get((hit[0].dotted, hit[1]))
1097
+ imported = module.imports.get(parts[0])
1098
+ if imported is None:
1099
+ return None
1100
+ if imported.name == "*":
1101
+ target = self.project.module(".".join([imported.module] + parts[1:-1]))
1102
+ else:
1103
+ module_name = imported.module + "." + imported.name
1104
+ target = self.project.module(".".join([module_name] + parts[1:-1]))
1105
+ if target is None and len(parts) == 2:
1106
+ target = self.project.module(imported.module)
1107
+ return self.functions.get((target.dotted, parts[-1])) if target is not None else None
1108
+
1109
+ def method_target(self, owner: ClassTarget, name: str) -> Optional[CallableTarget]:
1110
+ node = next((item for item in methods(owner.node) if item.name == name), None)
1111
+ if node is not None:
1112
+ return CallableTarget(owner.module, node, owner)
1113
+ for base in owner.node.bases:
1114
+ parent = self.resolve_class(owner.module, dotted(base))
1115
+ if parent is not None:
1116
+ found = self.method_target(parent, name)
1117
+ if found is not None:
1118
+ return found
1119
+ return None
1120
+
1121
+ def super_method(self, owner: Optional[ClassTarget], name: str) -> Optional[CallableTarget]:
1122
+ if owner is None:
1123
+ return None
1124
+ for base in owner.node.bases:
1125
+ parent = self.resolve_class(owner.module, dotted(base))
1126
+ if parent is not None:
1127
+ found = self.method_target(parent, name)
1128
+ if found is not None:
1129
+ return found
1130
+ return None
1131
+
1132
+ def class_env(self, target: ClassTarget) -> Dict[str, Tuple[str, object]]:
1133
+ out: Dict[str, Tuple[str, object]] = {}
1134
+ for name, value, _ in assigned(target.node):
1135
+ binding = self.constructed(target.module, value)
1136
+ if binding is not None:
1137
+ out["self." + name] = binding
1138
+ init = next((node for node in methods(target.node) if node.name == "__init__"), None)
1139
+ if init is not None:
1140
+ for stmt in ast.walk(init):
1141
+ if isinstance(stmt, ast.Assign) and len(stmt.targets) == 1 and isinstance(stmt.targets[0], ast.Attribute):
1142
+ binding = self.constructed(target.module, stmt.value)
1143
+ if binding is not None:
1144
+ out["self." + stmt.targets[0].attr] = binding
1145
+ return out
1146
+
1147
+ def model_for(self, module: Module, name: str) -> Optional[ModelDef]:
1148
+ short = name.split(".")[-1]
1149
+ candidates = self.models_by_name.get(short, [])
1150
+ if not candidates:
1151
+ return None
1152
+ hit = self.project.resolve(module, short) if "." not in name else None
1153
+ if hit is not None:
1154
+ exact = [model for model in candidates if model.module.dotted == hit[0].dotted and model.name == hit[1]]
1155
+ if len(exact) == 1:
1156
+ return exact[0]
1157
+ same_package = [model for model in candidates if module.dotted == model.app.dotted or module.dotted.startswith(model.app.dotted + ".")]
1158
+ if len(same_package) == 1:
1159
+ return same_package[0]
1160
+ return candidates[0] if len(candidates) == 1 else None
1161
+
1162
+ def external_name(self, module: Module, name: str) -> str:
1163
+ if not name:
1164
+ return ""
1165
+ parts = name.split(".")
1166
+ imported = module.imports.get(parts[0])
1167
+ if imported is None:
1168
+ return name
1169
+ base = imported.module if imported.name == "*" else imported.module + "." + imported.name
1170
+ return ".".join([base] + parts[1:])
1171
+
1172
+ def http_call(self, module: Module, node: ast.Call, holder, positional) -> Optional[Tuple[str, str]]:
1173
+ name = dotted(node.func)
1174
+ last = name.split(".")[-1].lower()
1175
+ if holder is not None and holder[0] == "http" and last in HTTP_METHODS:
1176
+ url = binding_string(positional[0]) if positional else ""
1177
+ url = url or (self.string_value(module, node.args[0]) if node.args else "")
1178
+ return last.upper(), url or expression(node.args[0] if node.args else None)
1179
+ external = self.external_name(module, name)
1180
+ library = external.split(".", 1)[0]
1181
+ if library not in HTTP_LIBRARIES:
1182
+ return None
1183
+ if last == "request" and len(node.args) >= 2:
1184
+ method = binding_string(positional[0]).upper() if positional else ""
1185
+ target = binding_string(positional[1]) if len(positional) > 1 else ""
1186
+ return method or "HTTP", target or self.string_value(module, node.args[1]) or expression(node.args[1])
1187
+ if last not in HTTP_METHODS:
1188
+ return None
1189
+ url = binding_string(positional[0]) if positional else ""
1190
+ url = url or (self.string_value(module, node.args[0]) if node.args else "")
1191
+ return last.upper(), url or expression(node.args[0] if node.args else None)
1192
+
1193
+ def string_value(self, module: Module, node: Optional[ast.AST], depth: int = 0) -> str:
1194
+ if node is None or depth > 5:
1195
+ return ""
1196
+ text = celery_conf.str_value(node, module)
1197
+ if text:
1198
+ return text
1199
+ if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
1200
+ left = self.string_value(module, node.left, depth + 1)
1201
+ right = self.string_value(module, node.right, depth + 1)
1202
+ return left + right if left or right else ""
1203
+ if isinstance(node, ast.JoinedStr):
1204
+ chunks = []
1205
+ for item in node.values:
1206
+ if isinstance(item, ast.Constant) and isinstance(item.value, str):
1207
+ chunks.append(item.value)
1208
+ elif isinstance(item, ast.FormattedValue):
1209
+ chunks.append("{%s}" % expression(item.value))
1210
+ return "".join(chunks)
1211
+ if isinstance(node, ast.Call) and self.external_name(module, dotted(node.func)) == "urllib.parse.urljoin" and len(node.args) >= 2:
1212
+ left = self.string_value(module, node.args[0], depth + 1)
1213
+ right = self.string_value(module, node.args[1], depth + 1)
1214
+ return left.rstrip("/") + "/" + right.lstrip("/") if left or right else ""
1215
+ name = dotted(node)
1216
+ if not name:
1217
+ return ""
1218
+ if name.startswith("settings."):
1219
+ return self.address_of(module, node)
1220
+ if "." not in name:
1221
+ hit = self.project.resolve(module, name)
1222
+ if hit is not None and hit[0] is not module:
1223
+ return self.string_value(hit[0], ast.Name(id=hit[1]), depth + 1)
1224
+ return ""
1225
+
1226
+ def http_step(self, d: Draft, method: str, target: str, line: str) -> None:
1227
+ parsed = urlparse(target)
1228
+ peer = parsed.netloc or parsed.path.split("/", 1)[0] or "external-http"
1229
+ path = parsed.path if parsed.netloc else target
1230
+ label = "%s %s" % (method, path or "/")
1231
+ lane = d.lane("http-" + slug(peer), "unknown", None, peer)
1232
+ d.add(self.opts.svc_id, lane, "rpc", label, catalog.UNRESOLVED, line=line)
1233
+
1234
+ def use_case(self, module: Module, name: str) -> Optional[UseCase]:
1235
+ """`issue_invoice(...)` imported from the services module,
1236
+ `services.issue_invoice(...)` through the module itself, or a function
1237
+ of the module being read."""
1238
+ parts = name.split(".")
1239
+ keys = []
1240
+ imported = module.imports.get(parts[0])
1241
+ if imported is not None:
1242
+ if imported.name != "*":
1243
+ keys.append(imported.module + "." + imported.name) # from .services import x
1244
+ keys.append(imported.module + "." + imported.name + "." + parts[-1]) # from . import services
1245
+ keys.append(imported.module + "." + parts[-1]) # import package.services
1246
+ keys.append(module.dotted + "." + parts[-1])
1247
+ for key in keys:
1248
+ hit = self.use_cases.get(key)
1249
+ if hit is not None:
1250
+ return hit
1251
+ return None
1252
+
1253
+ def module_env(self, module: Module) -> Dict[str, Tuple[str, object]]:
1254
+ """What a module-level name holds before a body starts: the clients a
1255
+ module keeps as singletons, which is how a Django project holds one."""
1256
+ out: Dict[str, Tuple[str, object]] = {}
1257
+ for name, value, _ in assigned(module.tree):
1258
+ binding = self.constructed(module, value)
1259
+ if binding is not None:
1260
+ out[name] = binding
1261
+ return out
1262
+
1263
+
1264
+ def terminal(body: List[ast.AST]) -> bool:
1265
+ return bool(body) and isinstance(body[-1], (ast.Return, ast.Raise))
1266
+
1267
+
1268
+ def expression(node: Optional[ast.AST]) -> str:
1269
+ if node is None:
1270
+ return ""
1271
+ try:
1272
+ return " ".join(ast.unparse(node).split()) # type: ignore[attr-defined]
1273
+ except Exception:
1274
+ return dotted(node)
1275
+
1276
+
1277
+ def binding_string(binding) -> str:
1278
+ return binding[1] if isinstance(binding, tuple) and len(binding) > 1 and binding[0] == "string" else ""