@cosmicdrift/kumiko-guards 0.356.0 → 0.358.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 (39) hide show
  1. package/package.json +2 -2
  2. package/src/_lib/bun-lock-packages.ts +5 -5
  3. package/src/_lib/guard-kit.ts +180 -131
  4. package/src/_lib/qn.ts +9 -9
  5. package/src/_lib/roots.ts +4 -3
  6. package/src/_lib/security-baseline.ts +193 -116
  7. package/src/_lib/sql-inventory.ts +34 -32
  8. package/src/changes.json +6 -0
  9. package/src/check-as-casts.ts +196 -180
  10. package/src/check-complexity.ts +68 -69
  11. package/src/check-predicates.ts +93 -67
  12. package/src/check-secret-literals.ts +49 -38
  13. package/src/check-single-runtime-instance.ts +1 -1
  14. package/src/cli.ts +30 -17
  15. package/src/guard-access-denied-test.ts +49 -23
  16. package/src/guard-app-feature-structure.ts +92 -73
  17. package/src/guard-broker-subscribe.ts +17 -17
  18. package/src/guard-comment-lang.ts +126 -54
  19. package/src/guard-direct-entity-writes.ts +58 -45
  20. package/src/guard-escape-hatch-declared.ts +26 -23
  21. package/src/guard-html-escape.ts +112 -86
  22. package/src/guard-i18n-keys.ts +39 -36
  23. package/src/guard-i18n-locale-mount.ts +47 -40
  24. package/src/guard-i18n-ui-strings.ts +108 -92
  25. package/src/guard-lib-test-coverage.ts +37 -37
  26. package/src/guard-loadall-events.ts +17 -17
  27. package/src/guard-no-framed-extension-sections.ts +4 -4
  28. package/src/guard-no-temporal-polyfill-import.ts +19 -1
  29. package/src/guard-pii-annotations.ts +16 -16
  30. package/src/guard-primitives-discipline.ts +61 -65
  31. package/src/guard-screen-conventions.ts +2 -0
  32. package/src/guard-table-ddl.ts +56 -62
  33. package/src/guard-tenant-escalation.ts +42 -34
  34. package/src/guard-test-stack-drift.ts +6 -6
  35. package/src/guard-test-timeouts.ts +5 -4
  36. package/src/guard-text-field-stance.ts +22 -22
  37. package/src/guard-thin-wrappers.ts +42 -45
  38. package/src/guard-write-handler-qns.ts +29 -29
  39. package/src/runtime-isolation-classify.ts +4 -3
@@ -1,18 +1,18 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
- * Guard: App-Mount-Punkte muessen das deutsche Locale-Feature registrieren,
4
- * wenn das Repo von @cosmicdrift/kumiko-locale-de abhaengt.
3
+ * Guard: app mount points must register the German locale feature
4
+ * when the repo depends on @cosmicdrift/kumiko-locale-de.
5
5
  *
