miniread 1.0.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/LICENSE +21 -0
- package/README.md +86 -0
- package/bin/miniread +17 -0
- package/bin/miniread-evaluate +17 -0
- package/bin/miniread-sample +17 -0
- package/dist/cli/config.d.ts +17 -0
- package/dist/cli/config.js +85 -0
- package/dist/cli/generate-code.d.ts +2 -0
- package/dist/cli/generate-code.js +7 -0
- package/dist/cli/output.d.ts +7 -0
- package/dist/cli/output.js +119 -0
- package/dist/cli/run-transforms.d.ts +16 -0
- package/dist/cli/run-transforms.js +145 -0
- package/dist/cli/runner.d.ts +3 -0
- package/dist/cli/runner.js +2 -0
- package/dist/cli/transform-stdin.d.ts +14 -0
- package/dist/cli/transform-stdin.js +58 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +121 -0
- package/dist/core/paths.d.ts +8 -0
- package/dist/core/paths.js +9 -0
- package/dist/core/project-graph.d.ts +6 -0
- package/dist/core/project-graph.js +40 -0
- package/dist/core/types.d.ts +30 -0
- package/dist/core/types.js +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/scripts/evaluate/create-evaluate-command.d.ts +5 -0
- package/dist/scripts/evaluate/create-evaluate-command.js +47 -0
- package/dist/scripts/evaluate/diff-utilities.d.ts +17 -0
- package/dist/scripts/evaluate/diff-utilities.js +114 -0
- package/dist/scripts/evaluate/evaluation-report.d.ts +22 -0
- package/dist/scripts/evaluate/evaluation-report.js +1 -0
- package/dist/scripts/evaluate/evaluation-types.d.ts +16 -0
- package/dist/scripts/evaluate/evaluation-types.js +1 -0
- package/dist/scripts/evaluate/metrics.d.ts +3 -0
- package/dist/scripts/evaluate/metrics.js +11 -0
- package/dist/scripts/evaluate/pair-evaluator.d.ts +21 -0
- package/dist/scripts/evaluate/pair-evaluator.js +104 -0
- package/dist/scripts/evaluate/parse-evaluate-cli-options.d.ts +40 -0
- package/dist/scripts/evaluate/parse-evaluate-cli-options.js +97 -0
- package/dist/scripts/evaluate/parse-transform-manifest.d.ts +22 -0
- package/dist/scripts/evaluate/parse-transform-manifest.js +29 -0
- package/dist/scripts/evaluate/resolve-evaluate-dependencies.d.ts +9 -0
- package/dist/scripts/evaluate/resolve-evaluate-dependencies.js +66 -0
- package/dist/scripts/evaluate/run-evaluate-cli.d.ts +1 -0
- package/dist/scripts/evaluate/run-evaluate-cli.js +112 -0
- package/dist/scripts/evaluate/run-evaluations.d.ts +25 -0
- package/dist/scripts/evaluate/run-evaluations.js +50 -0
- package/dist/scripts/evaluate/run-pair-transformations.d.ts +21 -0
- package/dist/scripts/evaluate/run-pair-transformations.js +57 -0
- package/dist/scripts/evaluate/sanitize.d.ts +2 -0
- package/dist/scripts/evaluate/sanitize.js +6 -0
- package/dist/scripts/evaluate/shell-utilities.d.ts +15 -0
- package/dist/scripts/evaluate/shell-utilities.js +68 -0
- package/dist/scripts/evaluate/transform-id-set.d.ts +3 -0
- package/dist/scripts/evaluate/transform-id-set.js +29 -0
- package/dist/scripts/evaluate/transform-manifest.d.ts +13 -0
- package/dist/scripts/evaluate/transform-manifest.js +102 -0
- package/dist/scripts/evaluate/write-evaluation-stdout.d.ts +7 -0
- package/dist/scripts/evaluate/write-evaluation-stdout.js +20 -0
- package/dist/scripts/evaluate/write-text-file-atomic.d.ts +5 -0
- package/dist/scripts/evaluate/write-text-file-atomic.js +47 -0
- package/dist/scripts/evaluate.d.ts +2 -0
- package/dist/scripts/evaluate.js +13 -0
- package/dist/scripts/sample/choose-line-window.d.ts +11 -0
- package/dist/scripts/sample/choose-line-window.js +19 -0
- package/dist/scripts/sample/clip-text-around-core.d.ts +12 -0
- package/dist/scripts/sample/clip-text-around-core.js +61 -0
- package/dist/scripts/sample/collect-function-candidates.d.ts +20 -0
- package/dist/scripts/sample/collect-function-candidates.js +82 -0
- package/dist/scripts/sample/create-function-excerpt.d.ts +20 -0
- package/dist/scripts/sample/create-function-excerpt.js +37 -0
- package/dist/scripts/sample/create-sample-command.d.ts +17 -0
- package/dist/scripts/sample/create-sample-command.js +50 -0
- package/dist/scripts/sample/extract-function-samples.d.ts +31 -0
- package/dist/scripts/sample/extract-function-samples.js +105 -0
- package/dist/scripts/sample/find-source-files.d.ts +1 -0
- package/dist/scripts/sample/find-source-files.js +49 -0
- package/dist/scripts/sample/format-sample-output.d.ts +22 -0
- package/dist/scripts/sample/format-sample-output.js +29 -0
- package/dist/scripts/sample/line-offsets.d.ts +4 -0
- package/dist/scripts/sample/line-offsets.js +40 -0
- package/dist/scripts/sample/parse-sample-cli-options.d.ts +18 -0
- package/dist/scripts/sample/parse-sample-cli-options.js +36 -0
- package/dist/scripts/sample/random.d.ts +10 -0
- package/dist/scripts/sample/random.js +60 -0
- package/dist/scripts/sample/run-sample-cli.d.ts +1 -0
- package/dist/scripts/sample/run-sample-cli.js +79 -0
- package/dist/scripts/sample.d.ts +2 -0
- package/dist/scripts/sample.js +13 -0
- package/dist/transforms/add-prefix/add-prefix-transform.d.ts +2 -0
- package/dist/transforms/add-prefix/add-prefix-transform.js +40 -0
- package/dist/transforms/add-suffix/add-suffix-transform.d.ts +2 -0
- package/dist/transforms/add-suffix/add-suffix-transform.js +40 -0
- package/dist/transforms/expand-boolean-literals/expand-boolean-literals-transform.d.ts +2 -0
- package/dist/transforms/expand-boolean-literals/expand-boolean-literals-transform.js +39 -0
- package/dist/transforms/expand-undefined-literals/expand-undefined-literals-transform.d.ts +2 -0
- package/dist/transforms/expand-undefined-literals/expand-undefined-literals-transform.js +40 -0
- package/dist/transforms/rename-catch-parameters/rename-catch-parameters-transform.d.ts +2 -0
- package/dist/transforms/rename-catch-parameters/rename-catch-parameters-transform.js +67 -0
- package/dist/transforms/rename-loop-index-variables/rename-loop-index-variables-transform.d.ts +2 -0
- package/dist/transforms/rename-loop-index-variables/rename-loop-index-variables-transform.js +125 -0
- package/dist/transforms/transform-presets.d.ts +3 -0
- package/dist/transforms/transform-presets.js +19 -0
- package/dist/transforms/transform-registry.d.ts +3 -0
- package/dist/transforms/transform-registry.js +15 -0
- package/package.json +84 -0
- package/transform-manifest.json +70 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import { haveSameIds, normalizeTransformIds, uniqueIds, } from "./transform-id-set.js";
|
|
3
|
+
import { parseTransformManifest } from "./parse-transform-manifest.js";
|
|
4
|
+
import { writeJsonFileAtomic } from "./write-text-file-atomic.js";
|
|
5
|
+
const average = (values) => {
|
|
6
|
+
if (values.length === 0)
|
|
7
|
+
return 0;
|
|
8
|
+
let total = 0;
|
|
9
|
+
for (const value of values) {
|
|
10
|
+
total += value;
|
|
11
|
+
}
|
|
12
|
+
return total / values.length;
|
|
13
|
+
};
|
|
14
|
+
export const updateTransformManifestFromEvaluation = async (options) => {
|
|
15
|
+
const { manifestPath, baselineTransforms, testTransforms, reductionRatios, transformRegistry, } = options;
|
|
16
|
+
try {
|
|
17
|
+
await fs.access(manifestPath);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return {
|
|
21
|
+
updated: false,
|
|
22
|
+
message: `Skipped updating transform-manifest.json (not found at ${manifestPath})`,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const manifest = parseTransformManifest(await fs.readFile(manifestPath, "utf8"));
|
|
26
|
+
const existingIds = new Set(manifest.transforms.map((t) => t.id));
|
|
27
|
+
const baselineIds = normalizeTransformIds(baselineTransforms);
|
|
28
|
+
const testIds = normalizeTransformIds(testTransforms);
|
|
29
|
+
const idsToEnsure = new Set([...baselineIds, ...testIds]);
|
|
30
|
+
idsToEnsure.delete("none");
|
|
31
|
+
let maxIteration = 0;
|
|
32
|
+
for (const entry of manifest.transforms) {
|
|
33
|
+
maxIteration = Math.max(maxIteration, entry.iteration);
|
|
34
|
+
}
|
|
35
|
+
let nextIteration = maxIteration + 1;
|
|
36
|
+
let updated = false;
|
|
37
|
+
for (const transformId of idsToEnsure) {
|
|
38
|
+
if (existingIds.has(transformId))
|
|
39
|
+
continue;
|
|
40
|
+
const transform = transformRegistry[transformId];
|
|
41
|
+
const description = transform?.description ?? `Auto-added by evaluation: ${transformId}`;
|
|
42
|
+
const scope = transform?.scope ?? "file";
|
|
43
|
+
const parallelizable = transform?.parallelizable ?? true;
|
|
44
|
+
manifest.transforms.push({
|
|
45
|
+
id: transformId,
|
|
46
|
+
description,
|
|
47
|
+
iteration: nextIteration++,
|
|
48
|
+
scope,
|
|
49
|
+
parallelizable,
|
|
50
|
+
diffReductionImpact: 0,
|
|
51
|
+
enabledByDefault: false,
|
|
52
|
+
notes: "Auto-added by evaluation script.",
|
|
53
|
+
});
|
|
54
|
+
existingIds.add(transformId);
|
|
55
|
+
updated = true;
|
|
56
|
+
}
|
|
57
|
+
const isBaselineNone = baselineIds.length === 0 ||
|
|
58
|
+
(baselineTransforms.length === 1 && baselineTransforms[0] === "none");
|
|
59
|
+
if (isBaselineNone && testIds.length === 1) {
|
|
60
|
+
const transformId = testIds[0];
|
|
61
|
+
if (transformId) {
|
|
62
|
+
const measuredImpact = average(reductionRatios);
|
|
63
|
+
const entry = manifest.transforms.find((t) => t.id === transformId);
|
|
64
|
+
if (entry && entry.diffReductionImpact !== measuredImpact) {
|
|
65
|
+
entry.diffReductionImpact = measuredImpact;
|
|
66
|
+
const measurementNote = `Measured with baseline none: ${(measuredImpact * 100).toFixed(2)}%.`;
|
|
67
|
+
entry.notes = entry.notes
|
|
68
|
+
? `${entry.notes} ${measurementNote}`
|
|
69
|
+
: measurementNote;
|
|
70
|
+
updated = true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (isBaselineNone) {
|
|
75
|
+
const recommendedIds = manifest.transforms
|
|
76
|
+
.filter((entry) => entry.enabledByDefault)
|
|
77
|
+
.map((entry) => entry.id);
|
|
78
|
+
const testUniqueIds = uniqueIds(testIds);
|
|
79
|
+
const recommendedUniqueIds = uniqueIds(recommendedIds);
|
|
80
|
+
if (testUniqueIds.length > 0 &&
|
|
81
|
+
haveSameIds(testUniqueIds, recommendedUniqueIds)) {
|
|
82
|
+
const measuredImpact = average(reductionRatios);
|
|
83
|
+
manifest.presetStats ??= {};
|
|
84
|
+
const existing = manifest.presetStats.recommended;
|
|
85
|
+
if (existing?.diffReductionImpact !== measuredImpact) {
|
|
86
|
+
manifest.presetStats.recommended = {
|
|
87
|
+
diffReductionImpact: measuredImpact,
|
|
88
|
+
notes: `Measured with baseline none: ${(measuredImpact * 100).toFixed(2)}%.`,
|
|
89
|
+
};
|
|
90
|
+
updated = true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (!updated) {
|
|
95
|
+
return {
|
|
96
|
+
updated: false,
|
|
97
|
+
message: "transform-manifest.json already up to date",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
await writeJsonFileAtomic(manifestPath, manifest, { overwrite: true });
|
|
101
|
+
return { updated: true, message: "Updated transform-manifest.json" };
|
|
102
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { OutputFormat } from "./create-evaluate-command.js";
|
|
2
|
+
import type { EvaluationOutcome, EvaluationReport } from "./evaluation-report.js";
|
|
3
|
+
export declare const writeEvaluationStdout: (options: {
|
|
4
|
+
format: OutputFormat;
|
|
5
|
+
report: EvaluationReport;
|
|
6
|
+
outcomes: EvaluationOutcome[];
|
|
7
|
+
}) => void;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { sanitizeTsvField } from "./sanitize.js";
|
|
2
|
+
export const writeEvaluationStdout = (options) => {
|
|
3
|
+
const { format, report, outcomes } = options;
|
|
4
|
+
if (format === "json") {
|
|
5
|
+
process.stdout.write(JSON.stringify(report, undefined, 2) + "\n");
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
process.stdout.write("FROM\tTO\tBASELINE_CHANGED_LINES\tTEST_CHANGED_LINES\tREDUCTION_RATIO\tSTATUS\tERROR\n");
|
|
9
|
+
for (const outcome of outcomes) {
|
|
10
|
+
if (!outcome.ok) {
|
|
11
|
+
process.stdout.write(`${outcome.pair.from}\t${outcome.pair.to}\t\t\t\tERROR\t${sanitizeTsvField(outcome.error)}\n`);
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
const baselineChangedLines = outcome.result.baselineDiff.addedLines +
|
|
15
|
+
outcome.result.baselineDiff.removedLines;
|
|
16
|
+
const testChangedLines = outcome.result.testDiff.addedLines + outcome.result.testDiff.removedLines;
|
|
17
|
+
const status = outcome.result.reductionRatio >= 0 ? "GOOD" : "BAD";
|
|
18
|
+
process.stdout.write(`${outcome.pair.from}\t${outcome.pair.to}\t${baselineChangedLines}\t${testChangedLines}\t${outcome.result.reductionRatio}\t${status}\t\n`);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const isErrnoException = (error) => {
|
|
5
|
+
return error instanceof Error;
|
|
6
|
+
};
|
|
7
|
+
const commitTemporaryFile = async (temporaryPath, outputPath, options) => {
|
|
8
|
+
if (options.overwrite) {
|
|
9
|
+
try {
|
|
10
|
+
await fs.rename(temporaryPath, outputPath);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (!isErrnoException(error))
|
|
15
|
+
throw error;
|
|
16
|
+
if (process.platform !== "win32")
|
|
17
|
+
throw error;
|
|
18
|
+
if (error.code !== "EEXIST" && error.code !== "EPERM")
|
|
19
|
+
throw error;
|
|
20
|
+
await fs.unlink(outputPath);
|
|
21
|
+
await fs.rename(temporaryPath, outputPath);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (process.platform === "win32") {
|
|
26
|
+
await fs.rename(temporaryPath, outputPath);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
await fs.link(temporaryPath, outputPath);
|
|
30
|
+
await fs.unlink(temporaryPath);
|
|
31
|
+
};
|
|
32
|
+
const writeTextFileAtomic = async (outputPath, content, options) => {
|
|
33
|
+
const outputDirectory = path.dirname(outputPath);
|
|
34
|
+
await fs.mkdir(outputDirectory, { recursive: true });
|
|
35
|
+
const temporaryPath = path.join(outputDirectory, `.miniread-evaluate-tmp-${randomUUID()}`);
|
|
36
|
+
await fs.writeFile(temporaryPath, content, { encoding: "utf8", flag: "wx" });
|
|
37
|
+
try {
|
|
38
|
+
await commitTemporaryFile(temporaryPath, outputPath, options);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
await fs.unlink(temporaryPath).catch(() => { });
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
export const writeJsonFileAtomic = async (outputPath, value, options) => {
|
|
46
|
+
await writeTextFileAtomic(outputPath, JSON.stringify(value, undefined, 2) + "\n", options);
|
|
47
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runEvaluateCli } from "./evaluate/run-evaluate-cli.js";
|
|
3
|
+
try {
|
|
4
|
+
const exitCode = await runEvaluateCli(process.argv);
|
|
5
|
+
if (exitCode !== 0) {
|
|
6
|
+
process.exitCode = exitCode;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
11
|
+
console.error(`Error: ${message}`);
|
|
12
|
+
process.exitCode = 1;
|
|
13
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const chooseLineWindow = (options) => {
|
|
2
|
+
const { totalLines, startLine, endLine, contextLines, maxLines } = options;
|
|
3
|
+
const candidateLines = endLine - startLine + 1;
|
|
4
|
+
if (candidateLines > maxLines) {
|
|
5
|
+
return {
|
|
6
|
+
windowStartLine: startLine,
|
|
7
|
+
windowEndLine: Math.min(totalLines, startLine + maxLines - 1),
|
|
8
|
+
truncatedByLines: true,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
const extraLinesAvailable = maxLines - candidateLines;
|
|
12
|
+
const before = Math.min(contextLines, Math.floor(extraLinesAvailable / 2));
|
|
13
|
+
const after = Math.min(contextLines, extraLinesAvailable - before);
|
|
14
|
+
return {
|
|
15
|
+
windowStartLine: Math.max(1, startLine - before),
|
|
16
|
+
windowEndLine: Math.min(totalLines, endLine + after),
|
|
17
|
+
truncatedByLines: false,
|
|
18
|
+
};
|
|
19
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const clipTextAroundCore: (options: {
|
|
2
|
+
text: string;
|
|
3
|
+
coreStart: number;
|
|
4
|
+
coreEnd: number;
|
|
5
|
+
contextChars: number;
|
|
6
|
+
maxChars: number;
|
|
7
|
+
}) => {
|
|
8
|
+
excerptStart: number;
|
|
9
|
+
excerptEnd: number;
|
|
10
|
+
code: string;
|
|
11
|
+
truncatedByChars: boolean;
|
|
12
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export const clipTextAroundCore = (options) => {
|
|
2
|
+
const { text, coreStart, coreEnd, contextChars, maxChars } = options;
|
|
3
|
+
if (text.length <= maxChars) {
|
|
4
|
+
return {
|
|
5
|
+
excerptStart: 0,
|
|
6
|
+
excerptEnd: text.length,
|
|
7
|
+
code: text,
|
|
8
|
+
truncatedByChars: false,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
const normalizedCoreStart = Math.max(0, Math.min(coreStart, text.length));
|
|
12
|
+
const normalizedCoreEnd = Math.max(normalizedCoreStart, Math.min(coreEnd, text.length));
|
|
13
|
+
const coreLength = normalizedCoreEnd - normalizedCoreStart;
|
|
14
|
+
const marker = "\n/* … truncated … */\n";
|
|
15
|
+
if (coreLength > maxChars) {
|
|
16
|
+
const available = maxChars - marker.length;
|
|
17
|
+
const headLength = Math.max(1, Math.floor(available / 2));
|
|
18
|
+
const tailLength = Math.max(1, available - headLength);
|
|
19
|
+
const head = text.slice(normalizedCoreStart, normalizedCoreStart + headLength);
|
|
20
|
+
const tail = text.slice(normalizedCoreEnd - tailLength, normalizedCoreEnd);
|
|
21
|
+
return {
|
|
22
|
+
excerptStart: normalizedCoreStart,
|
|
23
|
+
excerptEnd: normalizedCoreEnd,
|
|
24
|
+
code: `${head}${marker}${tail}`,
|
|
25
|
+
truncatedByChars: true,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const desiredStart = Math.max(0, normalizedCoreStart - contextChars);
|
|
29
|
+
const desiredEnd = Math.min(text.length, normalizedCoreEnd + contextChars);
|
|
30
|
+
const desiredLength = desiredEnd - desiredStart;
|
|
31
|
+
if (desiredLength > maxChars) {
|
|
32
|
+
const budget = maxChars - coreLength;
|
|
33
|
+
const availableBefore = Math.max(0, normalizedCoreStart - desiredStart);
|
|
34
|
+
const availableAfter = Math.max(0, desiredEnd - normalizedCoreEnd);
|
|
35
|
+
let before = Math.min(availableBefore, Math.floor(budget / 2));
|
|
36
|
+
const after = Math.min(availableAfter, budget - before);
|
|
37
|
+
before = Math.min(availableBefore, budget - after);
|
|
38
|
+
const excerptStart = Math.max(0, normalizedCoreStart - before);
|
|
39
|
+
const excerptEnd = Math.min(text.length, normalizedCoreEnd + after);
|
|
40
|
+
return {
|
|
41
|
+
excerptStart,
|
|
42
|
+
excerptEnd,
|
|
43
|
+
code: text.slice(excerptStart, excerptEnd),
|
|
44
|
+
truncatedByChars: true,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const leftover = maxChars - desiredLength;
|
|
48
|
+
const availableBefore = desiredStart;
|
|
49
|
+
const availableAfter = text.length - desiredEnd;
|
|
50
|
+
let before = Math.min(availableBefore, Math.floor(leftover / 2));
|
|
51
|
+
const after = Math.min(availableAfter, leftover - before);
|
|
52
|
+
before = Math.min(availableBefore, leftover - after);
|
|
53
|
+
const excerptStart = Math.max(0, desiredStart - before);
|
|
54
|
+
const excerptEnd = Math.min(text.length, desiredEnd + after);
|
|
55
|
+
return {
|
|
56
|
+
excerptStart,
|
|
57
|
+
excerptEnd,
|
|
58
|
+
code: text.slice(excerptStart, excerptEnd),
|
|
59
|
+
truncatedByChars: true,
|
|
60
|
+
};
|
|
61
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type FunctionCandidate = {
|
|
2
|
+
nodeType: string;
|
|
3
|
+
name: string | undefined;
|
|
4
|
+
startLine: number;
|
|
5
|
+
endLine: number;
|
|
6
|
+
startColumn: number;
|
|
7
|
+
endColumn: number;
|
|
8
|
+
startOffset: number;
|
|
9
|
+
endOffset: number;
|
|
10
|
+
};
|
|
11
|
+
export declare const collectFileFunctionCandidates: (options: {
|
|
12
|
+
filePath: string;
|
|
13
|
+
content: string;
|
|
14
|
+
}) => {
|
|
15
|
+
ok: true;
|
|
16
|
+
candidates: FunctionCandidate[];
|
|
17
|
+
} | {
|
|
18
|
+
ok: false;
|
|
19
|
+
error: string;
|
|
20
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { parse } from "@babel/parser";
|
|
3
|
+
import * as t from "@babel/types";
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
6
|
+
const traverse = require("@babel/traverse").default;
|
|
7
|
+
const getPlugins = (filePath) => {
|
|
8
|
+
const plugins = [];
|
|
9
|
+
if (filePath.endsWith(".ts") || filePath.endsWith(".tsx")) {
|
|
10
|
+
plugins.push("typescript");
|
|
11
|
+
}
|
|
12
|
+
if (filePath.endsWith(".jsx") || filePath.endsWith(".tsx")) {
|
|
13
|
+
plugins.push("jsx");
|
|
14
|
+
}
|
|
15
|
+
return plugins;
|
|
16
|
+
};
|
|
17
|
+
const inferFunctionName = (path) => {
|
|
18
|
+
const idNode = path.node.id;
|
|
19
|
+
if (idNode && t.isIdentifier(idNode))
|
|
20
|
+
return idNode.name;
|
|
21
|
+
const parent = path.parentPath;
|
|
22
|
+
// const foo = function() {} / const foo = () => {}
|
|
23
|
+
if (parent.isVariableDeclarator()) {
|
|
24
|
+
const { id } = parent.node;
|
|
25
|
+
if (id && t.isIdentifier(id))
|
|
26
|
+
return id.name;
|
|
27
|
+
}
|
|
28
|
+
// foo = function() {} / foo = () => {}
|
|
29
|
+
if (parent.isAssignmentExpression()) {
|
|
30
|
+
const { left } = parent.node;
|
|
31
|
+
if (left && t.isIdentifier(left))
|
|
32
|
+
return left.name;
|
|
33
|
+
}
|
|
34
|
+
// { foo: function() {} } / { foo() {} }
|
|
35
|
+
if (parent.isObjectProperty() || parent.isObjectMethod()) {
|
|
36
|
+
const { key } = parent.node;
|
|
37
|
+
if (key && t.isIdentifier(key))
|
|
38
|
+
return key.name;
|
|
39
|
+
if (key && t.isStringLiteral(key))
|
|
40
|
+
return key.value;
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
};
|
|
44
|
+
const collectFunctionCandidates = (ast) => {
|
|
45
|
+
const candidates = [];
|
|
46
|
+
traverse(ast, {
|
|
47
|
+
Function(path) {
|
|
48
|
+
const loc = path.node.loc;
|
|
49
|
+
const start = path.node.start;
|
|
50
|
+
const end = path.node.end;
|
|
51
|
+
if (!loc || typeof start !== "number" || typeof end !== "number")
|
|
52
|
+
return;
|
|
53
|
+
candidates.push({
|
|
54
|
+
nodeType: path.node.type,
|
|
55
|
+
name: inferFunctionName(path),
|
|
56
|
+
startLine: loc.start.line,
|
|
57
|
+
endLine: loc.end.line,
|
|
58
|
+
startColumn: loc.start.column,
|
|
59
|
+
endColumn: loc.end.column,
|
|
60
|
+
startOffset: start,
|
|
61
|
+
endOffset: end,
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
return candidates;
|
|
66
|
+
};
|
|
67
|
+
export const collectFileFunctionCandidates = (options) => {
|
|
68
|
+
const { filePath, content } = options;
|
|
69
|
+
let ast;
|
|
70
|
+
try {
|
|
71
|
+
ast = parse(content, {
|
|
72
|
+
sourceType: "unambiguous",
|
|
73
|
+
plugins: getPlugins(filePath),
|
|
74
|
+
sourceFilename: filePath,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
79
|
+
return { ok: false, error: `Failed to parse ${filePath}: ${message}` };
|
|
80
|
+
}
|
|
81
|
+
return { ok: true, candidates: collectFunctionCandidates(ast) };
|
|
82
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const createFunctionExcerpt: (options: {
|
|
2
|
+
content: string;
|
|
3
|
+
lineStartOffsets: number[];
|
|
4
|
+
totalLines: number;
|
|
5
|
+
startLine: number;
|
|
6
|
+
endLine: number;
|
|
7
|
+
startOffset: number;
|
|
8
|
+
endOffset: number;
|
|
9
|
+
contextLines: number;
|
|
10
|
+
contextChars: number;
|
|
11
|
+
maxLines: number;
|
|
12
|
+
maxChars: number;
|
|
13
|
+
}) => {
|
|
14
|
+
excerptStartLine: number;
|
|
15
|
+
excerptEndLine: number;
|
|
16
|
+
excerptStartOffset: number;
|
|
17
|
+
excerptEndOffset: number;
|
|
18
|
+
truncated: boolean;
|
|
19
|
+
code: string;
|
|
20
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { getLineEndOffset, getLineStartOffset, offsetToLineNumber, } from "./line-offsets.js";
|
|
2
|
+
import { clipTextAroundCore } from "./clip-text-around-core.js";
|
|
3
|
+
import { chooseLineWindow } from "./choose-line-window.js";
|
|
4
|
+
export const createFunctionExcerpt = (options) => {
|
|
5
|
+
const { content, lineStartOffsets, totalLines, startLine, endLine, startOffset, endOffset, contextLines, contextChars, maxLines, maxChars, } = options;
|
|
6
|
+
const { windowStartLine, windowEndLine, truncatedByLines } = chooseLineWindow({
|
|
7
|
+
totalLines,
|
|
8
|
+
startLine,
|
|
9
|
+
endLine,
|
|
10
|
+
contextLines,
|
|
11
|
+
maxLines,
|
|
12
|
+
});
|
|
13
|
+
const windowStartOffset = getLineStartOffset(lineStartOffsets, windowStartLine);
|
|
14
|
+
const windowEndOffset = getLineEndOffset(lineStartOffsets, windowEndLine, content.length);
|
|
15
|
+
const windowText = content.slice(windowStartOffset, windowEndOffset);
|
|
16
|
+
const coreStart = Math.max(0, startOffset - windowStartOffset);
|
|
17
|
+
const coreEnd = Math.min(windowText.length, endOffset - windowStartOffset);
|
|
18
|
+
const clipped = clipTextAroundCore({
|
|
19
|
+
text: windowText,
|
|
20
|
+
coreStart,
|
|
21
|
+
coreEnd,
|
|
22
|
+
contextChars,
|
|
23
|
+
maxChars,
|
|
24
|
+
});
|
|
25
|
+
const excerptStartOffset = windowStartOffset + clipped.excerptStart;
|
|
26
|
+
const excerptEndOffset = windowStartOffset + clipped.excerptEnd;
|
|
27
|
+
const excerptStartLine = offsetToLineNumber(lineStartOffsets, excerptStartOffset);
|
|
28
|
+
const excerptEndLine = offsetToLineNumber(lineStartOffsets, Math.max(excerptStartOffset, excerptEndOffset - 1));
|
|
29
|
+
return {
|
|
30
|
+
excerptStartLine,
|
|
31
|
+
excerptEndLine,
|
|
32
|
+
excerptStartOffset,
|
|
33
|
+
excerptEndOffset,
|
|
34
|
+
truncated: truncatedByLines || clipped.truncatedByChars,
|
|
35
|
+
code: clipped.code,
|
|
36
|
+
};
|
|
37
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Command } from "@commander-js/extra-typings";
|
|
2
|
+
export type SampleOutputFormat = "markdown" | "json";
|
|
3
|
+
export type SampleCliRawOptions = {
|
|
4
|
+
input: string;
|
|
5
|
+
snippets: number;
|
|
6
|
+
maxFiles: number;
|
|
7
|
+
minLines: number;
|
|
8
|
+
maxLines: number;
|
|
9
|
+
maxChars: number;
|
|
10
|
+
contextLines: number;
|
|
11
|
+
contextChars: number;
|
|
12
|
+
seed?: string;
|
|
13
|
+
format: SampleOutputFormat;
|
|
14
|
+
};
|
|
15
|
+
export declare const createSampleCommand: (options: {
|
|
16
|
+
version: string;
|
|
17
|
+
}) => Command;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Command, InvalidArgumentError } from "@commander-js/extra-typings";
|
|
2
|
+
const parsePositiveInt = (rawValue) => {
|
|
3
|
+
const value = Number(rawValue);
|
|
4
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
5
|
+
throw new InvalidArgumentError(`invalid value '${rawValue}' (expected a positive integer)`);
|
|
6
|
+
}
|
|
7
|
+
return value;
|
|
8
|
+
};
|
|
9
|
+
const parseNonNegativeInt = (rawValue) => {
|
|
10
|
+
const value = Number(rawValue);
|
|
11
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
12
|
+
throw new InvalidArgumentError(`invalid value '${rawValue}' (expected a non-negative integer)`);
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
};
|
|
16
|
+
const parseOutputFormat = (rawValue) => {
|
|
17
|
+
if (rawValue === "markdown" || rawValue === "json")
|
|
18
|
+
return rawValue;
|
|
19
|
+
throw new InvalidArgumentError(`invalid value '${rawValue}' (expected: markdown, json)`);
|
|
20
|
+
};
|
|
21
|
+
export const createSampleCommand = (options) => {
|
|
22
|
+
const program = new Command()
|
|
23
|
+
.name("miniread-sample")
|
|
24
|
+
.description("Generate random code samples from a source tree so an agent cannot choose the samples itself.")
|
|
25
|
+
.version(options.version)
|
|
26
|
+
.showHelpAfterError("(add --help for additional information)")
|
|
27
|
+
.showSuggestionAfterError()
|
|
28
|
+
.requiredOption("-i, --input <path>", "File or directory to sample from (e.g. sources/claude-code-2.1.11)")
|
|
29
|
+
.option("--snippets <count>", "How many random snippets to sample (total across all files under --input)", parsePositiveInt, 5)
|
|
30
|
+
.option("--max-files <count>", "Limit how many files are scanned when --input is a directory (0 = all files)", parseNonNegativeInt, 0)
|
|
31
|
+
.option("--min-lines <count>", "Minimum snippet size in lines (shorter function bodies are skipped when possible)", parsePositiveInt, 5)
|
|
32
|
+
.option("--max-lines <count>", "Maximum snippet size in lines (clips larger function bodies)", parsePositiveInt, 80)
|
|
33
|
+
.option("--max-chars <count>", "Maximum excerpt size in characters (clips very long lines/functions)", parsePositiveInt, 4000)
|
|
34
|
+
.option("--context-lines <count>", "Extra lines of context around each snippet (best-effort, may be reduced to fit --max-lines)", parseNonNegativeInt, 2)
|
|
35
|
+
.option("--context-chars <count>", "Extra characters of context around each snippet (best-effort, may be reduced to fit --max-chars)", parseNonNegativeInt, 200)
|
|
36
|
+
.option("--seed <value>", "Seed for deterministic sampling (optional)")
|
|
37
|
+
.option("--format <format>", "Output format: markdown, json", parseOutputFormat, "markdown");
|
|
38
|
+
program.addHelpText("after", `
|
|
39
|
+
Examples:
|
|
40
|
+
# Sample 5 random functions from a minified source directory
|
|
41
|
+
pnpm run miniread-sample -- --input sources/claude-code-2.1.11
|
|
42
|
+
|
|
43
|
+
# Sample from a single file
|
|
44
|
+
pnpm run miniread-sample -- --input sources/claude-code-2.1.11/cli.js --snippets 10
|
|
45
|
+
|
|
46
|
+
# Deterministic output (share the same samples with someone else)
|
|
47
|
+
pnpm run miniread-sample -- --input sources/claude-code-2.1.11 --seed 2026-01-21
|
|
48
|
+
`);
|
|
49
|
+
return program;
|
|
50
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { RandomNumberGenerator } from "./random.js";
|
|
2
|
+
export type FunctionSample = {
|
|
3
|
+
filePath: string;
|
|
4
|
+
filePathRelative: string;
|
|
5
|
+
nodeType: string;
|
|
6
|
+
name: string | undefined;
|
|
7
|
+
startLine: number;
|
|
8
|
+
endLine: number;
|
|
9
|
+
startColumn: number;
|
|
10
|
+
endColumn: number;
|
|
11
|
+
startOffset: number;
|
|
12
|
+
endOffset: number;
|
|
13
|
+
excerptStartLine: number;
|
|
14
|
+
excerptEndLine: number;
|
|
15
|
+
excerptStartOffset: number;
|
|
16
|
+
excerptEndOffset: number;
|
|
17
|
+
truncated: boolean;
|
|
18
|
+
code: string;
|
|
19
|
+
};
|
|
20
|
+
export declare const extractFunctionSamples: (options: {
|
|
21
|
+
filePaths: string[];
|
|
22
|
+
rootDirectory: string;
|
|
23
|
+
maxFiles: number;
|
|
24
|
+
snippets: number;
|
|
25
|
+
minLines: number;
|
|
26
|
+
maxLines: number;
|
|
27
|
+
contextLines: number;
|
|
28
|
+
contextChars: number;
|
|
29
|
+
maxChars: number;
|
|
30
|
+
rng: RandomNumberGenerator;
|
|
31
|
+
}) => Promise<FunctionSample[]>;
|