@cosmicdrift/kumiko-guards 0.360.0 → 0.362.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.360.0",
3
+ "version": "0.362.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>",
@@ -28,7 +28,7 @@
28
28
  "kumiko-pre-push": "./src/pre-push.sh"
29
29
  },
30
30
  "dependencies": {
31
- "@cosmicdrift/kumiko-repo-manifest": "0.360.0",
31
+ "@cosmicdrift/kumiko-repo-manifest": "0.362.0",
32
32
  "ts-morph": "^28.0.0"
33
33
  },
34
34
  "publishConfig": {
@@ -0,0 +1,25 @@
1
+ import { basename } from "node:path";
2
+ import { findLocalRepo, localRepoDirName } from "./roots";
3
+
4
+ // A worktree's folder name proves nothing (`kumiko-*` siblings share prefixes), so it is judged by its canonical checkout's directory name.
5
+ function pushRootMatchesCliScope(pushRepoRoot: string, cliScope: string): boolean {
6
+ if (basename(pushRepoRoot) === cliScope) return true;
7
+ const local = findLocalRepo(pushRepoRoot);
8
+ return local !== undefined && (local.name === cliScope || localRepoDirName(local) === cliScope);
9
+ }
10
+
11
+ // A push root left over from an earlier hook run or a debug session must not redirect a guard at the wrong repo, so it only counts when KUMIKO_CLI_SCOPE names it.
12
+ export function resolvePushRepoRoot(
13
+ vars: Readonly<Record<string, string | undefined>>,
14
+ ): string | undefined {
15
+ const cliScope = vars["KUMIKO_CLI_SCOPE"]?.trim();
16
+ const pushRepoRoot = vars["KUMIKO_PUSH_REPO_ROOT"]?.trim();
17
+ if (!cliScope || !pushRepoRoot) return undefined;
18
+ if (!pushRootMatchesCliScope(pushRepoRoot, cliScope)) {
19
+ console.error(
20
+ `[push-root] KUMIKO_PUSH_REPO_ROOT="${pushRepoRoot}" does not match KUMIKO_CLI_SCOPE="${cliScope}" — ignoring inherited/stale push root.`,
21
+ );
22
+ return undefined;
23
+ }
24
+ return pushRepoRoot;
25
+ }
package/src/_lib/roots.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { execFileSync } from "node:child_process";
11
11
  import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync } from "node:fs";
