@shortlink-org/portolan 0.1.0 → 0.2.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/README.md +69 -205
- package/cli/init.mjs +285 -0
- package/cli/init.test.mjs +158 -0
- package/cli/portolan.mjs +37 -73
- package/index.html +19 -0
- package/package.json +5 -10
- package/plugins/README.md +66 -27
- package/plugins/portolan-go.wasm +0 -0
- package/portolan.json +52 -73
- package/public/landing/cats/bug-hunter.webp +0 -0
- package/public/landing/cats/compass-nap.webp +0 -0
- package/public/landing/cats/diagrammer.webp +0 -0
- package/public/landing/cats/docs-reader.webp +0 -0
- package/public/landing/cats/laptop.webp +0 -0
- package/public/landing/cats/map-inspector.webp +0 -0
- package/public/landing/cats/server-break.webp +0 -0
- package/public/landing/cats/star-mapper.webp +0 -0
- package/public/landing/cats/system-builder.webp +0 -0
- package/public/landing/cats/thread-tangle.webp +0 -0
- package/public/og.png +0 -0
- package/public/readme-header.webp +0 -0
- package/schema/portolan.schema.json +180 -0
- package/scripts/builtin-plugins.mjs +14 -0
- package/scripts/delivery-presets.mjs +353 -0
- package/scripts/gen.mjs +37 -4
- package/scripts/history.mjs +126 -0
- package/scripts/history.test.mjs +110 -0
- package/scripts/host-plugins/fetch-bsr.mjs +351 -0
- package/{plugins/fetch-bsr/options.schema.json → scripts/host-plugins/fetch-bsr.options.json} +4 -0
- package/scripts/host-plugins/fetch-bsr.test.mjs +212 -0
- package/scripts/host-plugins/fetch-csr.mjs +386 -0
- package/scripts/host-plugins/fetch-csr.test.mjs +178 -0
- package/scripts/host-plugins/fetch-git.mjs +379 -0
- package/scripts/host-plugins/fetch-git.test.mjs +210 -0
- package/scripts/local-api.mjs +22 -4
- package/scripts/local-api.test.mjs +84 -2
- package/scripts/manifest.mjs +3 -2
- package/scripts/package-smoke.mjs +6 -2
- package/scripts/plugin-host.mjs +77 -18
- package/scripts/plugin-host.test.mjs +50 -1
- package/scripts/plugin-wasm-worker.mjs +6 -3
- package/scripts/run-builtin.mjs +6 -26
- package/scripts/schema.mjs +6 -1
- package/src/app/App.tsx +24 -429
- package/src/app/Breadcrumbs.test.ts +7 -0
- package/src/app/Breadcrumbs.tsx +15 -2
- package/src/app/CatalogApp.tsx +436 -0
- package/src/app/Sidebar.tsx +0 -1
- package/src/chat/ChatPanel.tsx +98 -41
- package/src/chat/Composer.tsx +4 -2
- package/src/chat/Conversation.tsx +79 -15
- package/src/chat/Header.tsx +43 -9
- package/src/chat/Starter.tsx +64 -3
- package/src/chat/Steps.tsx +1 -1
- package/src/chat/flags.test.ts +51 -14
- package/src/chat/flags.ts +8 -2
- package/src/chat/page-context.test.ts +51 -0
- package/src/chat/page-context.ts +128 -0
- package/src/chat/prompt.test.ts +49 -1
- package/src/chat/prompt.ts +42 -1
- package/src/chat/tools.ts +7 -2
- package/src/chat/transport.ts +16 -4
- package/src/components/ProblemRow.tsx +182 -0
- package/src/data.ts +20 -5
- package/src/er/ErCanvas.tsx +1 -1
- package/src/graph/DependencyGraph.tsx +10 -1
- package/src/graph/FocusedEventGraph.tsx +1 -1
- package/src/graph/GraphToolbar.tsx +2 -4
- package/src/index.css +220 -2
- package/src/landing/DraggableReveal.tsx +168 -0
- package/src/landing/EstateGraph.tsx +28 -0
- package/src/landing/FlowPlayback.tsx +282 -0
- package/src/landing/HeroMap.tsx +128 -0
- package/src/landing/LandingPage.tsx +757 -0
- package/src/landing/ProductFrame.tsx +68 -0
- package/src/landing/ProductTour.tsx +371 -0
- package/src/landing/catalog.ts +25 -0
- package/src/landing/motion.tsx +52 -0
- package/src/lib/all-problems.ts +28 -0
- package/src/lib/local-api.ts +35 -1
- package/src/lib/setup-info.test.ts +8 -0
- package/src/lib/setup-info.ts +3 -2
- package/src/lib/source-code.ts +2 -1
- package/src/likec4/C4View.tsx +5 -0
- package/src/likec4/InteractiveView.tsx +5 -1
- package/src/map/ContextMapGraph.tsx +47 -20
- package/src/pages/ContextMap.tsx +5 -1
- package/src/pages/FlowDetail.tsx +24 -7
- package/src/pages/Overview.tsx +68 -12
- package/src/pages/Problems.tsx +6 -193
- package/src/pages/Settings.tsx +133 -108
- package/src/pages/settings/DeliverySettings.tsx +229 -0
- package/src/pages/settings/PreferencesSettings.tsx +101 -0
- package/src/routes.test.ts +20 -0
- package/src/routes.ts +13 -1
- package/vite.config.ts +12 -8
- package/catalog/enum_test.go +0 -46
- package/catalog/model.go +0 -932
- package/catalog/roundtrip_test.go +0 -185
- package/catalog/via_test.go +0 -38
- package/go.mod +0 -14
- package/go.sum +0 -14
- package/internal/commands/cargo.go +0 -154
- package/internal/commands/commands.go +0 -143
- package/internal/commands/commands_test.go +0 -323
- package/internal/commands/gradle.go +0 -77
- package/internal/commands/justfile.go +0 -82
- package/internal/commands/makefile.go +0 -106
- package/internal/commands/maven.go +0 -93
- package/internal/commands/packagejson.go +0 -159
- package/internal/commands/pyproject.go +0 -169
- package/internal/commands/taskfile.go +0 -101
- package/internal/commands/testdata/estate/.cargo/config.toml +0 -7
- package/internal/commands/testdata/estate/Makefile +0 -33
- package/internal/commands/testdata/estate/Taskfile.yml +0 -21
- package/internal/commands/testdata/estate/build.gradle.kts +0 -21
- package/internal/commands/testdata/estate/justfile +0 -20
- package/internal/commands/testdata/estate/package.json +0 -13
- package/internal/commands/testdata/estate/pom.xml +0 -29
- package/internal/commands/testdata/estate/pyproject.toml +0 -27
- package/internal/commands/testdata/estate/xtask/src/main.rs +0 -27
- package/internal/commands/testdata/golden/commands.json +0 -305
- package/internal/gohttp/analyze.go +0 -2500
- package/internal/gohttp/endpoints.go +0 -1067
- package/internal/gohttp/roots.go +0 -320
- package/internal/gohttp/typed.go +0 -143
- package/internal/goscan/constants.go +0 -85
- package/internal/goscan/goscan_test.go +0 -227
- package/internal/goscan/names.go +0 -52
- package/internal/goscan/parse_test.go +0 -11
- package/internal/goscan/source.go +0 -36
- package/internal/goscan/tree.go +0 -155
- package/internal/goscan/types.go +0 -99
- package/internal/wsdl/ids.go +0 -127
- package/internal/wsdl/ids_test.go +0 -21
- package/internal/wsdl/model.go +0 -62
- package/internal/wsdl/parse.go +0 -920
- package/internal/wsdl/parse_test.go +0 -133
- package/plugin/describe.go +0 -107
- package/plugin/describe_test.go +0 -114
- package/plugin/protocol.go +0 -117
- package/plugin/schematest/schematest.go +0 -126
- package/plugins/extract-adr/describe.go +0 -19
- package/plugins/extract-adr/describe_test.go +0 -11
- package/plugins/extract-adr/extract.go +0 -153
- package/plugins/extract-adr/extract_test.go +0 -350
- package/plugins/extract-adr/history.go +0 -99
- package/plugins/extract-adr/main.go +0 -65
- package/plugins/extract-adr/parse.go +0 -642
- package/plugins/extract-adr/parse_test.go +0 -405
- package/plugins/extract-asyncapi/describe.go +0 -19
- package/plugins/extract-asyncapi/describe_test.go +0 -11
- package/plugins/extract-asyncapi/extract.go +0 -315
- package/plugins/extract-asyncapi/extract_test.go +0 -197
- package/plugins/extract-asyncapi/main.go +0 -48
- package/plugins/extract-asyncapi/spec.go +0 -151
- package/plugins/extract-commands/describe.go +0 -19
- package/plugins/extract-commands/describe_test.go +0 -11
- package/plugins/extract-commands/extract.go +0 -79
- package/plugins/extract-commands/extract_test.go +0 -92
- package/plugins/extract-commands/main.go +0 -55
- package/plugins/extract-csr/avro.go +0 -258
- package/plugins/extract-csr/describe.go +0 -19
- package/plugins/extract-csr/describe_test.go +0 -11
- package/plugins/extract-csr/extract.go +0 -338
- package/plugins/extract-csr/extract_test.go +0 -374
- package/plugins/extract-csr/jsonschema.go +0 -343
- package/plugins/extract-csr/lock.go +0 -25
- package/plugins/extract-csr/main.go +0 -84
- package/plugins/extract-csr/subject.go +0 -102
- package/plugins/extract-flows/describe.go +0 -19
- package/plugins/extract-flows/describe_test.go +0 -11
- package/plugins/extract-flows/extract.go +0 -91
- package/plugins/extract-flows/main.go +0 -45
- package/plugins/extract-flows/parse.go +0 -593
- package/plugins/extract-flows/parse_test.go +0 -204
- package/plugins/extract-glossary/describe.go +0 -19
- package/plugins/extract-glossary/describe_test.go +0 -11
- package/plugins/extract-glossary/extract.go +0 -115
- package/plugins/extract-glossary/extract_test.go +0 -220
- package/plugins/extract-glossary/main.go +0 -59
- package/plugins/extract-glossary/parse.go +0 -214
- package/plugins/extract-glossary/parse_test.go +0 -203
- package/plugins/extract-go/aggregate.go +0 -214
- package/plugins/extract-go/client.go +0 -409
- package/plugins/extract-go/client_test.go +0 -266
- package/plugins/extract-go/describe.go +0 -19
- package/plugins/extract-go/describe_test.go +0 -11
- package/plugins/extract-go/enum.go +0 -195
- package/plugins/extract-go/enum_test.go +0 -82
- package/plugins/extract-go/event.go +0 -99
- package/plugins/extract-go/extract.go +0 -191
- package/plugins/extract-go/extract_test.go +0 -261
- package/plugins/extract-go/flow.go +0 -1441
- package/plugins/extract-go/flow_test.go +0 -609
- package/plugins/extract-go/httpclient.go +0 -174
- package/plugins/extract-go/httpclient_test.go +0 -296
- package/plugins/extract-go/ids.go +0 -92
- package/plugins/extract-go/layout.go +0 -225
- package/plugins/extract-go/layout_test.go +0 -125
- package/plugins/extract-go/lifecycle.go +0 -324
- package/plugins/extract-go/lifecycle_test.go +0 -108
- package/plugins/extract-go/main.go +0 -79
- package/plugins/extract-go/operation.go +0 -157
- package/plugins/extract-go/source.go +0 -316
- package/plugins/extract-go/transport.go +0 -321
- package/plugins/extract-go/transport_test.go +0 -145
- package/plugins/extract-go/wiring.go +0 -434
- package/plugins/extract-go-nats/describe.go +0 -19
- package/plugins/extract-go-nats/describe_test.go +0 -11
- package/plugins/extract-go-nats/extract.go +0 -177
- package/plugins/extract-go-nats/extract_test.go +0 -389
- package/plugins/extract-go-nats/index.go +0 -379
- package/plugins/extract-go-nats/main.go +0 -42
- package/plugins/extract-go-nats/resolve.go +0 -161
- package/plugins/extract-go-nats/sites.go +0 -224
- package/plugins/extract-graphql/describe.go +0 -19
- package/plugins/extract-graphql/describe_test.go +0 -11
- package/plugins/extract-graphql/extract.go +0 -433
- package/plugins/extract-graphql/extract_test.go +0 -256
- package/plugins/extract-graphql/ids.go +0 -49
- package/plugins/extract-graphql/lex.go +0 -237
- package/plugins/extract-graphql/main.go +0 -51
- package/plugins/extract-graphql/parse.go +0 -621
- package/plugins/extract-graphql/parse_test.go +0 -122
- package/plugins/extract-http-clients/describe.go +0 -19
- package/plugins/extract-http-clients/describe_test.go +0 -11
- package/plugins/extract-http-clients/extract.go +0 -705
- package/plugins/extract-http-clients/extract_test.go +0 -1263
- package/plugins/extract-http-clients/main.go +0 -42
- package/plugins/extract-openapi/describe.go +0 -19
- package/plugins/extract-openapi/describe_test.go +0 -11
- package/plugins/extract-openapi/discover.go +0 -243
- package/plugins/extract-openapi/extract.go +0 -526
- package/plugins/extract-openapi/extract_test.go +0 -545
- package/plugins/extract-openapi/main.go +0 -75
- package/plugins/extract-openapi/spec.go +0 -350
- package/plugins/extract-project/describe.go +0 -19
- package/plugins/extract-project/describe_test.go +0 -11
- package/plugins/extract-project/extract.go +0 -221
- package/plugins/extract-project/extract_test.go +0 -109
- package/plugins/extract-project/main.go +0 -41
- package/plugins/extract-proto/ast.go +0 -125
- package/plugins/extract-proto/consumes.go +0 -77
- package/plugins/extract-proto/describe.go +0 -19
- package/plugins/extract-proto/extract.go +0 -293
- package/plugins/extract-proto/extract_test.go +0 -459
- package/plugins/extract-proto/ids.go +0 -89
- package/plugins/extract-proto/ids_test.go +0 -57
- package/plugins/extract-proto/lex.go +0 -285
- package/plugins/extract-proto/main.go +0 -100
- package/plugins/extract-proto/module.go +0 -120
- package/plugins/extract-proto/parse.go +0 -720
- package/plugins/extract-proto/parse_test.go +0 -307
- package/plugins/extract-proto/provides.go +0 -236
- package/plugins/extract-proto/resolve.go +0 -222
- package/plugins/extract-proto/resolve_test.go +0 -119
- package/plugins/extract-redis/describe.go +0 -19
- package/plugins/extract-redis/describe_test.go +0 -11
- package/plugins/extract-redis/extract.go +0 -183
- package/plugins/extract-redis/extract_test.go +0 -168
- package/plugins/extract-redis/keyspaces.go +0 -469
- package/plugins/extract-redis/main.go +0 -44
- package/plugins/extract-river/describe.go +0 -19
- package/plugins/extract-river/describe_test.go +0 -11
- package/plugins/extract-river/extract.go +0 -507
- package/plugins/extract-river/extract_test.go +0 -132
- package/plugins/extract-river/main.go +0 -42
- package/plugins/extract-sql/ddl.go +0 -893
- package/plugins/extract-sql/ddl_test.go +0 -401
- package/plugins/extract-sql/describe.go +0 -19
- package/plugins/extract-sql/describe_test.go +0 -11
- package/plugins/extract-sql/layout.go +0 -221
- package/plugins/extract-sql/layout_test.go +0 -55
- package/plugins/extract-sql/lineage.go +0 -107
- package/plugins/extract-sql/main.go +0 -141
- package/plugins/extract-sql/maps.go +0 -564
- package/plugins/extract-sql/maps_java.go +0 -117
- package/plugins/extract-sql/maps_rust.go +0 -333
- package/plugins/extract-sql/maps_rust_test.go +0 -70
- package/plugins/extract-sql/maps_test.go +0 -204
- package/plugins/extract-sql/maps_ts.go +0 -398
- package/plugins/extract-sql/maps_ts_test.go +0 -136
- package/plugins/extract-sql/projection.go +0 -70
- package/plugins/extract-sql/projection_test.go +0 -50
- package/plugins/extract-sql/store.go +0 -420
- package/plugins/extract-sql/store_test.go +0 -233
- package/plugins/extract-sql/view.go +0 -295
- package/plugins/extract-watermill/describe.go +0 -19
- package/plugins/extract-watermill/describe_test.go +0 -11
- package/plugins/extract-watermill/extract.go +0 -1228
- package/plugins/extract-watermill/extract_test.go +0 -234
- package/plugins/extract-watermill/main.go +0 -41
- package/plugins/extract-wsdl/describe.go +0 -19
- package/plugins/extract-wsdl/describe_test.go +0 -11
- package/plugins/extract-wsdl/extract.go +0 -148
- package/plugins/extract-wsdl/extract_test.go +0 -45
- package/plugins/extract-wsdl/main.go +0 -53
- package/plugins/fetch-bsr/auth.go +0 -114
- package/plugins/fetch-bsr/bsr.go +0 -240
- package/plugins/fetch-bsr/cache.go +0 -66
- package/plugins/fetch-bsr/describe.go +0 -19
- package/plugins/fetch-bsr/fetch.go +0 -192
- package/plugins/fetch-bsr/fetch_test.go +0 -465
- package/plugins/fetch-bsr/lock.go +0 -71
- package/plugins/fetch-bsr/main.go +0 -117
- package/plugins/fetch-csr/auth.go +0 -95
- package/plugins/fetch-csr/cache.go +0 -69
- package/plugins/fetch-csr/describe.go +0 -19
- package/plugins/fetch-csr/describe_test.go +0 -11
- package/plugins/fetch-csr/fetch.go +0 -254
- package/plugins/fetch-csr/fetch_test.go +0 -494
- package/plugins/fetch-csr/lock.go +0 -95
- package/plugins/fetch-csr/main.go +0 -122
- package/plugins/fetch-csr/registry.go +0 -209
- package/plugins/fetch-git/cache.go +0 -65
- package/plugins/fetch-git/describe.go +0 -19
- package/plugins/fetch-git/describe_test.go +0 -11
- package/plugins/fetch-git/fetch.go +0 -181
- package/plugins/fetch-git/fetch_test.go +0 -332
- package/plugins/fetch-git/git.go +0 -169
- package/plugins/fetch-git/lock.go +0 -72
- package/plugins/fetch-git/main.go +0 -127
- package/plugins/fetch-git/offline.go +0 -39
- package/plugins/fetch-git/pin.go +0 -90
- package/plugins/fetch-git/pin_test.go +0 -74
- package/plugins/gen-backstage/describe.go +0 -17
- package/plugins/gen-backstage/main.go +0 -22
- package/plugins/gen-backstage/plugin.go +0 -473
- package/plugins/gen-backstage/plugin_test.go +0 -145
- package/plugins/gen-backstage.wasm +0 -0
- package/plugins/gen-markdown/adr.go +0 -162
- package/plugins/gen-markdown/aggregate.go +0 -362
- package/plugins/gen-markdown/canonical.go +0 -93
- package/plugins/gen-markdown/context.go +0 -90
- package/plugins/gen-markdown/coverage_test.go +0 -89
- package/plugins/gen-markdown/describe.go +0 -19
- package/plugins/gen-markdown/describe_test.go +0 -11
- package/plugins/gen-markdown/external.go +0 -71
- package/plugins/gen-markdown/flow.go +0 -277
- package/plugins/gen-markdown/glossary.go +0 -76
- package/plugins/gen-markdown/glossary_test.go +0 -148
- package/plugins/gen-markdown/llms.go +0 -302
- package/plugins/gen-markdown/main.go +0 -22
- package/plugins/gen-markdown/markdown.go +0 -254
- package/plugins/gen-markdown/markdown_test.go +0 -100
- package/plugins/gen-markdown/module.go +0 -107
- package/plugins/gen-markdown/plugin.go +0 -39
- package/plugins/gen-markdown/quality_test.go +0 -176
- package/plugins/gen-markdown/redis_test.go +0 -38
- package/plugins/gen-markdown/render.go +0 -344
- package/plugins/gen-markdown/render_test.go +0 -186
- package/plugins/gen-markdown/service.go +0 -456
- package/plugins/gen-markdown/source.go +0 -177
- package/plugins/gen-markdown/store.go +0 -201
- package/plugins/gen-markdown.wasm +0 -0
- package/plugins/gen-mermaid/describe.go +0 -17
- package/plugins/gen-mermaid/main.go +0 -22
- package/plugins/gen-mermaid/plugin.go +0 -104
- package/plugins/gen-mermaid/plugin_test.go +0 -33
- package/plugins/gen-mermaid.wasm +0 -0
- package/plugins/openapi/ids.go +0 -261
- package/plugins/openapi/ids_test.go +0 -98
- package/plugins/verify-codeowners/describe.go +0 -19
- package/plugins/verify-codeowners/describe_test.go +0 -11
- package/plugins/verify-codeowners/main.go +0 -75
- package/plugins/verify-codeowners/match.go +0 -85
- package/plugins/verify-codeowners/match_test.go +0 -47
- package/plugins/verify-codeowners/owners.go +0 -164
- package/plugins/verify-codeowners/owners_test.go +0 -225
- package/plugins/verify-codeowners/parse.go +0 -90
- package/plugins/verify-codeowners/parse_test.go +0 -62
- package/plugins/verify-otel/describe.go +0 -19
- package/plugins/verify-otel/describe_test.go +0 -11
- package/plugins/verify-otel/main.go +0 -53
- package/plugins/verify-otel/match.go +0 -336
- package/plugins/verify-otel/otlp.go +0 -200
- package/plugins/verify-otel/verify.go +0 -734
- package/plugins/verify-otel/verify_test.go +0 -460
- /package/{plugins/fetch-csr/options.schema.json → scripts/host-plugins/fetch-csr.options.json} +0 -0
- /package/{plugins/fetch-git/options.schema.json → scripts/host-plugins/fetch-git.options.json} +0 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// What the reader is looking at when they open Ask the catalog.
|
|
2
|
+
//
|
|
3
|
+
// The route is navigation context, not evidence. It gives the model a useful
|
|
4
|
+
// referent for “this flow” and lets the empty conversation offer questions
|
|
5
|
+
// about the thing already on screen. The answer must still read the generated
|
|
6
|
+
// markdown page before it claims anything about that thing.
|
|
7
|
+
|
|
8
|
+
import type { Kind } from "../lib/kinds";
|
|
9
|
+
import type { PaletteItem } from "../lib/palette";
|
|
10
|
+
import { catalog } from "../data";
|
|
11
|
+
import { paletteItems } from "../lib/palette";
|
|
12
|
+
|
|
13
|
+
export interface ChatPageContext {
|
|
14
|
+
kind: Kind;
|
|
15
|
+
id: string;
|
|
16
|
+
title: string;
|
|
17
|
+
/** Path relative to the profile's docs directory, as llms.txt spells it. */
|
|
18
|
+
docPath: string | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const items = paletteItems(catalog);
|
|
22
|
+
|
|
23
|
+
function docsPath(item: PaletteItem): string | null {
|
|
24
|
+
if (!item.path) return null;
|
|
25
|
+
const parts = item.path.split("?")[0]?.split("#")[0]?.split("/").filter(Boolean) ?? [];
|
|
26
|
+
|
|
27
|
+
switch (item.kind) {
|
|
28
|
+
case "context":
|
|
29
|
+
return `${item.id}/README.md`;
|
|
30
|
+
case "service":
|
|
31
|
+
return parts.length >= 3 ? `${parts[1]}/${parts[2]}/README.md` : null;
|
|
32
|
+
case "aggregate":
|
|
33
|
+
case "event":
|
|
34
|
+
case "vo":
|
|
35
|
+
case "entity":
|
|
36
|
+
case "enum":
|
|
37
|
+
case "command":
|
|
38
|
+
case "query":
|
|
39
|
+
return parts.length >= 4
|
|
40
|
+
? `${parts[1]}/${parts[2]}/aggregates/${parts[3]}.md`
|
|
41
|
+
: null;
|
|
42
|
+
case "flow":
|
|
43
|
+
return `flows/${item.name}.md`;
|
|
44
|
+
case "adr":
|
|
45
|
+
return `adr/${item.id}.md`;
|
|
46
|
+
case "external":
|
|
47
|
+
return parts[1] ? `externals/${parts[1]}.md` : null;
|
|
48
|
+
case "module":
|
|
49
|
+
return parts[1] ? `modules/${parts[1]}.md` : null;
|
|
50
|
+
default:
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function titleOf(item: PaletteItem): string {
|
|
56
|
+
if (item.kind === "flow" || item.kind === "adr" || item.kind === "context") {
|
|
57
|
+
return item.detail;
|
|
58
|
+
}
|
|
59
|
+
return item.name;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The exact catalog entity page at this route, if there is one. */
|
|
63
|
+
export function pageContextFrom(
|
|
64
|
+
candidates: readonly PaletteItem[],
|
|
65
|
+
pathname: string,
|
|
66
|
+
): ChatPageContext | null {
|
|
67
|
+
const item = candidates.find((candidate) => candidate.path === pathname);
|
|
68
|
+
if (!item) return null;
|
|
69
|
+
return {
|
|
70
|
+
kind: item.kind,
|
|
71
|
+
id: item.id,
|
|
72
|
+
title: titleOf(item),
|
|
73
|
+
docPath: docsPath(item),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function pageContext(pathname: string): ChatPageContext | null {
|
|
78
|
+
return pageContextFrom(items, pathname);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Questions that use the current page instead of making the reader restate it. */
|
|
82
|
+
export function contextQuestions(context: ChatPageContext): string[] {
|
|
83
|
+
const id = context.id;
|
|
84
|
+
switch (context.kind) {
|
|
85
|
+
case "flow":
|
|
86
|
+
return [
|
|
87
|
+
`Summarize ${id} and its trust status.`,
|
|
88
|
+
`Which contexts and services does ${id} cross?`,
|
|
89
|
+
`Which steps in ${id} are declared rather than verified?`,
|
|
90
|
+
`Which decisions explain ${id}?`,
|
|
91
|
+
];
|
|
92
|
+
case "context":
|
|
93
|
+
return [
|
|
94
|
+
`What does ${id} own?`,
|
|
95
|
+
`Which services and aggregates are inside ${id}?`,
|
|
96
|
+
`Which flows cross the boundary of ${id}?`,
|
|
97
|
+
`Which problems and decisions touch ${id}?`,
|
|
98
|
+
];
|
|
99
|
+
case "service":
|
|
100
|
+
return [
|
|
101
|
+
`Summarize ${id} and what it owns.`,
|
|
102
|
+
`What does ${id} provide and consume?`,
|
|
103
|
+
`Which flows pass through ${id}?`,
|
|
104
|
+
`Which decisions and problems touch ${id}?`,
|
|
105
|
+
];
|
|
106
|
+
case "aggregate":
|
|
107
|
+
return [
|
|
108
|
+
`Summarize the invariants of ${id}.`,
|
|
109
|
+
`Show the lifecycle of ${id}.`,
|
|
110
|
+
`Which commands, queries and events belong to ${id}?`,
|
|
111
|
+
`Which flows change ${id}?`,
|
|
112
|
+
];
|
|
113
|
+
case "adr":
|
|
114
|
+
return [
|
|
115
|
+
`Summarize ${id} and its consequences.`,
|
|
116
|
+
`Which parts of the catalog does ${id} affect?`,
|
|
117
|
+
`Which flows are explained by ${id}?`,
|
|
118
|
+
`What evidence supports ${id}?`,
|
|
119
|
+
];
|
|
120
|
+
default:
|
|
121
|
+
return [
|
|
122
|
+
`Summarize ${id}.`,
|
|
123
|
+
`What is connected to ${id}?`,
|
|
124
|
+
`Which flows touch ${id}?`,
|
|
125
|
+
`Which decisions and problems touch ${id}?`,
|
|
126
|
+
];
|
|
127
|
+
}
|
|
128
|
+
}
|
package/src/chat/prompt.test.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
clipPage,
|
|
4
|
+
instructions,
|
|
5
|
+
pagePath,
|
|
6
|
+
PAGE_LIMIT,
|
|
7
|
+
promptPageContext,
|
|
8
|
+
} from "./prompt";
|
|
3
9
|
|
|
4
10
|
describe("pagePath", () => {
|
|
5
11
|
it("accepts a path as the index spells it", () => {
|
|
@@ -40,4 +46,46 @@ describe("instructions", () => {
|
|
|
40
46
|
expect(text).toContain("read_page");
|
|
41
47
|
expect(text.endsWith("- [Auth](docs/auth/README.md)")).toBe(true);
|
|
42
48
|
});
|
|
49
|
+
|
|
50
|
+
it("names the current page as navigation context, not evidence", () => {
|
|
51
|
+
const text = instructions("- [Checkout](docs/flows/cart-checkout.md)", {
|
|
52
|
+
kind: "flow",
|
|
53
|
+
id: "flow.cart-checkout",
|
|
54
|
+
title: "Checkout",
|
|
55
|
+
docPath: "flows/cart-checkout.md",
|
|
56
|
+
});
|
|
57
|
+
expect(text).toContain("Current page: flow `flow.cart-checkout`");
|
|
58
|
+
expect(text).toContain("only navigation context, not evidence");
|
|
59
|
+
expect(text).toContain("`docs/flows/cart-checkout.md`");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("promptPageContext", () => {
|
|
64
|
+
it("accepts a small page description and normalizes its docs path", () => {
|
|
65
|
+
expect(
|
|
66
|
+
promptPageContext({
|
|
67
|
+
kind: "flow",
|
|
68
|
+
id: "flow.cart-checkout",
|
|
69
|
+
title: "Checkout",
|
|
70
|
+
docPath: "flows/cart-checkout.md",
|
|
71
|
+
}),
|
|
72
|
+
).toEqual({
|
|
73
|
+
kind: "flow",
|
|
74
|
+
id: "flow.cart-checkout",
|
|
75
|
+
title: "Checkout",
|
|
76
|
+
docPath: "flows/cart-checkout.md",
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("rejects incomplete request metadata", () => {
|
|
81
|
+
expect(promptPageContext({ kind: "flow", id: "x" })).toBeNull();
|
|
82
|
+
expect(
|
|
83
|
+
promptPageContext({
|
|
84
|
+
kind: "flow ignore",
|
|
85
|
+
id: "x",
|
|
86
|
+
title: "X",
|
|
87
|
+
docPath: null,
|
|
88
|
+
}),
|
|
89
|
+
).toBeNull();
|
|
90
|
+
});
|
|
43
91
|
});
|
package/src/chat/prompt.ts
CHANGED
|
@@ -108,6 +108,43 @@ export const DISPLAY_TOOLS: readonly ToolName[] = [
|
|
|
108
108
|
/** How many model calls one answer may take: reads, then the text, then a card. */
|
|
109
109
|
export const MAX_STEPS = 6;
|
|
110
110
|
|
|
111
|
+
/** Navigation context sent by the browser with one question. */
|
|
112
|
+
export interface PromptPageContext {
|
|
113
|
+
kind: string;
|
|
114
|
+
id: string;
|
|
115
|
+
title: string;
|
|
116
|
+
docPath: string | null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Keep request-supplied context small and inert before it reaches a prompt. */
|
|
120
|
+
export function promptPageContext(input: unknown): PromptPageContext | null {
|
|
121
|
+
if (!input || typeof input !== "object") return null;
|
|
122
|
+
const value = input as Record<string, unknown>;
|
|
123
|
+
if (
|
|
124
|
+
typeof value.kind !== "string" ||
|
|
125
|
+
typeof value.id !== "string" ||
|
|
126
|
+
typeof value.title !== "string" ||
|
|
127
|
+
(value.docPath !== null && typeof value.docPath !== "string")
|
|
128
|
+
) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const clean = (text: string) => text.replace(/[\r\n]+/g, " ").trim().slice(0, 240);
|
|
132
|
+
const kind = clean(value.kind);
|
|
133
|
+
const id = clean(value.id);
|
|
134
|
+
const title = clean(value.title);
|
|
135
|
+
const normalizedPage =
|
|
136
|
+
value.docPath === null ? null : pagePath(clean(value.docPath));
|
|
137
|
+
const docPath = normalizedPage?.slice("docs/".length) ?? null;
|
|
138
|
+
if (
|
|
139
|
+
!/^[a-z][a-z-]{0,31}$/.test(kind) ||
|
|
140
|
+
!/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,239}$/.test(id) ||
|
|
141
|
+
!title
|
|
142
|
+
) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
return { kind, id, title, docPath };
|
|
146
|
+
}
|
|
147
|
+
|
|
111
148
|
/**
|
|
112
149
|
* A page path the tool may open, or null. The index links pages as
|
|
113
150
|
* `docs/<context>/<page>.md`; the model is allowed to drop the `docs/` and
|
|
@@ -140,8 +177,12 @@ export function clipPage(text: string): string {
|
|
|
140
177
|
}
|
|
141
178
|
|
|
142
179
|
/** The system prompt, with the index folded in. */
|
|
143
|
-
export function instructions(index: string): string {
|
|
180
|
+
export function instructions(index: string, page?: PromptPageContext | null): string {
|
|
181
|
+
const current = page
|
|
182
|
+
? `\nCurrent page: ${page.kind} \`${page.id}\`. Treat “this”, “it” and similar references as this catalog entity. This route is only navigation context, not evidence: read ${page.docPath ? `\`docs/${page.docPath}\`` : "the relevant page from the index"} before making claims about it.\n`
|
|
183
|
+
: "";
|
|
144
184
|
return `You are the guide to an architecture catalog: bounded contexts, services, aggregates, events, flows and decisions (ADRs). The index of its pages is below.
|
|
185
|
+
${current}
|
|
145
186
|
|
|
146
187
|
How to answer:
|
|
147
188
|
- Answer in the language the question was asked in.
|
package/src/chat/tools.ts
CHANGED
|
@@ -6,14 +6,19 @@
|
|
|
6
6
|
|
|
7
7
|
import { jsonSchema, tool } from "ai";
|
|
8
8
|
import { clipPage, pagePath, TOOL_SPECS } from "./prompt";
|
|
9
|
+
import { activeCatalogProfile } from "../data";
|
|
9
10
|
|
|
10
11
|
const base = import.meta.env.BASE_URL;
|
|
12
|
+
const profileSuffix =
|
|
13
|
+
activeCatalogProfile.id === "portolan" ? "" : `${activeCatalogProfile.id}/`;
|
|
14
|
+
const docsBase = `${base}docs/${profileSuffix}`;
|
|
15
|
+
const indexUrl = profileSuffix ? `${docsBase}llms.txt` : `${base}llms.txt`;
|
|
11
16
|
|
|
12
17
|
/** One page of the generated docs, as the model gets it. */
|
|
13
18
|
export async function readPage(raw: unknown): Promise<string> {
|
|
14
19
|
const path = pagePath(raw);
|
|
15
20
|
if (!path) return "That is not a page of the catalog. Use a path from the index.";
|
|
16
|
-
const response = await fetch(`${
|
|
21
|
+
const response = await fetch(`${docsBase}${path.slice("docs/".length)}`);
|
|
17
22
|
if (!response.ok) return `No page at ${path}. Use a path from the index.`;
|
|
18
23
|
return clipPage(await response.text());
|
|
19
24
|
}
|
|
@@ -21,7 +26,7 @@ export async function readPage(raw: unknown): Promise<string> {
|
|
|
21
26
|
/** llms.txt, once per session. */
|
|
22
27
|
let indexPromise: Promise<string> | null = null;
|
|
23
28
|
export function loadIndex(): Promise<string> {
|
|
24
|
-
indexPromise ??= fetch(
|
|
29
|
+
indexPromise ??= fetch(indexUrl).then((response) => {
|
|
25
30
|
if (!response.ok) {
|
|
26
31
|
indexPromise = null;
|
|
27
32
|
throw new Error("the catalog index (llms.txt) could not be read");
|
package/src/chat/transport.ts
CHANGED
|
@@ -14,7 +14,11 @@ import {
|
|
|
14
14
|
import type { ChatTransport, UIMessage, UIMessageChunk } from "ai";
|
|
15
15
|
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
16
16
|
import type { ChatRoute, OwnModel } from "./flags";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
instructions,
|
|
19
|
+
MAX_STEPS,
|
|
20
|
+
promptPageContext,
|
|
21
|
+
} from "./prompt";
|
|
18
22
|
import { useChatUi } from "./store";
|
|
19
23
|
import { browserTools, loadIndex } from "./tools";
|
|
20
24
|
|
|
@@ -22,10 +26,11 @@ type Answering = Extract<ChatRoute, { kind: "proxy" | "own" }>;
|
|
|
22
26
|
|
|
23
27
|
class OwnTransport implements ChatTransport<UIMessage> {
|
|
24
28
|
private inner: Promise<ChatTransport<UIMessage>> | null = null;
|
|
29
|
+
private contextKey = "";
|
|
25
30
|
|
|
26
31
|
constructor(private readonly own: OwnModel) {}
|
|
27
32
|
|
|
28
|
-
private async build(): Promise<ChatTransport<UIMessage>> {
|
|
33
|
+
private async build(pageContext: unknown): Promise<ChatTransport<UIMessage>> {
|
|
29
34
|
const ui = useChatUi.getState();
|
|
30
35
|
ui.setPhase("index");
|
|
31
36
|
let index: string;
|
|
@@ -41,7 +46,7 @@ class OwnTransport implements ChatTransport<UIMessage> {
|
|
|
41
46
|
});
|
|
42
47
|
const agent = new ToolLoopAgent({
|
|
43
48
|
model: provider.chatModel(this.own.model.trim()),
|
|
44
|
-
instructions: instructions(index),
|
|
49
|
+
instructions: instructions(index, promptPageContext(pageContext)),
|
|
45
50
|
tools: browserTools(),
|
|
46
51
|
stopWhen: stepCountIs(MAX_STEPS),
|
|
47
52
|
});
|
|
@@ -51,7 +56,14 @@ class OwnTransport implements ChatTransport<UIMessage> {
|
|
|
51
56
|
async sendMessages(
|
|
52
57
|
options: Parameters<ChatTransport<UIMessage>["sendMessages"]>[0],
|
|
53
58
|
): Promise<ReadableStream<UIMessageChunk>> {
|
|
54
|
-
|
|
59
|
+
const pageContext = (options.body as { pageContext?: unknown } | undefined)
|
|
60
|
+
?.pageContext;
|
|
61
|
+
const nextContextKey = JSON.stringify(promptPageContext(pageContext));
|
|
62
|
+
if (this.contextKey !== nextContextKey) {
|
|
63
|
+
this.inner = null;
|
|
64
|
+
this.contextKey = nextContextKey;
|
|
65
|
+
}
|
|
66
|
+
this.inner ??= this.build(pageContext).catch((error: unknown) => {
|
|
55
67
|
this.inner = null;
|
|
56
68
|
throw error;
|
|
57
69
|
});
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// One problem, as a row: what the near end is, where both ends live, and
|
|
2
|
+
// the words for what is wrong with the edge between them. The problems page
|
|
3
|
+
// lists every one of these; the overview shows the first few.
|
|
4
|
+
|
|
5
|
+
import { Link } from "react-router";
|
|
6
|
+
import type { Problem } from "../lib/derive";
|
|
7
|
+
import { ctxStyle } from "../lib/context-color";
|
|
8
|
+
import { staggerStyle } from "../lib/motion";
|
|
9
|
+
import {
|
|
10
|
+
eventPath,
|
|
11
|
+
servicePath,
|
|
12
|
+
storePath,
|
|
13
|
+
tablePath,
|
|
14
|
+
viewPath,
|
|
15
|
+
} from "../routes";
|
|
16
|
+
import { KindIcon } from "./kind";
|
|
17
|
+
import { Ident } from "./Ident";
|
|
18
|
+
|
|
19
|
+
/** The icon a problem row carries: what the near end of the edge IS. */
|
|
20
|
+
const KIND_OF: Record<Problem["kind"], "service" | "event" | "table"> = {
|
|
21
|
+
rpc: "service",
|
|
22
|
+
consumer: "event",
|
|
23
|
+
"cross-service-fk": "table",
|
|
24
|
+
"cross-service-lineage": "table",
|
|
25
|
+
"shared-store": "table",
|
|
26
|
+
"persistence-drift": "table",
|
|
27
|
+
"column-type": "table",
|
|
28
|
+
"outbox-payload": "table",
|
|
29
|
+
"proto-missing": "service",
|
|
30
|
+
"shared-channel": "event",
|
|
31
|
+
"channel-undeclared": "event",
|
|
32
|
+
"channel-unpublished": "service",
|
|
33
|
+
"subscription-unresolved": "service",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const KIND_NOTE: Record<Problem["kind"], string> = {
|
|
37
|
+
rpc: "the provider of this call is not in the catalog",
|
|
38
|
+
consumer: "this consumer of the event is not in the catalog",
|
|
39
|
+
"cross-service-fk": "foreign key across a service boundary",
|
|
40
|
+
"cross-service-lineage": "a value copied from another service's schema",
|
|
41
|
+
"shared-store": "a second service writes this database",
|
|
42
|
+
"persistence-drift": "this table no longer carries the aggregate it claims",
|
|
43
|
+
"column-type": "column type and domain type disagree",
|
|
44
|
+
"outbox-payload": "an outbox with no payload column",
|
|
45
|
+
"proto-missing":
|
|
46
|
+
"the provider is in the catalog but answers on no such method",
|
|
47
|
+
"shared-channel": "a second service publishes on this channel",
|
|
48
|
+
"channel-undeclared":
|
|
49
|
+
"this event goes out on a channel the service does not declare",
|
|
50
|
+
"channel-unpublished": "a declared channel no event of this service names",
|
|
51
|
+
"subscription-unresolved":
|
|
52
|
+
"nothing in the catalog publishes what this service listens for",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** Where the near end of a problem lives, by what kind of edge it is. */
|
|
56
|
+
function nearPath(problem: Problem): string | null {
|
|
57
|
+
switch (problem.kind) {
|
|
58
|
+
case "rpc":
|
|
59
|
+
// The near end is the CALLING service either way. `rpc` is a call whose
|
|
60
|
+
// peer is nobody; this is a call whose peer is known and whose method is
|
|
61
|
+
// not - and in both the thing to go and look at is the caller.
|
|
62
|
+
case "proto-missing":
|
|
63
|
+
return servicePath(problem.service);
|
|
64
|
+
case "consumer":
|
|
65
|
+
case "channel-undeclared":
|
|
66
|
+
return eventPath(problem.id);
|
|
67
|
+
// The near end is an event when the service has one on the channel, and
|
|
68
|
+
// the service itself when the claim comes from its document alone.
|
|
69
|
+
case "shared-channel":
|
|
70
|
+
return eventPath(problem.id) ?? servicePath(problem.service);
|
|
71
|
+
case "channel-unpublished":
|
|
72
|
+
case "subscription-unresolved":
|
|
73
|
+
return servicePath(problem.service);
|
|
74
|
+
case "shared-store":
|
|
75
|
+
case "persistence-drift":
|
|
76
|
+
case "outbox-payload":
|
|
77
|
+
return tablePath(problem.id);
|
|
78
|
+
// The id is "<relation>.<column>", so the relation is its own id minus the
|
|
79
|
+
// last segment — and the relation is what the canvas can actually show.
|
|
80
|
+
case "cross-service-fk":
|
|
81
|
+
case "column-type":
|
|
82
|
+
return relationPath(problem.id.split(".").slice(0, -1).join("."));
|
|
83
|
+
// Lineage is stated on a column, except when a view states it whole, so
|
|
84
|
+
// the id is one or the other and both are looked up the same way.
|
|
85
|
+
case "cross-service-lineage":
|
|
86
|
+
return (
|
|
87
|
+
relationPath(problem.id) ??
|
|
88
|
+
relationPath(problem.id.split(".").slice(0, -1).join("."))
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** A table or a view, whichever the id turns out to name. */
|
|
94
|
+
function relationPath(id: string): string | null {
|
|
95
|
+
return tablePath(id) ?? viewPath(id);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Where the far end lives, when the catalog knows it. */
|
|
99
|
+
function peerPath(problem: Problem): string | null {
|
|
100
|
+
switch (problem.kind) {
|
|
101
|
+
case "cross-service-fk":
|
|
102
|
+
return tablePath(problem.peer);
|
|
103
|
+
case "cross-service-lineage":
|
|
104
|
+
return (
|
|
105
|
+
relationPath(problem.peer) ??
|
|
106
|
+
relationPath(problem.peer.split(".").slice(0, -1).join("."))
|
|
107
|
+
);
|
|
108
|
+
case "shared-store":
|
|
109
|
+
case "shared-channel":
|
|
110
|
+
return servicePath(problem.peer);
|
|
111
|
+
case "outbox-payload":
|
|
112
|
+
return storePath(problem.peer);
|
|
113
|
+
default:
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function ProblemRow({ problem, index }: { problem: Problem; index: number }) {
|
|
119
|
+
const near = nearPath(problem);
|
|
120
|
+
const peerTo = peerPath(problem);
|
|
121
|
+
const tone =
|
|
122
|
+
problem.severity === "error"
|
|
123
|
+
? "var(--status-unresolved)"
|
|
124
|
+
: "var(--status-declared)";
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<div
|
|
128
|
+
className="stagger-in flex flex-wrap items-baseline gap-x-3 gap-y-1 rounded-control border px-3 py-2"
|
|
129
|
+
style={{ ...staggerStyle(index), borderColor: tone }}
|
|
130
|
+
>
|
|
131
|
+
<KindIcon kind={KIND_OF[problem.kind]} />
|
|
132
|
+
{near ? (
|
|
133
|
+
<Link
|
|
134
|
+
to={near}
|
|
135
|
+
data-nav-item
|
|
136
|
+
className="mono rounded-control text-accent hover:underline"
|
|
137
|
+
title={problem.id}
|
|
138
|
+
>
|
|
139
|
+
{problem.id}
|
|
140
|
+
</Link>
|
|
141
|
+
) : (
|
|
142
|
+
<Ident value={problem.id} />
|
|
143
|
+
)}
|
|
144
|
+
<span aria-hidden className="text-muted">
|
|
145
|
+
→
|
|
146
|
+
</span>
|
|
147
|
+
{peerTo ? (
|
|
148
|
+
<Link
|
|
149
|
+
to={peerTo}
|
|
150
|
+
className="mono rounded-control hover:underline"
|
|
151
|
+
style={{ color: tone }}
|
|
152
|
+
title={problem.peer}
|
|
153
|
+
>
|
|
154
|
+
{problem.peer}
|
|
155
|
+
</Link>
|
|
156
|
+
) : (
|
|
157
|
+
<Ident
|
|
158
|
+
value={problem.peer}
|
|
159
|
+
className={
|
|
160
|
+
problem.severity === "error" ? "text-unresolved" : "text-declared"
|
|
161
|
+
}
|
|
162
|
+
title={`${problem.peer} — ${KIND_NOTE[problem.kind]}. Click to copy.`}
|
|
163
|
+
/>
|
|
164
|
+
)}
|
|
165
|
+
<span className="chip ctx" style={ctxStyle(problem.context)}>
|
|
166
|
+
<span aria-hidden className="dot" />
|
|
167
|
+
{problem.context}
|
|
168
|
+
</span>
|
|
169
|
+
<span className="mono ml-auto text-muted">{KIND_NOTE[problem.kind]}</span>
|
|
170
|
+
{problem.note ? (
|
|
171
|
+
<p className="w-full border-l-2 pl-2 border-line-strong text-muted">
|
|
172
|
+
{problem.note}
|
|
173
|
+
</p>
|
|
174
|
+
) : null}
|
|
175
|
+
{problem.source ? (
|
|
176
|
+
<div className="mono w-full text-muted">
|
|
177
|
+
<Ident value={problem.source} />
|
|
178
|
+
</div>
|
|
179
|
+
) : null}
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
}
|
package/src/data.ts
CHANGED
|
@@ -35,11 +35,26 @@ import manifestJson from "../portolan.json";
|
|
|
35
35
|
|
|
36
36
|
const manifest = manifestJson as CatalogProfileManifest & { sources: string[] };
|
|
37
37
|
export const catalogProfiles: CatalogProfile[] = profilesFromManifest(manifest);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
/**
|
|
39
|
+
* The catalog the public landing page demonstrates. The landing has no
|
|
40
|
+
* `?catalog=` of its own - it is a page about the product, not about one
|
|
41
|
+
* estate - and the product is best shown on the example estate rather than on
|
|
42
|
+
* the three-service catalog Portolan keeps about itself. A manifest without
|
|
43
|
+
* an `example` profile falls back to its default, as any unknown name does.
|
|
44
|
+
*/
|
|
45
|
+
const LANDING_PROFILE = "example";
|
|
46
|
+
|
|
47
|
+
function requestedProfileFromUrl(): string | null {
|
|
48
|
+
if (typeof window === "undefined") return null;
|
|
49
|
+
const named = new URLSearchParams(window.location.search).get("catalog");
|
|
50
|
+
if (named) return named;
|
|
51
|
+
return /\/landing\/?$/.test(window.location.pathname) ? LANDING_PROFILE : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const activeCatalogProfile = catalogProfileNamed(
|
|
55
|
+
manifest,
|
|
56
|
+
requestedProfileFromUrl(),
|
|
57
|
+
);
|
|
43
58
|
|
|
44
59
|
/**
|
|
45
60
|
* Where sources are looked for. The patterns are written out because
|
package/src/er/ErCanvas.tsx
CHANGED
|
@@ -342,7 +342,7 @@ function Canvas({
|
|
|
342
342
|
wrapper={wrapper}
|
|
343
343
|
name={store.id}
|
|
344
344
|
/>
|
|
345
|
-
<div className="relative min-h-0 flex-1">
|
|
345
|
+
<div className="canvas-motion relative min-h-0 flex-1">
|
|
346
346
|
{layout.ready ? null : <DiagramSkeleton />}
|
|
347
347
|
<ReactFlow
|
|
348
348
|
nodes={nodes}
|
|
@@ -51,6 +51,12 @@ export interface DependencyGraphProps {
|
|
|
51
51
|
* one it replaced.
|
|
52
52
|
*/
|
|
53
53
|
fitKey: string;
|
|
54
|
+
/**
|
|
55
|
+
* Whether the wheel zooms the canvas. On the graph's own page it does; on a
|
|
56
|
+
* page the graph is embedded in, the wheel belongs to the page and the
|
|
57
|
+
* canvas is zoomed by pinch or by the fit control instead.
|
|
58
|
+
*/
|
|
59
|
+
zoomOnScroll?: boolean;
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
export function DependencyGraph({
|
|
@@ -58,6 +64,7 @@ export function DependencyGraph({
|
|
|
58
64
|
mode,
|
|
59
65
|
onMode,
|
|
60
66
|
fitKey,
|
|
67
|
+
zoomOnScroll = true,
|
|
61
68
|
}: DependencyGraphProps) {
|
|
62
69
|
const navigate = useNavigate();
|
|
63
70
|
const [layout, setLayout] = useState<Layout | null>(null);
|
|
@@ -217,7 +224,7 @@ export function DependencyGraph({
|
|
|
217
224
|
return (
|
|
218
225
|
/* The box is the box whether elk has answered or not: the skeleton is
|
|
219
226
|
laid over it, never in place of it, so nothing reflows on arrival. */
|
|
220
|
-
<div className="relative h-full w-full">
|
|
227
|
+
<div className="canvas-motion relative h-full w-full">
|
|
221
228
|
{ready ? null : <DiagramSkeleton />}
|
|
222
229
|
<TinyZoom.Provider value={tiny}>
|
|
223
230
|
<ReactFlow
|
|
@@ -238,6 +245,8 @@ export function DependencyGraph({
|
|
|
238
245
|
proOptions={{ hideAttribution: true }}
|
|
239
246
|
fitView
|
|
240
247
|
fitViewOptions={FIT_OPTIONS}
|
|
248
|
+
zoomOnScroll={zoomOnScroll}
|
|
249
|
+
preventScrolling={zoomOnScroll}
|
|
241
250
|
minZoom={0.08}
|
|
242
251
|
maxZoom={2}
|
|
243
252
|
key={ready ? `fit-${fitKey}-${nodes.length}-${edges.length}` : "pending"}
|
|
@@ -161,7 +161,7 @@ export function FocusedEventGraph({
|
|
|
161
161
|
return (
|
|
162
162
|
<div
|
|
163
163
|
style={{ height: canvasHeight }}
|
|
164
|
-
className="relative w-full overflow-hidden rounded-card border border-line shadow-xs"
|
|
164
|
+
className="canvas-motion relative w-full overflow-hidden rounded-card border border-line shadow-xs"
|
|
165
165
|
>
|
|
166
166
|
{ready ? null : <DiagramSkeleton />}
|
|
167
167
|
<ReactFlow
|
|
@@ -66,13 +66,11 @@ export function GraphToolbar({
|
|
|
66
66
|
type="button"
|
|
67
67
|
onClick={() => onFocusing(!focusing)}
|
|
68
68
|
aria-pressed={focusing}
|
|
69
|
-
className={focusing ? "is-on" :
|
|
69
|
+
className={`flex items-center gap-1.5 ${focusing ? "is-on" : ""}`}
|
|
70
70
|
title="Click a service to dim everything more than one hop away (Esc clears)"
|
|
71
71
|
>
|
|
72
72
|
<Crosshair size={13} aria-hidden />
|
|
73
|
-
<span
|
|
74
|
-
{focused && focusing ? focusedLabel(focused) : "focus"}
|
|
75
|
-
</span>
|
|
73
|
+
<span>{focused && focusing ? focusedLabel(focused) : "focus"}</span>
|
|
76
74
|
</button>
|
|
77
75
|
</div>
|
|
78
76
|
|