@cosmicdrift/kumiko-guards 0.381.0 → 0.383.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-guards",
3
- "version": "0.381.0",
3
+ "version": "0.383.0",
4
4
  "description": "AST-based security guards for Kumiko repos: direct-fs/fetch, tenant escalation, admin-API, escape hatches and related checks, run over a shared ts-morph project.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -51,7 +51,7 @@
51
51
  "LICENSE"
52
52
  ],
53
53
  "peerDependencies": {
54
- "@cosmicdrift/kumiko-framework": "0.381.0"
54
+ "@cosmicdrift/kumiko-framework": "0.383.0"
55
55
  },
56
56
  "peerDependenciesMeta": {
57
57
  "@cosmicdrift/kumiko-framework": {
@@ -59,6 +59,6 @@
59
59
  }
60
60
  },
61
61
  "devDependencies": {
62
- "@cosmicdrift/kumiko-framework": "0.381.0"
62
+ "@cosmicdrift/kumiko-framework": "0.383.0"
63
63
  }
64
64
  }
package/src/changes.json CHANGED
@@ -1,4 +1,11 @@
1
1
  [
2
+ {
3
+ "version": "0.383.0",
4
+ "type": "improvement",
5
+ "title": "Nine guards are removed because types and boot checks replace them",
6
+ "detail": "Removed guards and their replacement. Text-Field Personal-Stance: the field factories require `personal`. API-Object-Form: the registrar only has the object form. Table-DDL (warning only), Pre-ES-Patterns and No-Broker-Subscribe (the broker is not exported). Open-To-All-Reason and Escape-Hatch-Declared: boot and runtime reason validation. Loadall-Events: the function is removed. Section-Fields-Raw: EditFieldsSection is a union. Screen-Conventions loses rule R1 (metrics without label), the type enforces it.",
7
+ "migration": "Delete `.kumiko-text-field-stance-baseline.json` and `.kumiko-section-fields-raw-baseline.json`. In `.kumiko-security-baseline.json` the \"Escape-Hatch-Declared Guard\" and \"Open-To-All-Reason Guard\" entries in `hardFail` can be removed (they are ignored now). Remove leftover `kumiko-lint-ignore section-fields-raw` markers."
8
+ },
2
9
  {
3
10
  "version": "0.379.0",
4
11
  "type": "improvement",
@@ -23,7 +23,7 @@
23
23
  * Skips `openToAll: { reason: "..." }` / `escapeHatch: { reason: "..." }`
24
24
  * and `declareEscapeHatch({ reason: "..." })` — those are prose
25
25
  * access-declaration justifications, enforced instead by
26
- * guard-open-to-all-reason.ts / guard-escape-hatch-declared.ts.
26
+ * the boot validator and the runtime reasonProblem check.
27
27
  *
28
28
  * Non-literal reasons (computed, template strings with interpolation,
29
29
  * identifier references) are assumed to be typed-from-a-const and pass.
@@ -1,8 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  // Usability mistakes in screen definitions that are statically decidable and
3
- // otherwise come back screen by screen: a metric without a label, a
4
- // relatedList section whose rows lead nowhere, a row action labelled "edit"
5
- // that does something else. Opt-in per app repo via _app-test.yml.
3
+ // otherwise come back screen by screen: a relatedList section whose rows
4
+ // lead nowhere, a row action labelled "edit" that does something else. Opt-in per app repo via _app-test.yml.
6
5
 
7
6
  import { type ObjectLiteralExpression, type SourceFile, SyntaxKind } from "ts-morph";
8
7
  import { type AstGuard, type GuardViolation, runStandalone, type ScanSpec } from "./_lib/guard-kit";
@@ -56,40 +55,6 @@ function hasDeclaredProperty(obj: ObjectLiteralExpression, name: string): boolea
56
55
  return init !== undefined && init.getKind() !== SyntaxKind.UndefinedKeyword;
57
56
  }
58
57
 
59
- // R1: MetricSpec's plain-string shorthand carries no label — every metrics
60
- // entry on a projectionDetail screen must be the object form with `label`.
61
- function checkMetrics(
62
- root: ObjectLiteralExpression,
63
- sf: SourceFile,
64
- violations: GuardViolation[],
65
- ): void {
66
- const metricsProp = root.getProperty("metrics");
67
- // skip: screens without a literal metrics array have no entries to check
68
- if (!metricsProp?.isKind(SyntaxKind.PropertyAssignment)) return;
69
- const init = metricsProp.getInitializer();
70
- // skip: a computed metrics value cannot be checked statically
71
- if (!init?.isKind(SyntaxKind.ArrayLiteralExpression)) return;
72
- for (const el of init.getElements()) {
73
- if (hasIgnoreTag(el, IGNORE_TAG)) continue;
74
- if (el.isKind(SyntaxKind.StringLiteral)) {
75
- violations.push({
76
- file: sf.getFilePath(),
77
- line: el.getStartLineNumber(),
78
- message: `metrics entry "${el.getLiteralValue()}" is a plain string (a number with no meaning) — use the object form with \`label\`.`,
79
- });
80
- continue;
81
- }
82
- if (el.isKind(SyntaxKind.ObjectLiteralExpression) && !hasDeclaredProperty(el, "label")) {
83
- violations.push({
84
- file: sf.getFilePath(),
85
- line: el.getStartLineNumber(),
86
- message:
87
- "metrics entry without `label` (a number with no meaning) — the object form needs a label.",
88
- });
89
- }
90
- }
91
- }
92
-
93
58
  // R2: a relatedList section with neither rowClick nor rowActions renders a
94
59
  // list nobody can act on.
95
60
  function checkRelatedListSections(
@@ -142,8 +107,8 @@ export const guard: AstGuard = {
142
107
  name: "Screen-Conventions Guard",
143
108
  scan: SCAN,
144
109
  hint:
145
- "metrics entries need the object form with `label`; relatedList sections need " +
146
- '`rowClick` or `rowActions`; a rowAction label must not end in ".action.edit" ' +
110
+ "relatedList sections need `rowClick` or `rowActions`; a rowAction label " +
111
+ 'must not end in ".action.edit" ' +
147
112
  'when the `id` is not "edit". ' +
148
113
  `Justified exception: // ${IGNORE_TAG} <reason>`,
149
114
  run(files: readonly SourceFile[]) {
@@ -151,7 +116,6 @@ export const guard: AstGuard = {
151
116
  for (const sf of files) {
152
117
  if (EXCLUDE.test(sf.getFilePath())) continue;
153
118
  for (const root of findScreenObjectLiterals(sf)) {
154
- checkMetrics(root, sf, violations);
155
119
  checkRelatedListSections(root, sf, violations);
156
120
  checkRowActions(root, sf, violations);
157
121
  }
package/src/run-guards.ts CHANGED
@@ -25,14 +25,11 @@ import { guard as complexity } from "./check-complexity";
25
25
  import { guard as predicateExtraction } from "./check-predicates";
26
26
  import { guard as accessDeniedTest } from "./guard-access-denied-test";
27
27
  import { guard as adminApi } from "./guard-admin-api";
28
- import { guard as apiObjectForm } from "./guard-api-object-form";
29
28
  import { guard as appFeatureStructure } from "./guard-app-feature-structure";
30
- import { guard as brokerSubscribe } from "./guard-broker-subscribe";
31
29
  import { guard as crossFeatureImports } from "./guard-cross-feature-imports";
32
30
  import { guard as directEntityWrites } from "./guard-direct-entity-writes";
33
31
  import { guard as directFetch } from "./guard-direct-fetch";
34
32
  import { guard as errorReasons } from "./guard-error-reasons";
35
- import { guard as escapeHatchDeclared } from "./guard-escape-hatch-declared";
36
33
  import { guard as eventStoreWrites } from "./guard-event-store-writes";
37
34
  import { guard as fakeTests } from "./guard-fake-tests";
38
35
  import { guard as htmlEscape } from "./guard-html-escape";
@@ -41,23 +38,17 @@ import { guard as i18nLocaleMount } from "./guard-i18n-locale-mount";
41
38
  import { guard as i18nLocaleTerminology } from "./guard-i18n-locale-terminology";
42
39
  import { guard as importExtensions } from "./guard-import-extensions";
43
40
  import { guard as libTestCoverage } from "./guard-lib-test-coverage";
44
- import { guard as loadallEvents } from "./guard-loadall-events";
45
41
  import { guard as noDateApi } from "./guard-no-date-api";
46
42
  import { guard as noDirectFs } from "./guard-no-direct-fs";
47
43
  import { guard as noLogicInViews } from "./guard-no-logic-in-views";
48
44
  import { guard as noTemporalPolyfillImport } from "./guard-no-temporal-polyfill-import";
49
- import { guard as openToAllReason } from "./guard-open-to-all-reason";
50
45
  import { guard as piiAnnotations } from "./guard-pii-annotations";
51
- import { guard as preEsPatterns } from "./guard-pre-es-patterns";
52
46
  import { guard as restrictedSymbols } from "./guard-restricted-symbols";
53
47
  import { guard as screenConventions } from "./guard-screen-conventions";
54
- import { guard as sectionFieldsRaw } from "./guard-section-fields-raw";
55
48
  import { guard as silentSkip } from "./guard-silent-skip";
56
- import { guard as tableDdl } from "./guard-table-ddl";
57
49
  import { guard as tenantEscalation } from "./guard-tenant-escalation";
58
50
  import { guard as testTemplateDrift } from "./guard-test-template-drift";
59
51
  import { guard as testTimeouts } from "./guard-test-timeouts";
60
- import { guard as textFieldStance } from "./guard-text-field-stance";
61
52
  import { guard as unsafeAppendEvent } from "./guard-unsafe-append-event";
62
53
  import { guard as unsafeJsonParse } from "./guard-unsafe-json-parse";
63
54
  import { guard as writeHandlerQns } from "./guard-write-handler-qns";
@@ -65,14 +56,10 @@ import { guard as writeHandlerQns } from "./guard-write-handler-qns";
65
56
  export const GUARDS = [
66
57
  accessDeniedTest,
67
58
  adminApi,
68
- apiObjectForm,
69
59
  directEntityWrites,
70
60
  directFetch,
71
- escapeHatchDeclared,
72
61
  noDirectFs,
73
- openToAllReason,
74
62
  tenantEscalation,
75
- preEsPatterns,
76
63
  unsafeJsonParse,
77
64
  silentSkip,
78
65
  htmlEscape,
@@ -83,22 +70,17 @@ export const GUARDS = [
83
70
  restrictedSymbols,
84
71
  fakeTests,
85
72
  noLogicInViews,
86
- brokerSubscribe,
87
73
  errorReasons,
88
74
  i18nKeys,
89
75
  i18nLocaleMount,
90
76
  i18nLocaleTerminology,
91
77
  piiAnnotations,
92
- textFieldStance,
93
78
  complexity,
94
79
  predicateExtraction,
95
80
  screenConventions,
96
- sectionFieldsRaw,
97
81
  writeHandlerQns,
98
82
  asCasts,
99
- loadallEvents,
100
83
  eventStoreWrites,
101
- tableDdl,
102
84
  appFeatureStructure,
103
85
  libTestCoverage,
104
86
  testTimeouts,
@@ -1,211 +0,0 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * Guard: the registrar methods `r.writeHandler`, `r.queryHandler`, `r.entity`,
4
- * `r.requires` and `r.hook` only take the object form; `registerEntityCrud(r, …)`
5
- * is replaced by `r.crud(…)`.
6
- *
7
- * The positional-shape and receiver rules mirror the object-form codemod in
8
- * kumiko-framework (scripts/codemod/object-form.ts). This package does not
9
- * depend on the framework, so the two copies are held together by the parity
10
- * test in __tests__/guard-api-object-form.test.ts.
11
- *
12
- * Usage:
13
- * bun guards/guard-api-object-form.ts
14
- */
15
- import {
16
- type CallExpression,
17
- Node,
18
- type ParameterDeclaration,
19
- type SourceFile,
20
- SyntaxKind,
21
- } from "ts-morph";
22
- import {
23
- type AstGuard,
24
- type GuardViolation,
25
- relativeToCwd,
26
- runStandalone,
27
- type ScanSpec,
28
- } from "./_lib/guard-kit";
29
-
30
- const SCAN: ScanSpec = {
31
- scope: "source+tests",
32
- extensions: ["ts", "tsx"],
33
- frameworkWithin: ["packages/*/src/**", "samples/**"],
34
- };
35
-
36
- const REGISTRAR_METHODS = ["writeHandler", "queryHandler", "entity", "requires", "hook"] as const;
37
- type RegistrarMethod = (typeof REGISTRAR_METHODS)[number];
38
-
39
- const CRUD_FUNCTION_NAME = "registerEntityCrud";
40
- const REGISTRAR_TYPE_MARKER = "FeatureRegistrar";
41
- const FEATURE_FACTORY_NAME = "defineFeature";
42
- const REGISTRAR_RECEIVER_NAMES: ReadonlySet<string> = new Set(["r", "registrar"]);
43
- const EXCLUDED_PATH = /(\/__tests__\/fixtures\/|\.d\.ts$)/;
44
- // Cheap text superset of what positionalCallMethod can match; skips the AST walk for most files.
45
- const REGISTRAR_METHOD_ACCESS = new RegExp(`\\.\\s*(?:${REGISTRAR_METHODS.join("|")})\\b`);
46
- const UPGRADE_HINT = "run `bun kumiko-upgrade --apply` (codemod object-form)";
47
-
48
- type ReceiverResolution = "registrar" | "other" | "unresolved";
49
-
50
- function isRegistrarMethod(name: string): name is RegistrarMethod {
51
- return REGISTRAR_METHODS.some((method) => method === name);
52
- }
53
-
54
- function isFeatureFactoryCallee(callee: Node): boolean {
55
- if (Node.isIdentifier(callee)) return callee.getText() === FEATURE_FACTORY_NAME;
56
- if (Node.isPropertyAccessExpression(callee)) return callee.getName() === FEATURE_FACTORY_NAME;
57
- return false;
58
- }
59
-
60
- function isFeatureCallbackParameter(param: ParameterDeclaration): boolean {
61
- const fn = param.getParent();
62
- if (!Node.isArrowFunction(fn) && !Node.isFunctionExpression(fn)) return false;
63
- if (fn.getParameters()[0] !== param) return false;
64
- const call = fn.getParentIfKind(SyntaxKind.CallExpression);
65
- return (
66
- call !== undefined &&
67
- call.getArguments()[1] === fn &&
68
- isFeatureFactoryCallee(call.getExpression())
69
- );
70
- }
71
-
72
- function resolveParameter(param: ParameterDeclaration): ReceiverResolution {
73
- if (isFeatureCallbackParameter(param)) return "registrar";
74
- const typeNode = param.getTypeNode();
75
- if (typeNode === undefined) return "unresolved";
76
- return typeNode.getText().includes(REGISTRAR_TYPE_MARKER) ? "registrar" : "other";
77
- }
78
-
79
- function declaredVariableIn(scope: Node, name: string): ReceiverResolution | undefined {
80
- if (!Node.isBlock(scope) && !Node.isSourceFile(scope) && !Node.isModuleBlock(scope)) {
81
- return undefined;
82
- }
83
- for (const statement of scope.getStatements()) {
84
- if (!Node.isVariableStatement(statement)) continue;
85
- for (const declaration of statement.getDeclarations()) {
86
- if (declaration.getNameNode().getText() !== name) continue;
87
- const typeText = declaration.getTypeNode()?.getText();
88
- if (typeText === undefined) return "unresolved";
89
- return typeText.includes(REGISTRAR_TYPE_MARKER) ? "registrar" : "other";
90
- }
91
- }
92
- return undefined;
93
- }
94
-
95
- // Nearest binding of the receiver's name decides; no type checker involved.
96
- function resolveReceiver(receiver: Node): ReceiverResolution {
97
- if (!Node.isIdentifier(receiver)) return "other";
98
- const name = receiver.getText();
99
- for (
100
- let scope: Node | undefined = receiver.getParent();
101
- scope !== undefined;
102
- scope = scope.getParent()
103
- ) {
104
- if (Node.isParametered(scope)) {
105
- const param = scope.getParameters().find((p) => p.getNameNode().getText() === name);
106
- if (param !== undefined) return resolveParameter(param);
107
- }
108
- const variable = declaredVariableIn(scope, name);
109
- if (variable !== undefined) return variable;
110
- }
111
- return "unresolved";
112
- }
113
-
114
- function isRegistrarOrLookalike(receiver: Node): boolean {
115
- const resolution = resolveReceiver(receiver);
116
- if (resolution === "registrar") return true;
117
- return (
118
- resolution === "unresolved" &&
119
- Node.isIdentifier(receiver) &&
120
- REGISTRAR_RECEIVER_NAMES.has(receiver.getText())
121
- );
122
- }
123
-
124
- function isPositionalShape(method: RegistrarMethod, args: readonly Node[]): boolean {
125
- if (method === "requires") return args.some((arg) => !Node.isObjectLiteralExpression(arg));
126
- if (args.some((arg) => Node.isSpreadElement(arg))) return true;
127
- return args.length >= (method === "hook" ? 3 : 2);
128
- }
129
-
130
- function crudLocalNames(sf: SourceFile): Set<string> {
131
- const names = new Set<string>([CRUD_FUNCTION_NAME]);
132
- for (const specifier of sf.getDescendantsOfKind(SyntaxKind.ImportSpecifier)) {
133
- if (specifier.getName() !== CRUD_FUNCTION_NAME) continue;
134
- names.add(specifier.getAliasNode()?.getText() ?? CRUD_FUNCTION_NAME);
135
- }
136
- return names;
137
- }
138
-
139
- function positionalCallMethod(call: CallExpression): RegistrarMethod | undefined {
140
- const callee = call.getExpression();
141
- if (!Node.isPropertyAccessExpression(callee)) return undefined;
142
- const method = callee.getName();
143
- if (!isRegistrarMethod(method)) return undefined;
144
- if (!isPositionalShape(method, call.getArguments())) return undefined;
145
- return isRegistrarOrLookalike(callee.getExpression()) ? method : undefined;
146
- }
147
-
148
- export type ObjectFormFinding = { file: string; line: number; message: string };
149
-
150
- function positionalCallFindings(sf: SourceFile, file: string): ObjectFormFinding[] {
151
- if (!REGISTRAR_METHOD_ACCESS.test(sf.getFullText())) return [];
152
- const out: ObjectFormFinding[] = [];
153
- for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
154
- const method = positionalCallMethod(call);
155
- if (method === undefined) continue;
156
- out.push({
157
- file,
158
- line: call.getStartLineNumber(),
159
- message: `positional r.${method}(…) removed in 0.366 — ${UPGRADE_HINT}`,
160
- });
161
- }
162
- return out;
163
- }
164
-
165
- function crudFindings(sf: SourceFile, file: string): ObjectFormFinding[] {
166
- // Every crud local name is CRUD_FUNCTION_NAME itself or an import alias of it.
167
- if (!sf.getFullText().includes(CRUD_FUNCTION_NAME)) return [];
168
- const crudNames = crudLocalNames(sf);
169
- const out: ObjectFormFinding[] = [];
170
- for (const id of sf.getDescendantsOfKind(SyntaxKind.Identifier)) {
171
- if (!crudNames.has(id.getText())) continue;
172
- const parent = id.getParent();
173
- const isCall = Node.isCallExpression(parent) && parent.getExpression() === id;
174
- const isImportOrExport =
175
- Node.isImportSpecifier(parent) ||
176
- Node.isExportSpecifier(parent) ||
177
- id.getFirstAncestorByKind(SyntaxKind.ImportDeclaration) !== undefined;
178
- if (!isCall && !isImportOrExport) continue;
179
- if (Node.isImportSpecifier(parent) && parent.getAliasNode() === id) continue;
180
- out.push({
181
- file,
182
- line: id.getStartLineNumber(),
183
- message: `registerEntityCrud removed in 0.366 — use r.crud(…); ${UPGRADE_HINT}`,
184
- });
185
- }
186
- return out;
187
- }
188
-
189
- export function findPositionalRegistrarCalls(files: readonly SourceFile[]): ObjectFormFinding[] {
190
- const out: ObjectFormFinding[] = [];
191
- for (const sf of files) {
192
- if (EXCLUDED_PATH.test(sf.getFilePath())) continue;
193
- const file = relativeToCwd(sf.getFilePath());
194
- out.push(...positionalCallFindings(sf, file), ...crudFindings(sf, file));
195
- }
196
- return out;
197
- }
198
-
199
- export const guard: AstGuard = {
200
- name: "API-Object-Form Guard",
201
- scan: SCAN,
202
- hint: "Run `bun kumiko-upgrade --apply` (codemod object-form) or rewrite the call to the object form by hand; registerEntityCrud(r, …) becomes r.crud(…).",
203
- run(files) {
204
- const violations: GuardViolation[] = findPositionalRegistrarCalls(files);
205
- return { violations };
206
- },
207
- };
208
-
209
- if (import.meta.main) {
210
- runStandalone(guard);
211
- }
@@ -1,99 +0,0 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * Guard R8: forbids `broker.subscribe(...)` outside the framework.
4
- *
5
- * Feature and app code must NEVER subscribe to the event broker directly — the
6
- * registrar API is the only allowed way to consume events:
7
- * - `r.onEvent("event-name", handler)`
8
- * - `r.job({ trigger: { on: "event-name" }, handler })`
9
- *
10
- * A direct `broker.subscribe(...)` bypasses lifecycle, idempotency, dedup
11
- * and replay of the dispatcher — exactly the guarantees the registrar API
12
- * gives. Today there is NO such call in the Kumiko code (the broker is
13
- * framework-internal, not exported) — the guard is a tripwire that fires as
14
- * soon as someone introduces a broker abstraction and hooks feature code
15
- * directly onto it.
16
- *
17
- * Detection (name heuristic, no type resolution): `X.subscribe(...)` where the
18
- * terminal identifier of `X` is `broker` or `eventBroker` (case-
19
- * insensitive — catches `broker`, `eventBroker`, `ctx.broker`, `this.eventBroker`).
20
- * Store/observable `.subscribe` (RxJS, React `controller.subscribe`) is not
21
- * named `broker` → no hit.
22
- *
23
- * Exception: `packages/framework/src/pipeline/**` — the broker plumbing
24
- * itself lives there (framework-internal, allowed).
25
- *
26
- * Usage: bun guards/guard-broker-subscribe.ts
27
- * Exit 1 on a finding, 0 when clean.
28
- */
29
-
30
- import * as path from "node:path";
31
- import { type SourceFile, SyntaxKind } from "ts-morph";
32
- import { type AstGuard, runStandalone, type ScanSpec } from "./_lib/guard-kit";
33
-
34
- const ROOT = process.cwd();
35
-
36
- const SCAN: ScanSpec = {
37
- scope: "source",
38
- extensions: ["ts"],
39
- frameworkWithin: ["packages/*/src/**", "samples/**"],
40
- };
41
-
42
- const EXCLUDE = /(__tests__|\.test\.ts$|\.integration\.ts$|\.d\.ts$|\.g\.ts$)/;
43
-
44
- // Broker-Plumbing selbst — framework-intern, darf subscriben.
45
- const ALLOW = /(^|\/)packages\/framework\/src\/pipeline\//;
46
-
47
- const BROKER_RECEIVER = /^(event)?broker$/i;
48
-
49
- export function isAllowed(filePath: string): boolean {
50
- return ALLOW.test(path.relative(ROOT, filePath));
51
- }
52
-
53
- /** Terminaler Bezeichner einer Receiver-Expression: `ctx.eventBroker` → "eventBroker". */
54
- function receiverName(node: import("ts-morph").Node): string | undefined {
55
- if (node.getKind() === SyntaxKind.Identifier) return node.getText();
56
- const pae = node.asKind(SyntaxKind.PropertyAccessExpression);
57
- return pae?.getName();
58
- }
59
-
60
- export function collectBrokerSubscribeViolations(
61
- sf: SourceFile,
62
- ): Array<{ line: number; message: string }> {
63
- const hits: Array<{ line: number; message: string }> = [];
64
- for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
65
- const callee = call.getExpression();
66
- const pae = callee.asKind(SyntaxKind.PropertyAccessExpression);
67
- if (pae?.getName() !== "subscribe") continue;
68
- const recv = receiverName(pae.getExpression());
69
- if (!recv || !BROKER_RECEIVER.test(recv)) continue;
70
- hits.push({
71
- line: call.getStartLineNumber(),
72
- message: `[${recv}.subscribe] ${recv}.subscribe(...) — use r.onEvent(...) or r.job({ trigger: { on } })`,
73
- });
74
- }
75
- return hits;
76
- }
77
-
78
- export const guard: AstGuard = {
79
- name: "No-Broker-Subscribe Guard",
80
- scan: SCAN,
81
- hint: "Consume events through the registrar API (r.onEvent / r.job trigger.on), not directly on the broker — see docs/plans/architecture/lint-rules.md (R8).",
82
- run(files) {
83
- const violations: Array<{ file: string; line: number; message: string }> = [];
84
- for (const sf of files) {
85
- const file = sf.getFilePath();
86
- if (EXCLUDE.test(file) || isAllowed(file)) continue;
87
- for (const hit of collectBrokerSubscribeViolations(sf)) {
88
- violations.push({
89
- file: path.relative(ROOT, file),
90
- line: hit.line,
91
- message: hit.message,
92
- });
93
- }
94
- }
95
- return { violations };
96
- },
97
- };
98
-
99
- if (import.meta.main) runStandalone(guard);