@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.
Files changed (291) hide show
  1. package/README.md +174 -6
  2. package/catalog/enum_test.go +46 -0
  3. package/catalog/evidence_test.go +35 -0
  4. package/catalog/model.go +1213 -0
  5. package/catalog/roundtrip_test.go +210 -0
  6. package/catalog/via_test.go +38 -0
  7. package/cli/init.test.mjs +6 -1
  8. package/cli/portolan.mjs +14 -1
  9. package/cli/portolan.test.mjs +49 -0
  10. package/go.mod +21 -0
  11. package/go.sum +34 -0
  12. package/internal/gocall/README.md +19 -0
  13. package/internal/gocall/analyze.go +189 -0
  14. package/internal/gocall/analyze_test.go +107 -0
  15. package/internal/gohttp/analyze.go +2562 -0
  16. package/internal/gohttp/destination.go +373 -0
  17. package/internal/gohttp/endpoints.go +1067 -0
  18. package/internal/gohttp/roots.go +320 -0
  19. package/internal/gohttp/typed.go +96 -0
  20. package/internal/goscan/constants.go +85 -0
  21. package/internal/goscan/goscan_test.go +227 -0
  22. package/internal/goscan/index.go +629 -0
  23. package/internal/goscan/index_test.go +66 -0
  24. package/internal/goscan/names.go +52 -0
  25. package/internal/goscan/parse_test.go +11 -0
  26. package/internal/goscan/source.go +37 -0
  27. package/internal/goscan/tree.go +284 -0
  28. package/internal/goscan/types.go +99 -0
  29. package/internal/wsdl/ids.go +127 -0
  30. package/internal/wsdl/ids_test.go +21 -0
  31. package/internal/wsdl/model.go +70 -0
  32. package/internal/wsdl/parse.go +949 -0
  33. package/internal/wsdl/parse_test.go +170 -0
  34. package/package.json +28 -15
  35. package/plugin/describe.go +156 -0
  36. package/plugin/describe_test.go +114 -0
  37. package/plugin/protocol.go +141 -0
  38. package/plugin/schematest/schematest.go +126 -0
  39. package/plugins/README.md +404 -50
  40. package/plugins/cmd/portolan-http-clients/main.go +19 -0
  41. package/plugins/extract-argocd/options.schema.json +44 -0
  42. package/plugins/extract-celery/extract.py +0 -2
  43. package/plugins/extract-celery/extract_test.py +1 -1
  44. package/plugins/extract-celery/main.py +1 -0
  45. package/plugins/extract-csharp-ddd/README.md +213 -0
  46. package/plugins/extract-csharp-ddd/options.schema.json +55 -0
  47. package/plugins/extract-django/README.md +52 -19
  48. package/plugins/extract-django/choices.py +47 -0
  49. package/plugins/extract-django/domain.py +30 -18
  50. package/plugins/extract-django/extract.py +21 -7
  51. package/plugins/extract-django/extract_test.py +68 -2
  52. package/plugins/extract-django/lifecycle.py +4 -28
  53. package/plugins/extract-django/main.py +1 -0
  54. package/plugins/extract-django/operations.py +1 -1
  55. package/plugins/extract-django/routing_test.py +109 -1
  56. package/plugins/extract-django/rules.py +154 -0
  57. package/plugins/extract-django/rules_test.py +158 -0
  58. package/plugins/extract-django/store.py +1 -1
  59. package/plugins/extract-django/transport.py +101 -55
  60. package/plugins/extract-django/verbs.py +241 -0
  61. package/plugins/extract-go/README.md +47 -0
  62. package/plugins/extract-go-sqs/options.schema.json +21 -0
  63. package/plugins/extract-http-clients/describe.go +20 -0
  64. package/plugins/extract-http-clients/describe_test.go +11 -0
  65. package/plugins/extract-http-clients/extract.go +740 -0
  66. package/plugins/extract-http-clients/extract_test.go +1561 -0
  67. package/plugins/extract-http-clients/main.go +41 -0
  68. package/plugins/extract-java/build/org/portolan/extract/Extract.class +0 -0
  69. package/plugins/extract-java/build/org/portolan/extract/Main.class +0 -0
  70. package/plugins/extract-java/build/org/portolan/extract/Protocol$Builder.class +0 -0
  71. package/plugins/extract-java/build/org/portolan/extract/Protocol$Input.class +0 -0
  72. package/plugins/extract-java/build/org/portolan/extract/Protocol$Options.class +0 -0
  73. package/plugins/extract-k8s/options.schema.json +32 -0
  74. package/plugins/extract-laravel/Cargo.lock +962 -0
  75. package/plugins/extract-laravel/Cargo.toml +20 -0
  76. package/plugins/extract-laravel/README.md +200 -0
  77. package/plugins/extract-laravel/options.schema.json +68 -0
  78. package/plugins/extract-laravel/rustfmt.toml +1 -0
  79. package/plugins/extract-php-ddd/Cargo.lock +972 -0
  80. package/plugins/extract-php-ddd/Cargo.toml +22 -0
  81. package/plugins/extract-php-ddd/README.md +141 -0
  82. package/plugins/extract-php-ddd/options.schema.json +50 -0
  83. package/plugins/extract-php-ddd/rustfmt.toml +1 -0
  84. package/plugins/extract-python-kafka/extract.py +0 -2
  85. package/plugins/extract-python-kafka/extract_test.py +1 -1
  86. package/plugins/extract-python-kafka/main.py +1 -0
  87. package/plugins/extract-terraform/options.schema.json +26 -0
  88. package/plugins/extract-ts/extract.test.ts +2 -2
  89. package/plugins/extract-ts/extract.ts +4 -5
  90. package/plugins/extract-ts/graphql.test.ts +1 -1
  91. package/plugins/extract-ts/main.ts +1 -0
  92. package/plugins/openapi/ids.go +261 -0
  93. package/plugins/openapi/ids_test.go +98 -0
  94. package/plugins/phpscan/Cargo.lock +953 -0
  95. package/plugins/phpscan/Cargo.toml +19 -0
  96. package/plugins/phpscan/rustfmt.toml +1 -0
  97. package/plugins/portolan-go.wasm +0 -0
  98. package/plugins/pyplugin/catalog.py +24 -1
  99. package/plugins/pyplugin/protocol.py +1 -5
  100. package/plugins/verify-otel/options.schema.json +12 -0
  101. package/portolan.json +132 -4
  102. package/rules/builtin.json +218 -0
  103. package/schema/portolan.schema.json +905 -4
  104. package/scripts/README.md +21 -13
  105. package/scripts/catalog-sources.mjs +6 -0
  106. package/scripts/delivery-presets.mjs +57 -14
  107. package/scripts/diff.mjs +5 -1
  108. package/scripts/django-aggregates.test.mjs +58 -0
  109. package/scripts/gen-likec4.mjs +150 -17
  110. package/scripts/gen-likec4.test.mjs +96 -0
  111. package/scripts/gen.mjs +148 -118
  112. package/scripts/gitops-example.test.mjs +108 -0
  113. package/scripts/go-discovery.test.mjs +30 -0
  114. package/scripts/history.mjs +186 -3
  115. package/scripts/history.test.mjs +1 -1
  116. package/scripts/host-plugins/fetch-argocd.mjs +338 -0
  117. package/scripts/host-plugins/fetch-argocd.options.json +49 -0
  118. package/scripts/host-plugins/fetch-argocd.test.mjs +274 -0
  119. package/scripts/host-plugins/fetch-bsr.mjs +1 -0
  120. package/scripts/host-plugins/fetch-csr.mjs +1 -0
  121. package/scripts/host-plugins/fetch-git.mjs +78 -21
  122. package/scripts/host-plugins/fetch-git.test.mjs +62 -8
  123. package/scripts/host-plugins/fetch-k8s.mjs +263 -0
  124. package/scripts/host-plugins/fetch-k8s.options.json +50 -0
  125. package/scripts/host-plugins/fetch-k8s.test.mjs +259 -0
  126. package/scripts/host-plugins/k8s-topology.mjs +183 -0
  127. package/scripts/json-format.mjs +192 -0
  128. package/scripts/json-format.test.mjs +97 -0
  129. package/scripts/local-api.mjs +287 -12
  130. package/scripts/local-api.test.mjs +150 -5
  131. package/scripts/local-discovery.mjs +94 -9
  132. package/scripts/manifest.mjs +14 -3
  133. package/scripts/manifest.test.mjs +24 -0
  134. package/scripts/output-diff.mjs +94 -0
  135. package/scripts/output-diff.test.mjs +36 -0
  136. package/scripts/package-smoke.mjs +62 -4
  137. package/scripts/plugin-host.mjs +45 -3
  138. package/scripts/plugin-host.test.mjs +9 -0
  139. package/scripts/plugin-wasm-worker.mjs +4 -1
  140. package/scripts/plugins-fresh.mjs +97 -0
  141. package/scripts/plugins-fresh.test.mjs +64 -0
  142. package/scripts/provenance.mjs +72 -0
  143. package/scripts/provenance.test.mjs +149 -0
  144. package/scripts/run-builtin.mjs +62 -7
  145. package/scripts/schema.mjs +167 -19
  146. package/scripts/trace-trials.mjs +176 -0
  147. package/scripts/trace-trials.test.mjs +142 -0
  148. package/scripts/warning-policy.mjs +167 -0
  149. package/scripts/warning-policy.test.mjs +93 -0
  150. package/src/app/Breadcrumbs.test.ts +3 -0
  151. package/src/app/Breadcrumbs.tsx +3 -0
  152. package/src/app/CatalogApp.tsx +2 -0
  153. package/src/app/Sidebar.tsx +3 -3
  154. package/src/app/SidebarFooter.tsx +20 -4
  155. package/src/catalog-docs.test.ts +64 -0
  156. package/src/catalog-docs.ts +35 -0
  157. package/src/catalog-error.test.ts +15 -0
  158. package/src/catalog-index.ts +25 -0
  159. package/src/catalog-model.ts +293 -5
  160. package/src/catalog-validation.ts +113 -2
  161. package/src/catalog.test.ts +40 -1
  162. package/src/chat/Starter.tsx +5 -11
  163. package/src/chat/tools.test.ts +27 -0
  164. package/src/chat/tools.ts +5 -9
  165. package/src/components/CatalogStamp.tsx +10 -8
  166. package/src/components/ChannelRows.test.tsx +76 -1
  167. package/src/components/ChannelRows.tsx +223 -60
  168. package/src/components/DeploymentRows.tsx +131 -0
  169. package/src/components/DocsLinks.test.tsx +27 -0
  170. package/src/components/DocsLinks.tsx +56 -0
  171. package/src/components/FieldTree.tsx +5 -2
  172. package/src/components/HTTPDestinationEvidence.test.tsx +23 -0
  173. package/src/components/HTTPDestinationEvidence.tsx +31 -0
  174. package/src/components/Integrations.tsx +1 -1
  175. package/src/components/MachineDocs.tsx +6 -5
  176. package/src/components/MethodRows.tsx +9 -2
  177. package/src/components/PluginIcon.tsx +77 -0
  178. package/src/components/ProblemRow.tsx +173 -153
  179. package/src/components/RelationEvidence.test.tsx +14 -0
  180. package/src/components/RelationEvidence.tsx +53 -0
  181. package/src/components/RuleMarks.tsx +22 -0
  182. package/src/components/ShapeRows.tsx +24 -20
  183. package/src/data.ts +46 -7
  184. package/src/enrich.test.ts +459 -4
  185. package/src/enrich.ts +308 -7
  186. package/src/er/ErCanvas.tsx +218 -12
  187. package/src/er/GroupNode.tsx +57 -0
  188. package/src/er/StoreHeader.tsx +1 -0
  189. package/src/er/layout.test.ts +85 -2
  190. package/src/er/layout.ts +140 -5
  191. package/src/er/spec.test.ts +17 -0
  192. package/src/er/spec.ts +23 -10
  193. package/src/flow/Recordings.test.tsx +52 -0
  194. package/src/flow/Recordings.tsx +236 -0
  195. package/src/flow/StepDetail.tsx +59 -0
  196. package/src/flow/TraceTrial.tsx +419 -0
  197. package/src/flow/evidence.test.ts +16 -0
  198. package/src/flow/evidence.ts +34 -0
  199. package/src/flow/examples.test.ts +33 -0
  200. package/src/flow/examples.ts +37 -0
  201. package/src/flow/outline.test.ts +28 -0
  202. package/src/flow/outline.ts +7 -2
  203. package/src/flow/trace-trial-resume.test.ts +42 -0
  204. package/src/flow/trace-trial-resume.ts +74 -0
  205. package/src/graph/elk.ts +78 -0
  206. package/src/index.css +68 -0
  207. package/src/landing/DraggableReveal.tsx +3 -2
  208. package/src/landing/EvidencePipeline.tsx +105 -0
  209. package/src/landing/LandingPage.tsx +17 -70
  210. package/src/landing/ProductTour.tsx +6 -6
  211. package/src/lib/all-problems.ts +27 -17
  212. package/src/lib/catalog-diff.ts +1 -1
  213. package/src/lib/centrality.test.ts +251 -0
  214. package/src/lib/centrality.ts +232 -0
  215. package/src/lib/confluence.test.ts +37 -0
  216. package/src/lib/confluence.ts +41 -0
  217. package/src/lib/context-color.ts +1 -1
  218. package/src/lib/deployment-drift.ts +22 -0
  219. package/src/lib/derive.ts +43 -69
  220. package/src/lib/django-aggregates.d.mts +9 -0
  221. package/src/lib/django-aggregates.mjs +36 -0
  222. package/src/lib/django-aggregates.test.ts +29 -0
  223. package/src/lib/django-aggregates.ts +5 -0
  224. package/src/lib/environments.test.ts +83 -0
  225. package/src/lib/environments.ts +57 -0
  226. package/src/lib/integration-url.test.ts +30 -0
  227. package/src/lib/integration-url.ts +63 -0
  228. package/src/lib/kafka-ui.ts +3 -45
  229. package/src/lib/local-api.ts +116 -4
  230. package/src/lib/notion.ts +13 -0
  231. package/src/lib/plugin-index.json +2765 -0
  232. package/src/lib/plugins.test.ts +68 -0
  233. package/src/lib/plugins.ts +264 -0
  234. package/src/lib/problem-flows.test.ts +61 -0
  235. package/src/lib/problem-flows.ts +78 -0
  236. package/src/lib/problem-rules-cel.d.mts +43 -0
  237. package/src/lib/problem-rules-cel.mjs +407 -0
  238. package/src/lib/problem-rules.test.ts +287 -0
  239. package/src/lib/problem-rules.ts +271 -0
  240. package/src/lib/problem-subjects.ts +737 -0
  241. package/src/lib/rule-entries.ts +39 -0
  242. package/src/lib/{data-problems.test.ts → rules-data.test.ts} +18 -16
  243. package/src/lib/rules-deploy.test.ts +133 -0
  244. package/src/lib/{problems.test.ts → rules-edges.test.ts} +8 -3
  245. package/src/lib/{proto-problems.test.ts → rules-proto.test.ts} +9 -25
  246. package/src/lib/{wire-problems.test.ts → rules-wire.test.ts} +13 -11
  247. package/src/lib/rules.test.ts +51 -0
  248. package/src/lib/rules.ts +86 -0
  249. package/src/lib/setup-info.test.ts +17 -0
  250. package/src/lib/setup-info.ts +58 -0
  251. package/src/lib/shape.test.ts +32 -0
  252. package/src/lib/shape.ts +30 -6
  253. package/src/lib/tech.ts +16 -0
  254. package/src/lib/trace-project.test.ts +34 -0
  255. package/src/lib/trace-project.ts +50 -0
  256. package/src/lib/use-problems.ts +23 -0
  257. package/src/lib/warnings.test.ts +63 -0
  258. package/src/lib/warnings.ts +260 -0
  259. package/src/likec4/ids.test.ts +6 -2
  260. package/src/likec4/ids.ts +43 -0
  261. package/src/main.tsx +23 -0
  262. package/src/map/ContextMapGraph.tsx +76 -32
  263. package/src/merge-deployments.test.ts +127 -0
  264. package/src/merge.test.ts +82 -0
  265. package/src/merge.ts +189 -18
  266. package/src/pages/AggregatePage.tsx +65 -14
  267. package/src/pages/ContextMap.tsx +45 -3
  268. package/src/pages/ContextPage.tsx +8 -5
  269. package/src/pages/EventPage.tsx +15 -5
  270. package/src/pages/FlowDetail.tsx +23 -2
  271. package/src/pages/GraphPage.tsx +40 -3
  272. package/src/pages/Overview.tsx +152 -12
  273. package/src/pages/PluginIndex.tsx +190 -0
  274. package/src/pages/Problems.tsx +396 -128
  275. package/src/pages/ServicePage.tsx +62 -5
  276. package/src/pages/Settings.tsx +217 -43
  277. package/src/pages/settings/AboutSettings.tsx +8 -1
  278. package/src/pages/settings/DjangoAggregateChoices.tsx +79 -0
  279. package/src/pages/settings/IntegrationsSettings.tsx +63 -17
  280. package/src/pages/settings/RecordingSettings.tsx +138 -0
  281. package/src/pages/settings/RulesSettings.tsx +825 -0
  282. package/src/routes.test.ts +9 -0
  283. package/src/routes.ts +22 -1
  284. package/src/selection/DetailPanel.tsx +15 -0
  285. package/src/virtual-provenance.d.ts +11 -0
  286. package/vite.config.ts +5 -0
  287. package/scripts/vendor-lock.mjs +0 -58
  288. package/scripts/vendor-lock.test.mjs +0 -69
  289. package/src/lib/data-problems.ts +0 -314
  290. package/src/lib/proto-problems.ts +0 -237
  291. package/src/lib/wire-problems.ts +0 -342
