@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
package/src/lib/setup-info.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* bundle.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { djangoAggregateCandidates, djangoAggregateMessage } from "./django-aggregates.ts";
|
|
11
|
+
|
|
10
12
|
export type SetupPhase = "extract" | "verify" | "generate";
|
|
11
13
|
|
|
12
14
|
export interface SetupProject {
|
|
@@ -59,6 +61,24 @@ export interface SetupRunStep extends SetupStep {
|
|
|
59
61
|
* fix; a failed step's error stays out, because an error may quote anything.
|
|
60
62
|
*/
|
|
61
63
|
warnings: string[];
|
|
64
|
+
diagnostics: SetupDiagnostic[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type SetupDiagnosticSeverity = "error" | "warning" | "info";
|
|
68
|
+
|
|
69
|
+
export interface SetupDiagnostic {
|
|
70
|
+
aggregateCandidates?: import("./django-aggregates").DjangoAggregateCandidates;
|
|
71
|
+
plugin: string;
|
|
72
|
+
rule: string;
|
|
73
|
+
severity: SetupDiagnosticSeverity;
|
|
74
|
+
action: string;
|
|
75
|
+
message: string;
|
|
76
|
+
count: number;
|
|
77
|
+
project: string;
|
|
78
|
+
phase: SetupPhase;
|
|
79
|
+
ref?: string;
|
|
80
|
+
suppressed: boolean;
|
|
81
|
+
suppressionReason?: string;
|
|
62
82
|
}
|
|
63
83
|
|
|
64
84
|
export interface SetupRun {
|
|
@@ -249,6 +269,10 @@ function runStepFrom(
|
|
|
249
269
|
.filter((warning): warning is string => typeof warning === "string" && warning.trim() !== "")
|
|
250
270
|
.slice(0, MAX_WARNINGS)
|
|
251
271
|
.map((warning) => warning.slice(0, MAX_WARNING_LENGTH));
|
|
272
|
+
const diagnostics = array(item.diagnostics)
|
|
273
|
+
.map((diagnostic) => diagnosticFrom(diagnostic, item.plugin as string, item.phase as SetupPhase))
|
|
274
|
+
.filter((diagnostic): diagnostic is SetupDiagnostic => diagnostic !== null)
|
|
275
|
+
.slice(0, MAX_WARNINGS);
|
|
252
276
|
|
|
253
277
|
return {
|
|
254
278
|
ordinal: item.ordinal,
|
|
@@ -267,6 +291,40 @@ function runStepFrom(
|
|
|
267
291
|
changedCount: item.changedCount,
|
|
268
292
|
files,
|
|
269
293
|
warnings,
|
|
294
|
+
diagnostics,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function diagnosticFrom(value: unknown, plugin: string, phase: SetupPhase): SetupDiagnostic | null {
|
|
299
|
+
const item = record(value);
|
|
300
|
+
if (
|
|
301
|
+
item.plugin !== plugin || item.phase !== phase ||
|
|
302
|
+
typeof item.rule !== "string" || !/^[a-z0-9]+(?:[.-][a-z0-9]+)*$/.test(item.rule) ||
|
|
303
|
+
!["error", "warning", "info"].includes(item.severity as string) ||
|
|
304
|
+
typeof item.action !== "string" || item.action.trim() === "" ||
|
|
305
|
+
typeof item.message !== "string" || item.message.trim() === "" ||
|
|
306
|
+
!wholeNumber(item.count) || item.count < 1 ||
|
|
307
|
+
typeof item.project !== "string" ||
|
|
308
|
+
typeof item.suppressed !== "boolean"
|
|
309
|
+
) return null;
|
|
310
|
+
const ref = typeof item.ref === "string" && item.ref.trim() !== "" ? item.ref.slice(0, MAX_WARNING_LENGTH) : undefined;
|
|
311
|
+
const suppressionReason = typeof item.suppressionReason === "string" && item.suppressionReason.trim() !== ""
|
|
312
|
+
? item.suppressionReason.slice(0, MAX_WARNING_LENGTH)
|
|
313
|
+
: undefined;
|
|
314
|
+
if (item.suppressed && !suppressionReason) return null;
|
|
315
|
+
return {
|
|
316
|
+
plugin,
|
|
317
|
+
rule: item.rule,
|
|
318
|
+
severity: item.severity as SetupDiagnosticSeverity,
|
|
319
|
+
action: item.action.slice(0, MAX_WARNING_LENGTH),
|
|
320
|
+
message: djangoAggregateMessage(item.message).slice(0, MAX_WARNING_LENGTH),
|
|
321
|
+
...(djangoAggregateCandidates(item.message) ? { aggregateCandidates: djangoAggregateCandidates(item.message)! } : {}),
|
|
322
|
+
count: item.count,
|
|
323
|
+
project: item.project.slice(0, MAX_WARNING_LENGTH),
|
|
324
|
+
phase,
|
|
325
|
+
...(ref ? { ref } : {}),
|
|
326
|
+
suppressed: item.suppressed,
|
|
327
|
+
...(suppressionReason ? { suppressionReason } : {}),
|
|
270
328
|
};
|
|
271
329
|
}
|
|
272
330
|
|
package/src/lib/shape.test.ts
CHANGED
|
@@ -211,6 +211,38 @@ describe("schemaChanges", () => {
|
|
|
211
211
|
expect(removed.map((f) => f.name)).toEqual(["gone"]);
|
|
212
212
|
});
|
|
213
213
|
|
|
214
|
+
it("a field whose rules moved is changed, with the rules it had", () => {
|
|
215
|
+
const rules = {
|
|
216
|
+
id: "x.y.R",
|
|
217
|
+
versions: [
|
|
218
|
+
{
|
|
219
|
+
version: "v1",
|
|
220
|
+
fields: [
|
|
221
|
+
{ name: "code", type: "string", doc: "", rules: [{ name: "min_len", value: "8" }] },
|
|
222
|
+
{ name: "who", type: "string", doc: "" },
|
|
223
|
+
{ name: "same", type: "string", doc: "", rules: [{ name: "in", value: "a, b" }, { name: "unique" }] },
|
|
224
|
+
{ name: "both", type: "int32", doc: "", required: true },
|
|
225
|
+
],
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
version: "v2",
|
|
229
|
+
fields: [
|
|
230
|
+
{ name: "code", type: "string", doc: "", rules: [{ name: "min_len", value: "12" }] },
|
|
231
|
+
{ name: "who", type: "string", doc: "", required: true, rules: [{ name: "format", value: "uuid" }] },
|
|
232
|
+
// The same rules in another order are the same rules.
|
|
233
|
+
{ name: "same", type: "string", doc: "", rules: [{ name: "unique" }, { name: "in", value: "a, b" }] },
|
|
234
|
+
{ name: "both", type: "int64", doc: "" },
|
|
235
|
+
],
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
} as unknown as Event;
|
|
239
|
+
const { byField } = schemaChanges(rules, "v2");
|
|
240
|
+
expect(byField.get("code")).toEqual({ change: "changed", rulesFrom: "≥ 8 chars" });
|
|
241
|
+
expect(byField.get("who")).toEqual({ change: "changed", rulesFrom: "" });
|
|
242
|
+
expect(byField.get("same")).toBeUndefined();
|
|
243
|
+
expect(byField.get("both")).toEqual({ change: "changed", from: "int32", rulesFrom: "required" });
|
|
244
|
+
});
|
|
245
|
+
|
|
214
246
|
it("agrees with the fixture: OrderPlaced v2 added channel", () => {
|
|
215
247
|
const placed = eventById("shop.oms.order.OrderPlaced");
|
|
216
248
|
const { byField, removed } = schemaChanges(placed, "v2");
|
package/src/lib/shape.ts
CHANGED
|
@@ -26,6 +26,7 @@ import type {
|
|
|
26
26
|
} from "../catalog";
|
|
27
27
|
import { aggregateBlocks, blockFields, enumsOf } from "../catalog";
|
|
28
28
|
import type { DefUsage } from "./derive";
|
|
29
|
+
import { ruleMarks } from "./rules";
|
|
29
30
|
|
|
30
31
|
/** How many of the base type a field holds. */
|
|
31
32
|
export type Cardinality = "one" | "many" | "map";
|
|
@@ -336,13 +337,19 @@ export interface FieldChange {
|
|
|
336
337
|
change: Change;
|
|
337
338
|
/** The type the field had in the previous version, when it changed. */
|
|
338
339
|
from?: string;
|
|
340
|
+
/**
|
|
341
|
+
* The rules the field had in the previous version, as the marks read -
|
|
342
|
+
* "required, ≤ 8 chars" - when this version changed them; "" when it had
|
|
343
|
+
* none. The row itself shows what they are now.
|
|
344
|
+
*/
|
|
345
|
+
rulesFrom?: string;
|
|
339
346
|
}
|
|
340
347
|
|
|
341
348
|
/**
|
|
342
349
|
* What a version did to the schema: the fields it added, the ones whose
|
|
343
|
-
* type it changed, and - by name, since they are no longer in the
|
|
344
|
-
* own list - the ones it dropped. The first version changes
|
|
345
|
-
* is nothing before it to differ from.
|
|
350
|
+
* type or rules it changed, and - by name, since they are no longer in the
|
|
351
|
+
* version's own list - the ones it dropped. The first version changes
|
|
352
|
+
* nothing: there is nothing before it to differ from.
|
|
346
353
|
*/
|
|
347
354
|
export function schemaChanges(
|
|
348
355
|
event: Event,
|
|
@@ -360,9 +367,14 @@ export function schemaChanges(
|
|
|
360
367
|
const now = new Set(current.fields.map((f) => f.name));
|
|
361
368
|
for (const field of current.fields) {
|
|
362
369
|
const was = before.get(field.name);
|
|
363
|
-
if (!was)
|
|
364
|
-
|
|
365
|
-
|
|
370
|
+
if (!was) {
|
|
371
|
+
byField.set(field.name, { change: "new" });
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
const change: FieldChange = { change: "changed" };
|
|
375
|
+
if (was.type !== field.type) change.from = was.type;
|
|
376
|
+
if (!sameRules(was, field)) change.rulesFrom = ruleMarks(was).map((m) => m.text).join(", ");
|
|
377
|
+
if (change.from !== undefined || change.rulesFrom !== undefined) byField.set(field.name, change);
|
|
366
378
|
}
|
|
367
379
|
for (const field of prev.fields) {
|
|
368
380
|
if (!now.has(field.name)) removed.push(field);
|
|
@@ -370,6 +382,18 @@ export function schemaChanges(
|
|
|
370
382
|
return { byField, removed };
|
|
371
383
|
}
|
|
372
384
|
|
|
385
|
+
/**
|
|
386
|
+
* Whether two versions of a field ask the same of its value. The order the
|
|
387
|
+
* rules are listed in is the source's, not a fact about the value, so it
|
|
388
|
+
* is not a difference.
|
|
389
|
+
*/
|
|
390
|
+
function sameRules(a: Field, b: Field): boolean {
|
|
391
|
+
if (Boolean(a.required) !== Boolean(b.required)) return false;
|
|
392
|
+
const key = (field: Field) =>
|
|
393
|
+
(field.rules ?? []).map((r) => `${r.name}=${r.value ?? ""}`).sort().join("\n");
|
|
394
|
+
return key(a) === key(b);
|
|
395
|
+
}
|
|
396
|
+
|
|
373
397
|
// ---------------------------------------------------------------------------
|
|
374
398
|
// An enum and a lifecycle.
|
|
375
399
|
// ---------------------------------------------------------------------------
|
package/src/lib/tech.ts
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
siApachepulsar,
|
|
31
31
|
siArgo,
|
|
32
32
|
siAuth0,
|
|
33
|
+
siBackstage,
|
|
33
34
|
siBun,
|
|
34
35
|
siC,
|
|
35
36
|
siCelery,
|
|
@@ -37,6 +38,7 @@ import {
|
|
|
37
38
|
siClickhouse,
|
|
38
39
|
siClojure,
|
|
39
40
|
siCloudflare,
|
|
41
|
+
siConfluence,
|
|
40
42
|
siCockroachlabs,
|
|
41
43
|
siConsul,
|
|
42
44
|
siCouchbase,
|
|
@@ -93,7 +95,9 @@ import {
|
|
|
93
95
|
siLinkerd,
|
|
94
96
|
siLua,
|
|
95
97
|
siMariadb,
|
|
98
|
+
siMarkdown,
|
|
96
99
|
siMeilisearch,
|
|
100
|
+
siMermaid,
|
|
97
101
|
siMinio,
|
|
98
102
|
siMongodb,
|
|
99
103
|
siMqtt,
|
|
@@ -107,6 +111,7 @@ import {
|
|
|
107
111
|
siNginx,
|
|
108
112
|
siNodedotjs,
|
|
109
113
|
siNomad,
|
|
114
|
+
siNotion,
|
|
110
115
|
siNpm,
|
|
111
116
|
siOllama,
|
|
112
117
|
siOpenapiinitiative,
|
|
@@ -346,6 +351,11 @@ export const TECH_MARKS: readonly TechMark[] = [
|
|
|
346
351
|
mark(siJenkins),
|
|
347
352
|
mark(siArgo, "Argo CD", "ArgoCD"),
|
|
348
353
|
|
|
354
|
+
// Where a team writes. Not read off a manifest; the marks are for the
|
|
355
|
+
// reader-side integrations that open the wiki from a page.
|
|
356
|
+
mark(siConfluence),
|
|
357
|
+
mark(siNotion),
|
|
358
|
+
|
|
349
359
|
// Observability.
|
|
350
360
|
mark(siOpentelemetry, "otel"),
|
|
351
361
|
mark(siPrometheus),
|
|
@@ -374,6 +384,11 @@ export const TECH_MARKS: readonly TechMark[] = [
|
|
|
374
384
|
mark(siJest),
|
|
375
385
|
mark(siVitest),
|
|
376
386
|
mark(siCypress),
|
|
387
|
+
|
|
388
|
+
// What the generators write to.
|
|
389
|
+
mark(siMarkdown),
|
|
390
|
+
mark(siMermaid),
|
|
391
|
+
mark(siBackstage),
|
|
377
392
|
];
|
|
378
393
|
|
|
379
394
|
const BY_KEY: ReadonlyMap<string, TechGlyph> = (() => {
|
|
@@ -417,5 +432,6 @@ export const STORE_KIND_GLYPH: Record<StoreKind, TechGlyph | null> = {
|
|
|
417
432
|
clickhouse: techGlyph("clickhouse"),
|
|
418
433
|
// simple-icons was asked to drop Amazon's marks, S3's among them.
|
|
419
434
|
s3: null,
|
|
435
|
+
dynamodb: null,
|
|
420
436
|
other: null,
|
|
421
437
|
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { projectForFlow, projectsForFlow } from "./trace-project";
|
|
4
|
+
|
|
5
|
+
const projects = [
|
|
6
|
+
{ id: "auth", name: "Auth", root: "examples/auth", context: "auth", service: "auth" },
|
|
7
|
+
{ id: "shop-cart", name: "Cart", root: "examples/shop/cart", context: "shop", service: "cart" },
|
|
8
|
+
{ id: "shop-oms", name: "OMS", root: "examples/shop/oms", group: "shop", component: "oms" },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const flow = (owner: string, ...services: string[]) => ({
|
|
12
|
+
owner,
|
|
13
|
+
participants: [
|
|
14
|
+
{ id: "client", kind: "actor" as const, context: null },
|
|
15
|
+
...services.map((id) => ({ id, kind: "service" as const, context: id.split(".")[0] ?? null })),
|
|
16
|
+
{ id: "bus", kind: "broker" as const, context: null },
|
|
17
|
+
],
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("which project a recording of a flow belongs to", () => {
|
|
21
|
+
it("is the one project in the flow's context", () => {
|
|
22
|
+
expect(projectForFlow(projects, flow("auth", "auth.auth"))?.id).toBe("auth");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("is the project whose service the flow runs through when the context has several", () => {
|
|
26
|
+
expect(projectForFlow(projects, flow("shop", "shop.oms", "shop.pricing"))?.id).toBe("shop-oms");
|
|
27
|
+
expect(projectsForFlow(projects, flow("shop", "shop.cart")).map((p) => p.id)).toEqual(["shop-cart", "shop-oms"]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("is nobody's when the context has no project, or two the flow runs through", () => {
|
|
31
|
+
expect(projectForFlow(projects, flow("payments", "payments.ledger"))).toBeNull();
|
|
32
|
+
expect(projectForFlow(projects, flow("shop", "shop.cart", "shop.oms"))).toBeNull();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Which project a recording of a flow belongs to.
|
|
2
|
+
//
|
|
3
|
+
// A recording is kept beside the project whose verify step reads it, and a
|
|
4
|
+
// flow says which context owns it - one level up from a project. The project
|
|
5
|
+
// is the one in that context whose service the flow runs through; when the
|
|
6
|
+
// context has one project, that one; otherwise the page has to ask.
|
|
7
|
+
|
|
8
|
+
import type { Flow } from "../catalog";
|
|
9
|
+
import type { SetupProject } from "./setup-info";
|
|
10
|
+
|
|
11
|
+
/** The projects a flow's recording could be kept under, the likeliest first. */
|
|
12
|
+
export function projectsForFlow(
|
|
13
|
+
projects: readonly SetupProject[],
|
|
14
|
+
flow: Pick<Flow, "owner" | "participants">,
|
|
15
|
+
): SetupProject[] {
|
|
16
|
+
const inContext = projects.filter(
|
|
17
|
+
(project) => (project.group ?? project.context) === flow.owner,
|
|
18
|
+
);
|
|
19
|
+
const services = new Set(
|
|
20
|
+
flow.participants
|
|
21
|
+
.filter((p) => p.kind === "service" && p.context === flow.owner)
|
|
22
|
+
.map((p) => p.id.split(".").slice(1).join(".")),
|
|
23
|
+
);
|
|
24
|
+
const runsThrough = (project: SetupProject) =>
|
|
25
|
+
services.has(project.component ?? project.service ?? "");
|
|
26
|
+
|
|
27
|
+
return [...inContext].sort(
|
|
28
|
+
(a, b) => Number(runsThrough(b)) - Number(runsThrough(a)) || a.id.localeCompare(b.id),
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The one project a recording of the flow goes to, or null when it has to be asked. */
|
|
33
|
+
export function projectForFlow(
|
|
34
|
+
projects: readonly SetupProject[],
|
|
35
|
+
flow: Pick<Flow, "owner" | "participants">,
|
|
36
|
+
): SetupProject | null {
|
|
37
|
+
const candidates = projectsForFlow(projects, flow);
|
|
38
|
+
if (candidates.length === 1) return candidates[0]!;
|
|
39
|
+
if (candidates.length === 0) return null;
|
|
40
|
+
const services = new Set(
|
|
41
|
+
flow.participants
|
|
42
|
+
.filter((p) => p.kind === "service" && p.context === flow.owner)
|
|
43
|
+
.map((p) => p.id.split(".").slice(1).join(".")),
|
|
44
|
+
);
|
|
45
|
+
const through = candidates.filter((project) =>
|
|
46
|
+
services.has(project.component ?? project.service ?? ""),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
return through.length === 1 ? through[0]! : null;
|
|
50
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// The problems of the live catalog, for a component.
|
|
2
|
+
//
|
|
3
|
+
// Five places show the same list - the page, the sidebar badge, the overview,
|
|
4
|
+
// the landing's demo, the rules table - and each used to compute it once on
|
|
5
|
+
// mount. Now the list depends on the rules in force, which the Settings page
|
|
6
|
+
// changes under a running app, so the computation subscribes to them and the
|
|
7
|
+
// five places re-render together when a switch is flipped.
|
|
8
|
+
|
|
9
|
+
import { useMemo } from "react";
|
|
10
|
+
import { catalog, index } from "../data";
|
|
11
|
+
import { evaluateProblems } from "./all-problems";
|
|
12
|
+
import type { Problem } from "./derive";
|
|
13
|
+
import { useProblemRules } from "./problem-rules";
|
|
14
|
+
import type { RuleEvaluation } from "./problem-rules";
|
|
15
|
+
|
|
16
|
+
export function useProblemEvaluation(): RuleEvaluation {
|
|
17
|
+
const rules = useProblemRules();
|
|
18
|
+
return useMemo(() => evaluateProblems(catalog, index, rules), [rules]);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function useProblems(): Problem[] {
|
|
22
|
+
return useProblemEvaluation().problems;
|
|
23
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { groupDiagnostics, groupWarnings, warningDiagnostic } from "./warnings";
|
|
3
|
+
|
|
4
|
+
describe("warning diagnostics", () => {
|
|
5
|
+
it("assigns a stable rule, severity and action to known warnings", () => {
|
|
6
|
+
const diagnostic = warningDiagnostic({
|
|
7
|
+
plugin: "openapi",
|
|
8
|
+
message: "aviacore-api: no operationId on 2 of 9 operations; listed by verb and path: POST /book, POST /cancel",
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
expect(diagnostic).toMatchObject({
|
|
12
|
+
plugin: "openapi",
|
|
13
|
+
rule: "openapi.missing-operation-id",
|
|
14
|
+
severity: "warning",
|
|
15
|
+
ref: "aviacore-api",
|
|
16
|
+
suppressed: false,
|
|
17
|
+
});
|
|
18
|
+
expect(diagnostic.action).toContain("operationId");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("groups repetitions by plugin, rule and severity", () => {
|
|
22
|
+
const groups = groupWarnings([
|
|
23
|
+
{ plugin: "openapi", message: "api.v1: no operationId on 1 of 3 operations; listed by verb and path: POST /book" },
|
|
24
|
+
{ plugin: "openapi", message: "api.v2: no operationId on 1 of 3 operations; listed by verb and path: POST /cancel" },
|
|
25
|
+
{ plugin: "wsdl", message: "b/schema.xsd: duplicate declaration CodeType and 2 more in namespace urn:common; the declarations in a/schema.xsd are used" },
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
expect(groups.map(({ plugin, rule, count }) => ({ plugin, rule, count }))).toEqual([
|
|
29
|
+
{ plugin: "openapi", rule: "openapi.missing-operation-id", count: 2 },
|
|
30
|
+
{ plugin: "wsdl", rule: "schema.duplicate-declaration", count: 1 },
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("keeps one rule across the wordings the plugins emit for it", () => {
|
|
35
|
+
const rules = [
|
|
36
|
+
"calls ledger.v1 and the manifest names no peer for it; add it under `peers` to say which service answers, until then the calls are unresolved",
|
|
37
|
+
"src/app.rs: port `clock: Clock` is neither a domain port, a use case nor a client; its calls are left out of the flow",
|
|
38
|
+
"bus/router.go:12:3: Watermill handler orders is registered on topic `cfg.Topic`, which this reader cannot resolve to a literal, a constant, a config default or a caller's argument; the handler is kept with its topic unresolved",
|
|
39
|
+
].map((message) => warningDiagnostic({ plugin: "x", message }).rule);
|
|
40
|
+
expect(rules).toEqual(["catalog.unmapped-proto-peer", "flow.unknown-port", "watermill.unresolved-topic"]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("marks read and parse failures as errors and expected absence as info", () => {
|
|
44
|
+
expect(warningDiagnostic({ plugin: "sql", message: "schema.sql could not be parsed" }).severity).toBe("error");
|
|
45
|
+
expect(warningDiagnostic({ plugin: "river", message: "root: no River jobs were found" }).severity).toBe("info");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("groups persisted policy decisions without evaluating policies in the browser", () => {
|
|
49
|
+
const diagnostic = warningDiagnostic({ plugin: "openapi", message: "api: no operationId on 1 of 3 operations; listed by verb and path: POST /book" });
|
|
50
|
+
const groups = groupDiagnostics([{ ...diagnostic, suppressed: true, suppressionReason: "Owned upstream." }]);
|
|
51
|
+
expect(groups[0]).toMatchObject({ suppressed: true, suppressionReason: "Owned upstream.", count: 1 });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("keeps unrelated unknown warnings in separate fallback rules", () => {
|
|
55
|
+
const groups = groupWarnings([
|
|
56
|
+
{ plugin: "custom", message: "service: frobnicator needs attention" },
|
|
57
|
+
{ plugin: "custom", message: "service: another limitation was observed" },
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
expect(groups).toHaveLength(2);
|
|
61
|
+
expect(groups.every((group) => group.rule.startsWith("plugin.custom.other-"))).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
});
|