6
- * guard-i18n-keys.ts prueft nur, ob ein verwendeter t()-Key definiert ist —
7
- * und kennt kein Konzept von "Mount-Punkt". Ein Mount, der localeDeClient()
8
- * (Client) bzw. localeDe() (Server) nie aufruft, faellt dort komplett durch
9
- * (kumiko-studio#191, publicstatus#365, infra#533).
6
+ * guard-i18n-keys.ts only checks whether a used t() key is defined —
7
+ * and has no concept of a "mount point". A mount that never calls
8
+ * localeDeClient() (client) or localeDe() (server) slips through there
9
+ * completely (kumiko-studio#191, publicstatus#365, infra#533).
10
10
  *
11
- * Scope-Gate: das package.json am naechsten zur Datei muss auf
12
- * kumiko-locale-de zeigen (aktuell kumiko-studio, publicstatus, solon,
13
- * offlot-app) — ein Node-Resolution-Walk ueber ts-morphs FileSystemHost statt guards/_lib/
14
- * roots.ts' Sibling-Repo-Liste, damit der Gate auch im In-Memory-Test der
15
- * Guard-Suite (kein echter Sibling-Checkout) feuert.
11
+ * Scope gate: the package.json closest to the file must point at
12
+ * kumiko-locale-de (currently kumiko-studio, publicstatus, solon,
13
+ * offlot-app) — a node-resolution walk over ts-morph's FileSystemHost instead
14
+ * of guards/_lib/roots.ts' sibling-repo list, so the gate also fires in the
15
+ * in-memory test of the guard suite (no real sibling checkout).
16
16
  *
17
17
  * Usage:
18
18
  * bun guards/guard-i18n-locale-mount.ts
@@ -23,6 +23,7 @@ import {
23
23
  type FileSystemHost,
24
24
  type Identifier,
25
25
  type Node,
26
+ type ObjectLiteralExpression,
26
27
  type SourceFile,
27
28
  SyntaxKind,
28
29
  } from "ts-morph";
@@ -248,6 +249,39 @@ function isNothingLiteral(node: Node): boolean {
248
249
  );
249
250
  }
250
251
 
252
+ function lookupAcrossBranches(branches: readonly Node[], depth: number): ClientFeaturesLookup {
253
+ const found = branches
254
+ .map((branch) => lookupClientFeatures(branch, depth + 1))
255
+ .filter((lookup) => lookup.found);
256
+ if (found.length === 0) return CLIENT_FEATURES_ABSENT;
257
+ // Which branch runs is unknown statically: German must be registered in every branch that sets clientFeatures.
258
+ return {
259
+ found: true,
260
+ registersGerman: found.every((lookup) => lookup.found && lookup.registersGerman),
261
+ };
262
+ }
263
+
264
+ function lookupInObjectLiteral(obj: ObjectLiteralExpression, depth: number): ClientFeaturesLookup {
265
+ for (const prop of [...obj.getProperties()].reverse()) {
266
+ if (
267
+ (prop.isKind(SyntaxKind.PropertyAssignment) ||
268
+ prop.isKind(SyntaxKind.ShorthandPropertyAssignment)) &&
269
+ prop.getNameNode().getText() === "clientFeatures"
270
+ ) {
271
+ return {
272
+ found: true,
273
+ registersGerman:
274
+ clientFeaturesElements(prop)?.some((el) => elementRegistersGerman(el)) ?? false,
275
+ };
276
+ }
277
+ if (prop.isKind(SyntaxKind.SpreadAssignment)) {
278
+ const spread = lookupClientFeatures(prop.getExpression(), depth + 1);
279
+ if (spread.found) return spread;
280
+ }
281
+ }
282
+ return CLIENT_FEATURES_ABSENT;
283
+ }
284
+
251
285
  // Finds the clientFeatures property that wins at runtime in the mount call's
252
286
  // options — a literal right there, or an identifier pointing at a same-repo
253
287
  // constant (offlot's `APP_OPTIONS` shape). Properties are walked last to
@@ -265,37 +299,10 @@ function lookupClientFeatures(value: Node | undefined, depth = 0): ClientFeature
265
299
  : lookupClientFeatures(init, depth + 1);
266
300
  }
267
301
  const branches = conditionalBranches(unwrapped);
268
- if (branches !== undefined) {
269
- const found = branches
270
- .map((branch) => lookupClientFeatures(branch, depth + 1))
271
- .filter((lookup) => lookup.found);
272
- if (found.length === 0) return CLIENT_FEATURES_ABSENT;
273
- // Which branch runs is unknown statically: German must be registered in every branch that sets clientFeatures.
274
- return {
275
- found: true,
276
- registersGerman: found.every((lookup) => lookup.found && lookup.registersGerman),
277
- };
278
- }
302
+ if (branches !== undefined) return lookupAcrossBranches(branches, depth);
279
303
  if (isNothingLiteral(unwrapped)) return CLIENT_FEATURES_ABSENT;
280
304
  if (!unwrapped.isKind(SyntaxKind.ObjectLiteralExpression)) return CLIENT_FEATURES_UNRESOLVABLE;
281
- for (const prop of [...unwrapped.getProperties()].reverse()) {
282
- if (
283
- (prop.isKind(SyntaxKind.PropertyAssignment) ||
284
- prop.isKind(SyntaxKind.ShorthandPropertyAssignment)) &&
285
- prop.getNameNode().getText() === "clientFeatures"
286
- ) {
287
- return {
288
- found: true,
289
- registersGerman:
290
- clientFeaturesElements(prop)?.some((el) => elementRegistersGerman(el)) ?? false,
291
- };
292
- }
293
- if (prop.isKind(SyntaxKind.SpreadAssignment)) {
294
- const spread = lookupClientFeatures(prop.getExpression(), depth + 1);
295
- if (spread.found) return spread;
296
- }
297
- }
298
- return CLIENT_FEATURES_ABSENT;
305
+ return lookupInObjectLiteral(unwrapped, depth);
299
306
  }
300
307
 
