@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
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, utimesSync, 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
|
+
|
|
6
|
+
import { newestMtime, pluginsFresh } from "./plugins-fresh.mjs";
|
|
7
|
+
|
|
8
|
+
const roots = [];
|
|
9
|
+
afterEach(() => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); });
|
|
10
|
+
|
|
11
|
+
function at(path, seconds) {
|
|
12
|
+
utimesSync(path, seconds, seconds);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** A workspace with a built wasm and Java classes, all stamped at `built`, over sources stamped at `source`. */
|
|
16
|
+
function workspace({ source, built }) {
|
|
17
|
+
const root = mkdtempSync(join(tmpdir(), "portolan-fresh-"));
|
|
18
|
+
roots.push(root);
|
|
19
|
+
for (const dir of ["plugins/verify-otel", "plugins/extract-java/src/org", "plugins/extract-java/build/org", "catalog", "plugin"]) mkdirSync(join(root, dir), { recursive: true });
|
|
20
|
+
const files = {
|
|
21
|
+
"go.mod": source, "plugins/verify-otel/verify.go": source, "catalog/model.go": source, "plugin/protocol.go": source,
|
|
22
|
+
"plugins/extract-java/src/org/Main.java": source,
|
|
23
|
+
"plugins/portolan-go.wasm": built, "plugins/extract-java/build/org/Main.class": built,
|
|
24
|
+
};
|
|
25
|
+
for (const [name, stamp] of Object.entries(files)) {
|
|
26
|
+
writeFileSync(join(root, name), "x");
|
|
27
|
+
at(join(root, name), stamp);
|
|
28
|
+
}
|
|
29
|
+
return root;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("whether the built plugins are newer than their sources", () => {
|
|
33
|
+
it("is yes when every artefact is at least as new as every source", () => {
|
|
34
|
+
expect(pluginsFresh(workspace({ source: 1000, built: 2000 }))).toBe(true);
|
|
35
|
+
expect(pluginsFresh(workspace({ source: 2000, built: 2000 }))).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("is no when a Go source, a Java source, or go.mod is newer, or an artefact is missing", () => {
|
|
39
|
+
const go = workspace({ source: 1000, built: 2000 });
|
|
40
|
+
at(join(go, "catalog/model.go"), 3000);
|
|
41
|
+
expect(pluginsFresh(go)).toBe(false);
|
|
42
|
+
|
|
43
|
+
const java = workspace({ source: 1000, built: 2000 });
|
|
44
|
+
at(join(java, "plugins/extract-java/src/org/Main.java"), 3000);
|
|
45
|
+
expect(pluginsFresh(java)).toBe(false);
|
|
46
|
+
|
|
47
|
+
const mod = workspace({ source: 1000, built: 2000 });
|
|
48
|
+
at(join(mod, "go.mod"), 3000);
|
|
49
|
+
expect(pluginsFresh(mod)).toBe(false);
|
|
50
|
+
|
|
51
|
+
const missing = workspace({ source: 1000, built: 2000 });
|
|
52
|
+
rmSync(join(missing, "plugins/portolan-go.wasm"));
|
|
53
|
+
expect(pluginsFresh(missing)).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("leaves a plugin's own build output and test data out of the sources", () => {
|
|
57
|
+
const root = workspace({ source: 1000, built: 2000 });
|
|
58
|
+
mkdirSync(join(root, "plugins/extract-rust/target"), { recursive: true });
|
|
59
|
+
writeFileSync(join(root, "plugins/extract-rust/target/out.go"), "x");
|
|
60
|
+
at(join(root, "plugins/extract-rust/target/out.go"), 5000);
|
|
61
|
+
expect(pluginsFresh(root)).toBe(true);
|
|
62
|
+
expect(newestMtime(join(root, "plugins"), (path) => path.endsWith(".go"))).toBe(1000 * 1000);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Where each catalog source last changed, for the site's stamp.
|
|
2
|
+
//
|
|
3
|
+
// A fragment carries no provenance of its own (portolan.0010); the history of
|
|
4
|
+
// the checkout does. The browser cannot ask git, so this Vite plugin asks at
|
|
5
|
+
// build time - and again under the dev server whenever a source changes -
|
|
6
|
+
// and answers with one virtual module, keyed by the path `src/data.ts` gives
|
|
7
|
+
// a source. Nothing watches `.git`: a commit made while the dev server runs
|
|
8
|
+
// shows after a restart, or after the next change to a source.
|
|
9
|
+
|
|
10
|
+
import { existsSync, globSync, readFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
|
|
13
|
+
import { forgetHistory, stampsFor } from "./history.mjs";
|
|
14
|
+
import { readManifest } from "./manifest.mjs";
|
|
15
|
+
|
|
16
|
+
export const PROVENANCE_MODULE = "virtual:portolan-provenance";
|
|
17
|
+
const RESOLVED = `\0${PROVENANCE_MODULE}`;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The stamp of every source the manifest's patterns find under `workspace`,
|
|
21
|
+
* keyed by its path as the manifest spells it.
|
|
22
|
+
*
|
|
23
|
+
* A staged site (`portolan dev`, `portolan build`) imports its sources under
|
|
24
|
+
* flattened names, `portolan/source-0007.json`, and keeps the way back in
|
|
25
|
+
* `.portolan/source-paths.json`. Such a stamp is keyed by the staged name the
|
|
26
|
+
* browser imports and carries `source`, the path the file has in the
|
|
27
|
+
* workspace - the one a project's root is a prefix of, and the one a link to
|
|
28
|
+
* the file has to name. Without it the site would know its sources only by
|
|
29
|
+
* names nobody typed.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} workspace
|
|
32
|
+
* @param {string} siteRoot Vite's root, which may contain flattened sources
|
|
33
|
+
* @returns {Record<string, {commit: string, generatedAt: string, source?: string}>}
|
|
34
|
+
*/
|
|
35
|
+
export function provenance(workspace, siteRoot = workspace) {
|
|
36
|
+
const sourceMap = join(siteRoot, ".portolan/source-paths.json");
|
|
37
|
+
if (existsSync(sourceMap)) {
|
|
38
|
+
const paths = JSON.parse(readFileSync(sourceMap, "utf8"));
|
|
39
|
+
const stamps = stampsFor(workspace, Object.values(paths));
|
|
40
|
+
return Object.fromEntries(Object.entries(paths).map(([staged, source]) => [staged, { ...stamps.get(source), source }]));
|
|
41
|
+
}
|
|
42
|
+
const manifest = readManifest(join(workspace, "portolan.json"));
|
|
43
|
+
const paths = [];
|
|
44
|
+
for (const pattern of manifest.sources ?? []) {
|
|
45
|
+
for (const path of globSync(pattern, { cwd: workspace })) paths.push(path.split("\\").join("/"));
|
|
46
|
+
}
|
|
47
|
+
return Object.fromEntries(stampsFor(workspace, paths.sort()));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** @param {string} workspace */
|
|
51
|
+
export function provenancePlugin(workspace) {
|
|
52
|
+
let siteRoot = workspace;
|
|
53
|
+
return {
|
|
54
|
+
name: "portolan-provenance",
|
|
55
|
+
configResolved(config) {
|
|
56
|
+
siteRoot = config.root;
|
|
57
|
+
},
|
|
58
|
+
resolveId(id) {
|
|
59
|
+
return id === PROVENANCE_MODULE ? RESOLVED : undefined;
|
|
60
|
+
},
|
|
61
|
+
load(id) {
|
|
62
|
+
if (id !== RESOLVED) return undefined;
|
|
63
|
+
return `export default ${JSON.stringify(provenance(workspace, siteRoot))};\n`;
|
|
64
|
+
},
|
|
65
|
+
handleHotUpdate({ file, server }) {
|
|
66
|
+
if (!file.endsWith(".json") || file.includes("/node_modules/")) return;
|
|
67
|
+
forgetHistory();
|
|
68
|
+
const mod = server.moduleGraph.getModuleById(RESOLVED);
|
|
69
|
+
if (mod) server.moduleGraph.invalidateModule(mod);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// The provenance of a source is the history's (portolan.0010): the commit
|
|
2
|
+
// that last changed the file, read off the checkout and never written in.
|
|
3
|
+
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
9
|
+
|
|
10
|
+
import { changedSince, forgetHistory, lastCommitTouching, stampsFor } from "./history.mjs";
|
|
11
|
+
import { provenance } from "./provenance.mjs";
|
|
12
|
+
|
|
13
|
+
const created = [];
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
forgetHistory();
|
|
16
|
+
for (const path of created.splice(0)) rmSync(path, { recursive: true, force: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const ADA = { GIT_AUTHOR_NAME: "Ada Lovelace", GIT_AUTHOR_EMAIL: "ada@example.com", GIT_COMMITTER_NAME: "Ada Lovelace", GIT_COMMITTER_EMAIL: "ada@example.com" };
|
|
20
|
+
|
|
21
|
+
function repository() {
|
|
22
|
+
const root = mkdtempSync(join(tmpdir(), "portolan-provenance-"));
|
|
23
|
+
created.push(root);
|
|
24
|
+
const git = (args, date = "2026-01-01T09:00:00Z") =>
|
|
25
|
+
execFileSync("git", ["-C", root, ...args], {
|
|
26
|
+
encoding: "utf8",
|
|
27
|
+
env: { ...process.env, ...ADA, GIT_AUTHOR_DATE: date, GIT_COMMITTER_DATE: date },
|
|
28
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
29
|
+
}).trim();
|
|
30
|
+
const write = (name, contents) => {
|
|
31
|
+
mkdirSync(join(root, name, ".."), { recursive: true });
|
|
32
|
+
writeFileSync(join(root, name), contents);
|
|
33
|
+
};
|
|
34
|
+
git(["init", "-q"]);
|
|
35
|
+
return { root, git, write };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function elsewhere() {
|
|
39
|
+
const dir = mkdtempSync(join(tmpdir(), "portolan-no-repo-"));
|
|
40
|
+
created.push(dir);
|
|
41
|
+
return dir;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe("stampsFor", () => {
|
|
45
|
+
// The stamp is the commit the change landed in, which is what a stamp
|
|
46
|
+
// written into the file could never say.
|
|
47
|
+
it("dates a source by the commit that last changed it", () => {
|
|
48
|
+
const { root, git, write } = repository();
|
|
49
|
+
write("portolan/domain.json", "{}\n");
|
|
50
|
+
write("portolan/api.json", "{}\n");
|
|
51
|
+
git(["add", "."]);
|
|
52
|
+
git(["commit", "-q", "-m", "both"], "2026-01-01T09:00:00Z");
|
|
53
|
+
write("portolan/domain.json", '{"changed":true}\n');
|
|
54
|
+
git(["add", "."]);
|
|
55
|
+
git(["commit", "-q", "-m", "domain again"], "2026-02-02T10:00:00Z");
|
|
56
|
+
const second = git(["rev-parse", "--short=7", "HEAD"]);
|
|
57
|
+
const first = git(["rev-parse", "--short=7", "HEAD~1"]);
|
|
58
|
+
|
|
59
|
+
const stamps = stampsFor(root, ["portolan/domain.json", "portolan/api.json"]);
|
|
60
|
+
expect(stamps.get("portolan/domain.json")).toEqual({ commit: second, generatedAt: "2026-02-02T10:00:00Z" });
|
|
61
|
+
expect(stamps.get("portolan/api.json")).toEqual({ commit: first, generatedAt: "2026-01-01T09:00:00Z" });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("calls a modified, an untracked and an unversioned file uncommitted, dated by the working tree", () => {
|
|
65
|
+
const { root, git, write } = repository();
|
|
66
|
+
write("portolan/domain.json", "{}\n");
|
|
67
|
+
git(["add", "."]);
|
|
68
|
+
git(["commit", "-q", "-m", "one"]);
|
|
69
|
+
write("portolan/domain.json", '{"dirty":true}\n');
|
|
70
|
+
write("portolan/new.json", "{}\n");
|
|
71
|
+
const outside = elsewhere();
|
|
72
|
+
writeFileSync(join(outside, "authored.json"), "{}\n");
|
|
73
|
+
|
|
74
|
+
const stamps = stampsFor(root, ["portolan/domain.json", "portolan/new.json", join(outside, "authored.json")]);
|
|
75
|
+
expect(stamps.size).toBe(3);
|
|
76
|
+
for (const stamp of stamps.values()) {
|
|
77
|
+
expect(stamp.commit).toBe("uncommitted");
|
|
78
|
+
expect(Number.isNaN(Date.parse(stamp.generatedAt))).toBe(false);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("what changed since the output was last committed", () => {
|
|
84
|
+
it("lists the inputs that moved, working tree included, and leaves the output directory out", () => {
|
|
85
|
+
const { root, git, write } = repository();
|
|
86
|
+
write("svc/main.go", "package main\n");
|
|
87
|
+
write("svc/go.mod", "module svc\n");
|
|
88
|
+
write("svc/portolan/domain.json", "{}\n");
|
|
89
|
+
git(["add", "."]);
|
|
90
|
+
git(["commit", "-q", "-m", "generated"], "2026-01-01T09:00:00Z");
|
|
91
|
+
const generated = git(["rev-parse", "--short=7", "HEAD"]);
|
|
92
|
+
write("svc/main.go", "package main // changed\n");
|
|
93
|
+
git(["add", "."]);
|
|
94
|
+
git(["commit", "-q", "-m", "source moved"], "2026-02-02T10:00:00Z");
|
|
95
|
+
write("svc/go.mod", "module svc // dirty\n");
|
|
96
|
+
write("svc/new.go", "package main\n");
|
|
97
|
+
write("svc/portolan/domain.json", '{"regenerated":true}\n');
|
|
98
|
+
|
|
99
|
+
expect(lastCommitTouching(root, ["svc/portolan/domain.json"])).toEqual({ commit: generated, date: "2026-01-01T09:00:00Z" });
|
|
100
|
+
expect(changedSince(root, generated, ["svc"], ["svc/portolan"])).toEqual(["svc/go.mod", "svc/main.go", "svc/new.go"]);
|
|
101
|
+
expect(changedSince(root, generated, ["svc"])).toContain("svc/portolan/domain.json");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("answers null for an output nobody has committed, and nothing outside a repository", () => {
|
|
105
|
+
const { root, write } = repository();
|
|
106
|
+
write("svc/portolan/domain.json", "{}\n");
|
|
107
|
+
expect(lastCommitTouching(root, ["svc/portolan/domain.json"])).toBeNull();
|
|
108
|
+
const outside = elsewhere();
|
|
109
|
+
expect(lastCommitTouching(outside, ["x.json"])).toBeNull();
|
|
110
|
+
expect(changedSince(outside, "HEAD", ["."])).toEqual([]);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("the site's provenance module", () => {
|
|
115
|
+
it("stamps every source the manifest's patterns find, keyed as the manifest spells them", () => {
|
|
116
|
+
const { root, git, write } = repository();
|
|
117
|
+
write("portolan.json", JSON.stringify({ sources: ["portolan/*.json", "services/*/portolan/*.json"] }));
|
|
118
|
+
write("portolan/host.json", "{}\n");
|
|
119
|
+
write("services/oms/portolan/domain.json", "{}\n");
|
|
120
|
+
git(["add", "."]);
|
|
121
|
+
git(["commit", "-q", "-m", "one"], "2026-03-03T03:00:00Z");
|
|
122
|
+
const head = git(["rev-parse", "--short=7", "HEAD"]);
|
|
123
|
+
|
|
124
|
+
expect(provenance(root)).toEqual({
|
|
125
|
+
"portolan/host.json": { commit: head, generatedAt: "2026-03-03T03:00:00Z" },
|
|
126
|
+
"services/oms/portolan/domain.json": { commit: head, generatedAt: "2026-03-03T03:00:00Z" },
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// A staged site imports flattened copies. The stamp is the workspace file's,
|
|
131
|
+
// keyed by the name the browser imports, and it says which file that was -
|
|
132
|
+
// the path a project's root is a prefix of.
|
|
133
|
+
it("keys a staged site's sources by their flattened names and says where each came from", () => {
|
|
134
|
+
const { root, git, write } = repository();
|
|
135
|
+
write("portolan.json", JSON.stringify({ sources: ["services/*/portolan/*.json"] }));
|
|
136
|
+
write("services/oms/portolan/domain.json", "{}\n");
|
|
137
|
+
git(["add", "."]);
|
|
138
|
+
git(["commit", "-q", "-m", "one"], "2026-03-03T03:00:00Z");
|
|
139
|
+
const head = git(["rev-parse", "--short=7", "HEAD"]);
|
|
140
|
+
const stage = join(root, ".portolan", "site");
|
|
141
|
+
write(".portolan/site/.portolan/source-paths.json", JSON.stringify({
|
|
142
|
+
"portolan/source-0001.json": "services/oms/portolan/domain.json",
|
|
143
|
+
}));
|
|
144
|
+
|
|
145
|
+
expect(provenance(root, stage)).toEqual({
|
|
146
|
+
"portolan/source-0001.json": { commit: head, generatedAt: "2026-03-03T03:00:00Z", source: "services/oms/portolan/domain.json" },
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
});
|
package/scripts/run-builtin.mjs
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
// Process adapter for plugins bundled in the npm package. It keeps the
|
|
4
4
|
// plugin's cwd in the user's workspace while resolving source and toolchain
|
|
5
|
-
// files from the installation.
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// files from the installation. Most Go plugins are one wasm module
|
|
6
|
+
// (portolan.0006). A Go plugin that needs the project toolchain is compiled to
|
|
7
|
+
// a workspace-local sidecar here, then run with the user's workspace as cwd.
|
|
8
8
|
|
|
9
|
-
import { spawn } from "node:child_process";
|
|
10
|
-
import { existsSync } from "node:fs";
|
|
9
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
10
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
11
11
|
import { dirname, resolve } from "node:path";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
|
|
@@ -27,8 +27,11 @@ if (!plugin?.process) {
|
|
|
27
27
|
const command = plugin.process.command;
|
|
28
28
|
const args = [...(plugin.process.args ?? [])];
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
const prebuilt = command === "cargo" ? prebuiltCargoBinary(args) : "";
|
|
31
|
+
if (prebuilt) {
|
|
32
|
+
run(prebuilt, []);
|
|
33
|
+
} else if (command === "go") {
|
|
34
|
+
buildAndRunGo(args);
|
|
32
35
|
} else {
|
|
33
36
|
if (command === "cargo") {
|
|
34
37
|
process.env.CARGO_TARGET_DIR = resolve(workspace, ".portolan", "bin", "cargo");
|
|
@@ -40,9 +43,61 @@ if (command === "cargo" && existsSync(resolve(installRoot, "plugins/extract-rust
|
|
|
40
43
|
run(command, resolved);
|
|
41
44
|
}
|
|
42
45
|
|
|
46
|
+
// `go run` changes module resolution with the current directory, but the
|
|
47
|
+
// analyzer must keep the scanned workspace as its cwd. Build from Portolan's
|
|
48
|
+
// shipped module first, outside the workspace, and execute the resulting
|
|
49
|
+
// native sidecar from the workspace. The Go build cache makes subsequent
|
|
50
|
+
// describe/extract calls cheap; rebuilding also prevents a stale sidecar after
|
|
51
|
+
// an npm upgrade with the same workspace cache.
|
|
52
|
+
function buildAndRunGo(argv) {
|
|
53
|
+
// `go run [build flags] <package> [args]`: the build flags (-mod=mod, -tags)
|
|
54
|
+
// belong to the build, the package is what is built, the rest is the
|
|
55
|
+
// sidecar's own command line.
|
|
56
|
+
const flags = [];
|
|
57
|
+
let at = 1;
|
|
58
|
+
while (argv[0] === "run" && typeof argv[at] === "string" && argv[at].startsWith("-")) flags.push(argv[at++]);
|
|
59
|
+
const pkg = argv[at];
|
|
60
|
+
if (argv[0] !== "run" || typeof pkg !== "string" || !pkg) {
|
|
61
|
+
console.error(`portolan: built-in ${name} has an unsupported Go command`);
|
|
62
|
+
process.exitCode = 2;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const binDir = resolve(workspace, ".portolan", "bin", "go");
|
|
66
|
+
const executable = resolve(binDir, process.platform === "win32" ? `${name}.exe` : name);
|
|
67
|
+
mkdirSync(binDir, { recursive: true });
|
|
68
|
+
const built = spawnSync("go", ["build", "-mod=mod", ...flags.filter((flag) => flag !== "-mod=mod"), "-o", executable, pkg], {
|
|
69
|
+
cwd: installRoot,
|
|
70
|
+
env: { ...process.env, GOWORK: "off" },
|
|
71
|
+
stdio: "inherit",
|
|
72
|
+
});
|
|
73
|
+
if (built.error) {
|
|
74
|
+
console.error(`portolan: built-in ${name} could not build: ${built.error.message}`);
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (built.status !== 0) {
|
|
79
|
+
process.exitCode = built.status ?? 1;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
run(executable, argv.slice(at + 1));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// A Rust plugin already built in release mode under its own crate directory,
|
|
86
|
+
// `plugins/extract-rust/target/release/portolan-extract-rust`, is run as it
|
|
87
|
+
// is; the crate is named after its directory, which is what the manifest
|
|
88
|
+
// path names.
|
|
89
|
+
function prebuiltCargoBinary(argv) {
|
|
90
|
+
const manifest = argv[argv.indexOf("--manifest-path") + 1];
|
|
91
|
+
if (!manifest || argv.indexOf("--manifest-path") < 0) return "";
|
|
92
|
+
const crate = dirname(manifest).split("/").pop();
|
|
93
|
+
const binary = resolve(installRoot, dirname(manifest), "target", "release", `portolan-${crate}`);
|
|
94
|
+
return existsSync(binary) ? binary : "";
|
|
95
|
+
}
|
|
96
|
+
|
|
43
97
|
function resolveArgument(executable, index) {
|
|
44
98
|
if (["node", "python", "python3"].includes(executable)) return index === 0;
|
|
45
99
|
if (executable === "java") return index === 1;
|
|
100
|
+
if (executable === "dotnet") return index === 0;
|
|
46
101
|
if (executable === "cargo") return index === 3;
|
|
47
102
|
return false;
|
|
48
103
|
}
|
package/scripts/schema.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// Composes the JSON Schema for portolan.json out of what the plugins say about
|
|
2
2
|
// themselves.
|
|
3
3
|
//
|
|
4
|
-
// node scripts/schema.mjs write schema/portolan.schema.json
|
|
5
|
-
//
|
|
4
|
+
// node scripts/schema.mjs write schema/portolan.schema.json and
|
|
5
|
+
// src/lib/plugin-index.json
|
|
6
|
+
// node scripts/schema.mjs --check fail if either file on disk is not what
|
|
6
7
|
// the plugins describe now
|
|
7
8
|
//
|
|
8
9
|
// The manifest tells each plugin the things its source cannot carry - which
|
|
@@ -19,8 +20,14 @@
|
|
|
19
20
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
20
21
|
|
|
21
22
|
import { describePlugin } from "./plugin-host.mjs";
|
|
23
|
+
import { RULE_ID, SEVERITIES, SUBJECT_NAMES, SUBJECTS } from "../src/lib/problem-rules-cel.mjs";
|
|
24
|
+
|
|
25
|
+
// The built-in problem rules, so the schema can say which ids are switches
|
|
26
|
+
// and which must carry a whole rule.
|
|
27
|
+
const BUILTIN_RULES = JSON.parse(readFileSync("rules/builtin.json", "utf8"));
|
|
22
28
|
|
|
23
29
|
const OUT = "schema/portolan.schema.json";
|
|
30
|
+
const INDEX = "src/lib/plugin-index.json";
|
|
24
31
|
|
|
25
32
|
const check = process.argv.includes("--check");
|
|
26
33
|
// Bootstrap exception: this command produces the schema that loadManifest
|
|
@@ -51,25 +58,65 @@ for (const plugin of manifest.plugins ?? []) {
|
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
const composed = `${JSON.stringify(compose(), null, 2)}\n`;
|
|
61
|
+
const index = `${JSON.stringify(pluginIndex(), null, 2)}\n`;
|
|
62
|
+
|
|
63
|
+
let stale = false;
|
|
64
|
+
for (const [path, contents, directory] of [
|
|
65
|
+
[OUT, composed, "schema"],
|
|
66
|
+
[INDEX, index, "src/lib"],
|
|
67
|
+
]) {
|
|
68
|
+
let current = null;
|
|
69
|
+
try {
|
|
70
|
+
current = readFileSync(path, "utf8");
|
|
71
|
+
} catch {
|
|
72
|
+
// Absent, which is the same as out of date.
|
|
73
|
+
}
|
|
54
74
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
75
|
+
if (current === contents) {
|
|
76
|
+
console.log(`${path}: up to date`);
|
|
77
|
+
} else if (check) {
|
|
78
|
+
console.error(
|
|
79
|
+
`${path} is ${current === null ? "missing" : "out of date"}. Run \`npm run schema\`.`,
|
|
80
|
+
);
|
|
81
|
+
stale = true;
|
|
82
|
+
} else {
|
|
83
|
+
mkdirSync(directory, { recursive: true });
|
|
84
|
+
writeFileSync(path, contents);
|
|
85
|
+
console.log(`${path}: written from ${described.size} plugin${described.size === 1 ? "" : "s"}`);
|
|
86
|
+
}
|
|
60
87
|
}
|
|
88
|
+
if (stale) process.exit(1);
|
|
61
89
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
90
|
+
/**
|
|
91
|
+
* The plugin index: every plugin the package ships, as it describes itself,
|
|
92
|
+
* with how the manifest runs it. The site's /plugins page and the landing's
|
|
93
|
+
* list of inputs are rendered from this, so a plugin added to portolan.json
|
|
94
|
+
* appears there without anyone writing it down twice. Kept under src/ rather
|
|
95
|
+
* than beside the schema because the staged site copies src/ and nothing else.
|
|
96
|
+
*/
|
|
97
|
+
function pluginIndex() {
|
|
98
|
+
const out = [];
|
|
99
|
+
for (const plugin of manifest.plugins ?? []) {
|
|
100
|
+
const descriptor = described.get(plugin.name);
|
|
101
|
+
if (!descriptor) continue;
|
|
102
|
+
out.push({
|
|
103
|
+
name: plugin.name,
|
|
104
|
+
plugin: descriptor.name,
|
|
105
|
+
summary: descriptor.summary,
|
|
106
|
+
category: descriptor.category,
|
|
107
|
+
phases: descriptor.phases,
|
|
108
|
+
...(descriptor.needs?.length ? { needs: descriptor.needs } : {}),
|
|
109
|
+
runtime: plugin.wasm ? "wasm" : plugin.host ? "host" : "process",
|
|
110
|
+
// The toolchain a process plugin asks the build for; the sandboxed
|
|
111
|
+
// module and the host's own code ask for nothing.
|
|
112
|
+
...(plugin.process ? { toolchain: plugin.process.command } : {}),
|
|
113
|
+
source: plugin.host
|
|
114
|
+
? `scripts/host-plugins/${plugin.host}.mjs`
|
|
115
|
+
: `plugins/${descriptor.name}`,
|
|
116
|
+
options: descriptor.options ?? {},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
73
120
|
}
|
|
74
121
|
|
|
75
122
|
function compose() {
|
|
@@ -116,6 +163,20 @@ function compose() {
|
|
|
116
163
|
description:
|
|
117
164
|
"The source projects that make up the estate. A project gives repeated pipeline inputs one name for the generated site's Settings page; estate-wide inputs such as flows need no project.",
|
|
118
165
|
},
|
|
166
|
+
warningPolicies: {
|
|
167
|
+
type: "array",
|
|
168
|
+
maxItems: 100,
|
|
169
|
+
items: { $ref: "#/$defs/warningPolicy" },
|
|
170
|
+
description:
|
|
171
|
+
"CEL policies for reviewed extraction limitations. Expressions are type-checked when the manifest is read and suppression always requires a reason.",
|
|
172
|
+
},
|
|
173
|
+
problemRules: {
|
|
174
|
+
type: "array",
|
|
175
|
+
maxItems: 200,
|
|
176
|
+
items: { $ref: "#/$defs/problemRule" },
|
|
177
|
+
description:
|
|
178
|
+
"The rules the Problems page applies. An entry naming a built-in rule (rules/builtin.json) switches it off or re-grades it, with a reason; any other id is a rule of its own, written in CEL over one subject. Expressions are type-checked when the manifest is read and run in the page over the merged catalog.",
|
|
179
|
+
},
|
|
119
180
|
plugins: {
|
|
120
181
|
type: "array",
|
|
121
182
|
items: { $ref: "#/$defs/plugin" },
|
|
@@ -139,6 +200,29 @@ function compose() {
|
|
|
139
200
|
},
|
|
140
201
|
},
|
|
141
202
|
$defs: {
|
|
203
|
+
problemRule: problemRuleSchema(),
|
|
204
|
+
warningPolicy: {
|
|
205
|
+
type: "object",
|
|
206
|
+
additionalProperties: false,
|
|
207
|
+
required: ["when", "action", "reason"],
|
|
208
|
+
properties: {
|
|
209
|
+
when: {
|
|
210
|
+
type: "string",
|
|
211
|
+
minLength: 1,
|
|
212
|
+
maxLength: 1000,
|
|
213
|
+
description: "Boolean CEL expression over plugin, rule, severity, project, phase, ref, message and count.",
|
|
214
|
+
},
|
|
215
|
+
action: {
|
|
216
|
+
const: "suppress",
|
|
217
|
+
description: "Suppress matching diagnostics from the active view while retaining them in the report.",
|
|
218
|
+
},
|
|
219
|
+
reason: {
|
|
220
|
+
type: "string",
|
|
221
|
+
minLength: 1,
|
|
222
|
+
description: "Why this limitation is consciously accepted.",
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
},
|
|
142
226
|
catalogProfile: {
|
|
143
227
|
type: "object",
|
|
144
228
|
additionalProperties: false,
|
|
@@ -244,7 +328,7 @@ function compose() {
|
|
|
244
328
|
description: "What the steps below call this plugin. It need not be the plugin's own name.",
|
|
245
329
|
},
|
|
246
330
|
host: {
|
|
247
|
-
enum: ["fetch-git", "fetch-bsr", "fetch-csr"],
|
|
331
|
+
enum: ["fetch-git", "fetch-bsr", "fetch-csr", "fetch-k8s", "fetch-argocd"],
|
|
248
332
|
description:
|
|
249
333
|
"A plugin the host runs inside its own process, because it needs a socket or a git binary (portolan.0008). Only the ones shipped with Portolan can be named.",
|
|
250
334
|
},
|
|
@@ -371,3 +455,67 @@ function fail(message) {
|
|
|
371
455
|
console.error(`portolan schema: ${message}`);
|
|
372
456
|
process.exit(1);
|
|
373
457
|
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* One entry of `problemRules`. Two shapes under one id field: a built-in id
|
|
461
|
+
* may only be switched, re-graded and explained; any other id is a whole
|
|
462
|
+
* rule, over one subject, with the expressions the page runs.
|
|
463
|
+
*/
|
|
464
|
+
function problemRuleSchema() {
|
|
465
|
+
const builtinIds = BUILTIN_RULES.map((rule) => rule.id);
|
|
466
|
+
const subjectFields = SUBJECT_NAMES.map(
|
|
467
|
+
(name) => `${name}: ${Object.keys(SUBJECTS[name].schema).join(", ")}`,
|
|
468
|
+
).join("; ");
|
|
469
|
+
const expression = (returns, what) => ({
|
|
470
|
+
type: "string",
|
|
471
|
+
minLength: 1,
|
|
472
|
+
maxLength: 2000,
|
|
473
|
+
description: `${what} CEL returning ${returns}, over the subject named by \`over\` and \`estate\` (services, contexts, stores, channels, externals). Fields by subject: ${subjectFields}.`,
|
|
474
|
+
});
|
|
475
|
+
return {
|
|
476
|
+
type: "object",
|
|
477
|
+
additionalProperties: false,
|
|
478
|
+
required: ["id"],
|
|
479
|
+
properties: {
|
|
480
|
+
id: {
|
|
481
|
+
type: "string",
|
|
482
|
+
pattern: RULE_ID.source,
|
|
483
|
+
description: `The rule. One of the built-in ids - ${builtinIds.join(", ")} - to switch or re-grade that rule; any other id declares a rule of its own.`,
|
|
484
|
+
},
|
|
485
|
+
enabled: {
|
|
486
|
+
type: "boolean",
|
|
487
|
+
default: true,
|
|
488
|
+
description: "Whether the rule produces rows. A disabled rule needs a reason and stays listed on the Settings page.",
|
|
489
|
+
},
|
|
490
|
+
severity: {
|
|
491
|
+
enum: SEVERITIES,
|
|
492
|
+
description: "How wrong a row of this rule is. For a built-in rule this replaces the severity its passport was written with.",
|
|
493
|
+
},
|
|
494
|
+
reason: {
|
|
495
|
+
type: "string",
|
|
496
|
+
minLength: 1,
|
|
497
|
+
description: "Why the rule is off or re-graded, or, for a rule of your own, why the estate holds it.",
|
|
498
|
+
},
|
|
499
|
+
over: {
|
|
500
|
+
enum: SUBJECT_NAMES,
|
|
501
|
+
description: "What one row of the rule is about, and what `when` and `message` read.",
|
|
502
|
+
},
|
|
503
|
+
when: expression("bool", "The condition: a row exists for every subject where it holds."),
|
|
504
|
+
message: expression("string", "The note on the row."),
|
|
505
|
+
peer: expression("string", "The row's far end, when it has one; a service, event or table id links to its page."),
|
|
506
|
+
title: { type: "string", minLength: 1, description: "The rule's name on the Settings page." },
|
|
507
|
+
note: { type: "string", minLength: 1, description: "The short words on every row of the rule; the title when absent." },
|
|
508
|
+
description: { type: "string", description: "What the rule checks and why it matters." },
|
|
509
|
+
action: { type: "string", description: "What a reader does about a row." },
|
|
510
|
+
},
|
|
511
|
+
if: { properties: { id: { enum: builtinIds } } },
|
|
512
|
+
then: {
|
|
513
|
+
not: {
|
|
514
|
+
anyOf: ["over", "when", "message", "peer", "title", "note", "description", "action"].map((key) => ({
|
|
515
|
+
required: [key],
|
|
516
|
+
})),
|
|
517
|
+
},
|
|
518
|
+
},
|
|
519
|
+
else: { required: ["over", "when", "message", "title"] },
|
|
520
|
+
};
|
|
521
|
+
}
|