@shortlink-org/portolan 0.2.3 → 0.3.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 +23 -0
- package/catalog/enum_test.go +46 -0
- package/catalog/evidence_test.go +35 -0
- package/catalog/model.go +1066 -0
- package/catalog/roundtrip_test.go +203 -0
- package/catalog/via_test.go +38 -0
- package/cli/init.test.mjs +6 -1
- package/cli/portolan.mjs +8 -0
- package/cli/portolan.test.mjs +49 -0
- package/go.mod +14 -0
- package/go.sum +20 -0
- package/internal/gocall/README.md +19 -0
- package/internal/gocall/analyze.go +189 -0
- package/internal/gocall/analyze_test.go +107 -0
- package/internal/gohttp/analyze.go +2562 -0
- package/internal/gohttp/destination.go +373 -0
- package/internal/gohttp/endpoints.go +1067 -0
- package/internal/gohttp/roots.go +320 -0
- package/internal/gohttp/typed.go +96 -0
- package/internal/goscan/constants.go +85 -0
- package/internal/goscan/goscan_test.go +227 -0
- package/internal/goscan/index.go +629 -0
- package/internal/goscan/index_test.go +66 -0
- package/internal/goscan/names.go +52 -0
- package/internal/goscan/parse_test.go +11 -0
- package/internal/goscan/source.go +37 -0
- package/internal/goscan/tree.go +284 -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 +70 -0
- package/internal/wsdl/parse.go +949 -0
- package/internal/wsdl/parse_test.go +170 -0
- package/package.json +22 -10
- package/plugin/describe.go +118 -0
- package/plugin/describe_test.go +114 -0
- package/plugin/protocol.go +141 -0
- package/plugin/schematest/schematest.go +126 -0
- package/plugins/README.md +114 -46
- package/plugins/cmd/portolan-http-clients/main.go +19 -0
- package/plugins/extract-celery/extract.py +0 -2
- package/plugins/extract-celery/extract_test.py +1 -1
- package/plugins/extract-django/README.md +39 -17
- package/plugins/extract-django/domain.py +28 -17
- package/plugins/extract-django/extract.py +21 -7
- package/plugins/extract-django/extract_test.py +55 -2
- package/plugins/extract-django/lifecycle.py +2 -0
- package/plugins/extract-django/operations.py +1 -1
- package/plugins/extract-django/routing_test.py +109 -1
- package/plugins/extract-django/store.py +1 -1
- package/plugins/extract-django/transport.py +101 -55
- package/plugins/extract-django/verbs.py +241 -0
- package/plugins/extract-go/README.md +47 -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 +740 -0
- package/plugins/extract-http-clients/extract_test.go +1561 -0
- package/plugins/extract-http-clients/main.go +41 -0
- package/plugins/extract-java/build/org/portolan/extract/Extract.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-python-kafka/README.md +6 -0
- package/plugins/extract-python-kafka/extract.py +2 -4
- package/plugins/extract-python-kafka/extract_test.py +18 -2
- package/plugins/extract-ts/extract.test.ts +2 -2
- package/plugins/extract-ts/extract.ts +4 -5
- package/plugins/extract-ts/graphql.test.ts +1 -1
- package/plugins/openapi/ids.go +261 -0
- package/plugins/openapi/ids_test.go +98 -0
- package/plugins/portolan-go.wasm +0 -0
- package/plugins/pyplugin/catalog.py +12 -1
- package/plugins/pyplugin/kafka.py +74 -3
- package/plugins/pyplugin/protocol.py +1 -5
- package/portolan.json +3 -2
- package/schema/portolan.schema.json +34 -0
- package/scripts/README.md +18 -12
- package/scripts/catalog-sources.mjs +6 -0
- package/scripts/delivery-presets.mjs +21 -11
- package/scripts/diff.mjs +5 -1
- package/scripts/django-aggregates.test.mjs +58 -0
- package/scripts/gen-likec4.mjs +79 -21
- package/scripts/gen-likec4.test.mjs +25 -2
- package/scripts/gen.mjs +118 -115
- package/scripts/go-discovery.test.mjs +30 -0
- package/scripts/history.mjs +186 -3
- package/scripts/history.test.mjs +1 -1
- package/scripts/host-plugins/fetch-git.mjs +77 -21
- package/scripts/host-plugins/fetch-git.test.mjs +62 -8
- package/scripts/local-api.mjs +71 -4
- package/scripts/local-api.test.mjs +63 -4
- package/scripts/local-discovery.mjs +82 -9
- package/scripts/manifest.mjs +5 -3
- package/scripts/manifest.test.mjs +24 -0
- package/scripts/output-diff.mjs +94 -0
- package/scripts/output-diff.test.mjs +36 -0
- package/scripts/package-smoke.mjs +62 -4
- package/scripts/plugin-host.mjs +22 -2
- package/scripts/plugin-host.test.mjs +9 -0
- package/scripts/plugin-wasm-worker.mjs +4 -1
- package/scripts/provenance.mjs +72 -0
- package/scripts/provenance.test.mjs +149 -0
- package/scripts/run-builtin.mjs +39 -5
- package/scripts/schema.mjs +29 -0
- package/scripts/warning-policy.mjs +161 -0
- package/scripts/warning-policy.test.mjs +56 -0
- package/src/app/Breadcrumbs.test.ts +4 -0
- package/src/app/Breadcrumbs.tsx +1 -0
- package/src/app/Sidebar.tsx +3 -3
- package/src/catalog-docs.test.ts +64 -0
- package/src/catalog-docs.ts +35 -0
- package/src/catalog-error.test.ts +15 -0
- package/src/catalog-model.ts +70 -6
- package/src/catalog-stores.test.ts +17 -0
- package/src/catalog-validation.ts +52 -2
- package/src/catalog.test.ts +13 -2
- package/src/chat/Starter.tsx +5 -11
- package/src/chat/tools.test.ts +27 -0
- package/src/chat/tools.ts +5 -9
- package/src/components/CatalogStamp.tsx +10 -8
- package/src/components/ChannelRows.messagepack.test.tsx +28 -0
- package/src/components/ChannelRows.test.tsx +54 -0
- package/src/components/ChannelRows.tsx +57 -10
- package/src/components/HTTPDestinationEvidence.test.tsx +23 -0
- package/src/components/HTTPDestinationEvidence.tsx +31 -0
- package/src/components/Integrations.tsx +1 -1
- package/src/components/LifecycleDiagram.tsx +28 -12
- package/src/components/MachineDocs.tsx +6 -5
- package/src/components/MethodRows.tsx +9 -2
- package/src/components/ProblemRow.tsx +4 -0
- package/src/components/RelationEvidence.test.tsx +14 -0
- package/src/components/RelationEvidence.tsx +53 -0
- package/src/components/WhatLinksHere.tsx +6 -4
- package/src/data.ts +25 -7
- package/src/enrich.test.ts +336 -6
- package/src/enrich.ts +206 -3
- package/src/flow/StepDetail.tsx +104 -54
- package/src/flow/answers.test.ts +18 -1
- package/src/flow/answers.ts +37 -8
- package/src/flow/evidence.test.ts +16 -0
- package/src/flow/evidence.ts +34 -0
- package/src/index.css +44 -0
- package/src/landing/DraggableReveal.tsx +3 -2
- package/src/landing/EvidencePipeline.tsx +105 -0
- package/src/landing/LandingPage.tsx +2 -59
- package/src/lib/backlinks.test.ts +16 -1
- package/src/lib/backlinks.ts +20 -0
- package/src/lib/catalog-diff.test.ts +18 -0
- package/src/lib/catalog-diff.ts +20 -2
- package/src/lib/derive.ts +1 -0
- package/src/lib/django-aggregates.d.mts +9 -0
- package/src/lib/django-aggregates.mjs +36 -0
- package/src/lib/django-aggregates.test.ts +29 -0
- package/src/lib/django-aggregates.ts +5 -0
- package/src/lib/kafka-ui.test.ts +87 -0
- package/src/lib/kafka-ui.ts +105 -0
- package/src/lib/local-api.ts +20 -2
- package/src/lib/setup-info.test.ts +17 -0
- package/src/lib/setup-info.ts +58 -0
- package/src/lib/warnings.test.ts +54 -0
- package/src/lib/warnings.ts +260 -0
- package/src/lib/wire-problems.test.ts +21 -0
- package/src/lib/wire-problems.ts +62 -1
- package/src/likec4/FlowView.tsx +2 -6
- package/src/likec4/flow-edges.test.ts +64 -1
- package/src/likec4/flow-edges.ts +43 -7
- package/src/likec4/view-index.ts +8 -2
- package/src/map/ContextMapGraph.tsx +76 -32
- package/src/merge.test.ts +23 -0
- package/src/merge.ts +33 -10
- package/src/pages/AggregatePage.tsx +8 -7
- package/src/pages/CatalogFailure.tsx +2 -2
- package/src/pages/ContextPage.tsx +6 -5
- package/src/pages/ServicePage.tsx +4 -3
- package/src/pages/Settings.tsx +200 -44
- package/src/pages/settings/DjangoAggregateChoices.tsx +79 -0
- package/src/pages/settings/IntegrationsSettings.tsx +117 -0
- package/src/routes.test.ts +2 -0
- package/src/routes.ts +2 -1
- package/src/selection/DetailPanel.tsx +61 -1
- package/src/virtual-provenance.d.ts +11 -0
- package/vite.config.ts +5 -0
- package/scripts/vendor-lock.mjs +0 -58
- package/scripts/vendor-lock.test.mjs +0 -69
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
2
|
+
import { expect, it } from "vitest";
|
|
3
|
+
import { HTTPDestinationEvidence } from "./HTTPDestinationEvidence";
|
|
4
|
+
|
|
5
|
+
it("shows the join and the config default behind a resolved destination", () => {
|
|
6
|
+
const html = renderToStaticMarkup(<HTTPDestinationEvidence destination={{
|
|
7
|
+
callSite: "client.go:10", endpointExpression: "c.baseURL + path", method: "POST",
|
|
8
|
+
localPath: "/get-admin-settings", fullPath: "/settings/get-admin-settings",
|
|
9
|
+
baseURL: { expression: "cfg.SettingAddr", configField: "Config.SettingAddr", environmentVariable: "SETTINGS_ADDR", kind: "config-default", value: "http://localhost:8000/settings", source: "config.go:84", optionSource: "modules_tree.go:212" },
|
|
10
|
+
join: { expression: "c.baseURL + path", source: "client.go:10" },
|
|
11
|
+
resolution: { basis: "full-path", provider: "avia.aviaadmin", route: "/settings/get-admin-settings" },
|
|
12
|
+
}} />);
|
|
13
|
+
for (const value of ["Recovered full path", "Config default (runtime may override)", "SETTINGS_ADDR", "modules_tree.go:212", "Config.SettingAddr", "/settings/get-admin-settings", "avia.aviaadmin"]) expect(html).toContain(value);
|
|
14
|
+
expect(html).not.toContain("heuristic");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("labels suffix resolution as a heuristic", () => {
|
|
18
|
+
const html = renderToStaticMarkup(<HTTPDestinationEvidence destination={{
|
|
19
|
+
callSite: "client.go:10", endpointExpression: "/settings", method: "POST",
|
|
20
|
+
resolution: { basis: "unique-suffix", provider: "admin", route: "/admin/settings" },
|
|
21
|
+
}} />);
|
|
22
|
+
expect(html).toContain("Unique route suffix (heuristic)");
|
|
23
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { HTTPDestination } from "../catalog";
|
|
2
|
+
|
|
3
|
+
/** Display the facts for this invocation, including the limits of defaults. */
|
|
4
|
+
export function HTTPDestinationEvidence({ destination: d }: { destination: HTTPDestination }) {
|
|
5
|
+
const basis = d.resolution?.basis;
|
|
6
|
+
const rows = [
|
|
7
|
+
["Linked by", basis === "full-path" ? "Recovered full path" : basis === "exact-route" ? "Exact route" : basis === "unique-suffix" ? "Unique route suffix (heuristic)" : "Unresolved"],
|
|
8
|
+
["Call site", d.callSite],
|
|
9
|
+
["Endpoint expression", d.endpointExpression],
|
|
10
|
+
["Local path", d.localPath],
|
|
11
|
+
["Base URL expression", d.baseURL?.expression],
|
|
12
|
+
["Config field", d.baseURL?.configField],
|
|
13
|
+
["Environment variable", d.baseURL?.environmentVariable],
|
|
14
|
+
[d.baseURL?.kind === "config-default" ? "Config default (runtime may override)" : "Base URL", d.baseURL?.value],
|
|
15
|
+
["Config source", d.baseURL?.source],
|
|
16
|
+
["Option call site", d.baseURL?.optionSource],
|
|
17
|
+
["Service discovery alias", d.serviceDiscoveryAlias],
|
|
18
|
+
["Join expression", d.join?.expression],
|
|
19
|
+
["Join source", d.join?.source],
|
|
20
|
+
["Full path", d.fullPath],
|
|
21
|
+
...(d.transforms ?? []).map((transform) => ["Runtime URL modifier (not evaluated)", `${transform.expression} · ${transform.source}`]),
|
|
22
|
+
["Provider", d.resolution?.provider],
|
|
23
|
+
["Provider route", d.resolution?.route],
|
|
24
|
+
].filter(([, value]) => value);
|
|
25
|
+
return <section aria-label="HTTP destination evidence" className="border-t border-line px-3 py-3">
|
|
26
|
+
<h3 className="label mb-2">HTTP destination evidence</h3>
|
|
27
|
+
<dl className="grid min-w-0 grid-cols-1 gap-y-3">
|
|
28
|
+
{rows.map(([label, value], index) => <div key={`${label}-${index}`} className="min-w-0"><dt className="text-muted">{label}</dt><dd className="mono break-all text-ink">{value}</dd></div>)}
|
|
29
|
+
</dl>
|
|
30
|
+
</section>;
|
|
31
|
+
}
|
|
@@ -210,7 +210,7 @@ export function Integrations({ groups }: { groups: IntegrationGroup[] }) {
|
|
|
210
210
|
<RowActions copy={call.id} label={call.id} />
|
|
211
211
|
{method?.http ? (
|
|
212
212
|
<p className="mono w-full text-muted">
|
|
213
|
-
{method.http.method} {method.http.path}
|
|
213
|
+
{method.http.method ? `${method.http.method} ${method.http.path}` : method.http.path}
|
|
214
214
|
</p>
|
|
215
215
|
) : null}
|
|
216
216
|
{call.note ? (
|
|
@@ -3,6 +3,12 @@ import type { Aggregate } from "../catalog";
|
|
|
3
3
|
import { layoutLifecycle, METRICS } from "../lib/lifecycle";
|
|
4
4
|
import { KindIcon } from "./kind";
|
|
5
5
|
|
|
6
|
+
function transitionTone(on: string) {
|
|
7
|
+
if (on === "Fail") return { color: "var(--status-unresolved)", marker: "lc-arrow-fail" };
|
|
8
|
+
if (on === "Succeed") return { color: "var(--status-verified)", marker: "lc-arrow-succeed" };
|
|
9
|
+
return { color: "var(--fg-muted)", marker: "lc-arrow" };
|
|
10
|
+
}
|
|
11
|
+
|
|
6
12
|
/**
|
|
7
13
|
* The aggregate's state machine, as the code wrote it down. Boxes and arrows
|
|
8
14
|
* are SVG; the labels are HTML laid over it, because a label holds a link to
|
|
@@ -29,18 +35,27 @@ export function LifecycleDiagram({
|
|
|
29
35
|
<marker id="lc-arrow" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
|
|
30
36
|
<path d="M 0 0 L 8 4 L 0 8 z" fill="var(--fg-muted)" />
|
|
31
37
|
</marker>
|
|
38
|
+
<marker id="lc-arrow-fail" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
|
|
39
|
+
<path d="M 0 0 L 8 4 L 0 8 z" fill="var(--status-unresolved)" />
|
|
40
|
+
</marker>
|
|
41
|
+
<marker id="lc-arrow-succeed" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
|
|
42
|
+
<path d="M 0 0 L 8 4 L 0 8 z" fill="var(--status-verified)" />
|
|
43
|
+
</marker>
|
|
32
44
|
</defs>
|
|
33
|
-
{edges.map((e) =>
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
{edges.map((e) => {
|
|
46
|
+
const tone = transitionTone(e.on);
|
|
47
|
+
return (
|
|
48
|
+
<path
|
|
49
|
+
key={`${e.from}-${e.to}-${e.on}`}
|
|
50
|
+
d={e.path}
|
|
51
|
+
fill="none"
|
|
52
|
+
stroke={tone.color}
|
|
53
|
+
strokeWidth={1.25}
|
|
54
|
+
strokeDasharray={e.back ? "4 3" : undefined}
|
|
55
|
+
markerEnd={`url(#${tone.marker})`}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
})}
|
|
44
59
|
{boxes.map((b) => (
|
|
45
60
|
<g key={b.state}>
|
|
46
61
|
<rect
|
|
@@ -80,11 +95,12 @@ export function LifecycleDiagram({
|
|
|
80
95
|
))}
|
|
81
96
|
{edges.map((e) => {
|
|
82
97
|
const to = e.emits ? eventPath(e.emits) : null;
|
|
98
|
+
const tone = transitionTone(e.on);
|
|
83
99
|
return (
|
|
84
100
|
<div
|
|
85
101
|
key={`${e.from}-${e.to}-${e.on}-label`}
|
|
86
102
|
className="mono absolute flex -translate-x-1/2 -translate-y-1/2 items-center gap-1 whitespace-nowrap rounded-control bg-bg px-1 text-muted"
|
|
87
|
-
style={{ left: e.labelX, top: e.labelY }}
|
|
103
|
+
style={{ left: e.labelX, top: e.labelY, color: tone.color }}
|
|
88
104
|
title={e.source ? `made at ${e.source}` : undefined}
|
|
89
105
|
>
|
|
90
106
|
{e.on}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { FileText } from "lucide-react";
|
|
2
|
-
import { activeCatalogProfile } from "../data";
|
|
2
|
+
import { activeCatalogDocs, activeCatalogProfile } from "../data";
|
|
3
3
|
|
|
4
4
|
// The site as a language model reads it. The build (scripts/site-docs.mjs)
|
|
5
5
|
// places the generated markdown under docs/ and llms.txt at the root, in the
|
|
@@ -10,11 +10,12 @@ import { activeCatalogProfile } from "../data";
|
|
|
10
10
|
const base = import.meta.env.BASE_URL;
|
|
11
11
|
|
|
12
12
|
const suffix = activeCatalogProfile.id === "portolan" ? "" : `${activeCatalogProfile.id}/`;
|
|
13
|
-
const docs = `${base}docs/${suffix}`;
|
|
14
13
|
const FILES = [
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
...(activeCatalogDocs ? [
|
|
15
|
+
{ href: activeCatalogDocs.index, label: "llms.txt", title: "An index of every page, for a model that fetches on demand" },
|
|
16
|
+
{ href: activeCatalogDocs.full, label: "llms-full.txt", title: "Every page in one file, for a model with a context window" },
|
|
17
|
+
{ href: activeCatalogDocs.pages, label: "docs/", title: "The generated markdown, page by page" },
|
|
18
|
+
] : []),
|
|
18
19
|
{ href: `${base}exports/mermaid/${suffix}`, label: "Mermaid", title: "Standalone sequence diagrams for every architecture flow" },
|
|
19
20
|
...(activeCatalogProfile.id === "example"
|
|
20
21
|
? [{ href: `${base}exports/backstage/catalog-info.yaml`, label: "Backstage", title: "Components, systems, APIs and resources for a Backstage catalog" }]
|
|
@@ -167,8 +167,15 @@ export function MethodRows({
|
|
|
167
167
|
className={method.deprecated ? "line-through" : undefined}
|
|
168
168
|
/>
|
|
169
169
|
{method.http ? (
|
|
170
|
-
<span
|
|
171
|
-
|
|
170
|
+
<span
|
|
171
|
+
className="chip mono"
|
|
172
|
+
title={
|
|
173
|
+
method.http.method
|
|
174
|
+
? "the route this operation answers on"
|
|
175
|
+
: "the route this operation answers on; the HTTP verb is not declared in source"
|
|
176
|
+
}
|
|
177
|
+
>
|
|
178
|
+
{method.http.method ? `${method.http.method} ${method.http.path}` : method.http.path}
|
|
172
179
|
</span>
|
|
173
180
|
) : null}
|
|
174
181
|
{method.soap ? (
|
|
@@ -31,6 +31,7 @@ const KIND_OF: Record<Problem["kind"], "service" | "event" | "table"> = {
|
|
|
31
31
|
"shared-channel": "event",
|
|
32
32
|
"channel-undeclared": "event",
|
|
33
33
|
"channel-unpublished": "service",
|
|
34
|
+
"message-encoding": "service",
|
|
34
35
|
"subscription-unresolved": "service",
|
|
35
36
|
};
|
|
36
37
|
|
|
@@ -50,6 +51,7 @@ const KIND_NOTE: Record<Problem["kind"], string> = {
|
|
|
50
51
|
"channel-undeclared":
|
|
51
52
|
"this event goes out on a channel the service does not declare",
|
|
52
53
|
"channel-unpublished": "a declared channel no event of this service names",
|
|
54
|
+
"message-encoding": "publisher and subscriber use different payload encodings",
|
|
53
55
|
"subscription-unresolved":
|
|
54
56
|
"nothing in the catalog publishes what this service listens for",
|
|
55
57
|
};
|
|
@@ -72,6 +74,7 @@ function nearPath(problem: Problem): string | null {
|
|
|
72
74
|
case "shared-channel":
|
|
73
75
|
return eventPath(problem.id) ?? servicePath(problem.service);
|
|
74
76
|
case "channel-unpublished":
|
|
77
|
+
case "message-encoding":
|
|
75
78
|
case "subscription-unresolved":
|
|
76
79
|
return servicePath(problem.service);
|
|
77
80
|
case "shared-store":
|
|
@@ -110,6 +113,7 @@ function peerPath(problem: Problem): string | null {
|
|
|
110
113
|
);
|
|
111
114
|
case "shared-store":
|
|
112
115
|
case "shared-channel":
|
|
116
|
+
case "message-encoding":
|
|
113
117
|
return servicePath(problem.peer);
|
|
114
118
|
case "outbox-payload":
|
|
115
119
|
return storePath(problem.peer);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
2
|
+
import { expect, it } from "vitest";
|
|
3
|
+
import { RelationEvidencePanel } from "./RelationEvidence";
|
|
4
|
+
|
|
5
|
+
it("explains an ambiguous binding without choosing a candidate", () => {
|
|
6
|
+
const html = renderToStaticMarkup(<RelationEvidencePanel items={[
|
|
7
|
+
{ kind: "binding", rule: "provider-signature", source: "di.go:12", symbol: "Build" },
|
|
8
|
+
{ kind: "unresolved", rule: "ambiguous-binding", candidates: ["First", "Second"] },
|
|
9
|
+
]} />);
|
|
10
|
+
for (const value of ["Why this relation exists", "inferred", "di.go:12", "Several implementations remain possible", "First", "Second"]) expect(html).toContain(value);
|
|
11
|
+
});
|
|
12
|
+
it("shows the absence of evidence explicitly", () => {
|
|
13
|
+
expect(renderToStaticMarkup(<RelationEvidencePanel items={[]} />)).toContain("No source evidence recorded");
|
|
14
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { RelationEvidence } from "../catalog";
|
|
3
|
+
|
|
4
|
+
const labels: Record<RelationEvidence["kind"], string> = {
|
|
5
|
+
"call-site": "Call site", function: "Function", binding: "Dependency binding",
|
|
6
|
+
contract: "Contract", resolution: "Catalog resolution", unresolved: "Unresolved",
|
|
7
|
+
};
|
|
8
|
+
const rules: Record<string, string> = {
|
|
9
|
+
"source-expression": "Expression in source",
|
|
10
|
+
"source-function": "Enclosing source function",
|
|
11
|
+
"provider-signature": "Provider signature (inferred)",
|
|
12
|
+
"store-port-convention": "Storage field, type or package name (inferred)",
|
|
13
|
+
"domain-port-convention": "Domain port mapped to the configured store (inferred)",
|
|
14
|
+
"ambiguous-binding": "Several implementations remain possible",
|
|
15
|
+
"generated-client-method": "Method declared by generated client",
|
|
16
|
+
"http-expression": "Outbound HTTP expression",
|
|
17
|
+
"analyzed-call-path": "Static analysis path; not a runtime trace",
|
|
18
|
+
"client-contract": "Client contract",
|
|
19
|
+
"catalog-contract": "Matching interface and method",
|
|
20
|
+
"event-declaration": "Declared event",
|
|
21
|
+
"repository-sql-call": "SQL operation inside repository",
|
|
22
|
+
"full-path": "Recovered full HTTP path",
|
|
23
|
+
"exact-route": "Exact HTTP route",
|
|
24
|
+
"unique-suffix": "Unique route suffix (heuristic)",
|
|
25
|
+
"continuation-entrypoint": "Matched continuation entrypoint",
|
|
26
|
+
"response-to-request": "Response associated with request",
|
|
27
|
+
"unresolved-target": "No unique target established",
|
|
28
|
+
"sql-alter-table": "ALTER TABLE in migration",
|
|
29
|
+
"sql-view-definition": "SQL view definition",
|
|
30
|
+
"sql-create-table": "CREATE TABLE in migration",
|
|
31
|
+
"domain-root-and-repository-layout": "Domain root and repository convention (inferred)",
|
|
32
|
+
"migration-aggregate-annotation": "Explicit aggregate annotation in migration",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** One explanation surface for all flow kinds and schema relationships. */
|
|
36
|
+
export function RelationEvidencePanel({ items, renderSource }: {
|
|
37
|
+
items: readonly RelationEvidence[];
|
|
38
|
+
renderSource?: (source: string) => ReactNode;
|
|
39
|
+
}) {
|
|
40
|
+
return <section aria-label="Why this relation exists" className="my-3 rounded-card border border-line bg-surface p-3">
|
|
41
|
+
<h3 className="label mb-2">Why this relation exists</h3>
|
|
42
|
+
{items.length === 0 ? <p className="text-muted">No source evidence recorded in this fragment.</p> :
|
|
43
|
+
<ol className="space-y-3">
|
|
44
|
+
{items.map((item, i) => <li key={`${item.kind}-${i}`} className="min-w-0 border-l-2 border-line pl-3">
|
|
45
|
+
<div className={item.kind === "unresolved" ? "text-[var(--status-unresolved)]" : "text-ink"}>{labels[item.kind]}</div>
|
|
46
|
+
<div className="text-muted">{rules[item.rule] ?? item.rule}</div>
|
|
47
|
+
{item.symbol ? <div className="mono break-all">{item.symbol}</div> : null}
|
|
48
|
+
{item.source ? <div className="mono break-all text-muted">{renderSource ? renderSource(item.source) : item.source}</div> : null}
|
|
49
|
+
{item.candidates?.length ? <div className="mt-1"><span className="text-muted">Candidates</span><ul>{item.candidates.map((candidate) => <li className="mono break-all" key={candidate}>{candidate}</li>)}</ul></div> : null}
|
|
50
|
+
</li>)}
|
|
51
|
+
</ol>}
|
|
52
|
+
</section>;
|
|
53
|
+
}
|
|
@@ -14,7 +14,11 @@ import { useMemo } from "react";
|
|
|
14
14
|
import type { ReactNode } from "react";
|
|
15
15
|
import { Link } from "react-router";
|
|
16
16
|
import { catalog, index } from "../data";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
backlinkCount,
|
|
19
|
+
backlinksFor,
|
|
20
|
+
distinctBacklinks,
|
|
21
|
+
} from "../lib/backlinks";
|
|
18
22
|
import type { Backlink, BacklinkGroup, BacklinkTarget } from "../lib/backlinks";
|
|
19
23
|
import { plural } from "../lib/format";
|
|
20
24
|
import { KIND_LABEL, KIND_PLURAL } from "../lib/kinds";
|
|
@@ -163,9 +167,7 @@ export function WhatLinksHere({
|
|
|
163
167
|
className={`mono flex flex-wrap items-center gap-1.5 text-muted ${className || "mt-2"}`}
|
|
164
168
|
>
|
|
165
169
|
linked from
|
|
166
|
-
{groups
|
|
167
|
-
.flatMap((g) => g.links)
|
|
168
|
-
.map((link) => {
|
|
170
|
+
{distinctBacklinks(groups).map((link) => {
|
|
169
171
|
const to = backlinkPath(link);
|
|
170
172
|
const body = (
|
|
171
173
|
<>
|
package/src/data.ts
CHANGED
|
@@ -32,6 +32,11 @@ import type {
|
|
|
32
32
|
SourceStamp,
|
|
33
33
|
} from "./merge";
|
|
34
34
|
import manifestJson from "../portolan.json";
|
|
35
|
+
import { catalogDocs } from "./catalog-docs";
|
|
36
|
+
// When each source last changed, as the checkout's history says - served by
|
|
37
|
+
// scripts/provenance.mjs at build time, because a fragment carries no
|
|
38
|
+
// provenance of its own (portolan.0010) and a browser cannot ask git.
|
|
39
|
+
import provenance from "virtual:portolan-provenance";
|
|
35
40
|
|
|
36
41
|
const manifest = manifestJson as CatalogProfileManifest & { sources: string[] };
|
|
37
42
|
export const catalogProfiles: CatalogProfile[] = profilesFromManifest(manifest);
|
|
@@ -56,6 +61,8 @@ export const activeCatalogProfile = catalogProfileNamed(
|
|
|
56
61
|
requestedProfileFromUrl(),
|
|
57
62
|
);
|
|
58
63
|
|
|
64
|
+
export const activeCatalogDocs = catalogDocs(manifestJson, activeCatalogProfile.id, import.meta.env.BASE_URL);
|
|
65
|
+
|
|
59
66
|
/**
|
|
60
67
|
* Where sources are looked for. The patterns are written out because
|
|
61
68
|
* import.meta.glob resolves at build time and needs literals - and because
|
|
@@ -120,15 +127,26 @@ interface Loaded {
|
|
|
120
127
|
|
|
121
128
|
function load(): Loaded {
|
|
122
129
|
const sources: CatalogSource[] = Object.entries(modules)
|
|
123
|
-
.map(([
|
|
130
|
+
.map(([key, catalog]) => {
|
|
124
131
|
// Vite keys a glob by its pattern-relative path; the leading ../ is an
|
|
125
132
|
// artefact of this file's location, not part of where anything lives.
|
|
126
|
-
|
|
127
|
-
// A SOURCE, not a catalog:
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
133
|
+
const imported = key.replace(/^\.\.\//, "");
|
|
134
|
+
// A SOURCE, not a catalog: a file carries no stamp of its own, and the
|
|
135
|
+
// history's travels beside it (portolan.0010).
|
|
136
|
+
const { source, ...stamp } = provenance[imported] ?? {};
|
|
137
|
+
return {
|
|
138
|
+
imported,
|
|
139
|
+
// A staged site imports a source under a flattened name; the file is
|
|
140
|
+
// still the one in the workspace, and that is the path a project's
|
|
141
|
+
// root is a prefix of and a link to the file names.
|
|
142
|
+
path: source ?? imported,
|
|
143
|
+
catalog: catalog as SourceCatalog,
|
|
144
|
+
stamp: "commit" in stamp ? stamp : undefined,
|
|
145
|
+
};
|
|
146
|
+
})
|
|
147
|
+
// The profile spells its sources the way the site imports them.
|
|
148
|
+
.filter((source) => profileIncludesSource(activeCatalogProfile, source.imported))
|
|
149
|
+
.map(({ imported: _imported, ...source }) => source);
|
|
132
150
|
|
|
133
151
|
const merged = mergeCatalogs(sources);
|
|
134
152
|
// Enriched before it is validated: the edges the flows imply are part of
|