effect-analyzer 0.1.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.
@@ -0,0 +1,87 @@
1
+ import { readFileSync } from 'fs';
2
+ import { basename } from 'path';
3
+ import type { ScanReport, ScanRow } from './effect-scan-utils';
4
+
5
+ interface RowDelta {
6
+ readonly file: string;
7
+ readonly before?: string;
8
+ readonly after?: string;
9
+ }
10
+
11
+ function readJson(path: string): ScanReport {
12
+ return JSON.parse(readFileSync(path, 'utf8')) as ScanReport;
13
+ }
14
+
15
+ function rowMap(rows: readonly ScanRow[]): Map<string, ScanRow> {
16
+ return new Map(rows.map((r) => [r.file, r]));
17
+ }
18
+
19
+ function classifyDelta(before?: ScanRow, after?: ScanRow): 'added' | 'removed' | 'changed' | 'same' {
20
+ if (!before && after) return 'added';
21
+ if (before && !after) return 'removed';
22
+ if (!before || !after) return 'same';
23
+ return before.tag === after.tag ? 'same' : 'changed';
24
+ }
25
+
26
+ function main(): void {
27
+ const [beforePath, afterPath] = process.argv.slice(2);
28
+ if (!beforePath || !afterPath) {
29
+ console.error('usage: pnpm exec tsx scripts/diff-effect-scans.ts <before.json> <after.json>');
30
+ process.exit(1);
31
+ }
32
+
33
+ const before = readJson(beforePath);
34
+ const after = readJson(afterPath);
35
+ const beforeMap = rowMap(before.rows);
36
+ const afterMap = rowMap(after.rows);
37
+ const files = Array.from(new Set([...beforeMap.keys(), ...afterMap.keys()])).sort();
38
+
39
+ const added: RowDelta[] = [];
40
+ const removed: RowDelta[] = [];
41
+ const changed: RowDelta[] = [];
42
+ let improved = 0;
43
+ let regressed = 0;
44
+
45
+ for (const file of files) {
46
+ const b = beforeMap.get(file);
47
+ const a = afterMap.get(file);
48
+ const kind = classifyDelta(b, a);
49
+ if (kind === 'same') continue;
50
+ const delta: RowDelta = { file, before: b?.tag, after: a?.tag };
51
+ if (kind === 'added') added.push(delta);
52
+ else if (kind === 'removed') removed.push(delta);
53
+ else changed.push(delta);
54
+
55
+ if (b && a) {
56
+ if (b.tag.startsWith('FAIL:') && a.tag.startsWith('OK:')) improved++;
57
+ if (b.tag.startsWith('OK:') && a.tag.startsWith('FAIL:')) regressed++;
58
+ }
59
+ }
60
+
61
+ console.log(`before: ${basename(beforePath)} total=${before.summary.total} ok=${before.summary.ok} fail=${before.summary.fail} throw=${before.summary.throwCount}`);
62
+ console.log(`after: ${basename(afterPath)} total=${after.summary.total} ok=${after.summary.ok} fail=${after.summary.fail} throw=${after.summary.throwCount}`);
63
+ console.log(`changed=${changed.length} added=${added.length} removed=${removed.length} improved=${improved} regressed=${regressed}`);
64
+
65
+ const limit = 40;
66
+ if (changed.length > 0) {
67
+ console.log('changed rows (sample):');
68
+ for (const d of changed.slice(0, limit)) {
69
+ console.log(`- ${basename(d.file)}: ${d.before} -> ${d.after}`);
70
+ }
71
+ }
72
+ if (added.length > 0) {
73
+ console.log('added rows (sample):');
74
+ for (const d of added.slice(0, 20)) {
75
+ console.log(`- ${basename(d.file)}: ${d.after}`);
76
+ }
77
+ }
78
+ if (removed.length > 0) {
79
+ console.log('removed rows (sample):');
80
+ for (const d of removed.slice(0, 20)) {
81
+ console.log(`- ${basename(d.file)}: ${d.before}`);
82
+ }
83
+ }
84
+ }
85
+
86
+ main();
87
+
@@ -0,0 +1,275 @@
1
+ import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'fs';
2
+ import { basename, join, resolve } from 'path';
3
+ import { Effect } from 'effect';
4
+ import { analyze } from '../src/analyze';
5
+
6
+ export interface ScanRow {
7
+ readonly file: string;
8
+ readonly tag: string;
9
+ readonly info?: string;
10
+ readonly contains_effect_imports?: boolean;
11
+ readonly contains_effect_calls?: boolean;
12
+ readonly contains_top_level_program_candidate?: boolean;
13
+ readonly zeroProgramsReason?: ZeroProgramsReason;
14
+ }
15
+
16
+ export interface ScanSummary {
17
+ readonly total: number;
18
+ readonly ok: number;
19
+ readonly fail: number;
20
+ readonly throwCount: number;
21
+ }
22
+
23
+ export type ZeroProgramsReason =
24
+ | 'reexport_only_module'
25
+ | 'type_only_or_utility_module'
26
+ | 'state_adt_module'
27
+ | 'helper_functions_without_effect_program_roots'
28
+ | 'effect_family_type_references_only'
29
+ | 'unsupported_pattern'
30
+ | 'unknown';
31
+
32
+ export interface ScanReport {
33
+ readonly effectRepoPath: string;
34
+ readonly summary: ScanSummary;
35
+ readonly rows: readonly ScanRow[];
36
+ readonly srcRoot?: string;
37
+ readonly internalsRoot?: string;
38
+ readonly filteredByEffectFamilyCalls?: boolean;
39
+ }
40
+
41
+ export const DEFAULT_EFFECT_REPO_PATH = '/Users/jagreehal/dev/js/awaitly/__temp/effect';
42
+
43
+ export function getEffectRepoPath(): string {
44
+ return resolve(process.env.EFFECT_REPO_PATH ?? DEFAULT_EFFECT_REPO_PATH);
45
+ }
46
+
47
+ export function getEffectSrcRoot(effectRepoPath: string): string {
48
+ return join(effectRepoPath, 'packages', 'effect', 'src');
49
+ }
50
+
51
+ export function getEffectInternalsRoot(effectRepoPath: string): string {
52
+ return join(getEffectSrcRoot(effectRepoPath), 'internal');
53
+ }
54
+
55
+ export function listTopLevelTsFiles(dir: string): readonly string[] {
56
+ return readdirSync(dir)
57
+ .filter((name) => name.endsWith('.ts'))
58
+ .map((name) => join(dir, name))
59
+ .sort();
60
+ }
61
+
62
+ export function listTsFilesRecursive(dir: string): readonly string[] {
63
+ const out: string[] = [];
64
+ const visit = (current: string): void => {
65
+ for (const entry of readdirSync(current)) {
66
+ const full = join(current, entry);
67
+ const st = statSync(full);
68
+ if (st.isDirectory()) {
69
+ visit(full);
70
+ } else if (entry.endsWith('.ts')) {
71
+ out.push(full);
72
+ }
73
+ }
74
+ };
75
+ visit(dir);
76
+ out.sort();
77
+ return out;
78
+ }
79
+
80
+ export function containsEffectFamilyCalls(source: string): boolean {
81
+ return /\b(Effect|STM|Layer|Schedule|Stream|Channel|Sink|Cause|Exit|Fiber|Runtime)\./.test(source);
82
+ }
83
+
84
+ export function containsEffectFamilyImports(source: string): boolean {
85
+ return (
86
+ /\bfrom\s+["'](?:effect(?:["'/])|@effect\/)/.test(source) ||
87
+ /\bfrom\s+["'][^"']*(?:^|\/)(?:Effect|STM|Layer|Schedule|Stream|Channel|Sink|Cause|Exit|Fiber|Runtime)(?:\.[jt]sx?)?["']/.test(source)
88
+ );
89
+ }
90
+
91
+ export function containsEffectFamilyInvocations(source: string): boolean {
92
+ return /\b(Effect|STM|Layer|Schedule|Stream|Channel|Sink|Cause|Exit|Fiber|Runtime)\.[A-Za-z_][A-Za-z0-9_]*\s*\(/.test(source);
93
+ }
94
+
95
+ export function containsProgramRootFamilyInvocations(source: string): boolean {
96
+ return /\b(Effect|STM|Layer|Schedule|Stream|Channel|Sink)\.[A-Za-z_][A-Za-z0-9_]*\s*\(/.test(source);
97
+ }
98
+
99
+ function classifyZeroProgramsReason(source: string): ZeroProgramsReason {
100
+ const hasReexportFrom =
101
+ /export\s+\*\s+from\s+["'][^"']+["']/.test(source) ||
102
+ /export\s*\{[\s\S]*?\}\s*from\s*["'][^"']+["']/.test(source);
103
+ const hasReexportOnly =
104
+ hasReexportFrom &&
105
+ !/export\s+const\s+/.test(source) &&
106
+ !/export\s+function\s+/.test(source) &&
107
+ !/export\s+class\s+/.test(source) &&
108
+ !/export\s+(interface|type)\s+/.test(source);
109
+ if (hasReexportOnly) return 'reexport_only_module';
110
+
111
+ const hasExportConst = /export\s+const\s+/.test(source);
112
+ const hasExportFn = /export\s+function\s+/.test(source);
113
+ const hasExportClass = /export\s+class\s+/.test(source);
114
+ const hasExportTypeLike = /export\s+(interface|type)\s+/.test(source);
115
+ const hasEffectCalls = containsEffectFamilyCalls(source);
116
+ const hasEffectInvocations = containsEffectFamilyInvocations(source);
117
+ const hasProgramRootInvocations = containsProgramRootFamilyInvocations(source);
118
+ const hasClassOrInterface = /\b(class|interface)\s+[A-Za-z_]/.test(source);
119
+ const hasPrototypeOrMethods =
120
+ /\b(readonly\s+[A-Za-z_][A-Za-z0-9_]*\s*:\s*.*=>)|\b[A-Za-z_][A-Za-z0-9_]*\s*\([^)]*\)\s*\{/.test(source);
121
+
122
+ if (!hasEffectCalls) {
123
+ if (hasExportTypeLike || /import\s+type\s+/.test(source)) {
124
+ return 'type_only_or_utility_module';
125
+ }
126
+ if (hasExportConst || hasExportFn || hasExportClass) {
127
+ return 'helper_functions_without_effect_program_roots';
128
+ }
129
+ return 'unknown';
130
+ }
131
+
132
+ if (!hasEffectInvocations) {
133
+ if (hasClassOrInterface || hasExportTypeLike) {
134
+ return 'state_adt_module';
135
+ }
136
+ return 'effect_family_type_references_only';
137
+ }
138
+
139
+ if (!hasProgramRootInvocations) {
140
+ if (hasClassOrInterface || hasExportTypeLike) {
141
+ return 'state_adt_module';
142
+ }
143
+ return 'helper_functions_without_effect_program_roots';
144
+ }
145
+
146
+ if (hasExportTypeLike && !hasExportConst && !hasExportFn && !hasExportClass) {
147
+ return 'state_adt_module';
148
+ }
149
+
150
+ if (hasClassOrInterface || hasPrototypeOrMethods) {
151
+ return 'unsupported_pattern';
152
+ }
153
+
154
+ if (hasExportConst || hasExportFn || hasExportClass) {
155
+ return 'unsupported_pattern';
156
+ }
157
+
158
+ return 'unknown';
159
+ }
160
+
161
+ export async function analyzeFileToRow(
162
+ filePath: string,
163
+ knownEffectInternalsRoot: string,
164
+ maxNames = 8,
165
+ ): Promise<ScanRow> {
166
+ const source = readFileUtf8(filePath);
167
+ const fileSignals = {
168
+ contains_effect_imports: containsEffectFamilyImports(source),
169
+ contains_effect_calls: containsEffectFamilyInvocations(source),
170
+ contains_top_level_program_candidate: containsProgramRootFamilyInvocations(source),
171
+ } as const;
172
+
173
+ try {
174
+ const res = await Effect.runPromiseExit(
175
+ analyze(filePath, { knownEffectInternalsRoot }).all(),
176
+ );
177
+
178
+ if (res._tag === 'Failure') {
179
+ const cause = res.cause as unknown as {
180
+ _tag?: string;
181
+ error?: { code?: string };
182
+ failure?: { code?: string };
183
+ };
184
+ const code =
185
+ cause?._tag === 'Fail'
186
+ ? (cause.error?.code ?? cause.failure?.code ?? 'unknown')
187
+ : (cause?._tag ?? 'unknown');
188
+ const tag = `FAIL:${String(code)}`;
189
+ if (tag === 'FAIL:NO_EFFECTS_FOUND') {
190
+ return {
191
+ file: filePath,
192
+ tag,
193
+ ...fileSignals,
194
+ zeroProgramsReason: classifyZeroProgramsReason(source),
195
+ };
196
+ }
197
+ return { file: filePath, tag, ...fileSignals };
198
+ }
199
+
200
+ const names = res.value.map((ir) => ir.root.programName);
201
+ return {
202
+ file: filePath,
203
+ tag: `OK:${res.value.length}`,
204
+ info: names.slice(0, maxNames).join(','),
205
+ ...fileSignals,
206
+ };
207
+ } catch (error) {
208
+ return {
209
+ file: filePath,
210
+ tag: 'THROW',
211
+ info: String(error),
212
+ ...fileSignals,
213
+ };
214
+ }
215
+ }
216
+
217
+ export function summarizeRows(rows: readonly ScanRow[]): ScanSummary {
218
+ let ok = 0;
219
+ let fail = 0;
220
+ let throwCount = 0;
221
+ for (const row of rows) {
222
+ if (row.tag.startsWith('OK:')) ok++;
223
+ else if (row.tag.startsWith('FAIL:')) fail++;
224
+ else if (row.tag === 'THROW') throwCount++;
225
+ }
226
+ return {
227
+ total: rows.length,
228
+ ok,
229
+ fail,
230
+ throwCount,
231
+ };
232
+ }
233
+
234
+ export function printHumanSummary(label: string, rows: readonly ScanRow[]): void {
235
+ const summary = summarizeRows(rows);
236
+ console.log(`${label}: total=${summary.total} ok=${summary.ok} fail=${summary.fail} throw=${summary.throwCount}`);
237
+ const reasonCounts = new Map<string, number>();
238
+ for (const row of rows) {
239
+ if (!row.zeroProgramsReason) continue;
240
+ reasonCounts.set(row.zeroProgramsReason, (reasonCounts.get(row.zeroProgramsReason) ?? 0) + 1);
241
+ }
242
+ if (reasonCounts.size > 0) {
243
+ const ordered = Array.from(reasonCounts.entries()).sort((a, b) => b[1] - a[1]);
244
+ console.log(`${label} zeroProgramsReason counts:`);
245
+ for (const [reason, count] of ordered) {
246
+ console.log(`- ${reason}: ${count}`);
247
+ }
248
+ }
249
+ const examples = rows.filter((r) => !r.tag.startsWith('OK:')).slice(0, 15);
250
+ if (examples.length > 0) {
251
+ console.log(`${label} failures (sample):`);
252
+ for (const row of examples) {
253
+ const reason = row.zeroProgramsReason ? ` (${row.zeroProgramsReason})` : '';
254
+ console.log(`- ${basename(row.file)} ${row.tag}${reason}`);
255
+ }
256
+ }
257
+ }
258
+
259
+ export function readFileUtf8(path: string): string {
260
+ return readFileSync(path, 'utf8');
261
+ }
262
+
263
+ export function writeJsonFile(path: string, value: unknown): void {
264
+ const resolved = resolve(path);
265
+ mkdirSync(resolve(resolved, '..'), { recursive: true });
266
+ writeFileSync(resolved, JSON.stringify(value, null, 2));
267
+ }
268
+
269
+ export function getArgValue(flag: string): string | undefined {
270
+ const idx = process.argv.indexOf(flag);
271
+ if (idx === -1) return undefined;
272
+ const next = process.argv[idx + 1];
273
+ if (!next || next.startsWith('--')) return undefined;
274
+ return next;
275
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * ESLint rule: ban inline type assertions like `x as import('module').Type`.
3
+ * Use a named type import at the top instead.
4
+ */
5
+ export default {
6
+ meta: {
7
+ type: 'suggestion',
8
+ docs: {
9
+ description:
10
+ "Disallow inline type assertions using import() (e.g. `x as import('ts-morph').Node`). Use a named type import at the top of the file instead.",
11
+ },
12
+ schema: [],
13
+ messages: {
14
+ useNamedImport:
15
+ "Use a named type import instead of inline 'as import(...)'. Example: import type { TypeName } from 'module'; ... x as TypeName",
16
+ },
17
+ },
18
+ create(context) {
19
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
20
+ return {
21
+ Program(node) {
22
+ const text = sourceCode.getText(node);
23
+ const pattern = /\s+as\s+import\s*\(/g;
24
+ let match;
25
+ while ((match = pattern.exec(text)) !== null) {
26
+ const index = match.index;
27
+ const line = text.slice(0, index).split('\n').length;
28
+ const lineStart = text.lastIndexOf('\n', index);
29
+ const column = index - lineStart - 1;
30
+ context.report({
31
+ node,
32
+ loc: { start: { line, column }, end: { line, column: column + match[0].length } },
33
+ messageId: 'useNamedImport',
34
+ });
35
+ }
36
+ },
37
+ };
38
+ },
39
+ };
@@ -0,0 +1,52 @@
1
+ import { existsSync, mkdirSync, rmSync } from 'fs';
2
+ import { dirname, resolve } from 'path';
3
+ import { execFileSync } from 'child_process';
4
+
5
+ const DEFAULT_REPO_URL = 'https://github.com/Effect-TS/effect.git';
6
+ const DEFAULT_DEST = resolve('.cache/effect-repo');
7
+ const DEFAULT_REF = 'main';
8
+
9
+ function getArgValue(flag: string): string | undefined {
10
+ const idx = process.argv.indexOf(flag);
11
+ if (idx === -1) return undefined;
12
+ const next = process.argv[idx + 1];
13
+ if (!next || next.startsWith('--')) return undefined;
14
+ return next;
15
+ }
16
+
17
+ function hasFlag(flag: string): boolean {
18
+ return process.argv.includes(flag);
19
+ }
20
+
21
+ function runGit(args: readonly string[]): void {
22
+ execFileSync('git', [...args], { stdio: 'inherit' });
23
+ }
24
+
25
+ function main(): void {
26
+ const repoUrl = getArgValue('--repo-url') ?? process.env.EFFECT_REPO_URL ?? DEFAULT_REPO_URL;
27
+ const dest = resolve(getArgValue('--dir') ?? process.env.EFFECT_REPO_PATH ?? DEFAULT_DEST);
28
+ const ref = getArgValue('--ref') ?? process.env.EFFECT_REPO_REF ?? DEFAULT_REF;
29
+ const force = hasFlag('--force');
30
+
31
+ if (force && existsSync(dest)) {
32
+ rmSync(dest, { recursive: true, force: true });
33
+ }
34
+
35
+ mkdirSync(dirname(dest), { recursive: true });
36
+
37
+ if (!existsSync(dest)) {
38
+ runGit(['clone', '--depth', '1', '--branch', ref, repoUrl, dest]);
39
+ } else if (existsSync(resolve(dest, '.git'))) {
40
+ runGit(['-C', dest, 'fetch', '--depth', '1', 'origin', ref]);
41
+ runGit(['-C', dest, 'checkout', '--force', 'FETCH_HEAD']);
42
+ } else {
43
+ throw new Error(`Destination exists and is not a git repo: ${dest}`);
44
+ }
45
+
46
+ // Print export hints so callers can reuse the fetched clone.
47
+ console.log(`EFFECT_REPO_PATH=${dest}`);
48
+ console.log(`EFFECT_AUDIT_DIR=${resolve(dest, 'packages')}`);
49
+ console.log(`KNOWN_EFFECT_INTERNALS_ROOT=${resolve(dest, 'packages/effect/src/internal')}`);
50
+ }
51
+
52
+ main();
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Runtime OpenAPI generator - runs OpenApi.fromApi on a user's HttpApi.
4
+ * Usage: npx tsx openapi-runtime-runner.mjs <entrypoint> [exportName] [--output file.json]
5
+ *
6
+ * Requires: @effect/platform, tsx (for .ts files)
7
+ * Run with: npx tsx openapi-runtime-runner.mjs ./src/api.ts TodoApi
8
+ *
9
+ * Must be run with cwd = project root (so @effect/platform resolves from project's node_modules).
10
+ */
11
+
12
+ import { resolve, join } from 'path';
13
+ import { pathToFileURL } from 'url';
14
+ import { createRequire } from 'module';
15
+ import { writeFileSync } from 'fs';
16
+
17
+ const args = process.argv.slice(2);
18
+ const outputIdx = args.indexOf('--output');
19
+ const outputFile = outputIdx >= 0 ? args[outputIdx + 1] : null;
20
+ if (outputIdx >= 0) args.splice(outputIdx, 2);
21
+ const [entrypoint, exportName = 'default'] = args;
22
+
23
+ if (!entrypoint) {
24
+ console.error('Usage: npx tsx openapi-runtime-runner.mjs <entrypoint> [exportName] [--output file.json]');
25
+ console.error('Example: npx tsx openapi-runtime-runner.mjs ./src/api.ts TodoApi -o openapi.json');
26
+ process.exit(1);
27
+ }
28
+
29
+ async function main() {
30
+ let OpenApi;
31
+ try {
32
+ const require = createRequire(join(process.cwd(), 'package.json'));
33
+ const platform = require('@effect/platform');
34
+ OpenApi = platform.OpenApi;
35
+ } catch (e) {
36
+ console.error('Error: @effect/platform is required for runtime OpenAPI generation.');
37
+ console.error('Install it: pnpm add @effect/platform');
38
+ if (e?.message) console.error('Details:', e.message);
39
+ process.exit(1);
40
+ }
41
+
42
+ const absPath = entrypoint.startsWith('/') || /^[A-Za-z]:/.test(entrypoint)
43
+ ? entrypoint
44
+ : resolve(process.cwd(), entrypoint);
45
+ const mod = await import(pathToFileURL(absPath).href);
46
+ const api = mod[exportName] ?? mod.default;
47
+ if (!api) {
48
+ console.error(`Error: No export "${exportName}" found in ${entrypoint}`);
49
+ process.exit(1);
50
+ }
51
+
52
+ const spec = OpenApi.fromApi(api);
53
+ const json = JSON.stringify(spec, null, 2);
54
+
55
+ if (outputFile) {
56
+ writeFileSync(outputFile, json, 'utf8');
57
+ console.error(`Wrote OpenAPI spec to ${outputFile}`);
58
+ } else {
59
+ console.log(json);
60
+ }
61
+ }
62
+
63
+ main().catch((e) => {
64
+ console.error(e);
65
+ process.exit(1);
66
+ });
@@ -0,0 +1,53 @@
1
+ import {
2
+ analyzeFileToRow,
3
+ containsEffectFamilyCalls,
4
+ getArgValue,
5
+ getEffectInternalsRoot,
6
+ getEffectRepoPath,
7
+ listTsFilesRecursive,
8
+ printHumanSummary,
9
+ readFileUtf8,
10
+ summarizeRows,
11
+ writeJsonFile,
12
+ } from './effect-scan-utils';
13
+
14
+ async function main(): Promise<void> {
15
+ const effectRepoPath = getEffectRepoPath();
16
+ const internalsRoot = getEffectInternalsRoot(effectRepoPath);
17
+ const allFiles = listTsFilesRecursive(internalsRoot);
18
+ const files = allFiles.filter((file) => containsEffectFamilyCalls(readFileUtf8(file)));
19
+
20
+ const rows = [];
21
+ for (const file of files) {
22
+ rows.push(await analyzeFileToRow(file, internalsRoot, 4));
23
+ }
24
+
25
+ const report = {
26
+ effectRepoPath,
27
+ internalsRoot,
28
+ filteredByEffectFamilyCalls: true,
29
+ summary: summarizeRows(rows),
30
+ rows,
31
+ } as const;
32
+
33
+ if (process.argv.includes('--human')) {
34
+ printHumanSummary('effect-internals', rows);
35
+ }
36
+
37
+ const jsonOut = getArgValue('--json-out');
38
+ if (jsonOut) {
39
+ writeJsonFile(jsonOut, report);
40
+ if (process.argv.includes('--human')) {
41
+ console.log(`wrote ${jsonOut}`);
42
+ return;
43
+ }
44
+ }
45
+
46
+ if (process.argv.includes('--human')) {
47
+ return;
48
+ }
49
+
50
+ console.log(JSON.stringify(report, null, 2));
51
+ }
52
+
53
+ void main();
@@ -0,0 +1,52 @@
1
+ import {
2
+ analyzeFileToRow,
3
+ getArgValue,
4
+ getEffectInternalsRoot,
5
+ getEffectRepoPath,
6
+ getEffectSrcRoot,
7
+ listTopLevelTsFiles,
8
+ printHumanSummary,
9
+ summarizeRows,
10
+ writeJsonFile,
11
+ } from './effect-scan-utils';
12
+
13
+ async function main(): Promise<void> {
14
+ const effectRepoPath = getEffectRepoPath();
15
+ const srcRoot = getEffectSrcRoot(effectRepoPath);
16
+ const internalsRoot = getEffectInternalsRoot(effectRepoPath);
17
+ const files = listTopLevelTsFiles(srcRoot);
18
+
19
+ const rows = [];
20
+ for (const file of files) {
21
+ rows.push(await analyzeFileToRow(file, internalsRoot, 8));
22
+ }
23
+
24
+ const report = {
25
+ effectRepoPath,
26
+ srcRoot,
27
+ internalsRoot,
28
+ summary: summarizeRows(rows),
29
+ rows,
30
+ } as const;
31
+
32
+ if (process.argv.includes('--human')) {
33
+ printHumanSummary('effect-top-level', rows);
34
+ }
35
+
36
+ const jsonOut = getArgValue('--json-out');
37
+ if (jsonOut) {
38
+ writeJsonFile(jsonOut, report);
39
+ if (process.argv.includes('--human')) {
40
+ console.log(`wrote ${jsonOut}`);
41
+ return;
42
+ }
43
+ }
44
+
45
+ if (process.argv.includes('--human')) {
46
+ return;
47
+ }
48
+
49
+ console.log(JSON.stringify(report, null, 2));
50
+ }
51
+
52
+ void main();
@@ -0,0 +1,47 @@
1
+ import { readFileSync } from 'fs';
2
+ import { basename } from 'path';
3
+ import type { ScanReport } from './effect-scan-utils';
4
+
5
+ function main(): void {
6
+ const reportPath = process.argv[2];
7
+ if (!reportPath) {
8
+ console.error('usage: pnpm exec tsx scripts/summarize-scan.ts <scan.json> [--limit N]');
9
+ process.exit(1);
10
+ }
11
+
12
+ const limitArgIndex = process.argv.indexOf('--limit');
13
+ const limit =
14
+ limitArgIndex >= 0 && process.argv[limitArgIndex + 1]
15
+ ? Number.parseInt(process.argv[limitArgIndex + 1]!, 10)
16
+ : 15;
17
+
18
+ const report = JSON.parse(readFileSync(reportPath, 'utf8')) as ScanReport;
19
+
20
+ console.log(
21
+ `${basename(reportPath)} total=${report.summary.total} ok=${report.summary.ok} fail=${report.summary.fail} throw=${report.summary.throwCount}`,
22
+ );
23
+
24
+ const reasonCounts = new Map<string, number>();
25
+ for (const row of report.rows) {
26
+ if (!row.zeroProgramsReason) continue;
27
+ reasonCounts.set(row.zeroProgramsReason, (reasonCounts.get(row.zeroProgramsReason) ?? 0) + 1);
28
+ }
29
+
30
+ if (reasonCounts.size > 0) {
31
+ console.log('zeroProgramsReason counts:');
32
+ for (const [reason, count] of Array.from(reasonCounts.entries()).sort((a, b) => b[1] - a[1])) {
33
+ console.log(`- ${reason}: ${count}`);
34
+ }
35
+ }
36
+
37
+ const nonOk = report.rows.filter((row) => !row.tag.startsWith('OK:'));
38
+ if (nonOk.length > 0) {
39
+ console.log(`non-OK rows (sample ${Math.min(limit, nonOk.length)}):`);
40
+ for (const row of nonOk.slice(0, Math.max(1, limit))) {
41
+ const reason = row.zeroProgramsReason ? ` (${row.zeroProgramsReason})` : '';
42
+ console.log(`- ${basename(row.file)} ${row.tag}${reason}`);
43
+ }
44
+ }
45
+ }
46
+
47
+ main();