miniread 1.0.0 → 1.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.
Files changed (91) hide show
  1. package/README.md +2 -2
  2. package/dist/scripts/evaluate/check-expected-evaluations.d.ts +17 -0
  3. package/dist/scripts/evaluate/check-expected-evaluations.js +35 -0
  4. package/dist/scripts/evaluate/check-snapshots.d.ts +14 -0
  5. package/dist/scripts/evaluate/check-snapshots.js +111 -0
  6. package/dist/scripts/evaluate/clone-metrics-report.d.ts +2 -0
  7. package/dist/scripts/evaluate/clone-metrics-report.js +6 -0
  8. package/dist/scripts/evaluate/create-evaluate-command.js +5 -20
  9. package/dist/scripts/evaluate/evaluate-help-text.d.ts +1 -0
  10. package/dist/scripts/evaluate/evaluate-help-text.js +21 -0
  11. package/dist/scripts/evaluate/pair-evaluator.d.ts +1 -0
  12. package/dist/scripts/evaluate/pair-evaluator.js +14 -3
  13. package/dist/scripts/evaluate/parse-evaluate-cli-options.d.ts +2 -0
  14. package/dist/scripts/evaluate/parse-evaluate-cli-options.js +3 -1
  15. package/dist/scripts/evaluate/parse-transform-manifest.d.ts +1 -1
  16. package/dist/scripts/evaluate/parse-transform-manifest.js +1 -1
  17. package/dist/scripts/evaluate/relative-patch-path.d.ts +7 -0
  18. package/dist/scripts/evaluate/relative-patch-path.js +22 -0
  19. package/dist/scripts/evaluate/run-check-mode.d.ts +8 -0
  20. package/dist/scripts/evaluate/run-check-mode.js +116 -0
  21. package/dist/scripts/evaluate/run-evaluate-cli.js +11 -3
  22. package/dist/scripts/evaluate/run-evaluations.js +1 -0
  23. package/dist/scripts/evaluate/transform-content.d.ts +14 -0
  24. package/dist/scripts/evaluate/transform-content.js +31 -0
  25. package/dist/scripts/evaluate/transform-manifest.d.ts +0 -2
  26. package/dist/scripts/evaluate/transform-manifest.js +15 -34
  27. package/dist/transforms/expand-return-sequence/expand-return-sequence-transform.d.ts +2 -0
  28. package/dist/transforms/expand-return-sequence/expand-return-sequence-transform.js +91 -0
  29. package/dist/transforms/expand-sequence-expressions/expand-expression-statement-sequence.d.ts +3 -0
  30. package/dist/transforms/expand-sequence-expressions/expand-expression-statement-sequence.js +57 -0
  31. package/dist/transforms/expand-sequence-expressions/expand-sequence-expressions-transform.d.ts +2 -0
  32. package/dist/transforms/expand-sequence-expressions/expand-sequence-expressions-transform.js +34 -0
  33. package/dist/transforms/expand-sequence-expressions/expand-variable-declaration-sequence.d.ts +3 -0
  34. package/dist/transforms/expand-sequence-expressions/expand-variable-declaration-sequence.js +93 -0
  35. package/dist/transforms/expand-sequence-expressions-v2/expand-expression-statement-sequence.d.ts +3 -0
  36. package/dist/transforms/expand-sequence-expressions-v2/expand-expression-statement-sequence.js +55 -0
  37. package/dist/transforms/expand-sequence-expressions-v2/expand-return-sequence.d.ts +3 -0
  38. package/dist/transforms/expand-sequence-expressions-v2/expand-return-sequence.js +86 -0
  39. package/dist/transforms/expand-sequence-expressions-v2/expand-sequence-expressions-v2-transform.d.ts +2 -0
  40. package/dist/transforms/expand-sequence-expressions-v2/expand-sequence-expressions-v2-transform.js +41 -0
  41. package/dist/transforms/expand-sequence-expressions-v2/expand-variable-declaration-sequence.d.ts +3 -0
  42. package/dist/transforms/expand-sequence-expressions-v2/expand-variable-declaration-sequence.js +93 -0
  43. package/dist/transforms/expand-sequence-expressions-v3/expand-expression-statement-sequence.d.ts +3 -0
  44. package/dist/transforms/expand-sequence-expressions-v3/expand-expression-statement-sequence.js +64 -0
  45. package/dist/transforms/expand-sequence-expressions-v3/expand-return-sequence.d.ts +3 -0
  46. package/dist/transforms/expand-sequence-expressions-v3/expand-return-sequence.js +91 -0
  47. package/dist/transforms/expand-sequence-expressions-v3/expand-sequence-expressions-v3-transform.d.ts +2 -0
  48. package/dist/transforms/expand-sequence-expressions-v3/expand-sequence-expressions-v3-transform.js +48 -0
  49. package/dist/transforms/expand-sequence-expressions-v3/expand-throw-sequence.d.ts +3 -0
  50. package/dist/transforms/expand-sequence-expressions-v3/expand-throw-sequence.js +101 -0
  51. package/dist/transforms/expand-sequence-expressions-v3/expand-variable-declaration-sequence.d.ts +3 -0
  52. package/dist/transforms/expand-sequence-expressions-v3/expand-variable-declaration-sequence.js +99 -0
  53. package/dist/transforms/expand-throw-sequence/expand-throw-sequence-transform.d.ts +2 -0
  54. package/dist/transforms/expand-throw-sequence/expand-throw-sequence-transform.js +117 -0
  55. package/dist/transforms/rename-destructured-aliases/binding-context.d.ts +7 -0
  56. package/dist/transforms/rename-destructured-aliases/binding-context.js +83 -0
  57. package/dist/transforms/rename-destructured-aliases/rename-destructured-aliases-transform.d.ts +2 -0
  58. package/dist/transforms/rename-destructured-aliases/rename-destructured-aliases-transform.js +118 -0
  59. package/dist/transforms/rename-promise-executor-parameters/is-valid-binding-identifier.d.ts +1 -0
  60. package/dist/transforms/rename-promise-executor-parameters/is-valid-binding-identifier.js +10 -0
  61. package/dist/transforms/rename-promise-executor-parameters/promise-executor-heuristics.d.ts +6 -0
  62. package/dist/transforms/rename-promise-executor-parameters/promise-executor-heuristics.js +88 -0
  63. package/dist/transforms/rename-promise-executor-parameters/rename-binding-if-safe.d.ts +3 -0
  64. package/dist/transforms/rename-promise-executor-parameters/rename-binding-if-safe.js +40 -0
  65. package/dist/transforms/rename-promise-executor-parameters/rename-promise-executor-parameters-transform.d.ts +2 -0
  66. package/dist/transforms/rename-promise-executor-parameters/rename-promise-executor-parameters-transform.js +76 -0
  67. package/dist/transforms/rename-use-reference-guards/get-member-expression-for-reference.d.ts +3 -0
  68. package/dist/transforms/rename-use-reference-guards/get-member-expression-for-reference.js +10 -0
  69. package/dist/transforms/rename-use-reference-guards/get-reference-usage.d.ts +9 -0
  70. package/dist/transforms/rename-use-reference-guards/get-reference-usage.js +47 -0
  71. package/dist/transforms/rename-use-reference-guards/get-target-name.d.ts +2 -0
  72. package/dist/transforms/rename-use-reference-guards/get-target-name.js +23 -0
  73. package/dist/transforms/rename-use-reference-guards/is-use-reference-false-initializer.d.ts +2 -0
  74. package/dist/transforms/rename-use-reference-guards/is-use-reference-false-initializer.js +25 -0
  75. package/dist/transforms/rename-use-reference-guards/is-valid-binding-identifier.d.ts +1 -0
  76. package/dist/transforms/rename-use-reference-guards/is-valid-binding-identifier.js +10 -0
  77. package/dist/transforms/rename-use-reference-guards/rename-use-reference-guards-transform.d.ts +3 -0
  78. package/dist/transforms/rename-use-reference-guards/rename-use-reference-guards-transform.js +55 -0
  79. package/dist/transforms/split-variable-declarations/create-variable-declaration.d.ts +2 -0
  80. package/dist/transforms/split-variable-declarations/create-variable-declaration.js +28 -0
  81. package/dist/transforms/split-variable-declarations/is-safe-to-split-variable-declaration.d.ts +3 -0
  82. package/dist/transforms/split-variable-declarations/is-safe-to-split-variable-declaration.js +52 -0
  83. package/dist/transforms/split-variable-declarations/split-variable-declarations-transform.d.ts +2 -0
  84. package/dist/transforms/split-variable-declarations/split-variable-declarations-transform.js +81 -0
  85. package/dist/transforms/transform-registry.js +18 -4
  86. package/package.json +29 -23
  87. package/transform-manifest.json +96 -26
  88. package/dist/transforms/add-prefix/add-prefix-transform.d.ts +0 -2
  89. package/dist/transforms/add-prefix/add-prefix-transform.js +0 -40
  90. package/dist/transforms/add-suffix/add-suffix-transform.d.ts +0 -2
  91. package/dist/transforms/add-suffix/add-suffix-transform.js +0 -40
