@cosmicdrift/kumiko-guards 0.1.1 → 0.3.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 (52) hide show
  1. package/package.json +4 -1
  2. package/src/_lib/guard-kit.ts +74 -19
  3. package/src/_lib/qn.ts +21 -0
  4. package/src/_lib/security-baseline-cli.ts +3 -3
  5. package/src/_lib/security-baseline.ts +8 -8
  6. package/src/changes.json +32 -0
  7. package/src/check-as-casts.ts +648 -0
  8. package/src/check-complexity.ts +292 -0
  9. package/src/check-predicates.ts +218 -0
  10. package/src/check-secret-literals.ts +126 -0
  11. package/src/cli.ts +59 -0
  12. package/src/guard-admin-api.ts +1 -1
  13. package/src/guard-app-feature-structure.ts +114 -0
  14. package/src/guard-broker-subscribe.ts +99 -0
  15. package/src/guard-error-reasons.ts +185 -0
  16. package/src/guard-escape-hatch-declared.ts +26 -19
  17. package/src/guard-fake-tests.ts +1 -1
  18. package/src/guard-feature-integration-tests.ts +184 -0
  19. package/src/guard-html-escape.ts +1 -1
  20. package/src/guard-i18n-keys.ts +440 -0
  21. package/src/guard-i18n-locale-mount.ts +317 -0
  22. package/src/guard-i18n-locale-terminology.ts +117 -0
  23. package/src/guard-i18n-ui-strings.ts +248 -0
  24. package/src/guard-lib-test-coverage.ts +156 -0
  25. package/src/guard-loadall-events.ts +133 -0
  26. package/src/guard-no-custom-primitives.ts +9 -10
  27. package/src/guard-no-date-api.ts +1 -1
  28. package/src/guard-no-direct-fs.ts +1 -1
  29. package/src/guard-no-inline-styles.ts +4 -4
  30. package/src/guard-no-logic-in-views.ts +3 -3
  31. package/src/guard-no-raw-hooks.ts +4 -5
  32. package/src/guard-open-to-all-reason.ts +1 -1
  33. package/src/guard-pii-annotations.ts +267 -0
  34. package/src/guard-pre-es-patterns.ts +1 -1
  35. package/src/guard-primitives-discipline.ts +3 -3
  36. package/src/guard-raw-classname.ts +3 -3
  37. package/src/guard-raw-interactive-elements.ts +3 -3
  38. package/src/guard-raw-sql.ts +2 -2
  39. package/src/guard-renderer-boundaries.ts +1 -1
  40. package/src/guard-restricted-symbols.ts +1 -1
  41. package/src/guard-screen-conventions.ts +161 -0
  42. package/src/guard-silent-skip.ts +1 -1
  43. package/src/guard-table-ddl.ts +159 -0
  44. package/src/guard-tailwind-scan-surface.ts +12 -12
  45. package/src/guard-test-stack-drift.ts +147 -0
  46. package/src/guard-text-field-stance.ts +222 -0
  47. package/src/guard-thin-wrappers.ts +6 -1
  48. package/src/guard-unsafe-json-parse.ts +1 -1
  49. package/src/guard-write-handler-qns.ts +242 -0
  50. package/src/run-guards.ts +36 -3
  51. package/src/run-repo-checks.ts +10 -1
  52. package/src/run-ui-guards.ts +11 -2
