@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
package/src/flow/StepDetail.tsx
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
|
+
import { RelationEvidencePanel } from "../components/RelationEvidence";
|
|
2
|
+
import { stepRelationEvidence } from "./evidence";
|
|
3
|
+
import { HTTPDestinationEvidence } from "../components/HTTPDestinationEvidence";
|
|
1
4
|
import { useMemo } from "react";
|
|
2
5
|
import { Link } from "react-router";
|
|
3
|
-
import { AlertTriangle } from "lucide-react";
|
|
6
|
+
import { AlertTriangle, FileCode2 } from "lucide-react";
|
|
4
7
|
import { allRepos, stepFrames } from "../catalog";
|
|
5
8
|
import type { Flow, Step, StepFrame } from "../catalog";
|
|
6
9
|
import { catalog, index } from "../data";
|
|
7
10
|
import { Ident } from "../components/Ident";
|
|
8
|
-
import {
|
|
9
|
-
import { SourcePreviewButton } from "../components/SourcePreview";
|
|
11
|
+
import { SourcePreviewLink } from "../components/SourcePreview";
|
|
10
12
|
import { flowRepoService } from "../lib/derive";
|
|
11
13
|
import { sourceLocation } from "../lib/source-link";
|
|
12
14
|
import { AdrNumber, StatusChip } from "../components/primitives";
|
|
13
15
|
import { ShapeRows } from "../components/ShapeRows";
|
|
14
16
|
import { shapeFor } from "../components/MethodRows";
|
|
15
|
-
import { stepAnswer } from "./answers";
|
|
17
|
+
import { stepAnswer, stepRpcContract } from "./answers";
|
|
16
18
|
import {
|
|
17
19
|
aggregatePath,
|
|
18
20
|
paths,
|
|
@@ -154,12 +156,28 @@ function EventDetail({ step, flow }: { step: Step; flow: Flow }) {
|
|
|
154
156
|
}
|
|
155
157
|
|
|
156
158
|
function RpcDetail({ step, flow }: { step: Step; flow: Flow }) {
|
|
157
|
-
const
|
|
159
|
+
const contract = stepRpcContract(index, step);
|
|
160
|
+
const method = contract?.id ?? step.ref ?? step.label ?? "(unknown method)";
|
|
158
161
|
const call = step.ref ? index.rpcById.get(step.ref) : undefined;
|
|
159
|
-
const provider =
|
|
160
|
-
|
|
162
|
+
const provider = contract?.provider;
|
|
163
|
+
const internalProvider = provider
|
|
164
|
+
? index.serviceById.get(provider.id)
|
|
161
165
|
: undefined;
|
|
162
|
-
const providerPath =
|
|
166
|
+
const providerPath = internalProvider ? servicePath(internalProvider.id) : null;
|
|
167
|
+
const requestFields = contract
|
|
168
|
+
? shapeFor(
|
|
169
|
+
contract.provided,
|
|
170
|
+
contract.method.request,
|
|
171
|
+
contract.method.requestRef,
|
|
172
|
+
)
|
|
173
|
+
: null;
|
|
174
|
+
const responseFields = contract
|
|
175
|
+
? shapeFor(
|
|
176
|
+
contract.provided,
|
|
177
|
+
contract.method.response,
|
|
178
|
+
contract.method.responseRef,
|
|
179
|
+
)
|
|
180
|
+
: null;
|
|
163
181
|
// The flow records the hop; what comes back is the contract's to say.
|
|
164
182
|
const answer = stepAnswer(index, step);
|
|
165
183
|
|
|
@@ -175,6 +193,8 @@ function RpcDetail({ step, flow }: { step: Step; flow: Flow }) {
|
|
|
175
193
|
</div>
|
|
176
194
|
</header>
|
|
177
195
|
|
|
196
|
+
{step.destination ? <HTTPDestinationEvidence destination={step.destination} /> : null}
|
|
197
|
+
|
|
178
198
|
<DetailSection title="Contract">
|
|
179
199
|
<dl className="grid grid-cols-[minmax(7rem,auto)_minmax(0,1fr)] gap-x-4 gap-y-2">
|
|
180
200
|
{answer ? (
|
|
@@ -185,10 +205,12 @@ function RpcDetail({ step, flow }: { step: Step; flow: Flow }) {
|
|
|
185
205
|
) : null}
|
|
186
206
|
<dt className="mono text-muted">Provider</dt>
|
|
187
207
|
<dd>
|
|
188
|
-
{
|
|
208
|
+
{internalProvider && providerPath ? (
|
|
189
209
|
<Link to={providerPath} className="mono text-accent">
|
|
190
|
-
{
|
|
210
|
+
{internalProvider.id} →
|
|
191
211
|
</Link>
|
|
212
|
+
) : provider ? (
|
|
213
|
+
<span className="mono text-ink">{provider.id}</span>
|
|
192
214
|
) : (
|
|
193
215
|
<span className="mono inline-flex items-center gap-1.5 rounded-control border px-1.5 py-0.5 status-unresolved">
|
|
194
216
|
<AlertTriangle size={11} aria-hidden />
|
|
@@ -205,6 +227,34 @@ function RpcDetail({ step, flow }: { step: Step; flow: Flow }) {
|
|
|
205
227
|
</dl>
|
|
206
228
|
</DetailSection>
|
|
207
229
|
|
|
230
|
+
{contract?.method.request ? (
|
|
231
|
+
<DetailSection title="Request" meta={contract.method.request}>
|
|
232
|
+
{requestFields ? (
|
|
233
|
+
<ShapeRows
|
|
234
|
+
fields={requestFields}
|
|
235
|
+
enums={contract.provided.enums}
|
|
236
|
+
showHeader
|
|
237
|
+
/>
|
|
238
|
+
) : (
|
|
239
|
+
<div className="mono text-muted">shape not recorded</div>
|
|
240
|
+
)}
|
|
241
|
+
</DetailSection>
|
|
242
|
+
) : null}
|
|
243
|
+
|
|
244
|
+
{contract?.method.response ? (
|
|
245
|
+
<DetailSection title="Response" meta={contract.method.response}>
|
|
246
|
+
{responseFields ? (
|
|
247
|
+
<ShapeRows
|
|
248
|
+
fields={responseFields}
|
|
249
|
+
enums={contract.provided.enums}
|
|
250
|
+
showHeader
|
|
251
|
+
/>
|
|
252
|
+
) : (
|
|
253
|
+
<div className="mono text-muted">shape not recorded</div>
|
|
254
|
+
)}
|
|
255
|
+
</DetailSection>
|
|
256
|
+
) : null}
|
|
257
|
+
|
|
208
258
|
<DetailSection title="Source">
|
|
209
259
|
{call ? (
|
|
210
260
|
<SourceWhere where={call.source} flow={flow} structured />
|
|
@@ -525,48 +575,25 @@ function SourceWhere({
|
|
|
525
575
|
allRepos(catalog),
|
|
526
576
|
);
|
|
527
577
|
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
title="Open on the forge, at the built commit"
|
|
541
|
-
>
|
|
542
|
-
forge ↗
|
|
543
|
-
</a>
|
|
544
|
-
) : null}
|
|
545
|
-
<EditorLink
|
|
546
|
-
location={location}
|
|
547
|
-
variant="text"
|
|
548
|
-
className="inline-flex items-center border px-2 py-1 border-line bg-canvas hover:bg-raised hover:no-underline"
|
|
549
|
-
/>
|
|
550
|
-
</>
|
|
578
|
+
const source = (
|
|
579
|
+
<SourcePreviewLink
|
|
580
|
+
location={location}
|
|
581
|
+
className={
|
|
582
|
+
structured
|
|
583
|
+
? "w-full min-w-0 border px-2.5 py-2 border-line bg-canvas text-ink hover:border-line-strong hover:bg-raised hover:no-underline"
|
|
584
|
+
: "max-w-full min-w-0 text-muted hover:text-accent"
|
|
585
|
+
}
|
|
586
|
+
>
|
|
587
|
+
<FileCode2 size={14} aria-hidden className="shrink-0" />
|
|
588
|
+
<span className="min-w-0 break-all text-left">{where}</span>
|
|
589
|
+
</SourcePreviewLink>
|
|
551
590
|
);
|
|
552
591
|
|
|
553
592
|
if (structured) {
|
|
554
|
-
return
|
|
555
|
-
<div className="flex min-w-0 flex-col gap-2">
|
|
556
|
-
<div className="mono min-w-0 break-all text-muted">
|
|
557
|
-
<Ident block value={where} className="text-muted" />
|
|
558
|
-
</div>
|
|
559
|
-
<div className="flex flex-wrap items-center gap-1.5">{actions}</div>
|
|
560
|
-
</div>
|
|
561
|
-
);
|
|
593
|
+
return <div className="min-w-0">{source}</div>;
|
|
562
594
|
}
|
|
563
595
|
|
|
564
|
-
return
|
|
565
|
-
<div className="mono flex flex-wrap items-center gap-2 break-all text-muted">
|
|
566
|
-
<Ident block value={where} className="text-muted" />
|
|
567
|
-
{actions}
|
|
568
|
-
</div>
|
|
569
|
-
);
|
|
596
|
+
return <div className="min-w-0">{source}</div>;
|
|
570
597
|
}
|
|
571
598
|
|
|
572
599
|
function Where({ step, flow }: { step: Step; flow: Flow }) {
|
|
@@ -574,6 +601,34 @@ function Where({ step, flow }: { step: Step; flow: Flow }) {
|
|
|
574
601
|
return <SourceWhere where={step.line} flow={flow} />;
|
|
575
602
|
}
|
|
576
603
|
|
|
604
|
+
function CallDetail({ step, flow }: { step: Step; flow: Flow }) {
|
|
605
|
+
return (
|
|
606
|
+
<section
|
|
607
|
+
aria-label="Call detail"
|
|
608
|
+
className="overflow-hidden rounded-card border shadow-xs border-line"
|
|
609
|
+
>
|
|
610
|
+
<header className="border-b px-3 py-2.5 border-line bg-surface">
|
|
611
|
+
<h2 className="label">Call</h2>
|
|
612
|
+
<div className="mt-2">
|
|
613
|
+
<Ident
|
|
614
|
+
block
|
|
615
|
+
value={step.label ?? "internal call"}
|
|
616
|
+
className="text-ink"
|
|
617
|
+
/>
|
|
618
|
+
</div>
|
|
619
|
+
</header>
|
|
620
|
+
|
|
621
|
+
<DetailSection title="Source">
|
|
622
|
+
{step.line ? (
|
|
623
|
+
<SourceWhere where={step.line} flow={flow} structured />
|
|
624
|
+
) : (
|
|
625
|
+
<div className="mono text-muted">not recorded</div>
|
|
626
|
+
)}
|
|
627
|
+
</DetailSection>
|
|
628
|
+
</section>
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
|
|
577
632
|
function StoreCallDetail({ step, flow }: { step: Step; flow: Flow }) {
|
|
578
633
|
const access = step.storeAccess!;
|
|
579
634
|
const store = index.storeById.get(access.store);
|
|
@@ -668,6 +723,7 @@ export function StepDetailBody({ step, flow }: { step: Step; flow: Flow }) {
|
|
|
668
723
|
return (
|
|
669
724
|
<>
|
|
670
725
|
<ExecutionContext step={step} flow={flow} />
|
|
726
|
+
<RelationEvidencePanel items={stepRelationEvidence(index, step)} renderSource={(where) => <SourceWhere where={where} flow={flow} structured />} />
|
|
671
727
|
|
|
672
728
|
{/* A decision that names this step's event is the reason the step
|
|
673
729
|
looks the way it does. It belongs next to the step, not three
|
|
@@ -699,13 +755,7 @@ export function StepDetailBody({ step, flow }: { step: Step; flow: Flow }) {
|
|
|
699
755
|
) : step.storeAccess ? (
|
|
700
756
|
<StoreCallDetail step={step} flow={flow} />
|
|
701
757
|
) : (
|
|
702
|
-
|
|
703
|
-
<div className="mono text-[13px]">
|
|
704
|
-
{step.label ?? "internal call"}
|
|
705
|
-
</div>
|
|
706
|
-
<Label>Source</Label>
|
|
707
|
-
<Where step={step} flow={flow} />
|
|
708
|
-
</>
|
|
758
|
+
<CallDetail step={step} flow={flow} />
|
|
709
759
|
)}
|
|
710
760
|
|
|
711
761
|
{step.note ? (
|
package/src/flow/answers.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import { catalog, index } from "../data";
|
|
3
3
|
import { walkSteps } from "../catalog";
|
|
4
|
-
import { flowAnswers, stepAnswer } from "./answers";
|
|
4
|
+
import { flowAnswers, stepAnswer, stepRpcContract } from "./answers";
|
|
5
5
|
|
|
6
6
|
const flow = (slug: string) => {
|
|
7
7
|
const found = catalog.flows.find((f) => f.slug === slug);
|
|
@@ -16,6 +16,23 @@ const step = (slug: string, id: string) => {
|
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
describe("stepAnswer", () => {
|
|
19
|
+
it("resolves an incoming endpoint to its full request and response contract", () => {
|
|
20
|
+
const contract = stepRpcContract(
|
|
21
|
+
index,
|
|
22
|
+
step("pricing-archive-price-list", "s1"),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
expect(contract?.id).toBe("shop.v1.PriceLists/ArchivePriceList");
|
|
26
|
+
expect(contract?.provider.id).toBe("shop.pricing");
|
|
27
|
+
expect(contract?.method.request).toBe("ArchivePriceListRequest");
|
|
28
|
+
expect(contract?.method.response).toBe("ArchivePriceListResponse");
|
|
29
|
+
expect(
|
|
30
|
+
contract?.provided.messages?.find(
|
|
31
|
+
(message) => message.name === contract.method.request,
|
|
32
|
+
)?.fields.map((field) => field.name),
|
|
33
|
+
).toEqual(["price_list_id"]);
|
|
34
|
+
});
|
|
35
|
+
|
|
19
36
|
it("reads the answer of an endpoint off the document that declares it", () => {
|
|
20
37
|
// billing's ViewSet is exposed by billing.v1.Invoices, and the document
|
|
21
38
|
// says a void answers 204 and an issue answers with the invoice's id.
|
package/src/flow/answers.ts
CHANGED
|
@@ -9,14 +9,33 @@
|
|
|
9
9
|
// queryset - which no interface in the catalog describes; an event is a
|
|
10
10
|
// publication, and drawing a reply to it would be a lie about the bus.
|
|
11
11
|
|
|
12
|
-
import type {
|
|
12
|
+
import type {
|
|
13
|
+
CatalogIndex,
|
|
14
|
+
External,
|
|
15
|
+
Flow,
|
|
16
|
+
RpcMethod,
|
|
17
|
+
RpcService,
|
|
18
|
+
Service,
|
|
19
|
+
Step,
|
|
20
|
+
} from "../catalog";
|
|
13
21
|
import { walkSteps } from "../catalog";
|
|
14
22
|
|
|
15
|
-
|
|
16
|
-
|
|
23
|
+
export interface StepRpcContract {
|
|
24
|
+
id: string;
|
|
25
|
+
provider: Service | External;
|
|
26
|
+
provided: RpcService;
|
|
27
|
+
method: RpcMethod;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The interface and method an RPC step reaches, when the catalog has them. */
|
|
31
|
+
export function stepRpcContract(
|
|
32
|
+
index: CatalogIndex,
|
|
33
|
+
step: Step,
|
|
34
|
+
): StepRpcContract | undefined {
|
|
17
35
|
if (step.kind !== "rpc") return undefined;
|
|
18
36
|
|
|
19
|
-
//
|
|
37
|
+
// A recorded ref is the full `<interface>/<method>` id, whether the flow
|
|
38
|
+
// enters this service or calls another one.
|
|
20
39
|
if (step.ref) {
|
|
21
40
|
const cut = step.ref.lastIndexOf("/");
|
|
22
41
|
if (cut < 0) return undefined;
|
|
@@ -26,22 +45,32 @@ function methodOf(index: CatalogIndex, step: Step): RpcMethod | undefined {
|
|
|
26
45
|
const provider =
|
|
27
46
|
index.rpcProviderByMethod.get(step.ref) ?? index.externalProviderByMethod.get(step.ref);
|
|
28
47
|
const provided = provider?.provides.find((p) => p.id === interfaceId);
|
|
29
|
-
|
|
48
|
+
const method = provided?.methods.find((m) => m.name === name);
|
|
49
|
+
if (!provider || !provided || !method) return undefined;
|
|
50
|
+
return { id: step.ref, provider, provided, method };
|
|
30
51
|
}
|
|
31
52
|
|
|
32
|
-
//
|
|
53
|
+
// Older incoming flows recorded only the operation label. Keep resolving
|
|
54
|
+
// those catalogs while new extractors write the full ref above.
|
|
33
55
|
const service = index.serviceById.get(step.to);
|
|
34
56
|
if (!service || !step.label) return undefined;
|
|
35
57
|
for (const provided of service.provides) {
|
|
36
58
|
const found = provided.methods.find((m) => m.name === step.label);
|
|
37
|
-
if (found)
|
|
59
|
+
if (found) {
|
|
60
|
+
return {
|
|
61
|
+
id: `${provided.id}/${found.name}`,
|
|
62
|
+
provider: service,
|
|
63
|
+
provided,
|
|
64
|
+
method: found,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
38
67
|
}
|
|
39
68
|
return undefined;
|
|
40
69
|
}
|
|
41
70
|
|
|
42
71
|
/** What the callee hands back, as the contract names it. */
|
|
43
72
|
export function stepAnswer(index: CatalogIndex, step: Step): string | undefined {
|
|
44
|
-
return
|
|
73
|
+
return stepRpcContract(index, step)?.method.response || undefined;
|
|
45
74
|
}
|
|
46
75
|
|
|
47
76
|
/** Every request without an explicit response step that has an answer. */
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { expect, it } from "vitest";
|
|
2
|
+
import { buildIndex, type Catalog, type Step } from "../catalog";
|
|
3
|
+
import { stepRelationEvidence } from "./evidence";
|
|
4
|
+
|
|
5
|
+
const index = buildIndex({ generatedAt: "", commit: "", contexts: [], defs: {}, flows: [], adrs: [] } as Catalog);
|
|
6
|
+
it("gives legacy call, event, RPC and response steps an honest source explanation", () => {
|
|
7
|
+
for (const kind of ["call", "event", "rpc", "response"] as const) {
|
|
8
|
+
const step: Step = { type: "step", id: "s1", from: "a", to: "b", kind, status: "declared", line: "handler.go:8" };
|
|
9
|
+
expect(stepRelationEvidence(index, step)).toEqual([{ kind: "call-site", rule: "source-expression", source: "handler.go:8", symbol: undefined }]);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
it("keeps unresolved status and deduplicates recorded evidence", () => {
|
|
13
|
+
const evidence = { kind: "call-site" as const, rule: "source-expression", source: "handler.go:8" };
|
|
14
|
+
const step: Step = { type: "step", id: "s1", from: "a", to: "b", kind: "call", status: "unresolved", line: evidence.source, evidence: [evidence, evidence] };
|
|
15
|
+
expect(stepRelationEvidence(index, step).map((item) => item.kind)).toEqual(["call-site", "unresolved"]);
|
|
16
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { CatalogIndex, RelationEvidence, Step } from "../catalog";
|
|
2
|
+
import { stepRpcContract } from "./answers";
|
|
3
|
+
|
|
4
|
+
/** Adapt older fragments to the same explanation without inventing a path. */
|
|
5
|
+
export function stepRelationEvidence(index: CatalogIndex, step: Step): RelationEvidence[] {
|
|
6
|
+
const items = [...(step.evidence ?? [])];
|
|
7
|
+
if (step.line && !items.some((item) => item.kind === "call-site" && item.source === step.line)) {
|
|
8
|
+
items.push({ kind: "call-site", rule: "source-expression", source: step.line, symbol: step.label });
|
|
9
|
+
}
|
|
10
|
+
const contract = stepRpcContract(index, step);
|
|
11
|
+
if (contract) items.push({ kind: "contract", rule: "catalog-contract", source: contract.provided.source, symbol: contract.id });
|
|
12
|
+
if (step.kind === "event" && step.ref) {
|
|
13
|
+
const event = index.eventById.get(step.ref);
|
|
14
|
+
const version = event?.versions.at(-1);
|
|
15
|
+
if (version) items.push({ kind: "contract", rule: "event-declaration", source: version.source, symbol: step.ref });
|
|
16
|
+
}
|
|
17
|
+
if (step.storeAccess?.source) items.push({ kind: "binding", rule: "repository-sql-call", source: step.storeAccess.source, symbol: step.storeAccess.method });
|
|
18
|
+
if (step.destination?.resolution) {
|
|
19
|
+
const resolution = step.destination.resolution;
|
|
20
|
+
items.push({ kind: "resolution", rule: resolution.basis, symbol: `${resolution.provider} ${resolution.route}` });
|
|
21
|
+
}
|
|
22
|
+
if (step.continuesAt) items.push({ kind: "resolution", rule: "continuation-entrypoint", symbol: step.continuesAt });
|
|
23
|
+
if (step.kind === "response" && step.replyTo) items.push({ kind: "resolution", rule: "response-to-request", symbol: step.replyTo });
|
|
24
|
+
if (step.status === "unresolved" && !items.some((item) => item.kind === "unresolved")) {
|
|
25
|
+
items.push({ kind: "unresolved", rule: "unresolved-target", symbol: step.ref ?? step.label });
|
|
26
|
+
}
|
|
27
|
+
const seen = new Set<string>();
|
|
28
|
+
return items.filter((item) => {
|
|
29
|
+
const key = JSON.stringify(item);
|
|
30
|
+
if (seen.has(key)) return false;
|
|
31
|
+
seen.add(key);
|
|
32
|
+
return true;
|
|
33
|
+
});
|
|
34
|
+
}
|
package/src/index.css
CHANGED
|
@@ -681,6 +681,50 @@
|
|
|
681
681
|
outline: 2px solid color-mix(in srgb, var(--accent) 55%, white);
|
|
682
682
|
outline-offset: 3px;
|
|
683
683
|
}
|
|
684
|
+
.product-danger {
|
|
685
|
+
color: white;
|
|
686
|
+
border-color: #a33a4b;
|
|
687
|
+
background:
|
|
688
|
+
radial-gradient(
|
|
689
|
+
circle at 18% 0%,
|
|
690
|
+
rgb(255 255 255 / 0.16),
|
|
691
|
+
transparent 42%
|
|
692
|
+
),
|
|
693
|
+
linear-gradient(
|
|
694
|
+
to bottom,
|
|
695
|
+
#8e2b3d,
|
|
696
|
+
#651827
|
|
697
|
+
);
|
|
698
|
+
box-shadow:
|
|
699
|
+
inset 0 1px 0 rgb(255 255 255 / 0.2),
|
|
700
|
+
inset 0 -1px 0 rgb(0 0 0 / 0.28),
|
|
701
|
+
0 1px 2px rgb(0 0 0 / 0.24),
|
|
702
|
+
0 12px 28px -18px color-mix(in srgb, var(--status-unresolved) 84%, transparent);
|
|
703
|
+
text-shadow: 0 1px 0 rgb(0 0 0 / 0.32);
|
|
704
|
+
}
|
|
705
|
+
.product-danger:hover {
|
|
706
|
+
color: white;
|
|
707
|
+
border-color: #c45a6b;
|
|
708
|
+
background:
|
|
709
|
+
radial-gradient(
|
|
710
|
+
circle at 18% 0%,
|
|
711
|
+
rgb(255 255 255 / 0.2),
|
|
712
|
+
transparent 42%
|
|
713
|
+
),
|
|
714
|
+
linear-gradient(
|
|
715
|
+
to bottom,
|
|
716
|
+
#a13246,
|
|
717
|
+
#741b2c
|
|
718
|
+
);
|
|
719
|
+
box-shadow:
|
|
720
|
+
inset 0 1px 0 rgb(255 255 255 / 0.26),
|
|
721
|
+
inset 0 -1px 0 rgb(0 0 0 / 0.3),
|
|
722
|
+
0 2px 4px rgb(0 0 0 / 0.25),
|
|
723
|
+
0 16px 34px -17px color-mix(in srgb, var(--status-unresolved) 96%, transparent);
|
|
724
|
+
}
|
|
725
|
+
.product-danger:focus-visible {
|
|
726
|
+
outline-color: color-mix(in srgb, var(--status-unresolved) 62%, white);
|
|
727
|
+
}
|
|
684
728
|
.product-primary:disabled,
|
|
685
729
|
.tbtn:disabled {
|
|
686
730
|
cursor: not-allowed;
|
|
@@ -105,7 +105,9 @@ export function DraggableReveal({
|
|
|
105
105
|
className={`pointer-events-none absolute inset-0 z-0 flex ${catPlacement[revealEdge]}`}
|
|
106
106
|
>
|
|
107
107
|
<m.img
|
|
108
|
-
layout
|
|
108
|
+
// Deliberately not a layout animation: the preview's children can
|
|
109
|
+
// reflow independently, and their tab changes must not send this
|
|
110
|
+
// hidden illustration across the background.
|
|
109
111
|
src={cat}
|
|
110
112
|
alt=""
|
|
111
113
|
width={800}
|
|
@@ -118,7 +120,6 @@ export function DraggableReveal({
|
|
|
118
120
|
y: dragging ? -3 : 0,
|
|
119
121
|
}}
|
|
120
122
|
transition={{
|
|
121
|
-
layout: { type: "spring", stiffness: 260, damping: 28 },
|
|
122
123
|
opacity: { duration: 0.22 },
|
|
123
124
|
scale: { duration: 0.28 },
|
|
124
125
|
y: { duration: 0.28 },
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { ArrowRight, Braces, CircleDotDashed, Code2, Network } from "lucide-react";
|
|
2
|
+
import { useRef } from "react";
|
|
3
|
+
import { useInView } from "motion/react";
|
|
4
|
+
import { m, useReducedMotion } from "../lib/motion";
|
|
5
|
+
|
|
6
|
+
const steps = [
|
|
7
|
+
{
|
|
8
|
+
icon: Code2,
|
|
9
|
+
label: "01 / read",
|
|
10
|
+
title: "Code and specifications",
|
|
11
|
+
copy: "Existing source, schemas, contracts, ADRs and traces.",
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
icon: Braces,
|
|
15
|
+
label: "02 / extract",
|
|
16
|
+
title: "Local fragments",
|
|
17
|
+
copy: "Each service publishes the architectural facts it owns.",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
icon: CircleDotDashed,
|
|
21
|
+
label: "03 / validate",
|
|
22
|
+
title: "One estate model",
|
|
23
|
+
copy: "Portolan merges the union and checks every relationship.",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
icon: Network,
|
|
27
|
+
label: "04 / publish",
|
|
28
|
+
title: "A static catalog",
|
|
29
|
+
copy: "Browse the site or export Markdown, Mermaid and Backstage entities.",
|
|
30
|
+
},
|
|
31
|
+
] as const;
|
|
32
|
+
|
|
33
|
+
// One shared cycle keeps the arrows in sync, including the quiet pause.
|
|
34
|
+
const cycle = 6.8;
|
|
35
|
+
const pulse = (index: number) => {
|
|
36
|
+
const start = 0.4 + index * 0.85;
|
|
37
|
+
return {
|
|
38
|
+
duration: cycle,
|
|
39
|
+
repeat: Infinity,
|
|
40
|
+
ease: "easeInOut" as const,
|
|
41
|
+
times: [0, start, start + 0.2, start + 0.65, start + 1.15, cycle].map(
|
|
42
|
+
(time) => time / cycle,
|
|
43
|
+
),
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export function EvidencePipeline() {
|
|
48
|
+
const ref = useRef<HTMLOListElement>(null);
|
|
49
|
+
const inView = useInView(ref, { amount: 0.2 });
|
|
50
|
+
const reduced = useReducedMotion();
|
|
51
|
+
const playing = inView && !reduced;
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<ol
|
|
55
|
+
ref={ref}
|
|
56
|
+
aria-label="How Portolan turns evidence into architecture documentation"
|
|
57
|
+
className="mt-12 grid gap-4 md:grid-cols-2 xl:grid-cols-4"
|
|
58
|
+
>
|
|
59
|
+
{steps.map((item, index) => {
|
|
60
|
+
const Icon = item.icon;
|
|
61
|
+
const continues = index < steps.length - 1;
|
|
62
|
+
const connectsWithinRow = index === 0 || index === 2;
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<li
|
|
66
|
+
key={item.title}
|
|
67
|
+
className="relative rounded-card border border-line bg-canvas p-5 shadow-xs"
|
|
68
|
+
>
|
|
69
|
+
<div className="flex items-center justify-between">
|
|
70
|
+
<span className="flow-tile text-accent">
|
|
71
|
+
<Icon size={14} aria-hidden />
|
|
72
|
+
</span>
|
|
73
|
+
<span className="mono text-faint">{item.label}</span>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<h3 className="mt-8 text-md font-semibold">{item.title}</h3>
|
|
77
|
+
<p className="mt-2 text-sm text-muted">{item.copy}</p>
|
|
78
|
+
|
|
79
|
+
{continues ? (
|
|
80
|
+
<span
|
|
81
|
+
aria-hidden
|
|
82
|
+
className={`absolute top-[22px] -right-[17px] z-10 h-[18px] w-4 items-center justify-center bg-canvas text-faint ${
|
|
83
|
+
connectsWithinRow ? "hidden md:flex" : "hidden xl:flex"
|
|
84
|
+
}`}
|
|
85
|
+
>
|
|
86
|
+
<ArrowRight size={11} />
|
|
87
|
+
<m.span
|
|
88
|
+
className="absolute inset-0 flex items-center justify-center text-accent drop-shadow-[0_0_4px_var(--color-accent)]"
|
|
89
|
+
initial={{ opacity: 0, x: -4 }}
|
|
90
|
+
animate={playing ? {
|
|
91
|
+
opacity: [0, 0, 1, 1, 0, 0],
|
|
92
|
+
x: [-4, -4, -2, 2, 4, 4],
|
|
93
|
+
} : { opacity: 0, x: -4 }}
|
|
94
|
+
transition={playing ? pulse(index + 0.65) : { duration: 0.2 }}
|
|
95
|
+
>
|
|
96
|
+
<ArrowRight size={11} />
|
|
97
|
+
</m.span>
|
|
98
|
+
</span>
|
|
99
|
+
) : null}
|
|
100
|
+
</li>
|
|
101
|
+
);
|
|
102
|
+
})}
|
|
103
|
+
</ol>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ArrowRight,
|
|
3
3
|
Bot,
|
|
4
|
-
Braces,
|
|
5
4
|
Check,
|
|
6
|
-
CircleDotDashed,
|
|
7
|
-
Code2,
|
|
8
5
|
Copy,
|
|
9
6
|
FileSearch,
|
|
10
7
|
Files,
|
|
11
8
|
FileText,
|
|
12
9
|
GitBranch,
|
|
13
10
|
Moon,
|
|
14
|
-
Network,
|
|
15
11
|
ShieldCheck,
|
|
16
12
|
Sun,
|
|
17
13
|
Terminal,
|
|
@@ -27,6 +23,7 @@ import { m } from "../lib/motion";
|
|
|
27
23
|
import { paths } from "../routes";
|
|
28
24
|
import { catalogTo } from "./catalog";
|
|
29
25
|
import { DraggableReveal } from "./DraggableReveal";
|
|
26
|
+
import { EvidencePipeline } from "./EvidencePipeline";
|
|
30
27
|
import { heroColumn, heroLine, Reveal } from "./motion";
|
|
31
28
|
import { ProductFrame } from "./ProductFrame";
|
|
32
29
|
import { ProductTour } from "./ProductTour";
|
|
@@ -249,33 +246,6 @@ function CopyCommand({
|
|
|
249
246
|
);
|
|
250
247
|
}
|
|
251
248
|
|
|
252
|
-
const pipeline = [
|
|
253
|
-
{
|
|
254
|
-
icon: Code2,
|
|
255
|
-
label: "01 / read",
|
|
256
|
-
title: "Code and specifications",
|
|
257
|
-
copy: "Existing source, schemas, contracts, ADRs and traces.",
|
|
258
|
-
},
|
|
259
|
-
{
|
|
260
|
-
icon: Braces,
|
|
261
|
-
label: "02 / extract",
|
|
262
|
-
title: "Local fragments",
|
|
263
|
-
copy: "Each service publishes the architectural facts it owns.",
|
|
264
|
-
},
|
|
265
|
-
{
|
|
266
|
-
icon: CircleDotDashed,
|
|
267
|
-
label: "03 / validate",
|
|
268
|
-
title: "One estate model",
|
|
269
|
-
copy: "Portolan merges the union and checks every relationship.",
|
|
270
|
-
},
|
|
271
|
-
{
|
|
272
|
-
icon: Network,
|
|
273
|
-
label: "04 / publish",
|
|
274
|
-
title: "A static catalog",
|
|
275
|
-
copy: "Browse the site or export Markdown, Mermaid and Backstage entities.",
|
|
276
|
-
},
|
|
277
|
-
];
|
|
278
|
-
|
|
279
249
|
export function LandingPage() {
|
|
280
250
|
useDocumentTitle("Architecture from code");
|
|
281
251
|
|
|
@@ -422,34 +392,7 @@ export function LandingPage() {
|
|
|
422
392
|
Architecture documentation that begins with evidence.
|
|
423
393
|
</h2>
|
|
424
394
|
</Reveal>
|
|
425
|
-
<
|
|
426
|
-
{pipeline.map((item, index) => {
|
|
427
|
-
const Icon = item.icon;
|
|
428
|
-
return (
|
|
429
|
-
<Reveal
|
|
430
|
-
key={item.title}
|
|
431
|
-
delay={index * 0.07}
|
|
432
|
-
className="relative rounded-card border border-line bg-canvas p-5 shadow-xs"
|
|
433
|
-
>
|
|
434
|
-
<div className="flex items-center justify-between">
|
|
435
|
-
<span className="flow-tile text-accent">
|
|
436
|
-
<Icon size={14} />
|
|
437
|
-
</span>
|
|
438
|
-
<span className="mono text-faint">{item.label}</span>
|
|
439
|
-
</div>
|
|
440
|
-
<h3 className="mt-8 text-md font-semibold">{item.title}</h3>
|
|
441
|
-
<p className="mt-2 text-sm text-muted">{item.copy}</p>
|
|
442
|
-
{index < pipeline.length - 1 ? (
|
|
443
|
-
<ArrowRight
|
|
444
|
-
size={16}
|
|
445
|
-
className="absolute top-1/2 -right-[11px] z-10 hidden -translate-y-1/2 rounded-full bg-canvas text-faint xl:block"
|
|
446
|
-
aria-hidden
|
|
447
|
-
/>
|
|
448
|
-
) : null}
|
|
449
|
-
</Reveal>
|
|
450
|
-
);
|
|
451
|
-
})}
|
|
452
|
-
</div>
|
|
395
|
+
<EvidencePipeline />
|
|
453
396
|
</div>
|
|
454
397
|
</section>
|
|
455
398
|
|