@cosmicdrift/kumiko-guards 0.387.0 → 0.389.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.
@@ -335,16 +335,51 @@ function extractMarkerReason(node: FnNode): string | null {
335
335
  return null;
336
336
  }
337
337
 
338
+ // A typed lookup table (`const T: Record<K, Fn> = {…}` or `{…} satisfies …`) fixes one
339
+ // signature per slot; a one-call entry adapts that signature, so inlining it would mean
340
+ // dropping the table, not the wrapper.
341
+ function isTypedTableLiteral(literal: Node): boolean {
342
+ let parent = literal.getParent();
343
+ while (parent?.isKind(SyntaxKind.AsExpression)) parent = parent.getParent();
344
+ if (parent?.isKind(SyntaxKind.SatisfiesExpression)) return true;
345
+ return parent?.isKind(SyntaxKind.VariableDeclaration) === true && !!parent.getTypeNode();
346
+ }
347
+
348
+ function collectTypedTableValueNames(sf: SourceFile): Set<string> {
349
+ const names = new Set<string>();
350
+ for (const literal of sf.getDescendantsOfKind(SyntaxKind.ObjectLiteralExpression)) {
351
+ if (!isTypedTableLiteral(literal)) continue;
352
+ for (const property of literal.getProperties()) {
353
+ if (property.isKind(SyntaxKind.ShorthandPropertyAssignment)) names.add(property.getName());
354
+ if (!property.isKind(SyntaxKind.PropertyAssignment)) continue;
355
+ const value = property.getInitializer();
356
+ if (value?.isKind(SyntaxKind.Identifier)) names.add(value.getText());
357
+ }
358
+ }
359
+ return names;
360
+ }
361
+
362
+ function isInlineTypedTableValue(node: FnNode): boolean {
363
+ const property = node.getParent();
364
+ const literal = property?.isKind(SyntaxKind.PropertyAssignment)
365
+ ? property.getParent()
366
+ : undefined;
367
+ return literal !== undefined && isTypedTableLiteral(literal);
368
+ }
369
+
338
370
  // ── Scan ──────────────────────────────────────────────────────────────────
339
371
 
