repro-surgeon 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.
Files changed (111) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/CODE_OF_CONDUCT.md +7 -0
  3. package/CONTRIBUTING.md +31 -0
  4. package/LICENSE +21 -0
  5. package/README.md +159 -0
  6. package/SECURITY.md +15 -0
  7. package/dist/cli.d.ts +3 -0
  8. package/dist/cli.js +165 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/config.d.ts +3 -0
  11. package/dist/config.js +177 -0
  12. package/dist/config.js.map +1 -0
  13. package/dist/dependencies.d.ts +37 -0
  14. package/dist/dependencies.js +237 -0
  15. package/dist/dependencies.js.map +1 -0
  16. package/dist/engine.d.ts +25 -0
  17. package/dist/engine.js +327 -0
  18. package/dist/engine.js.map +1 -0
  19. package/dist/export.d.ts +7 -0
  20. package/dist/export.js +520 -0
  21. package/dist/export.js.map +1 -0
  22. package/dist/graph.d.ts +3 -0
  23. package/dist/graph.js +247 -0
  24. package/dist/graph.js.map +1 -0
  25. package/dist/index.d.ts +7 -0
  26. package/dist/index.js +7 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/oracle.d.ts +2 -0
  29. package/dist/oracle.js +81 -0
  30. package/dist/oracle.js.map +1 -0
  31. package/dist/protection.d.ts +2 -0
  32. package/dist/protection.js +6 -0
  33. package/dist/protection.js.map +1 -0
  34. package/dist/report.d.ts +5 -0
  35. package/dist/report.js +539 -0
  36. package/dist/report.js.map +1 -0
  37. package/dist/runner.d.ts +3 -0
  38. package/dist/runner.js +195 -0
  39. package/dist/runner.js.map +1 -0
  40. package/dist/snapshot.d.ts +11 -0
  41. package/dist/snapshot.js +220 -0
  42. package/dist/snapshot.js.map +1 -0
  43. package/dist/state.d.ts +9 -0
  44. package/dist/state.js +143 -0
  45. package/dist/state.js.map +1 -0
  46. package/dist/transforms.d.ts +3 -0
  47. package/dist/transforms.js +188 -0
  48. package/dist/transforms.js.map +1 -0
  49. package/dist/types.d.ts +207 -0
  50. package/dist/types.js +2 -0
  51. package/dist/types.js.map +1 -0
  52. package/docs/architecture.md +91 -0
  53. package/docs/assets/report.png +0 -0
  54. package/docs/configuration.md +73 -0
  55. package/docs/evidence/dependency-removal.json +109 -0
  56. package/docs/evidence/next-frameworks.json +271 -0
  57. package/docs/evidence/next-pages.json +152 -0
  58. package/docs/evidence/rounding.json +46 -0
  59. package/docs/report-surface.md +22 -0
  60. package/docs/troubleshooting.md +47 -0
  61. package/docs/validation.md +103 -0
  62. package/examples/next-container/README.md +7 -0
  63. package/examples/next-container/app/about/page.jsx +1 -0
  64. package/examples/next-container/app/case.css +5 -0
  65. package/examples/next-container/app/layout.jsx +3 -0
  66. package/examples/next-container/app/page.jsx +4 -0
  67. package/examples/next-container/lib/catalog.js +2 -0
  68. package/examples/next-container/next.config.mjs +1 -0
  69. package/examples/next-container/package-lock.json +952 -0
  70. package/examples/next-container/package.json +13 -0
  71. package/examples/next-container/repro-surgeon.json +37 -0
  72. package/examples/next-css-bom/README.md +7 -0
  73. package/examples/next-css-bom/app/about/page.jsx +1 -0
  74. package/examples/next-css-bom/app/case.css +3 -0
  75. package/examples/next-css-bom/app/layout.jsx +3 -0
  76. package/examples/next-css-bom/app/page.jsx +4 -0
  77. package/examples/next-css-bom/lib/catalog.js +2 -0
  78. package/examples/next-css-bom/next.config.mjs +1 -0
  79. package/examples/next-css-bom/package-lock.json +952 -0
  80. package/examples/next-css-bom/package.json +13 -0
  81. package/examples/next-css-bom/repro-surgeon.json +36 -0
  82. package/examples/next-pages/README.md +14 -0
  83. package/examples/next-pages/lib/unused-catalog.js +5 -0
  84. package/examples/next-pages/next.config.mjs +1 -0
  85. package/examples/next-pages/package-lock.json +952 -0
  86. package/examples/next-pages/package.json +11 -0
  87. package/examples/next-pages/pages/health.jsx +3 -0
  88. package/examples/next-pages/pages/index.jsx +9 -0
  89. package/examples/next-pages/repro-surgeon.json +18 -0
  90. package/examples/next-root-param/README.md +7 -0
  91. package/examples/next-root-param/app/[lang-country]/about/page.jsx +1 -0
  92. package/examples/next-root-param/app/[lang-country]/layout.tsx +4 -0
  93. package/examples/next-root-param/app/[lang-country]/page.tsx +3 -0
  94. package/examples/next-root-param/lib/catalog.js +2 -0
  95. package/examples/next-root-param/next-env.d.ts +2 -0
  96. package/examples/next-root-param/next.config.mjs +1 -0
  97. package/examples/next-root-param/package-lock.json +1016 -0
  98. package/examples/next-root-param/package.json +19 -0
  99. package/examples/next-root-param/repro-surgeon.json +37 -0
  100. package/examples/next-root-param/tsconfig.json +35 -0
  101. package/examples/rounding/README.md +16 -0
  102. package/examples/rounding/check.mjs +6 -0
  103. package/examples/rounding/config/store.json +7 -0
  104. package/examples/rounding/fixtures/invoice.json +11 -0
  105. package/examples/rounding/package-lock.json +7 -0
  106. package/examples/rounding/package.json +7 -0
  107. package/examples/rounding/repro-surgeon.json +12 -0
  108. package/examples/rounding/src/catalog.mjs +9 -0
  109. package/examples/rounding/src/shipping.mjs +7 -0
  110. package/examples/rounding/src/totals.mjs +11 -0
  111. package/package.json +29 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ Initial public release of the complete local application-reduction workflow.
