@cosmicdrift/kumiko-guards 0.377.0 → 0.379.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.
@@ -68,6 +68,8 @@ type Pattern = {
68
68
  // identifier in the AST (not in strings, not in comments).
69
69
  function identifierHits(sf: SourceFile, name: string): Array<{ line: number; snippet: string }> {
70
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;
71
73
  for (const id of sf.getDescendantsOfKind(SyntaxKind.Identifier)) {
72
74
  if (id.getText() !== name) continue;
73
75
  out.push({
@@ -85,6 +87,7 @@ function stringLiteralHits(
85
87
  literal: string,
86
88
  ): Array<{ line: number; snippet: string }> {
87
89
  const out: Array<{ line: number; snippet: string }> = [];
90
+ if (!sf.getFullText().includes(literal)) return out;
88
91
  for (const lit of sf.getDescendantsOfKind(SyntaxKind.StringLiteral)) {
89
92
  if (lit.getLiteralText() !== literal) continue;
90
93
  out.push({
@@ -129,6 +132,7 @@ const PATTERNS: readonly Pattern[] = [
129
132
  // Match `.postEvent(` property accesses to distinguish from the word
130
133
  // "postEvent" in prose. Identifier scan would miss method invocations.
131
134
  const out: Array<{ line: number; snippet: string }> = [];
135
+ if (!sf.getFullText().includes("postEvent")) return out;
132
136
  for (const pa of sf.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression)) {
133
137
  if (pa.getName() !== "postEvent") continue;
134
138
  // Only flag when invoked as a method — a property read named
@@ -6,9 +6,9 @@
6
6
  * belongs in `src/web` instead.
7
7
  *
8
8
  * `src/web/**` is the browser layer and not scanned at all: it may import
9
- * everything. `src/headless/**` and `src/live/**` only get the import rules:
10
- * the apex page builder emits HTML strings and the live dispatcher is a fetch
11
- * client, so the symbol and JSX-tag rules would be noise there. They stay free
9
+ * everything. `src/headless/**`, `src/flow-diagram/**` and `src/live/**` only get
10
+ * the import rules: the apex page builder emits HTML strings, the flow diagram
11
+ * emits SVG strings and the live dispatcher is a fetch client, so the symbol and JSX-tag rules would be noise there. They stay free
12
12
  * of react-dom, jsdom and any import of `src/web`.
13
13
  *
14
14
  * Deliberately simple: a regex scan over the source text. False positives
@@ -70,7 +70,7 @@ const FORBIDDEN_SYMBOLS = [
70
70
  const FORBIDDEN_JSX_TAG = /(^|\s|>|\()<([a-z][a-zA-Z0-9-]*)(?=[\s/>])/g;
71
71
 
72
72
  // Subtrees of packages/renderer/src that carry only the import rules.
73
- const IMPORT_RULES_ONLY_DIRS = ["headless", "live"] as const;
73
+ const IMPORT_RULES_ONLY_DIRS = ["headless", "flow-diagram", "live"] as const;
74
74
 
75
75
  // The browser layer may import anything, so it is not scanned.
76
76
  const UNSCANNED_TOP_LEVEL_DIRS = ["web"] as const;
@@ -42,6 +42,10 @@ const RESTRICTED_SYMBOLS = new Set([
42
42
  // an audited unsafeRaw escape hatch, so new callers need an explicit allowlist entry.
43
43
  "appendEventInTenantDb",
44
44
  "getStreamVersionInTenantDb",
45
+ // Grants the ES-write capability on the tenant being destroyed; only the destroy stage and its definition may use it.
46
+ "createTenantDataDestroyDb",
47
+ // Test seam that writes event-fed tables without events; runtime code importing it would be a back door.
48
+ "withExecutorManagedWritesForTests",
45
49
  ]);
46
50
 
47
51
  const ALLOWLIST = [
@@ -50,6 +54,10 @@ const ALLOWLIST = [
50
54
  /^packages\/framework\/src\/event-store\/tenant-db-append\.ts$/,
51
55
  /^packages\/bundled-features\/src\/cap-counter\/cap-reservation\.ts$/,
52
56
  /^packages\/bundled-features\/src\/user-data-rights-defaults\/hooks\/delivery-attempt\.userdata-hook\.ts$/,
57
+ /^packages\/framework\/src\/db\/tenant-db\.ts$/,
58
+ /^packages\/framework\/src\/db\/index\.ts$/,
59
+ /^packages\/bundled-features\/src\/tenant-lifecycle\/stages\.ts$/,
60
+ /^packages\/testing\/src\/helpers\/(seed|db-cleanup)\.ts$/,
53
61
  /^packages\/bundled-features\/src\/tenant\/seeding\.ts$/,
54
62
  /^packages\/bundled-features\/src\/tier-engine\/feature\.ts$/,
55
63
  ];
@@ -121,7 +129,7 @@ function findRestrictedSymbolReferences(sf: SourceFile): Violation[] {
121
129
  export const guard: AstGuard = {
122
130
  name: "Restricted-Symbols Guard",
123
131
  scan: SCAN,
124
- hint: 'getUnscopedAggregateStream{MaxVersion,Tenant} is an existence oracle for foreign tenants — only seed-/system-internal code may reference them. appendEventInTenantDb/getStreamVersionInTenantDb bypass the feature-ownership check of ctx.appendEvent — only framework-internal coordination code may use them. New caller needed? Extend the allowlist in guard-restricted-symbols.ts, with a reason. Known gap: `export * from "...event-store"` is not detected (named imports, re-exports, and namespace property access are covered).',
132
+ hint: 'getUnscopedAggregateStream{MaxVersion,Tenant} is an existence oracle for foreign tenants — only seed-/system-internal code may reference them. appendEventInTenantDb/getStreamVersionInTenantDb bypass the feature-ownership check of ctx.appendEvent — only framework-internal coordination code may use them. createTenantDataDestroyDb grants direct writes on event-fed tables and belongs to the tenant-destroy stage only. withExecutorManagedWritesForTests is the test-seed seam behind seedRow/deleteRows and must not reach runtime code. New caller needed? Extend the allowlist in guard-restricted-symbols.ts, with a reason. Known gap: `export * from "...event-store"` is not detected (named imports, re-exports, and namespace property access are covered).',
125
133
  run(files, roots = resolveRepoRoots()) {
126
134
  const violations: Array<{ file: string; line: number; message: string }> = [];
127
135
 
package/src/index.ts CHANGED
@@ -21,6 +21,7 @@ export {
21
21
  type ScanSpec,
22
22
  type SuiteInventory,
23
23
  } from "./_lib/guard-kit";
24
+ export { GUARDS_DOC_REL, renderGuardsDoc, writeGuardsDoc } from "./_lib/guards-doc";
24
25
  export { findLocalRepo, type RepoRoot, resolveRepoRoots } from "./_lib/roots";
25
26
  export { type RootScan, type ScanOptions, scanFiles, scanRoots } from "./_lib/scan-scope";
26
27
  export {
package/src/run-guards.ts CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  runGuards,
19
19
  } from "./_lib/guard-kit";
20
20
  import type { RepoRoot } from "./_lib/roots";
21
+ import { acquireGuardRunSlot } from "./_lib/run-slots";
21
22
  import { writeSecurityBaselines } from "./_lib/security-baseline-cli";
22
23
  import { guard as asCasts } from "./check-as-casts";
23
24
  import { guard as complexity } from "./check-complexity";
@@ -176,9 +177,15 @@ export function runGuardsCli(argv: readonly string[], deps: RunGuardsCliDeps = {
176
177
  }
177
178
  const strictSecurityBaseline = flags.includes("--strict-security-baseline");
178
179
  const guards = strictSecurityBaseline ? GUARDS.filter(isSecurityGuard) : GUARDS;
179
- const project = buildSharedProject(guards);
180
- printGuardKitBanner(guards.length, project);
181
- return reportResults(runGuards(guards, project, { strictSecurityBaseline }));
180
+ // Taken before the ts-morph project is built: that is where the memory goes.
181
+ const releaseSlot = acquireGuardRunSlot();
182
+ try {
183
+ const project = buildSharedProject(guards);
184
+ printGuardKitBanner(guards.length, project);
185
+ return reportResults(runGuards(guards, project, { strictSecurityBaseline }));
186
+ } finally {
187
+ releaseSlot();
188
+ }
182
189
  }
183
190
 
184
191
  // Only run on direct invocation — otherwise `import { GUARDS }` would kick
@@ -7,8 +7,10 @@ import { check as realProviderIsolation } from "./check-real-provider-isolation"
7
7
  import { check as runtimeIsolation } from "./check-runtime-isolation";
8
8
  import { check as secretLiterals } from "./check-secret-literals";
9
9
  import { check as singleRuntimeInstance } from "./check-single-runtime-instance";
10
+ import { check as subpathExports } from "./check-subpath-exports";
10
11
  import { check as agentDocsDrift } from "./guard-agent-docs-drift";
11
12
  import { check as featureIntegrationTests } from "./guard-feature-integration-tests";
13
+ import { check as guardsDocDrift } from "./guard-guards-doc-drift";
12
14
  import { check as noDirectProcessEnv } from "./guard-no-direct-process-env";
13
15
  import { check as primitivesDiscipline } from "./guard-primitives-discipline";
14
16
  import { check as rawSql } from "./guard-raw-sql";
@@ -31,6 +33,8 @@ export const REPO_CHECKS = [
31
33
  realProviderIsolation,
32
34
  singleRuntimeInstance,
33
35
  agentDocsDrift,
36
+ subpathExports,
37
+ guardsDocDrift,
34
38
  ];
35
39
 
36
40
  export const REPO_CHECK_FLAGS: readonly string[] = ["--write-baseline"];
@@ -49,6 +49,7 @@ const PATH_RULES: readonly (readonly [RegExp, Runtime])[] = [
49
49
  // sample and import kumiko-testing/e2e; without this rule they read as
50
50
  // runtime code importing the test tier. Deliberately not any nested e2e/.
51
51
  [/^(?:e2e|packages\/[^/]+\/e2e|samples\/(?:.+\/)?e2e)\//, "test"],
52
+ [/(?:^|\/)dev\//, "dev"],
52
53
  [/(?:^|\/)scripts\//, "tooling"],
53
54
  [/(?:^|\/)bin\//, "tooling"],
54
55
  [/\/drizzle\/[^/]+\.ts$/, "tooling"],
@@ -347,7 +348,10 @@ export function classifyByDirective(
347
348
  readHead: HeadReader = readFileHead,
348
349
  ): Runtime | null {
349
350
  const head = readHead(filePath);
350
- if (head === null) return null;
351
+ return head === null ? null : directiveInHead(head);
352
+ }
353
+
354
+ export function directiveInHead(head: string): Runtime | null {
351
355
  for (const line of head.split("\n").slice(0, DIRECTIVE_HEAD_LINES)) {
352
356
  const m = line.match(/\/\/\s*@runtime\s+(\w+)/);
353
357
  const declared = m?.[1];