@@ -0,0 +1,156 @@
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.
8
+ //
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.
17
+ //
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.
22
+ //
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.
28
+
29
+ import { dirname, resolve } from "node:path";
30
+ import { Node, type SourceFile, SyntaxKind } from "ts-morph";
31
+ import { type AstGuard, type GuardViolation, runStandalone, type ScanSpec } from "./_lib/guard-kit";
32
+ import { hasIgnoreTag } from "./_lib/ignore-tag";
33
+
34
+ // isTestFile() classifies by regex on the file's own path, not by which within-glob matched it, so lib/ vs. test classification stays correct regardless of scan-scope resolution.
35
+ const SCAN: ScanSpec = {
36
+ scope: "source",
37
+ extensions: ["ts", "tsx"],
38
+ kinds: ["library", "app"],
39
+ within: ["lib/**/*.ts", "features/*/lib/**/*.ts", "**/*.test.ts", "**/*.test.tsx"],
40
+ };
41
+ const IGNORE_TAG = "kumiko-lint-ignore lib-test-coverage";
42
+
43
+ type Export = { readonly name: string; readonly node: Node };
44
+
45
+ function isTestFile(path: string): boolean {
46
+ return /\.test\.tsx?$/.test(path) || /\/__tests__\//.test(path);
47
+ }
48
+
49
+ // Exportierte Callables: `export function f` und `export const f = () => …` /
50
+ // `= function () {}`. Reine Werte (`export const X = 5`), Typen und Interfaces
51
+ // zählen nicht — sie tragen keine Logik, die ein Verhaltenstest prüfen könnte.
52
+ function exportedCallables(sf: SourceFile): Export[] {
53
+ const out: Export[] = [];
54
+ for (const fd of sf.getFunctions()) {
55
+ const name = fd.getName();
56
+ if (name !== undefined && (fd.isExported() || fd.isDefaultExport()))
57
+ out.push({ name, node: fd });
58
+ }
59
+ for (const vs of sf.getVariableStatements()) {
60
+ if (!vs.isExported()) continue;
61
+ for (const decl of vs.getDeclarations()) {
62
+ const init = decl.getInitializer();
63
+ if (init !== undefined && (Node.isArrowFunction(init) || Node.isFunctionExpression(init))) {
64
+ out.push({ name: decl.getName(), node: decl });
65
+ }
66
+ }
67
+ }
68
+ return out;
69
+ }
70
+
71
+ function stripExt(path: string): string {
72
+ return path.replace(/\.tsx?$/, "");
73
+ }
74
+
75
+ // Ein Test ist mit einer lib-Datei verknüpft, wenn er relativ aus genau dieser
76
+ // Datei importiert — nicht per Pfad-Konvention. Das erlaubt Tests eine Ebene
77
+ // über lib/ (features/<x>/__tests__/foo.test.ts → "../lib/foo") und verhindert,
78
+ // dass generische Namen (fieldText, targetForRow) in fremden Tests fälschlich
79
+ // als Referenz zählen.
80
+ function testImportsLib(testSf: SourceFile, libPathNoExt: string): boolean {
81
+ const testDir = dirname(testSf.getFilePath());
82
+ for (const imp of testSf.getImportDeclarations()) {
83
+ const spec = imp.getModuleSpecifierValue();
84
+ if (!spec.startsWith(".")) continue;
85
+ if (stripExt(resolve(testDir, spec)) === libPathNoExt) return true;
86
+ }
87
+ return false;
88
+ }
89
+
90
+ // Import-Deklarationen ausklammern: ein benannter Import ohne jede Nutzung
91
+ // (`import { addFees, subFees } from "../calc"`, nur addFees aufgerufen)
92
+ // erfuellte den Guard fuer subFees schon durch den Import-Text allein —
93
+ // der per-Funktions-Check war fuer named imports quasi tautologisch.
94
+ function bodyTextWithoutImports(testSf: SourceFile): string {
95
+ const importRanges = testSf
96
+ .getDescendantsOfKind(SyntaxKind.ImportDeclaration)
97
+ .map((d) => [d.getStart(), d.getEnd()] as const);
98
+ const full = testSf.getFullText();
99
+ let out = "";
100
+ let cursor = 0;
101
+ for (const [start, end] of importRanges) {
102
+ out += full.slice(cursor, start);
103
+ cursor = end;
104
+ }
105
+ out += full.slice(cursor);
106
+ return out;
107
+ }
108
+
109
+ function referencesName(testSf: SourceFile, name: string): boolean {
110
+ return new RegExp(`\\b${name}\\b`).test(bodyTextWithoutImports(testSf));
111
+ }
112
+
113
+ export const guard: AstGuard = {
114
+ name: "Lib-Test-Coverage Guard (App-Repos)",
115
+ scan: SCAN,
116
+ hint:
117
+ "Jede lib-Datei mit exportierter Funktion braucht einen Test, der aus dem " +
118
+ "Modul importiert und jede Funktion namentlich referenziert. IO-Loader, die " +
119
+ `ein Integrationstest deckt: // ${IGNORE_TAG} <Grund, z.B. integration-covered via …>`,
120
+ run(files: readonly SourceFile[]) {
121
+ const violations: GuardViolation[] = [];
122
+ const testFiles: SourceFile[] = [];
123
+ const libFiles: SourceFile[] = [];
124
+ for (const sf of files) {
125
+ if (isTestFile(sf.getFilePath())) testFiles.push(sf);
126
+ else if (!sf.getFilePath().endsWith(".d.ts")) libFiles.push(sf);
127
+ }
128
+ for (const sf of libFiles) {
129
+ const relevant = exportedCallables(sf).filter((e) => !hasIgnoreTag(e.node, IGNORE_TAG));
130
+ if (relevant.length === 0) continue;
131
+
132
+ const libPathNoExt = stripExt(sf.getFilePath());
133
+ const linked = testFiles.filter((t) => testImportsLib(t, libPathNoExt));
134
+ if (linked.length === 0) {
135
+ violations.push({
136
+ file: sf.getFilePath(),
137
+ line: 1,
138
+ message: `Kein Test importiert dieses lib-Modul (${relevant.length} exportierte Funktion(en) ungetestet)`,
139
+ });
140
+ continue;
141
+ }
142
+
143
+ for (const e of relevant) {
144
+ if (linked.some((t) => referencesName(t, e.name))) continue;
145
+ violations.push({
146
+ file: sf.getFilePath(),
147
+ line: e.node.getStartLineNumber(),
148
+ message: `Exportierte Funktion "${e.name}" wird in keinem verknüpften Test namentlich referenziert`,
149
+ });
150
+ }
151
+ }
152
+ return { violations };
153
+ },
154
+ };
155
+
156
+ if (import.meta.main) runStandalone(guard);
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Guard: blockt Aufrufe von `loadAllEventsByType()` ausserhalb von Tests,
4
+ * Ops-Scripts und der Definition selbst.
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.
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.
16
+ *
17
+ * Usage:
18
+ * bun packages/guards/src/guard-loadall-events.ts
19
+ */
20
+
21
+ import * as path from "node:path";
22
+ import { type CallExpression, type Identifier, type SourceFile, SyntaxKind } from "ts-morph";
23
+ import { type AstGuard, type GuardViolation, runStandalone, type ScanSpec } from "./_lib/guard-kit";
24
+
25
+ const ROOT = process.cwd();
26
+
27
+ const SCAN: ScanSpec = {
28
+ scope: "source",
29
+ extensions: ["ts"],
30
+ kinds: ["framework", "library"],
31
+ };
32
+
33
+ // Test-Dateien dürfen die API frei benutzen — sie sind die primären
34
+ // Verifizierer und laufen gegen kleine, kontrollierte Stores.
35
+ const EXCLUDE = /(__tests__|\.test\.ts$|\.integration\.ts$|\.d\.ts$)/;
36
+
37
+ // Erlaubte Aufrufer: die Definition selbst + Ops-/Migration-Scripts (laufen
38
+ // auf bekannt kleinen aggregate_types, nicht im Prod-Hot-Path).
39
+ // ROOT ist gegen process.cwd() (Repo-Root) verankert, `^`-Anker matcht nur
40
+ // echte Top-Level-scripts/, nicht packages/.../scripts/*.
41
+ const ALLOWLIST: readonly RegExp[] = [
42
+ /^packages\/framework\/src\/event-store\/event-store\.ts$/,
43
+ /^scripts\//,
44
+ ];
45
+
46
+ const GUARDED_CALLS = new Set(["loadAllEventsByType"]);
47
+
48
+ export interface Violation {
49
+ file: string;
50
+ line: number;
51
+ functionName: string;
52
+ enclosingFunction: string;
53
+ }
54
+
55
+ export function isAllowed(relativePath: string): boolean {
56
+ return ALLOWLIST.some((re) => re.test(relativePath));
57
+ }
58
+
59
+ export function collectViolations(sourceFile: SourceFile): Violation[] {
60
+ const violations: Violation[] = [];
61
+ const relativePath = path.relative(ROOT, sourceFile.getFilePath());
62
+ if (isAllowed(relativePath)) return violations;
63
+
64
+ const calls = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
65
+ for (const call of calls) {
66
+ const fnName = getCalleeName(call);
67
+ if (!fnName || !GUARDED_CALLS.has(fnName)) continue;
68
+ violations.push({
69
+ file: relativePath,
70
+ line: call.getStartLineNumber(),
71
+ functionName: fnName,
72
+ enclosingFunction: findEnclosingName(call),
73
+ });
74
+ }
75
+ return violations;
76
+ }
77
+
78
+ // Extract the called name. Handles:
79
+ // loadAllEventsByType(...) → "loadAllEventsByType" (Identifier)
80
+ // someNamespace.loadAllEventsByType(...) → "loadAllEventsByType" (PropertyAccess)
81
+ // Any other callee shape returns null.
82
+ function getCalleeName(call: CallExpression): string | null {
83
+ const expr = call.getExpression();
84
+ if (expr.getKind() === SyntaxKind.Identifier) {
85
+ return (expr as Identifier).getText();
86
+ }
87
+ if (expr.getKind() === SyntaxKind.PropertyAccessExpression) {
88
+ return expr.asKindOrThrow(SyntaxKind.PropertyAccessExpression).getName();
89
+ }
90
+ return null;
91
+ }
92
+
93
+ function findEnclosingName(call: CallExpression): string {
94
+ let cur = call.getParent();
95
+ while (cur) {
96
+ if (cur.isKind(SyntaxKind.FunctionDeclaration) || cur.isKind(SyntaxKind.MethodDeclaration)) {
97
+ return cur.getName() ?? "<anonymous>";
98
+ }
99
+ if (cur.isKind(SyntaxKind.FunctionExpression) || cur.isKind(SyntaxKind.ArrowFunction)) {
100
+ const parent = cur.getParent();
101
+ if (parent?.isKind(SyntaxKind.VariableDeclaration)) return parent.getName();
102
+ if (parent?.isKind(SyntaxKind.PropertyAssignment)) return parent.getName();
103
+ return "<anonymous>";
104
+ }
105
+ cur = cur.getParent();
106
+ }
107
+ return "<top-level>";
108
+ }
109
+
110
+ export const guard: AstGuard = {
111
+ name: "loadAllEventsByType Guard",
112
+ scan: SCAN,
113
+ hint:
114
+ "loadAllEventsByType buffers ALL events of an aggregate_type in memory (OOM cliff > ~100k events). " +
115
+ "Use streamAllEventsByType (batched, memory-bounded) in production code. " +
116
+ "Allowed only in tests, scripts/, and the event-store definition.",
117
+ run(files) {
118
+ const violations: GuardViolation[] = [];
119
+ for (const sf of files) {
120
+ if (EXCLUDE.test(sf.getFilePath())) continue;
121
+ for (const v of collectViolations(sf)) {
122
+ violations.push({
123
+ file: v.file,
124
+ line: v.line,
125
+ message: `${v.functionName}(...) in ${v.enclosingFunction}`,
126
+ });
127
+ }
128
+ }
129
+ return { violations };
130
+ },
131
+ };
132
+
133
+ if (import.meta.main) runStandalone(guard);
@@ -85,11 +85,11 @@ const BASELINE_PATH = path.join(ROOT, BASELINE_FILE);
85
85
  const rawFormHtmlBaseline = baselineRatchet({
86
86
  file: BASELINE_PATH,
87
87
  formatVersion: 1,
88
- unit: "Fund(e) rohes Formular-HTML",
88
+ unit: "raw form HTML finding(s)",
89
89
  });