6
+
7
+ - Explicit argv commands and failure checks using exact exit codes, required output, and forbidden output.
8
+ - Repeated baseline, candidate and independent export checks; invalid executions never accepted.
9
+ - Hierarchical file removal, JavaScript/TypeScript/JSX syntax reductions, JSON reductions, and direct npm dependency removal with lock integrity checks.
10
+ - Immutable source snapshots, content-addressed checkpoints, cancellation and resumable search budgets.
11
+ - Single-package npm projects with a Next.js adapter.
12
+ - Standalone exported verifier and offline HTML/JSON evidence reports.
13
+ - Runnable examples, regression tests, and documented execution/privacy limits.
14
+
15
+ See [validation](docs/validation.md) for the exact release evidence and tested scope.
@@ -0,0 +1,7 @@
1
+ # Code of conduct
2
+
3
+ Repro Surgeon welcomes contributors with different backgrounds and levels of experience. Treat people with respect, give specific constructive feedback, and discuss the work without attacking the person.
4
+
5
+ Harassment, threats, discriminatory language, sexual attention, disclosure of private information, and repeated disruption are unacceptable. Do not publish anyone's source, credentials or personal information without permission.
6
+
7
+ This policy applies to the project's issues, pull requests, discussions and other project spaces. Report concerns to **pavan.gupta.352@gmail.com**. The maintainer will review reports privately where possible, consider relevant context, and may remove content, restrict participation or ban participants. Decisions should be proportional to the conduct and protect the community's ability to collaborate.
@@ -0,0 +1,31 @@
1
+ # Contributing
2
+
3
+ The most useful contribution is a case where the reduced export fails to preserve the configured observation, changes the original source, or cannot reproduce independently. Small licensed examples and precise failure checks help more than large log dumps.
4
+
5
+ ## Run locally
6
+
7
+ Use Node.js 22.18 or newer and npm 10 or newer on Linux or macOS.
8
+
9
+ ```sh
10
+ git clone https://github.com/pavangupta352/repro-surgeon.git
11
+ cd repro-surgeon
12
+ npm ci
13
+ npm run check
14
+ node dist/cli.js --help
15
+ ```
16
+
17
+ Tests use Node's test runner with native TypeScript stripping. Type checking and the published JavaScript build use the pinned TypeScript compiler. Unit and subprocess tests do not need a registry dependency installation; the slower framework validation installs pinned packages separately.
18
+
19
+ ## Make a change
20
+
21
+ Keep a pull request focused on an observable behavior. Add a regression that demonstrates a correctness bug before the fix, then run `npm run check`. For a new reduction, show both a preserved failure and a candidate that must be rejected. For execution changes, include cancellation, invalid setup, and process cleanup where relevant. For exported output, test the standalone verifier without this package installed.
22
+
23
+ The [architecture](docs/architecture.md) explains the acceptance contract. Never accept a candidate based only on an import graph or cached observation. Do not weaken the oracle to improve reduction numbers. Keep source and raw logs out of report HTML. Update the relevant documentation when behavior changes.
24
+
25
+ Run the rounding example through the packaged CLI before proposing release changes. UI changes need keyboard, narrow-screen, empty-state, and hostile-text checks. The generated report must keep working offline.
26
+
27
+ ## Share a case
28
+
29
+ Include the tool, Node, npm and operating-system versions; the argv command; the required and forbidden diagnostic fragments; and the difference between expected and observed behavior. Review your reproduction and reports for credentials and private information before attaching them. A run directory can contain the original snapshot and raw logs; do not upload the whole directory by default.
30
+
31
+ Only contribute source you have the right to license under this repository's MIT license. Preserve existing notices. By submitting a contribution, you agree it may be distributed under that license. Be considerate of maintainers' time and follow the [code of conduct](CODE_OF_CONDUCT.md).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pavan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,159 @@
1
+ # Repro Surgeon
2
+
3
+ **Shrink the project. Keep the failure. Hand over something runnable.**
4
+
5
+ [![CI](https://github.com/pavangupta352/repro-surgeon/actions/workflows/ci.yml/badge.svg)](https://github.com/pavangupta352/repro-surgeon/actions/workflows/ci.yml)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-006a62)](LICENSE)
7
+
8
+ “Can you provide a minimal reproduction?” is often the hardest part of a bug report.
9
+
10
+ Repro Surgeon takes a failing npm application, removes source that isn't needed for your failure check, and exports a smaller project with its own verifier. It tests every accepted change and checks the export again with a fresh dependency installation. Your original project stays in place.
11
+
12
+ No account. No telemetry. Source and reports stay local.
13
+
14
+ ![An actual rounding example reduced from 10 files to 5, with three independent verification runs](docs/assets/report.png)
15
+
16
+ *Actual bundled demonstration: 3,173 → 520 source bytes, 10 → 5 files, 115 evaluations. The assertion remains pinned. This is a small seeded example, not a framework benchmark. [Method and evidence](docs/validation.md).*
17
+
18
+ ## Try it
19
+
20
+ Requires **Node.js 22.18+ and npm 10+**, on Linux or macOS.
21
+
22
+ ```sh
23
+ git clone https://github.com/pavangupta352/repro-surgeon.git
24
+ cd repro-surgeon
25
+ npm ci
26
+ npm run build
27
+
28
+ node dist/cli.js reduce examples/rounding --out /tmp/rounding-repro
29
+ node /tmp/rounding-repro/repro/.repro/verify.mjs
30
+ ```
31
+
32
+ Choose a new output directory for each run. Open `/tmp/rounding-repro/report.html` to inspect the result. The verifier exits successfully when the application's **expected failure** occurs; the original application command still exits with its configured failure code.
33
+
34
+ Install the packaged release for your own projects:
35
+
36
+ ```sh
37
+ npm install --global repro-surgeon@0.1.0
38
+ ```
39
+
40
+ The same package is available from the [versioned GitHub release](https://github.com/pavangupta352/repro-surgeon/releases/tag/v0.1.0), with a checksum. You can also run `npx repro-surgeon@0.1.0 --help`.
41
+
42
+ ## Reduce your application
43
+
44
+ Start from a project whose failure reproduces with a finite command and a committed npm lockfile.
45
+
46
+ ```sh
47
+ repro-surgeon init ./my-app \
48
+ --match "the distinctive diagnostic from your failure" \
49
+ -- npm run build
50
+
51
+ repro-surgeon doctor ./my-app --json
52
+ repro-surgeon reduce ./my-app --out ./my-app-repro
53
+ ```
54
+
55
+ Replace the example diagnostic with text from your actual error. Use several `--match` fragments to identify it precisely, and `--forbid` to reject a known competing error. Pin your test harness or other essential source with `preserve` in `repro-surgeon.json`.
56
+
57
+ ```json
58
+ {
59
+ "version": 1,
60
+ "command": ["npm", "run", "build"],
61
+ "oracle": {
62
+ "exitCode": 1,
63
+ "allOf": ["Parsing CSS source code failed", "Unexpected end of input"],
64
+ "noneOf": ["Module not found"]
65
+ },
66
+ "preserve": ["scripts/check.mjs"],
67
+ "budget": { "maxEvaluations": 200, "maxSeconds": 900 }
68
+ }
69
+ ```
70
+
71
+ The failure check is the contract. A broad phrase such as `failed` can match the wrong problem. Try a fixed control and a deliberately different failure before starting an expensive run. Repeated matching output does not establish identical semantic root cause.
72
+
73
+ ## What you get
74
+
75
+ ```text
76
+ my-app-repro/
77
+ ├── repro/ # Reduced application to inspect and share
78
+ │ ├── package.json
79
+ │ ├── package-lock.json
80
+ │ ├── … retained source
81
+ │ └── .repro/
82
+ │ ├── README.md # Runtime, command and verification instructions
83
+ │ ├── config.json
84
+ │ ├── LICENSE # License for the generated verifier code
85
+ │ ├── manifest.json # Source and metadata integrity records
86
+ │ └── verify.mjs # Runs without Repro Surgeon installed
87
+ ├── report.html # Offline, searchable evidence
88
+ ├── report.json # Machine-readable report
89
+ └── … private run state # Original snapshots and raw logs; keep private
90
+ ```
91
+
92
+ The report shows accepted, rejected and invalid trials, before/after metrics, remaining files, the exact failure check and the fresh verification outcome. Search and filters work offline.
93
+
94
+ **Review the `repro/` folder before sharing it.** Exclusions and review findings help locate sensitive material; they do not certify that a reproduction is safe to publish. The full run directory contains original source and logs.
95
+
96
+ ## How the reduction works
97
+
98
+ 1. **Establish the failure.** Three uncached baseline observations must match.
99
+ 2. **Try smaller source.** Remove file groups and complements, syntax elements, JSON fields and direct npm dependencies. Keep retained dependency versions and integrity records fixed.
100
+ 3. **Confirm every accepted candidate.** Require two matching executions. Timeouts, signals, installation failures, output overflow and spawn failures are invalid, never evidence of preservation.
101
+ 4. **Export and verify independently.** Copy the result into a separate temporary directory, install dependencies afresh and check three times. The standalone verifier checks the exported file manifest as well.
102
+
103
+ The search seeks fewer source bytes, then fewer files, fewer declared dependencies and fewer total bytes. It finds the smallest candidate reached by the enabled passes within your budget; it does not promise a globally minimal program.
104
+
105
+ ## Stop, resume and inspect
106
+
107
+ Press Ctrl-C to checkpoint the accepted snapshot and stop child processes.
108
+
109
+ ```sh
110
+ repro-surgeon resume ./my-app-repro --max-evaluations 500 --max-seconds 1800
111
+ repro-surgeon verify ./my-app-repro/repro
112
+ repro-surgeon report ./my-app-repro
113
+ ```
114
+
115
+ Resume budgets are **new total limits**, including earlier search time and evaluations. After a normal search budget stop, the current best result still gets a separately bounded export verification. A baseline interrupted before calibration completes remains paused. Resume requires the same tool, Node, npm, platform and architecture. See [troubleshooting](docs/troubleshooting.md) for crash locks and incompatible environments.
116
+
117
+ ## Supported scope
118
+
119
+ | Works with | Boundary |
120
+ |---|---|
121
+ | Single-package npm applications | `package-lock.json` version 2 or 3; public portable registry dependencies |
122
+ | Deterministic command and build failures | Explicit argv, exit code, required and forbidden literal text |
123
+ | Next.js application structure | App/Pages Router detection and conservative entrypoint protection |
124
+ | JavaScript, TypeScript, JSX, TSX and JSON | Syntax-aware source edits and JSON/JSONC field removal |
125
+ | Linux and macOS | Runtime/OS matrix in CI; Windows is not yet supported |
126
+
127
+ npm workspaces, pnpm/Yarn lockfiles, shrinkwrap, local/Git/private dependencies, browser recording, and managed service startup are outside this release. Existing syntax errors can be preserved by file reduction; malformed files are skipped by structural passes. [Full configuration](docs/configuration.md) · [Architecture](docs/architecture.md) · [Validation](docs/validation.md).
128
+
129
+ ## Local execution, plainly
130
+
131
+ Commands execute with your host permissions. This is **not a security sandbox**. Run trusted source, or put the entire workflow inside your own isolated environment. Install scripts are disabled unless you enable them. Network access is required for uncached dependencies; your command may also use the network. Common credentials, environment files, generated output and symlinks are excluded. Only explicitly requested environment variables are inherited beyond operating-system essentials. [Security policy](SECURITY.md).
132
+
133
+ ## Run the checks
134
+
135
+ From the repository root:
136
+
137
+ ```sh
138
+ npm ci
139
+ npm run check
140
+ npm run test:package
141
+ ```
142
+
143
+ The native suite does not fetch framework fixture dependencies. Run these optional integrations separately; they download public pinned packages and the Next.js checks take several minutes:
144
+
145
+ ```sh
146
+ node scripts/validate-dependency.mjs /tmp/repro-dependency-validation
147
+ node scripts/validate-pages.mjs /tmp/repro-pages-validation
148
+ node scripts/validate-next.mjs /tmp/repro-framework-validation
149
+ ```
150
+
151
+ Choose new output directories. The scripts check fixed and different-error controls, preserve the original source, and verify the reduced exports. [Measured results and limitations](docs/validation.md).
152
+
153
+ ## Related work and contributions
154
+
155
+ This builds on [delta debugging](https://www.st.cs.uni-saarland.de/papers/tse2002/) and shares the goal of property-preserving reduction with [treereduce](https://github.com/langston-barrett/treereduce). [Replay](https://www.replay.io/debugging) addresses recorded execution. Repro Surgeon focuses on the path from an application source tree to an independently installable, verified reproduction. No comparative superiority is claimed.
156
+
157
+ Have a failure this cannot reduce reliably? A small licensed case with a precise failure check is especially useful. Read [CONTRIBUTING.md](CONTRIBUTING.md), open an [issue](https://github.com/pavangupta352/repro-surgeon/issues), or improve a reducer with a regression test.
158
+
159
+ Maintained by [Pavan](https://github.com/pavangupta352). [MIT licensed](LICENSE).
package/SECURITY.md ADDED
@@ -0,0 +1,15 @@
1
+ # Security policy
2
+
3
+ Report a suspected vulnerability privately to **pavan.gupta.352@gmail.com**. Include the affected version, impact, and a small reproduction using non-sensitive data. Do not disclose credentials or another person's private application source. Please give the maintainer an opportunity to investigate before public disclosure.
4
+
5
+ The latest published release receives security fixes. This is a small open-source project; no response-time guarantee is offered.
6
+
7
+ ## Execution boundary
8
+
9
+ Repro Surgeon runs the command you supply with your operating-system permissions. It is **not a security sandbox**. Use it only with projects and commands you trust, or run the whole workflow in your own appropriately isolated environment. A child process can access files and the network according to those host permissions even though the reducer itself never runs inside the original project.
10
+
11
+ Installation scripts are disabled by default. Enabling `execution.allowInstallScripts` permits package lifecycle code to run. Dependencies are fetched from the configured supported public registry locations. Repro Surgeon does not operate a source-upload service and includes no telemetry.
12
+
13
+ Run directories contain original source snapshots, reduced source, private logs, environment variable names and evidence. Keep them private. The tool excludes common credential files and scans retained content for review findings, but those checks are not complete secret detection. Review the exported source, commands, paths, diagnostic fragments, report and package metadata before sharing. The complete run directory is not the shareable artifact.
14
+
15
+ The standalone verifier is executable source. Recipients should inspect a reproduction before running it, just as they would inspect any unfamiliar project. A matching failure check establishes the configured observation; it does not prove that source is benign or that two failures have the same root cause.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import type { RunReport, RunState, Snapshot } from './types.ts';
3
+ export declare function createRunReport(state: RunState, snapshot: Snapshot): RunReport;
package/dist/cli.js ADDED
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env node
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { parseArgs, stripVTControlCharacters } from 'node:util';
5
+ import { loadConfig, parseConfig } from "./config.js";
6
+ import { runtimeInfo, validateNpmProject } from "./dependencies.js";
7
+ import { reduceProject, resumeProject } from "./engine.js";
8
+ import { exportReproduction, verifyReproduction } from "./export.js";
9
+ import { detectAdapter } from "./graph.js";
10
+ import { sanitizeReport, writeReport } from "./report.js";
11
+ import { inventoryProject, metrics } from "./snapshot.js";
12
+ import { loadCheckpoint } from "./state.js";
13
+ const help = `Repro Surgeon · smaller source, the failure intact
14
+
15
+ Usage
16
+ repro-surgeon init [project] --match "distinctive diagnostic" -- <command> [args]
17
+ repro-surgeon doctor [project] [--json]
18
+ repro-surgeon reduce [project] --out <run-directory> [--config <file>]
19
+ repro-surgeon resume <run-directory> [--max-evaluations <n>] [--max-seconds <n>]
20
+ repro-surgeon verify <exported-repro> [--json]
21
+ repro-surgeon report <run-directory> [--json]
22
+
23
+ Options
24
+ --match <text> Required output fragment; repeat for multiple signals
25
+ --forbid <text> Forbidden competing diagnostic; repeat as needed
26
+ --exit <number> Expected failure exit code (default 1)
27
+ --config <file> Configuration path (default project/repro-surgeon.json)
28
+ --out <directory> New run directory outside the source tree
29
+ --max-evaluations <n> Search evaluation budget
30
+ --max-seconds <n> Total search time budget
31
+ --json Machine-readable result; progress stays on stderr
32
+ --help, -h Show this help
33
+ --version, -v Show the installed version
34
+
35
+ Single-package npm projects · Node 22.18+ · Linux/macOS
36
+ Commands execute locally with your permissions. Review remaining source before sharing.
37
+ `;
38
+ export function createRunReport(state, snapshot) {
39
+ return sanitizeReport({
40
+ version: 1, id: state.id, name: state.config.name, createdAt: state.createdAt,
41
+ status: state.status, stopReason: state.stopReason,
42
+ command: state.config.command, oracle: state.config.oracle, runtime: state.runtime, adapter: state.adapter,
43
+ initial: state.initial, current: state.current, evaluations: state.evaluations, elapsedMs: state.elapsedMs,
44
+ baseline: { completed: state.baseline.filter(observation => observation.status === 'reproduced').length, required: state.config.runs.baseline },
45
+ trials: state.trials,
46
+ files: [...snapshot].sort(([a], [b]) => a.localeCompare(b, 'en')).map(([name, file]) => ({ path: name, bytes: file.content.length })),
47
+ excluded: state.excluded, warnings: state.warnings,
48
+ verification: state.verification,
49
+ reviewFindings: state.reviewFindings,
50
+ });
51
+ }
52
+ function terminal(message) { process.stderr.write(stripVTControlCharacters(message) + '\n'); }
53
+ async function main(args) {
54
+ const separator = args.indexOf('--');
55
+ const invocation = separator < 0 ? [] : args.slice(separator + 1);
56
+ const { values, positionals } = parseArgs({ args: separator < 0 ? args : args.slice(0, separator), strict: true, allowPositionals: true, options: {
57
+ help: { type: 'boolean', short: 'h' }, version: { type: 'boolean', short: 'v' }, json: { type: 'boolean' },
58
+ match: { type: 'string', multiple: true }, forbid: { type: 'string', multiple: true }, exit: { type: 'string' },
59
+ config: { type: 'string' }, out: { type: 'string' }, 'max-evaluations': { type: 'string' }, 'max-seconds': { type: 'string' },
60
+ } });
61
+ if (values.help || args.length === 0) {
62
+ process.stdout.write(help);
63
+ return 0;
64
+ }
65
+ if (values.version) {
66
+ process.stdout.write('0.1.0\n');
67
+ return 0;
68
+ }
69
+ const [command, target, ...extra] = positionals;
70
+ if (extra.length)
71
+ throw new Error('Too many positional arguments. See --help.');
72
+ if (process.platform === 'win32')
73
+ throw new Error('Windows execution is not validated yet. Use Linux or macOS.');
74
+ const directory = path.resolve(target ?? process.cwd());
75
+ const print = (value, plain) => { process.stdout.write(values.json ? JSON.stringify(value, null, 2) + '\n' : plain + '\n'); };
76
+ if (command === 'init') {
77
+ const config = parseConfig({ name: path.basename(directory), command: invocation, oracle: { exitCode: values.exit === undefined ? 1 : Number(values.exit), allOf: values.match ?? [], noneOf: values.forbid ?? [] } });
78
+ const destination = path.resolve(values.config ?? path.join(directory, 'repro-surgeon.json'));
79
+ await readFile(path.join(directory, 'package.json'));
80
+ await mkdir(path.dirname(destination), { recursive: true });
81
+ await writeFile(destination, JSON.stringify({ version: config.version, name: config.name, command: config.command, oracle: config.oracle, adapter: config.adapter }, null, 2) + '\n', { flag: 'wx', mode: 0o600 });
82
+ print({ config: destination }, `Created ${destination}\nRun repro-surgeon doctor, then repro-surgeon reduce.`);
83
+ return 0;
84
+ }
85
+ if (invocation.length)
86
+ throw new Error('Arguments after -- are supported only by init. Store the command in your configuration.');
87
+ if (command === 'doctor') {
88
+ let config;
89
+ try {
90
+ config = await loadConfig(path.resolve(values.config ?? path.join(directory, 'repro-surgeon.json')));
91
+ }
92
+ catch (error) {
93
+ if (values.config || error.code !== 'ENOENT')
94
+ throw error;
95
+ config = { include: [], exclude: [], adapter: 'auto' };
96
+ }
97
+ const inventory = await inventoryProject(directory, config);
98
+ validateNpmProject(inventory.snapshot);
99
+ const summary = { adapter: detectAdapter(inventory.snapshot, config.adapter), metrics: metrics(inventory.snapshot), runtime: await runtimeInfo(directory), excluded: inventory.excluded, warnings: inventory.warnings };
100
+ print(summary, `${summary.adapter.name} project · ${summary.metrics.files} files · ${summary.metrics.dependencies} dependencies\nInput checks passed. No project command was executed.\n${summary.excluded.length} paths excluded; inspect --json for details.`);
101
+ return 0;
102
+ }
103
+ const controller = new AbortController();
104
+ const interrupt = () => controller.abort();
105
+ process.once('SIGINT', interrupt);
106
+ process.once('SIGTERM', interrupt);
107
+ try {
108
+ if (command === 'verify') {
109
+ if (!target)
110
+ throw new Error('verify requires the exported reproduction directory.');
111
+ const verification = await verifyReproduction(directory, { signal: controller.signal });
112
+ print(verification, `${verification.status}: ${verification.reason}`);
113
+ return verification.status === 'verified' ? 0 : 1;
114
+ }
115
+ if (command === 'report') {
116
+ if (!target)
117
+ throw new Error('report requires a run directory.');
118
+ const { state, snapshot } = await loadCheckpoint(directory);
119
+ const report = createRunReport(state, snapshot);
120
+ await writeReport(report, directory);
121
+ print(report, `Report: ${path.join(directory, 'report.html')}`);
122
+ return 0;
123
+ }
124
+ if (command !== 'reduce' && command !== 'resume')
125
+ throw new Error(`Unknown command: ${command ?? '(none)'}. See --help.`);
126
+ if (command === 'resume' && !target)
127
+ throw new Error('resume requires a run directory.');
128
+ const onEvent = (event) => terminal(event.message);
129
+ let result;
130
+ let runRoot;
131
+ if (command === 'reduce') {
132
+ const config = await loadConfig(path.resolve(values.config ?? path.join(directory, 'repro-surgeon.json')));
133
+ if (values['max-evaluations'] !== undefined)
134
+ config.budget.maxEvaluations = Number(values['max-evaluations']);
135
+ if (values['max-seconds'] !== undefined)
136
+ config.budget.maxSeconds = Number(values['max-seconds']);
137
+ runRoot = path.resolve(values.out ?? path.join(path.dirname(directory), `${path.basename(directory)}-repro-${Date.now()}`));
138
+ result = await reduceProject({ sourceRoot: directory, runRoot, config, signal: controller.signal, onEvent });
139
+ }
140
+ else {
141
+ runRoot = directory;
142
+ result = await resumeProject({ runRoot, signal: controller.signal, onEvent, ...(values['max-evaluations'] === undefined ? {} : { maxEvaluations: Number(values['max-evaluations']) }), ...(values['max-seconds'] === undefined ? {} : { maxSeconds: Number(values['max-seconds']) }) });
143
+ }
144
+ if (result.state.status === 'verifying') {
145
+ terminal('Checking the exported source with a fresh dependency installation…');
146
+ result.state = await exportReproduction(runRoot, result.state, result.snapshot, { signal: controller.signal });
147
+ }
148
+ const report = createRunReport(result.state, result.snapshot);
149
+ await writeReport(report, runRoot);
150
+ print(report, `${report.initial.files} → ${report.current.files} files · ${report.verification.status}\nReproduction: ${path.join(runRoot, 'repro')}\nReport: ${path.join(runRoot, 'report.html')}\nReview the remaining source before sharing.`);
151
+ if (controller.signal.aborted)
152
+ return 130;
153
+ return report.verification.status === 'verified' ? 0 : 1;
154
+ }
155
+ finally {
156
+ process.removeListener('SIGINT', interrupt);
157
+ process.removeListener('SIGTERM', interrupt);
158
+ }
159
+ }
160
+ void main(process.argv.slice(2)).then(code => { process.exitCode = code; }).catch((error) => {
161
+ const message = error.code === 'EEXIST' ? 'Configuration already exists. Edit it or choose another --config path.' : error instanceof Error ? error.message : String(error);
162
+ terminal(`Repro Surgeon: ${message}`);
163
+ process.exitCode = 2;
164
+ });
165
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { Config } from './types.ts';
2
+ export declare function parseConfig(value: unknown): Config;
3
+ export declare function loadConfig(path: string): Promise<Config>;
package/dist/config.js ADDED
@@ -0,0 +1,177 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ const DEFAULTS = {
3
+ name: 'reproduction',
4
+ adapter: 'auto',
5
+ budget: { maxEvaluations: 200, maxSeconds: 900 },
6
+ runs: { baseline: 3, candidate: 2, final: 3 },
7
+ execution: {
8
+ timeoutMs: 60_000,
9
+ maxOutputBytes: 1_048_576,
10
+ installTimeoutMs: 120_000,
11
+ allowInstallScripts: false,
12
+ env: [],
13
+ },
14
+ reduce: { files: true, syntax: true, json: true, dependencies: true },
15
+ };
16
+ function record(value, path) {
17
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
18
+ throw new TypeError(`${path} must be an object`);
19
+ }
20
+ return value;
21
+ }
22
+ function rejectUnknown(value, allowed, path) {
23
+ const allowedFields = new Set(allowed);
24
+ for (const key of Object.keys(value)) {
25
+ if (!allowedFields.has(key))
26
+ throw new TypeError(`Unknown field ${path}.${key}`);
27
+ }
28
+ }
29
+ function text(value, path) {
30
+ if (typeof value !== 'string' || value.trim().length === 0 || value.includes('\0')) {
31
+ throw new TypeError(`${path} must be a non-empty string without NUL bytes`);
32
+ }
33
+ return value;
34
+ }
35
+ function commandArray(value) {
36
+ if (!Array.isArray(value) || value.length === 0) {
37
+ throw new TypeError('config.command must be a non-empty array of argv strings');
38
+ }
39
+ const command = value.map((item, index) => {
40
+ if (typeof item !== 'string' || item.includes('\0')) {
41
+ throw new TypeError(`config.command[${index}] must be a string without NUL bytes`);
42
+ }
43
+ return item;
44
+ });
45
+ if (command[0].trim().length === 0) {
46
+ throw new TypeError('config.command executable must be a non-empty string');
47
+ }
48
+ return command;
49
+ }
50
+ function stringArray(value, path, allowEmpty) {
51
+ if (!Array.isArray(value) || (!allowEmpty && value.length === 0)) {
52
+ throw new TypeError(`${path} must be ${allowEmpty ? 'an' : 'a non-empty'} array of strings`);
53
+ }
54
+ return value.map((item, index) => text(item, `${path}[${index}]`));
55
+ }
56
+ function integer(value, path, minimum, maximum = Number.MAX_SAFE_INTEGER) {
57
+ if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
58
+ throw new TypeError(`${path} must be a safe integer between ${minimum} and ${maximum}`);
59
+ }
60
+ return value;
61
+ }
62
+ function boolean(value, path) {
63
+ if (typeof value !== 'boolean')
64
+ throw new TypeError(`${path} must be a boolean`);
65
+ return value;
66
+ }
67
+ function optionalRecord(parent, key) {
68
+ return parent[key] === undefined ? {} : record(parent[key], key);
69
+ }
70
+ function parseOracle(value) {
71
+ const input = record(value, 'oracle');
72
+ rejectUnknown(input, ['exitCode', 'allOf', 'noneOf'], 'oracle');
73
+ if (input.exitCode === undefined)
74
+ throw new TypeError('oracle.exitCode must be provided');
75
+ if (input.allOf === undefined)
76
+ throw new TypeError('oracle.allOf target must be provided');
77
+ return {
78
+ exitCode: integer(input.exitCode, 'oracle.exitCode', 0, 255),
79
+ allOf: stringArray(input.allOf, 'oracle.allOf target', false),
80
+ noneOf: input.noneOf === undefined ? [] : stringArray(input.noneOf, 'oracle.noneOf', true),
81
+ };
82
+ }
83
+ export function parseConfig(value) {
84
+ const input = record(value, 'config');
85
+ rejectUnknown(input, ['version', 'name', 'command', 'oracle', 'adapter', 'budget', 'runs', 'execution', 'reduce', 'include', 'exclude', 'preserve'], 'config');
86
+ if (input.command === undefined)
87
+ throw new TypeError('config.command must be provided');
88
+ if (input.oracle === undefined)
89
+ throw new TypeError('config.oracle target must be provided');
90
+ if (input.version !== undefined && input.version !== 1)
91
+ throw new TypeError('config.version must be 1');
92
+ const budget = optionalRecord(input, 'budget');
93
+ rejectUnknown(budget, ['maxEvaluations', 'maxSeconds'], 'budget');
94
+ const runs = optionalRecord(input, 'runs');
95
+ rejectUnknown(runs, ['baseline', 'candidate', 'final'], 'runs');
96
+ const execution = optionalRecord(input, 'execution');
97
+ rejectUnknown(execution, ['timeoutMs', 'maxOutputBytes', 'installTimeoutMs', 'allowInstallScripts', 'env'], 'execution');
98
+ const reduce = optionalRecord(input, 'reduce');
99
+ rejectUnknown(reduce, ['files', 'syntax', 'json', 'dependencies'], 'reduce');
100
+ const adapter = input.adapter ?? DEFAULTS.adapter;
101
+ if (adapter !== 'auto' && adapter !== 'next' && adapter !== 'generic') {
102
+ throw new TypeError('config.adapter must be auto, next, or generic');
103
+ }
104
+ const environment = execution.env === undefined
105
+ ? []
106
+ : stringArray(execution.env, 'execution.env', true);
107
+ for (const name of environment) {
108
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
109
+ throw new TypeError(`execution.env contains invalid environment variable name: ${name}`);
110
+ }
111
+ }
112
+ return {
113
+ version: 1,
114
+ name: input.name === undefined ? DEFAULTS.name : text(input.name, 'config.name'),
115
+ command: commandArray(input.command),
116
+ oracle: parseOracle(input.oracle),
117
+ adapter,
118
+ budget: {
119
+ maxEvaluations: budget.maxEvaluations === undefined
120
+ ? DEFAULTS.budget.maxEvaluations
121
+ : integer(budget.maxEvaluations, 'budget.maxEvaluations', 1),
122
+ maxSeconds: budget.maxSeconds === undefined
123
+ ? DEFAULTS.budget.maxSeconds
124
+ : integer(budget.maxSeconds, 'budget.maxSeconds', 1),
125
+ },
126
+ runs: {
127
+ baseline: runs.baseline === undefined
128
+ ? DEFAULTS.runs.baseline
129
+ : integer(runs.baseline, 'runs.baseline', DEFAULTS.runs.baseline),
130
+ candidate: runs.candidate === undefined
131
+ ? DEFAULTS.runs.candidate
132
+ : integer(runs.candidate, 'runs.candidate', DEFAULTS.runs.candidate),
133
+ final: runs.final === undefined
134
+ ? DEFAULTS.runs.final
135
+ : integer(runs.final, 'runs.final', DEFAULTS.runs.final),
136
+ },
137
+ execution: {
138
+ timeoutMs: execution.timeoutMs === undefined
139
+ ? DEFAULTS.execution.timeoutMs
140
+ : integer(execution.timeoutMs, 'execution.timeoutMs', 1),
141
+ maxOutputBytes: execution.maxOutputBytes === undefined
142
+ ? DEFAULTS.execution.maxOutputBytes
143
+ : integer(execution.maxOutputBytes, 'execution.maxOutputBytes', 1),
144
+ installTimeoutMs: execution.installTimeoutMs === undefined
145
+ ? DEFAULTS.execution.installTimeoutMs
146
+ : integer(execution.installTimeoutMs, 'execution.installTimeoutMs', 1),
147
+ allowInstallScripts: execution.allowInstallScripts === undefined
148
+ ? DEFAULTS.execution.allowInstallScripts
149
+ : boolean(execution.allowInstallScripts, 'execution.allowInstallScripts'),
150
+ env: [...new Set(environment)],
151
+ },
152
+ reduce: {
153
+ files: reduce.files === undefined ? DEFAULTS.reduce.files : boolean(reduce.files, 'reduce.files'),
154
+ syntax: reduce.syntax === undefined ? DEFAULTS.reduce.syntax : boolean(reduce.syntax, 'reduce.syntax'),
155
+ json: reduce.json === undefined ? DEFAULTS.reduce.json : boolean(reduce.json, 'reduce.json'),
156
+ dependencies: reduce.dependencies === undefined
157
+ ? DEFAULTS.reduce.dependencies
158
+ : boolean(reduce.dependencies, 'reduce.dependencies'),
159
+ },
160
+ include: input.include === undefined ? [] : stringArray(input.include, 'config.include', true),
161
+ exclude: input.exclude === undefined ? [] : stringArray(input.exclude, 'config.exclude', true),
162
+ preserve: input.preserve === undefined ? [] : stringArray(input.preserve, 'config.preserve', true),
163
+ };
164
+ }
165
+ export async function loadConfig(path) {
166
+ const source = await readFile(path, 'utf8');
167
+ let value;
168
+ try {
169
+ value = JSON.parse(source);
170
+ }
171
+ catch (error) {
172
+ const message = error instanceof Error ? error.message : String(error);
173
+ throw new SyntaxError(`Invalid JSON in ${path}: ${message}`);
174
+ }
175
+ return parseConfig(value);
176
+ }
177
+ //# sourceMappingURL=config.js.map