12
- import { dirname, join, resolve } from "node:path";
12
+ import { basename, dirname, join, resolve } from "node:path";
13
13
  import {
14
14
  type LoadedRepoManifest,
15
15
  loadRepoManifest,
@@ -115,6 +115,23 @@ function gitToplevelOf(cwd: string): string | undefined {
115
115
  return spelled;
116
116
  }
117
117
 
118
+ /**
119
+ * The main checkout of the repo containing `from`, independent of what a linked
120
+ * worktree's folder is called. Undefined when git cannot answer or the common
121
+ * dir is not a `<repo>/.git`.
122
+ */
123
+ function canonicalCheckoutOf(from: string): string | undefined {
124
+ const commonDir = git(from, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
125
+ if (commonDir === undefined || basename(commonDir) !== ".git") return undefined;
126
+ const checkout = dirname(commonDir);
127
+ return existsSync(checkout) ? checkout : undefined;
128
+ }
129
+
130
+ /** Directory name of the repo's canonical checkout; `local.name` when git names none. */
131
+ export function localRepoDirName(local: RepoRoot): string {
132
+ return basename(canonicalCheckoutOf(local.absPath) ?? local.name);
133
+ }
134
+
118
135
  function rootFrom(name: string, dir: string, loaded: LoadedRepoManifest): RepoRoot {
119
136
  return {
120
137
  name,
package/src/changes.json CHANGED
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.361.0",
4
+ "type": "fix",
5
+ "title": "comment-lang guard scans tooling repos, handles division before a comment and ignores a stale KUMIKO_PUSH_REPO_ROOT",
6
+ "detail": "comment-lang scans tooling repos, no longer rescans a division as a regex literal, and honours KUMIKO_PUSH_REPO_ROOT only when KUMIKO_CLI_SCOPE names that checkout. The scan-scope helpers scanFiles and scanRoots are exported."
7
+ },
2
8
  {
3
9
  "version": "0.360.0",
4
10
  "type": "fix",
@@ -2,8 +2,9 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { ts } from "ts-morph";
4
4
  import { gitEnv } from "./_lib/git-env";
5
- import { cliFlagsError, isLocalFinding } from "./_lib/guard-kit";
5
+ import { ALL_REPO_KINDS, cliFlagsError, isLocalFinding } from "./_lib/guard-kit";
6
6
  import { lineHasIgnoreTag } from "./_lib/ignore-tag";
7
+ import { resolvePushRepoRoot } from "./_lib/push-root";
7
8
  import { resolveRepoRoots } from "./_lib/roots";
8
9
  import { type ScanSpec, scanFiles } from "./_lib/scan-scope";
9
10
 
@@ -28,6 +29,7 @@ const IGNORE_TAG = "kumiko-lint-ignore comment-lang";
28
29
 
29
30
  const SCAN: ScanSpec = {
30
31
  scope: "source",
32
+ kinds: ALL_REPO_KINDS,
31
33
  extensions: ["ts", "tsx"],
32
34
  frameworkWithin: ["packages/*/src/**"],
33
35
  };
@@ -66,6 +68,48 @@ function germanCommentSite(
66
68
  };
67
69
  }
68
70
 
71
+ // After these tokens a `/` is division, not the start of a regex literal.
72
+ const DIVISION_PRECEDING_KINDS: ReadonlySet<ts.SyntaxKind> = new Set([
73
+ ts.SyntaxKind.Identifier,
74
+ ts.SyntaxKind.ThisKeyword,
75
+ ts.SyntaxKind.TrueKeyword,
76
+ ts.SyntaxKind.FalseKeyword,
77
+ ts.SyntaxKind.NullKeyword,
78
+ ts.SyntaxKind.NumericLiteral,
79
+ ts.SyntaxKind.BigIntLiteral,
80
+ ts.SyntaxKind.StringLiteral,
81
+ ts.SyntaxKind.NoSubstitutionTemplateLiteral,
82
+ ts.SyntaxKind.TemplateTail,
83
+ ts.SyntaxKind.RegularExpressionLiteral,
84
+ ts.SyntaxKind.CloseParenToken,
85
+ ts.SyntaxKind.CloseBracketToken,
86
+ ts.SyntaxKind.CloseBraceToken,
87
+ ts.SyntaxKind.PlusPlusToken,
88
+ ts.SyntaxKind.MinusMinusToken,
89
+ ]);
90
+
91
+ const TRIVIA_KINDS: ReadonlySet<ts.SyntaxKind> = new Set([
92
+ ts.SyntaxKind.SingleLineCommentTrivia,
93
+ ts.SyntaxKind.MultiLineCommentTrivia,
94
+ ts.SyntaxKind.WhitespaceTrivia,
95
+ ts.SyntaxKind.NewLineTrivia,
96
+ ]);
97
+
98
+ // The raw scanner always yields SlashToken for `/`; without the rescan a regex
99
+ // body like /\{/ would shift braceDepth and end a template early. After an
100
+ // operand the rescan would instead swallow `/ 2; // {` as a regex and hide the comment.
101
+ function rescanSlashAsRegex(
102
+ scanner: ts.Scanner,
103
+ kind: ts.SyntaxKind,
104
+ previousSignificant: ts.SyntaxKind | undefined,
105
+ ): ts.SyntaxKind {
106
+ if (previousSignificant !== undefined && DIVISION_PRECEDING_KINDS.has(previousSignificant)) {
107
+ return kind;
108
+ }
109
+ const reScanned = scanner.reScanSlashToken();
110
+ return reScanned === ts.SyntaxKind.RegularExpressionLiteral ? reScanned : kind;
111
+ }
112
+
69
113
  function closesTemplateSubstitution(
70
114
  templateBraceDepths: readonly number[],
71
115
  braceDepth: number,
@@ -91,6 +135,7 @@ export function scanGermanComments(fullText: string, file: string): Site[] {
91
135
 
92
136
  const templateBraceDepths: number[] = [];
93
137
  let braceDepth = 0;
138
+ let previousSignificant: ts.SyntaxKind | undefined;
94
139
 
95
140
  let kind = scanner.scan();
96
141
  while (kind !== ts.SyntaxKind.EndOfFileToken) {
@@ -107,13 +152,9 @@ export function scanGermanComments(fullText: string, file: string): Site[] {
107
152
  break;
108
153
  }
109
154
  case ts.SyntaxKind.SlashToken:
110
- case ts.SyntaxKind.SlashEqualsToken: {
111
- // The raw scanner always yields SlashToken for `/`; without the rescan a
112
- // regex body like /\{/ would shift braceDepth and end a template early.
113
- const reScanned = scanner.reScanSlashToken();
114
- if (reScanned === ts.SyntaxKind.RegularExpressionLiteral) kind = reScanned;
155
+ case ts.SyntaxKind.SlashEqualsToken:
156
+ kind = rescanSlashAsRegex(scanner, kind, previousSignificant);
115
157
  break;
116
- }
117
158
  case ts.SyntaxKind.TemplateHead:
118
159
  case ts.SyntaxKind.TemplateMiddle:
119
160
  templateBraceDepths.push(braceDepth);
@@ -132,6 +173,7 @@ export function scanGermanComments(fullText: string, file: string): Site[] {
132
173
  default:
133
174
  break;
134
175
  }
176
+ if (!TRIVIA_KINDS.has(kind)) previousSignificant = kind;
135
177
  kind = scanner.scan();
136
178
  }
137
179
  return sites;
@@ -422,7 +464,7 @@ export function runCommentLangCli(
422
464
  return 1;
423
465
  }
424
466
 
425
- const pushRoot = env["KUMIKO_PUSH_REPO_ROOT"];
467
+ const pushRoot = resolvePushRepoRoot(env);
426
468
  const root = pushRoot && existsSync(pushRoot) ? pushRoot : cwd;
427
469
 
428
470
  try {
package/src/index.ts CHANGED
@@ -21,6 +21,7 @@ export {
21
21
  type SuiteInventory,
22
22
  } from "./_lib/guard-kit";
23
23
  export { findLocalRepo, type RepoRoot, resolveRepoRoots } from "./_lib/roots";
24
+ export { type RootScan, type ScanOptions, scanFiles, scanRoots } from "./_lib/scan-scope";
24
25
  export {
25
26
  loadSecurityBaseline,
26
27
  parseSecurityBaseline,