@cosmicdrift/kumiko-guards 0.337.1 → 0.339.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.337.1",
3
+ "version": "0.339.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.337.1",
31
+ "@cosmicdrift/kumiko-repo-manifest": "0.339.0",
32
32
  "ts-morph": "^28.0.0"
33
33
  },
34
34
  "publishConfig": {
@@ -12,12 +12,13 @@
12
12
  * kumiko.json manifest instead of hardcoded repo-kind globs.
13
13
  */
14
14
 
15
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
16
- import { relative as pathRelative, resolve } from "node:path";
15
+ import { existsSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
16
+ import { relative as pathRelative, resolve, sep } from "node:path";
17
17
  import { Project, type SourceFile } from "ts-morph";
18
18
  import { compareToBaseline, findRepoRootFor } from "./baseline-compare";
19
19
  import {
20
20
  explainRepoRoots,
21
+ findLocalRepo,
21
22
  frameworkTsConfigPath,
22
23
  type RepoRoot,
23
24
  type RootResolution,
@@ -62,6 +63,49 @@ export function isSecurityGuard(guard: Pick<AstGuard, "security">): boolean {
62
63
  return guard.security === true;
63
64
  }
64
65
 
66
+ // Followers call the predicate per import hop; findLocalRepo re-reads
67
+ // package.json and the manifest each time.
68
+ const localRepoRootByCwd = new Map<string, string | undefined>();
69
+
70
+ function localRepoRoot(): string | undefined {
71
+ const cwd = process.cwd();
72
+ if (!localRepoRootByCwd.has(cwd)) localRepoRootByCwd.set(cwd, findLocalRepo(cwd)?.absPath);
73
+ return localRepoRootByCwd.get(cwd);
74
+ }
75
+
76
+ function realOrResolved(path: string): string {
77
+ try {
78
+ return realpathSync(path);
79
+ } catch {
80
+ // In-memory test files and paths not on disk: compare as spelled.
81
+ return resolve(path);
82
+ }
83
+ }
84
+
85
+ /**
86
+ * True for a file outside the local repo's root — a neighbour repo of the
87
+ * parent workspace reached through a workspace symlink or a `../..` import. Such
88
+ * a file is as external as a package under node_modules: a guard may not scan it
89
+ * or follow imports into it, or a `.wt/<repo>` worktree reports findings that a
90
+ * fresh CI checkout (no parent workspace) never sees. The root comes from
91
+ * `findLocalRepo`, not a path heuristic; without a local repo nothing is
92
+ * decidable and nothing counts as outside.
93
+ */
94
+ export function isOutsideRepoRoot(
95
+ filePath: string,
96
+ repoRoot: string | undefined = localRepoRoot(),
97
+ ): boolean {
98
+ if (repoRoot === undefined) return false;
99
+ const root = realOrResolved(repoRoot);
100
+ const file = realOrResolved(filePath);
101
+ return file !== root && !file.startsWith(root + sep);
102
+ }
103
+
104
+ /** Import followers stop at these: a package from node_modules or a file outside the repo root. */
105
+ export function isExternalSourcePath(filePath: string): boolean {
106
+ return filePath.includes("/node_modules/") || isOutsideRepoRoot(filePath);
107
+ }
108
+
65
109
  export function buildSharedProject(
66
110
  guards: readonly AstGuard[],
67
111
  roots: readonly RepoRoot[] = resolveRepoRoots(),
@@ -88,7 +132,7 @@ export function buildSharedProject(
88
132
  }
89
133
  }),
90
134
  ),
91
- ];
135
+ ].filter((path) => roots.some((root) => !isOutsideRepoRoot(path, root.absPath)));
92
136
  // Exact paths, never re-glob: filenames like `[id].tsx` are literal here.
93
137
  for (const path of union) project.addSourceFileAtPath(path);
94
138
  return project;
package/src/changes.json CHANGED
@@ -1,4 +1,11 @@
1
1
  [
2
+ {
3
+ "version": "0.339.0",
4
+ "type": "fix",
5
+ "title": "Guards ignore neighbour repos of the parent workspace",
6
+ "detail": "The root comes from `findLocalRepo`, not a path heuristic. `isOutsideRepoRoot(filePath, repoRoot?)` compares real paths, so symlinks into a neighbour repo count as outside. `buildSharedProject` drops such files and `no-framed-extension-sections` stops following imports into them.",
7
+ "migration": "keine"
8
+ },
2
9
  {
3
10
  "version": "0.337.0",
4
11
  "type": "fix",
@@ -41,7 +41,13 @@ import {
41
41
  SyntaxKind,
42
42
  VariableDeclarationKind,
43
43
  } from "ts-morph";
44
- import { type AstGuard, type GuardViolation, runStandalone, type ScanSpec } from "./_lib/guard-kit";
44
+ import {
45
+ type AstGuard,
46
+ type GuardViolation,
47
+ isExternalSourcePath,
48
+ runStandalone,
49
+ type ScanSpec,
50
+ } from "./_lib/guard-kit";
45
51
  import { hasIgnoreTag } from "./_lib/ignore-tag";
46
52
 
47
53
  const ROOT = process.cwd();
@@ -58,11 +64,12 @@ const MAX_HOPS = 4;
58
64
  const FRAMED_TAG = /^(Card|SectionCard|CollapsibleSection)$/;
59
65
 
60
66
  // A vendored package's shipped .tsx source is still resolvable through
61
- // node_modules — treat it the same as a truly unresolvable specifier
67
+ // node_modules, and in a parent workspace a workspace link resolves into a
68
+ // neighbour repo — treat both the same as a truly unresolvable specifier
62
69
  // ("package component, checked inside the package itself"), not as a hop
63
70
  // into the App-Repo's own extension code.
64
71
  function isExternalSourceFile(sf: SourceFile): boolean {
65
- return sf.getFilePath().includes("/node_modules/");
72
+ return isExternalSourcePath(sf.getFilePath());
66
73
  }
67
74
 
68
75
  function stringConstantValue(node: Node): string | undefined {