package/README.md CHANGED
@@ -74,10 +74,10 @@ The `miniread-evaluate` CLI compares transform configurations by measuring diff
74
74
  pnpm run miniread-evaluate -- --help
75
75
 
76
76
  # Compare baseline none vs a transform
77
- pnpm run miniread-evaluate -- --compare v1.0:v1.1 --baseline none --test add-prefix
77
+ pnpm run miniread-evaluate -- --compare v1.0:v1.1 --baseline none --test rename-destructured-aliases
78
78
 
79
79
  # Pipeline example: sort by reduction ratio (best first)
80
- pnpm run miniread-evaluate -- --compare v1.0:v1.1 --compare v2.0:v2.1 --baseline none --test add-prefix \
80
+ pnpm run miniread-evaluate -- --compare v1.0:v1.1 --compare v2.0:v2.1 --baseline none --test rename-destructured-aliases \
81
81
  | tail -n +2 | sort -k5,5nr | head -10
82
82
  ```
83
83
 
@@ -0,0 +1,17 @@
1
+ import type { SnapshotIssue } from "./check-snapshots.js";
2
+ type MissingManifestEntry = {
3
+ transformId: string;
4
+ path: string;
5
+ };
6
+ type CheckExpectedEvaluationsResult = {
7
+ transformIds: string[];
8
+ missingManifestEntries: MissingManifestEntry[];
9
+ snapshotIssues: SnapshotIssue[];
10
+ allPresent: boolean;
11
+ };
12
+ type CheckExpectedEvaluationsOptions = {
13
+ manifestPath: string;
14
+ testCasesDirectory: string;
15
+ };
16
+ export declare const checkExpectedEvaluations: (options: CheckExpectedEvaluationsOptions) => Promise<CheckExpectedEvaluationsResult>;
17
+ export {};
@@ -0,0 +1,35 @@
1
+ import * as fs from "node:fs/promises";
2
+ import { allTransformIds } from "../../transforms/transform-registry.js";
3
+ import { transformRegistry } from "../../transforms/transform-registry.js";
4
+ import { parseTransformManifest } from "./parse-transform-manifest.js";
5
+ import { checkAllSnapshots } from "./check-snapshots.js";
6
+ export const checkExpectedEvaluations = async (options) => {
7
+ const { manifestPath, testCasesDirectory } = options;
8
+ const transformIds = allTransformIds.toSorted();
9
+ const missingManifestEntries = [];
10
+ // Check manifest entries
11
+ const manifestContent = await fs.readFile(manifestPath, "utf8");
12
+ const manifest = parseTransformManifest(manifestContent);
13
+ const manifestIds = new Set(manifest.transforms.map((t) => t.id));
14
+ for (const transformId of transformIds) {
15
+ if (!manifestIds.has(transformId)) {
16
+ missingManifestEntries.push({
17
+ transformId,
18
+ path: manifestPath,
19
+ });
20
+ }
21
+ }
22
+ // Check test-cases matrix with smart snapshot detection
23
+ const snapshotIssues = await checkAllSnapshots({
24
+ testCasesDirectory,
25
+ transformIds,
26
+ transformRegistry,
27
+ });
28
+ const allPresent = missingManifestEntries.length === 0 && snapshotIssues.length === 0;
29
+ return {
30
+ transformIds,
31
+ missingManifestEntries,
32
+ snapshotIssues,
33
+ allPresent,
34
+ };
35
+ };
@@ -0,0 +1,14 @@
1
+ import type { Transform } from "../../core/types.js";
2
+ export type SnapshotIssue = {
3
+ transformId: string;
4
+ testCase: string;
5
+ snapshotPath: string;
6
+ issue: "missing-required" | "unnecessary" | "outdated";
7
+ };
8
+ type CheckAllSnapshotsOptions = {
9
+ testCasesDirectory: string;
10
+ transformIds: string[];
11
+ transformRegistry: Record<string, Transform>;
12
+ };
13
+ export declare const checkAllSnapshots: (options: CheckAllSnapshotsOptions) => Promise<SnapshotIssue[]>;
14
+ export {};
@@ -0,0 +1,111 @@
1
+ import * as fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import * as prettier from "prettier";
4
+ import { transformContent } from "./transform-content.js";
5
+ const formatCode = async (code) => {
6
+ return prettier.format(code, { parser: "babel" });
7
+ };
8
+ const fileExists = async (filePath) => {
9
+ try {
10
+ await fs.access(filePath);
11
+ return true;
12
+ }
13
+ catch {
14
+ return false;
15
+ }
16
+ };
17
+ const checkSnapshot = async (options) => {
18
+ const { basePath, baseContent, formattedBase, snapshotPath, transformId, testCaseName, transform, } = options;
19
+ const snapshotExists = await fileExists(snapshotPath);
20
+ if (snapshotExists) {
21
+ const snapshotContent = await fs.readFile(snapshotPath, "utf8");
22
+ const formattedSnapshot = await formatCode(snapshotContent);
23
+ // Check if snapshot is unnecessary (formatted snapshot equals formatted base)
24
+ if (formattedSnapshot === formattedBase) {
25
+ return {
26
+ transformId,
27
+ testCase: testCaseName,
28
+ snapshotPath,
29
+ issue: "unnecessary",
30
+ };
31
+ }
32
+ if (transform) {
33
+ // Check if snapshot is outdated (formatted snapshot differs from current transform output)
34
+ const result = await transformContent({
35
+ content: baseContent,
36
+ filePath: basePath,
37
+ transform,
38
+ });
39
+ if (result.ok) {
40
+ const formattedOutput = await formatCode(result.output);
41
+ if (formattedOutput !== formattedSnapshot) {
42
+ return {
43
+ transformId,
44
+ testCase: testCaseName,
45
+ snapshotPath,
46
+ issue: "outdated",
47
+ };
48
+ }
49
+ }
50
+ }
51
+ }
52
+ else if (transform) {
53
+ // Check if snapshot is required (formatted transform output differs from formatted base)
54
+ const result = await transformContent({
55
+ content: baseContent,
56
+ filePath: basePath,
57
+ transform,
58
+ });
59
+ if (result.ok) {
60
+ const formattedOutput = await formatCode(result.output);
61
+ if (formattedOutput !== formattedBase) {
62
+ return {
63
+ transformId,
64
+ testCase: testCaseName,
65
+ snapshotPath,
66
+ issue: "missing-required",
67
+ };
68
+ }
69
+ }
70
+ }
71
+ return undefined;
72
+ };
73
+ const getTestCaseNames = async (testCasesDirectory) => {
74
+ const entries = await fs.readdir(testCasesDirectory, { withFileTypes: true });
75
+ const testCaseNames = [];
76
+ for (const entry of entries) {
77
+ if (!entry.isDirectory())
78
+ continue;
79
+ const basePath = path.join(testCasesDirectory, entry.name, "base.js");
80
+ if (await fileExists(basePath)) {
81
+ testCaseNames.push(entry.name);
82
+ }
83
+ }
84
+ return testCaseNames.toSorted();
85
+ };
86
+ export const checkAllSnapshots = async (options) => {
87
+ const { testCasesDirectory, transformIds, transformRegistry } = options;
88
+ const snapshotIssues = [];
89
+ const testCaseNames = await getTestCaseNames(testCasesDirectory);
90
+ for (const testCaseName of testCaseNames) {
91
+ const basePath = path.join(testCasesDirectory, testCaseName, "base.js");
92
+ const baseContent = await fs.readFile(basePath, "utf8");
93
+ const formattedBase = await formatCode(baseContent);
94
+ for (const transformId of transformIds) {
95
+ const snapshotPath = path.join(testCasesDirectory, testCaseName, `${transformId}.js`);
96
+ const issue = await checkSnapshot({
97
+ basePath,
98
+ baseContent,
99
+ formattedBase,
100
+ snapshotPath,
101
+ transformId,
102
+ testCaseName,
103
+ transform: transformRegistry[transformId],
104
+ });
105
+ if (issue) {
106
+ snapshotIssues.push(issue);
107
+ }
108
+ }
109
+ }
110
+ return snapshotIssues;
111
+ };
@@ -0,0 +1,2 @@
1
+ import type { EvaluationReport } from "./evaluation-report.js";
2
+ export declare const cloneMetricsReport: (report: EvaluationReport) => EvaluationReport;
@@ -0,0 +1,6 @@
1
+ export const cloneMetricsReport = (report) => ({
2
+ ...report,
3
+ results: report.results.map((result) => ({
4
+ ...result,
5
+ })),
6
+ });
@@ -1,4 +1,5 @@
1
1
  import { Command, InvalidArgumentError } from "@commander-js/extra-typings";
2
+ import { evaluateHelpText } from "./evaluate-help-text.js";
2
3
  const parseOutputFormat = (value) => {
3
4
  if (value === "tsv" || value === "json")
4
5
  return value;
@@ -21,27 +22,11 @@ export const createEvaluateCommand = (options) => {
21
22
  .option("--write-metrics", "Write metrics JSON to the evaluation directory")
22
23
  .option("--metrics-file <path>", "Write metrics JSON to a specific path")
23
24
  .option("--write-patches", "Write baseline/test diff patches to the evaluation directory")
24
- .option("-u, --update-manifest", "Update transform-manifest.json based on results")
25
+ .option("-u, --update-manifest", "Update transform-manifest.json metrics (does not add new entries or edit notes)")
25
26
  .option("--manifest-file <path>", "Path to transform-manifest.json", "transform-manifest.json")
26
27
  .option("-f, --overwrite", "Overwrite existing metrics/patch files")
27
- .option("--no-format-code", "Skip formatting temp directories with Prettier before diffing");
28
- program.addHelpText("after", `
29
- Dependencies:
30
- - diff (override: MINIREAD_EVALUATE_DIFF_PATH)
31
- - pnpm + prettier (used unless --no-format-code; overrides: MINIREAD_EVALUATE_PNPM_PATH, MINIREAD_EVALUATE_PRETTIER_PATH)
32
-
33
- Examples:
34
- # Compare baseline none vs a transform
35
- miniread-evaluate --compare v1.0:v1.1 --baseline none --test add-prefix
36
-
37
- # Multiple pairs, output JSON and inspect with jq
38
- miniread-evaluate --compare v1.0:v1.1 --compare v2.0:v2.1 --baseline none --test add-prefix --format json | jq '.results[] | {pair, reductionRatio}'
39
-
40
- # Pipeline example: sort by reduction ratio (best first)
41
- miniread-evaluate --compare v1.0:v1.1 --compare v2.0:v2.1 --baseline none --test add-prefix | tail -n +2 | sort -k5,5nr | head -10
42
-
43
- # Write patches + metrics (refuses to overwrite unless -f/--overwrite)
44
- miniread-evaluate --compare v1.0:v1.1 --baseline none --test add-prefix --write-patches --write-metrics
45
- `);
28
+ .option("--no-format-code", "Skip formatting temp directories with Prettier before diffing")
29
+ .option("--check", "Check that all expected evaluation metrics files exist (does not run evaluations)");
30
+ program.addHelpText("after", evaluateHelpText);
46
31
  return program;
47
32
  };
@@ -0,0 +1 @@
1
+ export declare const evaluateHelpText = "\nDependencies:\n - diff (override: MINIREAD_EVALUATE_DIFF_PATH)\n - pnpm + prettier (used unless --no-format-code; overrides: MINIREAD_EVALUATE_PNPM_PATH, MINIREAD_EVALUATE_PRETTIER_PATH)\n\nExamples:\n # Compare baseline none vs a transform\n miniread-evaluate --compare v1.0:v1.1 --baseline none --test rename-destructured-aliases\n\n # Multiple pairs, output JSON and inspect with jq\n miniread-evaluate --compare v1.0:v1.1 --compare v2.0:v2.1 --baseline none --test rename-destructured-aliases --format json | jq '.results[] | {pair, reductionRatio}'\n\n # Pipeline example: sort by reduction ratio (best first)\n miniread-evaluate --compare v1.0:v1.1 --compare v2.0:v2.1 --baseline none --test rename-destructured-aliases | tail -n +2 | sort -k5,5nr | head -10\n\n # Write patches + metrics (refuses to overwrite unless -f/--overwrite)\n miniread-evaluate --compare v1.0:v1.1 --baseline none --test rename-destructured-aliases --write-patches --write-metrics\n\n # Check that all expected evaluation metrics files exist\n miniread-evaluate --check\n";
@@ -0,0 +1,21 @@
1
+ export const evaluateHelpText = `
2
+ Dependencies:
3
+ - diff (override: MINIREAD_EVALUATE_DIFF_PATH)
4
+ - pnpm + prettier (used unless --no-format-code; overrides: MINIREAD_EVALUATE_PNPM_PATH, MINIREAD_EVALUATE_PRETTIER_PATH)
5
+
6
+ Examples:
7
+ # Compare baseline none vs a transform
8
+ miniread-evaluate --compare v1.0:v1.1 --baseline none --test rename-destructured-aliases
9
+
10
+ # Multiple pairs, output JSON and inspect with jq
11
+ miniread-evaluate --compare v1.0:v1.1 --compare v2.0:v2.1 --baseline none --test rename-destructured-aliases --format json | jq '.results[] | {pair, reductionRatio}'
12
+
13
+ # Pipeline example: sort by reduction ratio (best first)
14
+ miniread-evaluate --compare v1.0:v1.1 --compare v2.0:v2.1 --baseline none --test rename-destructured-aliases | tail -n +2 | sort -k5,5nr | head -10
15
+
16
+ # Write patches + metrics (refuses to overwrite unless -f/--overwrite)
17
+ miniread-evaluate --compare v1.0:v1.1 --baseline none --test rename-destructured-aliases --write-patches --write-metrics
18
+
19
+ # Check that all expected evaluation metrics files exist
20
+ miniread-evaluate --check
21
+ `;
@@ -5,6 +5,7 @@ type EvaluatePairOptions = {
5
5
  testTransforms: string[];
6
6
  sourcesDirectory: string;
7
7
  rootDirectory: string;
8
+ evaluationDirectory: string;
8
9
  nodePath: string;
9
10
  minireadCliPath: string;
10
11
  diffPath: string;
@@ -3,11 +3,12 @@ import path from "node:path";
3
3
  import envPaths from "env-paths";
4
4
  import { computeDirectoryDiff } from "./diff-utilities.js";
5
5
  import { calculateReductionRatio, formatDiffStats } from "./metrics.js";
6
+ import { getRelativePatchPath } from "./relative-patch-path.js";
6
7
  import { formatDirectoryWithPrettier } from "./shell-utilities.js";
7
8
  import { runPairTransformations } from "./run-pair-transformations.js";
8
9
  const appPaths = envPaths("miniread");
9
10
  export const evaluatePair = async (options) => {
10
- const { pair, baselineTransforms, testTransforms, sourcesDirectory, rootDirectory, nodePath, minireadCliPath, diffPath, pnpmPath, prettierPath, formatCode, baselinePatchPath, testPatchPath, overwrite, verbose, log, } = options;
11
+ const { pair, baselineTransforms, testTransforms, sourcesDirectory, rootDirectory, evaluationDirectory, nodePath, minireadCliPath, diffPath, pnpmPath, prettierPath, formatCode, baselinePatchPath, testPatchPath, overwrite, verbose, log, } = options;
11
12
  if (verbose) {
12
13
  log(`Evaluating: ${pair.from} -> ${pair.to}`);
13
14
  }
@@ -86,6 +87,16 @@ export const evaluatePair = async (options) => {
86
87
  const status = reductionRatio >= 0 ? "GOOD" : "BAD";
87
88
  if (verbose)
88
89
  log(`Reduction ratio: ${percentReduction}% (${status})`);
90
+ const reportBaselinePatchPath = getRelativePatchPath({
91
+ rootDirectory,
92
+ evaluationDirectory,
93
+ patchPath: baselinePatchPath,
94
+ });
95
+ const reportTestPatchPath = getRelativePatchPath({
96
+ rootDirectory,
97
+ evaluationDirectory,
98
+ patchPath: testPatchPath,
99
+ });
89
100
  return {
90
101
  timestamp: new Date().toISOString(),
91
102
  pair,
@@ -94,8 +105,8 @@ export const evaluatePair = async (options) => {
94
105
  baselineDiff,
95
106
  testDiff,
96
107
  reductionRatio,
97
- baselinePatchPath,
98
- testPatchPath,
108
+ baselinePatchPath: reportBaselinePatchPath,
109
+ testPatchPath: reportTestPatchPath,
99
110
  };
100
111
  }
101
112
  finally {
@@ -15,9 +15,11 @@ export type EvaluateCliRawOptions = {
15
15
  manifestFile: string;
16
16
  overwrite?: boolean;
17
17
  formatCode: boolean;
18
+ check?: boolean;
18
19
  };
19
20
  export type EvaluateCliOptions = {
20
21
  verbose: boolean;
22
+ checkMode: boolean;
21
23
  pairs: ComparisonPair[];
22
24
  baselineTransforms: string[];
23
25
  testTransforms: string[];
@@ -53,7 +53,8 @@ const parseComparePair = (value) => {
53
53
  };
54
54
  export const parseEvaluateCliOptions = (options) => {
55
55
  const { cwd, rawOptions } = options;
56
- if (rawOptions.compare.length === 0) {
56
+ const checkMode = rawOptions.check ?? false;
57
+ if (!checkMode && rawOptions.compare.length === 0) {
57
58
  throw new Error("at least one --compare flag is required");
58
59
  }
59
60
  const pairs = [];
@@ -78,6 +79,7 @@ export const parseEvaluateCliOptions = (options) => {
78
79
  : path.join(evaluationDirectory, `metrics-${baselineSlug}-vs-${testSlug}.json`);
79
80
  return {
80
81
  verbose,
82
+ checkMode,
81
83
  pairs,
82
84
  baselineTransforms,
83
85
  testTransforms,
@@ -2,11 +2,11 @@ import type { Transform } from "../../core/types.js";
2
2
  type TransformManifestEntry = {
3
3
  id: string;
4
4
  description: string;
5
- iteration: number;
6
5
  scope: Transform["scope"];
7
6
  parallelizable: boolean;
8
7
  diffReductionImpact: number;
9
8
  enabledByDefault: boolean;
9
+ evaluatedAt?: string;
10
10
  notes?: string;
11
11
  supersededBy?: string;
12
12
  };
@@ -3,11 +3,11 @@ const TransformManifestEntrySchema = z
3
3
  .object({
4
4
  id: z.string().min(1),
5
5
  description: z.string(),
6
- iteration: z.number().int().nonnegative(),
7
6
  scope: z.union([z.literal("file"), z.literal("project")]),
8
7
  parallelizable: z.boolean(),
9
8
  diffReductionImpact: z.number(),
10
9
  enabledByDefault: z.boolean(),
10
+ evaluatedAt: z.iso.datetime().optional(),
11
11
  notes: z.string().optional(),
12
12
  supersededBy: z.string().min(1).optional(),
13
13
  })
@@ -0,0 +1,7 @@
1
+ type RelativePatchPathOptions = {
2
+ rootDirectory: string;
3
+ evaluationDirectory: string;
4
+ patchPath: string | undefined;
5
+ };
6
+ export declare const getRelativePatchPath: (options: RelativePatchPathOptions) => string | undefined;
7
+ export {};
@@ -0,0 +1,22 @@
1
+ import path from "node:path";
2
+ export const getRelativePatchPath = (options) => {
3
+ const { rootDirectory, evaluationDirectory, patchPath } = options;
4
+ if (!patchPath)
5
+ return;
6
+ const relativePath = path.relative(rootDirectory, patchPath);
7
+ if (relativePath === "" || relativePath === ".")
8
+ return;
9
+ // Avoid leaking paths outside the repo when evaluation output is elsewhere.
10
+ if (!path.isAbsolute(relativePath) &&
11
+ relativePath !== ".." &&
12
+ !relativePath.startsWith(`..${path.sep}`)) {
13
+ return relativePath;
14
+ }
15
+ const evaluationRelativePath = path.relative(evaluationDirectory, patchPath);
16
+ if (!path.isAbsolute(evaluationRelativePath) &&
17
+ evaluationRelativePath !== ".." &&
18
+ !evaluationRelativePath.startsWith(`..${path.sep}`)) {
19
+ return evaluationRelativePath;
20
+ }
21
+ return path.basename(patchPath);
22
+ };
@@ -0,0 +1,8 @@
1
+ import type { OutputFormat } from "./create-evaluate-command.js";
2
+ type RunCheckModeOptions = {
3
+ manifestPath: string;
4
+ rootDirectory: string;
5
+ format: OutputFormat;
6
+ };
7
+ export declare const runCheckMode: (options: RunCheckModeOptions) => Promise<number>;
8
+ export {};
@@ -0,0 +1,116 @@
1
+ import path from "node:path";
2
+ import { checkExpectedEvaluations } from "./check-expected-evaluations.js";
3
+ export const runCheckMode = async (options) => {
4
+ const { manifestPath, rootDirectory, format } = options;
5
+ const checkResult = await checkExpectedEvaluations({
6
+ manifestPath,
7
+ testCasesDirectory: path.join(rootDirectory, "test-cases"),
8
+ });
9
+ if (format === "json") {
10
+ console.log(JSON.stringify(checkResult, undefined, 2));
11
+ }
12
+ else {
13
+ const allIssues = [
14
+ ...checkResult.missingManifestEntries.map((item) => ({
15
+ type: "manifest",
16
+ transformId: item.transformId,
17
+ testCase: "",
18
+ path: item.path,
19
+ })),
20
+ ...checkResult.snapshotIssues.map((item) => ({
21
+ type: item.issue,
22
+ transformId: item.transformId,
23
+ testCase: item.testCase,
24
+ path: item.snapshotPath,
25
+ })),
26
+ ];
27
+ if (allIssues.length > 0) {
28
+ console.log("type\ttransform_id\ttest_case\tpath");
29
+ for (const item of allIssues) {
30
+ console.log(`${item.type}\t${item.transformId}\t${item.testCase}\t${item.path}`);
31
+ }
32
+ }
33
+ }
34
+ let exitCode = 0;
35
+ if (checkResult.missingManifestEntries.length > 0) {
36
+ console.error(`\n[MISSING MANIFEST ENTRIES] ${checkResult.missingManifestEntries.length} transform(s) not in transform-manifest.json:`);
37
+ for (const item of checkResult.missingManifestEntries) {
38
+ console.error(` - ${item.transformId}`);
39
+ }
40
+ console.error(`\n To fix: Add entries manually to transform-manifest.json, or run evaluation with --update-manifest`);
41
+ exitCode = 1;
42
+ }
43
+ const missingSnapshots = checkResult.snapshotIssues.filter((issue) => issue.issue === "missing-required");
44
+ const unnecessarySnapshots = checkResult.snapshotIssues.filter((issue) => issue.issue === "unnecessary");
45
+ const outdatedSnapshots = checkResult.snapshotIssues.filter((issue) => issue.issue === "outdated");
46
+ if (missingSnapshots.length > 0) {
47
+ console.error(`\n[MISSING SNAPSHOTS] ${missingSnapshots.length} snapshot(s) required but missing:`);
48
+ const byTransform = new Map();
49
+ for (const item of missingSnapshots) {
50
+ const testCases = byTransform.get(item.transformId);
51
+ if (testCases) {
52
+ testCases.push(item.testCase);
53
+ }
54
+ else {
55
+ byTransform.set(item.transformId, [item.testCase]);
56
+ }
57
+ }
58
+ for (const [transformId, testCases] of byTransform) {
59
+ console.error(` - ${transformId}: ${testCases.join(", ")}`);
60
+ }
61
+ console.error(`\n To fix: Generate snapshots for each transform:`);
62
+ console.error(` for testcase in test-cases/*/; do`);
63
+ console.error(` cat "\${testcase}base.js" | pnpm -s run miniread -- --transforms <transform-id> \\`);
64
+ console.error(` | pnpm exec prettier --stdin-filepath test.js --parser babel \\`);
65
+ console.error(` > "\${testcase}<transform-id>.js"`);
66
+ console.error(` done`);
67
+ exitCode = 1;
68
+ }
69
+ if (unnecessarySnapshots.length > 0) {
70
+ console.error(`\n[UNNECESSARY SNAPSHOTS] ${unnecessarySnapshots.length} snapshot(s) identical to base.js (should be removed):`);
71
+ const byTransform = new Map();
72
+ for (const item of unnecessarySnapshots) {
73
+ const testCases = byTransform.get(item.transformId);
74
+ if (testCases) {
75
+ testCases.push(item.testCase);
76
+ }
77
+ else {
78
+ byTransform.set(item.transformId, [item.testCase]);
79
+ }
80
+ }
81
+ for (const [transformId, testCases] of byTransform) {
82
+ console.error(` - ${transformId}: ${testCases.join(", ")}`);
83
+ }
84
+ console.error(`\n To fix: Remove unnecessary snapshots:`);
85
+ for (const item of unnecessarySnapshots) {
86
+ console.error(` rm "${item.snapshotPath}"`);
87
+ }
88
+ exitCode = 1;
89
+ }
90
+ if (outdatedSnapshots.length > 0) {
91
+ console.error(`\n[OUTDATED SNAPSHOTS] ${outdatedSnapshots.length} snapshot(s) don't match current transform output:`);
92
+ const byTransform = new Map();
93
+ for (const item of outdatedSnapshots) {
94
+ const testCases = byTransform.get(item.transformId);
95
+ if (testCases) {
96
+ testCases.push(item.testCase);
97
+ }
98
+ else {
99
+ byTransform.set(item.transformId, [item.testCase]);
100
+ }
101
+ }
102
+ for (const [transformId, testCases] of byTransform) {
103
+ console.error(` - ${transformId}: ${testCases.join(", ")}`);
104
+ }
105
+ console.error(`\n To fix: Regenerate outdated snapshots:`);
106
+ for (const item of outdatedSnapshots) {
107
+ const testCaseDirectory = path.dirname(item.snapshotPath);
108
+ console.error(` cat "${testCaseDirectory}/base.js" | pnpm -s run miniread -- --transforms ${item.transformId} | pnpm exec prettier --stdin-filepath test.js --parser babel > "${item.snapshotPath}"`);
109
+ }
110
+ exitCode = 1;
111
+ }
112
+ if (exitCode === 0) {
113
+ console.error(`\nAll checks passed: ${checkResult.transformIds.length} transforms verified.`);
114
+ }
115
+ return exitCode;
116
+ };
@@ -1,12 +1,13 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import packageJson from "../../../package.json" with { type: "json" };
3
- import { transformRegistry } from "../../transforms/transform-registry.js";
4
3
  import { createEvaluateCommand } from "./create-evaluate-command.js";
5
4
  import { parseEvaluateCliOptions, } from "./parse-evaluate-cli-options.js";
6
5
  import { resolveEvaluateDependencies } from "./resolve-evaluate-dependencies.js";
6
+ import { runCheckMode } from "./run-check-mode.js";
7
7
  import { runEvaluations } from "./run-evaluations.js";
8
8
  import { writeEvaluationStdout } from "./write-evaluation-stdout.js";
9
9
  import { updateTransformManifestFromEvaluation } from "./transform-manifest.js";
10
+ import { cloneMetricsReport } from "./clone-metrics-report.js";
10
11
  import { writeJsonFileAtomic } from "./write-text-file-atomic.js";
11
12
  export const runEvaluateCli = async (argv) => {
12
13
  const program = createEvaluateCommand({ version: packageJson.version });
@@ -32,6 +33,13 @@ export const runEvaluateCli = async (argv) => {
32
33
  return;
33
34
  console.error(message);
34
35
  };
36
+ if (parsedOptions.checkMode) {
37
+ return runCheckMode({
38
+ manifestPath: parsedOptions.manifestPath,
39
+ rootDirectory: process.cwd(),
40
+ format: parsedOptions.format,
41
+ });
42
+ }
35
43
  let dependencies;
36
44
  try {
37
45
  dependencies = resolveEvaluateDependencies({
@@ -78,7 +86,8 @@ export const runEvaluateCli = async (argv) => {
78
86
  });
79
87
  if (parsedOptions.shouldWriteMetrics) {
80
88
  try {
81
- await writeJsonFileAtomic(parsedOptions.metricsPath, report, {
89
+ const sanitizedReport = cloneMetricsReport(report);
90
+ await writeJsonFileAtomic(parsedOptions.metricsPath, sanitizedReport, {
82
91
  overwrite: parsedOptions.overwrite,
83
92
  });
84
93
  log(`Wrote ${parsedOptions.metricsPath}`);
@@ -100,7 +109,6 @@ export const runEvaluateCli = async (argv) => {
100
109
  baselineTransforms: parsedOptions.baselineTransforms,
101
110
  testTransforms: parsedOptions.testTransforms,
102
111
  reductionRatios: results.map((r) => r.reductionRatio),
103
- transformRegistry,
104
112
  });
105
113
  console.error(updateResult.message);
106
114
  }
@@ -23,6 +23,7 @@ export const runEvaluations = async (options) => {
23
23
  testTransforms,
24
24
  sourcesDirectory,
25
25
  rootDirectory,
26
+ evaluationDirectory,
26
27
  nodePath: process.execPath,
27
28
  minireadCliPath: process.env.MINIREAD_EVALUATE_MINIREAD_PATH ?? "bin/miniread",
28
29
  diffPath: dependencies.diffPath,
@@ -0,0 +1,14 @@
1
+ import type { Transform } from "../../core/types.js";
2
+ type TransformContentResult = {
3
+ ok: true;
4
+ output: string;
5
+ } | {
6
+ ok: false;
7
+ error: string;
8
+ };
9
+ export declare const transformContent: (options: {
10
+ content: string;
11
+ filePath: string;
12
+ transform: Transform;
13
+ }) => Promise<TransformContentResult>;
14
+ export {};
@@ -0,0 +1,31 @@
1
+ import { buildProjectGraph } from "../../core/project-graph.js";
2
+ import { generateCode } from "../../cli/generate-code.js";
3
+ export const transformContent = async (options) => {
4
+ const { content, filePath, transform } = options;
5
+ const projectGraph = buildProjectGraph([{ path: filePath, content }]);
6
+ const file = projectGraph.files.get(filePath);
7
+ if (!file) {
8
+ return { ok: false, error: "Failed to parse input" };
9
+ }
10
+ try {
11
+ await transform.transform({
12
+ projectGraph,
13
+ currentFile: undefined,
14
+ options: {},
15
+ });
16
+ }
17
+ catch (error) {
18
+ const message = error instanceof Error ? error.message : String(error);
19
+ return {
20
+ ok: false,
21
+ error: `Transform '${transform.id}' failed: ${message}`,
22
+ };
23
+ }
24
+ const generated = generateCode(file.ast);
25
+ const output = generated === ""
26
+ ? ""
27
+ : generated.endsWith("\n")
28
+ ? generated
29
+ : `${generated}\n`;
30
+ return { ok: true, output };
31
+ };
@@ -1,10 +1,8 @@
1
- import type { Transform } from "../../core/types.js";
2
1
  type UpdateTransformManifestOptions = {
3
2
  manifestPath: string;
4
3
  baselineTransforms: string[];
5
4
  testTransforms: string[];
6
5
  reductionRatios: number[];
7
- transformRegistry: Record<string, Transform>;
8
6
  };
9
7
  export declare const updateTransformManifestFromEvaluation: (options: UpdateTransformManifestOptions) => Promise<{
10
8
  updated: boolean;