@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,178 @@
|
|
|
1
|
+
// The four persistence problems, each proved to fire and — more importantly —
|
|
2
|
+
// each proved not to fire on the cases that look like it.
|
|
3
|
+
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { rawCatalog } from "../test-catalog";
|
|
6
|
+
import { buildIndex, validateCatalog } from "../catalog";
|
|
7
|
+
import type { Catalog } from "../catalog";
|
|
8
|
+
import type { Problem } from "./derive";
|
|
9
|
+
import { dataProblems } from "./data-problems";
|
|
10
|
+
|
|
11
|
+
const clone = (): Catalog =>
|
|
12
|
+
JSON.parse(JSON.stringify(rawCatalog)) as unknown as Catalog;
|
|
13
|
+
|
|
14
|
+
function found(catalog: Catalog) {
|
|
15
|
+
return dataProblems(catalog, buildIndex(validateCatalog(catalog)));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const kinds = (list: { kind: string }[]) => list.map((p) => p.kind);
|
|
19
|
+
|
|
20
|
+
describe("the sample estate", () => {
|
|
21
|
+
const problems = found(clone());
|
|
22
|
+
|
|
23
|
+
it("reports the intentional cross-service foreign key", () => {
|
|
24
|
+
const fk = problems.find((p) => p.kind === "cross-service-fk");
|
|
25
|
+
expect(fk?.id).toBe("delivery.core.pg.packages.order_id");
|
|
26
|
+
expect(fk?.peer).toBe("shop.oms.pg.orders");
|
|
27
|
+
expect(fk?.severity).toBe("error");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("reports the table one service writes in another's database", () => {
|
|
31
|
+
const shared = problems.find((p) => p.kind === "shared-store");
|
|
32
|
+
expect(shared?.id).toBe("shop.oms.pg.price_snapshots");
|
|
33
|
+
expect(shared?.peer).toBe("shop.pricing");
|
|
34
|
+
expect(shared?.severity).toBe("error");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("reports no persistence drift: every table's columns follow its aggregate", () => {
|
|
38
|
+
expect(problems.filter((p) => p.kind === "persistence-drift")).toEqual([]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("reports a renamed table whose columns did not follow", () => {
|
|
42
|
+
const drifted = clone();
|
|
43
|
+
const orders = (drifted.stores ?? [])
|
|
44
|
+
.find((s) => s.id === "shop.oms.pg")
|
|
45
|
+
?.tables.find((t) => t.name === "orders");
|
|
46
|
+
if (!orders) throw new Error("fixture has no orders table");
|
|
47
|
+
for (const column of orders.columns) if (column.maps) column.maps = `Order.${column.name}Gone`;
|
|
48
|
+
const drift = found(drifted).find((p) => p.kind === "persistence-drift");
|
|
49
|
+
expect(drift?.id).toBe("shop.oms.pg.orders");
|
|
50
|
+
expect(drift?.severity).toBe("warning");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("reports every type disagreement and nothing else", () => {
|
|
54
|
+
const types = problems.filter((p) => p.kind === "column-type");
|
|
55
|
+
expect(types.map((p) => p.id)).toEqual([
|
|
56
|
+
"shop.oms.pg.orders.id",
|
|
57
|
+
"shop.oms.pg.order_items.quantity",
|
|
58
|
+
"delivery.core.pg.packages.order_id",
|
|
59
|
+
// Not lockouts.failures: `integer` under a Go `int` is a width the
|
|
60
|
+
// model never named, not a disagreement.
|
|
61
|
+
"shop.cart.pg.baskets.id",
|
|
62
|
+
"shop.cart.pg.basket_items.basket_id",
|
|
63
|
+
]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("reports the value delivery copies out of the OMS schema", () => {
|
|
67
|
+
const copied = problems.filter((p) => p.kind === "cross-service-lineage");
|
|
68
|
+
expect(copied.map((p) => p.id)).toEqual([
|
|
69
|
+
"delivery.core.pg.packages.ship_to",
|
|
70
|
+
]);
|
|
71
|
+
expect(copied[0]?.peer).toBe("shop.oms.pg.orders.ship_to");
|
|
72
|
+
// A warning, not an error: copying is how a service stays out of someone
|
|
73
|
+
// else's database, and the alternative — a foreign key — is the error
|
|
74
|
+
// right above it.
|
|
75
|
+
expect(copied[0]?.severity).toBe("warning");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("says nothing about lineage that stays inside one service", () => {
|
|
79
|
+
const inside = problems.filter(
|
|
80
|
+
(p) =>
|
|
81
|
+
p.kind === "cross-service-lineage" &&
|
|
82
|
+
p.id.startsWith("shop.oms.pg.outbox"),
|
|
83
|
+
);
|
|
84
|
+
expect(inside).toEqual([]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("says nothing about the outbox, which has its payload", () => {
|
|
88
|
+
expect(kinds(problems)).not.toContain("outbox-payload");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("puts every error before every warning", () => {
|
|
92
|
+
const at = problems.findIndex((p) => p.severity === "warning");
|
|
93
|
+
expect(problems.slice(0, at).every((p) => p.severity === "error")).toBe(
|
|
94
|
+
true,
|
|
95
|
+
);
|
|
96
|
+
expect(problems.slice(at).every((p) => p.severity === "warning")).toBe(
|
|
97
|
+
true,
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("cases that only look like problems", () => {
|
|
103
|
+
it("does not call a foreign key inside one store a boundary leak", () => {
|
|
104
|
+
const problems = found(clone());
|
|
105
|
+
const inside = problems.filter(
|
|
106
|
+
(p) =>
|
|
107
|
+
p.kind === "cross-service-fk" &&
|
|
108
|
+
p.id.startsWith("shop.oms.pg.order_items"),
|
|
109
|
+
);
|
|
110
|
+
expect(inside).toEqual([]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("does not call a projection of someone else's aggregate a shared database", () => {
|
|
114
|
+
// A local read model built from another service's events is the pattern
|
|
115
|
+
// this rule has to leave alone.
|
|
116
|
+
const catalog = clone();
|
|
117
|
+
const store = (catalog.stores ?? []).find((s) => s.id === "delivery.core.pg");
|
|
118
|
+
const stops = store?.tables.find((t) => t.name === "route_stops");
|
|
119
|
+
if (!stops) throw new Error("fixture has no projection");
|
|
120
|
+
stops.persists = { aggregate: "shop.oms.order" };
|
|
121
|
+
|
|
122
|
+
const shared = found(catalog).filter(
|
|
123
|
+
(p) => p.kind === "shared-store" && p.id === stops.id,
|
|
124
|
+
);
|
|
125
|
+
expect(shared).toEqual([]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("does not accuse a derived table of drifting from an aggregate", () => {
|
|
129
|
+
const problems = found(clone());
|
|
130
|
+
const derived = problems.filter(
|
|
131
|
+
(p) =>
|
|
132
|
+
p.kind === "persistence-drift" &&
|
|
133
|
+
p.id === "delivery.core.pg.route_stops",
|
|
134
|
+
);
|
|
135
|
+
expect(derived).toEqual([]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("does not report drift for a table whose columns still map", () => {
|
|
139
|
+
const problems = found(clone());
|
|
140
|
+
expect(
|
|
141
|
+
problems.filter(
|
|
142
|
+
(p) => p.kind === "persistence-drift" && p.id === "shop.oms.pg.orders",
|
|
143
|
+
),
|
|
144
|
+
).toEqual([]);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("an outbox with nothing in it", () => {
|
|
149
|
+
it("is a warning, and names the store", () => {
|
|
150
|
+
const catalog = clone();
|
|
151
|
+
const outbox = (catalog.stores ?? [])
|
|
152
|
+
.find((s) => s.id === "shop.oms.pg")
|
|
153
|
+
?.tables.find((t) => t.role === "outbox");
|
|
154
|
+
if (!outbox) throw new Error("fixture has no outbox");
|
|
155
|
+
outbox.columns = outbox.columns.filter((c) => c.type !== "jsonb");
|
|
156
|
+
|
|
157
|
+
const problem = found(catalog).find((p) => p.kind === "outbox-payload");
|
|
158
|
+
expect(problem?.severity).toBe("warning");
|
|
159
|
+
expect(problem?.id).toBe("shop.oms.pg.outbox");
|
|
160
|
+
expect(problem?.peer).toBe("shop.oms.pg");
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe("a catalog with no stores", () => {
|
|
165
|
+
it("has no persistence problems at all", () => {
|
|
166
|
+
const bare = clone();
|
|
167
|
+
delete bare.stores;
|
|
168
|
+
for (const context of bare.contexts) {
|
|
169
|
+
for (const service of context.services) delete service.stores;
|
|
170
|
+
}
|
|
171
|
+
expect(found(bare)).toEqual([]);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// A compile-time check that the shared shape really is shared: the page renders
|
|
176
|
+
// one row component over both lists, so the two must stay one type.
|
|
177
|
+
const _shape: (p: Problem) => string = (p) => `${p.severity}:${p.kind}`;
|
|
178
|
+
void _shape;
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
// Where the schema and the model disagree.
|
|
2
|
+
//
|
|
3
|
+
// The unresolved edges in derive.ts are about arrows that land nowhere. These
|
|
4
|
+
// are about arrows that land somewhere they should not, and about two claims
|
|
5
|
+
// in the catalog that cannot both be current. They share one shape and one
|
|
6
|
+
// page, because a reader scanning Problems asks the same question of all of
|
|
7
|
+
// them: what do I have to go and look at?
|
|
8
|
+
//
|
|
9
|
+
// Errors are boundary leaks — one service reaching into another's schema. They
|
|
10
|
+
// are errors because no amount of context makes them fine: a foreign key across
|
|
11
|
+
// a service boundary is a deployment order nobody wrote down, and a second
|
|
12
|
+
// writer in someone else's database is a bounded context with two owners.
|
|
13
|
+
//
|
|
14
|
+
// Warnings are staleness. A table that no longer carries the fields it claims,
|
|
15
|
+
// a column whose type has drifted from its field's — both are ordinary during a
|
|
16
|
+
// migration, so they are reported and not enforced.
|
|
17
|
+
|
|
18
|
+
import type {
|
|
19
|
+
Catalog,
|
|
20
|
+
CatalogIndex,
|
|
21
|
+
Column,
|
|
22
|
+
Store,
|
|
23
|
+
Table,
|
|
24
|
+
} from "../catalog";
|
|
25
|
+
import {
|
|
26
|
+
aggregateBlocks,
|
|
27
|
+
blockFields,
|
|
28
|
+
mapsFieldPath,
|
|
29
|
+
relationOfColumnId,
|
|
30
|
+
storeViews,
|
|
31
|
+
viewReads,
|
|
32
|
+
} from "../catalog";
|
|
33
|
+
import type { Problem } from "./derive";
|
|
34
|
+
import { payloadColumn, typesDisagree } from "./data-model";
|
|
35
|
+
|
|
36
|
+
/** The context a store sits in, via the service that owns it. */
|
|
37
|
+
function contextOf(index: CatalogIndex, serviceId: string): string {
|
|
38
|
+
return index.serviceContext.get(serviceId)?.id ?? "";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Every persistence problem, stores in catalog order and tables in theirs.
|
|
43
|
+
* Errors first within each store, so the boundary leaks are not buried under
|
|
44
|
+
* a column-by-column type audit.
|
|
45
|
+
*/
|
|
46
|
+
export function dataProblems(
|
|
47
|
+
catalog: Catalog,
|
|
48
|
+
index: CatalogIndex,
|
|
49
|
+
): Problem[] {
|
|
50
|
+
const errors: Problem[] = [];
|
|
51
|
+
const warnings: Problem[] = [];
|
|
52
|
+
|
|
53
|
+
for (const store of catalog.stores ?? []) {
|
|
54
|
+
for (const table of store.tables) {
|
|
55
|
+
errors.push(...crossServiceKeys(index, store, table));
|
|
56
|
+
errors.push(...sharedStore(index, store, table));
|
|
57
|
+
warnings.push(...outboxWithoutPayload(index, store, table));
|
|
58
|
+
warnings.push(...drift(catalog, index, store, table));
|
|
59
|
+
warnings.push(...typeDrift(catalog, index, store, table));
|
|
60
|
+
warnings.push(
|
|
61
|
+
...crossServiceLineage(index, store, table.id, table.columns),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
for (const view of storeViews(store)) {
|
|
65
|
+
warnings.push(...crossServiceLineage(index, store, view.id, view.columns));
|
|
66
|
+
// A view is defined over what it reads, so a `reads` entry pointing into
|
|
67
|
+
// someone else's store is the same coupling stated one level up.
|
|
68
|
+
for (const readId of viewReads(view)) {
|
|
69
|
+
const target =
|
|
70
|
+
index.tableById.get(readId) ?? index.viewById.get(readId);
|
|
71
|
+
if (!target || target.store.owner === store.owner) continue;
|
|
72
|
+
warnings.push({
|
|
73
|
+
kind: "cross-service-lineage",
|
|
74
|
+
severity: "warning",
|
|
75
|
+
context: contextOf(index, store.owner),
|
|
76
|
+
service: store.owner,
|
|
77
|
+
id: view.id,
|
|
78
|
+
peer: readId,
|
|
79
|
+
note: `${view.id} is defined over ${readId}, which ${target.store.owner} owns; a rename over there breaks this view with no error until it is read.`,
|
|
80
|
+
source: view.source ?? store.source,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return [...errors, ...warnings];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* A foreign key into a table another service owns. The database will enforce
|
|
91
|
+
* it, which is the problem: two services are now one schema, and neither can
|
|
92
|
+
* migrate its own table without the other's release.
|
|
93
|
+
*/
|
|
94
|
+
function crossServiceKeys(
|
|
95
|
+
index: CatalogIndex,
|
|
96
|
+
store: Store,
|
|
97
|
+
table: Table,
|
|
98
|
+
): Problem[] {
|
|
99
|
+
const out: Problem[] = [];
|
|
100
|
+
for (const column of table.columns) {
|
|
101
|
+
if (!column.fk) continue;
|
|
102
|
+
const target = index.tableById.get(column.fk.table);
|
|
103
|
+
if (!target || target.store.owner === store.owner) continue;
|
|
104
|
+
out.push({
|
|
105
|
+
kind: "cross-service-fk",
|
|
106
|
+
severity: "error",
|
|
107
|
+
context: contextOf(index, store.owner),
|
|
108
|
+
service: store.owner,
|
|
109
|
+
id: `${table.id}.${column.name}`,
|
|
110
|
+
peer: column.fk.table,
|
|
111
|
+
note: `${store.owner} holds a foreign key into a table owned by ${target.store.owner}; neither service can migrate that table alone.`,
|
|
112
|
+
source: store.source,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* A column copied from another service's schema.
|
|
120
|
+
*
|
|
121
|
+
* Not an error: copying is how a projection is built, and the alternative — a
|
|
122
|
+
* foreign key — is worse. It is a warning because the coupling is invisible
|
|
123
|
+
* from the other side. Nothing in the source database records that someone
|
|
124
|
+
* else's column is a copy of it, so the rename that breaks this is a rename
|
|
125
|
+
* that looked safe.
|
|
126
|
+
*/
|
|
127
|
+
function crossServiceLineage(
|
|
128
|
+
index: CatalogIndex,
|
|
129
|
+
store: Store,
|
|
130
|
+
relationId: string,
|
|
131
|
+
columns: Column[],
|
|
132
|
+
): Problem[] {
|
|
133
|
+
const out: Problem[] = [];
|
|
134
|
+
const seen = new Set<string>();
|
|
135
|
+
for (const column of columns) {
|
|
136
|
+
for (const ref of column.from ?? []) {
|
|
137
|
+
const source = relationOfColumnId(ref);
|
|
138
|
+
const target =
|
|
139
|
+
index.tableById.get(source) ?? index.viewById.get(source);
|
|
140
|
+
if (!target || target.store.owner === store.owner) continue;
|
|
141
|
+
if (seen.has(ref)) continue;
|
|
142
|
+
seen.add(ref);
|
|
143
|
+
out.push({
|
|
144
|
+
kind: "cross-service-lineage",
|
|
145
|
+
severity: "warning",
|
|
146
|
+
context: contextOf(index, store.owner),
|
|
147
|
+
service: store.owner,
|
|
148
|
+
id: `${relationId}.${column.name}`,
|
|
149
|
+
peer: ref,
|
|
150
|
+
note: `${store.owner} copies this value from ${ref}, which ${target.store.owner} owns; nothing on that side records that the copy exists.`,
|
|
151
|
+
source: store.source,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* A table in one service's store that holds another service's aggregate. An
|
|
160
|
+
* aggregate is written by the service that owns it, so this is a second writer
|
|
161
|
+
* in someone else's database — reading is allowed and is drawn ghosted, but
|
|
162
|
+
* this is not reading.
|
|
163
|
+
*/
|
|
164
|
+
function sharedStore(
|
|
165
|
+
index: CatalogIndex,
|
|
166
|
+
store: Store,
|
|
167
|
+
table: Table,
|
|
168
|
+
): Problem[] {
|
|
169
|
+
const aggregateId = table.persists?.aggregate;
|
|
170
|
+
if (!aggregateId) return [];
|
|
171
|
+
// A projection is a COPY of someone else's aggregate, maintained locally
|
|
172
|
+
// from their events. That is the pattern this rule exists to make room for,
|
|
173
|
+
// not the one it is looking for.
|
|
174
|
+
if (table.role === "projection") return [];
|
|
175
|
+
const writer = index.aggregateOwner.get(aggregateId);
|
|
176
|
+
if (!writer || writer.id === store.owner) return [];
|
|
177
|
+
return [
|
|
178
|
+
{
|
|
179
|
+
kind: "shared-store",
|
|
180
|
+
severity: "error",
|
|
181
|
+
context: contextOf(index, store.owner),
|
|
182
|
+
service: store.owner,
|
|
183
|
+
id: table.id,
|
|
184
|
+
peer: writer.id,
|
|
185
|
+
note: `${store.id} is owned by ${store.owner}, but this table holds ${aggregateId}, which ${writer.id} writes.`,
|
|
186
|
+
source: store.source,
|
|
187
|
+
},
|
|
188
|
+
];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* An outbox with nothing to deliver. The whole point of the pattern is that the
|
|
193
|
+
* event body is committed in the same transaction as the state change, so an
|
|
194
|
+
* outbox with no payload column is either not an outbox or not finished.
|
|
195
|
+
*/
|
|
196
|
+
function outboxWithoutPayload(
|
|
197
|
+
index: CatalogIndex,
|
|
198
|
+
store: Store,
|
|
199
|
+
table: Table,
|
|
200
|
+
): Problem[] {
|
|
201
|
+
if (table.role !== "outbox" || payloadColumn(table)) return [];
|
|
202
|
+
return [
|
|
203
|
+
{
|
|
204
|
+
kind: "outbox-payload",
|
|
205
|
+
severity: "warning",
|
|
206
|
+
context: contextOf(index, store.owner),
|
|
207
|
+
service: store.owner,
|
|
208
|
+
id: table.id,
|
|
209
|
+
peer: store.id,
|
|
210
|
+
note: "an outbox carries the event body in the same transaction as the state change; this table declares no json column to hold one",
|
|
211
|
+
source: store.source,
|
|
212
|
+
},
|
|
213
|
+
];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* A table that claims an aggregate none of its columns carry. Either the table
|
|
218
|
+
* was relabelled and the columns were not, or the aggregate moved on without
|
|
219
|
+
* it; both leave the `persists` claim asserting more than it can back up.
|
|
220
|
+
*/
|
|
221
|
+
function drift(
|
|
222
|
+
catalog: Catalog,
|
|
223
|
+
index: CatalogIndex,
|
|
224
|
+
store: Store,
|
|
225
|
+
table: Table,
|
|
226
|
+
): Problem[] {
|
|
227
|
+
const aggregateId = table.persists?.aggregate;
|
|
228
|
+
if (!aggregateId) return [];
|
|
229
|
+
// A derived table holds a copy shaped for reading, not the aggregate's own
|
|
230
|
+
// fields, so it has nothing to have drifted FROM. Only a table that claims
|
|
231
|
+
// to BE where the aggregate lives can contradict it.
|
|
232
|
+
if (table.role === "projection" || table.role === "outbox") return [];
|
|
233
|
+
const aggregate = index.aggregateById.get(aggregateId);
|
|
234
|
+
if (!aggregate) return [];
|
|
235
|
+
|
|
236
|
+
const declared = new Set<string>();
|
|
237
|
+
for (const { block } of aggregateBlocks(aggregate)) {
|
|
238
|
+
for (const field of blockFields(catalog, block)) declared.add(field.name);
|
|
239
|
+
}
|
|
240
|
+
// Nothing to drift from: an aggregate whose blocks are known by name only
|
|
241
|
+
// cannot contradict a schema.
|
|
242
|
+
if (declared.size === 0) return [];
|
|
243
|
+
|
|
244
|
+
const mapped = table.columns.filter((c) => {
|
|
245
|
+
if (!c.maps) return false;
|
|
246
|
+
const head = mapsFieldPath(c.maps).split(".")[0] ?? "";
|
|
247
|
+
return declared.has(head);
|
|
248
|
+
});
|
|
249
|
+
if (mapped.length > 0) return [];
|
|
250
|
+
|
|
251
|
+
const claimed = table.columns.filter((c) => c.maps).length;
|
|
252
|
+
return [
|
|
253
|
+
{
|
|
254
|
+
kind: "persistence-drift",
|
|
255
|
+
severity: "warning",
|
|
256
|
+
context: contextOf(index, store.owner),
|
|
257
|
+
service: store.owner,
|
|
258
|
+
id: table.id,
|
|
259
|
+
peer: aggregateId,
|
|
260
|
+
note:
|
|
261
|
+
claimed === 0
|
|
262
|
+
? `no column of ${table.name} maps to a field of ${aggregateId}`
|
|
263
|
+
: `${claimed} column(s) of ${table.name} map to fields ${aggregateId} no longer declares`,
|
|
264
|
+
source: store.source,
|
|
265
|
+
},
|
|
266
|
+
];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* A column and the field it carries that no longer agree on a type. One of the
|
|
271
|
+
* two has been changed and the other has not; which one is the reader's call,
|
|
272
|
+
* so the problem names both and stops there.
|
|
273
|
+
*/
|
|
274
|
+
function typeDrift(
|
|
275
|
+
catalog: Catalog,
|
|
276
|
+
index: CatalogIndex,
|
|
277
|
+
store: Store,
|
|
278
|
+
table: Table,
|
|
279
|
+
): Problem[] {
|
|
280
|
+
const out: Problem[] = [];
|
|
281
|
+
const aggregateId = table.persists?.aggregate;
|
|
282
|
+
const aggregate = aggregateId
|
|
283
|
+
? index.aggregateById.get(aggregateId)
|
|
284
|
+
: undefined;
|
|
285
|
+
if (!aggregate) return out;
|
|
286
|
+
|
|
287
|
+
const fields = new Map<string, string>();
|
|
288
|
+
for (const { block } of aggregateBlocks(aggregate)) {
|
|
289
|
+
for (const field of blockFields(catalog, block)) {
|
|
290
|
+
// First declaration wins: two blocks of one aggregate naming the same
|
|
291
|
+
// field agree about it far more often than not, and a column mapping to
|
|
292
|
+
// "Id" is not made clearer by picking the second one.
|
|
293
|
+
if (!fields.has(field.name)) fields.set(field.name, field.type);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
for (const column of table.columns) {
|
|
298
|
+
if (!column.maps) continue;
|
|
299
|
+
const head = mapsFieldPath(column.maps).split(".")[0] ?? "";
|
|
300
|
+
const domainType = fields.get(head);
|
|
301
|
+
if (!domainType || !typesDisagree(column.type, domainType)) continue;
|
|
302
|
+
out.push({
|
|
303
|
+
kind: "column-type",
|
|
304
|
+
severity: "warning",
|
|
305
|
+
context: contextOf(index, store.owner),
|
|
306
|
+
service: store.owner,
|
|
307
|
+
id: `${table.id}.${column.name}`,
|
|
308
|
+
peer: column.maps,
|
|
309
|
+
note: `column is ${column.type}, ${column.maps} is ${domainType}`,
|
|
310
|
+
source: store.source,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
return out;
|
|
314
|
+
}
|