90
90
  const RAW_FORM_HTML_REMEDIATION =
91
- "Framework-Widget nutzen (@cosmicdrift/kumiko-renderer-web: Field/Input, ComboboxInput, …) " +
92
- `oder usePrimitives(). Echte Ausnahme: // ${IGNORE_TAG} <Grund>`;
91
+ "Use a framework widget (@cosmicdrift/kumiko-renderer-web: Field/Input, ComboboxInput, …) " +
92
+ `or usePrimitives(). Real exception: // ${IGNORE_TAG} <reason>`;
93
93
 
94
94
  // The rule is new: measured backlog is 0 in every locally checked repo
95
95
  // (infra#748). Without a baseline file, check fail-closed against an empty
@@ -105,12 +105,11 @@ function checkRawFormHtmlBaseline(findings: readonly RawFormFinding[]): GuardVio
105
105
  return regressions.map((r) => ({
106
106
  file: r.file,
107
107
  line: resolveLine(r.file),
108
- message: `${r.current} Fund(e) rohes Formular-HTML (keine BaselineAltbestand ist 0). ${RAW_FORM_HTML_REMEDIATION}`,
108
+ message: `${r.current} raw form HTML finding(s) (no baseline existing backlog is 0). ${RAW_FORM_HTML_REMEDIATION}`,
109
109
  }));