340
372
  export function collectFindings(sf: SourceFile): Finding[] {
341
373
  const file = path.relative(ROOT, sf.getFilePath());
342
374
  const findings: Finding[] = [];
375
+ const typedTableValueNames = collectTypedTableValueNames(sf);
343
376
 
344
377
  const check = (node: FnNode) => {
345
378
  const fnName = getFnName(node);
346
379
  // skip: anonymous functions cannot be reported by name
347
380
  if (!fnName) return;
381
+ // skip: a typed table slot, see isTypedTableLiteral
382
+ if (typedTableValueNames.has(fnName) || isInlineTypedTableValue(node)) return;
348
383
 
349
384
  const callee = getSingleDirectCallee(node);
350
385
  // skip: not a single-callee wrapper
package/src/pre-push.sh CHANGED
@@ -18,8 +18,8 @@
18
18
  # sub-repo's own package.json `test` script — refuses the push if that
19
19
  # script is missing.
20
20
  #
21
- # Before the checks below, the hook runs a gitleaks scan of origin/main..HEAD
22
- # when the binary is installed.
21
+ # Before the checks below, the hook runs a gitleaks scan of origin/main..<pushed sha>
22
+ # for every pushed ref when the binary is installed.
23
23
  #
24
24
  # Extension point: a tracked, executable scripts/pre-push-extra.sh runs
25
25
  # before the main check in every branch above, with KUMIKO_PUSH_REPO_ROOT
@@ -123,11 +123,27 @@ if ! command -v gitleaks >/dev/null 2>&1; then
123
123
  elif ! git -C "$REPO_ROOT" rev-parse --verify --quiet origin/main >/dev/null 2>&1; then
124
124
  echo "[pre-push] origin/main not found — secret scan skipped" >&2
125
125
  else
126
- echo "[pre-push] gitleaks git origin/main..HEAD"
127
- if ! (cd "$REPO_ROOT" && gitleaks git --no-banner --redact --log-opts="origin/main..HEAD" .); then
128
- echo "[pre-push] gitleaks found secrets — refusing push" >&2
129
- exit 1
126
+ # git passes "<local ref> <local sha> <remote ref> <remote sha>" per pushed ref on stdin;
127
+ # scan those tips (not HEAD), skip deletes. Without stdin (direct call) fall back to HEAD.
128
+ SCAN_TIPS=""
129
+ PUSH_LINES=0
130
+ if [ ! -t 0 ]; then
131
+ while read -r _local_ref LOCAL_SHA _remote_ref _remote_sha; do
132
+ [ -n "$LOCAL_SHA" ] || continue
133
+ PUSH_LINES=$((PUSH_LINES + 1))
134
+ case "$LOCAL_SHA" in *[!0]*) SCAN_TIPS="$SCAN_TIPS $LOCAL_SHA" ;; esac
135
+ done
136
+ fi
137
+ if [ "$PUSH_LINES" -eq 0 ]; then
138
+ SCAN_TIPS=" HEAD"
130
139
  fi
140
+ for SCAN_TIP in $SCAN_TIPS; do
141
+ echo "[pre-push] gitleaks git origin/main..$SCAN_TIP"
142
+ if ! (cd "$REPO_ROOT" && gitleaks git --no-banner --redact --log-opts="origin/main..$SCAN_TIP" .); then
143
+ echo "[pre-push] gitleaks found secrets — refusing push" >&2
144
+ exit 1
145
+ fi
146
+ done
131
147
  fi
132
148
 
133
149
  # infra#559/#569: from a worktree, `--git-common-dir` resolves to the main
@@ -135,13 +151,25 @@ fi
135
151
  # canonical sibling path instead of this worktree — checking the wrong
136
152
  # code. Run the worktree-local check only when scripts/check-wt.sh is
137
153
  # tracked and executable (untracked/symlink stubs must not hijack push).
138
- # A check-wt.sh that wraps this hook sets KUMIKO_CHECK_WT=1 so it is not re-entered.
139
- if [ "${KUMIKO_CHECK_WT:-0}" != "1" ] \
154
+ # Re-entry from the wrapping check-wt.sh is detected by process ancestry: the
155
+ # hook records its own PID in KUMIKO_CHECK_WT_OWNER before delegating, so a
156
+ # freely exported variable cannot skip the tracked check-wt.sh.
157
+ is_reentry_from_check_wt() {
158
+ reentry_pid=$$
159
+ case "${KUMIKO_CHECK_WT_OWNER:-}" in '' | *[!0-9]*) return 1 ;; esac
160
+ while [ -n "$reentry_pid" ] && [ "$reentry_pid" -gt 1 ]; do
161
+ [ "$reentry_pid" = "$KUMIKO_CHECK_WT_OWNER" ] && return 0
162
+ reentry_pid=$(ps -o ppid= -p "$reentry_pid" 2>/dev/null | tr -d ' ')
163
+ done
164
+ return 1
165
+ }
166
+
167
+ if ! is_reentry_from_check_wt \
140
168
  && [ "$(git rev-parse --path-format=absolute --git-dir)" != "$GIT_COMMON_DIR" ] \
141
169
  && git -C "$REPO_ROOT" ls-files --error-unmatch scripts/check-wt.sh >/dev/null 2>&1 \
142
170
  && [ -x "$REPO_ROOT/scripts/check-wt.sh" ]; then
143
171
  echo "[pre-push] worktree detected — worktree check (see scripts/check-wt.sh)…"
144
- cd "$REPO_ROOT" && exec "$REPO_ROOT/scripts/check-wt.sh"
172
+ cd "$REPO_ROOT" && KUMIKO_CHECK_WT_OWNER=$$ exec "$REPO_ROOT/scripts/check-wt.sh"
145
173
  fi
146
174
 
147
175
  # Runs a guard bin from the worktree's or the parent's node_modules/.bin
@@ -39,20 +39,24 @@ function propertyName(node: Node): string | undefined {
39
39
  }
40
40
 
