@cosmicdrift/kumiko-guards 0.1.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 (47) hide show
  1. package/LICENSE +57 -0
  2. package/README.md +16 -0
  3. package/package.json +40 -0
  4. package/src/_lib/baseline-compare.ts +56 -0
  5. package/src/_lib/generic-reason.ts +39 -0
  6. package/src/_lib/guard-kit.ts +534 -0
  7. package/src/_lib/handler-name-forms.ts +29 -0
  8. package/src/_lib/ignore-tag.ts +24 -0
  9. package/src/_lib/primitives-access.ts +19 -0
  10. package/src/_lib/roots.ts +304 -0
  11. package/src/_lib/scan-lines.ts +25 -0
  12. package/src/_lib/scan-scope.ts +152 -0
  13. package/src/_lib/security-baseline-cli.ts +54 -0
  14. package/src/_lib/security-baseline.ts +325 -0
  15. package/src/_lib/sql-inventory.ts +267 -0
  16. package/src/guard-access-denied-test.ts +135 -0
  17. package/src/guard-admin-api.ts +134 -0
  18. package/src/guard-cross-feature-imports.ts +244 -0
  19. package/src/guard-direct-entity-writes.ts +387 -0
  20. package/src/guard-direct-fetch.ts +154 -0
  21. package/src/guard-escape-hatch-declared.ts +520 -0
  22. package/src/guard-fake-tests.ts +137 -0
  23. package/src/guard-html-escape.ts +345 -0
  24. package/src/guard-no-custom-primitives.ts +196 -0
  25. package/src/guard-no-date-api.ts +186 -0
  26. package/src/guard-no-direct-fs.ts +232 -0
  27. package/src/guard-no-direct-process-env.ts +126 -0
  28. package/src/guard-no-inline-styles.ts +58 -0
  29. package/src/guard-no-logic-in-views.ts +147 -0
  30. package/src/guard-no-raw-hooks.ts +76 -0
  31. package/src/guard-open-to-all-reason.ts +112 -0
  32. package/src/guard-pre-es-patterns.ts +199 -0
  33. package/src/guard-primitives-discipline.ts +330 -0
  34. package/src/guard-raw-classname.ts +111 -0
  35. package/src/guard-raw-interactive-elements.ts +154 -0
  36. package/src/guard-raw-sql.ts +89 -0
  37. package/src/guard-renderer-boundaries.ts +157 -0
  38. package/src/guard-restricted-symbols.ts +138 -0
  39. package/src/guard-silent-skip.ts +186 -0
  40. package/src/guard-tailwind-scan-surface.ts +588 -0
  41. package/src/guard-tenant-escalation.ts +312 -0
  42. package/src/guard-thin-wrappers.ts +422 -0
  43. package/src/guard-unsafe-json-parse.ts +86 -0
  44. package/src/index.ts +29 -0
  45. package/src/run-guards.ts +78 -0
  46. package/src/run-repo-checks.ts +22 -0
  47. package/src/run-ui-guards.ts +25 -0
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Guard: prevents `@cosmicdrift/kumiko-renderer` (shared, platform-neutral)
4
+ * from touching DOM- or platform-specific APIs. As soon as window/document/
5
+ * EventSource/react-dom show up, the separation is broken and the code
6
+ * belongs in `@cosmicdrift/kumiko-renderer-web` (or renderer-native) instead.
7
+ *
8
+ * Deliberately simple: a regex scan over the source text. False positives
9
+ * (e.g. "window" in a string literal) are possible but rare — the guard is a
10
+ * maintenance alarm, not a semantic proof.
11
+ *
12
+ * Checked:
13
+ * - Imports: react-dom/*, jsdom, @cosmicdrift/kumiko-renderer-web, @cosmicdrift/kumiko-renderer-native
14
+ * - Symbols: window., document., location., history., localStorage,
15
+ * sessionStorage, navigator., EventSource, fetch
16
+ * (bare — unqualified)
17
+ *
18
+ * __tests__ folders are excluded — tests mount in jsdom, that's expected.
19
+ */
20
+
21
+ import * as fs from "node:fs";
22
+ import * as path from "node:path";
23
+ import { type RepoCheck, reportResults, runRepoChecks } from "./_lib/guard-kit";
24
+
25
+ // Imports that are taboo in the shared layer.
26
+ const FORBIDDEN_IMPORTS = [
27
+ /from ["']react-dom(?:\/.*)?["']/,
28
+ /from ["']jsdom["']/,
29
+ /from ["']@cosmicdrift\/kumiko-renderer-web["']/,
30
+ /from ["']@cosmicdrift\/kumiko-renderer-native["']/,
31
+ /from ["']@cosmicdrift\/kumiko-dispatcher-live["']/,
32
+ ];
33
+
34
+ // Runtime symbols that mean DOM/browser. `\b` prevents substring matches
35
+ // (e.g. "origin" not matched as "origin.foo"). This is a coarse guard — a
36
+ // ts-morph-based check would be more precise, but not worth the effort here.
37
+ const FORBIDDEN_SYMBOLS = [
38
+ /\bwindow\s*\./,
39
+ /\bdocument\s*\./,
40
+ /\blocation\s*\./,
41
+ /\bhistory\s*\./,
42
+ /\blocalStorage\b/,
43
+ /\bsessionStorage\b/,
44
+ /\bnavigator\s*\./,
45
+ /\bEventSource\b/,
46
+ /\bHTMLElement\b/,
47
+ /\bcreateRoot\b/,
48
+ ];
49
+
50
+ // JSX elements with a lowercase name = HTML tags (React convention:
51
+ // lowercase = intrinsic DOM element, Capitalized = component). The shared
52
+ // renderer must not emit DOM tags — everything goes through primitives,
53
+ // which are platform-bound.
54
+ //
55
+ // Match: `<tagname` at the start of a JSX opening, with a lowercase letter.
56
+ // Ignored: fragment `<>`, components `<Capitalized`, attribute values like
57
+ // `<string>` in a type-annotation context (the regex only matches right
58
+ // after whitespace/newline/>, not after an identifier character).
59
+ const FORBIDDEN_JSX_TAG = /(^|\s|>|\()<([a-z][a-zA-Z0-9-]*)[\s/>]/;
60
+
61
+ type Violation = {
62
+ readonly file: string;
63
+ readonly line: number;
64
+ readonly rule: string;
65
+ readonly excerpt: string;
66
+ };
67
+
68
+ function walk(dir: string): string[] {
69
+ const out: string[] = [];
70
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
71
+ const full = path.join(dir, entry.name);
72
+ if (entry.isDirectory()) {
73
+ if (entry.name === "__tests__") continue;
74
+ if (entry.name === "node_modules" || entry.name === "dist") continue;
75
+ out.push(...walk(full));
76
+ continue;
77
+ }
78
+ if (entry.isFile() && /\.(ts|tsx)$/.test(entry.name)) {
79
+ out.push(full);
80
+ }
81
+ }
82
+ return out;
83
+ }
84
+
85
+ export function findViolations(file: string, root: string): Violation[] {
86
+ const text = fs.readFileSync(file, "utf-8");
87
+ const lines = text.split("\n");
88
+ const violations: Violation[] = [];
89
+ for (let i = 0; i < lines.length; i++) {
90
+ const line = lines[i] ?? "";
91
+ // Skip comments. Pure heuristic — multi-line block comments aren't
92
+ // caught. Good enough for source that doesn't jsdoc-block-paste 'window.*' as prose.
93
+ const trimmed = line.trim();
94
+ if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
95
+
96
+ for (const pattern of FORBIDDEN_IMPORTS) {
97
+ if (pattern.test(line)) {
98
+ violations.push({
99
+ file: path.relative(root, file),
100
+ line: i + 1,
101
+ rule: "forbidden-import",
102
+ excerpt: trimmed,
103
+ });
104
+ }
105
+ }
106
+ for (const pattern of FORBIDDEN_SYMBOLS) {
107
+ if (pattern.test(line)) {
108
+ violations.push({
109
+ file: path.relative(root, file),
110
+ line: i + 1,
111
+ rule: "forbidden-symbol",
112
+ excerpt: trimmed,
113
+ });
114
+ }
115
+ }
116
+ const jsxMatch = line.match(FORBIDDEN_JSX_TAG);
117
+ if (jsxMatch !== null) {
118
+ violations.push({
119
+ file: path.relative(root, file),
120
+ line: i + 1,
121
+ rule: `forbidden-jsx-tag (<${jsxMatch[2]}>)`,
122
+ excerpt: trimmed,
123
+ });
124
+ }
125
+ }
126
+ return violations;
127
+ }
128
+
129
+ export const check: RepoCheck = {
130
+ name: "Renderer-Boundaries Guard",
131
+ hint: "@cosmicdrift/kumiko-renderer darf keine DOM-/Browser-/Platform-APIs nutzen. Platform-spezifischer Code gehört nach @cosmicdrift/kumiko-renderer-web (oder renderer-native).",
132
+ run(roots) {
133
+ const frameworkRoots = roots.filter((r) => r.kind === "framework");
134
+ const scanDirs = frameworkRoots
135
+ .map((r) => path.join(r.absPath, "packages/renderer/src"))
136
+ .filter((p) => fs.existsSync(p));
137
+ if (scanDirs.length === 0) {
138
+ return { violations: [], matchedFiles: 0, notApplicable: true };
139
+ }
140
+ const files: string[] = [];
141
+ for (const dir of scanDirs) files.push(...walk(dir));
142
+ const root = frameworkRoots[0]?.absPath ?? process.cwd();
143
+ const violations = files.flatMap((file) =>
144
+ findViolations(file, root).map((v) => ({
145
+ file: v.file,
146
+ line: v.line,
147
+ message: `[${v.rule}] ${v.excerpt}`,
148
+ })),
149
+ );
150
+ return { violations, matchedFiles: files.length, notApplicable: false };
151
+ },
152
+ };
153
+
154
+ if (import.meta.main) {
155
+ const failed = reportResults(await runRepoChecks([check]));
156
+ process.exit(failed > 0 ? 1 : 0);
157
+ }
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Guard: finds imports/re-exports of the unscoped stream primitives outside
4
+ * an allowlist.
5
+ *
6
+ * `getUnscopedAggregateStreamMaxVersion` / `getUnscopedAggregateStreamTenant`
7
+ * (packages/framework/src/event-store/event-store.ts) have no tenant
8
+ * filter — they are an existence oracle for foreign tenants (see
9
+ * kumiko-framework#1269). Legitimate only for seed-/system-internal code
10
+ * that deliberately works across tenants.
11
+ *
12
+ * Usage:
13
+ * bun guards/guard-restricted-symbols.ts
14
+ *
15
+ * Exit 1 on violations in non-allowlisted files, 0 when clean.
16
+ */
17
+
18
+ import { type SourceFile, SyntaxKind } from "ts-morph";
19
+ import {
20
+ type AstGuard,
21
+ isAllowlisted,
22
+ relFromRepoRoot,
23
+ runStandalone,
24
+ type ScanSpec,
25
+ } from "./_lib/guard-kit";
26
+ import { resolveRepoRoots } from "./_lib/roots";
27
+
28
+ const SCAN: ScanSpec = {
29
+ scope: "source",
30
+ extensions: ["ts"],
31
+ frameworkWithin: ["packages/*/src/**", "samples/**"],
32
+ };
33
+
34
+ const EXCLUDE = /(__tests__|\.test\.ts$|\.integration\.ts$|\.d\.ts$)/;
35
+
36
+ const RESTRICTED_SYMBOLS = new Set([
37
+ "getUnscopedAggregateStreamMaxVersion",
38
+ "getUnscopedAggregateStreamTenant",
39
+ ]);
40
+
41
+ const ALLOWLIST = [
42
+ /^packages\/framework\/src\/event-store\/event-store\.ts$/,
43
+ /^packages\/framework\/src\/event-store\/index\.ts$/,
44
+ /^packages\/bundled-features\/src\/tenant\/seeding\.ts$/,
45
+ /^packages\/bundled-features\/src\/tier-engine\/feature\.ts$/,
46
+ ];
47
+
48
+ interface Violation {
49
+ line: number;
50
+ symbol: string;
51
+ }
52
+
53
+ // Namespace import (`import * as es from "...event-store"; es.getUnscoped...()`)
54
+ // doesn't bind a named import declaration, so it slips past
55
+ // findRestrictedSymbolReferences() below — catch `<namespace>.<restrictedSymbol>`
56
+ // property access instead. `export *` re-exports remain an unaddressed gap
57
+ // (see the guard's `hint` for the known limitation).
58
+ function findNamespacedSymbolAccesses(sf: SourceFile): Violation[] {
59
+ const violations: Violation[] = [];
60
+
61
+ const namespaceNames = new Set(
62
+ sf
63
+ .getImportDeclarations()
64
+ .map((d) => d.getNamespaceImport())
65
+ .filter((ns): ns is NonNullable<typeof ns> => ns !== undefined)
66
+ .map((ns) => ns.getText()),
67
+ );
68
+ if (namespaceNames.size === 0) return violations;
69
+
70
+ for (const access of sf.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression)) {
71
+ const objText = access.getExpression().getText();
72
+ const propName = access.getNameNode().getText();
73
+ if (namespaceNames.has(objText) && RESTRICTED_SYMBOLS.has(propName)) {
74
+ violations.push({ line: access.getStartLineNumber(), symbol: propName });
75
+ }
76
+ }
77
+
78
+ return violations;
79
+ }
80
+
81
+ // Named imports (`import { X } from ...`) and re-exports (`export { X } from
82
+ // ...`) both cross a module boundary — either shape counts as an unguarded
83
+ // reference. getName() returns the imported/exported symbol's original name,
84
+ // not a local alias, so `as`-renamed references are still caught.
85
+ function findRestrictedSymbolReferences(sf: SourceFile): Violation[] {
86
+ const violations: Violation[] = [];
87
+
88
+ for (const importDecl of sf.getImportDeclarations()) {
89
+ for (const named of importDecl.getNamedImports()) {
90
+ const name = named.getName();
91
+ if (RESTRICTED_SYMBOLS.has(name)) {
92
+ violations.push({ line: named.getStartLineNumber(), symbol: name });
93
+ }
94
+ }
95
+ }
96
+
97
+ for (const exportDecl of sf.getExportDeclarations()) {
98
+ if (!exportDecl.getModuleSpecifier()) continue;
99
+ for (const named of exportDecl.getNamedExports()) {
100
+ const name = named.getName();
101
+ if (RESTRICTED_SYMBOLS.has(name)) {
102
+ violations.push({ line: named.getStartLineNumber(), symbol: name });
103
+ }
104
+ }
105
+ }
106
+
107
+ violations.push(...findNamespacedSymbolAccesses(sf));
108
+
109
+ return violations;
110
+ }
111
+
112
+ export const guard: AstGuard = {
113
+ name: "Restricted-Symbols Guard",
114
+ scan: SCAN,
115
+ hint: 'getUnscopedAggregateStream{MaxVersion,Tenant} sind ein Existenz-Orakel für fremde Tenants — nur Seed-/System-interner Code darf sie referenzieren. Neuer Caller nötig? Allowlist in guard-restricted-symbols.ts erweitern, mit Begründung. Bekannte Lücke: `export * from "...event-store"` wird nicht erkannt (Named-Imports, Re-Exports und Namespace-Property-Access sind abgedeckt).',
116
+ run(files) {
117
+ const violations: Array<{ file: string; line: number; message: string }> = [];
118
+ const roots = resolveRepoRoots();
119
+
120
+ for (const sf of files) {
121
+ const file = sf.getFilePath();
122
+ const rel = relFromRepoRoot(file, roots);
123
+ if (EXCLUDE.test(rel)) continue;
124
+ if (isAllowlisted(rel, ALLOWLIST)) continue;
125
+ for (const v of findRestrictedSymbolReferences(sf)) {
126
+ violations.push({
127
+ file: rel,
128
+ line: v.line,
129
+ message: `[${v.symbol}] unscoped stream-primitive referenced outside allowlist`,
130
+ });
131
+ }
132
+ }
133
+
134
+ return { violations };
135
+ },
136
+ };
137
+
138
+ if (import.meta.main) runStandalone(guard);
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Guard: finds "silent skip" spots in production code.
4
+ *
5
+ * A "silent skip" is a bare `return;` (no value) in code that skips logic
6
+ * unnoticed — typical in hooks, handlers, middleware.
7
+ *
8
+ * Usage:
9
+ * bun guards/guard-silent-skip.ts
10
+ *
11
+ * Exit 1 on violations, 0 when clean.
12
+ */
13
+
14
+ import * as path from "node:path";
15
+ import { type Node, type ReturnStatement, type SourceFile, SyntaxKind } from "ts-morph";
16
+ import { type AstGuard, runStandalone, type ScanSpec } from "./_lib/guard-kit";
17
+
18
+ const ROOT = process.cwd();
19
+
20
+ const SCAN: ScanSpec = {
21
+ scope: "source",
22
+ extensions: ["ts"],
23
+ frameworkWithin: ["packages/*/src/**"],
24
+ };
25
+
26
+ const EXCLUDE = /(__tests__|\.test\.ts$|\.integration\.ts$|\.d\.ts$)/;
27
+
28
+ export interface SkipSite {
29
+ file: string;
30
+ line: number;
31
+ enclosingFunction: string;
32
+ enclosingKind: "hook" | "handler" | "middleware" | "function" | "arrow" | "method";
33
+ precedingText: string;
34
+ commentsAbove?: string;
35
+ snippet: string;
36
+ }
37
+
38
+ /**
39
+ * Builds the context for a bare return. Pure data extraction — no policy.
40
+ * The allow/deny decision happens in isAllowed().
41
+ */
42
+ function describeSite(ret: ReturnStatement, sourceFile: SourceFile): SkipSite {
43
+ const enclosing = findEnclosingFunction(ret);
44
+ const fnName = enclosing.name;
45
+ const kind = classifyEnclosing(ret, enclosing.node);
46
+
47
+ const prev = ret.getPreviousSibling();
48
+ const precedingText = prev?.getText() ?? "";
49
+
50
+ // Collect comments from multiple possible locations:
51
+ // 1. Leading trivia on the return itself (`// skip:` on the line above a standalone return)
52
+ // 2. Leading trivia on the enclosing IfStatement (`// skip:` before `if (x) return;`)
53
+ // 3. Trailing comment on the previous sibling statement
54
+ const comments: string[] = [];
55
+ comments.push(...ret.getLeadingCommentRanges().map((r) => r.getText()));
56
+ const enclosingIf = ret.getFirstAncestorByKind(SyntaxKind.IfStatement);
57
+ if (enclosingIf && enclosingIf.getStartLineNumber() >= ret.getStartLineNumber() - 2) {
58
+ comments.push(...enclosingIf.getLeadingCommentRanges().map((r) => r.getText()));
59
+ }
60
+ if (prev?.getTrailingCommentRanges) {
61
+ comments.push(...prev.getTrailingCommentRanges().map((r) => r.getText()));
62
+ }
63
+
64
+ return {
65
+ file: path.relative(ROOT, sourceFile.getFilePath()),
66
+ line: ret.getStartLineNumber(),
67
+ enclosingFunction: fnName,
68
+ enclosingKind: kind,
69
+ precedingText,
70
+ commentsAbove: comments.join("\n"),
71
+ snippet: ret.getText(),
72
+ };
73
+ }
74
+
75
+ function findEnclosingFunction(node: Node): { node: Node; name: string } {
76
+ let cur: Node | undefined = node.getParent();
77
+ while (cur) {
78
+ if (
79
+ cur.isKind(SyntaxKind.FunctionDeclaration) ||
80
+ cur.isKind(SyntaxKind.FunctionExpression) ||
81
+ cur.isKind(SyntaxKind.ArrowFunction) ||
82
+ cur.isKind(SyntaxKind.MethodDeclaration)
83
+ ) {
84
+ return { node: cur, name: guessName(cur) };
85
+ }
86
+ cur = cur.getParent();
87
+ }
88
+ return { node, name: "<top-level>" };
89
+ }
90
+
91
+ function guessName(fn: Node): string {
92
+ if (fn.isKind(SyntaxKind.FunctionDeclaration) || fn.isKind(SyntaxKind.MethodDeclaration)) {
93
+ return fn.getName() ?? "<anonymous>";
94
+ }
95
+ const parent = fn.getParent();
96
+ if (parent?.isKind(SyntaxKind.VariableDeclaration)) return parent.getName();
97
+ if (parent?.isKind(SyntaxKind.PropertyAssignment)) return parent.getName();
98
+ const callExpr = parent?.isKind(SyntaxKind.CallExpression) ? parent : undefined;
99
+ if (callExpr) return `${callExpr.getExpression().getText()}(...)`;
100
+ return "<anonymous>";
101
+ }
102
+
103
+ function classifyEnclosing(_ret: ReturnStatement, fn: Node): SkipSite["enclosingKind"] {
104
+ const text = fn.getParent()?.getText() ?? "";
105
+ if (/\bhook\s*\(|postSave|preSave|validation/.test(text)) return "hook";
106
+ if (/Handler\b|writeHandler|queryHandler/.test(text)) return "handler";
107
+ if (/middleware|Middleware/.test(text)) return "middleware";
108
+ if (fn.isKind(SyntaxKind.MethodDeclaration)) return "method";
109
+ if (fn.isKind(SyntaxKind.ArrowFunction)) return "arrow";
110
+ return "function";
111
+ }
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // POLICY
115
+ // ---------------------------------------------------------------------------
116
+
117
+ /**
118
+ * Policy: a bare `return;` needs one of the following markers right before it
119
+ * — otherwise it is flagged.
120
+ *
121
+ * (a) Log call: ctx.log?.debug(...), logger.debug(...), context.log?.info(...)
122
+ * (b) Skip comment: // skip: <reason>
123
+ * (c) Throw escape: preceded by a throw (the return is dead code, should go)
124
+ *
125
+ * (a) is for pipeline code with ctx in scope — the skip is visible at
126
+ * runtime. (b) is for utils without logger access — documentation in code.
127
+ * (c) catches the rare cases where TS narrowing leaves a dead return behind.
128
+ */
129
+ function isAllowed(site: SkipSite): boolean {
130
+ const preceding = site.precedingText;
131
+
132
+ // (a) log call on ctx/context/opts.context or module-level logger
133
+ if (/\b(?:\w+\.)?(?:log|logger)\??\.(?:debug|info|warn|error|trace)\s*\(/.test(preceding)) {
134
+ return true;
135
+ }
136
+
137
+ // (b) explicit // skip: marker — check trailing comment on preceding OR leading on return
138
+ if (/\/\/\s*skip:/i.test(preceding)) return true;
139
+ if (hasLeadingSkipComment(site)) return true;
140
+
141
+ // (c) preceding is a throw
142
+ if (/^\s*throw\b/.test(preceding)) return true;
143
+
144
+ return false;
145
+ }
146
+
147
+ function hasLeadingSkipComment(site: SkipSite): boolean {
148
+ // The ts-morph scanner captures the previous sibling as precedingText, but
149
+ // a `// skip:` comment directly above the return is leading trivia on the
150
+ // return statement itself — handled by describeSite via `commentsAbove`.
151
+ return /\/\/\s*skip:/i.test(site.commentsAbove ?? "");
152
+ }
153
+
154
+ // ---------------------------------------------------------------------------
155
+ // Scanner + reporter
156
+ // ---------------------------------------------------------------------------
157
+
158
+ export const guard: AstGuard = {
159
+ name: "Silent-Skip Guard",
160
+ scan: SCAN,
161
+ hint: "Nacktes `return;` braucht davor: Log-Call, `// skip: <grund>`-Kommentar, oder vorangehenden throw.",
162
+ run(files) {
163
+ const violations: Array<{ file: string; line: number; message: string }> = [];
164
+
165
+ for (const sf of files) {
166
+ const file = sf.getFilePath();
167
+ if (EXCLUDE.test(file)) continue;
168
+
169
+ const returns = sf.getDescendantsOfKind(SyntaxKind.ReturnStatement);
170
+ for (const ret of returns) {
171
+ if (ret.getExpression()) continue; // has a value, not "silent"
172
+ const site = describeSite(ret, sf);
173
+ if (isAllowed(site)) continue;
174
+ violations.push({
175
+ file: site.file,
176
+ line: site.line,
177
+ message: `[${site.enclosingKind}] in ${site.enclosingFunction}`,
178
+ });
179
+ }
180
+ }
181
+
182
+ return { violations };
183
+ },
184
+ };
185
+
186
+ if (import.meta.main) runStandalone(guard);