301
308
  function objectRegistersGermanClientFeatures(value: Node | undefined): boolean {
@@ -1,17 +1,17 @@
1
1
  #!/usr/bin/env bun
2
- // Kein hardcodeter UI-Text in App-Web-Code: JSX-Textknoten und Label-artige
3
- // String-Props müssen über t("…")-Keys laufen (guard-i18n-keys prüft dann,
4
- // dass die Keys definiert sind — dieser Guard schließt die Lücke davor:
5
- // Strings, die nie zu Keys wurden, z.B. "Lade Tenants…").
2
+ // No hardcoded UI text in app web code: JSX text nodes and label-like
3
+ // string props must go through t("…") keys (guard-i18n-keys then checks
4
+ // that the keys are defined — this guard closes the gap before that:
5
+ // strings that never became keys, e.g. "Loading tenants…").
6
6
  //
7
- // Teil von App-Mounting 2.0 (infra#208).
7
+ // Part of App-Mounting 2.0 (infra#208).
8
8
 
9
9
  import { type CallExpression, type Node, type SourceFile, SyntaxKind } from "ts-morph";
10
10
  import { type AstGuard, type GuardViolation, runStandalone, type ScanSpec } from "./_lib/guard-kit";
11
11
  import { hasIgnoreTag } from "./_lib/ignore-tag";
12
12
 
13
- // Nur Web-/Public-Code — Server-Code (Handler-Fehlertexte) läuft über die
14
- // Error-i18n-Pipeline und hat eigene Guards.
13
+ // Web/public code only — server code (handler error texts) goes through the
14
+ // error i18n pipeline and has its own guards.
15
15
  //
16
16
  // samples/ deliberately NOT scanned (tried in infra#478, rolled back): the
17
17
  // showcase/gallery pages are dev docs about the framework API itself
@@ -38,8 +38,8 @@ const SCAN: ScanSpec = {
38
38
  const EXCLUDE = /(__tests__|\.test\.tsx?$|\.integration\.tsx?$|\.d\.ts$)/;
39
39
  const IGNORE_TAG = "kumiko-lint-ignore i18n-ui-strings";
40
40
 
41
- // Mindestens zwei Buchstaben in Folge = menschenlesbarer Text (lässt "—",
42
- // Zahlen, Interpunktion und Einzel-Zeichen durch).
41
+ // At least two consecutive letters = human-readable text (lets "—",
42
+ // numbers, punctuation and single characters through).
43
43
  const HUMAN_TEXT = /\p{L}{2,}/u;
44
44
 
45
45
  // i18n-Key-Shape (e.g. "tenant.nav.members", "money-horse:nav.scenarioCompare"):
@@ -147,6 +147,101 @@ function registrarCallChain(prop: Node): CallExpression[] | undefined {
147
147
  return undefined;
148
148
  }
149
149
 
150
+ function checkJsxTextNodes(sf: SourceFile, violations: GuardViolation[]): void {
151
+ for (const textNode of sf.getDescendantsOfKind(SyntaxKind.JsxText)) {
152
+ const text = textNode.getText().trim();
153
+ if (!HUMAN_TEXT.test(text)) continue;
154
+ if (hasIgnoreTag(textNode, IGNORE_TAG)) continue;
155
+ violations.push({
156
+ file: sf.getFilePath(),
157
+ line: textNode.getStartLineNumber(),
158
+ message: `hardcoded JSX text: "${text.slice(0, 40)}${text.length > 40 ? "…" : ""}"`,
159
+ });
160
+ }
161
+ }
162
+
163
+ function checkLabelAttributes(sf: SourceFile, violations: GuardViolation[]): void {
164
+ for (const attr of sf.getDescendantsOfKind(SyntaxKind.JsxAttribute)) {
165
+ if (!LABEL_PROPS.has(attr.getNameNode().getText())) continue;
166
+ const init = attr.getInitializer();
167
+ if (init === undefined || init.getKind() !== SyntaxKind.StringLiteral) continue;
168
+ const value = init.asKindOrThrow(SyntaxKind.StringLiteral).getLiteralText();
169
+ if (!HUMAN_TEXT.test(value)) continue;
170
+ if (hasIgnoreTag(attr, IGNORE_TAG)) continue;
171
+ violations.push({
172
+ file: sf.getFilePath(),
173
+ line: attr.getStartLineNumber(),
174
+ message: `hardcoded label prop ${attr.getNameNode().getText()}="${value.slice(0, 40)}"`,
175
+ });
176
+ }
177
+ }
178
+
179
+ function checkTernaryLogicalExpressions(sf: SourceFile, violations: GuardViolation[]): void {
180
+ // Ternary/logical JSX expressions: `{saving ? "Saving…" : "Save"}` as a
181
+ // child, or `title={busy ? "…" : "Send"}` on a label prop — a string
182
+ // literal reachable only via getDescendantsOfKind(JsxText/JsxAttribute)
183
+ // above never fires here since the literal sits one level deeper,
184
+ // inside the {…} expression.
185
+ for (const jsxExpr of sf.getDescendantsOfKind(SyntaxKind.JsxExpression)) {
186
+ const inner = jsxExpr.getExpression();
187
+ if (inner === undefined || !isTernaryOrLogical(inner)) continue;
188
+ const attr = jsxExpr.getParentIfKind(SyntaxKind.JsxAttribute);
189
+ if (attr !== undefined && !LABEL_PROPS.has(attr.getNameNode().getText())) continue;
190
+ if (hasIgnoreTag(jsxExpr, IGNORE_TAG)) continue;
191
+ for (const piece of collectTernaryLogicText(inner)) {
192
+ if (!HUMAN_TEXT.test(piece.text)) continue;
193
+ const shown = `${piece.text.slice(0, 40)}${piece.text.length > 40 ? "…" : ""}`;
194
+ violations.push({
195
+ file: sf.getFilePath(),
196
+ line: piece.node.getStartLineNumber(),
197
+ message:
198
+ attr !== undefined
199
+ ? `hardcoded label prop ${attr.getNameNode().getText()} in ternary/logical expression: "${shown}"`
200
+ : `hardcoded JSX text in ternary/logical expression: "${shown}"`,
201
+ });
202
+ }
203
+ }
204
+ }
205
+
206
+ function checkRegistrarLabelProperties(sf: SourceFile, violations: GuardViolation[]): void {
207
+ // r.nav({ label: "..." }) etc.: registrar param is conventionally
208
+ // named "r" across framework/bundled-features/app repos (verified,
209
+ // no exceptions found) — see the samples/ scan-boundary note above.
210
+ for (const prop of sf.getDescendantsOfKind(SyntaxKind.PropertyAssignment)) {
211
+ if (!LABEL_PROPS.has(prop.getName())) continue;
212
+ if (HEADER_SPEC_PROPS.has(prop.getName())) {
213
+ const enclosingObject = prop.getParentIfKind(SyntaxKind.ObjectLiteralExpression);
214
+ const enclosingProp = enclosingObject?.getParentIfKind(SyntaxKind.PropertyAssignment);
215
+ if (enclosingProp?.getName() === "header") continue;
216
+ }
217
+ const init = prop.getInitializer();
218
+ if (init === undefined || init.getKind() !== SyntaxKind.StringLiteral) continue;
219
+ const value = init.asKindOrThrow(SyntaxKind.StringLiteral).getLiteralText();
220
+ if (!HUMAN_TEXT.test(value) || I18N_KEY_SHAPE.test(value)) continue;
221
+ const callChain = registrarCallChain(prop);
222
+ if (callChain === undefined) continue;
223
+ // The agent-doc slot (kumiko-framework#2615) is LLM metadata for the
224
+ // AI tool catalog: English by design, never rendered as UI text. The
225
+ // slot is the options object handed to a call on the registrar
226
+ // argument chain, so forwarding it through a `defineEntity*Handler`
227
+ // helper is the same slot and stays exempt.
228
+ if (
229
+ prop.getName() === "description" &&
230
+ prop
231
+ .getParentIfKind(SyntaxKind.ObjectLiteralExpression)
232
+ ?.getParentIfKind(SyntaxKind.CallExpression) !== undefined
233
+ )
234
+ continue;
235
+ if (callChain.some((c) => hasIgnoreTag(c, IGNORE_TAG)) || hasIgnoreTag(prop, IGNORE_TAG))
236
+ continue;
237
+ violations.push({
238
+ file: sf.getFilePath(),
239
+ line: prop.getStartLineNumber(),
240
+ message: `hardcoded label property ${prop.getName()}="${value.slice(0, 40)}" in r.* call — use an i18n key instead of plain text`,
241
+ });
242
+ }
243
+ }
244
+
150
245
  export const guard: AstGuard = {
151
246
  name: "i18n-UI-Strings Guard (App-Repos)",
152
247
  scan: SCAN,
@@ -157,89 +252,10 @@ export const guard: AstGuard = {
157
252
  const violations: GuardViolation[] = [];
158
253
  for (const sf of files) {
159
254
  if (EXCLUDE.test(sf.getFilePath())) continue;
160
- for (const textNode of sf.getDescendantsOfKind(SyntaxKind.JsxText)) {
161
- const text = textNode.getText().trim();
162
- if (!HUMAN_TEXT.test(text)) continue;
163
- if (hasIgnoreTag(textNode, IGNORE_TAG)) continue;
164
- violations.push({
165
- file: sf.getFilePath(),
166
- line: textNode.getStartLineNumber(),
167
- message: `hardcoded JSX text: "${text.slice(0, 40)}${text.length > 40 ? "…" : ""}"`,
168
- });
169
- }
170
- for (const attr of sf.getDescendantsOfKind(SyntaxKind.JsxAttribute)) {
171
- if (!LABEL_PROPS.has(attr.getNameNode().getText())) continue;
172
- const init = attr.getInitializer();
173
- if (init === undefined || init.getKind() !== SyntaxKind.StringLiteral) continue;
174
- const value = init.asKindOrThrow(SyntaxKind.StringLiteral).getLiteralText();
175
- if (!HUMAN_TEXT.test(value)) continue;
176
- if (hasIgnoreTag(attr, IGNORE_TAG)) continue;
177
- violations.push({
178
- file: sf.getFilePath(),
179
- line: attr.getStartLineNumber(),
180
- message: `hardcoded label prop ${attr.getNameNode().getText()}="${value.slice(0, 40)}"`,
181
- });
182
- }
183
- // Ternary/logical JSX expressions: `{saving ? "Saving…" : "Save"}` as a
184
- // child, or `title={busy ? "…" : "Send"}` on a label prop — a string
185
- // literal reachable only via getDescendantsOfKind(JsxText/JsxAttribute)
186
- // above never fires here since the literal sits one level deeper,
187
- // inside the {…} expression.
188
- for (const jsxExpr of sf.getDescendantsOfKind(SyntaxKind.JsxExpression)) {
189
- const inner = jsxExpr.getExpression();
190
- if (inner === undefined || !isTernaryOrLogical(inner)) continue;
191
- const attr = jsxExpr.getParentIfKind(SyntaxKind.JsxAttribute);
192
- if (attr !== undefined && !LABEL_PROPS.has(attr.getNameNode().getText())) continue;
193
- if (hasIgnoreTag(jsxExpr, IGNORE_TAG)) continue;
194
- for (const piece of collectTernaryLogicText(inner)) {
195
- if (!HUMAN_TEXT.test(piece.text)) continue;
196
- const shown = `${piece.text.slice(0, 40)}${piece.text.length > 40 ? "…" : ""}`;
197
- violations.push({
198
- file: sf.getFilePath(),
199
- line: piece.node.getStartLineNumber(),
200
- message:
201
- attr !== undefined
202
- ? `hardcoded label prop ${attr.getNameNode().getText()} in ternary/logical expression: "${shown}"`
203
- : `hardcoded JSX text in ternary/logical expression: "${shown}"`,
204
- });
205
- }
206
- }
207
- // r.nav({ label: "..." }) etc.: registrar param is conventionally
208
- // named "r" across framework/bundled-features/app repos (verified,
209
- // no exceptions found) — see the samples/ scan-boundary note above.
210
- for (const prop of sf.getDescendantsOfKind(SyntaxKind.PropertyAssignment)) {
211
- if (!LABEL_PROPS.has(prop.getName())) continue;
212
- if (HEADER_SPEC_PROPS.has(prop.getName())) {
213
- const enclosingObject = prop.getParentIfKind(SyntaxKind.ObjectLiteralExpression);
214
- const enclosingProp = enclosingObject?.getParentIfKind(SyntaxKind.PropertyAssignment);
215
- if (enclosingProp?.getName() === "header") continue;
216
- }
217
- const init = prop.getInitializer();
218
- if (init === undefined || init.getKind() !== SyntaxKind.StringLiteral) continue;
219
- const value = init.asKindOrThrow(SyntaxKind.StringLiteral).getLiteralText();
220
- if (!HUMAN_TEXT.test(value) || I18N_KEY_SHAPE.test(value)) continue;
221
- const callChain = registrarCallChain(prop);
222
- if (callChain === undefined) continue;
223
- // The agent-doc slot (kumiko-framework#2615) is LLM metadata for the
224
- // AI tool catalog: English by design, never rendered as UI text. The
225
- // slot is the options object handed to a call on the registrar
226
- // argument chain, so forwarding it through a `defineEntity*Handler`
227
- // helper is the same slot and stays exempt.
228
- if (
229
- prop.getName() === "description" &&
230
- prop
231
- .getParentIfKind(SyntaxKind.ObjectLiteralExpression)
232
- ?.getParentIfKind(SyntaxKind.CallExpression) !== undefined
233
- )
234
- continue;
235
- if (callChain.some((c) => hasIgnoreTag(c, IGNORE_TAG)) || hasIgnoreTag(prop, IGNORE_TAG))
236
- continue;
237
- violations.push({
238
- file: sf.getFilePath(),
239
- line: prop.getStartLineNumber(),
240
- message: `hardcoded label property ${prop.getName()}="${value.slice(0, 40)}" in r.* call — use an i18n key instead of plain text`,
241
- });
242
- }
255
+ checkJsxTextNodes(sf, violations);
256
+ checkLabelAttributes(sf, violations);
257
+ checkTernaryLogicalExpressions(sf, violations);
258
+ checkRegistrarLabelProperties(sf, violations);
243
259
  }
244
260
  return { violations };
245
261
  },
@@ -1,30 +1,29 @@
1
1
  #!/usr/bin/env bun
2
- // lib/ ist die Heimat der extrahierten Logik (Berechnung, Parsing, Mapping) —
3
- // der no-logic-in-views-Guard schiebt sie dorthin, dieser Guard sorgt dafür,
4
- // dass sie an der neuen Stelle auch GETESTET ist. Für jede lib-Datei mit
5
- // mindestens einer exportierten Funktion muss ein Test existieren, der aus dem
6
- // Modul importiert, und jede exportierte Funktion muss dort namentlich
7
- // vorkommen.
2
+ // lib/ is the home of extracted logic (computation, parsing, mapping) —
3
+ // the no-logic-in-views guard pushes it there, this guard ensures it is also
4
+ // TESTED at the new location. For every lib file with at least one exported
5
+ // function, a test must exist that imports from the module, and every
6
+ // exported function must appear there by name.
8
7
  //
9
- // Warum statisch (ts-morph) statt Coverage-Threshold: Coverage lügt zweifach —
10
- // sie sieht nie-importierte Module gar nicht, und "ausgeführt" heißt nicht
11
- // "sinnvoll geprüft" (ein Integrationstest, der die Funktion transitiv über
12
- // HTTP streift, färbt sie grün ohne eine einzige Assertion auf ihr Verhalten).
13
- // Der Guard ist der strukturelle Gegenpart: er koppelt Test↔Modul über den
14
- // echten Import und zählt namentliche Referenzen, nicht Zeilen. Der
15
- // Coverage-Threshold in bunfig.toml bleibt daneben bestehen — beide zusammen,
16
- // nie der Threshold allein.
8
+ // Why static (ts-morph) instead of a coverage threshold: coverage lies twice —
9
+ // it does not see never-imported modules at all, and "executed" does not mean
10
+ // "meaningfully tested" (an integration test that touches the function
11
+ // transitively over HTTP turns it green without a single assertion on its
12
+ // behavior). This guard is the structural counterpart: it couples test↔module
13
+ // through the real import and counts references by name, not lines. The
14
+ // coverage threshold in bunfig.toml stays in place alongside — both together,
15
+ // never the threshold alone.
17
16
  //
18
- // Ausgenommen: Dateien ohne exportierte Funktion (reine Typen/Konstanten wie
19
- // eine Farb-Map — ein Test dafür wäre ein Fake-Test). IO-Loader, die bereits
20
- // durch einen Integrationstest über HTTP gedeckt sind, tragen den ignore-Tag
21
- // mit wahrheitsgemäßer Begründung.
17
+ // Excluded: files without an exported function (pure types/constants like
18
+ // a color map — a test for those would be a fake test). IO loaders already
19
+ // covered by an integration test over HTTP carry the ignore tag with a
20
+ // truthful justification.
22
21
  //
23
- // Bewusst out-of-scope: Re-Exports (`export { foo } from "./bar"`) zählen
24
- // nicht als eigene Callable — die Quelldatei "./bar" trägt ihre eigene
25
- // Coverage-Pflicht. Kommt lib/ je re-exportierte Funktionen ohne eigene
26
- // Quelldatei im Scope vor, ist das ein stiller Blindspot; bisher (Stand
27
- // dieser Fix-Runde) kommt das Pattern in keinem App-Repo vor.
22
+ // Deliberately out of scope: re-exports (`export { foo } from "./bar"`) do not
23
+ // count as their own callable — the source file "./bar" carries its own
24
+ // coverage duty. If lib/ ever contains re-exported functions without their own
25
+ // source file in scope, that is a silent blind spot; so far (as of this fix
26
+ // round) the pattern occurs in no app repo.
28
27
 
29
28
  import { dirname, resolve } from "node:path";
30
29
  import { Node, type SourceFile, SyntaxKind } from "ts-morph";
@@ -53,9 +52,9 @@ function isTestSupportFile(path: string): boolean {
53
52
  return /\/__tests__\//.test(path);
54
53
  }
55
54
 
56
- // Exportierte Callables: `export function f` und `export const f = () => …` /
57
- // `= function () {}`. Reine Werte (`export const X = 5`), Typen und Interfaces
58
- // zählen nicht — sie tragen keine Logik, die ein Verhaltenstest prüfen könnte.
55
+ // Exported callables: `export function f` and `export const f = () => …` /
56
+ // `= function () {}`. Pure values (`export const X = 5`), types and interfaces
57
+ // do not count — they carry no logic a behavior test could check.
59
58
  function exportedCallables(sf: SourceFile): Export[] {
60
59
  const out: Export[] = [];
61
60
  for (const fd of sf.getFunctions()) {
@@ -76,17 +75,18 @@ function exportedCallables(sf: SourceFile): Export[] {
76
75
  }
77
76
 
78
77
  // NodeNext-Importe schreiben ".js"/".jsx", obwohl die Quelle ".ts"/".tsx" ist
79
- // (und analog .mjs/.cjs zu .mts/.cts) — die Extension muss beim Vergleich der
80
- // Modulpfade also für alle vier Paar-Varianten fallen, sonst reißt der Link.
78
+ // (and likewise .mjs/.cjs for .mts/.cts) — the extension must therefore be
79
+ // stripped for all four pair variants when comparing module paths, otherwise
80
+ // the link breaks.
81
81
  function stripExt(path: string): string {
82
82
  return path.replace(/\.(?:[cm]?tsx?|[cm]?jsx?)$/, "");
83
83
  }
84
84
 
85
- // Ein Test ist mit einer lib-Datei verknüpft, wenn er relativ aus genau dieser
86
- // Datei importiert — nicht per Pfad-Konvention. Das erlaubt Tests eine Ebene
87
- // über lib/ (features/<x>/__tests__/foo.test.ts → "../lib/foo") und verhindert,
88
- // dass generische Namen (fieldText, targetForRow) in fremden Tests fälschlich
89
- // als Referenz zählen.
85
+ // A test is linked to a lib file when it imports from exactly that file by
86
+ // relative path — not by path convention. This allows tests one level above
87
+ // lib/ (features/<x>/__tests__/foo.test.ts → "../lib/foo") and prevents
88
+ // generic names (fieldText, targetForRow) in unrelated tests from wrongly
89
+ // counting as a reference.
90
90
  function testImportsLib(testSf: SourceFile, libPathNoExt: string): boolean {
91
91
  const testDir = dirname(testSf.getFilePath());
92
92
  for (const imp of testSf.getImportDeclarations()) {
@@ -97,10 +97,10 @@ function testImportsLib(testSf: SourceFile, libPathNoExt: string): boolean {
97
97
  return false;
98
98
  }
99
99
 
100
- // Import-Deklarationen ausklammern: ein benannter Import ohne jede Nutzung
101
- // (`import { addFees, subFees } from "../calc"`, nur addFees aufgerufen)
102
- // erfuellte den Guard fuer subFees schon durch den Import-Text allein —
103
- // der per-Funktions-Check war fuer named imports quasi tautologisch.
100
+ // Exclude import declarations: a named import without any usage
101
+ // (`import { addFees, subFees } from "../calc"`, only addFees called)
102
+ // used to satisfy the guard for subFees through the import text alone —
103
+ // the per-function check was nearly tautological for named imports.
104
104
  function bodyTextWithoutImports(testSf: SourceFile): string {
105
105
  const importRanges = testSf
106
106
  .getDescendantsOfKind(SyntaxKind.ImportDeclaration)
@@ -1,18 +1,18 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
- * Guard: blockt Aufrufe von `loadAllEventsByType()` ausserhalb von Tests,
4
- * Ops-Scripts und der Definition selbst.
3
+ * Guard: blocks calls of `loadAllEventsByType()` outside tests,
4
+ * ops scripts and the definition itself.
5
5
  *
6
- * `loadAllEventsByType` buffert ALLE Events eines aggregate_type in den
7
- * Speicher (ein `SELECT … ORDER BY` ohne Limit). Jenseits ~100k Events pro
8
- * Typ ist das ein OOM-Cliff, der erst unter Prod-Last auffällt — genau die
9
- * Art still-und-spät-Fehler, die ein Guard fängt bevor sie ausgeliefert wird.
6
+ * `loadAllEventsByType` buffers ALL events of an aggregate_type in
7
+ * memory (a `SELECT … ORDER BY` without a limit). Beyond ~100k events per
8
+ * type that is an OOM cliff that only shows under prod load — exactly the
9
+ * kind of silent-and-late failure a guard catches before it ships.
10
10
  *
11
- * Der memory-bounded Ersatz ist `streamAllEventsByType` (yield't batchweise,
12
- * nie mehr als batchSize Rows resident). Production-Projection-Rebuild geht
13
- * bereits über diesen Streaming-Pfad. `loadAllEventsByType` bleibt legitim
14
- * für Tests (kleine, kontrollierte Stores) und Ops-Scripts auf bekannt
15
- * kleinen aggregate_types — daher die Allowlist statt einer Entfernung.
11
+ * The memory-bounded replacement is `streamAllEventsByType` (yields in
12
+ * batches, never more than batchSize rows resident). Production projection
13
+ * rebuild already goes through this streaming path. `loadAllEventsByType` stays legitimate
14
+ * for tests (small, controlled stores) and ops scripts on known
15
+ * small aggregate_types — hence the allowlist instead of a removal.
16
16
  *
17
17
  * Usage:
18
18
  * bun packages/guards/src/guard-loadall-events.ts
@@ -36,14 +36,14 @@ const SCAN: ScanSpec = {
36
36
  kinds: ["framework", "library"],
37
37
  };
38
38
 
39
- // Test-Dateien dürfen die API frei benutzen — sie sind die primären
40
- // Verifizierer und laufen gegen kleine, kontrollierte Stores.
39
+ // Test files may use the API freely — they are the primary verifiers and
40
+ // run against small, controlled stores.
41
41
  const EXCLUDE = /(__tests__|\.test\.ts$|\.integration\.ts$|\.d\.ts$)/;
42
42
 
43
- // Erlaubte Aufrufer: die Definition selbst + Ops-/Migration-Scripts (laufen
44
- // auf bekannt kleinen aggregate_types, nicht im Prod-Hot-Path).
45
- // ROOT ist gegen process.cwd() (Repo-Root) verankert, `^`-Anker matcht nur
46
- // echte Top-Level-scripts/, nicht packages/.../scripts/*.
43
+ // Allowed callers: the definition itself + ops/migration scripts (run
44
+ // on known small aggregate_types, not in the prod hot path).
45
+ // ROOT is anchored to process.cwd() (repo root), the `^` anchor only matches
46
+ // real top-level scripts/, not packages/.../scripts/*.
47
47
  const ALLOWLIST: readonly RegExp[] = [
48
48
  /^packages\/framework\/src\/event-store\/event-store\.ts$/,
49
49
  /^scripts\//,
@@ -106,6 +106,7 @@ function addRegistryMembers(
106
106
  entries: Map<string, string>,
107
107
  visited: Set<Node>,
108
108
  ): void {
109
+ // skip: a registry spread into itself would otherwise recurse forever
109
110
  if (visited.has(literal)) return;
110
111
  visited.add(literal);
111
112
  for (const member of literal.getProperties()) {
@@ -140,11 +141,10 @@ function addRegistryFromExpression(
140
141
  ): void {
141
142
  if (Node.isObjectLiteralExpression(expression)) {
142
143
  addRegistryMembers(sf, expression, entries, visited);
143
- return;
144
+ } else if (Node.isIdentifier(expression)) {
145
+ const init = sf.getVariableDeclaration(expression.getText())?.getInitializer();
146
+ if (init !== undefined) addRegistryFromExpression(sf, init, entries, visited);
144
147
  }
145
- if (!Node.isIdentifier(expression)) return;
146
- const init = sf.getVariableDeclaration(expression.getText())?.getInitializer();
147
- if (init !== undefined) addRegistryFromExpression(sf, init, entries, visited);
148
148
  }
149
149
 
150
150
  /** Maps each registered component's identifier to the registry key usage sites reference via `__component: SOME_CONST`. */
@@ -61,7 +61,7 @@ function isTypeOnlyImport(decl: ImportDeclaration): boolean {
61
61
 
62
62
  type PolyfillImport = { readonly line: number; readonly specifier: string };
63
63
 
64
- function findPolyfillValueImports(sf: SourceFile): PolyfillImport[] {
64
+ function findPolyfillImportDeclarations(sf: SourceFile): PolyfillImport[] {
65
65
  const found: PolyfillImport[] = [];
66
66
  for (const decl of sf.getImportDeclarations()) {
67
67
  const specifier = decl.getModuleSpecifierValue();
@@ -69,12 +69,22 @@ function findPolyfillValueImports(sf: SourceFile): PolyfillImport[] {
69
69
  found.push({ line: decl.getStartLineNumber(), specifier });
70
70
  }
71
71
  }
72
+ return found;
73
+ }
74
+
75
+ function findPolyfillExportDeclarations(sf: SourceFile): PolyfillImport[] {
76
+ const found: PolyfillImport[] = [];
72
77
  for (const decl of sf.getExportDeclarations()) {
73
78
  const specifier = decl.getModuleSpecifierValue();
74
79
  if (specifier !== undefined && isPolyfillSpecifier(specifier) && !decl.isTypeOnly()) {
75
80
  found.push({ line: decl.getStartLineNumber(), specifier });
76
81
  }
77
82
  }
83
+ return found;
84
+ }
85
+
86
+ function findPolyfillRequireOrDynamicImports(sf: SourceFile): PolyfillImport[] {
87
+ const found: PolyfillImport[] = [];
78
88
  for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
79
89
  const callee = call.getExpression();
80
90
  const isRequireCall = Node.isIdentifier(callee) && callee.getText() === "require";
@@ -88,6 +98,14 @@ function findPolyfillValueImports(sf: SourceFile): PolyfillImport[] {
88
98
  return found;
89
99
  }
90
100
 
101
+ function findPolyfillValueImports(sf: SourceFile): PolyfillImport[] {
102
+ return [
103
+ ...findPolyfillImportDeclarations(sf),
104
+ ...findPolyfillExportDeclarations(sf),
105
+ ...findPolyfillRequireOrDynamicImports(sf),
106
+ ];
107
+ }
108
+
91
109
  export const guard: AstGuard = {
92
110
  name: "No-Temporal-Polyfill-Import Guard",
93
111
  scan: SCAN,