41
41
  function objectProperty(object: ObjectLiteralExpression, name: string): Node | undefined {
42
+ let found: Node | undefined;
43
+ let opaqueAfterFound = false;
42
44
  for (const property of object.getProperties()) {
43
- if (
44
- property.isKind(SyntaxKind.SpreadAssignment) ||
45
- property.isKind(SyntaxKind.ShorthandPropertyAssignment)
46
- ) {
47
- throw new StaticConfigError(
48
- `${name}: spread and shorthand properties cannot be read statically`,
49
- );
50
- }
51
- if (propertyName(property) === name && property.isKind(SyntaxKind.PropertyAssignment)) {
52
- return property.getInitializerOrThrow();
45
+ if (property.isKind(SyntaxKind.SpreadAssignment)) {
46
+ opaqueAfterFound = true;
47
+ } else if (property.isKind(SyntaxKind.ShorthandPropertyAssignment)) {
48
+ if (property.getName() === name) opaqueAfterFound = true;
49
+ } else if (propertyName(property) === name && property.isKind(SyntaxKind.PropertyAssignment)) {
50
+ found = property.getInitializerOrThrow();
51
+ opaqueAfterFound = false;
53
52
  }
54
53
  }
55
- return undefined;
54
+ if (opaqueAfterFound) {
55
+ throw new StaticConfigError(
56
+ `${name}: spread and shorthand properties cannot be read statically`,
57
+ );
58
+ }
59
+ return found;
56
60
  }
57
61
 
58
62
  function numericLiteral(node: Node | undefined, path: string): number {
package/src/run-guards.ts CHANGED
@@ -47,6 +47,7 @@ import { guard as restrictedSymbols } from "./guard-restricted-symbols";
47
47
  import { guard as screenConventions } from "./guard-screen-conventions";
48
48
  import { guard as silentSkip } from "./guard-silent-skip";
49
49
  import { guard as tenantEscalation } from "./guard-tenant-escalation";
50
+ import { guard as testEsFixtures } from "./guard-test-es-fixtures";
50
51
  import { guard as testTemplateDrift } from "./guard-test-template-drift";
51
52
  import { guard as testTimeouts } from "./guard-test-timeouts";
52
53
  import { guard as unsafeJsonParse } from "./guard-unsafe-json-parse";
@@ -81,6 +82,7 @@ export const GUARDS = [
81
82
  appFeatureStructure,
82
83
  libTestCoverage,
83
84
  testTimeouts,
85
+ testEsFixtures,
84
86
  testTemplateDrift,
85
87
  fileSize,
86
88
  commentIssueRefOnly,
@@ -97,6 +99,8 @@ const GUARD_NAME_PREFIX = "--guard=";
97
99
 
98
100
  export type RunGuardsCliDeps = {
99
101
  readonly writeBaselineRoots?: readonly RepoRoot[];
102
+ readonly acquireSlot?: () => () => void;
103
+ readonly buildProject?: typeof buildSharedProject;
100
104
  };
101
105
 
102
106
  // Shared by the direct `bun run-guards.ts` invocation below and by the
@@ -115,7 +119,7 @@ export function runGuardsCli(argv: readonly string[], deps: RunGuardsCliDeps = {
115
119
  return 1;
116
120
  }
117
121
  if (flags.includes("--explain")) {
118
- for (const line of explainGuards(GUARDS, buildSharedProject(GUARDS))) {
122
+ for (const line of explainGuards(GUARDS, (deps.buildProject ?? buildSharedProject)(GUARDS))) {
119
123
  console.log(line);
120
124
  }
121
125
  return 0;
@@ -155,12 +159,15 @@ export function runGuardsCli(argv: readonly string[], deps: RunGuardsCliDeps = {
155
159
  );
156
160
  return 0;
157
161
  }
158
- const strictSecurityBaseline = flags.includes("--strict-security-baseline");
162
+ return runAllGuards(flags.includes("--strict-security-baseline"), deps);
163
+ }
164
+
165
+ function runAllGuards(strictSecurityBaseline: boolean, deps: RunGuardsCliDeps): number {
159
166
  const guards = strictSecurityBaseline ? GUARDS.filter(isSecurityGuard) : GUARDS;
160
167
  // Taken before the ts-morph project is built: that is where the memory goes.
161
- const releaseSlot = acquireGuardRunSlot();
168
+ const releaseSlot = (deps.acquireSlot ?? acquireGuardRunSlot)();
162
169
  try {
163
- const project = buildSharedProject(guards);
170
+ const project = (deps.buildProject ?? buildSharedProject)(guards);
164
171
  printGuardKitBanner(guards.length, project);
165
172
  return reportResults(runGuards(guards, project, { strictSecurityBaseline }));
166
173
  } finally {
@@ -49,7 +49,6 @@ 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"],
53
52
  [/(?:^|\/)scripts\//, "tooling"],
54
53
  [/(?:^|\/)bin\//, "tooling"],
55
54
  [/\/drizzle\/[^/]+\.ts$/, "tooling"],
@@ -75,6 +74,8 @@ const PATH_RULES: readonly (readonly [RegExp, Runtime])[] = [
75
74
  // some other package.
76
75
  [/(?:^|\/)web\//, "client"],
77
76
  [/\/web\.tsx?$/, "client"],
77
+ // After the web/ carve-out: client code under web/dev/ must stay client.
78
+ [/(?:^|\/)dev\//, "dev"],
78
79
  [/^packages\/framework\/src\/(?:time|utils|engine\/types|errors)\//, "client"],
79
80
 
80
81
  // More single-file carve-outs, same "package marker is coarser than the