ushman-equiv
v0.4.0
Published
Pure-Node equivalence checks for ushman refactors. Runs import-graph, module-load, symbol-diff, and replay tiers inside Node-only sandboxes.
Maintainers
Readme
ushman-equiv
Pure-Node behavioral and structural equivalence checks for ushman refactors.
Runs four Node-only tiers against a candidate workspace: import-graph resolution, module-load smoke, symbol-preservation diff, and replay against captured fixtures. Designed for the same air-gapped handoff zips and local-link workflows as @ushman/verify.
When runEquiv() executes multiple tiers together, it shares entrypoint discovery, source walking, and AST parsing across tiers so each source file is parsed once per run.
flowchart LR
A["runEquiv(opts)"] --> B["EquivExecutionContext"]
B --> C["Analysis Context"]
B --> D["Candidate Boot"]
C --> E["Tier I: import graph"]
C --> F["Tier S: symbol diff"]
C --> G["Tier L: module load"]
C --> H["Tier R: replay"]
D --> G
D --> HStatus
Extracted as a sister package for the cleanup lane. Runtime: pure Node ≥ 24. No Bun APIs in published runtime code. Tests run with Bun. Distribution: local-link only for now. Do not publish to npm in v0.
Install
cd ~/workspace/ushman-lab-types
bun link
cd ~/workspace/ushman-equiv
bun link
cd ~/workspace/ushman
bun link ushman-equivQuick start
import { runEquiv } from 'ushman-equiv';
const report = await runEquiv({ workspaceRoot: '.' });
console.log(report.verdict); // 'green' | 'yellow' | 'red'ushman-equiv . # all tiers
ushman-equiv ./candidate --tier=R
ushman-equiv emit-baseline --workspace=.Tiers
| Tier | What it checks |
|------|----------------|
| I | Static import-graph resolution for relative imports and bare specifiers against the workspace importmap |
| L | Child-process module-load smoke with browser-global shims and vendor stubs |
| S | Baseline vs candidate top-level symbol preservation |
| R | Replay of captured fixture calls against refactored exports |
Public API
See src/index.ts for the full surface. Top-level:
runEquiv(opts: EquivOptions): Promise<EquivReport>;
renderEquivReport(report: EquivReport): string;
writeEquivResult(opts: { report: EquivReport; workspaceRoot: string }): Promise<void>;
emitSymbolBaseline(opts: { workspaceRoot: string; outputPath?: string }): Promise<{ symbolCount: number; outputPath: string }>;
canonicalize(value: unknown, opts?: { precision?: number; exemptFields?: readonly string[]; maxBytes?: number }): unknown;canonicalize() treats exemptFields as dot-notation paths rooted at the replay payload, for example ['events.0.timestamp', 'meta.uuid'].
emitSymbolBaseline also accepts { bundlePath, outputPath } for bundle-driven baseline generation when the caller is not operating on a live v4 workspace.
Plus per-tier helpers (checkImportGraph, checkModuleLoad, checkSymbolDiff, checkReplay) for callers that want one tier.
createEquivExecutionContext() plus the check*WithContext() variants are also exported for callers that want to reuse one shared boot/analysis cache across multiple tier calls in the same process.
Error behavior:
runEquiv()catches unexpected tier crashes and records them as red tier results so the overall report still renders.- Standalone tier helpers still throw for invalid input/options and unexpected setup failures. They return an
EquivCheckResultonly after the tier has started running normally. check*WithContextexports exist for callers that want to reuse oneEquivExecutionContextacross multiple tier invocations in the same process.
CLI
ushman-equiv <workspace> [--tiers=I,L,S,R | --tier=I] [--filter=<glob>]
[--entrypoint=<path>] [--baseline=<path>]
[--fixtures=<dir>] [--modules=<dir>]
[--src-roots=<comma-list>] [--mode=preview|dev]
[--write-result] [--json]
[--max-concurrency=<n>]
ushman-equiv emit-baseline [<workspace> | --workspace=<path>] [--out=<path>]
ushman-equiv --versionExit codes:
0: verdict isgreenoryellow1: verdict isred2: equiv runtime / CLI error
--mode=preview is the authoritative default for tiers L and R; --mode=dev is the faster smoke path.
--write-result writes <workspace>/.lab/equiv/result.json atomically.
--filter matches fixture names for tier R, symbol names for tier S, and relative source-file paths for tier I.
--baseline may point at a workspace-relative or absolute baseline file.
--max-concurrency is capped at 64.
Defaults for v4 workspaces:
- Tier
Sbaseline:<workspace>/.lab/equiv/baseline.json - Tier
Rfixtures:<workspace>/.lab/characterize/replay/ - Tier
Rfallback modules:<workspace>/.lab/characterize/modules/
Relevant environment overrides:
USHMAN_EQUIV_NODE_BINARY: override the Node binary used for TierLchild-process module loading when the parent process is not plain Node.USHMAN_EQUIV_MODULE_LOAD_TIMEOUT_MS: cap TierLchild-process runtime.USHMAN_EQUIV_BOOT_TIMEOUT_MS,USHMAN_EQUIV_BUILD_TIMEOUT_MS,USHMAN_EQUIV_MAX_LOG_BYTES: tune preview/dev boot behavior.USHMAN_EQUIV_BOOT_PORT_RETRY_LIMIT: retry strict-port preview/dev boot when the chosen port loses a race to another local process.
Internal execution notes for contributors:
- Tier orchestration uses a shared execution context so Tier
Land TierRreuse the same boot smoke result duringrunEquiv(). - Import-graph and symbol analysis are memoized per workspace/entrypoint/source-root set, then filtered per tier request.
- Tier
Ldelivers one shared vendor-stub helper module into the child runtime so large bare-specifier graphs do not duplicate the proxy factory in every generated stub. - Candidate boot still uses strict ports for deterministic probing, but it now retries
EADDRINUSEcontention with fresh ports before failing.
v3 workspaces are refused with the standard v4 cutover hint:
This workspace is in v3 layout (legacy stages/, .ushman/, handoff.json at root).
ushman v4 no longer ships the v3 layout readers or the live migrator.
`ushman migrate-v3-v4` is retained only as a diagnostic stub.
All known v3 workspaces were migrated during the v4 cutover.
If this workspace was not on that migration list, restore shibuk v2.x + ushman v3.x to migrate it first.
Workspace: <workspace>This package does not ship the migrator itself. The migration is owned by the main ushman CLI.
Examples:
ushman-equiv ./candidate --tier=R --filter='clamp*'
ushman-equiv ./candidate --tier=I --filter='src/**/math/*.js'
ushman-equiv ./candidate --tier=S --filter='/^Game/'Replay fixture shape:
{
"fn": "clamp",
"module": ".lab/characterize/modules/clamp.mjs",
"calls": [
{ "args": [-1, 0, 10], "return": 0 },
{ "args": [4, 0, 10], "return": 4 }
]
}module is optional. When present, it is resolved relative to the workspace root. When omitted, Tier R falls back to <workspace>/.lab/characterize/modules/<sanitized-fn>.mjs.
thisArg and threw are also supported in calls when characterize captured a bound-method invocation or an expected throw.
Workspace-relative path resolution is validated twice:
- lexical path containment under the workspace root
- realpath containment under the workspace's real root, including the nearest existing ancestor for future output paths
This rejects symlink escapes while still allowing symlinks that resolve back inside the workspace.
Advanced replay knobs:
fixturesDir: override the default.lab/characterize/replay/directory.modulesDir: override the fallback replay-module directory.maxConcurrency: cap fixture replay fan-out. The CLI enforces a maximum of64.
Why This Exists Separate From ushman
ushman-equiv runs in the same pure-Node LLM sandbox and operator-side handoff flow as @ushman/verify, but it answers a different question. verify proves the candidate still parses and references sane files. equiv proves the refactor still resolves, loads, preserves required symbols, and reproduces captured outputs.
Where This Fits In The Family
| | |
|---|---|
| Runs after | @ushman/verify preflight |
| Reuses output from | @ushman/characterize replay fixture generation |
| Runs before | operator-side parity / merge acceptance |
| Does not run | browsers, screenshots, or networked validation |
Safety Boundary
Tiers L and R execute workspace modules inside Node. Run ushman-equiv only against operator-controlled workspaces and handoff trees you trust to execute locally.
