@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.
Files changed (184) hide show
  1. package/README.md +23 -0
  2. package/catalog/enum_test.go +46 -0
  3. package/catalog/evidence_test.go +35 -0
  4. package/catalog/model.go +1066 -0
  5. package/catalog/roundtrip_test.go +203 -0
  6. package/catalog/via_test.go +38 -0
  7. package/cli/init.test.mjs +6 -1
  8. package/cli/portolan.mjs +8 -0
  9. package/cli/portolan.test.mjs +49 -0
  10. package/go.mod +14 -0
  11. package/go.sum +20 -0
  12. package/internal/gocall/README.md +19 -0
  13. package/internal/gocall/analyze.go +189 -0
  14. package/internal/gocall/analyze_test.go +107 -0
  15. package/internal/gohttp/analyze.go +2562 -0
  16. package/internal/gohttp/destination.go +373 -0
  17. package/internal/gohttp/endpoints.go +1067 -0
  18. package/internal/gohttp/roots.go +320 -0
  19. package/internal/gohttp/typed.go +96 -0
  20. package/internal/goscan/constants.go +85 -0
  21. package/internal/goscan/goscan_test.go +227 -0
  22. package/internal/goscan/index.go +629 -0
  23. package/internal/goscan/index_test.go +66 -0
  24. package/internal/goscan/names.go +52 -0
  25. package/internal/goscan/parse_test.go +11 -0
  26. package/internal/goscan/source.go +37 -0
  27. package/internal/goscan/tree.go +284 -0
  28. package/internal/goscan/types.go +99 -0
  29. package/internal/wsdl/ids.go +127 -0
  30. package/internal/wsdl/ids_test.go +21 -0
  31. package/internal/wsdl/model.go +70 -0
  32. package/internal/wsdl/parse.go +949 -0
  33. package/internal/wsdl/parse_test.go +170 -0
  34. package/package.json +22 -10
  35. package/plugin/describe.go +118 -0
  36. package/plugin/describe_test.go +114 -0
  37. package/plugin/protocol.go +141 -0
  38. package/plugin/schematest/schematest.go +126 -0
  39. package/plugins/README.md +114 -46
  40. package/plugins/cmd/portolan-http-clients/main.go +19 -0
  41. package/plugins/extract-celery/extract.py +0 -2
  42. package/plugins/extract-celery/extract_test.py +1 -1
  43. package/plugins/extract-django/README.md +39 -17
  44. package/plugins/extract-django/domain.py +28 -17
  45. package/plugins/extract-django/extract.py +21 -7
  46. package/plugins/extract-django/extract_test.py +55 -2
  47. package/plugins/extract-django/lifecycle.py +2 -0
  48. package/plugins/extract-django/operations.py +1 -1
  49. package/plugins/extract-django/routing_test.py +109 -1
  50. package/plugins/extract-django/store.py +1 -1
  51. package/plugins/extract-django/transport.py +101 -55
  52. package/plugins/extract-django/verbs.py +241 -0
  53. package/plugins/extract-go/README.md +47 -0
  54. package/plugins/extract-http-clients/describe.go +19 -0
  55. package/plugins/extract-http-clients/describe_test.go +11 -0
  56. package/plugins/extract-http-clients/extract.go +740 -0
  57. package/plugins/extract-http-clients/extract_test.go +1561 -0
  58. package/plugins/extract-http-clients/main.go +41 -0
  59. package/plugins/extract-java/build/org/portolan/extract/Extract.class +0 -0
  60. package/plugins/extract-java/build/org/portolan/extract/Protocol$Builder.class +0 -0
  61. package/plugins/extract-java/build/org/portolan/extract/Protocol$Input.class +0 -0
  62. package/plugins/extract-java/build/org/portolan/extract/Protocol$Options.class +0 -0
  63. package/plugins/extract-python-kafka/README.md +6 -0
  64. package/plugins/extract-python-kafka/extract.py +2 -4
  65. package/plugins/extract-python-kafka/extract_test.py +18 -2
  66. package/plugins/extract-ts/extract.test.ts +2 -2
  67. package/plugins/extract-ts/extract.ts +4 -5
  68. package/plugins/extract-ts/graphql.test.ts +1 -1
  69. package/plugins/openapi/ids.go +261 -0
  70. package/plugins/openapi/ids_test.go +98 -0
  71. package/plugins/portolan-go.wasm +0 -0
  72. package/plugins/pyplugin/catalog.py +12 -1
  73. package/plugins/pyplugin/kafka.py +74 -3
  74. package/plugins/pyplugin/protocol.py +1 -5
  75. package/portolan.json +3 -2
  76. package/schema/portolan.schema.json +34 -0
  77. package/scripts/README.md +18 -12
  78. package/scripts/catalog-sources.mjs +6 -0
  79. package/scripts/delivery-presets.mjs +21 -11
  80. package/scripts/diff.mjs +5 -1
  81. package/scripts/django-aggregates.test.mjs +58 -0
  82. package/scripts/gen-likec4.mjs +79 -21
  83. package/scripts/gen-likec4.test.mjs +25 -2
  84. package/scripts/gen.mjs +118 -115
  85. package/scripts/go-discovery.test.mjs +30 -0
  86. package/scripts/history.mjs +186 -3
  87. package/scripts/history.test.mjs +1 -1
  88. package/scripts/host-plugins/fetch-git.mjs +77 -21
  89. package/scripts/host-plugins/fetch-git.test.mjs +62 -8
  90. package/scripts/local-api.mjs +71 -4
  91. package/scripts/local-api.test.mjs +63 -4
  92. package/scripts/local-discovery.mjs +82 -9
  93. package/scripts/manifest.mjs +5 -3
  94. package/scripts/manifest.test.mjs +24 -0
  95. package/scripts/output-diff.mjs +94 -0
  96. package/scripts/output-diff.test.mjs +36 -0
  97. package/scripts/package-smoke.mjs +62 -4
  98. package/scripts/plugin-host.mjs +22 -2
  99. package/scripts/plugin-host.test.mjs +9 -0
  100. package/scripts/plugin-wasm-worker.mjs +4 -1
  101. package/scripts/provenance.mjs +72 -0
  102. package/scripts/provenance.test.mjs +149 -0
  103. package/scripts/run-builtin.mjs +39 -5
  104. package/scripts/schema.mjs +29 -0
  105. package/scripts/warning-policy.mjs +161 -0
  106. package/scripts/warning-policy.test.mjs +56 -0
  107. package/src/app/Breadcrumbs.test.ts +4 -0
  108. package/src/app/Breadcrumbs.tsx +1 -0
  109. package/src/app/Sidebar.tsx +3 -3
  110. package/src/catalog-docs.test.ts +64 -0
  111. package/src/catalog-docs.ts +35 -0
  112. package/src/catalog-error.test.ts +15 -0
  113. package/src/catalog-model.ts +70 -6
  114. package/src/catalog-stores.test.ts +17 -0
  115. package/src/catalog-validation.ts +52 -2
  116. package/src/catalog.test.ts +13 -2
  117. package/src/chat/Starter.tsx +5 -11
  118. package/src/chat/tools.test.ts +27 -0
  119. package/src/chat/tools.ts +5 -9
  120. package/src/components/CatalogStamp.tsx +10 -8
  121. package/src/components/ChannelRows.messagepack.test.tsx +28 -0
  122. package/src/components/ChannelRows.test.tsx +54 -0
  123. package/src/components/ChannelRows.tsx +57 -10
  124. package/src/components/HTTPDestinationEvidence.test.tsx +23 -0
  125. package/src/components/HTTPDestinationEvidence.tsx +31 -0
  126. package/src/components/Integrations.tsx +1 -1
  127. package/src/components/LifecycleDiagram.tsx +28 -12
  128. package/src/components/MachineDocs.tsx +6 -5
  129. package/src/components/MethodRows.tsx +9 -2
  130. package/src/components/ProblemRow.tsx +4 -0
  131. package/src/components/RelationEvidence.test.tsx +14 -0
  132. package/src/components/RelationEvidence.tsx +53 -0
  133. package/src/components/WhatLinksHere.tsx +6 -4
  134. package/src/data.ts +25 -7
  135. package/src/enrich.test.ts +336 -6
  136. package/src/enrich.ts +206 -3
  137. package/src/flow/StepDetail.tsx +104 -54
  138. package/src/flow/answers.test.ts +18 -1
  139. package/src/flow/answers.ts +37 -8
  140. package/src/flow/evidence.test.ts +16 -0
  141. package/src/flow/evidence.ts +34 -0
  142. package/src/index.css +44 -0
  143. package/src/landing/DraggableReveal.tsx +3 -2
  144. package/src/landing/EvidencePipeline.tsx +105 -0
  145. package/src/landing/LandingPage.tsx +2 -59
  146. package/src/lib/backlinks.test.ts +16 -1
  147. package/src/lib/backlinks.ts +20 -0
  148. package/src/lib/catalog-diff.test.ts +18 -0
  149. package/src/lib/catalog-diff.ts +20 -2
  150. package/src/lib/derive.ts +1 -0
  151. package/src/lib/django-aggregates.d.mts +9 -0
  152. package/src/lib/django-aggregates.mjs +36 -0
  153. package/src/lib/django-aggregates.test.ts +29 -0
  154. package/src/lib/django-aggregates.ts +5 -0
  155. package/src/lib/kafka-ui.test.ts +87 -0
  156. package/src/lib/kafka-ui.ts +105 -0
  157. package/src/lib/local-api.ts +20 -2
  158. package/src/lib/setup-info.test.ts +17 -0
  159. package/src/lib/setup-info.ts +58 -0
  160. package/src/lib/warnings.test.ts +54 -0
  161. package/src/lib/warnings.ts +260 -0
  162. package/src/lib/wire-problems.test.ts +21 -0
  163. package/src/lib/wire-problems.ts +62 -1
  164. package/src/likec4/FlowView.tsx +2 -6
  165. package/src/likec4/flow-edges.test.ts +64 -1
  166. package/src/likec4/flow-edges.ts +43 -7
  167. package/src/likec4/view-index.ts +8 -2
  168. package/src/map/ContextMapGraph.tsx +76 -32
  169. package/src/merge.test.ts +23 -0
  170. package/src/merge.ts +33 -10
  171. package/src/pages/AggregatePage.tsx +8 -7
  172. package/src/pages/CatalogFailure.tsx +2 -2
  173. package/src/pages/ContextPage.tsx +6 -5
  174. package/src/pages/ServicePage.tsx +4 -3
  175. package/src/pages/Settings.tsx +200 -44
  176. package/src/pages/settings/DjangoAggregateChoices.tsx +79 -0
  177. package/src/pages/settings/IntegrationsSettings.tsx +117 -0
  178. package/src/routes.test.ts +2 -0
  179. package/src/routes.ts +2 -1
  180. package/src/selection/DetailPanel.tsx +61 -1
  181. package/src/virtual-provenance.d.ts +11 -0
  182. package/vite.config.ts +5 -0
  183. package/scripts/vendor-lock.mjs +0 -58
  184. package/scripts/vendor-lock.test.mjs +0 -69
