@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.
- package/LICENSE +21 -0
- package/README.md +387 -0
- package/catalog/enum_test.go +46 -0
- package/catalog/model.go +932 -0
- package/catalog/roundtrip_test.go +185 -0
- package/catalog/via_test.go +38 -0
- package/cli/portolan.mjs +368 -0
- package/go.mod +14 -0
- package/go.sum +14 -0
- package/index.html +13 -0
- package/internal/commands/cargo.go +154 -0
- package/internal/commands/commands.go +143 -0
- package/internal/commands/commands_test.go +323 -0
- package/internal/commands/gradle.go +77 -0
- package/internal/commands/justfile.go +82 -0
- package/internal/commands/makefile.go +106 -0
- package/internal/commands/maven.go +93 -0
- package/internal/commands/packagejson.go +159 -0
- package/internal/commands/pyproject.go +169 -0
- package/internal/commands/taskfile.go +101 -0
- package/internal/commands/testdata/estate/.cargo/config.toml +7 -0
- package/internal/commands/testdata/estate/Makefile +33 -0
- package/internal/commands/testdata/estate/Taskfile.yml +21 -0
- package/internal/commands/testdata/estate/build.gradle.kts +21 -0
- package/internal/commands/testdata/estate/justfile +20 -0
- package/internal/commands/testdata/estate/package.json +13 -0
- package/internal/commands/testdata/estate/pom.xml +29 -0
- package/internal/commands/testdata/estate/pyproject.toml +27 -0
- package/internal/commands/testdata/estate/xtask/src/main.rs +27 -0
- package/internal/commands/testdata/golden/commands.json +305 -0
- package/internal/gohttp/analyze.go +2500 -0
- package/internal/gohttp/endpoints.go +1067 -0
- package/internal/gohttp/roots.go +320 -0
- package/internal/gohttp/typed.go +143 -0
- package/internal/goscan/constants.go +85 -0
- package/internal/goscan/goscan_test.go +227 -0
- package/internal/goscan/names.go +52 -0
- package/internal/goscan/parse_test.go +11 -0
- package/internal/goscan/source.go +36 -0
- package/internal/goscan/tree.go +155 -0
- package/internal/goscan/types.go +99 -0
- package/internal/wsdl/ids.go +127 -0
- package/internal/wsdl/ids_test.go +21 -0
- package/internal/wsdl/model.go +62 -0
- package/internal/wsdl/parse.go +920 -0
- package/internal/wsdl/parse_test.go +133 -0
- package/package.json +117 -0
- package/plugin/describe.go +107 -0
- package/plugin/describe_test.go +114 -0
- package/plugin/protocol.go +117 -0
- package/plugin/schematest/schematest.go +126 -0
- package/plugins/README.md +1160 -0
- package/plugins/extract-adr/describe.go +19 -0
- package/plugins/extract-adr/describe_test.go +11 -0
- package/plugins/extract-adr/extract.go +153 -0
- package/plugins/extract-adr/extract_test.go +350 -0
- package/plugins/extract-adr/history.go +99 -0
- package/plugins/extract-adr/main.go +65 -0
- package/plugins/extract-adr/options.schema.json +26 -0
- package/plugins/extract-adr/parse.go +642 -0
- package/plugins/extract-adr/parse_test.go +405 -0
- package/plugins/extract-asyncapi/describe.go +19 -0
- package/plugins/extract-asyncapi/describe_test.go +11 -0
- package/plugins/extract-asyncapi/extract.go +315 -0
- package/plugins/extract-asyncapi/extract_test.go +197 -0
- package/plugins/extract-asyncapi/main.go +48 -0
- package/plugins/extract-asyncapi/options.schema.json +26 -0
- package/plugins/extract-asyncapi/spec.go +151 -0
- package/plugins/extract-celery/README.md +169 -0
- package/plugins/extract-celery/extract.py +346 -0
- package/plugins/extract-celery/extract_test.py +192 -0
- package/plugins/extract-celery/main.py +66 -0
- package/plugins/extract-celery/options.py +31 -0
- package/plugins/extract-celery/options.schema.json +31 -0
- package/plugins/extract-celery/producers.py +156 -0
- package/plugins/extract-celery/routes_test.py +106 -0
- package/plugins/extract-commands/describe.go +19 -0
- package/plugins/extract-commands/describe_test.go +11 -0
- package/plugins/extract-commands/extract.go +79 -0
- package/plugins/extract-commands/extract_test.go +92 -0
- package/plugins/extract-commands/main.go +55 -0
- package/plugins/extract-commands/options.schema.json +20 -0
- package/plugins/extract-csr/avro.go +258 -0
- package/plugins/extract-csr/describe.go +19 -0
- package/plugins/extract-csr/describe_test.go +11 -0
- package/plugins/extract-csr/extract.go +338 -0
- package/plugins/extract-csr/extract_test.go +374 -0
- package/plugins/extract-csr/jsonschema.go +343 -0
- package/plugins/extract-csr/lock.go +25 -0
- package/plugins/extract-csr/main.go +84 -0
- package/plugins/extract-csr/options.schema.json +43 -0
- package/plugins/extract-csr/subject.go +102 -0
- package/plugins/extract-django/README.md +386 -0
- package/plugins/extract-django/apps.py +100 -0
- package/plugins/extract-django/auth.py +302 -0
- package/plugins/extract-django/auth_test.py +188 -0
- package/plugins/extract-django/clients.py +121 -0
- package/plugins/extract-django/contracts.py +458 -0
- package/plugins/extract-django/database.py +119 -0
- package/plugins/extract-django/domain.py +318 -0
- package/plugins/extract-django/events.py +86 -0
- package/plugins/extract-django/extract.py +601 -0
- package/plugins/extract-django/extract_test.py +397 -0
- package/plugins/extract-django/flows.py +1278 -0
- package/plugins/extract-django/flows_test.py +338 -0
- package/plugins/extract-django/ids.py +42 -0
- package/plugins/extract-django/ids_test.py +30 -0
- package/plugins/extract-django/lifecycle.py +222 -0
- package/plugins/extract-django/lifecycle_test.py +76 -0
- package/plugins/extract-django/main.py +66 -0
- package/plugins/extract-django/openapi.py +229 -0
- package/plugins/extract-django/openapi_test.py +60 -0
- package/plugins/extract-django/operations.py +96 -0
- package/plugins/extract-django/options.py +61 -0
- package/plugins/extract-django/options.schema.json +126 -0
- package/plugins/extract-django/routing.py +214 -0
- package/plugins/extract-django/routing_test.py +198 -0
- package/plugins/extract-django/serializers.py +558 -0
- package/plugins/extract-django/store.py +434 -0
- package/plugins/extract-django/transport.py +242 -0
- package/plugins/extract-flows/describe.go +19 -0
- package/plugins/extract-flows/describe_test.go +11 -0
- package/plugins/extract-flows/extract.go +91 -0
- package/plugins/extract-flows/main.go +45 -0
- package/plugins/extract-flows/options.schema.json +16 -0
- package/plugins/extract-flows/parse.go +593 -0
- package/plugins/extract-flows/parse_test.go +204 -0
- package/plugins/extract-glossary/describe.go +19 -0
- package/plugins/extract-glossary/describe_test.go +11 -0
- package/plugins/extract-glossary/extract.go +115 -0
- package/plugins/extract-glossary/extract_test.go +220 -0
- package/plugins/extract-glossary/main.go +59 -0
- package/plugins/extract-glossary/options.schema.json +21 -0
- package/plugins/extract-glossary/parse.go +214 -0
- package/plugins/extract-glossary/parse_test.go +203 -0
- package/plugins/extract-go/aggregate.go +214 -0
- package/plugins/extract-go/client.go +409 -0
- package/plugins/extract-go/client_test.go +266 -0
- package/plugins/extract-go/describe.go +19 -0
- package/plugins/extract-go/describe_test.go +11 -0
- package/plugins/extract-go/enum.go +195 -0
- package/plugins/extract-go/enum_test.go +82 -0
- package/plugins/extract-go/event.go +99 -0
- package/plugins/extract-go/extract.go +191 -0
- package/plugins/extract-go/extract_test.go +261 -0
- package/plugins/extract-go/flow.go +1441 -0
- package/plugins/extract-go/flow_test.go +609 -0
- package/plugins/extract-go/httpclient.go +174 -0
- package/plugins/extract-go/httpclient_test.go +296 -0
- package/plugins/extract-go/ids.go +92 -0
- package/plugins/extract-go/layout.go +225 -0
- package/plugins/extract-go/layout_test.go +125 -0
- package/plugins/extract-go/lifecycle.go +324 -0
- package/plugins/extract-go/lifecycle_test.go +108 -0
- package/plugins/extract-go/main.go +79 -0
- package/plugins/extract-go/operation.go +157 -0
- package/plugins/extract-go/options.schema.json +66 -0
- package/plugins/extract-go/source.go +316 -0
- package/plugins/extract-go/transport.go +321 -0
- package/plugins/extract-go/transport_test.go +145 -0
- package/plugins/extract-go/wiring.go +434 -0
- package/plugins/extract-go-nats/describe.go +19 -0
- package/plugins/extract-go-nats/describe_test.go +11 -0
- package/plugins/extract-go-nats/extract.go +177 -0
- package/plugins/extract-go-nats/extract_test.go +389 -0
- package/plugins/extract-go-nats/index.go +379 -0
- package/plugins/extract-go-nats/main.go +42 -0
- package/plugins/extract-go-nats/options.schema.json +21 -0
- package/plugins/extract-go-nats/resolve.go +161 -0
- package/plugins/extract-go-nats/sites.go +224 -0
- package/plugins/extract-graphql/describe.go +19 -0
- package/plugins/extract-graphql/describe_test.go +11 -0
- package/plugins/extract-graphql/extract.go +433 -0
- package/plugins/extract-graphql/extract_test.go +256 -0
- package/plugins/extract-graphql/ids.go +49 -0
- package/plugins/extract-graphql/lex.go +237 -0
- package/plugins/extract-graphql/main.go +51 -0
- package/plugins/extract-graphql/options.schema.json +31 -0
- package/plugins/extract-graphql/parse.go +621 -0
- package/plugins/extract-graphql/parse_test.go +122 -0
- package/plugins/extract-http-clients/describe.go +19 -0
- package/plugins/extract-http-clients/describe_test.go +11 -0
- package/plugins/extract-http-clients/extract.go +705 -0
- package/plugins/extract-http-clients/extract_test.go +1263 -0
- package/plugins/extract-http-clients/main.go +42 -0
- package/plugins/extract-http-clients/options.schema.json +33 -0
- package/plugins/extract-java/README.md +190 -0
- package/plugins/extract-java/build/org/portolan/extract/Catalog.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$1.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$2.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$Call.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$Client.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$Route.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Domain$Aggregate.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Domain.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Events$Found.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Events.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Extract.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$1.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$Binding.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$Draft.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$Frame.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$Options.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Ids.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Json$Reader.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Json.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Lifecycle$1.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Lifecycle$Move.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Lifecycle.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Main.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/OpenApi$Operation.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/OpenApi$Spec.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/OpenApi.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Operations$1.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Operations$UseCase.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Operations.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Proto$Rpc.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Proto$Service.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Proto.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Builder.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Input.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Options.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Source$Project.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Source$Unit.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Source.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Transport$Endpoint.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Transport.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Yaml$Line.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Yaml$Reader.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Yaml.class +0 -0
- package/plugins/extract-java/options.schema.json +70 -0
- package/plugins/extract-openapi/describe.go +19 -0
- package/plugins/extract-openapi/describe_test.go +11 -0
- package/plugins/extract-openapi/discover.go +243 -0
- package/plugins/extract-openapi/extract.go +526 -0
- package/plugins/extract-openapi/extract_test.go +545 -0
- package/plugins/extract-openapi/main.go +75 -0
- package/plugins/extract-openapi/options.schema.json +62 -0
- package/plugins/extract-openapi/spec.go +350 -0
- package/plugins/extract-project/README.md +22 -0
- package/plugins/extract-project/describe.go +19 -0
- package/plugins/extract-project/describe_test.go +11 -0
- package/plugins/extract-project/extract.go +221 -0
- package/plugins/extract-project/extract_test.go +109 -0
- package/plugins/extract-project/main.go +41 -0
- package/plugins/extract-project/options.schema.json +48 -0
- package/plugins/extract-proto/ast.go +125 -0
- package/plugins/extract-proto/consumes.go +77 -0
- package/plugins/extract-proto/describe.go +19 -0
- package/plugins/extract-proto/extract.go +293 -0
- package/plugins/extract-proto/extract_test.go +459 -0
- package/plugins/extract-proto/ids.go +89 -0
- package/plugins/extract-proto/ids_test.go +57 -0
- package/plugins/extract-proto/lex.go +285 -0
- package/plugins/extract-proto/main.go +100 -0
- package/plugins/extract-proto/module.go +120 -0
- package/plugins/extract-proto/options.schema.json +51 -0
- package/plugins/extract-proto/parse.go +720 -0
- package/plugins/extract-proto/parse_test.go +307 -0
- package/plugins/extract-proto/provides.go +236 -0
- package/plugins/extract-proto/resolve.go +222 -0
- package/plugins/extract-proto/resolve_test.go +119 -0
- package/plugins/extract-python-kafka/README.md +36 -0
- package/plugins/extract-python-kafka/extract.py +211 -0
- package/plugins/extract-python-kafka/extract_test.py +68 -0
- package/plugins/extract-python-kafka/main.py +59 -0
- package/plugins/extract-python-kafka/options.py +29 -0
- package/plugins/extract-python-kafka/options.schema.json +31 -0
- package/plugins/extract-redis/README.md +11 -0
- package/plugins/extract-redis/describe.go +19 -0
- package/plugins/extract-redis/describe_test.go +11 -0
- package/plugins/extract-redis/extract.go +183 -0
- package/plugins/extract-redis/extract_test.go +168 -0
- package/plugins/extract-redis/keyspaces.go +469 -0
- package/plugins/extract-redis/main.go +44 -0
- package/plugins/extract-redis/options.schema.json +30 -0
- package/plugins/extract-river/describe.go +19 -0
- package/plugins/extract-river/describe_test.go +11 -0
- package/plugins/extract-river/extract.go +507 -0
- package/plugins/extract-river/extract_test.go +132 -0
- package/plugins/extract-river/main.go +42 -0
- package/plugins/extract-river/options.schema.json +21 -0
- package/plugins/extract-rust/Cargo.lock +133 -0
- package/plugins/extract-rust/Cargo.toml +23 -0
- package/plugins/extract-rust/README.md +204 -0
- package/plugins/extract-rust/options.schema.json +65 -0
- package/plugins/extract-rust/rustfmt.toml +1 -0
- package/plugins/extract-sql/ddl.go +893 -0
- package/plugins/extract-sql/ddl_test.go +401 -0
- package/plugins/extract-sql/describe.go +19 -0
- package/plugins/extract-sql/describe_test.go +11 -0
- package/plugins/extract-sql/layout.go +221 -0
- package/plugins/extract-sql/layout_test.go +55 -0
- package/plugins/extract-sql/lineage.go +107 -0
- package/plugins/extract-sql/main.go +141 -0
- package/plugins/extract-sql/maps.go +564 -0
- package/plugins/extract-sql/maps_java.go +117 -0
- package/plugins/extract-sql/maps_rust.go +333 -0
- package/plugins/extract-sql/maps_rust_test.go +70 -0
- package/plugins/extract-sql/maps_test.go +204 -0
- package/plugins/extract-sql/maps_ts.go +398 -0
- package/plugins/extract-sql/maps_ts_test.go +136 -0
- package/plugins/extract-sql/options.schema.json +53 -0
- package/plugins/extract-sql/projection.go +70 -0
- package/plugins/extract-sql/projection_test.go +50 -0
- package/plugins/extract-sql/store.go +420 -0
- package/plugins/extract-sql/store_test.go +233 -0
- package/plugins/extract-sql/view.go +295 -0
- package/plugins/extract-ts/README.md +306 -0
- package/plugins/extract-ts/ast.test.ts +178 -0
- package/plugins/extract-ts/ast.ts +648 -0
- package/plugins/extract-ts/clients.ts +185 -0
- package/plugins/extract-ts/domain.ts +164 -0
- package/plugins/extract-ts/extract.test.ts +208 -0
- package/plugins/extract-ts/extract.ts +200 -0
- package/plugins/extract-ts/flows.ts +783 -0
- package/plugins/extract-ts/graphql.test.ts +73 -0
- package/plugins/extract-ts/graphql.ts +181 -0
- package/plugins/extract-ts/ids.test.ts +21 -0
- package/plugins/extract-ts/ids.ts +52 -0
- package/plugins/extract-ts/lifecycle.ts +138 -0
- package/plugins/extract-ts/main.ts +48 -0
- package/plugins/extract-ts/openapi.test.ts +64 -0
- package/plugins/extract-ts/openapi.ts +99 -0
- package/plugins/extract-ts/operations.ts +93 -0
- package/plugins/extract-ts/options.schema.json +96 -0
- package/plugins/extract-ts/source.test.ts +174 -0
- package/plugins/extract-ts/source.ts +465 -0
- package/plugins/extract-ts/transport.ts +147 -0
- package/plugins/extract-ts/tsconfig.json +13 -0
- package/plugins/extract-ts/wiring.ts +190 -0
- package/plugins/extract-watermill/describe.go +19 -0
- package/plugins/extract-watermill/describe_test.go +11 -0
- package/plugins/extract-watermill/extract.go +1228 -0
- package/plugins/extract-watermill/extract_test.go +234 -0
- package/plugins/extract-watermill/main.go +41 -0
- package/plugins/extract-watermill/options.schema.json +21 -0
- package/plugins/extract-wsdl/describe.go +19 -0
- package/plugins/extract-wsdl/describe_test.go +11 -0
- package/plugins/extract-wsdl/extract.go +148 -0
- package/plugins/extract-wsdl/extract_test.go +45 -0
- package/plugins/extract-wsdl/main.go +53 -0
- package/plugins/extract-wsdl/options.schema.json +59 -0
- package/plugins/fetch-bsr/auth.go +114 -0
- package/plugins/fetch-bsr/bsr.go +240 -0
- package/plugins/fetch-bsr/cache.go +66 -0
- package/plugins/fetch-bsr/describe.go +19 -0
- package/plugins/fetch-bsr/fetch.go +192 -0
- package/plugins/fetch-bsr/fetch_test.go +465 -0
- package/plugins/fetch-bsr/lock.go +71 -0
- package/plugins/fetch-bsr/main.go +117 -0
- package/plugins/fetch-bsr/options.schema.json +42 -0
- package/plugins/fetch-csr/auth.go +95 -0
- package/plugins/fetch-csr/cache.go +69 -0
- package/plugins/fetch-csr/describe.go +19 -0
- package/plugins/fetch-csr/describe_test.go +11 -0
- package/plugins/fetch-csr/fetch.go +254 -0
- package/plugins/fetch-csr/fetch_test.go +494 -0
- package/plugins/fetch-csr/lock.go +95 -0
- package/plugins/fetch-csr/main.go +122 -0
- package/plugins/fetch-csr/options.schema.json +42 -0
- package/plugins/fetch-csr/registry.go +209 -0
- package/plugins/fetch-git/cache.go +65 -0
- package/plugins/fetch-git/describe.go +19 -0
- package/plugins/fetch-git/describe_test.go +11 -0
- package/plugins/fetch-git/fetch.go +181 -0
- package/plugins/fetch-git/fetch_test.go +332 -0
- package/plugins/fetch-git/git.go +169 -0
- package/plugins/fetch-git/lock.go +72 -0
- package/plugins/fetch-git/main.go +127 -0
- package/plugins/fetch-git/offline.go +39 -0
- package/plugins/fetch-git/options.schema.json +42 -0
- package/plugins/fetch-git/pin.go +90 -0
- package/plugins/fetch-git/pin_test.go +74 -0
- package/plugins/gen-backstage/describe.go +17 -0
- package/plugins/gen-backstage/main.go +22 -0
- package/plugins/gen-backstage/options.schema.json +30 -0
- package/plugins/gen-backstage/plugin.go +473 -0
- package/plugins/gen-backstage/plugin_test.go +145 -0
- package/plugins/gen-backstage.wasm +0 -0
- package/plugins/gen-markdown/adr.go +162 -0
- package/plugins/gen-markdown/aggregate.go +362 -0
- package/plugins/gen-markdown/canonical.go +93 -0
- package/plugins/gen-markdown/context.go +90 -0
- package/plugins/gen-markdown/coverage_test.go +89 -0
- package/plugins/gen-markdown/describe.go +19 -0
- package/plugins/gen-markdown/describe_test.go +11 -0
- package/plugins/gen-markdown/external.go +71 -0
- package/plugins/gen-markdown/flow.go +277 -0
- package/plugins/gen-markdown/glossary.go +76 -0
- package/plugins/gen-markdown/glossary_test.go +148 -0
- package/plugins/gen-markdown/llms.go +302 -0
- package/plugins/gen-markdown/main.go +22 -0
- package/plugins/gen-markdown/markdown.go +254 -0
- package/plugins/gen-markdown/markdown_test.go +100 -0
- package/plugins/gen-markdown/module.go +107 -0
- package/plugins/gen-markdown/options.schema.json +16 -0
- package/plugins/gen-markdown/plugin.go +39 -0
- package/plugins/gen-markdown/quality_test.go +176 -0
- package/plugins/gen-markdown/redis_test.go +38 -0
- package/plugins/gen-markdown/render.go +344 -0
- package/plugins/gen-markdown/render_test.go +186 -0
- package/plugins/gen-markdown/service.go +456 -0
- package/plugins/gen-markdown/source.go +177 -0
- package/plugins/gen-markdown/store.go +201 -0
- package/plugins/gen-markdown.wasm +0 -0
- package/plugins/gen-mermaid/describe.go +17 -0
- package/plugins/gen-mermaid/main.go +22 -0
- package/plugins/gen-mermaid/options.schema.json +10 -0
- package/plugins/gen-mermaid/plugin.go +104 -0
- package/plugins/gen-mermaid/plugin_test.go +33 -0
- package/plugins/gen-mermaid.wasm +0 -0
- package/plugins/openapi/ids.go +261 -0
- package/plugins/openapi/ids_test.go +98 -0
- package/plugins/pyplugin/catalog.py +209 -0
- package/plugins/pyplugin/celery_conf.py +436 -0
- package/plugins/pyplugin/celery_tasks.py +76 -0
- package/plugins/pyplugin/kafka.py +570 -0
- package/plugins/pyplugin/names.py +38 -0
- package/plugins/pyplugin/protocol.py +79 -0
- package/plugins/pyplugin/source.py +285 -0
- package/plugins/verify-codeowners/describe.go +19 -0
- package/plugins/verify-codeowners/describe_test.go +11 -0
- package/plugins/verify-codeowners/main.go +75 -0
- package/plugins/verify-codeowners/match.go +85 -0
- package/plugins/verify-codeowners/match_test.go +47 -0
- package/plugins/verify-codeowners/options.schema.json +15 -0
- package/plugins/verify-codeowners/owners.go +164 -0
- package/plugins/verify-codeowners/owners_test.go +225 -0
- package/plugins/verify-codeowners/parse.go +90 -0
- package/plugins/verify-codeowners/parse_test.go +62 -0
- package/plugins/verify-otel/describe.go +19 -0
- package/plugins/verify-otel/describe_test.go +11 -0
- package/plugins/verify-otel/main.go +53 -0
- package/plugins/verify-otel/match.go +336 -0
- package/plugins/verify-otel/options.schema.json +30 -0
- package/plugins/verify-otel/otlp.go +200 -0
- package/plugins/verify-otel/verify.go +734 -0
- package/plugins/verify-otel/verify_test.go +460 -0
- package/portolan.json +1073 -0
- package/public/favicon.svg +12 -0
- package/schema/portolan.schema.json +2558 -0
- package/scripts/README.md +53 -0
- package/scripts/build-report.mjs +60 -0
- package/scripts/build-report.test.mjs +44 -0
- package/scripts/builtin-plugins.mjs +49 -0
- package/scripts/catalog-sources.mjs +67 -0
- package/scripts/diff.mjs +315 -0
- package/scripts/diff.test.mjs +151 -0
- package/scripts/forge-comment.mjs +144 -0
- package/scripts/forge-comment.test.mjs +151 -0
- package/scripts/forge-release.mjs +138 -0
- package/scripts/forge-release.test.mjs +106 -0
- package/scripts/forge.mjs +61 -0
- package/scripts/gen-likec4.mjs +854 -0
- package/scripts/gen-likec4.test.mjs +258 -0
- package/scripts/gen.mjs +603 -0
- package/scripts/generated-docs.test.mjs +66 -0
- package/scripts/local-api.mjs +1192 -0
- package/scripts/local-api.test.mjs +387 -0
- package/scripts/manifest.mjs +214 -0
- package/scripts/manifest.test.mjs +148 -0
- package/scripts/output-path.mjs +79 -0
- package/scripts/output-path.test.mjs +45 -0
- package/scripts/package-smoke.mjs +37 -0
- package/scripts/plugin-host.mjs +419 -0
- package/scripts/plugin-host.test.mjs +109 -0
- package/scripts/plugin-wasm-worker.mjs +34 -0
- package/scripts/run-builtin.mjs +81 -0
- package/scripts/schema.mjs +357 -0
- package/scripts/site-docs.mjs +295 -0
- package/scripts/site-docs.test.mjs +181 -0
- package/scripts/vendor-lock.mjs +58 -0
- package/scripts/vendor-lock.test.mjs +69 -0
- package/src/README.md +38 -0
- package/src/app/App.tsx +448 -0
- package/src/app/BranchPicker.test.ts +46 -0
- package/src/app/BranchPicker.tsx +153 -0
- package/src/app/Breadcrumbs.test.ts +38 -0
- package/src/app/Breadcrumbs.tsx +223 -0
- package/src/app/BuildStamp.tsx +146 -0
- package/src/app/CommandPalette.tsx +292 -0
- package/src/app/HashScroll.tsx +45 -0
- package/src/app/Sidebar.tsx +1742 -0
- package/src/app/TopBar.tsx +393 -0
- package/src/app/density.tsx +64 -0
- package/src/app/forge-access.test.ts +27 -0
- package/src/app/forge-access.tsx +80 -0
- package/src/app/panels.tsx +141 -0
- package/src/app/pins.tsx +242 -0
- package/src/app/query-client.ts +24 -0
- package/src/app/responsive.ts +55 -0
- package/src/app/shortcuts.test.ts +39 -0
- package/src/app/shortcuts.tsx +282 -0
- package/src/app/theme.tsx +51 -0
- package/src/app/title.test.ts +14 -0
- package/src/app/title.ts +30 -0
- package/src/app/toast.tsx +59 -0
- package/src/app/tree-keys.test.ts +80 -0
- package/src/app/tree-keys.ts +87 -0
- package/src/app/ui-store.ts +52 -0
- package/src/catalog-error.test.ts +105 -0
- package/src/catalog-profile.test.ts +59 -0
- package/src/catalog-profile.ts +101 -0
- package/src/catalog-stores.test.ts +431 -0
- package/src/catalog.test.ts +1215 -0
- package/src/catalog.ts +3125 -0
- package/src/chat/Cards.tsx +408 -0
- package/src/chat/ChatPanel.tsx +79 -0
- package/src/chat/ChatSettings.tsx +219 -0
- package/src/chat/Composer.tsx +99 -0
- package/src/chat/Conversation.tsx +281 -0
- package/src/chat/Header.tsx +46 -0
- package/src/chat/Message.tsx +49 -0
- package/src/chat/Notice.tsx +195 -0
- package/src/chat/Starter.tsx +67 -0
- package/src/chat/Steps.tsx +45 -0
- package/src/chat/Waiting.tsx +87 -0
- package/src/chat/flags.test.ts +59 -0
- package/src/chat/flags.ts +155 -0
- package/src/chat/linkify.test.ts +30 -0
- package/src/chat/linkify.ts +45 -0
- package/src/chat/prefs.ts +56 -0
- package/src/chat/prompt.test.ts +43 -0
- package/src/chat/prompt.ts +157 -0
- package/src/chat/store.ts +35 -0
- package/src/chat/tools.ts +58 -0
- package/src/chat/transport.ts +70 -0
- package/src/components/AdrRow.tsx +29 -0
- package/src/components/AnchorLink.tsx +67 -0
- package/src/components/ApiReference.tsx +134 -0
- package/src/components/AsyncApiReference.tsx +120 -0
- package/src/components/CatalogStamp.tsx +155 -0
- package/src/components/ChannelRows.tsx +132 -0
- package/src/components/CommandRows.tsx +139 -0
- package/src/components/DiagramSkeleton.tsx +71 -0
- package/src/components/EditorLink.tsx +85 -0
- package/src/components/FieldTree.tsx +305 -0
- package/src/components/FlowTrigger.tsx +34 -0
- package/src/components/Ident.tsx +89 -0
- package/src/components/Integrations.tsx +243 -0
- package/src/components/LifecycleDiagram.tsx +110 -0
- package/src/components/MachineDocs.tsx +42 -0
- package/src/components/Markdown.tsx +173 -0
- package/src/components/MarkdownTable.tsx +206 -0
- package/src/components/Mermaid.tsx +211 -0
- package/src/components/MethodRows.tsx +309 -0
- package/src/components/Overlay.tsx +125 -0
- package/src/components/PageHeader.tsx +189 -0
- package/src/components/RowActions.tsx +97 -0
- package/src/components/SchemaDocument.tsx +89 -0
- package/src/components/Select.tsx +126 -0
- package/src/components/ShapeRows.tsx +118 -0
- package/src/components/SourceDoc.tsx +82 -0
- package/src/components/SourcePreview.tsx +441 -0
- package/src/components/TabRow.tsx +182 -0
- package/src/components/TechIcon.tsx +34 -0
- package/src/components/Toc.tsx +132 -0
- package/src/components/WhatLinksHere.tsx +273 -0
- package/src/components/WsdlReference.tsx +84 -0
- package/src/components/ddd-icons.tsx +107 -0
- package/src/components/kind.tsx +157 -0
- package/src/components/logo.tsx +61 -0
- package/src/components/primitives.tsx +264 -0
- package/src/data.test.ts +18 -0
- package/src/data.ts +202 -0
- package/src/enrich.test.ts +1150 -0
- package/src/enrich.ts +672 -0
- package/src/er/ErCanvas.tsx +533 -0
- package/src/er/RedisSchema.tsx +74 -0
- package/src/er/RelationCard.tsx +267 -0
- package/src/er/StoreHeader.tsx +155 -0
- package/src/er/TableNode.tsx +84 -0
- package/src/er/ViewNode.tsx +83 -0
- package/src/er/layout.test.ts +115 -0
- package/src/er/layout.ts +77 -0
- package/src/er/lineage.test.ts +140 -0
- package/src/er/lineage.ts +107 -0
- package/src/er/markers.tsx +79 -0
- package/src/er/spec.test.ts +344 -0
- package/src/er/spec.ts +426 -0
- package/src/flow/ChainList.tsx +162 -0
- package/src/flow/FlowTable.tsx +123 -0
- package/src/flow/FlowToolbar.tsx +372 -0
- package/src/flow/StepDetail.tsx +380 -0
- package/src/flow/StepRail.tsx +428 -0
- package/src/flow/answers.test.ts +50 -0
- package/src/flow/answers.ts +58 -0
- package/src/flow/chain.test.ts +383 -0
- package/src/flow/chain.ts +353 -0
- package/src/flow/chapters.test.ts +185 -0
- package/src/flow/chapters.ts +225 -0
- package/src/flow/continues.test.ts +102 -0
- package/src/flow/continues.ts +65 -0
- package/src/flow/cross-context.test.ts +97 -0
- package/src/flow/cross-context.ts +35 -0
- package/src/flow/mermaid.test.ts +22 -0
- package/src/flow/mermaid.ts +141 -0
- package/src/flow/outline.test.ts +212 -0
- package/src/flow/outline.ts +170 -0
- package/src/flow/paths.test.ts +129 -0
- package/src/flow/paths.ts +147 -0
- package/src/flow/prefs.test.ts +80 -0
- package/src/flow/prefs.ts +104 -0
- package/src/graph/DependencyGraph.tsx +283 -0
- package/src/graph/DependencyNodes.tsx +162 -0
- package/src/graph/EdgeLabel.tsx +61 -0
- package/src/graph/FocusedEventGraph.tsx +211 -0
- package/src/graph/GraphToolbar.tsx +187 -0
- package/src/graph/RoutedEdge.tsx +80 -0
- package/src/graph/dependency-layout.test.ts +360 -0
- package/src/graph/dependency-layout.ts +444 -0
- package/src/graph/elk.test.ts +70 -0
- package/src/graph/elk.ts +287 -0
- package/src/graph/nodes.tsx +97 -0
- package/src/graph/theme.ts +81 -0
- package/src/graph/useElkFlow.ts +125 -0
- package/src/index.css +1547 -0
- package/src/language/InLanguage.tsx +40 -0
- package/src/language/TermCard.tsx +132 -0
- package/src/language/cards.test.ts +114 -0
- package/src/language/cards.ts +111 -0
- package/src/lib/admonition.test.ts +49 -0
- package/src/lib/admonition.ts +48 -0
- package/src/lib/adr.test.ts +131 -0
- package/src/lib/adr.ts +68 -0
- package/src/lib/api.test.ts +122 -0
- package/src/lib/api.ts +135 -0
- package/src/lib/backlinks.test.ts +330 -0
- package/src/lib/backlinks.ts +567 -0
- package/src/lib/branch-compare.ts +16 -0
- package/src/lib/build-info.test.ts +76 -0
- package/src/lib/build-info.ts +80 -0
- package/src/lib/catalog-diff.test.ts +275 -0
- package/src/lib/catalog-diff.ts +655 -0
- package/src/lib/clipboard.ts +43 -0
- package/src/lib/comparison-memory.test.ts +40 -0
- package/src/lib/comparison-memory.ts +39 -0
- package/src/lib/context-color.ts +27 -0
- package/src/lib/context-map.test.ts +148 -0
- package/src/lib/context-map.ts +399 -0
- package/src/lib/data-model.test.ts +245 -0
- package/src/lib/data-model.ts +399 -0
- package/src/lib/data-problems.test.ts +178 -0
- package/src/lib/data-problems.ts +314 -0
- package/src/lib/derive.test.ts +251 -0
- package/src/lib/derive.ts +515 -0
- package/src/lib/editor-link.test.ts +125 -0
- package/src/lib/editor-link.ts +96 -0
- package/src/lib/editor-prefs.ts +27 -0
- package/src/lib/event-graph.test.ts +237 -0
- package/src/lib/event-graph.ts +327 -0
- package/src/lib/export-canvas.ts +37 -0
- package/src/lib/export-file.test.ts +30 -0
- package/src/lib/export-file.ts +20 -0
- package/src/lib/flow-tree.test.ts +177 -0
- package/src/lib/flow-tree.ts +160 -0
- package/src/lib/forge-refs.test.ts +50 -0
- package/src/lib/forge-refs.ts +60 -0
- package/src/lib/format.test.ts +35 -0
- package/src/lib/format.ts +64 -0
- package/src/lib/github-catalog.test.ts +301 -0
- package/src/lib/github-catalog.ts +610 -0
- package/src/lib/integrations.test.ts +119 -0
- package/src/lib/integrations.ts +133 -0
- package/src/lib/kinds.test.ts +91 -0
- package/src/lib/kinds.ts +204 -0
- package/src/lib/lifecycle.test.ts +93 -0
- package/src/lib/lifecycle.ts +165 -0
- package/src/lib/local-api.ts +194 -0
- package/src/lib/motion-features.ts +11 -0
- package/src/lib/motion.test.ts +120 -0
- package/src/lib/motion.tsx +269 -0
- package/src/lib/palette-recent.test.ts +80 -0
- package/src/lib/palette-recent.ts +90 -0
- package/src/lib/palette.test.ts +365 -0
- package/src/lib/palette.ts +593 -0
- package/src/lib/pins.test.ts +139 -0
- package/src/lib/pins.ts +130 -0
- package/src/lib/problems.test.ts +112 -0
- package/src/lib/project-preview.ts +5 -0
- package/src/lib/proto-problems.test.ts +121 -0
- package/src/lib/proto-problems.ts +65 -0
- package/src/lib/queries.test.ts +113 -0
- package/src/lib/queries.ts +131 -0
- package/src/lib/registry.test.ts +219 -0
- package/src/lib/registry.ts +208 -0
- package/src/lib/scenarios.ts +547 -0
- package/src/lib/setup-info.test.ts +199 -0
- package/src/lib/setup-info.ts +415 -0
- package/src/lib/shape.test.ts +289 -0
- package/src/lib/shape.ts +454 -0
- package/src/lib/sidebar-prefs.test.ts +32 -0
- package/src/lib/sidebar-prefs.ts +75 -0
- package/src/lib/source-code.test.ts +159 -0
- package/src/lib/source-code.ts +286 -0
- package/src/lib/source-doc.test.ts +131 -0
- package/src/lib/source-doc.ts +103 -0
- package/src/lib/source-highlight.test.ts +29 -0
- package/src/lib/source-highlight.ts +24 -0
- package/src/lib/source-link.test.ts +153 -0
- package/src/lib/source-link.ts +201 -0
- package/src/lib/spec-files.test.ts +24 -0
- package/src/lib/spec-files.ts +23 -0
- package/src/lib/table-directive.test.ts +139 -0
- package/src/lib/table-directive.ts +106 -0
- package/src/lib/tech.test.ts +120 -0
- package/src/lib/tech.ts +421 -0
- package/src/lib/terms.test.ts +67 -0
- package/src/lib/terms.ts +179 -0
- package/src/lib/wire-problems.test.ts +263 -0
- package/src/lib/wire-problems.ts +281 -0
- package/src/likec4/C4View.tsx +55 -0
- package/src/likec4/CanvasBridge.tsx +187 -0
- package/src/likec4/FlowView.tsx +235 -0
- package/src/likec4/InteractiveView.tsx +144 -0
- package/src/likec4/canvas-viewport.test.ts +149 -0
- package/src/likec4/canvas-viewport.ts +135 -0
- package/src/likec4/flow-edges.test.ts +51 -0
- package/src/likec4/flow-edges.ts +89 -0
- package/src/likec4/frame-theme.test.ts +76 -0
- package/src/likec4/frame-theme.ts +101 -0
- package/src/likec4/highlight-css.test.ts +49 -0
- package/src/likec4/highlight-css.ts +52 -0
- package/src/likec4/ids.test.ts +151 -0
- package/src/likec4/ids.ts +115 -0
- package/src/likec4/levels.tsx +70 -0
- package/src/likec4/mapping.test.ts +66 -0
- package/src/likec4/mapping.ts +54 -0
- package/src/likec4/reserved.json +141 -0
- package/src/likec4/view-index.ts +78 -0
- package/src/main.tsx +13 -0
- package/src/map/ContextMapGraph.tsx +274 -0
- package/src/map/ContextNode.tsx +70 -0
- package/src/map/FloatingEdge.tsx +86 -0
- package/src/merge.test.ts +1038 -0
- package/src/merge.ts +1004 -0
- package/src/pages/AdrDetail.tsx +242 -0
- package/src/pages/AdrIndex.tsx +114 -0
- package/src/pages/AggregatePage.tsx +727 -0
- package/src/pages/BlockPage.tsx +478 -0
- package/src/pages/CatalogFailure.tsx +111 -0
- package/src/pages/Changes.tsx +313 -0
- package/src/pages/ContextMap.tsx +534 -0
- package/src/pages/ContextPage.tsx +346 -0
- package/src/pages/EnumPage.tsx +282 -0
- package/src/pages/EventPage.tsx +686 -0
- package/src/pages/ExternalPage.tsx +204 -0
- package/src/pages/FlowDetail.tsx +663 -0
- package/src/pages/FlowIndex.tsx +303 -0
- package/src/pages/GraphPage.tsx +217 -0
- package/src/pages/Language.tsx +257 -0
- package/src/pages/ModulePage.tsx +366 -0
- package/src/pages/NotFound.tsx +54 -0
- package/src/pages/Overview.tsx +344 -0
- package/src/pages/Problems.tsx +360 -0
- package/src/pages/RegistryIndex.tsx +118 -0
- package/src/pages/ServicePage.tsx +746 -0
- package/src/pages/Settings.tsx +975 -0
- package/src/pages/StorePage.tsx +114 -0
- package/src/peek/PeekCard.test.ts +35 -0
- package/src/peek/PeekCard.tsx +118 -0
- package/src/peek/PeekLayer.tsx +147 -0
- package/src/peek/model.test.ts +156 -0
- package/src/peek/model.ts +253 -0
- package/src/routes.test.ts +222 -0
- package/src/routes.ts +449 -0
- package/src/selection/DetailPanel.tsx +1407 -0
- package/src/selection/hash.test.ts +73 -0
- package/src/selection/hash.ts +58 -0
- package/src/selection/model.test.ts +199 -0
- package/src/selection/model.ts +473 -0
- package/src/selection/pages.test.ts +115 -0
- package/src/selection/pages.ts +268 -0
- package/src/selection/store.test.ts +62 -0
- package/src/selection/store.ts +46 -0
- package/src/selection/sync.tsx +99 -0
- package/src/table/ColumnsMenu.tsx +72 -0
- package/src/table/DataTable.tsx +780 -0
- package/src/table/Facets.tsx +54 -0
- package/src/table/GroupMenu.tsx +69 -0
- package/src/table/Toolbar.tsx +190 -0
- package/src/table/cells.tsx +117 -0
- package/src/table/compare.test.ts +117 -0
- package/src/table/compare.ts +196 -0
- package/src/table/export.test.ts +88 -0
- package/src/table/export.ts +66 -0
- package/src/table/facet-groups.test.ts +74 -0
- package/src/table/facet-groups.ts +56 -0
- package/src/table/grouping.test.ts +84 -0
- package/src/table/grouping.ts +82 -0
- package/src/table/infer.test.ts +57 -0
- package/src/table/infer.ts +50 -0
- package/src/table/persist.test.ts +76 -0
- package/src/table/persist.ts +112 -0
- package/src/table/sort-url.test.ts +75 -0
- package/src/table/sort-url.ts +52 -0
- package/src/table/types.ts +80 -0
- package/src/table/useTableState.ts +220 -0
- package/src/test-catalog.ts +7 -0
- package/src/testing/estate/data__catalog.json +2847 -0
- package/src/testing/estate/data__flows.json +1724 -0
- package/src/testing/estate/examples__auth__portolan__api.json +212 -0
- package/src/testing/estate/examples__auth__portolan__domain.json +1204 -0
- package/src/testing/estate/examples__auth__portolan__glossary.json +226 -0
- package/src/testing/estate/examples__auth__portolan__observed.json +757 -0
- package/src/testing/estate/examples__auth__portolan__stores.json +195 -0
- package/src/testing/estate/examples__shop__cart__portolan__api.json +234 -0
- package/src/testing/estate/examples__shop__cart__portolan__commands.json +86 -0
- package/src/testing/estate/examples__shop__cart__portolan__domain.json +966 -0
- package/src/testing/estate/examples__shop__cart__portolan__observed.json +498 -0
- package/src/testing/estate/examples__shop__cart__portolan__stores.json +208 -0
- package/src/testing/estate/examples__shop__oms__portolan__glossary.json +98 -0
- package/src/testing/estate/examples__shop__oms__portolan__proto.json +186 -0
- package/src/testing/estate.ts +49 -0
- package/src/trail/Trail.tsx +81 -0
- package/src/trail/model.test.ts +157 -0
- package/src/trail/model.ts +189 -0
- package/src/trail/record.tsx +32 -0
- package/src/trail/store.test.ts +66 -0
- package/src/trail/store.ts +102 -0
- package/tsconfig.json +35 -0
- package/vite.config.ts +171 -0
|
@@ -0,0 +1,1160 @@
|
|
|
1
|
+
# Generators
|
|
2
|
+
|
|
3
|
+
A generator turns the catalog into something else. The built-in generators
|
|
4
|
+
produce Markdown and LLM indexes, standalone Mermaid diagrams, and a Backstage
|
|
5
|
+
Software Catalog bundle; each lives in its own directory beside the plugin.
|
|
6
|
+
|
|
7
|
+
The contract is one JSON message in and one JSON message out:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
→ { "portolanVersion": "0.1.0", "catalog": { ... }, "options": { ... } }
|
|
11
|
+
← { "files": [{ "name": "shop/oms/README.md", "contents": "..." }] }
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
There is a second question, asked with `"kind": "describe"`, and the answer is
|
|
15
|
+
the plugin itself rather than its work:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
→ { "portolanVersion": "0.1.0", "kind": "describe" }
|
|
19
|
+
← { "files": [],
|
|
20
|
+
"describe": { "name": "extract-go", "summary": "...",
|
|
21
|
+
"phases": ["extract"],
|
|
22
|
+
"options": { "type": "object", "additionalProperties": false,
|
|
23
|
+
"properties": { "context": { ... } } } } }
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The options a plugin takes are facts the source does not carry, so only the
|
|
27
|
+
plugin knows what it can be told. `npm run schema` asks all of them and composes
|
|
28
|
+
`schema/portolan.schema.json`, which an editor reads while the manifest is being
|
|
29
|
+
written and `gen` checks before it runs anything. `additionalProperties: false`
|
|
30
|
+
is what makes that worth having: `encoding/json` drops a field it does not
|
|
31
|
+
recognise, so before this a misspelled option was no option at all and nothing
|
|
32
|
+
said so.
|
|
33
|
+
|
|
34
|
+
A generator **names** files; it never writes them. `scripts/gen.mjs` writes what
|
|
35
|
+
comes back, refuses a name that points outside the output directory, and deletes
|
|
36
|
+
pages that stopped being generated. That is what lets a generator run as a wasm
|
|
37
|
+
module with no directory preopened at all — the sandbox is not a restriction
|
|
38
|
+
worked around, it is the reason the protocol has this shape.
|
|
39
|
+
|
|
40
|
+
Three obligations, and they are the whole of it:
|
|
41
|
+
|
|
42
|
+
1. **Valid output.** Every file a generator names is committed and read.
|
|
43
|
+
2. **Determinism.** The same catalog produces byte-identical output, so
|
|
44
|
+
reviewing generated documentation is reviewing a diff. Sort anything that
|
|
45
|
+
comes out of a map; never read a clock.
|
|
46
|
+
3. **One decisive response.** A malformed request, incompatible protocol,
|
|
47
|
+
unsafe filename or invalid response fails the run. Non-fatal extraction
|
|
48
|
+
notes go to stderr; there is no advisory response property a caller may
|
|
49
|
+
accidentally ignore. A note that opens with `warning: ` is kept beside the
|
|
50
|
+
step in `.portolan/build-report.json` and listed on the Settings page.
|
|
51
|
+
|
|
52
|
+
The repository enforces those obligations with schema/field coverage tests,
|
|
53
|
+
byte-for-byte permutation tests, generated-link and anchor checks, Mermaid
|
|
54
|
+
parser checks, and Backstage relationship validation. A new catalog field must
|
|
55
|
+
either be rendered or be explicitly acknowledged by the relevant exporter.
|
|
56
|
+
|
|
57
|
+
## Adding one
|
|
58
|
+
|
|
59
|
+
1. Write it. In Go, a new directory here with a `main` that hands its options
|
|
60
|
+
type to `plugin.Serve`, which reads the request, answers a describe and calls
|
|
61
|
+
the work; `catalog.Catalog` from `github.com/shortlink-org/portolan/catalog`
|
|
62
|
+
is the mirror of the schema, and `internal/goscan` is the tree as syntax -
|
|
63
|
+
the files parsed once, the import path of each package, the string
|
|
64
|
+
constants followed to their literals - which River and Watermill share and
|
|
65
|
+
the next Go extractor should not copy. In Python, the same three things live in
|
|
66
|
+
`pyplugin/` - `protocol.py`, `source.py` for the tree as syntax, and
|
|
67
|
+
`catalog.py` for the fragment shapes - and `extract-django` and
|
|
68
|
+
`extract-celery` are what using them looks like. In any other language,
|
|
69
|
+
anything that speaks the protocol above.
|
|
70
|
+
2. Describe it. An `options.schema.json` beside the source, embedded with
|
|
71
|
+
`go:embed` and returned in the descriptor. `schematest.Check` in a test keeps
|
|
72
|
+
it from drifting from the options struct: a field renamed on one side and not
|
|
73
|
+
the other fails, and so does an option with no description.
|
|
74
|
+
3. Build it. For a wasm plugin, `GOOS=wasip1 GOARCH=wasm go build`. Add the
|
|
75
|
+
line to `plugins:build` in `package.json`.
|
|
76
|
+
4. Declare it in `portolan.json`, under `plugins` (how to run it) and
|
|
77
|
+
`generate` (what to run it on), then run `npm run schema` so the manifest
|
|
78
|
+
schema learns its options.
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"plugins": [
|
|
83
|
+
{ "name": "markdown", "wasm": { "url": "file://plugins/gen-markdown.wasm" } }
|
|
84
|
+
],
|
|
85
|
+
"generate": [
|
|
86
|
+
{ "plugin": "markdown", "out": "docs", "options": { "title": "Example estate" } }
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Lifecycles
|
|
92
|
+
|
|
93
|
+
An aggregate whose root has a status gets a `lifecycle` on the catalog: the
|
|
94
|
+
states, the first being where a new root starts, and one move per edge - the
|
|
95
|
+
method that makes it and the event it hands back. All three extractors read it
|
|
96
|
+
off a table the code keeps, never off the branches of the methods, because the
|
|
97
|
+
table is the claim and the methods are held to it: an edge in the table no
|
|
98
|
+
method makes, a move into a state the table lacks, and a status changed
|
|
99
|
+
outside the one way through the table are each reported.
|
|
100
|
+
|
|
101
|
+
In Go the table is a `go-sdk/fsm` rule set, `var Rules = fsm.TransitionRuleSet{
|
|
102
|
+
StateLive: {EventRevoke: StateRevoked}, …}`, with the states and events as
|
|
103
|
+
string constants; the method whose body calls `TriggerEvent` is the mover, and
|
|
104
|
+
every exported method that hands it a constant makes the edges that constant
|
|
105
|
+
names. The event type a method returns is what its last move publishes - a
|
|
106
|
+
method lapsing a lock and then locking again hands `AccountLocked` back for
|
|
107
|
+
the lock. In TypeScript it is `export const TRANSITIONS = { open: ["checked-out"],
|
|
108
|
+
… }` and the method that assigns `this.status`; see `extract-ts/README.md`. In
|
|
109
|
+
Rust it is `pub const TRANSITIONS: &[(&str, &[&str])] = &[("placed", &["confirmed"]),
|
|
110
|
+
…]` and the method that assigns `self.status`, handed a string or a variant of
|
|
111
|
+
the status enum; see `extract-rust/README.md`. In Python it is that mapping on
|
|
112
|
+
the Django model, beside the `TextChoices` that names the states, and the
|
|
113
|
+
method assigning `self.status` - or django-fsm's `@transition(field=status,
|
|
114
|
+
source=…, target=…)`, which is the same table written one edge at a time; see
|
|
115
|
+
`extract-django/README.md`. In Java it is `TRANSITIONS` beside the status enum
|
|
116
|
+
and the method assigning `this.status`; Java is also the one language here with
|
|
117
|
+
a vocabulary for the model, so the rest of `extract-java` reads what jMolecules
|
|
118
|
+
declares rather than what the layout implies.
|
|
119
|
+
Terminal states are derived on the page - nothing leads out - and never
|
|
120
|
+
written down. A move the clock makes, a session expiring, a lock running out,
|
|
121
|
+
is not a move: nothing runs when it happens, so it is not in the table.
|
|
122
|
+
|
|
123
|
+
## Enums
|
|
124
|
+
|
|
125
|
+
An aggregate's fields take some of their values from closed sets - a reason,
|
|
126
|
+
a status, a code - and a consumer of its events switches on them. Those sets
|
|
127
|
+
are the aggregate's `enums`: one entry per set, id `<aggregate>.<slug>`,
|
|
128
|
+
the values in declaration order, each with its doc and a `deprecated` mark
|
|
129
|
+
when the source carries one. A status enum is read here as well as by the
|
|
130
|
+
lifecycle: the lifecycle keeps the moves, the enum keeps the doc on each
|
|
131
|
+
value, and a page may draw both.
|
|
132
|
+
|
|
133
|
+
What counts as one is a convention per language, and each extractor's README
|
|
134
|
+
says which. In Go, which has no enum, it is a named type over a basic one -
|
|
135
|
+
`type Reason string` - and a const block whose constants are of that type,
|
|
136
|
+
looked for in the aggregate's package, under `vo/`, and under `event/`;
|
|
137
|
+
a value's name is the constant's literal, because that is what the wire
|
|
138
|
+
carries, and the constant's own name only for an iota. A `Deprecated:`
|
|
139
|
+
paragraph in the doc marks the value. In Rust it is a `pub enum` whose every
|
|
140
|
+
variant is a bare name, the literal an `as_str` answers standing in for the
|
|
141
|
+
variant. In Java it is a top-level enum in the aggregate's package. In proto,
|
|
142
|
+
the enums the messages reach through their fields sit on the interface as
|
|
143
|
+
`enums`, with the numbers the wire uses.
|
|
144
|
+
|
|
145
|
+
## A repository without a domain model
|
|
146
|
+
|
|
147
|
+
`extract-project` is the baseline extractor for any repository. It reads only
|
|
148
|
+
README and build/deployment manifests, emits a neutral group and component,
|
|
149
|
+
and leaves `aggregates` empty. This is intentional: a package called `domain`
|
|
150
|
+
is not evidence that the project models aggregates. OpenAPI, AsyncAPI,
|
|
151
|
+
GraphQL, proto and SQL extractors merge their facts into the same component.
|
|
152
|
+
|
|
153
|
+
The language-specific domain extractors are optional enrichments. The local
|
|
154
|
+
setup wizard offers one only when it finds the structure that extractor
|
|
155
|
+
requires; for Go this means an aggregate package in either
|
|
156
|
+
`internal/domain/<aggregate>` or `internal/<aggregate>/domain`, containing the
|
|
157
|
+
root struct named after that aggregate. The extractor follows the same choice
|
|
158
|
+
for application use cases, transport adapters, policies, integration-event
|
|
159
|
+
DTOs, and assembly bindings, so horizontal layers and feature slices can
|
|
160
|
+
coexist while a service is being migrated.
|
|
161
|
+
|
|
162
|
+
The SQL extractor follows the same migration path. With no `repositories` or
|
|
163
|
+
`projectors` option it discovers both
|
|
164
|
+
`internal/infrastructure/repository/<aggregate>/migrations` and
|
|
165
|
+
`internal/<aggregate>/infrastructure/repository/migrations` (and the matching
|
|
166
|
+
projector forms), merging every package into one store. An explicit root keeps
|
|
167
|
+
the original collection layout for TypeScript, Rust, Java, or custom trees and
|
|
168
|
+
may also point directly at one feature repository.
|
|
169
|
+
|
|
170
|
+
`extract-river` is another independent enrichment for Go repositories. It
|
|
171
|
+
joins a job argument's `Kind()` to `Client.Insert`/`InsertTx`, the selected
|
|
172
|
+
queue, `Worker[Args].Work`, and `river.AddWorker`. The result is a work-queue
|
|
173
|
+
channel plus a two-hop enqueue/dispatch flow, with payload fields and source
|
|
174
|
+
lines. It does not need aggregates and does not treat a job as a domain event.
|
|
175
|
+
|
|
176
|
+
`extract-redis` finds runtime construction of go-redis, rueidis and redigo
|
|
177
|
+
clients in non-test Go source. That source evidence adds a service-owned Redis
|
|
178
|
+
store to the catalog even when the repository has no SQL migrations or domain
|
|
179
|
+
model. It follows literals, constants, concatenation, `fmt.Sprintf`, helper
|
|
180
|
+
functions and conditional suffixes into common Redis operations, producing
|
|
181
|
+
key patterns with their read/write/delete use, TTL, value type and source. The
|
|
182
|
+
patterns remain Redis keyspaces rather than being presented as SQL tables.
|
|
183
|
+
|
|
184
|
+
`extract-watermill` reads Watermill `Router.AddHandler` and
|
|
185
|
+
`AddNoPublisherHandler` registrations. It resolves literal and constant topics,
|
|
186
|
+
plus defaults on env-config structs, follows direct `Publisher.Publish` calls
|
|
187
|
+
and one-hop publishing helpers, and traces JSON marshal/unmarshal values back to
|
|
188
|
+
their Go payload structs. It carries enclosing `if`/`else` conditions and early
|
|
189
|
+
returns through the handler's control flow: proven alternatives become one
|
|
190
|
+
catalog `alt`; publications whose relationship cannot be proven remain separate
|
|
191
|
+
possible routes. The channels merge normally with AsyncAPI declarations by address. Generic
|
|
192
|
+
`NewEventHandler[T]` and `NewCommandHandler[T]` registrations on Watermill CQRS
|
|
193
|
+
processors are also extracted; fixed topic generators and the standard
|
|
194
|
+
event/command-name generator form are resolved from source.
|
|
195
|
+
|
|
196
|
+
`extract-go-nats` reads nats.go and JetStream calls into the subjects a service
|
|
197
|
+
listens on and publishes to. A call is known by the type it is made on -
|
|
198
|
+
`*nats.Conn`, `nats.JetStreamContext`, `jetstream.JetStream`, `jetstream.Stream` -
|
|
199
|
+
and not by its name, because a service's own bus port has a `Subscribe` too.
|
|
200
|
+
The subject is followed to a literal, a constant, a config default, or a
|
|
201
|
+
parameter; a parameter is followed up to two hops through the callers,
|
|
202
|
+
including calls through an interface the adapter satisfies, which is how a
|
|
203
|
+
port `Subscribe(ctx, topic, name, handler)` reads as the assembly's
|
|
204
|
+
`Subscribe(ctx, cart.Topic, cart.BasketCheckedOut{}.Name(), …)`. When the port
|
|
205
|
+
takes exactly one other string beside the subject, that string is the
|
|
206
|
+
message's name; a direct call names no message, and its direction is in the
|
|
207
|
+
channel's doc. A subject read off a database row is a warning at the call,
|
|
208
|
+
not a channel. Consumer configs give the filter subject and the durable name;
|
|
209
|
+
streams, wildcard subjects and work-queue retention are not read yet.
|
|
210
|
+
|
|
211
|
+
`extract-python-kafka` is the framework-independent Kafka enrichment for
|
|
212
|
+
Python. It recognizes confluent-kafka, kafka-python and aiokafka by their
|
|
213
|
+
imported client types, follows literal topics through constants, environment
|
|
214
|
+
defaults, settings and local factories, and emits generic message streams with
|
|
215
|
+
producer and consumer flows. Only client-side configuration proven in source
|
|
216
|
+
is retained; authentication values are omitted, while broker-side partitions,
|
|
217
|
+
replication and retention remain explicitly unknown. See
|
|
218
|
+
`extract-python-kafka/README.md` for the supported call shapes.
|
|
219
|
+
|
|
220
|
+
`extract-wsdl` reads WSDL 1.1 contracts as structured SOAP APIs. It follows
|
|
221
|
+
local WSDL imports and XSD imports/includes without network access, keeps
|
|
222
|
+
distinct services, ports and SOAP 1.1/1.2 bindings, and records operation
|
|
223
|
+
actions, request/response messages, faults, headers and reachable XSD shapes.
|
|
224
|
+
It can describe a contract implemented by the component or a vendored copy for
|
|
225
|
+
an external system (`mode: external`). Remote imports are reported as missing
|
|
226
|
+
evidence rather than fetched during generation.
|
|
227
|
+
|
|
228
|
+
`extract-http-clients` is the outbound counterpart and does not require a
|
|
229
|
+
domain layout. It reads `net/http` request construction, calls through an
|
|
230
|
+
`oapi-codegen` client, and SOAP `Call`/`CallContext` sites. A generated client
|
|
231
|
+
is joined to the OpenAPI document beside it, so the call uses the document's
|
|
232
|
+
operation id and lands on an external with the contract the document declares.
|
|
233
|
+
SOAP actions are joined to WSDL bindings when the action matches. Generated and
|
|
234
|
+
hand-written wrapper signatures are learned from their call into the SOAP
|
|
235
|
+
transport or their `SOAPAction`/SOAP 1.2 content-type header, so the action,
|
|
236
|
+
request and response positions are taken from code rather than assumed. A
|
|
237
|
+
raw request whose peer or contract cannot be proved is still useful evidence:
|
|
238
|
+
it is emitted as `unresolved`, with its method, path and source line, rather
|
|
239
|
+
than being assigned to a guessed system. Conditions guarding a call and the
|
|
240
|
+
opposite path after an early return are carried into the flow note.
|
|
241
|
+
Calls through local wrappers retain their argument values, including closure
|
|
242
|
+
arguments, so a path and HTTP method declared by a business operation survive
|
|
243
|
+
the trip into the transport. URL-shaped configuration is followed through a
|
|
244
|
+
constructor and client field to the request, and the resulting evidence chain
|
|
245
|
+
is included in the flow instead of presenting a receiver field as an endpoint.
|
|
246
|
+
Inbound composition also follows fixed factories, string-keyed constructor
|
|
247
|
+
maps, capability type assertions, and interface fields wired by composite
|
|
248
|
+
literals, direct assignments, or setters. A standalone flow says whether no
|
|
249
|
+
source caller exists or callers exist but no inbound/asynchronous root was
|
|
250
|
+
proved, so the UI exposes the missing evidence instead of implying a complete
|
|
251
|
+
business path.
|
|
252
|
+
When a provider branch still ends before its transport, the extractor loads
|
|
253
|
+
the module with `go/packages`, builds SSA, and uses `x/tools` VTA to resolve
|
|
254
|
+
calls through interface parameters, function values, return values, and
|
|
255
|
+
interface-typed struct fields. Typed edges are followed only after a concrete
|
|
256
|
+
factory branch is selected: applying context-insensitive VTA to a shared
|
|
257
|
+
dispatcher would otherwise attach every request implementation to every
|
|
258
|
+
endpoint. Factory conditions and HTTP/SOAP meaning continue to come from the
|
|
259
|
+
source extractor. Module loading is read-only and bounded; unavailable private
|
|
260
|
+
dependencies, type errors, or a timeout produce a warning and retain the
|
|
261
|
+
syntax-only result rather than failing generation.
|
|
262
|
+
Routes without a provider factory are also joined to their outbound calls,
|
|
263
|
+
including handlers invoked from closures and methods on locally constructed
|
|
264
|
+
values. A Swagger `@Router` annotation is medium-confidence root evidence for
|
|
265
|
+
handler factories behind custom registries. Direct calls reached through a
|
|
266
|
+
`main → Run`/`Start`/`Bootstrap` assembly path become high-confidence startup
|
|
267
|
+
flows. `AddFunc`, `AfterFunc`, and `Schedule` registrations create scheduled
|
|
268
|
+
roots when their handler reaches an outbound call. Extracted HTTP, callback,
|
|
269
|
+
startup, scheduled, River-job and Watermill-event flows publish that trigger
|
|
270
|
+
provenance; unmatched transport fragments are explicitly `unproven` with low
|
|
271
|
+
confidence.
|
|
272
|
+
|
|
273
|
+
After fragments merge, Portolan composes them into root-oriented,
|
|
274
|
+
cross-protocol flows. The seam is machine evidence rather than a display-name
|
|
275
|
+
match: an exact source function reached by another extractor, a River job's
|
|
276
|
+
queue plus `Kind()`, or a Watermill transport plus topic address.
|
|
277
|
+
Composition is recursive, so one request can continue through provider
|
|
278
|
+
HTTP/SOAP calls, enqueue a River job, enter its worker, and make further
|
|
279
|
+
outbound calls. Ambiguous handoffs remain separate. Each composed flow records
|
|
280
|
+
the source fragment slugs it includes, and the UI exposes that provenance as a
|
|
281
|
+
`cross-protocol` badge. Trigger-bearing job and event flows stay available as
|
|
282
|
+
standalone entry views; transport-only fragments consumed by a proven root are
|
|
283
|
+
removed from the top-level flow list.
|
|
284
|
+
|
|
285
|
+
## Flows written by hand
|
|
286
|
+
|
|
287
|
+
Some flows will always be written by people: the design doc for something not
|
|
288
|
+
built yet, the reconstruction after an incident, the path no test pins. The
|
|
289
|
+
catalog's JSON is the wrong place to write one - a tree of nodes, a unique id
|
|
290
|
+
per step, every lane declared twice - so `extract-flows` reads a text form
|
|
291
|
+
that reads like the sequence diagram it becomes: one file per flow, one line
|
|
292
|
+
per hop, frames closed by `end`. The estate here declares no such step: every
|
|
293
|
+
flow it shows is read out of code that runs, and one written by hand over files
|
|
294
|
+
nobody wrote would be the single page in the catalog nothing holds to account.
|
|
295
|
+
A repository that has a real one points an `extract-flows` step at the directory
|
|
296
|
+
the files live in.
|
|
297
|
+
|
|
298
|
+
```markdown
|
|
299
|
+
# Order accepted
|
|
300
|
+
owner: shop
|
|
301
|
+
source: services/oms/test/integration/order_accepted_test.go
|
|
302
|
+
|
|
303
|
+
The narrow slice one integration test pins end to end.
|
|
304
|
+
|
|
305
|
+
## Participants
|
|
306
|
+
- oms-db: store in shop "oms-db (postgres)"
|
|
307
|
+
- psp-gateway: external "psp-gateway (external)"
|
|
308
|
+
|
|
309
|
+
## Steps
|
|
310
|
+
shop.oms -> oms-db: insertOrderAndOutboxRow [verified] @internal/oms/adapter/postgres/order_repo.go:141 #a1
|
|
311
|
+
> The order row and the outbox row commit in one transaction.
|
|
312
|
+
shop.oms -> bus: event shop.oms.order.OrderPlaced [verified]
|
|
313
|
+
shop.oms -> shop.pricing: rpc shop.v1.Pricing/GetQuote as "GetQuote (250 ms)"
|
|
314
|
+
|
|
315
|
+
alt score below 40 #alt-risk
|
|
316
|
+
bus -> payments.ledger: event shop.oms.order.OrderPlaced
|
|
317
|
+
else score at or above 40
|
|
318
|
+
shop.oms -> bus: event shop.oms.order.OrderCancelled
|
|
319
|
+
stop
|
|
320
|
+
else
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
par OrderPlaced fan-out
|
|
324
|
+
bus -> payments.ledger: event shop.oms.order.OrderPlaced
|
|
325
|
+
and
|
|
326
|
+
bus -> delivery.core: event shop.oms.order.OrderPlaced
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
loop outbox relay, every 200 ms until the batch is empty
|
|
330
|
+
shop.oms -> oms-db: SELECT ... FOR UPDATE SKIP LOCKED
|
|
331
|
+
end
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
The head is the name, `owner:` (the context the flow belongs to), an optional
|
|
335
|
+
`source:` (where it was read from; the file itself when left out) and an
|
|
336
|
+
optional `slug:` (the file's name when left out), then the summary. A hop is
|
|
337
|
+
`from -> to: [call|rpc|event] label-or-ref` - `call` when no kind is written -
|
|
338
|
+
followed in any order by `as "label"`, `[status]`, `@where` and `#id`. An
|
|
339
|
+
event names its ref, `shop.oms.order.OrderPlaced`; an rpc names its call id,
|
|
340
|
+
`shop.v1.Pricing/GetQuote`, or, for a call no interface declares - a webhook
|
|
341
|
+
arriving on a route - just a label. The label of an event or rpc is the last
|
|
342
|
+
segment of its ref unless `as` says otherwise; the status is `declared`
|
|
343
|
+
unless written; `@` is a file and line, or wherever the hop was seen. A note is
|
|
344
|
+
the `>` lines under the hop. Ids are numbered unless given, and giving them
|
|
345
|
+
is what keeps deep links to a step stable across edits.
|
|
346
|
+
|
|
347
|
+
`alt <when> … else <when> … end` is a choice; `else` alone is "otherwise", a
|
|
348
|
+
branch with nothing in it is allowed, and `stop` as the last line of a branch
|
|
349
|
+
says the flow ends there rather than rejoining. `par [title] … and … end`
|
|
350
|
+
runs its branches side by side; `loop <until> … end` repeats. Frames nest.
|
|
351
|
+
|
|
352
|
+
Services are known by their `context.service` id, `bus` and `client` by name;
|
|
353
|
+
any other lane is declared under Participants, in the order the lanes should
|
|
354
|
+
be drawn, as `- <id>: <kind> [in <context>] ["label"]`. Lines starting with
|
|
355
|
+
`//` are comments. A mistake fails the run with its file and line, the way a
|
|
356
|
+
compiler would: a flow silently left out is the kind of missing nobody
|
|
357
|
+
notices. A ref that resolves to nothing is caught later, by the validator,
|
|
358
|
+
because only the merged catalog can say.
|
|
359
|
+
|
|
360
|
+
## Decisions written by hand
|
|
361
|
+
|
|
362
|
+
Every decision worth keeping is already written down, in a file beside the code
|
|
363
|
+
it constrains, in the MADR shape a person reads. Typing it a second time into
|
|
364
|
+
the catalog's JSON - an id, a slug, a number, a scope and a body with every
|
|
365
|
+
newline escaped - makes the JSON the source and the markdown a copy, and the
|
|
366
|
+
copy is the one that goes stale: the three records that lived in
|
|
367
|
+
`data/catalog.json` before `extract-adr` existed all named files that were
|
|
368
|
+
never in the repository at all.
|
|
369
|
+
|
|
370
|
+
So the markdown is the source and the fragment is the output. `extract-adr`
|
|
371
|
+
reads `docs/adr/*.md` under a service's root - a `README.md` among them is the
|
|
372
|
+
directory's index, not a decision, and is skipped - and answers with one
|
|
373
|
+
fragment holding the records.
|
|
374
|
+
|
|
375
|
+
```markdown
|
|
376
|
+
# auth.0003 — Session expiry publishes no event
|
|
377
|
+
|
|
378
|
+
- **Status:** accepted
|
|
379
|
+
- **Date:** 2026-08-22
|
|
380
|
+
- **Scope:** auth.auth
|
|
381
|
+
- **Superseded by:** auth.0007
|
|
382
|
+
- **Supersedes:** auth.0001, auth.0002
|
|
383
|
+
- **Relates:** auth.auth.session.SessionEnded, shop.cart, checkout
|
|
384
|
+
- **Note:** how a revocation is kept out of the cache was decided again in
|
|
385
|
+
auth.0010; the drop described below turned out not to be enough.
|
|
386
|
+
|
|
387
|
+
## Context and Problem Statement
|
|
388
|
+
…
|
|
389
|
+
## Decision Outcome
|
|
390
|
+
…
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
The title carries the record's id, an em dash, and the title. The id is a
|
|
394
|
+
prefix and four padded digits, and the prefix is whatever the record is about -
|
|
395
|
+
a service (`auth`, `cart`, `oms`), a context (`payments`) or the organisation
|
|
396
|
+
(`org`). The file is named `NNNN-kebab-slug.md` with the same number, because
|
|
397
|
+
the slug the catalog keeps is built from both: the id with its dots opened out,
|
|
398
|
+
then the file's kebab, as in `auth-0003-expiry-publishes-nothing`. A file
|
|
399
|
+
renamed away from its record would silently change the address of a decision
|
|
400
|
+
somebody linked to, so the two are held against each other.
|
|
401
|
+
|
|
402
|
+
`Status` and `Date` are required, and so is `Scope` unless the step's `scope`
|
|
403
|
+
option names it for the whole tree; the rest are written when there is
|
|
404
|
+
something to write, and a bullet the format does not have fails the record
|
|
405
|
+
rather than being dropped. A bullet may wrap onto the next line, indented under
|
|
406
|
+
itself - the break is the author's line width and closes up into a space.
|
|
407
|
+
`Scope` says what the record is about by how many segments it has: none, or
|
|
408
|
+
`org`, for the organisation, one for a context, two for `<context>.<service>`.
|
|
409
|
+
`Relates` names events, services and flows in one list and they are told apart
|
|
410
|
+
by their shape - a flow by its slug, which has no dots, a service by
|
|
411
|
+
`<context>.<service>`, an event by the aggregate and `Name` after that -
|
|
412
|
+
because an author should not have to remember which of three lists a name
|
|
413
|
+
belongs in. `Note` is prose no other field holds, most often that part of a
|
|
414
|
+
record was decided again somewhere else without the whole of it being
|
|
415
|
+
superseded; it sits in the page's header, above the frozen body.
|
|
416
|
+
|
|
417
|
+
Everything from the first `##` onward is the record. It goes into the catalog
|
|
418
|
+
exactly as written and comes back out onto the page the same way, headings and
|
|
419
|
+
all: an ADR is frozen history, and nothing on its page is redrawn from the
|
|
420
|
+
model as it stands now. Prose above that first `##` is a mistake - a paragraph
|
|
421
|
+
that drifted up there would be read by a person and dropped by the extractor.
|
|
422
|
+
|
|
423
|
+
The [adr-tools](https://github.com/npryce/adr-tools) shape is read too: number
|
|
424
|
+
and title on the title line, `Date:` above the record, status as the first
|
|
425
|
+
section with its `Superseded by` / `Supersedes` link. It has no prefix or
|
|
426
|
+
scope, so both come from the step's `scope` option.
|
|
427
|
+
|
|
428
|
+
`src/catalog.ts` fails the whole app on load if a record breaks any of its
|
|
429
|
+
rules, so the extractor checks first, where the file that caused it can be
|
|
430
|
+
named: ids and slugs unique, an id ending in its own zero-padded number, a date
|
|
431
|
+
that parses, a status from the five, and both halves of a supersession. A file
|
|
432
|
+
that breaks one of them is left out with a warning naming the file and line,
|
|
433
|
+
and the rest of the tree is read; only a supersession with one half recorded
|
|
434
|
+
refuses the whole run. That is why `Superseded by` and `Supersedes` are two
|
|
435
|
+
bullets rather than one derived from the other - supersession is a two-way
|
|
436
|
+
fact, and half of it recorded is a bug. The halves that live in one step's
|
|
437
|
+
tree are held against each other there; a record superseded by one in another
|
|
438
|
+
service's tree is a claim only the merged catalog can check, and the validator
|
|
439
|
+
checks it. The same goes for `Scope` and `Relates`: an extractor sees one root,
|
|
440
|
+
so it validates the shape of a name and leaves whether the thing exists to the
|
|
441
|
+
far side.
|
|
442
|
+
|
|
443
|
+
The demo estate's org-wide and context-wide records live in `data/adr`, and are
|
|
444
|
+
read by a step that points at that directory with a glob of its own. Root
|
|
445
|
+
`docs/` is where `gen-markdown` writes, so nothing hand-written can live there.
|
|
446
|
+
`in` is the directory of records rather than `data` itself: a step's fragment is
|
|
447
|
+
only left out of its own stamp when the output is *inside* the input root, and
|
|
448
|
+
`in: data` with `out: data` would be stamped from the file it writes.
|
|
449
|
+
|
|
450
|
+
```json
|
|
451
|
+
{
|
|
452
|
+
"plugins": [{ "name": "adr", "process": { "cmd": "go run ./plugins/extract-adr" } }],
|
|
453
|
+
"extract": [
|
|
454
|
+
{
|
|
455
|
+
"plugin": "adr",
|
|
456
|
+
"in": "data/adr",
|
|
457
|
+
"out": "data",
|
|
458
|
+
"options": { "files": ["*.md"], "out": "adr.json" }
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
"plugin": "adr",
|
|
462
|
+
"in": "examples/auth",
|
|
463
|
+
"out": "examples/auth/portolan",
|
|
464
|
+
"options": { "out": "adr.json" }
|
|
465
|
+
}
|
|
466
|
+
]
|
|
467
|
+
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
## Vocabulary written by hand
|
|
471
|
+
|
|
472
|
+
A context's glossary is the one file in a service written for a person and read
|
|
473
|
+
by everyone: what a word means inside the boundary it is spoken in. Nothing
|
|
474
|
+
generates it and nothing should - a definition is a decision about language,
|
|
475
|
+
not a fact about a type - so `extract-glossary` only reads. It takes
|
|
476
|
+
`GLOSSARY.md` at a service's root and answers with a fragment of terms.
|
|
477
|
+
|
|
478
|
+
```markdown
|
|
479
|
+
# Glossary — auth
|
|
480
|
+
|
|
481
|
+
One meaning per word inside this context.
|
|
482
|
+
|
|
483
|
+
**Session.** Proof that a user logged in, how long that proof is good for, and
|
|
484
|
+
whether it has been taken away.
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
A title, an optional line or two saying what the vocabulary covers, then one
|
|
488
|
+
paragraph per term in alphabetical order. The paragraph opens with the term in
|
|
489
|
+
bold and the full stop inside the bold, so `**Email address.**` names a
|
|
490
|
+
two-word term and nothing has to guess where the name ends. Everything after it
|
|
491
|
+
is the definition, carried through as written. Hard wrapping is the author's
|
|
492
|
+
business: a soft break inside a paragraph is a space, here as in every markdown
|
|
493
|
+
renderer.
|
|
494
|
+
|
|
495
|
+
Nothing reads the definition for structure. A glossary is a person explaining a
|
|
496
|
+
word to another person, and a parser that went looking for shapes inside the
|
|
497
|
+
explanation would be a parser telling an estate how to phrase itself.
|
|
498
|
+
|
|
499
|
+
The shapes a glossary is otherwise written in are refused by name: a table, a
|
|
500
|
+
bullet list, a heading per term. Also refused: a file that does not open with
|
|
501
|
+
`# Glossary`, an entry that defines nothing, and one word defined twice -
|
|
502
|
+
inside a file or across the files of one step - because a word with two
|
|
503
|
+
meanings in one context is the failure the glossary exists to prevent.
|
|
504
|
+
|
|
505
|
+
What is merely untidy comes back as a warning and the fragment is still
|
|
506
|
+
written: a file that has drifted out of alphabetical order, a root with no
|
|
507
|
+
glossary at all.
|
|
508
|
+
|
|
509
|
+
The term's id is `<context>.<slug>` - `auth.session`, `shop.order` - so the
|
|
510
|
+
context has to be told to the step rather than derived from the directory: a
|
|
511
|
+
glossary sits beside a SERVICE, and `examples/shop/oms/GLOSSARY.md` holds
|
|
512
|
+
words that belong to `shop`. The same word in two contexts is two terms, which
|
|
513
|
+
is the point of the id; the same word twice in one context is an error.
|
|
514
|
+
|
|
515
|
+
```json
|
|
516
|
+
{
|
|
517
|
+
"plugins": [{ "name": "glossary", "process": { "cmd": "go run ./plugins/extract-glossary" } }],
|
|
518
|
+
"extract": [
|
|
519
|
+
{
|
|
520
|
+
"plugin": "glossary",
|
|
521
|
+
"in": "examples/auth",
|
|
522
|
+
"out": "examples/auth/portolan",
|
|
523
|
+
"options": { "context": "auth", "out": "glossary.json" }
|
|
524
|
+
}
|
|
525
|
+
]
|
|
526
|
+
}
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
## Commands: what to type
|
|
530
|
+
|
|
531
|
+
The first thing a reader new to a checkout wants is not the aggregate list but
|
|
532
|
+
`how do I build this, test it, run it`. The README answers that sometimes; the
|
|
533
|
+
runner files answer it always, and they are already in the tree. `extract-commands`
|
|
534
|
+
reads them and puts the answer on the service as a list of commands - the line
|
|
535
|
+
to type, what the file says it is for, what the runner would execute, and where
|
|
536
|
+
it was read.
|
|
537
|
+
|
|
538
|
+
| File | Runner | Line to type | Description read from |
|
|
539
|
+
| --- | --- | --- | --- |
|
|
540
|
+
| `Makefile` | make | `make <target>` | `## comment` on the target's line, else the `#` block over it |
|
|
541
|
+
| `justfile` | just | `just <recipe>` | `[doc("...")]`, else the `#` block over it |
|
|
542
|
+
| `Taskfile.yml` | task | `task <name>` | `desc`, else `summary` |
|
|
543
|
+
| `package.json` `scripts` | npm, or pnpm/yarn/bun by the lockfile | `npm run <name>`; `npm test`, `npm start` | nothing - the script line is the body |
|
|
544
|
+
| `pyproject.toml` | poe (`[tool.poe.tasks]`), pdm (`[tool.pdm.scripts]`) | `poe <name>`, `pdm run <name>` | `help` |
|
|
545
|
+
| `pom.xml` | mvn, or `./mvnw` when the wrapper is there | `mvn test`, `mvn package`, and the goals of declared plugins: `spring-boot:run`, `quarkus:dev`, `flyway:migrate`, … | the goal's own purpose |
|
|
546
|
+
| `build.gradle(.kts)` | gradle, or `./gradlew` | `gradle build`, `gradle test`; `run` with the application plugin, `bootRun` with Spring Boot; every `tasks.register("...")` | the task's `description` |
|
|
547
|
+
| `.cargo/config.toml` | cargo | `cargo <alias>` for each `[alias]`; when one runs the `xtask` package, `cargo xtask <sub>` for each subcommand of its `main.rs` | `///` over a clap variant, `//` over a match arm |
|
|
548
|
+
|
|
549
|
+
Left out, because the runner leaves them out too: `_`-prefixed and `[private]`
|
|
550
|
+
entries, `internal` tasks, make's special and pattern targets (`.PHONY`, `%.o`),
|
|
551
|
+
targets spelled through a variable, and `pre`/`post` hooks of a script that is
|
|
552
|
+
itself listed. `[project.scripts]` in a pyproject is not read: those are
|
|
553
|
+
programs the package installs, not tasks a developer runs. `uv` has no task
|
|
554
|
+
section and Go has no runner of its own, so a repository with only those
|
|
555
|
+
declares no commands.
|
|
556
|
+
|
|
557
|
+
Maven and Gradle are the other way round: the build is the runner, and what
|
|
558
|
+
a project declares is which plugins extend it. So a pom lists the two
|
|
559
|
+
lifecycle phases every pom answers to, `test` and `package`, and then the
|
|
560
|
+
goals a person types that its `<build><plugins>` add - `spring-boot:run` for
|
|
561
|
+
the Spring Boot plugin, `flyway:migrate` for Flyway - from a short table of
|
|
562
|
+
the plugins an estate meets. A plugin bound to a phase, like protobuf
|
|
563
|
+
generation, is run by the phase and is not listed; neither is one under
|
|
564
|
+
`<pluginManagement>`, which pins a version and runs nothing. A Gradle script
|
|
565
|
+
lists `build` and `test`, `run` when it applies the application plugin,
|
|
566
|
+
`bootRun` for Spring Boot, and every task it registers itself, with the
|
|
567
|
+
description it sets. The script is read as text, not run: a task registered
|
|
568
|
+
in a loop or by an unnamed plugin is not here.
|
|
569
|
+
|
|
570
|
+
Cargo declares commands in one place, `[alias]` in `.cargo/config.toml`, and
|
|
571
|
+
the xtask convention is an alias that runs a package: `xtask = "run --package
|
|
572
|
+
xtask --"`. Each alias is a command with its expansion as the body, and when
|
|
573
|
+
the expansion runs a package and leaves the subcommand to the caller, that
|
|
574
|
+
package's `main.rs` is read for the subcommands - the variants of a clap enum
|
|
575
|
+
deriving `Subcommand`, in kebab-case, with their `///` comment as the doc, and
|
|
576
|
+
the string literals of a `match` on the first argument, with a `//` comment
|
|
577
|
+
over the arm. An alias that names its subcommand already, like `gen = "run -p
|
|
578
|
+
xtask -- gen"`, is listed as the one command it is.
|
|
579
|
+
|
|
580
|
+
Nothing is evaluated. A target inside an `ifeq` is listed; a name that depends
|
|
581
|
+
on a variable's value is not, because the name it would have is not in the
|
|
582
|
+
file. The pyproject reader is a line scanner that follows the table headers
|
|
583
|
+
and the shapes both runners document, not a TOML parser.
|
|
584
|
+
|
|
585
|
+
The fragment names the service and lists its commands, and claims nothing
|
|
586
|
+
else: merging puts the list on the service a domain extractor described. The
|
|
587
|
+
runner files sit beside the service, so the step is told which one it is.
|
|
588
|
+
|
|
589
|
+
```json
|
|
590
|
+
{
|
|
591
|
+
"plugins": [{ "name": "commands", "process": { "cmd": "go run ./plugins/extract-commands" } }],
|
|
592
|
+
"extract": [
|
|
593
|
+
{
|
|
594
|
+
"plugin": "commands",
|
|
595
|
+
"in": "examples/shop/pricing",
|
|
596
|
+
"out": "examples/shop/pricing/portolan",
|
|
597
|
+
"options": { "context": "shop", "service": "pricing", "out": "commands.json" }
|
|
598
|
+
}
|
|
599
|
+
]
|
|
600
|
+
}
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
`extract-project` reads the same files for the component it describes, so a
|
|
604
|
+
repository read by that plugin does not need this one.
|
|
605
|
+
|
|
606
|
+
Downstream, `gen-markdown` draws the list as a Commands table on the service
|
|
607
|
+
page, and `gen-backstage` puts it on the Component twice, because Backstage
|
|
608
|
+
has no field for it: as the `portolan.io/commands` annotation, one command a
|
|
609
|
+
line with its description, and as entity links of type `command`, each
|
|
610
|
+
leading to the line of the runner file it was read from, when `sourceBaseUrl`
|
|
611
|
+
says where the repository is.
|
|
612
|
+
|
|
613
|
+
## Outside the estate: an external with a contract
|
|
614
|
+
|
|
615
|
+
A service calls things nobody here builds - a card network, a tax API, a
|
|
616
|
+
carrier. The first answer the catalog gave was `unknown`: the call was
|
|
617
|
+
recorded and left unresolved, the lane drawn dashed and red, and the Problems
|
|
618
|
+
page listed it beside real defects. True, and unhelpful, once the far end is
|
|
619
|
+
Stripe and Stripe publishes a document.
|
|
620
|
+
|
|
621
|
+
An **external** is what the catalog may claim about such a system, and no
|
|
622
|
+
more: what it answers on, read from the copy of its document vendored beside
|
|
623
|
+
the adapter that calls it, and what the manifest says it is called and is
|
|
624
|
+
for. It sits at the root beside the contexts (`catalog.externals`) with a bare
|
|
625
|
+
id, no context, no aggregates and no repository, and the estate's picture
|
|
626
|
+
draws it outside, muted, exactly where it is.
|
|
627
|
+
|
|
628
|
+
Two steps describe one, and neither knows the other exists:
|
|
629
|
+
|
|
630
|
+
```json
|
|
631
|
+
{ "plugin": "openapi", "in": "examples/payments/ledger",
|
|
632
|
+
"out": "examples/payments/ledger/portolan",
|
|
633
|
+
"options": { "external": "stripe", "externalName": "Stripe",
|
|
634
|
+
"externalUrl": "https://docs.stripe.com/api",
|
|
635
|
+
"spec": "src/main/java/.../infrastructure/stripe/openapi/openapi.yaml",
|
|
636
|
+
"out": "stripe.json" } },
|
|
637
|
+
{ "plugin": "java-domain", "in": "examples/payments/ledger", "...": "...",
|
|
638
|
+
"options": { "externals": { "stripe.v1": "stripe" } } }
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
The first reads the copy and says what `stripe` answers on. The second reads
|
|
642
|
+
the adapter, finds the verb and the route in each call, looks the operation up
|
|
643
|
+
in the same copy, and - told by `externals` that the copy's api id belongs to
|
|
644
|
+
`stripe` - records the call as `stripe.v1/PostPaymentIntents`, declared, on a
|
|
645
|
+
lane of kind `external`. The merge joins the two by the id; a call to an
|
|
646
|
+
operation the copy declares resolves, one it does not is reported by the
|
|
647
|
+
extractor and left out.
|
|
648
|
+
|
|
649
|
+
Neither line is needed when the tree can say it itself. An `openapi` step
|
|
650
|
+
with no `spec` walks the tree for every document and reads what sits beside
|
|
651
|
+
each one: a server generated from it - oapi-codegen's `ServerInterface`,
|
|
652
|
+
swag's `docs` package, a handler or controller written against it - means the
|
|
653
|
+
service implements it, and the document is what the service provides; a
|
|
654
|
+
client generated from it - `ClientInterface`, a `client.gen.go`, an adapter
|
|
655
|
+
named after a client - means the service calls it, and the document names a
|
|
656
|
+
system this tree does not implement. That system is an external, with the
|
|
657
|
+
id, the name and the summary the document gives itself: a copy titled
|
|
658
|
+
"Gordian Flights & Ancillaries API" becomes `gordian-flights-ancillaries`,
|
|
659
|
+
the trailing "API" dropped because the system is the thing and not its
|
|
660
|
+
interface. `peers` on the same step says which called documents are ours -
|
|
661
|
+
`{"auth.v1": "auth.auth"}` - and those are skipped, because the service that
|
|
662
|
+
implements them describes them; `externals` names a system when the title
|
|
663
|
+
would not, `{"stripe.v1": "stripe"}`. A document with neither a server nor a
|
|
664
|
+
client beside it is reported and left alone, since reading it as either would
|
|
665
|
+
be a guess.
|
|
666
|
+
|
|
667
|
+
The domain extractor keeps the same rule from its side: a generated HTTP
|
|
668
|
+
client whose api no `peers` line claims is read as calling the system the
|
|
669
|
+
document beside it is titled after, and the call lands on an `external` lane,
|
|
670
|
+
declared, under the id both sides derive from that title. `externals` on the
|
|
671
|
+
domain step overrides the name; a proto client, whose contract names no
|
|
672
|
+
system, stays unresolved until `peers` says who answers.
|
|
673
|
+
|
|
674
|
+
The copy is **narrow** - the operations the service calls and the schemas they
|
|
675
|
+
answer with, every field verbatim - for the reason org.0001 gives for a proto:
|
|
676
|
+
what is vendored has to be reviewable, and Stripe's whole document is not. It
|
|
677
|
+
carries one line the original does not, `x-portolan-api: stripe.v1` in
|
|
678
|
+
`info`: the copy is already the consumer's translation boundary, so it is the
|
|
679
|
+
one place the estate's name for the document is written, and every reader of
|
|
680
|
+
the copy takes the id from there rather than from Stripe's title and version
|
|
681
|
+
- or from two manifests that would have to agree.
|
|
682
|
+
|
|
683
|
+
## Verifiers: the third phase
|
|
684
|
+
|
|
685
|
+
An extractor reads source and runs before there is a catalog; a generator
|
|
686
|
+
reads the catalog and writes pages. A **verifier** sits between them. It reads
|
|
687
|
+
something observed - traces today, a test's record tomorrow - and answers with
|
|
688
|
+
a fragment like an extractor's, but one that only makes sense against the
|
|
689
|
+
merged catalog: "this hop was seen running" names a hop somebody else declared.
|
|
690
|
+
So a `verify` step is handed both `input` and `catalog`, and the catalog it is
|
|
691
|
+
handed leaves out the step's own last output. Without that, what it wrote last
|
|
692
|
+
time would count as evidence this time, and the fragment could never be checked
|
|
693
|
+
against a clean run.
|
|
694
|
+
|
|
695
|
+
```json
|
|
696
|
+
{
|
|
697
|
+
"plugins": [{ "name": "otel", "process": { "command": "go", "args": ["run", "./plugins/verify-otel"] } }],
|
|
698
|
+
"verify": [
|
|
699
|
+
{
|
|
700
|
+
"plugin": "otel",
|
|
701
|
+
"in": "examples/auth",
|
|
702
|
+
"out": "examples/auth/portolan",
|
|
703
|
+
"options": { "traces": ["telemetry/traces.jsonl"], "out": "observed.json" }
|
|
704
|
+
}
|
|
705
|
+
]
|
|
706
|
+
}
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
What comes back is merged like any other source, under two rules that exist
|
|
710
|
+
for it. A flow declared twice is accepted when the second declaration differs
|
|
711
|
+
only in status: `declared` steps become `verified`, and anything else that
|
|
712
|
+
differs - a lane, a hop, a branch - is the conflict it always was. A consumer
|
|
713
|
+
or a call declared twice keeps the first note and takes `verified` if either
|
|
714
|
+
side has it.
|
|
715
|
+
|
|
716
|
+
### verify-otel
|
|
717
|
+
|
|
718
|
+
Reads OTLP JSON - one batch per file or one per line, as a collector's file
|
|
719
|
+
exporter writes it - and turns each trace into hops between lanes:
|
|
720
|
+
|
|
721
|
+
| span | hop |
|
|
722
|
+
| --- | --- |
|
|
723
|
+
| kind server, `http.route` | `client → service`, an rpc; matched to the operation whose `http` verb and path the OpenAPI extractor recorded, which is what opens an endpoint flow |
|
|
724
|
+
| kind client, `rpc.service` + `rpc.method` | `service → provider`, an rpc; `unknown` lane and `unresolved` when nothing provides it, however often it ran |
|
|
725
|
+
| `db.system.name`, `db.operation.name` | `service → its store`, a call; the statement nested under a query is not a second call |
|
|
726
|
+
| kind producer, `event.name` | `service → bus`, the event whose name that is among the service's own; a producer span under another for the same name is the relay's and the same publish |
|
|
727
|
+
| kind consumer, `event.name` | `bus → service`, and a `verified` consumer on the event |
|
|
728
|
+
|
|
729
|
+
A trace whose root opens a declared flow raises the steps it shows: the call
|
|
730
|
+
in, the events out, the rpcs with a ref. A `call` step is never raised - a
|
|
731
|
+
`SELECT` ran, which is not the same claim as "the repository's `ByEmail` was
|
|
732
|
+
called" - and `unresolved` is never raised, because a trace does not put the
|
|
733
|
+
far end in the catalog. A consumer span inside a trace opens a flow of its own
|
|
734
|
+
and is matched the same way, so one password change verifies both the
|
|
735
|
+
request's flow and the policy's. A root no flow opens is written down as
|
|
736
|
+
`observed-<service>-<route>`, once per shape, with a summary saying how many
|
|
737
|
+
traces showed it.
|
|
738
|
+
|
|
739
|
+
`service.name` is matched to the one service whose slug it is, `event.name` to
|
|
740
|
+
the one event whose `wire.name` it is, or failing that to the one event of the
|
|
741
|
+
publisher's with that last segment; `services` and `events` in the options say
|
|
742
|
+
otherwise where an estate's names differ. A publish span whose
|
|
743
|
+
`messaging.destination.name` is not the event's `wire.channel` is a warning:
|
|
744
|
+
the event went out, but not where the code says it does.
|
|
745
|
+
|
|
746
|
+
### verify-codeowners
|
|
747
|
+
|
|
748
|
+
Reads the `CODEOWNERS` a repository already keeps and says who to ask about
|
|
749
|
+
each service.
|
|
750
|
+
|
|
751
|
+
"Who do I ask about `shop.oms`" was the question the estate answered worst.
|
|
752
|
+
`owner` on a flow, a store or a module means the bounded context that holds it
|
|
753
|
+
- a grouping, not a team - and there was nothing on any page a reader could act
|
|
754
|
+
on. The answer was already written down, in the one file the forge itself
|
|
755
|
+
enforces: a team that owns a directory is a team that gets the pull request.
|
|
756
|
+
|
|
757
|
+
```json
|
|
758
|
+
{
|
|
759
|
+
"plugins": [{ "name": "codeowners", "process": { "command": "go", "args": ["run", "./plugins/verify-codeowners"] } }],
|
|
760
|
+
"verify": [
|
|
761
|
+
{ "plugin": "codeowners", "in": ".github", "out": "data", "options": { "out": "owners.json" } }
|
|
762
|
+
]
|
|
763
|
+
}
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
Point `in` at the directory the file is in, not at the repository root: the
|
|
767
|
+
host dates a fragment from the last commit to touch the step's input, and the
|
|
768
|
+
subject of this one is the `CODEOWNERS` file. Rooted at the repository, it
|
|
769
|
+
would be restamped by every commit ever made. Left with no `file`, the three
|
|
770
|
+
places a forge looks are tried in order - `CODEOWNERS`, `.github/CODEOWNERS`,
|
|
771
|
+
`docs/CODEOWNERS` - and a `file` that names something absent fails the run,
|
|
772
|
+
because answering "nobody owns anything" to a typo is only noticed a month
|
|
773
|
+
later.
|
|
774
|
+
|
|
775
|
+
It is a verifier and not an extractor because a rule is a path and only the
|
|
776
|
+
merged catalog knows where each service is; an extractor would have to be told,
|
|
777
|
+
service by service, in the manifest, what the catalog already says. It earns
|
|
778
|
+
the name twice over. A service no rule matches is reported, and so is a rule
|
|
779
|
+
that matches no service - a team believing it owns something the estate does
|
|
780
|
+
not have, which is the one failure a `CODEOWNERS` file can never report about
|
|
781
|
+
itself. A rule that matches and never wins is reported differently, because the
|
|
782
|
+
fix is different: nothing is wrong with the path, everything it covers is just
|
|
783
|
+
claimed by a rule below it.
|
|
784
|
+
|
|
785
|
+
What lands on the catalog is `owners` on the service: handles exactly as the
|
|
786
|
+
file spells them, `@acme/oms-team`, `@someone`, `dev@acme.io`. Deliberately
|
|
787
|
+
nothing more - resolving a handle to the people currently in it is a call to a
|
|
788
|
+
forge's API, which needs a credential this does not have and answers
|
|
789
|
+
differently tomorrow, and a handle is what a reviewer types anyway. Two sources
|
|
790
|
+
naming owners are unioned, because two rules that both matched are two facts
|
|
791
|
+
and not two answers.
|
|
792
|
+
|
|
793
|
+
The grammar is gitignore's, minus the parts CODEOWNERS does not have. A
|
|
794
|
+
pattern owns a directory when it names the directory or anything above it, and
|
|
795
|
+
does not when it names only something inside it: `services/oms/internal` is a
|
|
796
|
+
rule about part of a service, and reading it as ownership of the whole would
|
|
797
|
+
hand a team a page it never asked for. Later rules win. A pattern with no
|
|
798
|
+
owners after it wins too - taking ownership back is the only reason anybody
|
|
799
|
+
writes one. GitLab's sections change which rule wins and are read the flatter
|
|
800
|
+
way GitHub means, with a warning saying so, because the difference only ever
|
|
801
|
+
shows up as an owner quietly missing from a page.
|
|
802
|
+
|
|
803
|
+
The demo estate's file is `data/codeowners/CODEOWNERS`, which is deliberately
|
|
804
|
+
not one of the three places a forge reads: this repository is both the tool and
|
|
805
|
+
the estate it describes, and a real `CODEOWNERS` here would ask GitHub to
|
|
806
|
+
request reviews from teams that do not exist. A real repository puts it where
|
|
807
|
+
the forge looks.
|
|
808
|
+
|
|
809
|
+
## wasm or process
|
|
810
|
+
|
|
811
|
+
`wasm` is the default and should stay that way. The module gets no filesystem,
|
|
812
|
+
no network and no environment; a plugin from somebody else's repository can be
|
|
813
|
+
run over your source tree without reading it.
|
|
814
|
+
|
|
815
|
+
`process` is the escape hatch for a generator that needs a toolchain — one
|
|
816
|
+
reading Go source has to run `go list`, and no wasm module can spawn anything.
|
|
817
|
+
It gets the same protocol and none of the sandbox, which is the trade being made
|
|
818
|
+
and the reason it is not the default. It declares `command` and an `args` array;
|
|
819
|
+
the host never feeds a command string through a shell.
|
|
820
|
+
|
|
821
|
+
A plugin fetched over `https://` must declare its `sha256`; the host verifies it
|
|
822
|
+
and caches by digest. A `file://` plugin may declare one, but a checksum
|
|
823
|
+
protects a download, not a module built from the source next to it.
|
|
824
|
+
Downloads do not follow redirects and are bounded in time and size. Every run
|
|
825
|
+
has a deadline and bounded stdout/stderr; wasm runs in a worker so even a module
|
|
826
|
+
stuck in a loop can be terminated. Responses reject unknown properties,
|
|
827
|
+
duplicate or unsafe filenames, and non-string contents before anything is
|
|
828
|
+
written.
|
|
829
|
+
|
|
830
|
+
## Services in other repositories: fetch-git
|
|
831
|
+
|
|
832
|
+
`fetch-git` is `fetch-bsr` for a repository rather than a registry, and it
|
|
833
|
+
lives by the same four rules. A pin is a repository, a commit and the paths
|
|
834
|
+
actually read; the step fetches exactly those directories at exactly that
|
|
835
|
+
commit and hands them back as files, so the host writes them into the tree
|
|
836
|
+
beside a `git.lock.json` naming the commit and the digest of every file. The
|
|
837
|
+
paths inside the copy are the repository's own, which is the point: the
|
|
838
|
+
extract step that follows points its `in` at the vendored service and reads
|
|
839
|
+
it exactly as it would read that service's checkout.
|
|
840
|
+
|
|
841
|
+
```json
|
|
842
|
+
{
|
|
843
|
+
"sources": ["data/*.json", "vendor/repos/*/*/git.repo.json"],
|
|
844
|
+
"plugins": [{ "name": "git", "process": { "command": "go", "args": ["run", "./plugins/fetch-git"] } }],
|
|
845
|
+
"extract": [
|
|
846
|
+
{
|
|
847
|
+
"plugin": "git",
|
|
848
|
+
"in": "vendor",
|
|
849
|
+
"out": "vendor/repos",
|
|
850
|
+
"options": {
|
|
851
|
+
"cache": "vendor/repos",
|
|
852
|
+
"repos": [
|
|
853
|
+
{ "repo": "github.com/acme/shop", "commit": "c1d2e3f4…", "paths": ["services/oms", "proto"] }
|
|
854
|
+
]
|
|
855
|
+
}
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
"plugin": "go-domain",
|
|
859
|
+
"in": "vendor/repos/acme/shop/services/oms",
|
|
860
|
+
"out": "data/shop",
|
|
861
|
+
"options": { "context": "shop", "service": "oms", "store": "pg" }
|
|
862
|
+
}
|
|
863
|
+
]
|
|
864
|
+
}
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
It runs the `git` the host already needs for stamps: a fetch of the one
|
|
868
|
+
commit into a directory that exists for one call, and an archive of the paths
|
|
869
|
+
wanted, read straight into memory. Whatever git is configured to do about
|
|
870
|
+
credentials and hosts - a helper, a netrc entry, an ssh agent - it does here
|
|
871
|
+
too, and the plugin reads none of it. `PORTOLAN_OFFLINE` (or any truthy `CI`)
|
|
872
|
+
replays the committed copies against their locks; a commit the manifest does
|
|
873
|
+
not pin is resolved online with a warning and refused offline; a fetch that
|
|
874
|
+
fails falls back to the committed copy when there is one, and is a red build
|
|
875
|
+
when there is not; a vendored file edited by hand is reported by path.
|
|
876
|
+
|
|
877
|
+
### What the copy says about itself
|
|
878
|
+
|
|
879
|
+
Two files land beside every copy, and the difference between them is who reads
|
|
880
|
+
them. `git.lock.json` is for the next run of this step: the commit, and the
|
|
881
|
+
digest of every file, which is what makes replaying the copy equivalent to
|
|
882
|
+
fetching it again. `git.repo.json` is for the estate - a catalog fragment
|
|
883
|
+
holding one line, the repository and the commit it is a copy of - which is why
|
|
884
|
+
it is in `sources` above.
|
|
885
|
+
|
|
886
|
+
Nothing else can say it. A service says which repository it lives in, and an
|
|
887
|
+
extractor reads a directory as a pure function of what is on disk; neither has
|
|
888
|
+
any idea which commit somebody fetched. Without that line, every source path
|
|
889
|
+
of every vendored service is dead text on the page - the file and the line are
|
|
890
|
+
known, and there is nowhere to send a reader - and every fragment read out of
|
|
891
|
+
the copy is stamped with the commit that VENDORED it, so the service looks
|
|
892
|
+
fresh whenever the fetch is re-run and unchanged when its own repository
|
|
893
|
+
moves. With it, `sourceHref` links the line at the commit it was read at and
|
|
894
|
+
`stampFor` dates the fragment from the code rather than the vendoring.
|
|
895
|
+
|
|
896
|
+
One more line is needed for the app itself: `SOURCE_GLOBS` in `src/data.ts`,
|
|
897
|
+
where the same patterns are written out a second time because
|
|
898
|
+
`import.meta.glob` resolves at build time and needs literals.
|
|
899
|
+
|
|
900
|
+
## Schema modules: fetch and parse, kept apart
|
|
901
|
+
|
|
902
|
+
`fetch-bsr` and `extract-proto` are two plugins on purpose, and the split is
|
|
903
|
+
the whole design.
|
|
904
|
+
|
|
905
|
+
| | `fetch-bsr` | `extract-proto` |
|
|
906
|
+
| --- | --- | --- |
|
|
907
|
+
| job | registry wire → `.proto` bytes | `.proto` bytes → catalog fragment |
|
|
908
|
+
| network | yes | never |
|
|
909
|
+
| environment | reads `BUF_TOKEN` | never |
|
|
910
|
+
| output | `.proto` files and a `bsr.lock.json` per module | one catalog fragment |
|
|
911
|
+
| deterministic | only because it is pinned and cached | absolutely |
|
|
912
|
+
|
|
913
|
+
Extraction stays a pure function of the tree. Fetching is the step that can
|
|
914
|
+
fail, need a credential, or come back with something different than it did
|
|
915
|
+
yesterday, and confining that to its own step is what lets everything after it
|
|
916
|
+
be replayed byte-for-byte from a checkout.
|
|
917
|
+
|
|
918
|
+
**The fetched protos are the plugin's `Response.Files`, not a side effect.** The
|
|
919
|
+
host writes them like any other generated file, so they get a manifest entry,
|
|
920
|
+
are compared by `gen:check`, and are removed when the step stops naming them.
|
|
921
|
+
The cache is not a second copy of anything — it *is* the tree. Refreshing a pin
|
|
922
|
+
produces one pull request holding the pin bump, the proto diff, the lock diff
|
|
923
|
+
and the fragment diff, which is the review worth having.
|
|
924
|
+
|
|
925
|
+
Declare the fetch step **before** the extract step: steps run in list order, so
|
|
926
|
+
its protos and locks are on disk by the time the parser reads them.
|
|
927
|
+
|
|
928
|
+
```json
|
|
929
|
+
{
|
|
930
|
+
"plugins": [
|
|
931
|
+
{ "name": "bsr", "process": { "command": "go", "args": ["run", "./plugins/fetch-bsr"] } },
|
|
932
|
+
{ "name": "proto", "process": { "command": "go", "args": ["run", "./plugins/extract-proto"] } }
|
|
933
|
+
],
|
|
934
|
+
"extract": [
|
|
935
|
+
{
|
|
936
|
+
"plugin": "bsr",
|
|
937
|
+
"in": "examples/shop",
|
|
938
|
+
"out": "examples/shop/vendor/proto",
|
|
939
|
+
"options": {
|
|
940
|
+
"cache": "examples/shop/vendor/proto",
|
|
941
|
+
"modules": [
|
|
942
|
+
{ "module": "buf.build/acme/shop", "commit": "c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6" }
|
|
943
|
+
]
|
|
944
|
+
}
|
|
945
|
+
},
|
|
946
|
+
{
|
|
947
|
+
"plugin": "proto",
|
|
948
|
+
"in": "examples/shop",
|
|
949
|
+
"out": "examples/shop/portolan",
|
|
950
|
+
"options": {
|
|
951
|
+
"context": "shop",
|
|
952
|
+
"service": "oms",
|
|
953
|
+
"paths": ["vendor/proto/acme/shop"],
|
|
954
|
+
"vendored": ["internal/infrastructure/pricing"],
|
|
955
|
+
"peers": { "pricing.v1": "shop.pricing" },
|
|
956
|
+
"out": "proto.json"
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
]
|
|
960
|
+
}
|
|
961
|
+
```
|
|
962
|
+
|
|
963
|
+
`cache` repeats the step's `out` because a plugin is never told where its output
|
|
964
|
+
goes. That is the shape of the protocol — a plugin returns files and the host
|
|
965
|
+
decides what to do with them — and not an oversight to work around.
|
|
966
|
+
|
|
967
|
+
### Why fetch-bsr can never be wasm
|
|
968
|
+
|
|
969
|
+
It needs a socket and a credential. `process` exists for exactly that trade, and
|
|
970
|
+
`auth.go` is the only file in either plugin that reads the environment. The
|
|
971
|
+
protocol's "no ambient state" rule is about *facts*: nothing about the estate may
|
|
972
|
+
come from anywhere but the request. A credential is not a fact about the estate —
|
|
973
|
+
it decides whether the fetch succeeds, never what the fetch says — and a test
|
|
974
|
+
asserts the output is byte-identical with and without a token.
|
|
975
|
+
|
|
976
|
+
### Pinning, and the offline rule
|
|
977
|
+
|
|
978
|
+
Pin every module to a commit. A BSR commit is immutable, so a pinned download is
|
|
979
|
+
byte-reproducible, which is the only reason replaying from disk is equivalent to
|
|
980
|
+
fetching again. An unpinned module is resolved and warned about when online, and
|
|
981
|
+
refused when offline — there is nothing to replay against.
|
|
982
|
+
|
|
983
|
+
`PORTOLAN_OFFLINE=1` (or any truthy `CI`) turns the fetch off. The step then
|
|
984
|
+
re-emits the committed copies, checked against their locks. Set it in CI; the
|
|
985
|
+
workflow already does.
|
|
986
|
+
|
|
987
|
+
Four rules govern what happens when a fetch does not:
|
|
988
|
+
|
|
989
|
+
1. Fetch succeeded → the fetched files and a regenerated lock.
|
|
990
|
+
2. Skipped or failed, cache complete and matching its digests → the cached files
|
|
991
|
+
byte-identically, plus a warning. Output unchanged, so `--check` stays clean.
|
|
992
|
+
3. Failed **and** no usable cache → a non-zero exit, never a short file list. The
|
|
993
|
+
host deletes files a step stops naming, and dropping a repository's vendored
|
|
994
|
+
protos because a laptop went offline is worse than a red build.
|
|
995
|
+
4. A cached file whose digest no longer matches is reported by path — someone
|
|
996
|
+
edited a vendored copy, which is the drift `docs/adr/org.0001.md` wants seen.
|
|
997
|
+
|
|
998
|
+
### Why the proto parser is hand-written
|
|
999
|
+
|
|
1000
|
+
`docs/adr/org.0001.md` has consumers keeping *narrowed* vendored copies, and a
|
|
1001
|
+
narrowed copy routinely imports a file nobody vendored beside it. A compiler —
|
|
1002
|
+
`protocompile`, `protoc` — refuses to produce anything for that input. The whole
|
|
1003
|
+
point of reading vendored copies is to describe files that do not build
|
|
1004
|
+
standalone, so the parser is tolerant: every construct it declines to model
|
|
1005
|
+
(`extend`, a proto2 `group`, an aggregate option body) is named in a diagnostic
|
|
1006
|
+
rather than dropped, and only a file that cannot be tokenised is fatal.
|
|
1007
|
+
|
|
1008
|
+
It also keeps types **as written** — `[]LineItem`, `map[string]Money`, the
|
|
1009
|
+
`optional` keyword, the author's declaration order — the same stance the catalog
|
|
1010
|
+
already takes for schemas, and one a descriptor has already thrown away.
|
|
1011
|
+
|
|
1012
|
+
### What extract-proto will not claim
|
|
1013
|
+
|
|
1014
|
+
`status` is only ever `declared` or `unresolved`, **never `verified`**. Reading a
|
|
1015
|
+
`.proto` proves a call was written down. `verified` in the shipped catalog means
|
|
1016
|
+
a test exercises it end to end, which is a property of the merged catalog — and
|
|
1017
|
+
every extractor runs before one exists.
|
|
1018
|
+
|
|
1019
|
+
A message named `OrderPlaced` in an `events.proto` stays an `RpcMessage` and does
|
|
1020
|
+
not become a catalog `Event`. `Event.id` is `<service>.<aggregate>.<Name>` and
|
|
1021
|
+
this extractor knows the package, not the aggregate; a guess would collide with
|
|
1022
|
+
the event `extract-go` already emits, or invent a ghost aggregate that would sit
|
|
1023
|
+
beside the real one forever.
|
|
1024
|
+
|
|
1025
|
+
## Schema registry subjects: the same split, one topic further
|
|
1026
|
+
|
|
1027
|
+
`fetch-csr` and `extract-csr` are the Confluent Schema Registry half of the same
|
|
1028
|
+
argument, and they are shaped like `fetch-bsr` and `extract-proto` on purpose:
|
|
1029
|
+
one owns the socket and the credential, the other owns the reading, and CI runs
|
|
1030
|
+
the reading over a tree it can verify without a registry existing at all.
|
|
1031
|
+
|
|
1032
|
+
| | `fetch-csr` | `extract-csr` |
|
|
1033
|
+
| --- | --- | --- |
|
|
1034
|
+
| job | registry wire → schema files | schema files → catalog fragment |
|
|
1035
|
+
| network | yes | never |
|
|
1036
|
+
| environment | reads `CSR_API_KEY`/`CSR_API_SECRET`, or `CSR_TOKEN` | never |
|
|
1037
|
+
| output | one schema and a `csr.lock.json` per subject | one catalog fragment |
|
|
1038
|
+
| deterministic | only because it is pinned and cached | absolutely |
|
|
1039
|
+
|
|
1040
|
+
A registered version is immutable: subject `orders-value` at version 3 is the
|
|
1041
|
+
same bytes today and next year, and re-registering a changed schema makes
|
|
1042
|
+
version 4. That is the promise a BSR commit makes, so the same four rules govern
|
|
1043
|
+
a fetch that does not happen, and `PORTOLAN_OFFLINE=1` replays the committed
|
|
1044
|
+
copies against their locks exactly as it does there.
|
|
1045
|
+
|
|
1046
|
+
A subject a fetched schema **references** is fetched too, pinned by the
|
|
1047
|
+
reference rather than by the manifest, and needs no entry of its own — the
|
|
1048
|
+
version is part of the bytes we already have, so following one adds no lottery.
|
|
1049
|
+
Each lands in its own directory, and the referring subject's lock is what an
|
|
1050
|
+
offline run follows to find them.
|
|
1051
|
+
|
|
1052
|
+
Avro and JSON schemas arrive minified onto one line. They are written out
|
|
1053
|
+
indented — with `json.Indent`, which reformats without reordering, so the file
|
|
1054
|
+
still says what the registry said in the order it said it — because a version
|
|
1055
|
+
bump that is one unreadable line is a review nobody can do. The digest is over
|
|
1056
|
+
the bytes as written, so verifying needs no reformatting of anything.
|
|
1057
|
+
|
|
1058
|
+
### The strategy the registry does not record
|
|
1059
|
+
|
|
1060
|
+
A subject name is whatever the producer's serializer decided to call the
|
|
1061
|
+
registration, and the rule it used — the `SubjectNameStrategy` — is nowhere in
|
|
1062
|
+
the registry's answer. `shop.oms.order-value` is a topic plus a suffix under
|
|
1063
|
+
`TopicNameStrategy` and a record's full name under `RecordNameStrategy`, and
|
|
1064
|
+
nothing but the manifest can say which. So `strategy` is an option, and
|
|
1065
|
+
everything `extract-csr` does with a name follows from being told it.
|
|
1066
|
+
|
|
1067
|
+
Under `topic-record` the separator is a hyphen and both halves may contain one,
|
|
1068
|
+
so the split is made by matching the **schema's own full name** as the suffix
|
|
1069
|
+
rather than by searching for a delimiter.
|
|
1070
|
+
|
|
1071
|
+
A `-key` subject is kept as a shape and put on no channel: a key is part of
|
|
1072
|
+
every message on the topic, not a message on it. Under `record` there is no
|
|
1073
|
+
topic at all, and a fragment with no channels is the right answer rather than a
|
|
1074
|
+
gap — that strategy exists so a record can be reused across many.
|
|
1075
|
+
|
|
1076
|
+
### What extract-csr will not claim
|
|
1077
|
+
|
|
1078
|
+
**It emits no events.** An `Event` in the catalog belongs to an aggregate, and a
|
|
1079
|
+
registry holds schemas, not domains — it has no idea which aggregate raises
|
|
1080
|
+
what. So the shapes land in `defs`, where a shared shape belongs, and the topics
|
|
1081
|
+
land in the service's channels beside the ones an AsyncAPI document declares.
|
|
1082
|
+
The domain extractor says an aggregate raises `OrderPlaced` and calls it
|
|
1083
|
+
`shop.oms.OrderPlaced` on the wire; this says a schema by that name is
|
|
1084
|
+
registered against topic `shop.oms.order` and has these fields. Neither knows
|
|
1085
|
+
the other exists, and the pages hold the two against each other.
|
|
1086
|
+
|
|
1087
|
+
**It does not say who produces.** A registry records no producer and no
|
|
1088
|
+
consumer. `direction` is told, per step and per subject, or it would be invented.
|
|
1089
|
+
|
|
1090
|
+
**It does not parse protobuf.** That is `extract-proto`'s whole job, and a
|
|
1091
|
+
second, worse parser here would be a second answer to one question. A `PROTOBUF`
|
|
1092
|
+
subject still names its topic — which is the one thing a `.proto` file cannot
|
|
1093
|
+
say — and a diagnostic points at `extract-proto` for the fields.
|
|
1094
|
+
|
|
1095
|
+
A field referencing a shape nothing in the estate vendored keeps its **name**
|
|
1096
|
+
and loses its **ref**. The catalog validates that every ref resolves, and
|
|
1097
|
+
failing a run over a reference that is genuinely true — the shape really does
|
|
1098
|
+
live in another estate — would be the wrong end of the trade.
|
|
1099
|
+
|
|
1100
|
+
## The bus: a channel is a claim, not an event
|
|
1101
|
+
|
|
1102
|
+
`extract-asyncapi` reads an AsyncAPI document and answers with the channels a
|
|
1103
|
+
service declares — the address the broker knows, and each message on it with the
|
|
1104
|
+
direction it travels. What it does **not** answer with is events.
|
|
1105
|
+
|
|
1106
|
+
That looks like a gap and is a boundary. `Event.id` is
|
|
1107
|
+
`<service>.<aggregate>.<Name>`, and an AsyncAPI document knows the message on the
|
|
1108
|
+
wire, not the aggregate that raised it. An extractor that guessed would either
|
|
1109
|
+
collide with the event `extract-go` and `extract-ts` already emit or invent a
|
|
1110
|
+
ghost aggregate that would sit beside the real one forever — the same rule
|
|
1111
|
+
`extract-proto` keeps about a message called `OrderPlaced`.
|
|
1112
|
+
|
|
1113
|
+
So the two sources meet in the merge instead, and the pages hold them against
|
|
1114
|
+
each other. The domain says an aggregate raises `BasketCreated` and how it leaves,
|
|
1115
|
+
in `wire`; the document says the service sends `cart.BasketCreated` on
|
|
1116
|
+
`shop.cart.basket`. Where they agree the catalog says the same thing twice, which
|
|
1117
|
+
is worth nothing. Where they disagree it is worth a row on the Problems page,
|
|
1118
|
+
because one of the two is stale:
|
|
1119
|
+
|
|
1120
|
+
- an event whose channel the document does not declare — a subscriber reading
|
|
1121
|
+
the document does not know the message exists;
|
|
1122
|
+
- a channel the document declares and no event names — a promise nothing keeps;
|
|
1123
|
+
- a message the document listens for that nothing in the estate publishes.
|
|
1124
|
+
|
|
1125
|
+
That last one is the only edge in the catalog that runs from the subscriber
|
|
1126
|
+
outwards. Everywhere else a publisher names its consumers; here the subscriber
|
|
1127
|
+
names a message and the estate is searched for whoever puts it on the wire. A
|
|
1128
|
+
subscription that resolves is how two repositories that never mention each other
|
|
1129
|
+
are found to be joined — and a channel that two services both declare a send on
|
|
1130
|
+
is a second publisher, which is an error for the reason a second writer in a
|
|
1131
|
+
database is.
|
|
1132
|
+
|
|
1133
|
+
### 2.x says publish and subscribe backwards
|
|
1134
|
+
|
|
1135
|
+
In AsyncAPI 3.x an operation carries `action: send` or `action: receive`, from
|
|
1136
|
+
the application's side, and there is nothing to get wrong. In 2.x a channel has
|
|
1137
|
+
`publish` and `subscribe`, and both are written from the **client's** side:
|
|
1138
|
+
`publish` is what somebody else publishes *to* the application, so the
|
|
1139
|
+
application receives it, and `subscribe` is what the application produces for
|
|
1140
|
+
somebody else to subscribe to.
|
|
1141
|
+
|
|
1142
|
+
Reading 2.x the obvious way puts every arrow in the estate the wrong way round.
|
|
1143
|
+
The extractor reads both versions and answers in 3.x's vocabulary, which is the
|
|
1144
|
+
one the catalog keeps.
|
|
1145
|
+
|
|
1146
|
+
### A work queue is a channel of kind `job`
|
|
1147
|
+
|
|
1148
|
+
A task queue is a channel too - an address the broker knows, messages that
|
|
1149
|
+
travel on it - with one difference the catalog has to be told: many callers
|
|
1150
|
+
put the same job on it by design, and no domain event stands behind a job.
|
|
1151
|
+
`kind: "job"` on the channel says so. The merge does not call two senders on a
|
|
1152
|
+
job queue rival publishers, and the Problems page does not look for an event
|
|
1153
|
+
with the job's wire name. `extract-celery` answers with these for a Python tree:
|
|
1154
|
+
one channel per queue, a `send` per task the tree enqueues and a `receive` per
|
|
1155
|
+
task it declares, and one flow per task that is both - the call that enqueues
|
|
1156
|
+
it, then the worker that runs it. It reads the queue the way Celery decides
|
|
1157
|
+
it, the call before the decorator before `task_routes` before the default,
|
|
1158
|
+
and `transaction.on_commit(...)` around an enqueue is a note on the step,
|
|
1159
|
+
which is the one fact about *when* a message leaves that the code states
|
|
1160
|
+
plainly.
|