@shortlink-org/portolan 0.2.4 → 0.4.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 +174 -6
- package/catalog/enum_test.go +46 -0
- package/catalog/evidence_test.go +35 -0
- package/catalog/model.go +1213 -0
- package/catalog/roundtrip_test.go +210 -0
- package/catalog/via_test.go +38 -0
- package/cli/init.test.mjs +6 -1
- package/cli/portolan.mjs +14 -1
- package/cli/portolan.test.mjs +49 -0
- package/go.mod +21 -0
- package/go.sum +34 -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 +28 -15
- package/plugin/describe.go +156 -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 +404 -50
- package/plugins/cmd/portolan-http-clients/main.go +19 -0
- package/plugins/extract-argocd/options.schema.json +44 -0
- package/plugins/extract-celery/extract.py +0 -2
- package/plugins/extract-celery/extract_test.py +1 -1
- package/plugins/extract-celery/main.py +1 -0
- package/plugins/extract-csharp-ddd/README.md +213 -0
- package/plugins/extract-csharp-ddd/options.schema.json +55 -0
- package/plugins/extract-django/README.md +52 -19
- package/plugins/extract-django/choices.py +47 -0
- package/plugins/extract-django/domain.py +30 -18
- package/plugins/extract-django/extract.py +21 -7
- package/plugins/extract-django/extract_test.py +68 -2
- package/plugins/extract-django/lifecycle.py +4 -28
- package/plugins/extract-django/main.py +1 -0
- package/plugins/extract-django/operations.py +1 -1
- package/plugins/extract-django/routing_test.py +109 -1
- package/plugins/extract-django/rules.py +154 -0
- package/plugins/extract-django/rules_test.py +158 -0
- 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-go-sqs/options.schema.json +21 -0
- package/plugins/extract-http-clients/describe.go +20 -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/Main.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-k8s/options.schema.json +32 -0
- package/plugins/extract-laravel/Cargo.lock +962 -0
- package/plugins/extract-laravel/Cargo.toml +20 -0
- package/plugins/extract-laravel/README.md +200 -0
- package/plugins/extract-laravel/options.schema.json +68 -0
- package/plugins/extract-laravel/rustfmt.toml +1 -0
- package/plugins/extract-php-ddd/Cargo.lock +972 -0
- package/plugins/extract-php-ddd/Cargo.toml +22 -0
- package/plugins/extract-php-ddd/README.md +141 -0
- package/plugins/extract-php-ddd/options.schema.json +50 -0
- package/plugins/extract-php-ddd/rustfmt.toml +1 -0
- package/plugins/extract-python-kafka/extract.py +0 -2
- package/plugins/extract-python-kafka/extract_test.py +1 -1
- package/plugins/extract-python-kafka/main.py +1 -0
- package/plugins/extract-terraform/options.schema.json +26 -0
- 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/extract-ts/main.ts +1 -0
- package/plugins/openapi/ids.go +261 -0
- package/plugins/openapi/ids_test.go +98 -0
- package/plugins/phpscan/Cargo.lock +953 -0
- package/plugins/phpscan/Cargo.toml +19 -0
- package/plugins/phpscan/rustfmt.toml +1 -0
- package/plugins/portolan-go.wasm +0 -0
- package/plugins/pyplugin/catalog.py +24 -1
- package/plugins/pyplugin/protocol.py +1 -5
- package/plugins/verify-otel/options.schema.json +12 -0
- package/portolan.json +132 -4
- package/rules/builtin.json +218 -0
- package/schema/portolan.schema.json +905 -4
- package/scripts/README.md +21 -13
- package/scripts/catalog-sources.mjs +6 -0
- package/scripts/delivery-presets.mjs +57 -14
- package/scripts/diff.mjs +5 -1
- package/scripts/django-aggregates.test.mjs +58 -0
- package/scripts/gen-likec4.mjs +150 -17
- package/scripts/gen-likec4.test.mjs +96 -0
- package/scripts/gen.mjs +148 -118
- package/scripts/gitops-example.test.mjs +108 -0
- 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-argocd.mjs +338 -0
- package/scripts/host-plugins/fetch-argocd.options.json +49 -0
- package/scripts/host-plugins/fetch-argocd.test.mjs +274 -0
- package/scripts/host-plugins/fetch-bsr.mjs +1 -0
- package/scripts/host-plugins/fetch-csr.mjs +1 -0
- package/scripts/host-plugins/fetch-git.mjs +78 -21
- package/scripts/host-plugins/fetch-git.test.mjs +62 -8
- package/scripts/host-plugins/fetch-k8s.mjs +263 -0
- package/scripts/host-plugins/fetch-k8s.options.json +50 -0
- package/scripts/host-plugins/fetch-k8s.test.mjs +259 -0
- package/scripts/host-plugins/k8s-topology.mjs +183 -0
- package/scripts/json-format.mjs +192 -0
- package/scripts/json-format.test.mjs +97 -0
- package/scripts/local-api.mjs +287 -12
- package/scripts/local-api.test.mjs +150 -5
- package/scripts/local-discovery.mjs +94 -9
- package/scripts/manifest.mjs +14 -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 +45 -3
- package/scripts/plugin-host.test.mjs +9 -0
- package/scripts/plugin-wasm-worker.mjs +4 -1
- package/scripts/plugins-fresh.mjs +97 -0
- package/scripts/plugins-fresh.test.mjs +64 -0
- package/scripts/provenance.mjs +72 -0
- package/scripts/provenance.test.mjs +149 -0
- package/scripts/run-builtin.mjs +62 -7
- package/scripts/schema.mjs +167 -19
- package/scripts/trace-trials.mjs +176 -0
- package/scripts/trace-trials.test.mjs +142 -0
- package/scripts/warning-policy.mjs +167 -0
- package/scripts/warning-policy.test.mjs +93 -0
- package/src/app/Breadcrumbs.test.ts +3 -0
- package/src/app/Breadcrumbs.tsx +3 -0
- package/src/app/CatalogApp.tsx +2 -0
- package/src/app/Sidebar.tsx +3 -3
- package/src/app/SidebarFooter.tsx +20 -4
- 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-index.ts +25 -0
- package/src/catalog-model.ts +293 -5
- package/src/catalog-validation.ts +113 -2
- package/src/catalog.test.ts +40 -1
- 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.test.tsx +76 -1
- package/src/components/ChannelRows.tsx +223 -60
- package/src/components/DeploymentRows.tsx +131 -0
- package/src/components/DocsLinks.test.tsx +27 -0
- package/src/components/DocsLinks.tsx +56 -0
- package/src/components/FieldTree.tsx +5 -2
- 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/MachineDocs.tsx +6 -5
- package/src/components/MethodRows.tsx +9 -2
- package/src/components/PluginIcon.tsx +77 -0
- package/src/components/ProblemRow.tsx +173 -153
- package/src/components/RelationEvidence.test.tsx +14 -0
- package/src/components/RelationEvidence.tsx +53 -0
- package/src/components/RuleMarks.tsx +22 -0
- package/src/components/ShapeRows.tsx +24 -20
- package/src/data.ts +46 -7
- package/src/enrich.test.ts +459 -4
- package/src/enrich.ts +308 -7
- package/src/er/ErCanvas.tsx +218 -12
- package/src/er/GroupNode.tsx +57 -0
- package/src/er/StoreHeader.tsx +1 -0
- package/src/er/layout.test.ts +85 -2
- package/src/er/layout.ts +140 -5
- package/src/er/spec.test.ts +17 -0
- package/src/er/spec.ts +23 -10
- package/src/flow/Recordings.test.tsx +52 -0
- package/src/flow/Recordings.tsx +236 -0
- package/src/flow/StepDetail.tsx +59 -0
- package/src/flow/TraceTrial.tsx +419 -0
- package/src/flow/evidence.test.ts +16 -0
- package/src/flow/evidence.ts +34 -0
- package/src/flow/examples.test.ts +33 -0
- package/src/flow/examples.ts +37 -0
- package/src/flow/outline.test.ts +28 -0
- package/src/flow/outline.ts +7 -2
- package/src/flow/trace-trial-resume.test.ts +42 -0
- package/src/flow/trace-trial-resume.ts +74 -0
- package/src/graph/elk.ts +78 -0
- package/src/index.css +68 -0
- package/src/landing/DraggableReveal.tsx +3 -2
- package/src/landing/EvidencePipeline.tsx +105 -0
- package/src/landing/LandingPage.tsx +17 -70
- package/src/landing/ProductTour.tsx +6 -6
- package/src/lib/all-problems.ts +27 -17
- package/src/lib/catalog-diff.ts +1 -1
- package/src/lib/centrality.test.ts +251 -0
- package/src/lib/centrality.ts +232 -0
- package/src/lib/confluence.test.ts +37 -0
- package/src/lib/confluence.ts +41 -0
- package/src/lib/context-color.ts +1 -1
- package/src/lib/deployment-drift.ts +22 -0
- package/src/lib/derive.ts +43 -69
- 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/environments.test.ts +83 -0
- package/src/lib/environments.ts +57 -0
- package/src/lib/integration-url.test.ts +30 -0
- package/src/lib/integration-url.ts +63 -0
- package/src/lib/kafka-ui.ts +3 -45
- package/src/lib/local-api.ts +116 -4
- package/src/lib/notion.ts +13 -0
- package/src/lib/plugin-index.json +2765 -0
- package/src/lib/plugins.test.ts +68 -0
- package/src/lib/plugins.ts +264 -0
- package/src/lib/problem-flows.test.ts +61 -0
- package/src/lib/problem-flows.ts +78 -0
- package/src/lib/problem-rules-cel.d.mts +43 -0
- package/src/lib/problem-rules-cel.mjs +407 -0
- package/src/lib/problem-rules.test.ts +287 -0
- package/src/lib/problem-rules.ts +271 -0
- package/src/lib/problem-subjects.ts +737 -0
- package/src/lib/rule-entries.ts +39 -0
- package/src/lib/{data-problems.test.ts → rules-data.test.ts} +18 -16
- package/src/lib/rules-deploy.test.ts +133 -0
- package/src/lib/{problems.test.ts → rules-edges.test.ts} +8 -3
- package/src/lib/{proto-problems.test.ts → rules-proto.test.ts} +9 -25
- package/src/lib/{wire-problems.test.ts → rules-wire.test.ts} +13 -11
- package/src/lib/rules.test.ts +51 -0
- package/src/lib/rules.ts +86 -0
- package/src/lib/setup-info.test.ts +17 -0
- package/src/lib/setup-info.ts +58 -0
- package/src/lib/shape.test.ts +32 -0
- package/src/lib/shape.ts +30 -6
- package/src/lib/tech.ts +16 -0
- package/src/lib/trace-project.test.ts +34 -0
- package/src/lib/trace-project.ts +50 -0
- package/src/lib/use-problems.ts +23 -0
- package/src/lib/warnings.test.ts +63 -0
- package/src/lib/warnings.ts +260 -0
- package/src/likec4/ids.test.ts +6 -2
- package/src/likec4/ids.ts +43 -0
- package/src/main.tsx +23 -0
- package/src/map/ContextMapGraph.tsx +76 -32
- package/src/merge-deployments.test.ts +127 -0
- package/src/merge.test.ts +82 -0
- package/src/merge.ts +189 -18
- package/src/pages/AggregatePage.tsx +65 -14
- package/src/pages/ContextMap.tsx +45 -3
- package/src/pages/ContextPage.tsx +8 -5
- package/src/pages/EventPage.tsx +15 -5
- package/src/pages/FlowDetail.tsx +23 -2
- package/src/pages/GraphPage.tsx +40 -3
- package/src/pages/Overview.tsx +152 -12
- package/src/pages/PluginIndex.tsx +190 -0
- package/src/pages/Problems.tsx +396 -128
- package/src/pages/ServicePage.tsx +62 -5
- package/src/pages/Settings.tsx +217 -43
- package/src/pages/settings/AboutSettings.tsx +8 -1
- package/src/pages/settings/DjangoAggregateChoices.tsx +79 -0
- package/src/pages/settings/IntegrationsSettings.tsx +63 -17
- package/src/pages/settings/RecordingSettings.tsx +138 -0
- package/src/pages/settings/RulesSettings.tsx +825 -0
- package/src/routes.test.ts +9 -0
- package/src/routes.ts +22 -1
- package/src/selection/DetailPanel.tsx +15 -0
- 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/lib/data-problems.ts +0 -314
- package/src/lib/proto-problems.ts +0 -237
- package/src/lib/wire-problems.ts +0 -342
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { useMemo } from "react";
|
|
2
1
|
import { NavLink } from "react-router";
|
|
3
2
|
import {
|
|
4
3
|
Check,
|
|
5
4
|
PanelLeftOpen,
|
|
5
|
+
Puzzle,
|
|
6
6
|
Settings2,
|
|
7
7
|
TriangleAlert,
|
|
8
8
|
} from "lucide-react";
|
|
9
9
|
|
|
10
10
|
import { allModules } from "../catalog";
|
|
11
|
+
import { catalog } from "../data";
|
|
11
12
|
import { KindIcon } from "../components/kind";
|
|
12
13
|
import { CompassRose } from "../components/logo";
|
|
13
|
-
import {
|
|
14
|
-
import { allProblems } from "../lib/all-problems";
|
|
14
|
+
import { useProblems } from "../lib/use-problems";
|
|
15
15
|
import type { Kind } from "../lib/kinds";
|
|
16
16
|
import { paths } from "../routes";
|
|
17
17
|
|
|
@@ -28,7 +28,7 @@ export function SidebarBottomGroup() {
|
|
|
28
28
|
// opens. The badge is red while anything on it is an error and amber when
|
|
29
29
|
// only the schema disagrees with itself - a page of warnings is not a clean
|
|
30
30
|
// estate, and a green tick over it would be the one lie the row can tell.
|
|
31
|
-
const found =
|
|
31
|
+
const found = useProblems();
|
|
32
32
|
const errors = found.filter((p) => p.severity === "error").length;
|
|
33
33
|
const colour =
|
|
34
34
|
errors > 0 ? "var(--status-unresolved)" : "var(--status-declared)";
|
|
@@ -76,6 +76,22 @@ export function SidebarBottomGroup() {
|
|
|
76
76
|
</span>
|
|
77
77
|
)}
|
|
78
78
|
</NavLink>
|
|
79
|
+
<NavLink
|
|
80
|
+
to={paths.plugins()}
|
|
81
|
+
data-nav-item
|
|
82
|
+
title="Plugins — what Portolan can read, and what it makes"
|
|
83
|
+
style={({ isActive }) => ({
|
|
84
|
+
paddingLeft: indent(0),
|
|
85
|
+
background: isActive ? "var(--surface-2)" : undefined,
|
|
86
|
+
borderLeftWidth: 2,
|
|
87
|
+
borderLeftStyle: "solid",
|
|
88
|
+
borderLeftColor: isActive ? "var(--accent)" : "transparent",
|
|
89
|
+
})}
|
|
90
|
+
className="tree-row flex items-center gap-1.5 py-[3px] pr-2 text-muted t-micro transition-colors hover:bg-surface hover:text-ink"
|
|
91
|
+
>
|
|
92
|
+
<Puzzle size={14} aria-hidden className="block shrink-0" />
|
|
93
|
+
<span className="truncate">Plugins</span>
|
|
94
|
+
</NavLink>
|
|
79
95
|
<NavLink
|
|
80
96
|
to={paths.settings()}
|
|
81
97
|
data-nav-item
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { catalogDocs } from "./catalog-docs";
|
|
6
|
+
// @ts-expect-error The build's documentation mount is a plain JavaScript module.
|
|
7
|
+
import { siteDocs } from "../scripts/site-docs.mjs";
|
|
8
|
+
|
|
9
|
+
const roots: string[] = [];
|
|
10
|
+
afterEach(() => roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true })));
|
|
11
|
+
|
|
12
|
+
describe("catalog documentation URLs", () => {
|
|
13
|
+
it.each(["default", "avia", "portolan"])("opens the files actually published for root catalog %s", (id) => {
|
|
14
|
+
const root = mkdtempSync(join(tmpdir(), "catalog-docs-"));
|
|
15
|
+
roots.push(root);
|
|
16
|
+
const out = join(root, "generated", "markdown");
|
|
17
|
+
const dist = join(root, "dist");
|
|
18
|
+
mkdirSync(out, { recursive: true });
|
|
19
|
+
mkdirSync(dist);
|
|
20
|
+
writeFileSync(join(out, "llms.txt"), "# Catalog\n[Home](README.md)\n");
|
|
21
|
+
writeFileSync(join(out, "llms-full.txt"), "# Full catalog\n");
|
|
22
|
+
writeFileSync(join(out, "README.md"), "# Home\n");
|
|
23
|
+
const manifest = { generate: [{ plugin: "markdown", ...(id === "default" ? {} : { catalog: id }), out }] };
|
|
24
|
+
siteDocs({ manifest, dist });
|
|
25
|
+
|
|
26
|
+
const urls = catalogDocs(manifest, id, "/portolan/")!;
|
|
27
|
+
expect(urls.index).toBe("/portolan/llms.txt");
|
|
28
|
+
const published = (url: string) => readFileSync(join(dist, url.slice("/portolan/".length)), "utf8");
|
|
29
|
+
expect(published(urls.index)).toContain("[Home](docs/README.md)");
|
|
30
|
+
expect(published(urls.full)).toBe("# Full catalog\n");
|
|
31
|
+
expect(published(`${urls.pages}README.md`)).toBe("# Home\n");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("uses a nested output directory even when it differs from the catalog id", () => {
|
|
35
|
+
const manifest = { generate: [
|
|
36
|
+
{ plugin: "markdown", catalog: "avia", out: "./generated/docs/" },
|
|
37
|
+
{ plugin: "markdown", catalog: "example", out: "generated/docs/samples" },
|
|
38
|
+
] };
|
|
39
|
+
expect(catalogDocs(manifest, "example", "/portolan")).toEqual({
|
|
40
|
+
pages: "/portolan/docs/samples/",
|
|
41
|
+
index: "/portolan/docs/samples/llms.txt",
|
|
42
|
+
full: "/portolan/docs/samples/llms-full.txt",
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("preserves the demo's example paths and supports a site served at /", () => {
|
|
47
|
+
const manifest = { generate: [
|
|
48
|
+
{ plugin: "markdown", catalog: "portolan", out: "docs" },
|
|
49
|
+
{ plugin: "markdown", catalog: "example", out: "docs/example" },
|
|
50
|
+
] };
|
|
51
|
+
expect(catalogDocs(manifest, "example", "/")?.index).toBe("/docs/example/llms.txt");
|
|
52
|
+
expect(catalogDocs(manifest, "portolan", "/")?.index).toBe("/llms.txt");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("does not invent URLs for missing or unmounted documentation", () => {
|
|
56
|
+
expect(catalogDocs({}, "default", "/")).toBeNull();
|
|
57
|
+
const manifest = { generate: [
|
|
58
|
+
{ plugin: "markdown", catalog: "avia", out: "docs" },
|
|
59
|
+
{ plugin: "markdown", catalog: "example", out: "docs-other" },
|
|
60
|
+
] };
|
|
61
|
+
expect(catalogDocs(manifest, "unknown", "/")).toBeNull();
|
|
62
|
+
expect(catalogDocs(manifest, "example", "/")).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface DocsManifest {
|
|
2
|
+
generate?: { plugin: string; catalog?: string; out: string }[];
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export interface CatalogDocs {
|
|
6
|
+
pages: string;
|
|
7
|
+
index: string;
|
|
8
|
+
full: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Match site-docs.mjs: the first markdown output is mounted at docs/.
|
|
12
|
+
* Other catalogs are available when generated inside that output directory.
|
|
13
|
+
*/
|
|
14
|
+
export function catalogDocs(
|
|
15
|
+
manifest: DocsManifest,
|
|
16
|
+
profileId: string,
|
|
17
|
+
base: string,
|
|
18
|
+
): CatalogDocs | null {
|
|
19
|
+
const steps = manifest.generate?.filter((step) => step.plugin === "markdown") ?? [];
|
|
20
|
+
const root = steps[0];
|
|
21
|
+
const selected = steps.find((step) => step.catalog === profileId)
|
|
22
|
+
?? steps.find((step) => !step.catalog);
|
|
23
|
+
if (!root || !selected) return null;
|
|
24
|
+
|
|
25
|
+
const directory = (out: string) => new URL(`${out.replace(/\/$/, "")}/`, "https://workspace.invalid/").pathname;
|
|
26
|
+
const rootPath = directory(root.out);
|
|
27
|
+
const selectedPath = directory(selected.out);
|
|
28
|
+
if (!selectedPath.startsWith(rootPath)) return null;
|
|
29
|
+
|
|
30
|
+
const prefix = base.endsWith("/") ? base : `${base}/`;
|
|
31
|
+
const suffix = selectedPath.slice(rootPath.length);
|
|
32
|
+
const pages = `${prefix}docs/${suffix}`;
|
|
33
|
+
const indexRoot = suffix ? pages : prefix;
|
|
34
|
+
return { pages, index: `${indexRoot}llms.txt`, full: `${indexRoot}llms-full.txt` };
|
|
35
|
+
}
|
|
@@ -21,6 +21,21 @@ function failureOf(catalog: Catalog): CatalogError {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
describe("CatalogError.path", () => {
|
|
24
|
+
it("allows rootless source groups while preserving aggregate root validation", () => {
|
|
25
|
+
const data = clone();
|
|
26
|
+
const aggregate = data.contexts[0]!.services[0]!.aggregates[0]!;
|
|
27
|
+
aggregate.root = "";
|
|
28
|
+
delete aggregate.lifecycle;
|
|
29
|
+
expect(() => validateCatalog(data)).toThrow(/names no root/);
|
|
30
|
+
aggregate.kind = "model-group";
|
|
31
|
+
expect(() => validateCatalog(data)).not.toThrow();
|
|
32
|
+
aggregate.root = aggregate.entities[0]!.name;
|
|
33
|
+
expect(() => validateCatalog(data)).toThrow(/cannot declare an aggregate root/);
|
|
34
|
+
aggregate.root = "";
|
|
35
|
+
aggregate.lifecycle = { states: ["Active"], transitions: [] };
|
|
36
|
+
expect(() => validateCatalog(data)).toThrow(/cannot declare an aggregate root or lifecycle/);
|
|
37
|
+
});
|
|
38
|
+
|
|
24
39
|
it("names the flow and the step for a step that points at a missing lane", () => {
|
|
25
40
|
const bad = clone();
|
|
26
41
|
const flow = bad.flows[0];
|
package/src/catalog-index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
BoundedContext,
|
|
7
7
|
Catalog,
|
|
8
8
|
Column,
|
|
9
|
+
Deployment,
|
|
9
10
|
Enum,
|
|
10
11
|
Event,
|
|
11
12
|
External,
|
|
@@ -21,12 +22,14 @@ import type {
|
|
|
21
22
|
View,
|
|
22
23
|
} from "./catalog-model.ts";
|
|
23
24
|
import {
|
|
25
|
+
allDeployments,
|
|
24
26
|
allExternals,
|
|
25
27
|
allModules,
|
|
26
28
|
allStores,
|
|
27
29
|
allTerms,
|
|
28
30
|
aggregateBlocks,
|
|
29
31
|
columnId,
|
|
32
|
+
deploys,
|
|
30
33
|
enumsOf,
|
|
31
34
|
storeViews,
|
|
32
35
|
viewReads,
|
|
@@ -140,6 +143,14 @@ export interface CatalogIndex {
|
|
|
140
143
|
lineageInto: Map<string, string[]>;
|
|
141
144
|
/** service id -> stores it owns, in catalog order */
|
|
142
145
|
storesOwnedBy: Map<string, Store[]>;
|
|
146
|
+
/**
|
|
147
|
+
* service id -> where it runs, in snapshot order.
|
|
148
|
+
*
|
|
149
|
+
* Joined here rather than written by the fetcher, which reads a control
|
|
150
|
+
* plane and knows no service: an Application is this service's when it
|
|
151
|
+
* deploys from the service's repository, inside the service's directory.
|
|
152
|
+
*/
|
|
153
|
+
deploymentsByService: Map<string, Deployment[]>;
|
|
143
154
|
/** aggregate id -> tables naming it in `persists`, in catalog order */
|
|
144
155
|
tablesByAggregate: Map<string, Table[]>;
|
|
145
156
|
/** aggregate id -> views naming it in `persists`, in catalog order */
|
|
@@ -323,6 +334,19 @@ export function buildIndex(catalog: Catalog): CatalogIndex {
|
|
|
323
334
|
}
|
|
324
335
|
}
|
|
325
336
|
|
|
337
|
+
// Deployments come after the services because the join runs over them: a
|
|
338
|
+
// row is placed on every service whose repository and directory it deploys
|
|
339
|
+
// from, and on none when nobody in the estate claims that repository.
|
|
340
|
+
const deploymentsByService = new Map<string, Deployment[]>();
|
|
341
|
+
for (const deployment of allDeployments(catalog)) {
|
|
342
|
+
for (const service of serviceById.values()) {
|
|
343
|
+
if (!deploys(deployment, service)) continue;
|
|
344
|
+
const placed = deploymentsByService.get(service.id) ?? [];
|
|
345
|
+
placed.push(deployment);
|
|
346
|
+
deploymentsByService.set(service.id, placed);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
326
350
|
// Stores come after the domain tree because they point into it: a table says
|
|
327
351
|
// which aggregate it persists, and a column which block it maps to, so both
|
|
328
352
|
// are resolved against maps that are already full.
|
|
@@ -467,6 +491,7 @@ export function buildIndex(catalog: Catalog): CatalogIndex {
|
|
|
467
491
|
lineageFrom,
|
|
468
492
|
lineageInto,
|
|
469
493
|
storesOwnedBy,
|
|
494
|
+
deploymentsByService,
|
|
470
495
|
moduleById,
|
|
471
496
|
moduleBySlug,
|
|
472
497
|
interfacesByModule,
|
package/src/catalog-model.ts
CHANGED
|
@@ -44,6 +44,13 @@ export interface Catalog {
|
|
|
44
44
|
* to pin and renders as it did before there was anything to pin.
|
|
45
45
|
*/
|
|
46
46
|
repos?: RepoPin[];
|
|
47
|
+
/**
|
|
48
|
+
* Where the estate's services run, read from what deploys them. Optional
|
|
49
|
+
* in the file and never optional downstream, exactly like `repos`: an
|
|
50
|
+
* estate nobody has pointed at a deployer renders as it did before there
|
|
51
|
+
* was anything to place.
|
|
52
|
+
*/
|
|
53
|
+
deployments?: Deployment[];
|
|
47
54
|
/**
|
|
48
55
|
* The systems outside the estate that a service calls on a contract: a
|
|
49
56
|
* payment provider, a tax API, a carrier. Nobody here builds one, so it has
|
|
@@ -173,6 +180,24 @@ export interface Service {
|
|
|
173
180
|
* same as a service that cannot be built.
|
|
174
181
|
*/
|
|
175
182
|
commands?: Command[];
|
|
183
|
+
/**
|
|
184
|
+
* The names this service answers on, read from what deploys it: a
|
|
185
|
+
* Kubernetes Service's name in its short, namespaced, `svc` and fully
|
|
186
|
+
* qualified forms, and the hosts of the Ingress or HTTPRoute in front of
|
|
187
|
+
* it. Written so that a call another service is configured to make to
|
|
188
|
+
* `pricing.shop.svc` can find the service that answers. Absent when
|
|
189
|
+
* nothing in the tree says where the service is reachable.
|
|
190
|
+
*/
|
|
191
|
+
hosts?: string[];
|
|
192
|
+
/**
|
|
193
|
+
* The in-cluster names this service's workload is configured to reach,
|
|
194
|
+
* read out of its environment and config maps and reduced to the host
|
|
195
|
+
* alone. A value is never kept: not the variable it came from, not the
|
|
196
|
+
* scheme, port, path or credentials around the name. Only a name the
|
|
197
|
+
* cluster resolves qualifies, so a password in an environment variable is
|
|
198
|
+
* not something this list can hold by shape.
|
|
199
|
+
*/
|
|
200
|
+
dials?: string[];
|
|
176
201
|
}
|
|
177
202
|
|
|
178
203
|
/**
|
|
@@ -297,7 +322,11 @@ export interface RpcMethod {
|
|
|
297
322
|
}
|
|
298
323
|
|
|
299
324
|
export interface HttpRoute {
|
|
300
|
-
/**
|
|
325
|
+
/**
|
|
326
|
+
* Upper case: `POST`. Empty when a framework extractor proved the mount but
|
|
327
|
+
* no declaration proved the verb; such a route is never matched against an
|
|
328
|
+
* outbound call, and renderers show the path alone.
|
|
329
|
+
*/
|
|
301
330
|
method: string;
|
|
302
331
|
/** As templated in the document: `/v1/users/{id}`. */
|
|
303
332
|
path: string;
|
|
@@ -361,7 +390,32 @@ export interface EdgeVia {
|
|
|
361
390
|
flow: string; // Flow.slug
|
|
362
391
|
step: string; // Step.id
|
|
363
392
|
}
|
|
393
|
+
/** Source facts remain separate from the merge's choice of provider. */
|
|
394
|
+
export interface HTTPDestination {
|
|
395
|
+
callSite: string;
|
|
396
|
+
endpointExpression: string;
|
|
397
|
+
method: string;
|
|
398
|
+
localPath?: string;
|
|
399
|
+
baseURL?: HTTPBaseURL;
|
|
400
|
+
serviceDiscoveryAlias?: string;
|
|
401
|
+
fullPath?: string;
|
|
402
|
+
join?: { expression: string; source: string };
|
|
403
|
+
/** Runtime URL modifiers after the proven join; not evaluated statically. */
|
|
404
|
+
transforms?: { expression: string; source: string }[];
|
|
405
|
+
resolution?: { basis: "full-path" | "exact-route" | "unique-suffix" | "kubernetes-host"; provider: string; route: string };
|
|
406
|
+
}
|
|
407
|
+
export interface HTTPBaseURL {
|
|
408
|
+
expression: string;
|
|
409
|
+
configField?: string;
|
|
410
|
+
environmentVariable?: string;
|
|
411
|
+
value?: string;
|
|
412
|
+
kind: "literal" | "config-default" | "symbolic";
|
|
413
|
+
source: string;
|
|
414
|
+
optionSource?: string;
|
|
415
|
+
}
|
|
364
416
|
export interface RpcCall {
|
|
417
|
+
evidence?: RelationEvidence[];
|
|
418
|
+
destination?: HTTPDestination;
|
|
365
419
|
id: string; // "<proto.package.Service>/<Method>"
|
|
366
420
|
peer: string; // service id if resolved, else raw name
|
|
367
421
|
status: Status;
|
|
@@ -442,12 +496,112 @@ export interface RepoPin {
|
|
|
442
496
|
/** The commit the copy was made of. Full sha: it is not resolved locally, so there is nothing to expand it against. */
|
|
443
497
|
commit: string;
|
|
444
498
|
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* One place a service runs: an Argo CD Application, as the deployer listed
|
|
502
|
+
* it, reduced to what a deploy changes.
|
|
503
|
+
*
|
|
504
|
+
* A list on the catalog rather than a field on `Service`, for two reasons.
|
|
505
|
+
* The fetcher that writes it reads a control plane, not a service's tree, so
|
|
506
|
+
* it does not know which service an Application is; the join is made where
|
|
507
|
+
* the whole estate is known, by the repository and path the Application
|
|
508
|
+
* deploys from against the ones a service says it lives at. And one service
|
|
509
|
+
* stands in several places - staging, production, a second region - and
|
|
510
|
+
* each is its own row.
|
|
511
|
+
*
|
|
512
|
+
* Nothing here moves without a deploy. Health, sync state and the time of
|
|
513
|
+
* the last operation move on their own, and a page regenerated from the
|
|
514
|
+
* tree cannot follow them; they are not kept.
|
|
515
|
+
*/
|
|
516
|
+
export interface Deployment {
|
|
517
|
+
/** `<argocd namespace>/<application name>`: unique per control plane. */
|
|
518
|
+
id: string;
|
|
519
|
+
/** The Application's name, as `argocd app get` takes it. */
|
|
520
|
+
name: string;
|
|
521
|
+
/** The Argo CD project it belongs to. */
|
|
522
|
+
project: string;
|
|
523
|
+
/**
|
|
524
|
+
* Where a reader would say it runs: the application's environment label,
|
|
525
|
+
* else the cluster it deploys to. What the page groups by.
|
|
526
|
+
*/
|
|
527
|
+
environment: string;
|
|
528
|
+
/** The destination cluster, by its Argo CD name; `in-cluster` for the one Argo CD runs in. */
|
|
529
|
+
cluster: string;
|
|
530
|
+
/** The destination namespace. */
|
|
531
|
+
namespace: string;
|
|
532
|
+
/** The repository it deploys from, spelled the way `Service.repo` spells it. Empty for a chart from a registry. */
|
|
533
|
+
repo: string;
|
|
534
|
+
/** The directory in that repository the manifests are read from, as a reader would type it. Empty for a chart. */
|
|
535
|
+
path: string;
|
|
536
|
+
/** The Helm chart name, when the source is a chart registry rather than a repository. */
|
|
537
|
+
chart?: string;
|
|
538
|
+
/** What the Application tracks: a branch, a tag, a chart version. */
|
|
539
|
+
targetRevision: string;
|
|
540
|
+
/** What is deployed now: the commit, or chart version, the last sync resolved `targetRevision` to. */
|
|
541
|
+
revision: string;
|
|
542
|
+
/** `helm`, `kustomize`, `directory` or `plugin`, as Argo CD says it; empty until it has said. */
|
|
543
|
+
tool: string;
|
|
544
|
+
/** The Application in the Argo CD UI. */
|
|
545
|
+
url: string;
|
|
546
|
+
/**
|
|
547
|
+
* The service it deploys, by id, when the Application's labels say so:
|
|
548
|
+
* `app.kubernetes.io/part-of` and `app.kubernetes.io/name`, the same
|
|
549
|
+
* labels a workload carries. A GitOps repository's Application points at
|
|
550
|
+
* an overlay, not at the service's directory, so the path alone cannot
|
|
551
|
+
* place it; the labels can, and an ApplicationSet stamps them on every
|
|
552
|
+
* Application it makes. Absent when the labels are, and the path decides.
|
|
553
|
+
*/
|
|
554
|
+
service?: string;
|
|
555
|
+
/** The container images the deployed resources run, as the deployer summarised them, sorted. */
|
|
556
|
+
images?: string[];
|
|
557
|
+
/**
|
|
558
|
+
* Who said so. `manifest` is a row an extractor read out of a GitOps
|
|
559
|
+
* tree: what should run. `api` is a row a fetcher read off the deployer:
|
|
560
|
+
* what does. `both` is one the merge folded from the two, which is the
|
|
561
|
+
* ordinary case for an estate that keeps both, and the only one that can
|
|
562
|
+
* carry drift. Absent reads as `api`, which is what every row was before
|
|
563
|
+
* there were trees to read.
|
|
564
|
+
*/
|
|
565
|
+
basis?: DeploymentBasis;
|
|
566
|
+
/**
|
|
567
|
+
* What the tree says where it and the deployer disagree, set by the merge
|
|
568
|
+
* when both spoke (portolan.0013). A field is present only when the two
|
|
569
|
+
* differ; a row with no drift has no field at all.
|
|
570
|
+
*/
|
|
571
|
+
drift?: DeploymentDrift;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
export type DeploymentBasis = "manifest" | "api" | "both";
|
|
575
|
+
|
|
576
|
+
export const DEPLOYMENT_BASES: readonly DeploymentBasis[] = [
|
|
577
|
+
"manifest",
|
|
578
|
+
"api",
|
|
579
|
+
"both",
|
|
580
|
+
] as const;
|
|
581
|
+
|
|
582
|
+
/** The GitOps tree's word on the fields where the deployer says otherwise. */
|
|
583
|
+
export interface DeploymentDrift {
|
|
584
|
+
project?: string;
|
|
585
|
+
cluster?: string;
|
|
586
|
+
namespace?: string;
|
|
587
|
+
path?: string;
|
|
588
|
+
targetRevision?: string;
|
|
589
|
+
/** The images the overlay pins, when they are not the ones running. */
|
|
590
|
+
images?: string[];
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/** The basis a row carries, `api` when it carries none. */
|
|
594
|
+
export function deploymentBasis(deployment: Deployment): DeploymentBasis {
|
|
595
|
+
return deployment.basis ?? "api";
|
|
596
|
+
}
|
|
445
597
|
export interface Aggregate {
|
|
446
598
|
id: string;
|
|
447
599
|
slug: string;
|
|
448
600
|
name: string;
|
|
449
601
|
readme: string;
|
|
450
|
-
/**
|
|
602
|
+
/** A source grouping has no confirmed aggregate boundary. Omitted for aggregates. */
|
|
603
|
+
kind?: "model-group";
|
|
604
|
+
/** Name of the root entity; empty only for a model-group. */
|
|
451
605
|
root: string;
|
|
452
606
|
entities: Entity[];
|
|
453
607
|
valueObjects: ValueObject[];
|
|
@@ -506,6 +660,15 @@ export interface Operation {
|
|
|
506
660
|
* estate can only reach from inside, which is sometimes exactly the point.
|
|
507
661
|
*/
|
|
508
662
|
exposedBy?: string[];
|
|
663
|
+
/**
|
|
664
|
+
* What the caller hands in: the command's or query's own shape, as the
|
|
665
|
+
* message class declares it - the fields of `CreateCourseCommand`. Absent
|
|
666
|
+
* when the extractor does not read messages; empty is a message that
|
|
667
|
+
* carries nothing, `FindCoursesCounterQuery`.
|
|
668
|
+
*/
|
|
669
|
+
fields?: Field[];
|
|
670
|
+
/** Where the handler is, `path:line`, for the reader who wants the code. */
|
|
671
|
+
source?: string;
|
|
509
672
|
}
|
|
510
673
|
|
|
511
674
|
/**
|
|
@@ -656,7 +819,31 @@ export interface Field {
|
|
|
656
819
|
ref?: string;
|
|
657
820
|
/** Protobuf field number; absent for sources whose wire has no field numbers. */
|
|
658
821
|
number?: number;
|
|
822
|
+
/**
|
|
823
|
+
* The source says the field must be sent: a Protovalidate `required`, a
|
|
824
|
+
* name in a JSON Schema `required` list. Absent means the source does not
|
|
825
|
+
* say, which in proto3 and OpenAPI alike means it may be left out.
|
|
826
|
+
*/
|
|
827
|
+
required?: boolean;
|
|
828
|
+
/** What the source says a value must satisfy, in the order it said it. */
|
|
829
|
+
rules?: FieldRule[];
|
|
659
830
|
} // ref -> defs key
|
|
831
|
+
/**
|
|
832
|
+
* One constraint on a field's value, in the catalog's own vocabulary so a
|
|
833
|
+
* Protovalidate `min_len` and a JSON Schema `minLength` are one rule:
|
|
834
|
+
* `min_len`, `max_len`, `len`, `pattern`, `prefix`, `suffix`, `contains`,
|
|
835
|
+
* `not_contains`, `format`, `gt`, `gte`, `lt`, `lte`, `const`, `in`,
|
|
836
|
+
* `not_in`, `multiple_of`, `min_items`, `max_items`, `unique`, `min_pairs`,
|
|
837
|
+
* `max_pairs`, `defined_only`, `lt_now`, `gt_now`, `cel`. A rule on what a
|
|
838
|
+
* list holds is prefixed `items.`; on a map's keys or values, `keys.` or
|
|
839
|
+
* `values.`. A custom option the catalog has no word for keeps the name the
|
|
840
|
+
* source gave it, `(acme.pii)`, so it is shown rather than lost.
|
|
841
|
+
*/
|
|
842
|
+
export interface FieldRule {
|
|
843
|
+
name: string;
|
|
844
|
+
/** The bound as the source wrote it, text so a 64-bit number survives; absent for a bare flag such as `unique`. */
|
|
845
|
+
value?: string;
|
|
846
|
+
}
|
|
660
847
|
export interface TypeDef {
|
|
661
848
|
fields: Field[];
|
|
662
849
|
}
|
|
@@ -679,6 +866,7 @@ export type StoreKind =
|
|
|
679
866
|
| "mongodb"
|
|
680
867
|
| "clickhouse"
|
|
681
868
|
| "s3"
|
|
869
|
+
| "dynamodb"
|
|
682
870
|
| "other";
|
|
683
871
|
|
|
684
872
|
export const STORE_KINDS: readonly StoreKind[] = [
|
|
@@ -689,6 +877,7 @@ export const STORE_KINDS: readonly StoreKind[] = [
|
|
|
689
877
|
"mongodb",
|
|
690
878
|
"clickhouse",
|
|
691
879
|
"s3",
|
|
880
|
+
"dynamodb",
|
|
692
881
|
"other",
|
|
693
882
|
] as const;
|
|
694
883
|
|
|
@@ -734,7 +923,7 @@ export interface RedisKeyspace {
|
|
|
734
923
|
value?: string;
|
|
735
924
|
source?: string;
|
|
736
925
|
/** Aggregate or block whose value this key family holds, when provable. */
|
|
737
|
-
persists?: { aggregate?: string; block?: string };
|
|
926
|
+
persists?: { aggregate?: string; block?: string; evidence?: RelationEvidence[] };
|
|
738
927
|
/** Individual client calls, before they are folded into `operations`. */
|
|
739
928
|
accesses?: RedisAccess[];
|
|
740
929
|
}
|
|
@@ -767,13 +956,14 @@ export const TABLE_ROLES: readonly TableRole[] = [
|
|
|
767
956
|
] as const;
|
|
768
957
|
|
|
769
958
|
export interface Table {
|
|
959
|
+
evidence?: RelationEvidence[];
|
|
770
960
|
id: string; // "<store id>.<table>"
|
|
771
961
|
name: string;
|
|
772
962
|
doc?: string;
|
|
773
963
|
columns: Column[];
|
|
774
964
|
indexes?: TableIndex[];
|
|
775
965
|
/** The domain object this table holds: an aggregate id, and optionally a block id. */
|
|
776
|
-
persists?: { aggregate?: string; block?: string };
|
|
966
|
+
persists?: { aggregate?: string; block?: string; evidence?: RelationEvidence[] };
|
|
777
967
|
role?: TableRole;
|
|
778
968
|
/** Source-backed repository methods that read or write this table. */
|
|
779
969
|
accesses?: TableAccess[];
|
|
@@ -853,7 +1043,7 @@ export interface View {
|
|
|
853
1043
|
/** The SELECT, as the migration declares it. Shown, never parsed. */
|
|
854
1044
|
definition?: string;
|
|
855
1045
|
/** The domain object this view presents, when it presents exactly one. */
|
|
856
|
-
persists?: { aggregate?: string; block?: string };
|
|
1046
|
+
persists?: { aggregate?: string; block?: string; evidence?: RelationEvidence[] };
|
|
857
1047
|
/** Migration or model file, as a reader would open it. */
|
|
858
1048
|
source?: string;
|
|
859
1049
|
}
|
|
@@ -886,6 +1076,47 @@ export interface Flow {
|
|
|
886
1076
|
owner: string;
|
|
887
1077
|
participants: Participant[]; // order is significant - it is the lane order
|
|
888
1078
|
steps: FlowNode[];
|
|
1079
|
+
/**
|
|
1080
|
+
* Recordings of this flow running: one per trace a verifier kept, each
|
|
1081
|
+
* naming the steps it showed with what the spans said about them. Examples
|
|
1082
|
+
* of data, not evidence - the evidence is the status on the step.
|
|
1083
|
+
*/
|
|
1084
|
+
examples?: FlowExample[];
|
|
1085
|
+
}
|
|
1086
|
+
/** One recorded run of a flow, read from one trace. */
|
|
1087
|
+
export interface FlowExample {
|
|
1088
|
+
/** "<recording>#<trace id>": unique within the flow, stable across runs. */
|
|
1089
|
+
id: string;
|
|
1090
|
+
/**
|
|
1091
|
+
* The file the trace was read from, relative to the verify step's input
|
|
1092
|
+
* root - where an uploaded recording is kept.
|
|
1093
|
+
*/
|
|
1094
|
+
recording: string;
|
|
1095
|
+
traceId: string;
|
|
1096
|
+
/** When the root span started, RFC 3339 UTC; absent when the recording has no clock. */
|
|
1097
|
+
recordedAt?: string;
|
|
1098
|
+
/** Root start to the last end the trace shows. */
|
|
1099
|
+
durationMs: number;
|
|
1100
|
+
/** The steps this trace showed, in the order it showed them. */
|
|
1101
|
+
steps: ExampleStep[];
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* What one span said about one step. Only an allowlist of attributes is
|
|
1105
|
+
* carried: names, verbs, routes, status codes - never a query text, a header
|
|
1106
|
+
* or a path with an id in it.
|
|
1107
|
+
*/
|
|
1108
|
+
export interface ExampleStep {
|
|
1109
|
+
step: string;
|
|
1110
|
+
label?: string;
|
|
1111
|
+
durationMs: number;
|
|
1112
|
+
attributes?: Record<string, string>;
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* How many recordings showed a step. On a declared step it accompanies the
|
|
1116
|
+
* raised status; on a step no source declares it is why the step is there.
|
|
1117
|
+
*/
|
|
1118
|
+
export interface StepSeen {
|
|
1119
|
+
traces: number;
|
|
889
1120
|
}
|
|
890
1121
|
export interface FlowTrigger {
|
|
891
1122
|
kind:
|
|
@@ -908,7 +1139,18 @@ export interface Participant {
|
|
|
908
1139
|
label?: string;
|
|
909
1140
|
}
|
|
910
1141
|
export type FlowNode = Step | Parallel | Alt | Loop;
|
|
1142
|
+
/** Source facts used to derive a relationship; not a runtime trace. */
|
|
1143
|
+
export interface RelationEvidence {
|
|
1144
|
+
kind: "call-site" | "function" | "binding" | "contract" | "resolution" | "unresolved";
|
|
1145
|
+
rule: string;
|
|
1146
|
+
source?: string;
|
|
1147
|
+
symbol?: string;
|
|
1148
|
+
candidates?: string[];
|
|
1149
|
+
}
|
|
1150
|
+
|
|
911
1151
|
export interface Step {
|
|
1152
|
+
evidence?: RelationEvidence[];
|
|
1153
|
+
destination?: HTTPDestination;
|
|
912
1154
|
type: "step";
|
|
913
1155
|
id: string;
|
|
914
1156
|
from: string;
|
|
@@ -931,6 +1173,8 @@ export interface Step {
|
|
|
931
1173
|
handoff?: FlowHandoff;
|
|
932
1174
|
/** Repository call resolved to a concrete store operation after merge. */
|
|
933
1175
|
storeAccess?: FlowStoreAccess;
|
|
1176
|
+
/** Recordings that showed this hop; set by a verifier. */
|
|
1177
|
+
seen?: StepSeen;
|
|
934
1178
|
}
|
|
935
1179
|
export interface HTTPResponse {
|
|
936
1180
|
status?: number;
|
|
@@ -986,6 +1230,12 @@ export interface AltBranch {
|
|
|
986
1230
|
steps: FlowNode[];
|
|
987
1231
|
/** True when the flow stops here instead of continuing past the alt. */
|
|
988
1232
|
terminal?: boolean;
|
|
1233
|
+
/**
|
|
1234
|
+
* Recordings that went this way, on a frame a verifier wrote where the
|
|
1235
|
+
* recordings parted. A branch with no steps and a count is the recordings
|
|
1236
|
+
* that went no further.
|
|
1237
|
+
*/
|
|
1238
|
+
seen?: StepSeen;
|
|
989
1239
|
}
|
|
990
1240
|
export interface Loop {
|
|
991
1241
|
type: "loop";
|
|
@@ -1229,6 +1479,44 @@ export function allRepos(catalog: Catalog): RepoPin[] {
|
|
|
1229
1479
|
return catalog.repos ?? [];
|
|
1230
1480
|
}
|
|
1231
1481
|
|
|
1482
|
+
/** Every place a service runs. Absent means none, exactly as with repos. */
|
|
1483
|
+
export function allDeployments(catalog: Catalog): Deployment[] {
|
|
1484
|
+
return catalog.deployments ?? [];
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
/**
|
|
1488
|
+
* Whether a deployment is of this service. The labels decide when the
|
|
1489
|
+
* Application carries them: `service` is the id, and an Application
|
|
1490
|
+
* labelled for one service is not another's however its path reads. Without
|
|
1491
|
+
* labels, the path: the same repository, and the manifests read from inside
|
|
1492
|
+
* the service's directory - or from anywhere in the repository when the
|
|
1493
|
+
* service is the whole of it. An Application that matches nothing is a fact
|
|
1494
|
+
* for the Problems page rather than for a guess.
|
|
1495
|
+
*/
|
|
1496
|
+
export function deploys(deployment: Deployment, service: Service): boolean {
|
|
1497
|
+
if (deployment.service) return deployment.service === service.id;
|
|
1498
|
+
if (!deployment.repo || deployment.repo !== service.repo) return false;
|
|
1499
|
+
const root = service.path.replace(/^\/+|\/+$/g, "");
|
|
1500
|
+
if (!root) return true;
|
|
1501
|
+
return deployment.path === root || deployment.path.startsWith(`${root}/`);
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
/**
|
|
1505
|
+
* The environment a reader would say a deployment stands in: the one the
|
|
1506
|
+
* deployer named, else the cluster, else the one word that says neither was
|
|
1507
|
+
* said. Every picture and filter groups by this, so it is decided once.
|
|
1508
|
+
*/
|
|
1509
|
+
export function environmentOf(deployment: Deployment): string {
|
|
1510
|
+
return deployment.environment || deployment.cluster || "unplaced";
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
/** Where this service runs, in catalog order: the snapshot's, which is by id. */
|
|
1514
|
+
export function deploymentsOf(catalog: Catalog, service: Service): Deployment[] {
|
|
1515
|
+
return allDeployments(catalog).filter((deployment) =>
|
|
1516
|
+
deploys(deployment, service),
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1232
1520
|
/** Who to ask about a service, without the caller having to know the field is optional. */
|
|
1233
1521
|
export function ownersOf(service: Service): string[] {
|
|
1234
1522
|
return service.owners ?? [];
|