110
110
  }
111
111
  return rawFormHtmlBaseline.check(current, RAW_FORM_HTML_REMEDIATION, {
112
- formatDriftRemediation:
113
- "Einmalig `bun guards/guard-no-custom-primitives.ts --write-baseline` aufrufen.",
112
+ formatDriftRemediation: "Run `bun guards/guard-no-custom-primitives.ts --write-baseline` once.",
114
113
  resolveLine,
115
114
  });
116
115
  }
@@ -149,13 +148,13 @@ function analyse(
149
148
  violations.push({
150
149
  file: sf.getFilePath(),
151
150
  line: c.line,
152
- message: `App-lokales UI-Primitive "${c.name}" — Framework-Widget/Primitive nutzen`,
151
+ message: `App-local UI primitive "${c.name}" — use a framework widget/primitive`,
153
152
  });
154
153
  }
155
154
  rawFormFindings.push(...collectRawFormHtml(sf));
156
155
  }
157
156
  if (!compareBaseline) {
158
- console.log(" Baseline-Vergleich uebersprungen (--no-baseline).");
157
+ console.log(" Baseline comparison skipped (--no-baseline).");
159
158
  } else {
160
159
  violations.push(...checkRawFormHtmlBaseline(rawFormFindings));
161
160
  }
@@ -166,8 +165,8 @@ export const guard: AstGuard = {
166
165
  name: "No-Custom-Primitives Guard (App-Repos)",
167
166
  scan: SCAN,
168
167
  hint:
169
- "Framework-Widget nutzen (@cosmicdrift/kumiko-renderer-web: StatCard, SectionCard, StatusBadge, QueryTable, Charts, …) " +
170
- `oder usePrimitives(). Echte Domain-Komponente ohne Framework-Pendant: // ${IGNORE_TAG} <Grund>`,
168
+ "Use a framework widget (@cosmicdrift/kumiko-renderer-web: StatCard, SectionCard, StatusBadge, QueryTable, Charts, …) " +
169
+ `or usePrimitives(). Real domain component without a framework equivalent: // ${IGNORE_TAG} <reason>`,
171
170
  run: (files: readonly SourceFile[]) => analyse(files, true),
172
171
  };
