no-mistakes 0.47.0 → 0.48.1

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/README.md CHANGED
@@ -43,8 +43,6 @@ const {
43
43
  root: process.cwd(),
44
44
  files: ["src/main.mts"],
45
45
  relationships: ["import"],
46
- timeout: 30,
47
- lockTimeout: 30,
48
46
  });
49
47
  const tests = await dependents({
50
48
  root: process.cwd(),
@@ -72,7 +70,7 @@ const {
72
70
  changedFiles: ["src/utils.mts"],
73
71
  });
74
72
  // Complete changed-file inventory, including paths that selected no tests.
75
- console.log(plan.changed_files);
73
+ console.log(plan.changedFiles);
76
74
  const targetCommands = await testsTargets({
77
75
  root: process.cwd(),
78
76
  framework: "vitest",
@@ -121,10 +119,11 @@ const {
121
119
 
122
120
  CLI and Node analyses share a per-user machine-wide lock. CLI flags
123
121
  `--timeout`, `--lock-timeout`, and `--fail-on-lock` have Node equivalents
124
- `timeout`, `lockTimeout`, and `failOnLock`. Both timeouts default to 30 seconds;
125
- `0` disables either CLI timeout, while `0` or `null` disables it in Node.
126
- Waiting does not alter successful output, and Node lock/timeout failures reject
127
- the returned Promise.
122
+ `timeout`, `lockTimeout`, and `failOnLock`. CLI timeouts default to 30 seconds;
123
+ Node/N-API omits both deadlines unless you set them. `0` disables either CLI
124
+ timeout, while `0` or `null` disables it in Node. While waiting, stderr reports
125
+ the holder pid and elapsed seconds. Waiting does not alter successful output,
126
+ and Node lock/timeout failures reject the returned Promise.
128
127
 
129
128
  Dependency graph, query, and test-planning resolution is per workspace by
130
129
  default: when `tsconfig` is omitted, each import uses the config that owns its
package/flow-types.d.ts CHANGED
@@ -15,7 +15,14 @@ export interface FlowOptions {
15
15
 
16
16
  export interface FlowNode {
17
17
  id: string;
18
- kind: "file" | "symbol" | "module" | "queue-job" | "workflow-job" | "workflow-step";
18
+ kind:
19
+ | "file"
20
+ | "symbol"
21
+ | "module"
22
+ | "queue-job"
23
+ | "workflow-job"
24
+ | "workflow-step"
25
+ | "trpc-procedure";
19
26
  depth: number;
20
27
  file?: string;
21
28
  symbol?: string;
@@ -27,6 +34,10 @@ export interface FlowNode {
27
34
  job?: string;
28
35
  /** Zero-based step index for a virtual GitHub Actions workflow step node. */
29
36
  step?: number;
37
+ /** Router file for a virtual tRPC procedure node. */
38
+ routerFile?: string;
39
+ /** Dotted procedure path for a virtual tRPC procedure node (`user.get`). */
40
+ procedure?: string;
30
41
  }
31
42
 
32
43
  export interface FlowEdge {
package/index.d.ts CHANGED
@@ -24,6 +24,7 @@ import type {
24
24
  ResolveCheckFilesOptions,
25
25
  ResolveCheckResult,
26
26
  ResolveCheckBatchResult,
27
+ ResolvedConfig,
27
28
  GraphEdge,
28
29
  PlaywrightOptions,
29
30
  PlaywrightRelatedOptions,
@@ -98,6 +99,9 @@ export function resolveCheck(
98
99
  export function fetches(options?: WithInvocationOptions<FetchesOptions>): Promise<FetchReport>;
99
100
  export function flow(options: WithInvocationOptions<FlowOptions>): Promise<FlowReport>;
100
101
  export function check(options?: WithInvocationOptions<CheckOptions>): Promise<CheckReport>;
102
+ export function resolveConfig(
103
+ options?: WithInvocationOptions<ProjectOptions>,
104
+ ): Promise<ResolvedConfig>;
101
105
  export function validateMermaidMarkdown(
102
106
  options: WithInvocationOptions<MermaidValidationOptions>,
103
107
  ): Promise<MermaidValidationResult>;
package/index.js CHANGED
@@ -5,9 +5,12 @@
5
5
  const native = require(process.env.NO_MISTAKES_TEST_NAPI_ADDON_PATH || "./bin/no-mistakes.node");
6
6
  const planning = require("./planning");
7
7
  const { createWorkflowTopologyIndex } = require("./workflow-topology-index");
8
+ const fs = require("node:fs");
9
+ const path = require("node:path");
8
10
 
9
11
  async function callJson(fn, options) {
10
- return JSON.parse(await fn(JSON.stringify(options || {})));
12
+ const input = Buffer.from(JSON.stringify(options || {}));
13
+ return JSON.parse(await fn(input));
11
14
  }
12
15
 
13
16
  function createJsonApis(descriptors) {
@@ -23,6 +26,7 @@ const jsonApis = createJsonApis({
23
26
  analyzeProject: "analyzeProjectJson",
24
27
  callSites: "callSitesJson",
25
28
  check: "checkJson",
29
+ resolveConfig: "resolveConfigJson",
26
30
  ciEnv: "ciEnvJson",
27
31
  ciImpact: "ciImpactJson",
28
32
  ciTopology: "ciTopologyJson",
@@ -57,18 +61,90 @@ const jsonApis = createJsonApis({
57
61
  symbols: "symbolsJson",
58
62
  });
59
63
 
64
+ const PLAN_INPUT_REPORTS = new Set(["testsComment", "testsGraph", "testsGraphMermaid"]);
65
+ const CAMELIZE_REPORTS = new Set(["testsPlan", "testsImpact", "testsTargets", "testsGraph"]);
66
+
67
+ async function analyzeProject(options = {}) {
68
+ const request = { ...options };
69
+ const generatedDirs = [];
70
+ try {
71
+ if (Array.isArray(request.reports)) {
72
+ request.reports = await Promise.all(
73
+ request.reports.map(async (report) => {
74
+ if (report.type === "testsWhy") {
75
+ const prepared = await planning.prepareWhyPlan(report);
76
+ if (prepared.generatedDir) generatedDirs.push(prepared.generatedDir);
77
+ return prepared.request;
78
+ }
79
+ return PLAN_INPUT_REPORTS.has(report.type)
80
+ ? await planning.decamelizePlanOptions(report)
81
+ : report;
82
+ }),
83
+ );
84
+ }
85
+ const result = await jsonApis.analyzeProject(request);
86
+ for (const report of result.reports || []) {
87
+ if (report.type === "testsWhy") {
88
+ report.result = planning.camelizeWhy(report.result);
89
+ } else if (CAMELIZE_REPORTS.has(report.type)) {
90
+ report.result = planning.camelizeValue(report.result);
91
+ }
92
+ }
93
+ return result;
94
+ } finally {
95
+ await Promise.all(generatedDirs.map((dir) => planning.removeGeneratedDir(dir)));
96
+ }
97
+ }
98
+
99
+ const topologyMemo = new Map();
100
+
101
+ async function ciTopology(options) {
102
+ const root = path.resolve((options && options.root) || process.cwd());
103
+ const configPath = path.resolve(root, (options && options.config) || ".no-mistakes.yml");
104
+ let mtime = 0;
105
+ try {
106
+ mtime = fs.statSync(configPath).mtimeMs;
107
+ } catch {
108
+ mtime = 0;
109
+ }
110
+ const workflows = JSON.stringify(
111
+ []
112
+ .concat((options && options.workflows) || [])
113
+ .map(String)
114
+ .sort(),
115
+ );
116
+ const identity = `${root}\0${configPath}\0`;
117
+ const key = `${identity}${mtime}\0${workflows}`;
118
+ const stale = [];
119
+ for (const memoKey of topologyMemo.keys()) {
120
+ if (!memoKey.startsWith(identity)) continue;
121
+ const memoMtime = memoKey.slice(identity.length).split("\0")[0];
122
+ if (memoMtime !== String(mtime)) stale.push(memoKey);
123
+ }
124
+ for (const memoKey of stale) topologyMemo.delete(memoKey);
125
+ const cached = topologyMemo.get(key);
126
+ if (cached) return cached.then((value) => structuredClone(value));
127
+ const pending = jsonApis.ciTopology({ ...options, root }).catch((error) => {
128
+ topologyMemo.delete(key);
129
+ throw error;
130
+ });
131
+ topologyMemo.set(key, pending);
132
+ return pending.then((value) => structuredClone(value));
133
+ }
134
+
60
135
  async function version() {
61
136
  return native.version();
62
137
  }
63
138
 
64
139
  module.exports.createWorkflowTopologyIndex = createWorkflowTopologyIndex;
65
140
  module.exports.version = version;
66
- module.exports.analyzeProject = jsonApis.analyzeProject;
141
+ module.exports.analyzeProject = analyzeProject;
67
142
  module.exports.callSites = jsonApis.callSites;
68
143
  module.exports.check = jsonApis.check;
144
+ module.exports.resolveConfig = jsonApis.resolveConfig;
69
145
  module.exports.ciEnv = jsonApis.ciEnv;
70
146
  module.exports.ciImpact = jsonApis.ciImpact;
71
- module.exports.ciTopology = jsonApis.ciTopology;
147
+ module.exports.ciTopology = ciTopology;
72
148
  module.exports.dataPw = jsonApis.dataPw;
73
149
  module.exports.deadExports = jsonApis.deadExports;
74
150
  module.exports.dependencies = jsonApis.dependencies;
package/index.mjs ADDED
@@ -0,0 +1,64 @@
1
+ import { createRequire } from "node:module";
2
+
3
+ const require = createRequire(import.meta.url);
4
+ const cjs = require("./index.js");
5
+
6
+ export const {
7
+ analyzeProject,
8
+ callSites,
9
+ check,
10
+ ciEnv,
11
+ ciImpact,
12
+ ciTopology,
13
+ createWorkflowTopologyIndex,
14
+ dataPw,
15
+ deadExports,
16
+ dependencies,
17
+ dependents,
18
+ effects,
19
+ exportsOf,
20
+ fetches,
21
+ flow,
22
+ impactedChecks,
23
+ importUsages,
24
+ importers,
25
+ infraOutputs,
26
+ infraResourceRefs,
27
+ infraTestFor,
28
+ lockfileDiff,
29
+ playwrightCheck,
30
+ playwrightEdges,
31
+ playwrightRelated,
32
+ playwrightTests,
33
+ queueCheck,
34
+ queueEdges,
35
+ queueRelated,
36
+ queues,
37
+ reactAnalyze,
38
+ reactCheck,
39
+ reactUsages,
40
+ registryExtension,
41
+ related,
42
+ resolveCheck,
43
+ resolveConfig,
44
+ rscCallers,
45
+ serverContracts,
46
+ serverRouteEdges,
47
+ serverRouteList,
48
+ serverRouteRelated,
49
+ serverRoutes,
50
+ swiftImporters,
51
+ swiftTestTargets,
52
+ symbols,
53
+ testsComment,
54
+ testsGraph,
55
+ testsGraphMermaid,
56
+ testsImpact,
57
+ testsPlan,
58
+ testsTargets,
59
+ testsWhy,
60
+ validateMermaidMarkdown,
61
+ version,
62
+ } = cjs;
63
+
64
+ export default cjs;
@@ -1,8 +1,8 @@
1
1
  /** Controls shared by every analysis invocation. Durations are in seconds. */
2
2
  export interface InvocationOptions {
3
- /** Command execution timeout. Defaults to 30; 0 or null disables it. */
3
+ /** Command execution timeout in whole non-negative seconds. Omit, `0`, or `null` disables it. CLI default remains 30. */
4
4
  timeout?: number | null;
5
- /** Maximum time to wait for the machine-wide lock. Defaults to 30; 0 or null waits indefinitely. */
5
+ /** Maximum time to wait for the machine-wide lock, in whole non-negative seconds. Omit, `0`, or `null` waits indefinitely. CLI default remains 30. */
6
6
  lockTimeout?: number | null;
7
7
  /** Fail immediately instead of waiting when another invocation holds the lock. */
8
8
  failOnLock?: boolean;
@@ -11,6 +11,8 @@ export interface InvocationOptions {
11
11
  * `0` uses the CPU count, matching CLI `--jobs 0`.
12
12
  */
13
13
  jobs?: number | null;
14
+ /** `ci` sets unbounded command and lock timeouts. CLI `--profile ci` does the same. */
15
+ profile?: "ci";
14
16
  }
15
17
 
16
18
  export type WithInvocationOptions<T> = T & InvocationOptions;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "no-mistakes",
3
- "version": "0.47.0",
3
+ "version": "0.48.1",
4
4
  "description": "Static codebase analysis tools for TS/JS dependencies, dependents, and symbols",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -15,8 +15,10 @@
15
15
  "bin/",
16
16
  "*.d.ts",
17
17
  "index.js",
18
+ "index.mjs",
18
19
  "planning.js",
19
20
  "workflow-topology-index.js",
21
+ "workflow-topology-index-helpers.js",
20
22
  "scripts/install.js",
21
23
  "scripts/install/",
22
24
  "README.md",
@@ -24,6 +26,15 @@
24
26
  ],
25
27
  "main": "index.js",
26
28
  "types": "index.d.ts",
29
+ "exports": {
30
+ ".": {
31
+ "types": "./index.d.ts",
32
+ "import": "./index.mjs",
33
+ "require": "./index.js"
34
+ },
35
+ "./planning.js": "./planning.js",
36
+ "./workflow-topology-index.js": "./workflow-topology-index.js"
37
+ },
27
38
  "publishConfig": {
28
39
  "access": "public"
29
40
  },
package/planning.js CHANGED
@@ -1,9 +1,13 @@
1
1
  "use strict";
2
2
 
3
+ const fs = require("node:fs/promises");
4
+ const os = require("node:os");
5
+ const path = require("node:path");
3
6
  const native = require(process.env.NO_MISTAKES_TEST_NAPI_ADDON_PATH || "./bin/no-mistakes.node");
4
7
 
5
8
  async function callJson(fn, options) {
6
- return JSON.parse(await fn(JSON.stringify(options || {})));
9
+ const input = Buffer.from(JSON.stringify(options || {}));
10
+ return JSON.parse(await fn(input));
7
11
  }
8
12
 
9
13
  function createJsonApis(descriptors) {
@@ -15,12 +19,110 @@ function createJsonApis(descriptors) {
15
19
  );
16
20
  }
17
21
 
22
+ function camelizeKey(key) {
23
+ return key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
24
+ }
25
+
26
+ function decamelizeKey(key) {
27
+ return key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
28
+ }
29
+
30
+ function mapKeys(value, mapKey) {
31
+ if (Array.isArray(value)) return value.map((item) => mapKeys(item, mapKey));
32
+ if (value && typeof value === "object") {
33
+ return Object.fromEntries(
34
+ Object.entries(value).map(([key, nested]) => [mapKey(key), mapKeys(nested, mapKey)]),
35
+ );
36
+ }
37
+ return value;
38
+ }
39
+
40
+ function camelizeValue(value) {
41
+ return mapKeys(value, camelizeKey);
42
+ }
43
+
44
+ function decamelizeValue(value) {
45
+ return mapKeys(value, decamelizeKey);
46
+ }
47
+
48
+ function loadPlanJson(planJson) {
49
+ let parsed = planJson;
50
+ if (typeof parsed === "string") {
51
+ try {
52
+ parsed = JSON.parse(parsed);
53
+ } catch {
54
+ return planJson;
55
+ }
56
+ }
57
+ if (parsed && typeof parsed === "object") {
58
+ return decamelizeValue(parsed);
59
+ }
60
+ return planJson;
61
+ }
62
+
63
+ async function readPlanFile(planPath) {
64
+ try {
65
+ return JSON.parse(await fs.readFile(planPath, "utf8"));
66
+ } catch {
67
+ return undefined;
68
+ }
69
+ }
70
+
71
+ async function decamelizePlanOptions(options = {}) {
72
+ const next = { ...options };
73
+ if (next.planJson != null) {
74
+ next.planJson = loadPlanJson(next.planJson);
75
+ } else if (typeof next.plan === "string") {
76
+ const document = await readPlanFile(next.plan);
77
+ if (document !== undefined) {
78
+ next.planJson = loadPlanJson(document);
79
+ delete next.plan;
80
+ }
81
+ }
82
+ return next;
83
+ }
84
+
85
+ async function prepareWhyPlan(options = {}) {
86
+ const next = { ...options };
87
+ let document = next.planJson;
88
+ if (document == null && typeof next.plan === "string") {
89
+ document = await readPlanFile(next.plan);
90
+ if (document === undefined) return { request: next };
91
+ }
92
+ if (document == null) return { request: next };
93
+ const generatedDir = await fs.mkdtemp(path.join(os.tmpdir(), "no-mistakes-why-"));
94
+ await fs.writeFile(path.join(generatedDir, "plan.json"), JSON.stringify(loadPlanJson(document)));
95
+ next.plan = path.join(generatedDir, "plan.json");
96
+ delete next.planJson;
97
+ return { request: next, generatedDir };
98
+ }
99
+
100
+ async function materializeWhyPlan(options = {}) {
101
+ return (await prepareWhyPlan(options)).request;
102
+ }
103
+
104
+ async function removeGeneratedDir(generatedDir) {
105
+ if (!generatedDir) return;
106
+ await fs.rm(generatedDir, { recursive: true, force: true }).catch(() => {});
107
+ }
108
+
109
+ function camelizeWhy(value) {
110
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
111
+ return camelizeValue(value);
112
+ }
113
+ return Object.fromEntries(
114
+ Object.entries(value).map(([key, nested]) => [key, camelizeValue(nested)]),
115
+ );
116
+ }
117
+
18
118
  async function testsComment(options) {
19
- return native.testsCommentMarkdown(JSON.stringify(options || {}));
119
+ const input = Buffer.from(JSON.stringify(await decamelizePlanOptions(options)));
120
+ return String(await native.testsCommentMarkdown(input));
20
121
  }
21
122
 
22
123
  async function testsGraphMermaid(options) {
23
- return native.testsGraphMermaid(JSON.stringify(options || {}));
124
+ const input = Buffer.from(JSON.stringify(await decamelizePlanOptions(options)));
125
+ return String(await native.testsGraphMermaid(input));
24
126
  }
25
127
 
26
128
  const jsonApis = createJsonApis({
@@ -41,8 +143,44 @@ const jsonApis = createJsonApis({
41
143
  testsWhy: "testsWhyJson",
42
144
  });
43
145
 
146
+ async function testsPlan(options) {
147
+ return camelizeValue(await jsonApis.testsPlan(options));
148
+ }
149
+
150
+ async function testsImpact(options) {
151
+ return camelizeValue(await jsonApis.testsImpact(options));
152
+ }
153
+
154
+ async function testsTargets(options) {
155
+ return camelizeValue(await jsonApis.testsTargets(options));
156
+ }
157
+
158
+ async function testsWhy(options) {
159
+ const { request, generatedDir } = await prepareWhyPlan(options);
160
+ try {
161
+ return camelizeWhy(await jsonApis.testsWhy(request));
162
+ } finally {
163
+ await removeGeneratedDir(generatedDir);
164
+ }
165
+ }
166
+
167
+ async function testsGraph(options) {
168
+ return camelizeValue(await jsonApis.testsGraph(await decamelizePlanOptions(options)));
169
+ }
170
+
44
171
  module.exports = {
172
+ camelizeValue,
173
+ camelizeWhy,
174
+ decamelizePlanOptions,
175
+ materializeWhyPlan,
176
+ prepareWhyPlan,
177
+ removeGeneratedDir,
45
178
  testsComment,
46
179
  testsGraphMermaid,
47
180
  ...jsonApis,
181
+ testsGraph,
182
+ testsImpact,
183
+ testsPlan,
184
+ testsTargets,
185
+ testsWhy,
48
186
  };
@@ -0,0 +1,50 @@
1
+ import type { TestPlanFramework } from "./test-types";
2
+
3
+ export interface ResolvedConfig {
4
+ configPath?: string | null;
5
+ frontendApps: ResolvedFrontendApp[];
6
+ playwright: ResolvedPlaywright;
7
+ vitestFullSuiteTriggers: ResolvedTrigger[];
8
+ fullSuiteTriggers: ResolvedFrameworkTriggers[];
9
+ }
10
+
11
+ export interface ResolvedFrontendApp {
12
+ project?: string | null;
13
+ root: string;
14
+ routeRoot: string;
15
+ selectorRoots: string[];
16
+ }
17
+
18
+ export interface ResolvedPlaywright {
19
+ coverageRoutes: boolean;
20
+ coverageSelectors: boolean;
21
+ frontendRoot?: string | null;
22
+ selectorRoots: string[];
23
+ apps: ResolvedPlaywrightApp[];
24
+ }
25
+
26
+ export interface ResolvedPlaywrightApp {
27
+ playwrightProject: string;
28
+ project?: string | null;
29
+ frontendRoot?: string | null;
30
+ selectorRoots: string[];
31
+ rewrites: ResolvedRewrite[];
32
+ ignoreRoutes: string[];
33
+ }
34
+
35
+ export interface ResolvedRewrite {
36
+ source: string;
37
+ destination: string;
38
+ }
39
+
40
+ export interface ResolvedTrigger {
41
+ name: string;
42
+ paths: string[];
43
+ targets: string[];
44
+ source: "triggers" | "projects";
45
+ }
46
+
47
+ export interface ResolvedFrameworkTriggers {
48
+ framework: TestPlanFramework;
49
+ triggers: ResolvedTrigger[];
50
+ }
package/test-types.d.ts CHANGED
@@ -10,7 +10,12 @@ export type TestPlanFramework =
10
10
  | "go"
11
11
  | "cargo"
12
12
  | "rails"
13
- | "php";
13
+ | "php"
14
+ | "java"
15
+ | "kotlin"
16
+ | "elixir"
17
+ | "dart"
18
+ | "jest";
14
19
 
15
20
  interface TestsPlanOptionsBase {
16
21
  framework?: TestPlanFramework;
@@ -36,6 +41,10 @@ interface TestsPlanOptionsBase {
36
41
  limitPercent?: number;
37
42
  limitFiles?: number;
38
43
  globalConfigFallback?: boolean;
44
+ /** Include the markdown PR comment as `comment` on the returned plan. */
45
+ includeComment?: boolean;
46
+ /** Keep only selected tests whose relative path matches one of these globs. */
47
+ includeGlob?: string[];
39
48
  }
40
49
 
41
50
  /**
@@ -85,16 +94,29 @@ export interface TestsTargetsOptions {
85
94
 
86
95
  export interface TestPlan {
87
96
  /** Complete deterministic changed-file inventory, relative to the request root. */
88
- changed_files: string[];
89
- selected_tests: SelectedTest[];
97
+ changedFiles: string[];
98
+ selectedTests: SelectedTest[];
90
99
  groups?: TestPlanGroup[];
91
100
  warnings: TestPlanWarning[];
92
- fallback_triggered: boolean;
93
- fallback_reason?: string | null;
101
+ fallbackTriggered: boolean;
102
+ fallbackReason?: string | null;
103
+ executionTargets?: GroupedExecutionTarget[];
104
+ comment?: string | null;
105
+ }
106
+
107
+ export interface GroupedExecutionTarget {
108
+ runner: TestPlanFramework;
109
+ config?: string | null;
110
+ project?: string | null;
111
+ /** Path-prefix display name, such as a Swift package root. */
112
+ name?: string;
113
+ baseCommand: string[];
114
+ runnerArgs: string[];
115
+ testFiles: string[];
94
116
  }
95
117
 
96
118
  export interface SelectedTest {
97
- test_file: string;
119
+ testFile: string;
98
120
  confidence: "low" | "medium" | "high";
99
121
  reasons: ImpactReason[];
100
122
  targets?: TestExecutionTarget[];
@@ -106,24 +128,26 @@ export interface TestExecutionTarget {
106
128
  /** True when config is a Vitest workspace/project-array source rendered with --workspace. */
107
129
  workspace?: boolean;
108
130
  project?: string | null;
109
- base_command: string[];
110
- runner_args: string[];
131
+ /** Path-prefix display name, such as a Swift package root. */
132
+ name?: string;
133
+ baseCommand: string[];
134
+ runnerArgs: string[];
111
135
  }
112
136
 
113
137
  export interface ImpactReason {
114
- changed_file: string;
138
+ changedFile: string;
115
139
  path: string[];
116
140
  via: string[];
117
141
  /** When present, aligns index-for-index with `via`. */
118
- via_details?: Array<ImpactEdgeDetail | null>;
142
+ viaDetails?: Array<ImpactEdgeDetail | null>;
119
143
  }
120
144
 
121
145
  export type ImpactEdgeDetail = ResourceImpactEdgeDetail | VitestSetupImpactEdgeDetail;
122
146
 
123
147
  export interface ResourceImpactEdgeDetail {
124
148
  type: "resource";
125
- consumer_file: string;
126
- call_sites: ResourceCallSite[];
149
+ consumerFile: string;
150
+ callSites: ResourceCallSite[];
127
151
  }
128
152
 
129
153
  export interface VitestSetupImpactEdgeDetail {
@@ -132,7 +156,7 @@ export interface VitestSetupImpactEdgeDetail {
132
156
  }
133
157
 
134
158
  export interface ResourceCallSite {
135
- call_kind: ResourceCallKind;
159
+ callKind: ResourceCallKind;
136
160
  line: number;
137
161
  }
138
162
 
@@ -186,6 +210,7 @@ export interface TestsWhyOptions {
186
210
  test: string;
187
211
  changed?: string;
188
212
  plan?: string;
213
+ planJson?: SavedTestPlan | string;
189
214
  }
190
215
 
191
216
  export interface WhyStep {
@@ -194,10 +219,8 @@ export interface WhyStep {
194
219
  detail?: ImpactEdgeDetail | null;
195
220
  }
196
221
 
197
- /** A current or pre-`changed_files` plan accepted by saved-plan document APIs. */
198
- export type SavedTestPlan = Omit<TestPlan, "changed_files"> & {
199
- changed_files?: string[];
200
- };
222
+ /** A current or pre-`changedFiles` plan accepted by saved-plan document APIs. */
223
+ export type SavedTestPlan = TestPlan;
201
224
 
202
225
  export interface TestsPlanDocumentOptions {
203
226
  plan?: string;
@@ -31,7 +31,12 @@ export type Relationship =
31
31
  | "rust"
32
32
  | "ruby"
33
33
  | "php"
34
+ | "java"
35
+ | "kotlin"
36
+ | "elixir"
37
+ | "dart"
34
38
  | "resource"
39
+ | "trpc"
35
40
  | "all";
36
41
 
37
42
  export interface TraverseOptions {
@@ -59,6 +64,10 @@ export interface DependencyFile {
59
64
  job?: string;
60
65
  /** Zero-based step index for a virtual GitHub Actions workflow step node. */
61
66
  step?: number;
67
+ /** Router file for a virtual tRPC procedure node. */
68
+ routerFile?: string;
69
+ /** Dotted procedure path for a virtual tRPC procedure node (`user.get`). */
70
+ procedure?: string;
62
71
  module?: string;
63
72
  depth: number;
64
73
  via?: string[];
package/types.d.ts CHANGED
@@ -11,3 +11,4 @@ export * from "./workflow-topology-types";
11
11
  export * from "./query-types";
12
12
  export * from "./named-query-types";
13
13
  export * from "./mermaid-types";
14
+ export * from "./resolve-config-types";
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+
3
+ function stepMatches(selector, step) {
4
+ return (
5
+ (selector.id === undefined || selector.id === step.id) &&
6
+ (selector.uses === undefined || selector.uses === step.uses) &&
7
+ (selector.name === undefined || selector.name === step.name)
8
+ );
9
+ }
10
+
11
+ function directCallerJobIdsForUses(jobsById, uses) {
12
+ const ids = [];
13
+ for (const job of jobsById.values()) {
14
+ if (job.steps.some((step) => step.uses === uses)) ids.push(job.id);
15
+ }
16
+ return Object.freeze(ids.sort());
17
+ }
18
+
19
+ function stepOrderIndexes(job, selectors) {
20
+ return Object.freeze(
21
+ selectors.map((selector) => {
22
+ const step = job.steps.find((candidate) => stepMatches(selector, candidate));
23
+ return step == null ? -1 : step.index;
24
+ }),
25
+ );
26
+ }
27
+
28
+ module.exports = { directCallerJobIdsForUses, stepOrderIndexes };
@@ -24,6 +24,13 @@ export interface WorkflowTopologyIndex {
24
24
  directDownstreamJobIds(jobId: string): readonly string[];
25
25
  transitiveDownstreamJobIds(jobId: string): readonly string[];
26
26
  directCallerJobIds(workflowPath: string): readonly string[];
27
+ /** Job ids whose steps `uses:` this action or workflow path. */
28
+ directCallerJobIdsForUses(uses: string): readonly string[];
29
+ /** Step indexes for `selectors` in `jobId`, or `-1` when a selector is missing. */
30
+ stepOrderIndexes(
31
+ jobId: string,
32
+ selectors: ReadonlyArray<{ id?: string; uses?: string; name?: string }>,
33
+ ): readonly number[];
27
34
  directCallerWorkflowPaths(workflowPath: string): readonly string[];
28
35
  transitiveCallerWorkflowPaths(workflowPath: string): readonly string[];
29
36
  directCalleeWorkflowPaths(workflowPath: string): readonly string[];
@@ -1,5 +1,10 @@
1
1
  "use strict";
2
2
 
3
+ const {
4
+ directCallerJobIdsForUses,
5
+ stepOrderIndexes,
6
+ } = require("./workflow-topology-index-helpers");
7
+
3
8
  // A pure-JS query index rebuilt from the `ciTopology()` JSON, ported from
4
9
  // the original engine's `topology-index.mts` + `frozen-topology.mts`. This
5
10
  // stays JS-only by design: it returns closures over frozen `Map`s, which
@@ -214,6 +219,11 @@ function createWorkflowTopologyIndex(topology) {
214
219
  transitiveWorkflowRunSubscriberPaths: workflowQuery(workflowRunSubscribers, true),
215
220
  artifactProducersForConsumerJob: artifactEdgeQuery(artifactProducers),
216
221
  artifactConsumersForProducerJob: artifactEdgeQuery(artifactConsumers),
222
+ directCallerJobIdsForUses: (uses) => directCallerJobIdsForUses(jobsById, uses),
223
+ stepOrderIndexes: (jobId, selectors) => {
224
+ assertKnown(jobsById, jobId, "workflow job");
225
+ return stepOrderIndexes(jobsById.get(jobId), selectors);
226
+ },
217
227
  });
218
228
  }
219
229