@@ -1,3 +1,4 @@
1
+ import { RelationEvidencePanel } from "../components/RelationEvidence";
1
2
  // The right rail. One panel, one selection, every page that draws a diagram.
2
3
  //
3
4
  // It renders by kind rather than by page, so an event opened from a flow reads
@@ -34,7 +35,7 @@ import { stepLabel } from "../flow/labels";
34
35
  import { KIND_LABEL } from "../lib/kinds";
35
36
  import { eventScope, resolveShape } from "../lib/shape";
36
37
  import { stepsInto } from "../lib/backlinks";
37
- import { treeHref } from "../lib/source-link";
38
+ import { sourceLocation, treeHref } from "../lib/source-link";
38
39
  import { walkSteps } from "../catalog";
39
40
  import { methodCount } from "../lib/api";
40
41
  import {
@@ -49,6 +50,7 @@ import { upstreamOf } from "../er/lineage";
49
50
  import type { LineageMaps } from "../er/lineage";
50
51
  import { ctxStyle } from "../lib/context-color";
51
52
  import { Ident } from "../components/Ident";
53
+ import { SourcePreviewLink } from "../components/SourcePreview";
52
54
  import { StatusChip } from "../components/primitives";
53
55
  import { StepDetailBody } from "../flow/StepDetail";
54
56
  import {
@@ -237,6 +239,10 @@ function ViewBody({
237
239
  return (
238
240
  <>
239
241
  {view.doc ? <p className="mt-2 text-muted">{view.doc}</p> : null}
242
+ <RelationEvidencePanel items={[
243
+ ...(view.source ? [{ kind: "contract" as const, rule: "sql-view-definition", source: view.source, symbol: view.name }] : []),
244
+ ...(view.persists?.evidence ?? []),
245
+ ]} renderSource={(source) => <SchemaEvidenceSource source={source} owner={store.owner} />} />
240
246
 
241
247
  <Label>Store</Label>
242
248
  <SelectLink id={store.id}>{store.id}</SelectLink>
@@ -343,6 +349,7 @@ function TableBody({
343
349
  return (
344
350
  <>
345
351
  {table.doc ? <p className="mt-2 text-muted">{table.doc}</p> : null}
352
+ <RelationEvidencePanel items={[...(table.evidence ?? []), ...(table.persists?.evidence ?? [])]} renderSource={(source) => <SchemaEvidenceSource source={source} owner={store.owner} />} />
346
353
 
347
354
  <Label>Store</Label>
348
355
  <SelectLink id={store.id}>{store.id}</SelectLink>
@@ -420,6 +427,50 @@ function TableBody({
420
427
  </>
421
428
  ) : null}
422
429
 
430
+ {(table.accesses ?? []).length > 0 ? (
431
+ <>
432
+ <Label>Reads / writes</Label>
433
+ {(table.accesses ?? []).map((access, accessIndex) => {
434
+ const location = access.source
435
+ ? sourceLocation(
436
+ access.source,
437
+ index.serviceById.get(store.owner),
438
+ allRepos(catalog),
439
+ )
440
+ : null;
441
+ const tone =
442
+ access.operation === "read"
443
+ ? "text-accent"
444
+ : access.operation === "write"
445
+ ? "text-verified"
446
+ : "text-unresolved";
447
+ return (
448
+ <div
449
+ key={`${access.operation}:${access.method}:${access.source}:${accessIndex}`}
450
+ className="grid grid-cols-[auto_minmax(0,1fr)] gap-x-2 gap-y-0.5 py-1"
451
+ >
452
+ <span className={`chip self-start uppercase ${tone}`}>
453
+ {access.operation}
454
+ </span>
455
+ <div className="min-w-0">
456
+ <div className="mono break-all text-ink">
457
+ {access.method ?? "SQL client call"}
458
+ </div>
459
+ {access.source ? (
460
+ <SourcePreviewLink
461
+ location={location}
462
+ className="mono block break-all text-muted hover:text-accent"
463
+ >
464
+ {access.source}
465
+ </SourcePreviewLink>
466
+ ) : null}
467
+ </div>
468
+ </div>
469
+ );
470
+ })}
471
+ </>
472
+ ) : null}
473
+
423
474
  <Label>Referenced by</Label>
424
475
  {into.length === 0 ? (
425
476
  <div className="mono text-muted">nothing points at this table</div>
@@ -489,6 +540,11 @@ function ColumnBody({
489
540
  return (
490
541
  <>
491
542
  {column.doc ? <p className="mt-2 text-muted">{column.doc}</p> : null}
543
+ {(column.fk || column.from?.length || column.maps) ? <RelationEvidencePanel items={[
544
+ ...(table?.evidence ?? []),
545
+ ...(table?.persists?.evidence ?? []),
546
+ ...(view?.source ? [{ kind: "contract" as const, rule: "sql-view-definition", source: view.source, symbol: view.name }] : []),
547
+ ]} renderSource={(source) => <SchemaEvidenceSource source={source} owner={resolved.store.owner} />} /> : null}
492
548
 
493
549
  <Label>{view ? "View" : "Table"}</Label>
494
550
  {view ? (
@@ -1505,3 +1561,7 @@ export function WithDetail({
1505
1561
  </SavedGroup>
1506
1562
  );
1507
1563
  }
1564
+
1565
+ function SchemaEvidenceSource({ source, owner }: { source: string; owner: string }) {
1566
+ return <SourcePreviewLink location={sourceLocation(source, index.serviceById.get(owner), allRepos(catalog))} className="mono break-all text-muted hover:text-accent">{source}</SourcePreviewLink>;
1567
+ }
@@ -0,0 +1,11 @@
1
+ // The stamp of every catalog source, as the checkout's history says: served
2
+ // by `scripts/provenance.mjs` at build time (portolan.0010). Keyed by the
3
+ // path `src/data.ts` gives a source. A staged site imports its sources under
4
+ // flattened names; `source` is then the path the file has in the workspace.
5
+ declare module "virtual:portolan-provenance" {
6
+ const provenance: Record<
7
+ string,
8
+ { commit: string; generatedAt: string; source?: string }
9
+ >;
10
+ export default provenance;
11
+ }
package/vite.config.ts CHANGED
@@ -17,6 +17,10 @@ import { localApiPlugin } from "./scripts/local-api.mjs";
17
17
  // generator wrote them; the build copies the same files into dist.
18
18
  // @ts-expect-error plain JavaScript module intentionally has no browser types
19
19
  import { siteDocsPlugin } from "./scripts/site-docs.mjs";
20
+ // When each catalog source last changed, read from git at build time and
21
+ // served to the app as one virtual module (portolan.0010).
22
+ // @ts-expect-error plain JavaScript module intentionally has no browser types
23
+ import { provenancePlugin } from "./scripts/provenance.mjs";
20
24
 
21
25
  /** A git answer, or "" when there is nothing to answer with (no repo, no git). */
22
26
  function git(args: string): string {
@@ -160,6 +164,7 @@ export default defineConfig({
160
164
  // TypeScript below node_modules.
161
165
  localApiPlugin(workspace, publicSetupFrom),
162
166
  siteDocsPlugin(workspace),
167
+ provenancePlugin(workspace),
163
168
  react(),
164
169
  tailwindcss(),
165
170
  // The AsyncAPI reference brings a parser written for Node, and it calls
@@ -1,58 +0,0 @@
1
- // The commit a vendored copy is of.
2
- //
3
- // `stampFor` dates a fragment from the last commit that touched the directory
4
- // it was read from, which is the right answer for code that lives here and the
5
- // wrong one for code that does not. A service fetched out of another
6
- // repository is stamped, without this, with the commit that VENDORED it: the
7
- // estate then reports the service as fresh because somebody re-ran the fetch,
8
- // and as unchanged when its own repository moved.
9
- //
10
- // The right answer is already on disk. `fetch-git` writes `git.lock.json`
11
- // beside each copy, naming the commit it fetched, and a copy re-fetched at the
12
- // same commit does not move it. Reading it here keeps the rule the host has
13
- // always kept - the host works out the stamp, never the plugin - while making
14
- // the subject of the stamp the code rather than the vendoring.
15
-
16
- import { readFileSync } from "node:fs";
17
- import { dirname, join } from "node:path";
18
-
19
- /** The file `fetch-git` writes beside every copy. Its name is that plugin's. */
20
- export const LOCK = "git.lock.json";
21
-
22
- /**
23
- * The commit `root` is a copy of, or null when it is not a copy of anything.
24
- *
25
- * Walks up from the directory the extractor was pointed at, because the lock
26
- * sits at the root of the repository's copy and the step reads a service some
27
- * way inside it: `vendor/repos/acme/shop/services/oms` is stamped by the lock
28
- * three levels above it. It stops at the repository root, so a run in a tree
29
- * with no vendored copies reads nothing and answers null.
30
- *
31
- * A lock naming other than exactly one repository is not one of these - the
32
- * fetcher writes one per directory - and is left to the fetcher to complain
33
- * about, which it does with the path and the count. Nothing here fails a run:
34
- * the caller has a git history to fall back on, and a stamp is not worth
35
- * refusing to describe an estate over.
36
- */
37
- export function vendoredCommit(root, read = readFileSync) {
38
- for (let at = root; at && at !== "." && at !== "/"; at = dirname(at)) {
39
- let raw;
40
- try {
41
- raw = read(join(at, LOCK), "utf8");
42
- } catch {
43
- continue; // No lock here; the copy's root may still be above.
44
- }
45
-
46
- try {
47
- const lock = JSON.parse(raw);
48
- const [only, ...rest] = lock.repos ?? [];
49
- if (only?.commit && rest.length === 0) return only.commit;
50
- } catch {
51
- // Unreadable, which the fetcher reports against the copy itself.
52
- }
53
-
54
- return null;
55
- }
56
-
57
- return null;
58
- }
@@ -1,69 +0,0 @@
1
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { tmpdir } from "node:os";
4
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
-
6
- import { vendoredCommit } from "./vendor-lock.mjs";
7
-
8
- const COMMIT = "c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0";
9
-
10
- describe("vendoredCommit", () => {
11
- let root;
12
-
13
- beforeEach(() => {
14
- root = mkdtempSync(join(tmpdir(), "portolan-vendor-"));
15
- });
16
- afterEach(() => {
17
- rmSync(root, { recursive: true, force: true });
18
- });
19
-
20
- const copy = (at, lock) => {
21
- mkdirSync(join(root, at), { recursive: true });
22
- writeFileSync(join(root, at, "git.lock.json"), JSON.stringify(lock));
23
- };
24
-
25
- // The case this exists for: the extractor is pointed at a service some way
26
- // inside the copy, and the lock is at the copy's root.
27
- it("reads the lock above the directory the extractor was given", () => {
28
- copy("vendor/repos/acme/shop", { repos: [{ repo: "github.com/acme/shop", commit: COMMIT }] });
29
- mkdirSync(join(root, "vendor/repos/acme/shop/services/oms"), { recursive: true });
30
-
31
- expect(vendoredCommit(join(root, "vendor/repos/acme/shop/services/oms"))).toBe(COMMIT);
32
- });
33
-
34
- it("answers null for a directory that is nobody's copy", () => {
35
- mkdirSync(join(root, "examples/shop/oms"), { recursive: true });
36
-
37
- expect(vendoredCommit(join(root, "examples/shop/oms"))).toBeNull();
38
- });
39
-
40
- // The fetcher writes one repository per directory. Anything else is its
41
- // problem to report, with the path and the count; a stamp is not worth
42
- // failing a run over, so this stops rather than guessing which one won.
43
- it("answers null for a lock naming more than one repository", () => {
44
- copy("vendor/repos/acme/shop", {
45
- repos: [
46
- { repo: "github.com/acme/shop", commit: COMMIT },
47
- { repo: "github.com/acme/pay", commit: "0000000" },
48
- ],
49
- });
50
-
51
- expect(vendoredCommit(join(root, "vendor/repos/acme/shop"))).toBeNull();
52
- });
53
-
54
- it("answers null for a lock that cannot be read", () => {
55
- mkdirSync(join(root, "vendor/repos/acme/shop"), { recursive: true });
56
- writeFileSync(join(root, "vendor/repos/acme/shop/git.lock.json"), "{ not json");
57
-
58
- expect(vendoredCommit(join(root, "vendor/repos/acme/shop"))).toBeNull();
59
- });
60
-
61
- // A copy nested inside another copy takes the innermost lock: it is the one
62
- // that names the files the extractor is about to read.
63
- it("takes the nearest lock", () => {
64
- copy("vendor/repos/acme", { repos: [{ repo: "github.com/acme/outer", commit: "9999999" }] });
65
- copy("vendor/repos/acme/shop", { repos: [{ repo: "github.com/acme/shop", commit: COMMIT }] });
66
-
67
- expect(vendoredCommit(join(root, "vendor/repos/acme/shop"))).toBe(COMMIT);
68
- });
69
- });