173
172
 
@@ -162,7 +162,7 @@ function findDateApiUsages(sf: SourceFile): Omit<Violation, "file">[] {
162
162
  export const guard: AstGuard = {
163
163
  name: "No-Date-API Guard",
164
164
  scan: SCAN,
165
- hint: "Ersetze mit Temporal.Now.instant() / Temporal.Instant.from / .toString() / .epochMilliseconds — siehe docs/plans/architecture/timezones.md.",
165
+ hint: "Replace with Temporal.Now.instant() / Temporal.Instant.from / .toString() / .epochMilliseconds — see docs/plans/architecture/timezones.md.",
166
166
  run(files) {
167
167
  const violations: Array<{ file: string; line: number; message: string }> = [];
168
168
 
@@ -203,7 +203,7 @@ export const guard: AstGuard = {
203
203
  name: "No-Direct-Fs Guard",
204
204
  scan: SCAN,
205
205
  security: true,
206
- hint: "Direkter node:fs-Import außerhalb der Allowlistnutze FileStorageProvider (packages/framework/src/files/) statt fs selbst zu verdrahten. Path-Traversal-Guard existiert nur dort (resolveContainedPath). Legitimer neuer Tooling-Caller? Allowlist in guard-no-direct-fs.ts erweitern, mit Begründung + repo-Scope.",
206
+ hint: "Direct node:fs import outside the allowlist use FileStorageProvider (packages/framework/src/files/) instead of wiring fs yourself. The path-traversal guard (resolveContainedPath) only exists there. Legitimate new tooling caller? Extend the allowlist in guard-no-direct-fs.ts, with a reason + repo scope.",
207
207
  run(files) {
208
208
  const violations: Array<{ file: string; line: number; message: string }> = [];
209
209
  const roots = resolveRepoRoots();
@@ -20,8 +20,8 @@ export const guard: AstGuard = {
20
20
  name: "No-Inline-Styles Guard (App-Repos)",
21
21
  scan: SCAN,
22
22
  hint:
23
- "style=/CSSProperties in App-Code durch Widgets + Theme-Tokens ersetzen. " +
24
- `Begründete Ausnahme (z.B. dynamische Breite aus Daten): // ${IGNORE_TAG} <Grund>`,
23
+ "Replace style=/CSSProperties in app code with widgets + theme tokens. " +
24
+ `Justified exception (e.g. dynamic width from data): // ${IGNORE_TAG} <reason>`,
25
25
  run(files: readonly SourceFile[]) {
26
26
  const violations: GuardViolation[] = [];
27
27
  for (const sf of files) {
@@ -32,7 +32,7 @@ export const guard: AstGuard = {
32
32
  violations.push({
33
33
  file: sf.getFilePath(),
34
34
  line: attr.getStartLineNumber(),
35
- message: "style=-Prop in App-Code (Theme-Tokens/Widgets nutzen)",
35
+ message: "style= prop in app code (use theme tokens/widgets)",
36
36
  });
37
37
  }
38
38
  // ponytail: name-text comparison doesn't tolerate aliased imports
@@ -47,7 +47,7 @@ export const guard: AstGuard = {
47
47
  violations.push({
48
48
  file: sf.getFilePath(),
49
49
  line: ref.getStartLineNumber(),
50
- message: "CSSProperties-Style-Objekt in App-Code (Theme-Tokens/Widgets nutzen)",
50
+ message: "CSSProperties style object in app code (use theme tokens/widgets)",
51
51
  });
52
52
  }
53
53
  }
@@ -124,8 +124,8 @@ export const guard: AstGuard = {
124
124
  name: "No-Logic-in-Views Guard (App-Repos)",
125
125
  scan: SCAN,
126
126
  hint:
127
- "Berechnung/Parsing/Aggregation gehört nach lib/ (mit Test) — web/ enthält nur " +
128
- `Komponenten und Hooks. Begründete Ausnahme: // ${IGNORE_TAG} <Grund>`,
127
+ "Computation/parsing/aggregation belongs in lib/ (with a test) — web/ holds only " +
128
+ `components and hooks. Justified exception: // ${IGNORE_TAG} <reason>`,
129
129
  run(files: readonly SourceFile[]) {
130
130
  const violations: GuardViolation[] = [];
131
131
  for (const sf of files) {
@@ -136,7 +136,7 @@ export const guard: AstGuard = {
136
136
  violations.push({
137
137
  file: sf.getFilePath(),
138
138
  line: fn.getStartLineNumber(),
139
- message: `View-Logik "${name}" gehört nach lib/ (mit Test) — web/ nur Komponenten/Hooks`,
139
+ message: `View logic "${name}" belongs in lib/ (with a test) — web/ only components/hooks`,
140
140
  });
141
141
  }
142
142
  }
@@ -40,8 +40,8 @@ export const guard: AstGuard = {
40
40
  name: "No-Raw-Hooks Guard (App-Repos)",
41
41
  scan: SCAN,
42
42
  hint:
43
- "Framework-Hook-Satz nutzen: useQuery (live: true für SSE), useMutation, useDisclosure. " +
44
- `Echter Sonderfall (DOM-Integration o.ä.): // ${IGNORE_TAG} <Grund>`,
43
+ "Use the framework hook set: useQuery (live: true for SSE), useMutation, useDisclosure. " +
44
+ `Real special case (DOM integration or similar): // ${IGNORE_TAG} <reason>`,
45
45
  run(files: readonly SourceFile[]) {
46
46
  const violations: GuardViolation[] = [];
47
47
  for (const sf of files) {
@@ -54,7 +54,7 @@ export const guard: AstGuard = {
54
54
  violations.push({
55
55
  file: sf.getFilePath(),
56
56
  line: call.getStartLineNumber(),
57
- message: `${name} in App-Screen — Framework-Hooks nutzen (useQuery/useMutation/useDisclosure)`,
57
+ message: `${name} in App-Screen — use framework hooks (useQuery/useMutation/useDisclosure)`,
58
58
  });
59
59
  continue;
60
60
  }
@@ -63,8 +63,7 @@ export const guard: AstGuard = {
63
63
  violations.push({
64
64
  file: sf.getFilePath(),
65
65
  line: call.getStartLineNumber(),
66
- message:
67
- "fetch() in App-Screen — useQuery/useMutation bzw. einen Api-Client (*.ts) nutzen",
66
+ message: "fetch() in App-Screen — use useQuery/useMutation or an API client (*.ts)",
68
67
  });
69
68
  }
70
69
  }
@@ -85,7 +85,7 @@ export function createOpenToAllReasonGuard(opts: { root: string }): AstGuard {
85
85
  name: "Open-To-All-Reason Guard",
86
86
  scan: SCAN,
87
87
  security: true,
88
- hint: 'openToAll: { reason: "<why any authenticated user may call this>" } angeben (+ personalData: "tenant-members" bei Write-Handlern mit nicht gebundenen Personendaten). Baseline nach bewusster Reduktion: `bun guards/run-guards.ts --write-security-baseline`',
88
+ hint: 'Provide openToAll: { reason: "<why any authenticated user may call this>" } (+ personalData: "tenant-members" for write handlers with unbound personal data). Baseline after a deliberate reduction: `bun guards/run-guards.ts --write-security-baseline`',
89
89
  run(files) {
90
90
  const violations: GuardViolation[] = [
91
91
  ...findGenericOpenToAllReasons(files, opts.root).map((f) => ({