@cosmicdrift/kumiko-guards 0.382.0 → 0.383.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/changes.json +7 -0
- package/src/guard-error-reasons.ts +1 -1
- package/src/guard-screen-conventions.ts +4 -40
- package/src/run-guards.ts +0 -18
- package/src/guard-api-object-form.ts +0 -211
- package/src/guard-broker-subscribe.ts +0 -99
- package/src/guard-escape-hatch-declared.ts +0 -743
- package/src/guard-loadall-events.ts +0 -139
- package/src/guard-open-to-all-reason.ts +0 -119
- package/src/guard-pre-es-patterns.ts +0 -206
- package/src/guard-section-fields-raw.ts +0 -300
- package/src/guard-table-ddl.ts +0 -155
- package/src/guard-text-field-stance.ts +0 -248
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
/**
|
|
3
|
-
* Guard: blocks calls of `loadAllEventsByType()` outside tests,
|
|
4
|
-
* ops scripts and the definition itself.
|
|
5
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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 SourceFile, SyntaxKind } from "ts-morph";
|
|
23
|
-
import {
|
|
24
|
-
type AstGuard,
|
|
25
|
-
type GuardViolation,
|
|
26
|
-
isLocalFinding,
|
|
27
|
-
runStandalone,
|
|
28
|
-
type ScanSpec,
|
|
29
|
-
} from "./_lib/guard-kit";
|
|
30
|
-
|
|
31
|
-
const ROOT = process.cwd();
|
|
32
|
-
|
|
33
|
-
const SCAN: ScanSpec = {
|
|
34
|
-
scope: "source",
|
|
35
|
-
extensions: ["ts"],
|
|
36
|
-
kinds: ["framework", "library"],
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
// Test files may use the API freely — they are the primary verifiers and
|
|
40
|
-
// run against small, controlled stores.
|
|
41
|
-
const EXCLUDE = /(__tests__|\.test\.ts$|\.integration\.ts$|\.d\.ts$)/;
|
|
42
|
-
|
|
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
|
-
const ALLOWLIST: readonly RegExp[] = [
|
|
48
|
-
/^packages\/framework\/src\/event-store\/event-store\.ts$/,
|
|
49
|
-
/^scripts\//,
|
|
50
|
-
];
|
|
51
|
-
|
|
52
|
-
const GUARDED_CALLS = new Set(["loadAllEventsByType"]);
|
|
53
|
-
|
|
54
|
-
export interface Violation {
|
|
55
|
-
file: string;
|
|
56
|
-
line: number;
|
|
57
|
-
functionName: string;
|
|
58
|
-
enclosingFunction: string;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function isAllowed(relativePath: string): boolean {
|
|
62
|
-
return ALLOWLIST.some((re) => re.test(relativePath));
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function collectViolations(sourceFile: SourceFile): Violation[] {
|
|
66
|
-
const violations: Violation[] = [];
|
|
67
|
-
const relativePath = path.relative(ROOT, sourceFile.getFilePath());
|
|
68
|
-
if (isAllowed(relativePath)) return violations;
|
|
69
|
-
|
|
70
|
-
const calls = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
71
|
-
for (const call of calls) {
|
|
72
|
-
const fnName = getCalleeName(call);
|
|
73
|
-
if (!fnName || !GUARDED_CALLS.has(fnName)) continue;
|
|
74
|
-
violations.push({
|
|
75
|
-
file: relativePath,
|
|
76
|
-
line: call.getStartLineNumber(),
|
|
77
|
-
functionName: fnName,
|
|
78
|
-
enclosingFunction: findEnclosingName(call),
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
return violations;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Extract the called name. Handles:
|
|
85
|
-
// loadAllEventsByType(...) → "loadAllEventsByType" (Identifier)
|
|
86
|
-
// someNamespace.loadAllEventsByType(...) → "loadAllEventsByType" (PropertyAccess)
|
|
87
|
-
// Any other callee shape returns null.
|
|
88
|
-
function getCalleeName(call: CallExpression): string | null {
|
|
89
|
-
const expr = call.getExpression();
|
|
90
|
-
if (expr.getKind() === SyntaxKind.Identifier) {
|
|
91
|
-
return expr.getText();
|
|
92
|
-
}
|
|
93
|
-
if (expr.getKind() === SyntaxKind.PropertyAccessExpression) {
|
|
94
|
-
return expr.asKindOrThrow(SyntaxKind.PropertyAccessExpression).getName();
|
|
95
|
-
}
|
|
96
|
-
return null;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function findEnclosingName(call: CallExpression): string {
|
|
100
|
-
let cur = call.getParent();
|
|
101
|
-
while (cur) {
|
|
102
|
-
if (cur.isKind(SyntaxKind.FunctionDeclaration) || cur.isKind(SyntaxKind.MethodDeclaration)) {
|
|
103
|
-
return cur.getName() ?? "<anonymous>";
|
|
104
|
-
}
|
|
105
|
-
if (cur.isKind(SyntaxKind.FunctionExpression) || cur.isKind(SyntaxKind.ArrowFunction)) {
|
|
106
|
-
const parent = cur.getParent();
|
|
107
|
-
if (parent?.isKind(SyntaxKind.VariableDeclaration)) return parent.getName();
|
|
108
|
-
if (parent?.isKind(SyntaxKind.PropertyAssignment)) return parent.getName();
|
|
109
|
-
return "<anonymous>";
|
|
110
|
-
}
|
|
111
|
-
cur = cur.getParent();
|
|
112
|
-
}
|
|
113
|
-
return "<top-level>";
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export const guard: AstGuard = {
|
|
117
|
-
name: "loadAllEventsByType Guard",
|
|
118
|
-
scan: SCAN,
|
|
119
|
-
hint:
|
|
120
|
-
"loadAllEventsByType buffers ALL events of an aggregate_type in memory (OOM cliff > ~100k events). " +
|
|
121
|
-
"Use streamAllEventsByType (batched, memory-bounded) in production code. " +
|
|
122
|
-
"Allowed only in tests, scripts/, and the event-store definition.",
|
|
123
|
-
run(files) {
|
|
124
|
-
const violations: GuardViolation[] = [];
|
|
125
|
-
for (const sf of files) {
|
|
126
|
-
if (EXCLUDE.test(sf.getFilePath())) continue;
|
|
127
|
-
for (const v of collectViolations(sf).filter(isLocalFinding)) {
|
|
128
|
-
violations.push({
|
|
129
|
-
file: v.file,
|
|
130
|
-
line: v.line,
|
|
131
|
-
message: `${v.functionName}(...) in ${v.enclosingFunction}`,
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
return { violations };
|
|
136
|
-
},
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
if (import.meta.main) runStandalone(guard);
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
/**
|
|
3
|
-
* Guard: `openToAll` access declarations need a real reason.
|
|
4
|
-
*
|
|
5
|
-
* The framework boot validator (access-declarations.ts) already rejects an
|
|
6
|
-
* empty `reason`, and separately checks `openToAll.personalData` on
|
|
7
|
-
* query/stream handlers and a PII write that is neither owner-bound nor
|
|
8
|
-
* declares `personalData` — none of that is this
|
|
9
|
-
* guard's job. This guard only catches what the boot validator can't: a
|
|
10
|
-
* `reason` string that IS non-empty but is a placeholder (`"todo"`,
|
|
11
|
-
* `"legacy"`, ...), plus the deprecated bare `openToAll: true` shape.
|
|
12
|
-
*
|
|
13
|
-
* Usage:
|
|
14
|
-
* bun guards/guard-open-to-all-reason.ts
|
|
15
|
-
* Baseline: kumiko-guards guards --write-security-baseline
|
|
16
|
-
*/
|
|
17
|
-
import * as path from "node:path";
|
|
18
|
-
import { type SourceFile, SyntaxKind } from "ts-morph";
|
|
19
|
-
import { isGenericReason, literalReasonText } from "./_lib/generic-reason";
|
|
20
|
-
import {
|
|
21
|
-
ALL_REPO_KINDS,
|
|
22
|
-
type AstGuard,
|
|
23
|
-
type GuardViolation,
|
|
24
|
-
runStandalone,
|
|
25
|
-
type ScanSpec,
|
|
26
|
-
} from "./_lib/guard-kit";
|
|
27
|
-
|
|
28
|
-
const SCAN: ScanSpec = {
|
|
29
|
-
kinds: ALL_REPO_KINDS,
|
|
30
|
-
scope: "source",
|
|
31
|
-
extensions: ["ts"],
|
|
32
|
-
frameworkWithin: ["packages/*/src/**", "samples/**"],
|
|
33
|
-
};
|
|
34
|
-
const EXCLUDE = /(__tests__\/|\.test\.tsx?$|\.d\.ts$)/;
|
|
35
|
-
|
|
36
|
-
function scannableFiles(files: readonly SourceFile[]): SourceFile[] {
|
|
37
|
-
return files.filter((sf) => !EXCLUDE.test(sf.getFilePath()));
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function openToAllPropertyAssignments(sf: SourceFile) {
|
|
41
|
-
return sf
|
|
42
|
-
.getDescendantsOfKind(SyntaxKind.PropertyAssignment)
|
|
43
|
-
.filter((pa) => pa.getName() === "openToAll");
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function findDeprecatedOpenToAll(
|
|
47
|
-
files: readonly SourceFile[],
|
|
48
|
-
root: string,
|
|
49
|
-
): { file: string; line: number; message: string }[] {
|
|
50
|
-
const out: { file: string; line: number; message: string }[] = [];
|
|
51
|
-
for (const sf of scannableFiles(files)) {
|
|
52
|
-
for (const pa of openToAllPropertyAssignments(sf)) {
|
|
53
|
-
if (pa.getInitializer()?.getKind() !== SyntaxKind.TrueKeyword) continue;
|
|
54
|
-
out.push({
|
|
55
|
-
file: path.relative(root, sf.getFilePath()),
|
|
56
|
-
line: pa.getStartLineNumber(),
|
|
57
|
-
message:
|
|
58
|
-
'openToAll: true is deprecated — replace with openToAll: { reason: "<why any authenticated user may call this>" } (+ personalData: "tenant-members" for write handlers with unbound personal data).',
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return out;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function findGenericOpenToAllReasons(
|
|
66
|
-
files: readonly SourceFile[],
|
|
67
|
-
root: string,
|
|
68
|
-
): { file: string; line: number; message: string }[] {
|
|
69
|
-
const out: { file: string; line: number; message: string }[] = [];
|
|
70
|
-
for (const sf of scannableFiles(files)) {
|
|
71
|
-
for (const pa of openToAllPropertyAssignments(sf)) {
|
|
72
|
-
const init = pa.getInitializer();
|
|
73
|
-
if (!init?.isKind(SyntaxKind.ObjectLiteralExpression)) continue;
|
|
74
|
-
const reasonProp = init.getProperty("reason");
|
|
75
|
-
if (!reasonProp?.isKind(SyntaxKind.PropertyAssignment)) continue;
|
|
76
|
-
const reasonText = literalReasonText(reasonProp.getInitializer());
|
|
77
|
-
if (reasonText === undefined) continue;
|
|
78
|
-
if (reasonText.trim() === "") continue;
|
|
79
|
-
if (!isGenericReason(reasonText)) continue;
|
|
80
|
-
out.push({
|
|
81
|
-
file: path.relative(root, sf.getFilePath()),
|
|
82
|
-
line: pa.getStartLineNumber(),
|
|
83
|
-
message: `openToAll: { reason: "${reasonText}" } uses a placeholder reason — give a concrete justification for why any authenticated user may call this.`,
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return out;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function createOpenToAllReasonGuard(opts: { root: string }): AstGuard {
|
|
91
|
-
return {
|
|
92
|
-
name: "Open-To-All-Reason Guard",
|
|
93
|
-
scan: SCAN,
|
|
94
|
-
security: true,
|
|
95
|
-
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: `kumiko-guards guards --write-security-baseline`',
|
|
96
|
-
run(files) {
|
|
97
|
-
const violations: GuardViolation[] = [
|
|
98
|
-
...findGenericOpenToAllReasons(files, opts.root).map((f) => ({
|
|
99
|
-
file: f.file,
|
|
100
|
-
line: f.line,
|
|
101
|
-
message: f.message,
|
|
102
|
-
neverFrozen: true,
|
|
103
|
-
})),
|
|
104
|
-
...findDeprecatedOpenToAll(files, opts.root).map((f) => ({
|
|
105
|
-
file: f.file,
|
|
106
|
-
line: f.line,
|
|
107
|
-
message: f.message,
|
|
108
|
-
})),
|
|
109
|
-
];
|
|
110
|
-
return { violations };
|
|
111
|
-
},
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export const guard = createOpenToAllReasonGuard({ root: process.cwd() });
|
|
116
|
-
|
|
117
|
-
if (import.meta.main) {
|
|
118
|
-
runStandalone(guard);
|
|
119
|
-
}
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
/**
|
|
3
|
-
* Guard: finds pre-ES patterns that were removed with the event-sourcing
|
|
4
|
-
* pivot and must not return.
|
|
5
|
-
*
|
|
6
|
-
* Checked patterns:
|
|
7
|
-
*
|
|
8
|
-
* 1. `createEventLog` / `EventLog` / `EventLogEntry`
|
|
9
|
-
* The Redis-stream-based activity log was replaced by the `events`
|
|
10
|
-
* table (postgres). A new import of this symbol is always a mistake —
|
|
11
|
-
* the file no longer exists.
|
|
12
|
-
*
|
|
13
|
-
* 2. `ctx.emit` / `r.postEvent`
|
|
14
|
-
* Before sprint E.2 these were the pubsub-event APIs. Replaced by
|
|
15
|
-
* `ctx.appendEvent` (domain event on an aggregate stream) +
|
|
16
|
-
* `r.multiStreamProjection` (async cross-aggregate consumers). The
|
|
17
|
-
* symbols are gone; a use points at unmigrated code or an old
|
|
18
|
-
* copy-paste from an orphaned sample.
|
|
19
|
-
*
|
|
20
|
-
* 3. `aggregateType: "configChanges"` as a string literal
|
|
21
|
-
* Pre-ES stream from before the config→configValue refactor
|
|
22
|
-
* (2026-04-24). Consumers now filter on
|
|
23
|
-
* `configValue.created/updated/deleted`. A remaining string literal
|
|
24
|
-
* would be a dead subscriber.
|
|
25
|
-
*
|
|
26
|
-
* 4. `CONFIG_CHANGED_EVENT_NAME` / `"config:event:config-changed"`
|
|
27
|
-
* The pre-ES "config-changed" event was replaced by auto lifecycle
|
|
28
|
-
* events.
|
|
29
|
-
*
|
|
30
|
-
* Exclude: markdown docs + this guard itself + commit messages. Comments in
|
|
31
|
-
* TS code are deliberately scanned too — a "this is ok as a reference"
|
|
32
|
-
* comment allowlist would open the door to revivals. Anyone who genuinely
|
|
33
|
-
* needs to write about this in a comment uses backticks (`EventLog`) to
|
|
34
|
-
* avoid the exact symbol, or a different spelling.
|
|
35
|
-
*
|
|
36
|
-
* Usage:
|
|
37
|
-
* bun guards/guard-pre-es-patterns.ts (standalone)
|
|
38
|
-
* via _lib/guard-kit shared runner (kumiko check)
|
|
39
|
-
*
|
|
40
|
-
* Exit 1 on violations, 0 when clean.
|
|
41
|
-
*/
|
|
42
|
-
|
|
43
|
-
import * as fs from "node:fs";
|
|
44
|
-
import * as path from "node:path";
|
|
45
|
-
import { type SourceFile, SyntaxKind } from "ts-morph";
|
|
46
|
-
import { type AstGuard, runStandalone, type ScanSpec } from "./_lib/guard-kit";
|
|
47
|
-
|
|
48
|
-
const ROOT = process.cwd();
|
|
49
|
-
|
|
50
|
-
const SCAN: ScanSpec = {
|
|
51
|
-
scope: "source",
|
|
52
|
-
extensions: ["ts"],
|
|
53
|
-
frameworkWithin: ["packages/*/src/**", "samples/**"],
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
// Files that are NOT checked.
|
|
57
|
-
// - dist/ — build output
|
|
58
|
-
// - this guard itself (contains the pattern names in code)
|
|
59
|
-
const EXCLUDE = /(^|\/)(dist|node_modules)\/|guard-pre-es-patterns\.ts$/;
|
|
60
|
-
|
|
61
|
-
type Pattern = {
|
|
62
|
-
readonly name: string;
|
|
63
|
-
readonly description: string;
|
|
64
|
-
readonly check: (sf: SourceFile) => Array<{ line: number; snippet: string }>;
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
// Identifier scan: checks whether a given name is used anywhere as an
|
|
68
|
-
// identifier in the AST (not in strings, not in comments).
|
|
69
|
-
function identifierHits(sf: SourceFile, name: string): Array<{ line: number; snippet: string }> {
|
|
70
|
-
const out: Array<{ line: number; snippet: string }> = [];
|
|
71
|
-
// Cheap text prefilter: a full descendant walk over every file costs seconds.
|
|
72
|
-
if (!sf.getFullText().includes(name)) return out;
|
|
73
|
-
for (const id of sf.getDescendantsOfKind(SyntaxKind.Identifier)) {
|
|
74
|
-
if (id.getText() !== name) continue;
|
|
75
|
-
out.push({
|
|
76
|
-
line: id.getStartLineNumber(),
|
|
77
|
-
snippet: id.getParent()?.getText().slice(0, 120) ?? name,
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
return out;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// String-literal scan: checks whether a given string appears as a literal
|
|
84
|
-
// value (e.g. in map lookups, SQL filters, event-match blocks).
|
|
85
|
-
function stringLiteralHits(
|
|
86
|
-
sf: SourceFile,
|
|
87
|
-
literal: string,
|
|
88
|
-
): Array<{ line: number; snippet: string }> {
|
|
89
|
-
const out: Array<{ line: number; snippet: string }> = [];
|
|
90
|
-
if (!sf.getFullText().includes(literal)) return out;
|
|
91
|
-
for (const lit of sf.getDescendantsOfKind(SyntaxKind.StringLiteral)) {
|
|
92
|
-
if (lit.getLiteralText() !== literal) continue;
|
|
93
|
-
out.push({
|
|
94
|
-
line: lit.getStartLineNumber(),
|
|
95
|
-
snippet: lit.getParent()?.getText().slice(0, 120) ?? literal,
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
for (const lit of sf.getDescendantsOfKind(SyntaxKind.NoSubstitutionTemplateLiteral)) {
|
|
99
|
-
if (lit.getLiteralText() !== literal) continue;
|
|
100
|
-
out.push({
|
|
101
|
-
line: lit.getStartLineNumber(),
|
|
102
|
-
snippet: lit.getParent()?.getText().slice(0, 120) ?? literal,
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
return out;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const PATTERNS: readonly Pattern[] = [
|
|
109
|
-
{
|
|
110
|
-
name: "createEventLog",
|
|
111
|
-
description:
|
|
112
|
-
"Pre-ES Redis-Stream-Activity-Log. Ersetzt durch die events-Tabelle + ctx.loadAggregate / queryProjection.",
|
|
113
|
-
check: (sf) => identifierHits(sf, "createEventLog"),
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
name: "EventLog / EventLogEntry (Type-Import)",
|
|
117
|
-
description:
|
|
118
|
-
"Type-Imports aus dem entfernten pipeline/event-log.ts. Ersetzt durch StoredEvent + getAllProjectionProgress.",
|
|
119
|
-
check: (sf) => [...identifierHits(sf, "EventLog"), ...identifierHits(sf, "EventLogEntry")],
|
|
120
|
-
},
|
|
121
|
-
{
|
|
122
|
-
name: "ctx.emit / emitEvent(ctx, …) pre-E.2",
|
|
123
|
-
description:
|
|
124
|
-
"Sprint-E.2 entfernte ctx.emit + PUBSUB_AGGREGATE_TYPE. Domain-Events gehen via ctx.appendEvent auf Aggregate-Streams.",
|
|
125
|
-
check: (sf) => identifierHits(sf, "PUBSUB_AGGREGATE_TYPE"),
|
|
126
|
-
},
|
|
127
|
-
{
|
|
128
|
-
name: "r.postEvent",
|
|
129
|
-
description:
|
|
130
|
-
"Sprint-E.2 entfernte r.postEvent als Registrar-API. Ersatz: r.multiStreamProjection für Cross-Aggregate-Konsumenten.",
|
|
131
|
-
check: (sf) => {
|
|
132
|
-
// Match `.postEvent(` property accesses to distinguish from the word
|
|
133
|
-
// "postEvent" in prose. Identifier scan would miss method invocations.
|
|
134
|
-
const out: Array<{ line: number; snippet: string }> = [];
|
|
135
|
-
if (!sf.getFullText().includes("postEvent")) return out;
|
|
136
|
-
for (const pa of sf.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression)) {
|
|
137
|
-
if (pa.getName() !== "postEvent") continue;
|
|
138
|
-
// Only flag when invoked as a method — a property read named
|
|
139
|
-
// postEvent on foreign types (theoretical) isn't the registrar API.
|
|
140
|
-
const parent = pa.getParent();
|
|
141
|
-
if (parent?.getKind() !== SyntaxKind.CallExpression) continue;
|
|
142
|
-
out.push({
|
|
143
|
-
line: pa.getStartLineNumber(),
|
|
144
|
-
snippet: pa.getParent()?.getText().slice(0, 120) ?? ".postEvent",
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
return out;
|
|
148
|
-
},
|
|
149
|
-
},
|
|
150
|
-
{
|
|
151
|
-
name: 'aggregateType: "configChanges"',
|
|
152
|
-
description:
|
|
153
|
-
'Pre-ES-Stream-Name. Konsumenten filtern auf aggregateType: "configValue" + Event-Types configValue.created/updated/deleted.',
|
|
154
|
-
check: (sf) => stringLiteralHits(sf, "configChanges"),
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
name: "config:event:config-changed Event-Name",
|
|
158
|
-
description:
|
|
159
|
-
"Pre-ES-Event. Ersetzt durch auto-Lifecycle-Events (configValue.created / .updated / .deleted).",
|
|
160
|
-
check: (sf) => [
|
|
161
|
-
...stringLiteralHits(sf, "config:event:config-changed"),
|
|
162
|
-
...identifierHits(sf, "CONFIG_CHANGED_EVENT_NAME"),
|
|
163
|
-
],
|
|
164
|
-
},
|
|
165
|
-
];
|
|
166
|
-
|
|
167
|
-
export const guard: AstGuard = {
|
|
168
|
-
name: "Pre-ES-Patterns Guard",
|
|
169
|
-
scan: SCAN,
|
|
170
|
-
hint:
|
|
171
|
-
"These APIs were removed with the event-sourcing migration. Use the events table with ctx.loadAggregate / queryProjection, ctx.appendEvent on aggregate streams, " +
|
|
172
|
-
'r.multiStreamProjection for cross-aggregate consumers, and configValue.created/updated/deleted events (aggregateType: "configValue") for config changes.',
|
|
173
|
-
run(files) {
|
|
174
|
-
const violations: Array<{ file: string; line: number; message: string }> = [];
|
|
175
|
-
|
|
176
|
-
for (const sf of files) {
|
|
177
|
-
const file = sf.getFilePath();
|
|
178
|
-
if (EXCLUDE.test(file)) continue;
|
|
179
|
-
|
|
180
|
-
for (const pat of PATTERNS) {
|
|
181
|
-
for (const hit of pat.check(sf)) {
|
|
182
|
-
violations.push({
|
|
183
|
-
file: path.relative(ROOT, file),
|
|
184
|
-
line: hit.line,
|
|
185
|
-
message: `[${pat.name}] ${hit.snippet}`,
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Sanity assertion: pipeline/event-log.ts must not exist.
|
|
192
|
-
const eventLogPath = path.join(ROOT, "packages/framework/src/pipeline/event-log.ts");
|
|
193
|
-
if (fs.existsSync(eventLogPath)) {
|
|
194
|
-
violations.push({
|
|
195
|
-
file: "packages/framework/src/pipeline/event-log.ts",
|
|
196
|
-
line: 0,
|
|
197
|
-
message:
|
|
198
|
-
"BLOCKED: pipeline/event-log.ts was restored. The file must be deleted — the events table now takes over its role.",
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return { violations };
|
|
203
|
-
},
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
if (import.meta.main) runStandalone(guard);
|