no-mistakes 0.33.0 → 0.34.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/README.md +9 -0
- package/analyze-project-types.d.ts +3 -0
- package/ci-types.d.ts +7 -0
- package/flow-types.d.ts +5 -0
- package/import-usage-types.d.ts +1 -0
- package/index-ci-infra.d.ts +70 -0
- package/index.d.ts +1 -55
- package/index.js +7 -0
- package/named-query-types.d.ts +9 -0
- package/package.json +2 -1
- package/report-types.d.ts +6 -0
- package/scripts/install/retry.js +9 -3
- package/test-types.d.ts +39 -1
- package/traversal-types.d.ts +25 -0
- package/types.d.ts +1 -0
- package/workflow-topology-artifact-types.d.ts +56 -0
- package/workflow-topology-index-types.d.ts +39 -0
- package/workflow-topology-index.js +220 -0
- package/workflow-topology-types.d.ts +179 -0
package/README.md
CHANGED
|
@@ -77,6 +77,8 @@ const {
|
|
|
77
77
|
});
|
|
78
78
|
const projectCheck = await check({
|
|
79
79
|
root: process.cwd(),
|
|
80
|
+
// Path to tsconfig.json for alias resolution; searched upward if omitted.
|
|
81
|
+
// In monorepos, pass the workspace-scoped tsconfig (e.g. "web/tsconfig.json").
|
|
80
82
|
tsconfig: "tsconfig.json",
|
|
81
83
|
});
|
|
82
84
|
const localFlow = await flow({
|
|
@@ -117,6 +119,13 @@ CLI and Node analyses share a per-user machine-wide lock. CLI flags
|
|
|
117
119
|
Waiting does not alter successful output, and Node lock/timeout failures reject
|
|
118
120
|
the returned Promise.
|
|
119
121
|
|
|
122
|
+
Dependency graph, query, and test-planning resolution is per workspace by
|
|
123
|
+
default: when `tsconfig` is omitted, each import uses the config that owns its
|
|
124
|
+
importing file, including referenced projects. This keeps conflicting package
|
|
125
|
+
aliases isolated while shared code can still select all importing tests. Pass
|
|
126
|
+
`tsconfig` explicitly to force one config for a whole invocation when debugging
|
|
127
|
+
or preserving a legacy single-config workflow.
|
|
128
|
+
|
|
120
129
|
External `no-mistakes-*` executables on `PATH` can be invoked as subcommands.
|
|
121
130
|
For example, after installing `no-mistakes-scripts`:
|
|
122
131
|
|
|
@@ -55,8 +55,11 @@ export type AnalyzeProjectReportRequest =
|
|
|
55
55
|
| ({ type: "check"; id?: string } & BatchedCheckOptions);
|
|
56
56
|
|
|
57
57
|
export interface AnalyzeProjectOptions {
|
|
58
|
+
/** Project root. Defaults to the current working directory. */
|
|
58
59
|
root?: string;
|
|
60
|
+
/** Path to tsconfig.json for alias resolution. Searched upward if omitted. */
|
|
59
61
|
tsconfig?: string;
|
|
62
|
+
/** Path to the no-mistakes config file (e.g. .no-mistakes.yml). Auto-discovered in root if omitted. */
|
|
60
63
|
config?: string;
|
|
61
64
|
filters?: string[];
|
|
62
65
|
reports: AnalyzeProjectReportRequest[];
|
package/ci-types.d.ts
CHANGED
|
@@ -3,22 +3,29 @@
|
|
|
3
3
|
// snake_case to match the serde-serialized output.
|
|
4
4
|
|
|
5
5
|
export interface CiImpactOptions {
|
|
6
|
+
/** Project root. Defaults to the current working directory. */
|
|
6
7
|
root?: string;
|
|
8
|
+
/** Path to the no-mistakes config file (e.g. .no-mistakes.yml). Auto-discovered in root if omitted. */
|
|
7
9
|
config?: string;
|
|
8
10
|
/** Changed file paths (relative to root or absolute). */
|
|
9
11
|
files: string[];
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
export interface CiEnvOptions {
|
|
15
|
+
/** Project root. Defaults to the current working directory. */
|
|
13
16
|
root?: string;
|
|
17
|
+
/** Path to the no-mistakes config file (e.g. .no-mistakes.yml). Auto-discovered in root if omitted. */
|
|
14
18
|
config?: string;
|
|
15
19
|
/** Environment variable name (case-sensitive). */
|
|
16
20
|
var: string;
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
export interface ImpactedChecksOptions {
|
|
24
|
+
/** Project root. Defaults to the current working directory. */
|
|
20
25
|
root?: string;
|
|
26
|
+
/** Path to the no-mistakes config file (e.g. .no-mistakes.yml). Auto-discovered in root if omitted. */
|
|
21
27
|
config?: string;
|
|
28
|
+
/** Path to tsconfig.json for alias resolution. Searched upward if omitted. */
|
|
22
29
|
tsconfig?: string;
|
|
23
30
|
base?: string;
|
|
24
31
|
head?: string;
|
package/flow-types.d.ts
CHANGED
|
@@ -2,8 +2,11 @@ import type { Relationship } from "./traversal-types";
|
|
|
2
2
|
|
|
3
3
|
export interface FlowOptions {
|
|
4
4
|
target: string;
|
|
5
|
+
/** Project root. Defaults to the current working directory. */
|
|
5
6
|
root?: string;
|
|
7
|
+
/** Path to tsconfig.json for alias resolution. Searched upward if omitted. */
|
|
6
8
|
tsconfig?: string;
|
|
9
|
+
/** Path to the no-mistakes config file (e.g. .no-mistakes.yml). Auto-discovered in root if omitted. */
|
|
7
10
|
config?: string;
|
|
8
11
|
direction?: "deps" | "dependents" | "both";
|
|
9
12
|
depth?: number;
|
|
@@ -35,7 +38,9 @@ export interface FlowReport {
|
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
export interface FetchesOptions {
|
|
41
|
+
/** Project root. Defaults to the current working directory. */
|
|
38
42
|
root?: string;
|
|
43
|
+
/** Path to the no-mistakes config file (e.g. .no-mistakes.yml). Auto-discovered in root if omitted. */
|
|
39
44
|
config?: string;
|
|
40
45
|
targets?: string[];
|
|
41
46
|
}
|
package/import-usage-types.d.ts
CHANGED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// CI, impacted-checks, data-flow, infra, and Swift API declarations, split
|
|
2
|
+
// out of `index.d.ts` to stay under the 200-line file limit.
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
CiEnvOptions,
|
|
6
|
+
CiEnvReport,
|
|
7
|
+
CiImpactOptions,
|
|
8
|
+
CiImpactReport,
|
|
9
|
+
CiTopologyOptions,
|
|
10
|
+
DataPwOptions,
|
|
11
|
+
DataPwReport,
|
|
12
|
+
EffectsOptions,
|
|
13
|
+
EffectsReport,
|
|
14
|
+
ImpactedChecksOptions,
|
|
15
|
+
ImpactedChecksReport,
|
|
16
|
+
InfraOptions,
|
|
17
|
+
LockfileDiffEntry,
|
|
18
|
+
LockfileDiffOptions,
|
|
19
|
+
ModuleOutputsResult,
|
|
20
|
+
RegistryExtensionOptions,
|
|
21
|
+
RegistryExtensionReport,
|
|
22
|
+
ResourceRefRow,
|
|
23
|
+
RscCallersOptions,
|
|
24
|
+
RscCallersReport,
|
|
25
|
+
SwiftImporterRow,
|
|
26
|
+
SwiftOptions,
|
|
27
|
+
SwiftTestTargetRow,
|
|
28
|
+
TestForRow,
|
|
29
|
+
WithInvocationOptions,
|
|
30
|
+
WorkflowTopology,
|
|
31
|
+
WorkflowTopologyIndex,
|
|
32
|
+
} from "./types";
|
|
33
|
+
|
|
34
|
+
export function lockfileDiff(
|
|
35
|
+
options: WithInvocationOptions<LockfileDiffOptions>,
|
|
36
|
+
): Promise<LockfileDiffEntry[]>;
|
|
37
|
+
export function ciImpact(options: WithInvocationOptions<CiImpactOptions>): Promise<CiImpactReport>;
|
|
38
|
+
export function ciEnv(options: WithInvocationOptions<CiEnvOptions>): Promise<CiEnvReport>;
|
|
39
|
+
export function ciTopology(
|
|
40
|
+
options?: WithInvocationOptions<CiTopologyOptions>,
|
|
41
|
+
): Promise<WorkflowTopology>;
|
|
42
|
+
/** Builds a query index over a `ciTopology()` result. Pure JS — never crosses the N-API boundary. */
|
|
43
|
+
export function createWorkflowTopologyIndex(topology: WorkflowTopology): WorkflowTopologyIndex;
|
|
44
|
+
export function impactedChecks(
|
|
45
|
+
options: WithInvocationOptions<ImpactedChecksOptions>,
|
|
46
|
+
): Promise<ImpactedChecksReport>;
|
|
47
|
+
export function dataPw(options: WithInvocationOptions<DataPwOptions>): Promise<DataPwReport>;
|
|
48
|
+
export function effects(options: WithInvocationOptions<EffectsOptions>): Promise<EffectsReport>;
|
|
49
|
+
export function rscCallers(
|
|
50
|
+
options: WithInvocationOptions<RscCallersOptions>,
|
|
51
|
+
): Promise<RscCallersReport>;
|
|
52
|
+
export function registryExtension(
|
|
53
|
+
options: WithInvocationOptions<RegistryExtensionOptions>,
|
|
54
|
+
): Promise<RegistryExtensionReport>;
|
|
55
|
+
export function infraResourceRefs(
|
|
56
|
+
options: WithInvocationOptions<InfraOptions & { address: string }>,
|
|
57
|
+
): Promise<ResourceRefRow[]>;
|
|
58
|
+
export function infraOutputs(
|
|
59
|
+
options: WithInvocationOptions<InfraOptions & { moduleDir: string }>,
|
|
60
|
+
): Promise<ModuleOutputsResult>;
|
|
61
|
+
export function infraTestFor(
|
|
62
|
+
options: WithInvocationOptions<InfraOptions & { tfFile: string }>,
|
|
63
|
+
): Promise<TestForRow[]>;
|
|
64
|
+
export function swiftImporters(
|
|
65
|
+
options: WithInvocationOptions<SwiftOptions & { file: string }>,
|
|
66
|
+
): Promise<SwiftImporterRow[]>;
|
|
67
|
+
export function swiftTestTargets(
|
|
68
|
+
options: WithInvocationOptions<SwiftOptions & { file: string }>,
|
|
69
|
+
): Promise<SwiftTestTargetRow[]>;
|
|
70
|
+
export function version(): Promise<string>;
|
package/index.d.ts
CHANGED
|
@@ -2,30 +2,16 @@ import type {
|
|
|
2
2
|
CheckReport,
|
|
3
3
|
AnalyzeProjectOptions,
|
|
4
4
|
AnalyzeProjectResult,
|
|
5
|
-
CiEnvOptions,
|
|
6
|
-
CiEnvReport,
|
|
7
|
-
CiImpactOptions,
|
|
8
|
-
CiImpactReport,
|
|
9
5
|
CallSitesOptions,
|
|
10
6
|
CallSitesResult,
|
|
11
|
-
DataPwOptions,
|
|
12
|
-
DataPwReport,
|
|
13
7
|
DeadExportsOptions,
|
|
14
8
|
DeadExportsResult,
|
|
15
|
-
EffectsOptions,
|
|
16
|
-
EffectsReport,
|
|
17
|
-
RscCallersOptions,
|
|
18
|
-
RscCallersReport,
|
|
19
|
-
RegistryExtensionOptions,
|
|
20
|
-
RegistryExtensionReport,
|
|
21
9
|
DependencyResult,
|
|
22
10
|
ExportsOfOptions,
|
|
23
11
|
ExportsOfResult,
|
|
24
12
|
FetchesOptions,
|
|
25
13
|
FlowOptions,
|
|
26
14
|
FlowReport,
|
|
27
|
-
ImpactedChecksOptions,
|
|
28
|
-
ImpactedChecksReport,
|
|
29
15
|
ImportUsagesOptions,
|
|
30
16
|
ImportUsagesResult,
|
|
31
17
|
ImportersOptions,
|
|
@@ -33,10 +19,6 @@ import type {
|
|
|
33
19
|
ResolveCheckOptions,
|
|
34
20
|
ResolveCheckResult,
|
|
35
21
|
GraphEdge,
|
|
36
|
-
InfraOptions,
|
|
37
|
-
LockfileDiffEntry,
|
|
38
|
-
LockfileDiffOptions,
|
|
39
|
-
ModuleOutputsResult,
|
|
40
22
|
PlaywrightOptions,
|
|
41
23
|
PlaywrightRelatedOptions,
|
|
42
24
|
ProjectOptions,
|
|
@@ -44,14 +26,9 @@ import type {
|
|
|
44
26
|
ReactComponentFacts,
|
|
45
27
|
ReactUsagesReport,
|
|
46
28
|
ReactViolation,
|
|
47
|
-
ResourceRefRow,
|
|
48
29
|
ServerRoutesReport,
|
|
49
30
|
ServerContractsReport,
|
|
50
31
|
SignatureImpactResult,
|
|
51
|
-
SwiftImporterRow,
|
|
52
|
-
SwiftOptions,
|
|
53
|
-
SwiftTestTargetRow,
|
|
54
|
-
TestForRow,
|
|
55
32
|
SymbolsListOptions,
|
|
56
33
|
SymbolsOptions,
|
|
57
34
|
SymbolsResult,
|
|
@@ -70,6 +47,7 @@ import type {
|
|
|
70
47
|
} from "./types";
|
|
71
48
|
|
|
72
49
|
export * from "./types";
|
|
50
|
+
export * from "./index-ci-infra";
|
|
73
51
|
|
|
74
52
|
export function dependencies(
|
|
75
53
|
options: WithInvocationOptions<TraverseOptions>,
|
|
@@ -166,35 +144,3 @@ export function reactCheck(
|
|
|
166
144
|
export function reactUsages(
|
|
167
145
|
options: WithInvocationOptions<ProjectOptions & { target: string }>,
|
|
168
146
|
): Promise<ReactUsagesReport>;
|
|
169
|
-
export function lockfileDiff(
|
|
170
|
-
options: WithInvocationOptions<LockfileDiffOptions>,
|
|
171
|
-
): Promise<LockfileDiffEntry[]>;
|
|
172
|
-
export function ciImpact(options: WithInvocationOptions<CiImpactOptions>): Promise<CiImpactReport>;
|
|
173
|
-
export function ciEnv(options: WithInvocationOptions<CiEnvOptions>): Promise<CiEnvReport>;
|
|
174
|
-
export function impactedChecks(
|
|
175
|
-
options: WithInvocationOptions<ImpactedChecksOptions>,
|
|
176
|
-
): Promise<ImpactedChecksReport>;
|
|
177
|
-
export function dataPw(options: WithInvocationOptions<DataPwOptions>): Promise<DataPwReport>;
|
|
178
|
-
export function effects(options: WithInvocationOptions<EffectsOptions>): Promise<EffectsReport>;
|
|
179
|
-
export function rscCallers(
|
|
180
|
-
options: WithInvocationOptions<RscCallersOptions>,
|
|
181
|
-
): Promise<RscCallersReport>;
|
|
182
|
-
export function registryExtension(
|
|
183
|
-
options: WithInvocationOptions<RegistryExtensionOptions>,
|
|
184
|
-
): Promise<RegistryExtensionReport>;
|
|
185
|
-
export function infraResourceRefs(
|
|
186
|
-
options: WithInvocationOptions<InfraOptions & { address: string }>,
|
|
187
|
-
): Promise<ResourceRefRow[]>;
|
|
188
|
-
export function infraOutputs(
|
|
189
|
-
options: WithInvocationOptions<InfraOptions & { moduleDir: string }>,
|
|
190
|
-
): Promise<ModuleOutputsResult>;
|
|
191
|
-
export function infraTestFor(
|
|
192
|
-
options: WithInvocationOptions<InfraOptions & { tfFile: string }>,
|
|
193
|
-
): Promise<TestForRow[]>;
|
|
194
|
-
export function swiftImporters(
|
|
195
|
-
options: WithInvocationOptions<SwiftOptions & { file: string }>,
|
|
196
|
-
): Promise<SwiftImporterRow[]>;
|
|
197
|
-
export function swiftTestTargets(
|
|
198
|
-
options: WithInvocationOptions<SwiftOptions & { file: string }>,
|
|
199
|
-
): Promise<SwiftTestTargetRow[]>;
|
|
200
|
-
export function version(): Promise<string>;
|
package/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const native = require("./bin/no-mistakes.node");
|
|
4
4
|
const planning = require("./planning");
|
|
5
|
+
const { createWorkflowTopologyIndex } = require("./workflow-topology-index");
|
|
5
6
|
|
|
6
7
|
async function callJson(fn, options) {
|
|
7
8
|
return JSON.parse(await fn(JSON.stringify(options || {})));
|
|
@@ -115,6 +116,10 @@ async function ciEnv(options) {
|
|
|
115
116
|
return callJson(native.ciEnvJson, options);
|
|
116
117
|
}
|
|
117
118
|
|
|
119
|
+
async function ciTopology(options) {
|
|
120
|
+
return callJson(native.ciTopologyJson, options);
|
|
121
|
+
}
|
|
122
|
+
|
|
118
123
|
async function impactedChecks(options) {
|
|
119
124
|
return callJson(native.impactedChecksJson, options);
|
|
120
125
|
}
|
|
@@ -149,6 +154,8 @@ module.exports = {
|
|
|
149
154
|
check,
|
|
150
155
|
ciEnv,
|
|
151
156
|
ciImpact,
|
|
157
|
+
ciTopology,
|
|
158
|
+
createWorkflowTopologyIndex,
|
|
152
159
|
dataPw,
|
|
153
160
|
deadExports,
|
|
154
161
|
dependencies,
|
package/named-query-types.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// Named query modes (issue #419): data-pw, effects, rsc-callers, registry-extension.
|
|
2
2
|
|
|
3
3
|
export interface DataPwOptions {
|
|
4
|
+
/** Project root. Defaults to the current working directory. */
|
|
4
5
|
root?: string;
|
|
6
|
+
/** Path to the no-mistakes config file (e.g. .no-mistakes.yml). Auto-discovered in root if omitted. */
|
|
5
7
|
config?: string;
|
|
6
8
|
/** The selector-attribute value to find (e.g. `search-bar`). */
|
|
7
9
|
value: string;
|
|
@@ -29,8 +31,11 @@ export interface DataPwReport {
|
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
export interface EffectsOptions {
|
|
34
|
+
/** Project root. Defaults to the current working directory. */
|
|
32
35
|
root?: string;
|
|
36
|
+
/** Path to tsconfig.json for alias resolution. Searched upward if omitted. */
|
|
33
37
|
tsconfig?: string;
|
|
38
|
+
/** Path to the no-mistakes config file (e.g. .no-mistakes.yml). Auto-discovered in root if omitted. */
|
|
34
39
|
config?: string;
|
|
35
40
|
/** Effect kind to resolve (a key under `effects:` in config). */
|
|
36
41
|
kind: string;
|
|
@@ -59,8 +64,11 @@ export interface EffectsReport {
|
|
|
59
64
|
}
|
|
60
65
|
|
|
61
66
|
export interface RscCallersOptions {
|
|
67
|
+
/** Project root. Defaults to the current working directory. */
|
|
62
68
|
root?: string;
|
|
69
|
+
/** Path to tsconfig.json for alias resolution. Searched upward if omitted. */
|
|
63
70
|
tsconfig?: string;
|
|
71
|
+
/** Path to the no-mistakes config file (e.g. .no-mistakes.yml). Auto-discovered in root if omitted. */
|
|
64
72
|
config?: string;
|
|
65
73
|
/** Component file to find RSC callers of. */
|
|
66
74
|
component: string;
|
|
@@ -81,6 +89,7 @@ export interface RscCallersReport {
|
|
|
81
89
|
}
|
|
82
90
|
|
|
83
91
|
export interface RegistryExtensionOptions {
|
|
92
|
+
/** Project root. Defaults to the current working directory. */
|
|
84
93
|
root?: string;
|
|
85
94
|
/** Registry file to summarize the entry pattern of. */
|
|
86
95
|
registryFile: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "no-mistakes",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
4
4
|
"description": "Static codebase analysis tools for TS/JS dependencies, dependents, and symbols",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"*.d.ts",
|
|
17
17
|
"index.js",
|
|
18
18
|
"planning.js",
|
|
19
|
+
"workflow-topology-index.js",
|
|
19
20
|
"scripts/install.js",
|
|
20
21
|
"scripts/install/",
|
|
21
22
|
"README.md",
|
package/report-types.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export interface PlaywrightOptions {
|
|
2
|
+
/** Project root. Defaults to the current working directory. */
|
|
2
3
|
root?: string;
|
|
4
|
+
/** Path to the no-mistakes config file (e.g. .no-mistakes.yml). Auto-discovered in root if omitted. */
|
|
3
5
|
config?: string;
|
|
4
6
|
playwrightConfig?: string[];
|
|
5
7
|
project?: string;
|
|
@@ -40,7 +42,9 @@ export interface GraphEdge {
|
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
export interface InfraOptions {
|
|
45
|
+
/** Project root. Defaults to the current working directory. */
|
|
43
46
|
root?: string;
|
|
47
|
+
/** Path to the no-mistakes config file (e.g. .no-mistakes.yml). Auto-discovered in root if omitted. */
|
|
44
48
|
config?: string;
|
|
45
49
|
/** `infraResourceRefs` address (`<type>.<name>`). */
|
|
46
50
|
address?: string;
|
|
@@ -79,7 +83,9 @@ export interface TestForRow {
|
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
export interface SwiftOptions {
|
|
86
|
+
/** Project root. Defaults to the current working directory. */
|
|
82
87
|
root?: string;
|
|
88
|
+
/** Path to the no-mistakes config file (e.g. .no-mistakes.yml). Auto-discovered in root if omitted. */
|
|
83
89
|
config?: string;
|
|
84
90
|
/** The Swift source file to query (relative to root). */
|
|
85
91
|
file?: string;
|
package/scripts/install/retry.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
// A GitHub Releases asset download or its separate .sha256 checksum fetch can
|
|
4
|
+
// hit a short-lived CDN 5xx, timeout, or connection reset. These defaults give
|
|
5
|
+
// each network operation a bounded multi-attempt window (~15s avg / ~31s worst
|
|
6
|
+
// case total sleep) to ride that out, rather than exhausting a tiny budget in a
|
|
7
|
+
// few seconds. Override via NO_MISTAKES_DOWNLOAD_MAX_ATTEMPTS /
|
|
8
|
+
// NO_MISTAKES_DOWNLOAD_RETRY_BASE_MS.
|
|
9
|
+
const DEFAULT_MAX_ATTEMPTS = 6;
|
|
10
|
+
const DEFAULT_RETRY_BASE_MS = 1_000;
|
|
11
|
+
const RETRY_MAX_DELAY_MS = 30_000;
|
|
6
12
|
const RETRYABLE_NETWORK_CODES = new Set([
|
|
7
13
|
"ECONNRESET",
|
|
8
14
|
"ECONNREFUSED",
|
package/test-types.d.ts
CHANGED
|
@@ -2,8 +2,11 @@ import type { SymbolEntrypoint } from "./traversal-types";
|
|
|
2
2
|
|
|
3
3
|
export interface TestsPlanOptions {
|
|
4
4
|
framework?: "vitest" | "playwright" | "dotnet" | "swift";
|
|
5
|
+
/** Project root. Defaults to the current working directory. */
|
|
5
6
|
root?: string;
|
|
7
|
+
/** Path to the no-mistakes config file (e.g. .no-mistakes.yml). Auto-discovered in root if omitted. */
|
|
6
8
|
config?: string;
|
|
9
|
+
/** Path to tsconfig.json for alias resolution. Searched upward if omitted. */
|
|
7
10
|
tsconfig?: string;
|
|
8
11
|
base?: string;
|
|
9
12
|
head?: string;
|
|
@@ -24,8 +27,11 @@ export interface TestsPlanOptions {
|
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
export interface TestsImpactOptions {
|
|
30
|
+
/** Project root. Defaults to the current working directory. */
|
|
27
31
|
root?: string;
|
|
32
|
+
/** Path to the no-mistakes config file (e.g. .no-mistakes.yml). Auto-discovered in root if omitted. */
|
|
28
33
|
config?: string;
|
|
34
|
+
/** Path to tsconfig.json for alias resolution. Searched upward if omitted. */
|
|
29
35
|
tsconfig?: string;
|
|
30
36
|
/** Entrypoints to trace impact from: strings may use file#export, or pass { file, symbol }. */
|
|
31
37
|
entrypoints: Array<string | SymbolEntrypoint>;
|
|
@@ -35,7 +41,9 @@ export interface TestsImpactOptions {
|
|
|
35
41
|
|
|
36
42
|
export interface TestsTargetsOptions {
|
|
37
43
|
framework: "vitest" | "playwright" | "dotnet" | "swift";
|
|
44
|
+
/** Project root. Defaults to the current working directory. */
|
|
38
45
|
root?: string;
|
|
46
|
+
/** Path to the no-mistakes config file (e.g. .no-mistakes.yml). Auto-discovered in root if omitted. */
|
|
39
47
|
config?: string;
|
|
40
48
|
files: string[];
|
|
41
49
|
}
|
|
@@ -67,8 +75,32 @@ export interface ImpactReason {
|
|
|
67
75
|
changed_file: string;
|
|
68
76
|
path: string[];
|
|
69
77
|
via: string[];
|
|
78
|
+
/** When present, aligns index-for-index with `via`. */
|
|
79
|
+
via_details?: Array<ImpactEdgeDetail | null>;
|
|
70
80
|
}
|
|
71
81
|
|
|
82
|
+
export type ImpactEdgeDetail = ResourceImpactEdgeDetail;
|
|
83
|
+
|
|
84
|
+
export interface ResourceImpactEdgeDetail {
|
|
85
|
+
type: "resource";
|
|
86
|
+
consumer_file: string;
|
|
87
|
+
call_sites: ResourceCallSite[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface ResourceCallSite {
|
|
91
|
+
call_kind: ResourceCallKind;
|
|
92
|
+
line: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Static runtime filesystem API that created a resource dependency edge. */
|
|
96
|
+
export type ResourceCallKind =
|
|
97
|
+
| "read-file"
|
|
98
|
+
| "read-file-sync"
|
|
99
|
+
| "read-directory"
|
|
100
|
+
| "read-directory-sync"
|
|
101
|
+
| "glob"
|
|
102
|
+
| "glob-sync";
|
|
103
|
+
|
|
72
104
|
export interface TestPlanGroup {
|
|
73
105
|
type: string;
|
|
74
106
|
selected: string[];
|
|
@@ -80,6 +112,7 @@ export interface TestPlanWarning {
|
|
|
80
112
|
type: string;
|
|
81
113
|
message: string;
|
|
82
114
|
file: string;
|
|
115
|
+
line?: number;
|
|
83
116
|
}
|
|
84
117
|
|
|
85
118
|
export interface TestsTargetsReport {
|
|
@@ -100,8 +133,11 @@ export interface TestTargetWarning {
|
|
|
100
133
|
}
|
|
101
134
|
|
|
102
135
|
export interface TestsWhyOptions {
|
|
136
|
+
/** Project root. Defaults to the current working directory. */
|
|
103
137
|
root?: string;
|
|
138
|
+
/** Path to the no-mistakes config file (e.g. .no-mistakes.yml). Auto-discovered in root if omitted. */
|
|
104
139
|
config?: string;
|
|
140
|
+
/** Path to tsconfig.json for alias resolution. Searched upward if omitted. */
|
|
105
141
|
tsconfig?: string;
|
|
106
142
|
test: string;
|
|
107
143
|
changed?: string;
|
|
@@ -111,6 +147,7 @@ export interface TestsWhyOptions {
|
|
|
111
147
|
export interface WhyStep {
|
|
112
148
|
node: string;
|
|
113
149
|
via?: string | null;
|
|
150
|
+
detail?: ImpactEdgeDetail | null;
|
|
114
151
|
}
|
|
115
152
|
|
|
116
153
|
export interface TestsPlanDocumentOptions {
|
|
@@ -120,10 +157,11 @@ export interface TestsPlanDocumentOptions {
|
|
|
120
157
|
|
|
121
158
|
export interface TestGraph {
|
|
122
159
|
nodes: Array<{ name: string; type: "changed" | "test" | "intermediate" }>;
|
|
123
|
-
edges: Array<{ from: string; to: string; via: string }>;
|
|
160
|
+
edges: Array<{ from: string; to: string; via: string; detail?: ImpactEdgeDetail }>;
|
|
124
161
|
}
|
|
125
162
|
|
|
126
163
|
export interface LockfileDiffOptions {
|
|
164
|
+
/** Project root. Defaults to the current working directory. */
|
|
127
165
|
root?: string;
|
|
128
166
|
base: string;
|
|
129
167
|
head?: string;
|
package/traversal-types.d.ts
CHANGED
|
@@ -19,11 +19,14 @@ export type Relationship =
|
|
|
19
19
|
| "dotnet"
|
|
20
20
|
| "swift"
|
|
21
21
|
| "terraform"
|
|
22
|
+
| "resource"
|
|
22
23
|
| "all";
|
|
23
24
|
|
|
24
25
|
export interface TraverseOptions {
|
|
25
26
|
files: Array<string | SymbolEntrypoint>;
|
|
27
|
+
/** Project root. Defaults to the current working directory. */
|
|
26
28
|
root?: string;
|
|
29
|
+
/** Path to tsconfig.json for alias resolution. Searched upward if omitted. */
|
|
27
30
|
tsconfig?: string;
|
|
28
31
|
depth?: number;
|
|
29
32
|
filters?: string[];
|
|
@@ -49,9 +52,25 @@ export interface SymbolEntrypoint {
|
|
|
49
52
|
symbol?: string;
|
|
50
53
|
}
|
|
51
54
|
|
|
55
|
+
export interface TsConfigDiagnostic {
|
|
56
|
+
kind: "ambiguous-ownership" | "invalid-config" | "invalid-extends" | "invalid-reference";
|
|
57
|
+
config: string | null;
|
|
58
|
+
file: string | null;
|
|
59
|
+
detail: string;
|
|
60
|
+
candidates: string[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface TsConfigProvenance {
|
|
64
|
+
importer: string;
|
|
65
|
+
config: string | null;
|
|
66
|
+
forced: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
52
69
|
export interface DependencyResult {
|
|
53
70
|
roots: string[];
|
|
54
71
|
files: DependencyFile[];
|
|
72
|
+
diagnostics: TsConfigDiagnostic[];
|
|
73
|
+
tsconfig_provenance: TsConfigProvenance[];
|
|
55
74
|
}
|
|
56
75
|
|
|
57
76
|
export type ExportKind =
|
|
@@ -68,8 +87,11 @@ export type ExportKind =
|
|
|
68
87
|
|
|
69
88
|
export interface SymbolsOptions {
|
|
70
89
|
files: string[];
|
|
90
|
+
/** Project root. Defaults to the current working directory. */
|
|
71
91
|
root?: string;
|
|
92
|
+
/** Path to tsconfig.json for alias resolution. Searched upward if omitted. */
|
|
72
93
|
tsconfig?: string;
|
|
94
|
+
/** Path to the no-mistakes config file (e.g. .no-mistakes.yml). Auto-discovered in root if omitted. */
|
|
73
95
|
config?: string;
|
|
74
96
|
mode?: "list" | "signature-impact";
|
|
75
97
|
symbol?: string;
|
|
@@ -155,8 +177,11 @@ export interface SignatureImpactResult {
|
|
|
155
177
|
}
|
|
156
178
|
|
|
157
179
|
export interface ProjectOptions {
|
|
180
|
+
/** Project root. Defaults to the current working directory. */
|
|
158
181
|
root?: string;
|
|
182
|
+
/** Path to tsconfig.json for alias resolution. Searched upward if omitted. */
|
|
159
183
|
tsconfig?: string;
|
|
184
|
+
/** Path to the no-mistakes config file (e.g. .no-mistakes.yml). Auto-discovered in root if omitted. */
|
|
160
185
|
config?: string;
|
|
161
186
|
filters?: string[];
|
|
162
187
|
targets?: string[];
|
package/types.d.ts
CHANGED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Artifact-dataflow types for `ciTopology()`, split out of
|
|
2
|
+
// `workflow-topology-types.d.ts` to stay under the 200-line file limit.
|
|
3
|
+
// Mirrors the schema-v1 JSON contract exactly (field names, casing) — see
|
|
4
|
+
// `docs/node-api.md` for the stability guarantees.
|
|
5
|
+
|
|
6
|
+
export type ArtifactValue =
|
|
7
|
+
| { kind: "static"; raw: string; value: string; instanceCount?: number }
|
|
8
|
+
| { kind: "finite"; raw: string; values: string[]; instanceCounts: Record<string, number> }
|
|
9
|
+
| { kind: "dynamic"; raw: string }
|
|
10
|
+
| { kind: "path-derived"; reason: "archive-disabled" };
|
|
11
|
+
|
|
12
|
+
export type ArtifactActionFlag =
|
|
13
|
+
| { kind: "static"; raw?: string; effective: boolean }
|
|
14
|
+
| { kind: "dynamic"; raw: string };
|
|
15
|
+
|
|
16
|
+
export interface ArtifactUploadDeclaration {
|
|
17
|
+
kind: "upload";
|
|
18
|
+
name: ArtifactValue;
|
|
19
|
+
archive: ArtifactActionFlag;
|
|
20
|
+
overwrite: ArtifactActionFlag;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type ArtifactDownloadSelector =
|
|
24
|
+
| { kind: "name"; name: ArtifactValue }
|
|
25
|
+
| { kind: "pattern"; pattern: ArtifactValue }
|
|
26
|
+
| { kind: "all" }
|
|
27
|
+
| { kind: "artifact-ids"; artifactIds: ArtifactValue }
|
|
28
|
+
| {
|
|
29
|
+
kind: "unresolved";
|
|
30
|
+
reason: "name-with-artifact-ids";
|
|
31
|
+
name: ArtifactValue;
|
|
32
|
+
artifactIds: ArtifactValue;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type ArtifactDownloadSource =
|
|
36
|
+
| { kind: "current-run"; repository?: ArtifactValue; runId?: ArtifactValue }
|
|
37
|
+
| { kind: "external"; repository?: ArtifactValue; runId?: ArtifactValue }
|
|
38
|
+
| { kind: "dynamic"; repository?: ArtifactValue; runId?: ArtifactValue };
|
|
39
|
+
|
|
40
|
+
export interface ArtifactDownloadDeclaration {
|
|
41
|
+
kind: "download";
|
|
42
|
+
selector: ArtifactDownloadSelector;
|
|
43
|
+
source: ArtifactDownloadSource;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type ArtifactDeclaration = ArtifactUploadDeclaration | ArtifactDownloadDeclaration;
|
|
47
|
+
|
|
48
|
+
export interface ArtifactEdge {
|
|
49
|
+
kind: "artifact";
|
|
50
|
+
from: string;
|
|
51
|
+
to: string;
|
|
52
|
+
name: string;
|
|
53
|
+
producerStep: number;
|
|
54
|
+
consumerStep: number;
|
|
55
|
+
match: "exact" | "pattern" | "all" | "possible";
|
|
56
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// The JS-only `createWorkflowTopologyIndex()` query index type, split out
|
|
2
|
+
// of `workflow-topology-types.d.ts` to stay under the 200-line file limit.
|
|
3
|
+
|
|
4
|
+
import type { ArtifactEdge } from "./workflow-topology-artifact-types";
|
|
5
|
+
import type {
|
|
6
|
+
WorkflowJobNode,
|
|
7
|
+
WorkflowNode,
|
|
8
|
+
WorkflowRunEdge,
|
|
9
|
+
WorkflowTopology,
|
|
10
|
+
} from "./workflow-topology-types";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A frozen, sorted query index over a {@link WorkflowTopology}, built
|
|
14
|
+
* entirely in JS from `ciTopology()`'s output via
|
|
15
|
+
* {@link createWorkflowTopologyIndex}. Every array-returning method throws
|
|
16
|
+
* `Error("unknown workflow job: <id>")` / `Error("unknown workflow: <path>")`
|
|
17
|
+
* for an id not present in the topology.
|
|
18
|
+
*/
|
|
19
|
+
export interface WorkflowTopologyIndex {
|
|
20
|
+
readonly workflowsByPath: ReadonlyMap<string, Readonly<WorkflowNode>>;
|
|
21
|
+
readonly jobsById: ReadonlyMap<string, Readonly<WorkflowJobNode>>;
|
|
22
|
+
directUpstreamJobIds(jobId: string): readonly string[];
|
|
23
|
+
transitiveUpstreamJobIds(jobId: string): readonly string[];
|
|
24
|
+
directDownstreamJobIds(jobId: string): readonly string[];
|
|
25
|
+
transitiveDownstreamJobIds(jobId: string): readonly string[];
|
|
26
|
+
directCallerJobIds(workflowPath: string): readonly string[];
|
|
27
|
+
directCallerWorkflowPaths(workflowPath: string): readonly string[];
|
|
28
|
+
transitiveCallerWorkflowPaths(workflowPath: string): readonly string[];
|
|
29
|
+
directCalleeWorkflowPaths(workflowPath: string): readonly string[];
|
|
30
|
+
transitiveCalleeWorkflowPaths(workflowPath: string): readonly string[];
|
|
31
|
+
incomingWorkflowRunEdges(workflowPath: string): readonly Readonly<WorkflowRunEdge>[];
|
|
32
|
+
outgoingWorkflowRunEdges(workflowPath: string): readonly Readonly<WorkflowRunEdge>[];
|
|
33
|
+
directWorkflowRunSourcePaths(workflowPath: string): readonly string[];
|
|
34
|
+
transitiveWorkflowRunSourcePaths(workflowPath: string): readonly string[];
|
|
35
|
+
directWorkflowRunSubscriberPaths(workflowPath: string): readonly string[];
|
|
36
|
+
transitiveWorkflowRunSubscriberPaths(workflowPath: string): readonly string[];
|
|
37
|
+
artifactProducersForConsumerJob(jobId: string): readonly Readonly<ArtifactEdge>[];
|
|
38
|
+
artifactConsumersForProducerJob(jobId: string): readonly Readonly<ArtifactEdge>[];
|
|
39
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// A pure-JS query index rebuilt from the `ciTopology()` JSON, ported from
|
|
4
|
+
// the original engine's `topology-index.mts` + `frozen-topology.mts`. This
|
|
5
|
+
// stays JS-only by design: it returns closures over frozen `Map`s, which
|
|
6
|
+
// doesn't cross the N-API boundary cleanly, and the native side already
|
|
7
|
+
// does the expensive parse/diagnose/resolve work — this index is just
|
|
8
|
+
// deterministic, sorted traversal over data it's handed.
|
|
9
|
+
|
|
10
|
+
function deepFrozenClone(value) {
|
|
11
|
+
return freezeRecursively(structuredClone(value));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function freezeRecursively(value) {
|
|
15
|
+
if (value === null || typeof value !== "object") return value;
|
|
16
|
+
for (const nested of Object.values(value)) freezeRecursively(nested);
|
|
17
|
+
return Object.freeze(value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// `Object.freeze(new Map())` does NOT stop `.set()`/`.delete()` from
|
|
21
|
+
// working (freeze only covers own enumerable properties, not a Map's
|
|
22
|
+
// internal slots) — this wrapper is what actually makes `workflowsByPath`
|
|
23
|
+
// / `jobsById` read-only.
|
|
24
|
+
class FrozenReadonlyMap {
|
|
25
|
+
#source;
|
|
26
|
+
|
|
27
|
+
constructor(source) {
|
|
28
|
+
this.#source = source;
|
|
29
|
+
Object.freeze(this);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get size() {
|
|
33
|
+
return this.#source.size;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
entries() {
|
|
37
|
+
return this.#source.entries();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
forEach(callback, thisArg) {
|
|
41
|
+
for (const [key, value] of this.#source) callback.call(thisArg, value, key, this);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get(key) {
|
|
45
|
+
return this.#source.get(key);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
has(key) {
|
|
49
|
+
return this.#source.has(key);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
keys() {
|
|
53
|
+
return this.#source.keys();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
values() {
|
|
57
|
+
return this.#source.values();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
[Symbol.iterator]() {
|
|
61
|
+
return this.entries();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get [Symbol.toStringTag]() {
|
|
65
|
+
return "Map";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function adjacencyMap(ids) {
|
|
70
|
+
return new Map([...ids].map((id) => [id, new Set()]));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function artifactEdgeMap(ids) {
|
|
74
|
+
return new Map([...ids].map((id) => [id, []]));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function assertKnown(values, id, label) {
|
|
78
|
+
if (!values.has(id)) throw new Error(`unknown ${label}: ${id}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function query(adjacency, source, transitive) {
|
|
82
|
+
const direct = adjacency.get(source);
|
|
83
|
+
if (!transitive) return Object.freeze([...direct].sort());
|
|
84
|
+
const visited = new Set();
|
|
85
|
+
const pending = [...direct];
|
|
86
|
+
while (pending.length > 0) {
|
|
87
|
+
const current = pending.pop();
|
|
88
|
+
if (current === source || visited.has(current)) continue;
|
|
89
|
+
visited.add(current);
|
|
90
|
+
for (const neighbor of adjacency.get(current)) {
|
|
91
|
+
if (neighbor !== source && !visited.has(neighbor)) pending.push(neighbor);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return Object.freeze([...visited].sort());
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function workflowPathFromId(id) {
|
|
98
|
+
const index = id.indexOf("#");
|
|
99
|
+
return index === -1 ? id : id.slice(0, index);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function artifactEdgeKey(edge) {
|
|
103
|
+
return [edge.name, edge.from, edge.to, edge.producerStep, edge.consumerStep, edge.match].join(
|
|
104
|
+
"\0",
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function workflowRunEdgeKey(edge) {
|
|
109
|
+
const metadata = {
|
|
110
|
+
types: edge.types,
|
|
111
|
+
branches: edge.branches,
|
|
112
|
+
branchesIgnore: edge.branchesIgnore,
|
|
113
|
+
};
|
|
114
|
+
return [edge.from, edge.to, JSON.stringify(metadata)].join("\0");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Builds a frozen, sorted query index over a `WorkflowTopology` (the
|
|
119
|
+
* parsed object returned by `ciTopology()`, or `JSON.parse`d from
|
|
120
|
+
* `no-mistakes ci topology --format json`).
|
|
121
|
+
*/
|
|
122
|
+
function createWorkflowTopologyIndex(topology) {
|
|
123
|
+
const workflowsByPath = new Map(
|
|
124
|
+
topology.workflows.map((workflow) => [workflow.path, deepFrozenClone(workflow)]),
|
|
125
|
+
);
|
|
126
|
+
const jobsById = new Map(topology.jobs.map((job) => [job.id, deepFrozenClone(job)]));
|
|
127
|
+
const upstreamJobs = adjacencyMap(jobsById.keys());
|
|
128
|
+
const downstreamJobs = adjacencyMap(jobsById.keys());
|
|
129
|
+
const callerJobs = adjacencyMap(workflowsByPath.keys());
|
|
130
|
+
const callers = adjacencyMap(workflowsByPath.keys());
|
|
131
|
+
const callees = adjacencyMap(workflowsByPath.keys());
|
|
132
|
+
const workflowRunSources = adjacencyMap(workflowsByPath.keys());
|
|
133
|
+
const workflowRunSubscribers = adjacencyMap(workflowsByPath.keys());
|
|
134
|
+
const artifactProducers = artifactEdgeMap(jobsById.keys());
|
|
135
|
+
const artifactConsumers = artifactEdgeMap(jobsById.keys());
|
|
136
|
+
const incomingWorkflowRunEdges = new Map([...workflowsByPath.keys()].map((path) => [path, []]));
|
|
137
|
+
const outgoingWorkflowRunEdges = new Map([...workflowsByPath.keys()].map((path) => [path, []]));
|
|
138
|
+
|
|
139
|
+
for (const edge of topology.edges) {
|
|
140
|
+
if (edge.kind === "needs") {
|
|
141
|
+
if (!jobsById.has(edge.from) || !jobsById.has(edge.to)) continue;
|
|
142
|
+
upstreamJobs.get(edge.to).add(edge.from);
|
|
143
|
+
downstreamJobs.get(edge.from).add(edge.to);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (edge.kind === "workflow-run") {
|
|
147
|
+
if (!workflowsByPath.has(edge.from) || !workflowsByPath.has(edge.to)) continue;
|
|
148
|
+
const edgeSnapshot = deepFrozenClone(edge);
|
|
149
|
+
workflowRunSources.get(edge.to).add(edge.from);
|
|
150
|
+
workflowRunSubscribers.get(edge.from).add(edge.to);
|
|
151
|
+
incomingWorkflowRunEdges.get(edge.to).push(edgeSnapshot);
|
|
152
|
+
outgoingWorkflowRunEdges.get(edge.from).push(edgeSnapshot);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (edge.kind === "artifact") {
|
|
156
|
+
if (!jobsById.has(edge.from) || !jobsById.has(edge.to)) continue;
|
|
157
|
+
const edgeSnapshot = deepFrozenClone(edge);
|
|
158
|
+
artifactProducers.get(edge.to).push(edgeSnapshot);
|
|
159
|
+
artifactConsumers.get(edge.from).push(edgeSnapshot);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
// edge.kind === "calls"
|
|
163
|
+
if (!edge.local || !edge.to || !jobsById.has(edge.from) || !workflowsByPath.has(edge.to))
|
|
164
|
+
continue;
|
|
165
|
+
const callerPath = workflowPathFromId(edge.from);
|
|
166
|
+
if (!workflowsByPath.has(callerPath)) continue;
|
|
167
|
+
callerJobs.get(edge.to).add(edge.from);
|
|
168
|
+
callers.get(edge.to).add(callerPath);
|
|
169
|
+
callees.get(callerPath).add(edge.to);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const jobQuery = (adjacency, transitive) => (jobId) => {
|
|
173
|
+
assertKnown(jobsById, jobId, "workflow job");
|
|
174
|
+
return query(adjacency, jobId, transitive);
|
|
175
|
+
};
|
|
176
|
+
const workflowQuery = (adjacency, transitive) => (workflowPath) => {
|
|
177
|
+
assertKnown(workflowsByPath, workflowPath, "workflow");
|
|
178
|
+
return query(adjacency, workflowPath, transitive);
|
|
179
|
+
};
|
|
180
|
+
const workflowRunEdgeQuery = (edgesByPath) => (workflowPath) => {
|
|
181
|
+
assertKnown(workflowsByPath, workflowPath, "workflow");
|
|
182
|
+
return Object.freeze(
|
|
183
|
+
[...edgesByPath.get(workflowPath)].sort((left, right) =>
|
|
184
|
+
workflowRunEdgeKey(left).localeCompare(workflowRunEdgeKey(right)),
|
|
185
|
+
),
|
|
186
|
+
);
|
|
187
|
+
};
|
|
188
|
+
const artifactEdgeQuery = (edgesByJob) => (jobId) => {
|
|
189
|
+
assertKnown(jobsById, jobId, "workflow job");
|
|
190
|
+
return Object.freeze(
|
|
191
|
+
[...edgesByJob.get(jobId)].sort((left, right) =>
|
|
192
|
+
artifactEdgeKey(left).localeCompare(artifactEdgeKey(right)),
|
|
193
|
+
),
|
|
194
|
+
);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
return Object.freeze({
|
|
198
|
+
workflowsByPath: new FrozenReadonlyMap(workflowsByPath),
|
|
199
|
+
jobsById: new FrozenReadonlyMap(jobsById),
|
|
200
|
+
directUpstreamJobIds: jobQuery(upstreamJobs, false),
|
|
201
|
+
transitiveUpstreamJobIds: jobQuery(upstreamJobs, true),
|
|
202
|
+
directDownstreamJobIds: jobQuery(downstreamJobs, false),
|
|
203
|
+
transitiveDownstreamJobIds: jobQuery(downstreamJobs, true),
|
|
204
|
+
directCallerJobIds: workflowQuery(callerJobs, false),
|
|
205
|
+
directCallerWorkflowPaths: workflowQuery(callers, false),
|
|
206
|
+
transitiveCallerWorkflowPaths: workflowQuery(callers, true),
|
|
207
|
+
directCalleeWorkflowPaths: workflowQuery(callees, false),
|
|
208
|
+
transitiveCalleeWorkflowPaths: workflowQuery(callees, true),
|
|
209
|
+
incomingWorkflowRunEdges: workflowRunEdgeQuery(incomingWorkflowRunEdges),
|
|
210
|
+
outgoingWorkflowRunEdges: workflowRunEdgeQuery(outgoingWorkflowRunEdges),
|
|
211
|
+
directWorkflowRunSourcePaths: workflowQuery(workflowRunSources, false),
|
|
212
|
+
transitiveWorkflowRunSourcePaths: workflowQuery(workflowRunSources, true),
|
|
213
|
+
directWorkflowRunSubscriberPaths: workflowQuery(workflowRunSubscribers, false),
|
|
214
|
+
transitiveWorkflowRunSubscriberPaths: workflowQuery(workflowRunSubscribers, true),
|
|
215
|
+
artifactProducersForConsumerJob: artifactEdgeQuery(artifactProducers),
|
|
216
|
+
artifactConsumersForProducerJob: artifactEdgeQuery(artifactConsumers),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
module.exports = { createWorkflowTopologyIndex };
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// Types for `ciTopology()` / `no-mistakes ci topology` and the JS-only
|
|
2
|
+
// `createWorkflowTopologyIndex()` query index built from its output.
|
|
3
|
+
//
|
|
4
|
+
// This mirrors the schema-v1 JSON contract exactly (field names, casing) —
|
|
5
|
+
// see `docs/node-api.md` for the stability guarantees (field order and
|
|
6
|
+
// array/diagnostic sort order are part of the contract).
|
|
7
|
+
|
|
8
|
+
import type { ArtifactDeclaration, ArtifactEdge } from "./workflow-topology-artifact-types";
|
|
9
|
+
|
|
10
|
+
export * from "./workflow-topology-artifact-types";
|
|
11
|
+
export * from "./workflow-topology-index-types";
|
|
12
|
+
|
|
13
|
+
export interface CiTopologyOptions {
|
|
14
|
+
/** Project root. Defaults to the current working directory. */
|
|
15
|
+
root?: string;
|
|
16
|
+
/** Path to the no-mistakes config file (e.g. .no-mistakes.yml). Auto-discovered in root if omitted. */
|
|
17
|
+
config?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Restrict output to these workflow(s) (basename or a path inside
|
|
20
|
+
* `.github/workflows`) plus their transitive local reusable-workflow
|
|
21
|
+
* callees. Omit or pass `[]` for every workflow.
|
|
22
|
+
*/
|
|
23
|
+
workflows?: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type JsonScalar = boolean | number | string;
|
|
27
|
+
export type JsonValue =
|
|
28
|
+
| null
|
|
29
|
+
| boolean
|
|
30
|
+
| number
|
|
31
|
+
| string
|
|
32
|
+
| JsonValue[]
|
|
33
|
+
| { [key: string]: JsonValue };
|
|
34
|
+
|
|
35
|
+
export type ConcurrencyValue = boolean | string;
|
|
36
|
+
|
|
37
|
+
export interface WorkflowConcurrency {
|
|
38
|
+
raw: { group: string; cancelInProgress?: ConcurrencyValue; queue?: string };
|
|
39
|
+
effective: { group: string; cancelInProgress: ConcurrencyValue; queue: string };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface WorkflowTrigger {
|
|
43
|
+
event: string;
|
|
44
|
+
config?: JsonValue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface WorkflowCallInput {
|
|
48
|
+
type?: "boolean" | "number" | "string";
|
|
49
|
+
required: boolean;
|
|
50
|
+
default?: JsonScalar;
|
|
51
|
+
description?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface WorkflowCallSecret {
|
|
55
|
+
required: boolean;
|
|
56
|
+
description?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface WorkflowCallOutput {
|
|
60
|
+
value?: string;
|
|
61
|
+
description?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface WorkflowCallContract {
|
|
65
|
+
inputs: Record<string, WorkflowCallInput>;
|
|
66
|
+
secrets: Record<string, WorkflowCallSecret>;
|
|
67
|
+
outputs: Record<string, WorkflowCallOutput>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type WorkflowNode = {
|
|
71
|
+
id: string;
|
|
72
|
+
path: string;
|
|
73
|
+
name: string;
|
|
74
|
+
triggers: WorkflowTrigger[];
|
|
75
|
+
jobIds: string[];
|
|
76
|
+
concurrency?: WorkflowConcurrency;
|
|
77
|
+
} & (
|
|
78
|
+
| { callable: true; workflowCall: WorkflowCallContract }
|
|
79
|
+
| { callable: false; workflowCall?: undefined }
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
export interface WorkflowStep {
|
|
83
|
+
index: number;
|
|
84
|
+
kind: "action" | "run" | "other";
|
|
85
|
+
id?: string;
|
|
86
|
+
name?: string;
|
|
87
|
+
condition?: string;
|
|
88
|
+
uses?: string;
|
|
89
|
+
/** Present when this step is an `actions/{upload,download}-artifact` action. */
|
|
90
|
+
artifact?: ArtifactDeclaration;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface WorkflowJobNode {
|
|
94
|
+
id: string;
|
|
95
|
+
workflowId: string;
|
|
96
|
+
key: string;
|
|
97
|
+
kind: "job" | "matrix-template";
|
|
98
|
+
name?: string;
|
|
99
|
+
condition?: string;
|
|
100
|
+
matrix?: JsonValue;
|
|
101
|
+
concurrency?: WorkflowConcurrency;
|
|
102
|
+
steps: WorkflowStep[];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface NeedsEdge {
|
|
106
|
+
kind: "needs";
|
|
107
|
+
from: string;
|
|
108
|
+
to: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface WorkflowCallEdge {
|
|
112
|
+
kind: "calls";
|
|
113
|
+
from: string;
|
|
114
|
+
target: string;
|
|
115
|
+
local: boolean;
|
|
116
|
+
bindings: {
|
|
117
|
+
inputs: Record<string, JsonScalar>;
|
|
118
|
+
secrets: { mode: "inherit" } | { mode: "explicit"; values: Record<string, JsonScalar> };
|
|
119
|
+
};
|
|
120
|
+
/** Present only for local (`./`) calls. */
|
|
121
|
+
to?: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface WorkflowRunEdge {
|
|
125
|
+
kind: "workflow-run";
|
|
126
|
+
from: string;
|
|
127
|
+
to: string;
|
|
128
|
+
types?: string[];
|
|
129
|
+
branches?: string[];
|
|
130
|
+
branchesIgnore?: string[];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export type WorkflowTopologyEdge = NeedsEdge | WorkflowCallEdge | WorkflowRunEdge | ArtifactEdge;
|
|
134
|
+
|
|
135
|
+
export type WorkflowTopologyDiagnosticCode =
|
|
136
|
+
| "malformed-workflow"
|
|
137
|
+
| "missing-needs-dependency"
|
|
138
|
+
| "job-dependency-cycle"
|
|
139
|
+
| "duplicate-step-id"
|
|
140
|
+
| "unknown-step-reference"
|
|
141
|
+
| "non-prior-step-reference"
|
|
142
|
+
| "duplicate-workflow-name"
|
|
143
|
+
| "missing-local-workflow"
|
|
144
|
+
| "non-callable-workflow"
|
|
145
|
+
| "workflow-call-cycle"
|
|
146
|
+
| "missing-workflow-run-source"
|
|
147
|
+
| "ambiguous-workflow-run-source"
|
|
148
|
+
| "workflow-run-cycle"
|
|
149
|
+
| "workflow-run-chain-limit"
|
|
150
|
+
| "unknown-workflow-filter"
|
|
151
|
+
| "invalid-workflow-filter"
|
|
152
|
+
| "missing-workflow-call-input"
|
|
153
|
+
| "unknown-workflow-call-input"
|
|
154
|
+
| "workflow-call-input-type-mismatch"
|
|
155
|
+
| "missing-workflow-call-secret"
|
|
156
|
+
| "unknown-workflow-call-secret"
|
|
157
|
+
| "unknown-workflow-call-output"
|
|
158
|
+
| "missing-artifact-producer"
|
|
159
|
+
| "ambiguous-artifact-producer"
|
|
160
|
+
| "artifact-resolution-limit";
|
|
161
|
+
|
|
162
|
+
export interface WorkflowTopologyDiagnostic {
|
|
163
|
+
severity: "error";
|
|
164
|
+
code: WorkflowTopologyDiagnosticCode;
|
|
165
|
+
message: string;
|
|
166
|
+
workflowPath: string;
|
|
167
|
+
jobId?: string;
|
|
168
|
+
stepIndex?: number;
|
|
169
|
+
callJobId?: string;
|
|
170
|
+
calleeWorkflowPath?: string;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface WorkflowTopology {
|
|
174
|
+
schemaVersion: 1;
|
|
175
|
+
workflows: WorkflowNode[];
|
|
176
|
+
jobs: WorkflowJobNode[];
|
|
177
|
+
edges: WorkflowTopologyEdge[];
|
|
178
|
+
diagnostics: WorkflowTopologyDiagnostic[];
|
|
179
|
+
}
|