@@ -0,0 +1,271 @@
1
+ // The rules the Problems page is made of, as one list a reader can see.
2
+ //
3
+ // Every rule is the same thing: a subject, a condition in CEL, a message in
4
+ // CEL, and a passport - id, severity, title, the words on the row, what it
5
+ // checks, what to do. The package ships its own in rules/builtin.json; the
6
+ // estate writes more under `problemRules` in the manifest, and there it can
7
+ // also switch a shipped rule off or re-grade it, with a reason.
8
+ //
9
+ // A rule reads one row of one subject, and the row already carries every
10
+ // fact the rule would otherwise have to look up (problem-subjects.ts). So a
11
+ // rule never walks the catalog, and the whole of what makes a row a problem
12
+ // is in the expression a reader can see on the Settings page.
13
+ //
14
+ // Rules are applied here, in the browser, over the merged catalog, and not
15
+ // at generation. Nothing about a problem is written to a fragment: the
16
+ // catalog says what is, and a rule says what should not be, and the second
17
+ // is the estate's to change without a build. That is also why a switch
18
+ // flipped on the Settings page takes effect on the next render, and why the
19
+ // same expression is type-checked twice, once when the manifest is read and
20
+ // once here, by the same module (portolan.0016, portolan.0017).
21
+
22
+ import { useMemo } from "react";
23
+ import { create } from "zustand";
24
+
25
+ import builtinJson from "../../rules/builtin.json";
26
+ import manifestJson from "../../portolan.json";
27
+ import type { Catalog, CatalogIndex } from "../catalog";
28
+ import type { Problem } from "./derive";
29
+ import { compileExpression, SUBJECT_NAMES } from "./problem-rules-cel.mjs";
30
+ import type { RuleSeverity, RuleSubject } from "./problem-rules-cel.mjs";
31
+ import { estateOf, subjectsOf } from "./problem-subjects";
32
+
33
+ export type { RuleSeverity, RuleSubject } from "./problem-rules-cel.mjs";
34
+ export { SUBJECTS } from "./problem-rules-cel.mjs";
35
+ export { estateOf, subjectsOf } from "./problem-subjects";
36
+ export type { Subject } from "./problem-subjects";
37
+
38
+ /** What every rule says about itself, and what it says in CEL. */
39
+ export interface RulePassport {
40
+ id: string;
41
+ /** What a row of this rule is about, and where its near end links to. */
42
+ over: RuleSubject;
43
+ severity: RuleSeverity;
44
+ title: string;
45
+ /** The words on the row, after the arrow. */
46
+ note: string;
47
+ description: string;
48
+ /** What a reader does about a row. */
49
+ action: string;
50
+ /** Boolean CEL over the subject; the row exists when it holds. */
51
+ when: string;
52
+ /** String CEL over the subject; the row's note. */
53
+ message: string;
54
+ /** String CEL over the subject; the row's far end. Optional. */
55
+ peer?: string;
56
+ }
57
+
58
+ /** One entry of `problemRules` in the manifest, as written. */
59
+ export interface ProblemRuleEntry {
60
+ id: string;
61
+ enabled?: boolean;
62
+ severity?: RuleSeverity;
63
+ /** Why a rule is off or re-graded; required with `enabled: false`. */
64
+ reason?: string;
65
+ over?: RuleSubject;
66
+ when?: string;
67
+ message?: string;
68
+ peer?: string;
69
+ title?: string;
70
+ note?: string;
71
+ description?: string;
72
+ action?: string;
73
+ }
74
+
75
+ /** A rule as the page shows it: passport, switch, and where it came from. */
76
+ export interface ProblemRule extends RulePassport {
77
+ builtin: boolean;
78
+ enabled: boolean;
79
+ /** The severity the passport was written with, before the manifest re-graded it. */
80
+ defaultSeverity: RuleSeverity;
81
+ reason?: string;
82
+ }
83
+
84
+ export const BUILTIN_RULES: readonly RulePassport[] = builtinJson as RulePassport[];
85
+
86
+ const BUILTIN_BY_ID = new Map(BUILTIN_RULES.map((rule) => [rule.id, rule]));
87
+
88
+ export function isBuiltinRule(id: string): boolean {
89
+ return BUILTIN_BY_ID.has(id);
90
+ }
91
+
92
+ /**
93
+ * The rules in force: every shipped one, switched or re-graded where the
94
+ * manifest says, then the manifest's own, in manifest order. An entry naming
95
+ * a shipped id changes that rule; any other entry is a rule of its own.
96
+ */
97
+ export function resolveRules(entries: readonly ProblemRuleEntry[]): ProblemRule[] {
98
+ const byId = new Map(entries.map((entry) => [entry.id, entry]));
99
+ const builtin = BUILTIN_RULES.map((passport): ProblemRule => {
100
+ const entry = byId.get(passport.id);
101
+ return {
102
+ ...passport,
103
+ builtin: true,
104
+ enabled: entry?.enabled !== false,
105
+ defaultSeverity: passport.severity,
106
+ severity: entry?.severity ?? passport.severity,
107
+ ...(entry?.reason ? { reason: entry.reason } : {}),
108
+ };
109
+ });
110
+ const custom = entries
111
+ .filter((entry) => !BUILTIN_BY_ID.has(entry.id))
112
+ .map((entry): ProblemRule => {
113
+ const severity = entry.severity ?? "warning";
114
+ return {
115
+ id: entry.id,
116
+ over: entry.over ?? "service",
117
+ severity,
118
+ defaultSeverity: severity,
119
+ title: entry.title ?? entry.id,
120
+ note: entry.note ?? entry.title ?? entry.id,
121
+ description: entry.description ?? "",
122
+ action: entry.action ?? "",
123
+ when: entry.when ?? "",
124
+ message: entry.message ?? "",
125
+ ...(entry.peer ? { peer: entry.peer } : {}),
126
+ builtin: false,
127
+ enabled: entry.enabled !== false,
128
+ ...(entry.reason ? { reason: entry.reason } : {}),
129
+ };
130
+ });
131
+ return [...builtin, ...custom];
132
+ }
133
+
134
+ export function ruleById(rules: readonly ProblemRule[], id: string): ProblemRule | undefined {
135
+ return rules.find((rule) => rule.id === id);
136
+ }
137
+
138
+ // ---------------------------------------------------------------------------
139
+ // Evaluation.
140
+
141
+ export interface RuleFailure {
142
+ rule: string;
143
+ message: string;
144
+ }
145
+
146
+ export interface RuleEvaluation {
147
+ /** Rows the enabled rules produced, in rule order, not yet sorted by severity. */
148
+ problems: Problem[];
149
+ /** Rules that could not be compiled or run, and why; their rows are absent. */
150
+ failures: RuleFailure[];
151
+ /**
152
+ * How many rows each rule would produce, enabled or not. A switched-off
153
+ * rule with a count is the page's way of saying what turning it on costs.
154
+ */
155
+ matches: Map<string, number>;
156
+ }
157
+
158
+ /**
159
+ * Runs one rule over the catalog. Every row is tried; the first expression
160
+ * that throws stops the rule, because a rule that fails on one row is a rule
161
+ * that is wrong, not a row that is.
162
+ */
163
+ export function runRule(
164
+ rule: Pick<ProblemRule, "id" | "over" | "severity" | "when" | "message" | "peer">,
165
+ catalog: Catalog,
166
+ index: CatalogIndex,
167
+ estate: Record<string, string[]> = estateOf(catalog),
168
+ ): { problems: Problem[]; failure?: RuleFailure } {
169
+ const problems: Problem[] = [];
170
+ try {
171
+ if (!rule.when || !rule.message) throw new Error("a rule needs both `when` and `message`");
172
+ if (!SUBJECT_NAMES.includes(rule.over)) throw new Error(`unknown subject "${rule.over}"`);
173
+ const when = compileExpression(rule.over, rule.when, "bool");
174
+ const message = compileExpression(rule.over, rule.message, "string");
175
+ const peer = rule.peer ? compileExpression(rule.over, rule.peer, "string") : null;
176
+ for (const subject of subjectsOf(catalog, index, rule.over)) {
177
+ const context = { [rule.over]: subject.row, estate };
178
+ if (when(context) !== true) continue;
179
+ const note = String(message(context));
180
+ problems.push({
181
+ rule: rule.id,
182
+ severity: rule.severity,
183
+ context: subject.context,
184
+ service: subject.service,
185
+ id: subject.id,
186
+ peer: peer ? String(peer(context)) : "",
187
+ note: note || undefined,
188
+ source: subject.source,
189
+ });
190
+ }
191
+ return { problems };
192
+ } catch (cause) {
193
+ return { problems: [], failure: { rule: rule.id, message: cause instanceof Error ? cause.message.split("\n", 1)[0]! : String(cause) } };
194
+ }
195
+ }
196
+
197
+ /** `runRule`, by the name the Settings editor calls it: the draft is a rule like any other. */
198
+ export const runCustomRule = runRule;
199
+
200
+ /**
201
+ * Every rule over the catalog. A switched-off rule is still run, so the page
202
+ * can say how many rows it would produce, and its rows are left out.
203
+ */
204
+ export function evaluateRules(catalog: Catalog, index: CatalogIndex, rules: readonly ProblemRule[]): RuleEvaluation {
205
+ const estate = estateOf(catalog);
206
+ const matches = new Map<string, number>();
207
+ const failures: RuleFailure[] = [];
208
+ const problems: Problem[] = [];
209
+ for (const rule of rules) {
210
+ const ran = runRule(rule, catalog, index, estate);
211
+ if (ran.failure) failures.push(ran.failure);
212
+ matches.set(rule.id, ran.problems.length);
213
+ if (rule.enabled) problems.push(...ran.problems);
214
+ }
215
+ return { problems, failures, matches };
216
+ }
217
+
218
+ /**
219
+ * The shipped rules, all on, over a catalog - or only the ones named. What
220
+ * the readers used to answer, for a test that holds a rule to a fixture.
221
+ */
222
+ export function builtinProblems(catalog: Catalog, index: CatalogIndex, ids?: readonly string[]): Problem[] {
223
+ const rules = resolveRules([]).filter((rule) => !ids || ids.includes(rule.id));
224
+ const { problems, failures } = evaluateRules(catalog, index, rules);
225
+ if (failures.length > 0) throw new Error(failures.map((failure) => `${failure.rule}: ${failure.message}`).join("\n"));
226
+ return [...problems.filter((p) => p.severity === "error"), ...problems.filter((p) => p.severity === "warning")];
227
+ }
228
+
229
+ // ---------------------------------------------------------------------------
230
+ // The manifest's entries, live.
231
+
232
+ type ManifestWithRules = { problemRules?: ProblemRuleEntry[] };
233
+
234
+ /** The `problemRules` of a manifest, or none: a manifest without the key has every shipped rule on. */
235
+ export function problemRulesFromManifest(manifest: unknown): ProblemRuleEntry[] {
236
+ const entries = (manifest as ManifestWithRules | null | undefined)?.problemRules;
237
+ return Array.isArray(entries) ? entries : [];
238
+ }
239
+
240
+ interface RuleEntriesState {
241
+ entries: ProblemRuleEntry[];
242
+ setEntries: (entries: ProblemRuleEntry[]) => void;
243
+ }
244
+
245
+ /**
246
+ * The entries in force in this page. They start as the manifest the bundle
247
+ * was built with; in local mode the Settings page replaces them with what it
248
+ * wrote, and a change to portolan.json on disk arrives through HMR below, so
249
+ * the Problems page never shows rules the file no longer has.
250
+ */
251
+ export const useRuleEntries = create<RuleEntriesState>((set) => ({
252
+ entries: problemRulesFromManifest(manifestJson),
253
+ setEntries: (entries) => set({ entries }),
254
+ }));
255
+
256
+ export function currentRuleEntries(): ProblemRuleEntry[] {
257
+ return useRuleEntries.getState().entries;
258
+ }
259
+
260
+ /** The rules in force, recomputed when the entries change. For components. */
261
+ export function useProblemRules(): ProblemRule[] {
262
+ const entries = useRuleEntries((state) => state.entries);
263
+ return useMemo(() => resolveRules(entries), [entries]);
264
+ }
265
+
266
+ if (import.meta.hot) {
267
+ import.meta.hot.accept("../../portolan.json", (next) => {
268
+ const incoming = (next as { default?: unknown } | undefined)?.default;
269
+ if (incoming) useRuleEntries.getState().setEntries(problemRulesFromManifest(incoming));
270
+ });
271
+ }