ushman-characterize 0.4.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/AGENTS.md +110 -0
- package/CHANGELOG.md +41 -0
- package/LICENSE.md +21 -0
- package/README.md +193 -0
- package/bin/ushman-characterize +19 -0
- package/dist/babel-config.d.ts +7 -0
- package/dist/babel-config.d.ts.map +1 -0
- package/dist/babel-config.js +17 -0
- package/dist/capture-server.d.ts +31 -0
- package/dist/capture-server.d.ts.map +1 -0
- package/dist/capture-server.js +199 -0
- package/dist/capture.d.ts +97 -0
- package/dist/capture.d.ts.map +1 -0
- package/dist/capture.js +620 -0
- package/dist/cli/logger.d.ts +7 -0
- package/dist/cli/logger.d.ts.map +1 -0
- package/dist/cli/logger.js +14 -0
- package/dist/cli/parse-flags.d.ts +8 -0
- package/dist/cli/parse-flags.d.ts.map +1 -0
- package/dist/cli/parse-flags.js +60 -0
- package/dist/cli.d.ts +39 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +439 -0
- package/dist/constants.d.ts +20 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +19 -0
- package/dist/dedupe-contract.d.ts +26 -0
- package/dist/dedupe-contract.d.ts.map +1 -0
- package/dist/dedupe-contract.js +12 -0
- package/dist/default-export.d.ts +6 -0
- package/dist/default-export.d.ts.map +1 -0
- package/dist/default-export.js +52 -0
- package/dist/format-contract.d.ts +25 -0
- package/dist/format-contract.d.ts.map +1 -0
- package/dist/format-contract.js +96 -0
- package/dist/function-utils.d.ts +6 -0
- package/dist/function-utils.d.ts.map +1 -0
- package/dist/function-utils.js +22 -0
- package/dist/generate-replay.d.ts +18 -0
- package/dist/generate-replay.d.ts.map +1 -0
- package/dist/generate-replay.js +158 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/instrument.d.ts +39 -0
- package/dist/instrument.d.ts.map +1 -0
- package/dist/instrument.js +605 -0
- package/dist/ledger.d.ts +19 -0
- package/dist/ledger.d.ts.map +1 -0
- package/dist/ledger.js +50 -0
- package/dist/puppeteer-harness.d.ts +74 -0
- package/dist/puppeteer-harness.d.ts.map +1 -0
- package/dist/puppeteer-harness.js +248 -0
- package/dist/purity-classifier.d.ts +28 -0
- package/dist/purity-classifier.d.ts.map +1 -0
- package/dist/purity-classifier.js +363 -0
- package/dist/rebind.d.ts +26 -0
- package/dist/rebind.d.ts.map +1 -0
- package/dist/rebind.js +356 -0
- package/dist/replay-report.d.ts +18 -0
- package/dist/replay-report.d.ts.map +1 -0
- package/dist/replay-report.js +12 -0
- package/dist/scene.d.ts +24 -0
- package/dist/scene.d.ts.map +1 -0
- package/dist/scene.js +235 -0
- package/dist/schema-types.d.ts +40 -0
- package/dist/schema-types.d.ts.map +1 -0
- package/dist/schema-types.js +32 -0
- package/dist/seed-scaffolds.d.ts +31 -0
- package/dist/seed-scaffolds.d.ts.map +1 -0
- package/dist/seed-scaffolds.js +96 -0
- package/dist/shared.d.ts +36 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +390 -0
- package/dist/state-dag.d.ts +5 -0
- package/dist/state-dag.d.ts.map +1 -0
- package/dist/state-dag.js +27 -0
- package/dist/stub-pure.d.ts +57 -0
- package/dist/stub-pure.d.ts.map +1 -0
- package/dist/stub-pure.js +987 -0
- package/dist/time.d.ts +3 -0
- package/dist/time.d.ts.map +1 -0
- package/dist/time.js +10 -0
- package/dist/trace-format.d.ts +24 -0
- package/dist/trace-format.d.ts.map +1 -0
- package/dist/trace-format.js +213 -0
- package/dist/trace-serializer.d.ts +94 -0
- package/dist/trace-serializer.d.ts.map +1 -0
- package/dist/trace-serializer.js +607 -0
- package/dist/tracer-runtime.d.ts +25 -0
- package/dist/tracer-runtime.d.ts.map +1 -0
- package/dist/tracer-runtime.js +291 -0
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +0 -0
- package/dist/workspace-paths.d.ts +64 -0
- package/dist/workspace-paths.d.ts.map +1 -0
- package/dist/workspace-paths.js +288 -0
- package/package.json +86 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# AGENTS.md — ushman-characterize
|
|
2
|
+
|
|
3
|
+
Read this before changing code in this package.
|
|
4
|
+
|
|
5
|
+
## What this package is
|
|
6
|
+
|
|
7
|
+
Refactor-safety net for the post-Ushman decomposition phase. Per-function trace capture + replay. Three.js-aware deep-equality on captured arg/return shapes.
|
|
8
|
+
|
|
9
|
+
## What this package is NOT
|
|
10
|
+
|
|
11
|
+
- Not a coverage tool. We measure behavior, not lines.
|
|
12
|
+
- Not a parity harness. `ushman parity` owns whole-app behavioral equivalence.
|
|
13
|
+
- Not a Three.js library. We *consume* Three.js types via `ushman-threejs-tools`; we don't ship Three.
|
|
14
|
+
|
|
15
|
+
## Read order
|
|
16
|
+
|
|
17
|
+
1. `README.md`.
|
|
18
|
+
2. `src/instrument.ts` — the Babel transform.
|
|
19
|
+
3. `src/workspace-paths.ts` — v4 workspace detection, path resolution, and lock semantics.
|
|
20
|
+
4. `src/capture.ts` — preview boot, trace merging, and ledger-backed capture flow.
|
|
21
|
+
5. `src/tracer-runtime.ts` — browser tracer runtime generation and async attribution rules.
|
|
22
|
+
6. `src/trace-serializer.ts` — the canonical serializer and replay comparison rules.
|
|
23
|
+
7. `src/format-contract.ts` — versioned trace/replay format guards and compatibility rules.
|
|
24
|
+
8. `src/trace-format.ts` — generated workspace harness wiring.
|
|
25
|
+
9. `src/generate-replay.ts` — how traces become `bun test` files.
|
|
26
|
+
|
|
27
|
+
## Code map
|
|
28
|
+
|
|
29
|
+
| File | Owns |
|
|
30
|
+
|------|------|
|
|
31
|
+
| `src/babel-config.ts` | Shared Babel parser configuration for bundle/test AST work |
|
|
32
|
+
| `src/purity-classifier.ts` | Phase A — AST classification of pure top-level functions |
|
|
33
|
+
| `src/stub-pure.ts` | Phase A — emits `tests/pure/<fn>.test.ts` files |
|
|
34
|
+
| `src/scene.ts` | Phase B — scene-state snapshot scaffold from `screenshots/<state>/scene-heavy.json` |
|
|
35
|
+
| `src/instrument.ts` | Phase C.1 — Babel transform that wraps every top-level fn with `__tracer.enter/exit` |
|
|
36
|
+
| `src/workspace-paths.ts` | V4 workspace detection, `.lab/characterize/` path resolution, and pipeline lock handling |
|
|
37
|
+
| `src/capture-server.ts` | Phase C — Vite build/preview boot host for candidate capture |
|
|
38
|
+
| `src/capture.ts` | Phase C.2 — Puppeteer-driven capture during state DAG playback |
|
|
39
|
+
| `src/tracer-runtime.ts` | Phase C — browser tracer runtime generation and sampling |
|
|
40
|
+
| `src/trace-serializer.ts` | Phase C — shared canonicalization/hydration/comparison logic |
|
|
41
|
+
| `src/trace-format.ts` | Phase C — generated replay harness and workspace harness paths |
|
|
42
|
+
| `src/generate-replay.ts` | Phase C.3 — emits `tests/replay/<fn>.test.ts` |
|
|
43
|
+
| `src/ledger.ts` | Phase C/D — characterize report writing and `validator-result` ledger emission |
|
|
44
|
+
| `src/rebind.ts` | Phase D — rewrite import paths in existing replay tests post-refactor |
|
|
45
|
+
| `src/replay-report.ts` | Versioned `VerifyReport` output for `replay --strict` / `--lenient` |
|
|
46
|
+
|
|
47
|
+
## Conventions and Rules
|
|
48
|
+
|
|
49
|
+
- Always use `bun` and `bunx`, HARD BAN on: `npm` unless absolutely necessary.
|
|
50
|
+
- Prefer `Bun.file()` instead of `node:fs` whenever possible.
|
|
51
|
+
- Kill any browser instance you start or stray `bun`, `node` processes after you are done. Never leave it running.
|
|
52
|
+
- Prefer arrow functions to classical functions, and `type` over `interface` in TS.
|
|
53
|
+
- Fixes are made using TDD approach.
|
|
54
|
+
- `bun format` and `bun typecheck` should always be ran at the end of your completion, if you introduced any warnings or errors clean them up yourself before you complete.
|
|
55
|
+
- NEVER disable a biome rule or TS rule without explicit permission from the user.
|
|
56
|
+
- If you believe the code you are changing requires some clarification for future AI agents to understand why the change was made, add a brief comment.
|
|
57
|
+
- Do NOT use decorative section headers made of repeated characters (e.g. `// -----`, `// =====`, etc.).
|
|
58
|
+
- Use `it('should...')` style tests.
|
|
59
|
+
- Unit-tests always live in the same folder as their implementation, never in the `test` folder.
|
|
60
|
+
|
|
61
|
+
## Working rules
|
|
62
|
+
|
|
63
|
+
- **TDD the serializer.** It's the most error-prone surface (Three.js cycles, BufferGeometry hashing, async-attribution). Add ≥30 cases.
|
|
64
|
+
- **Phase C output must pass `ushman-verify` Tier 0c + 0d.** The instrumented bundle is still a real bundle.
|
|
65
|
+
- **Trace files are JSONL, append-friendly, dedup by (fnName + arg-shape hash + return-shape hash).** Cap at 50 MB per state.
|
|
66
|
+
- **Float tolerance is 4 decimals by default**, configurable per-test.
|
|
67
|
+
- **Capture and rebind runs emit `validator-result` ledger entries.** Treat missing ledger output as a real regression, not optional metadata.
|
|
68
|
+
- **V4 state DAG input is `.lab/state-dag.json` only.** Do not reintroduce `.ushman/state-dag.json` fallback logic for v4 workspaces.
|
|
69
|
+
- **Async side-effect attribution only covers semantically owned work.**
|
|
70
|
+
Supported:
|
|
71
|
+
- Native `await` inside traced top-level functions.
|
|
72
|
+
- Synchronous callbacks scheduled from an active traced chunk through promise/timer primitives.
|
|
73
|
+
|
|
74
|
+
Unsupported unless the brief explicitly expands the runtime model:
|
|
75
|
+
- Async callbacks that later suspend with their own `await`.
|
|
76
|
+
- `for await...of` / async iterators.
|
|
77
|
+
- DOM event listeners.
|
|
78
|
+
- Worker/message boundaries.
|
|
79
|
+
- Shared callback registries.
|
|
80
|
+
- **`rebind` never auto-commits.** Always `--dry-run`-first, require `--yes` for write.
|
|
81
|
+
- **No regex on the bundle text.** All instrumentation goes through `@babel/traverse`.
|
|
82
|
+
- **The tracer initializes AFTER the v2 determinism preboot** (Date / Math.random / crypto stubs run first).
|
|
83
|
+
|
|
84
|
+
## Commands
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
bun test
|
|
88
|
+
bun run typecheck
|
|
89
|
+
./bin/ushman-characterize --help
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Validation rules
|
|
93
|
+
|
|
94
|
+
- `bun test` green; the `end-to-end.browser.test.ts` smoke test stays green (instrument → capture → generate → replay on a synthetic mini-bundle).
|
|
95
|
+
- `bun run typecheck` green.
|
|
96
|
+
- New phase code ⇒ doc update in README.
|
|
97
|
+
- The `VerifyReport` shape produced by `replay --strict` is **versioned**.
|
|
98
|
+
- If serializer or replay harness semantics change, update the runtime/support versioning story in the docs at the same time.
|
|
99
|
+
|
|
100
|
+
## LLM boundaries
|
|
101
|
+
|
|
102
|
+
Allowed:
|
|
103
|
+
- LLMs can run `replay --strict` as the green-bar gate during a decomposition brief.
|
|
104
|
+
- LLMs can be given a brief that says "extract `<symbol>` to `<path>`" and may run `rebind` after the operator approves.
|
|
105
|
+
- LLMs can propose new test cases for the purity classifier corpus.
|
|
106
|
+
|
|
107
|
+
Not allowed:
|
|
108
|
+
- LLMs **never edit files under `tests/replay/`, `tests/pure/`, `tests/scene/`** during a normal refactor brief. Tests are operator-owned.
|
|
109
|
+
- LLMs cannot bypass `--strict` and switch to `--lenient` without explicit operator sign-off in the brief.
|
|
110
|
+
- LLMs cannot delete trace fixtures.
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `ushman-characterize` will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
### Breaking Changes
|
|
8
|
+
|
|
9
|
+
- Hard-cut v4 workspace layout:
|
|
10
|
+
- characterize fixtures moved from `.ushman/test-harness/` to `.lab/characterize/`
|
|
11
|
+
- replay, pure, and scene tests are derived into `tests/{replay,pure,scene}/`
|
|
12
|
+
- capture now boots the candidate through Vite preview by default instead of the legacy static serve path
|
|
13
|
+
- v3 workspaces are refused with a migration hint instead of being read opportunistically
|
|
14
|
+
- Trace deduplication support version bumped:
|
|
15
|
+
- replay fixtures and trace JSONL files now treat `thisArg` snapshots and side-effect shapes as part of the uniqueness key
|
|
16
|
+
- regenerate captures and replay fixtures after upgrading so older deduped traces are not reused silently
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- Characterization and replay generation for anonymous default-exported callables via stable synthetic bindings such as `defaultExport`.
|
|
21
|
+
|
|
22
|
+
### Removed
|
|
23
|
+
|
|
24
|
+
- Seed-time scaffold population (`populateScaffolds`, `populateSmokeScaffolds`, `stub-pure|stub-states --regen-stale`, `tests/.seed-fingerprints.json` sidecar) and the legacy state-trace synthesis path. These were the consumer side of the pipeline-emitted test-corpus chain that v4.1 explicitly descoped. The manual operator surfaces (`stub-pure --bundle=`, `stub-states`, `capture`, `generate-replay`, `rebind`) are unchanged.
|
|
25
|
+
|
|
26
|
+
### Migration Notes
|
|
27
|
+
|
|
28
|
+
1. Move any workflow or CI references from `.ushman/test-harness/` to `.lab/characterize/`.
|
|
29
|
+
2. Update scripts that invoked stage-based bundle paths so they point at the candidate tree, for example `public/assets/...` or `src/...`.
|
|
30
|
+
3. Expect generated tests under `tests/replay/`, `tests/pure/`, and `tests/scene/`.
|
|
31
|
+
4. Re-run `ushman-characterize capture <ws> ...` and `ushman-characterize generate-replay <ws> ...` after upgrading to refresh the expanded dedupe contract.
|
|
32
|
+
5. If the workspace is still v3-shaped, restore the v3 toolchain first; `ushman migrate-v3-v4` is only a diagnostic stub in v4.
|
|
33
|
+
6. Drop any consumer of `populateScaffolds` / `populateSmokeScaffolds` / `--regen-stale`; operators now own seed-time test population for their own candidate repos.
|
|
34
|
+
|
|
35
|
+
## [0.1.0] - 2026-05-08
|
|
36
|
+
|
|
37
|
+
- Finished standalone link-readiness wiring for the CLI and public package exports.
|
|
38
|
+
- Added a shared trace serializer used by capture, replay harness generation, and runtime parity tests.
|
|
39
|
+
- Switched replay import rewriting from regexes to AST-based rewriting with dry-run previews.
|
|
40
|
+
- Added a versioned replay `VerifyReport` written to `.ushman/test-harness/reports/verify-report.json`.
|
|
41
|
+
- Added serializer, instrumentation, flag parsing, replay generation, and workspace-lock tests.
|
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ragaeeb Haq
|
|
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,193 @@
|
|
|
1
|
+
# ushman-characterize
|
|
2
|
+
|
|
3
|
+
> **Per-function regression detection for refactor safety.** Characterization tests for the post-Ushman cleanup phase: classify pure functions, instrument every top-level function/method, capture per-call args/returns at runtime, generate a per-function bun-test that fails the moment a refactor drifts.
|
|
4
|
+
|
|
5
|
+
This is a generalization of Michael Feathers' **characterization testing** discipline (capture observed behavior of legacy code so a refactor can verify behavioral equivalence) plus **trace-and-replay** (instrument once, replay forever).
|
|
6
|
+
|
|
7
|
+
The trace serializer has Three.js-aware brand-checks — but those come from the optional [`ushman-threejs-tools`](../ushman-threejs-tools) peer dependency. **For non-graphical donors (browser extensions, plain web apps, embedded webviews) the brand-checks are inert and the package operates on plain JS values.** Graphics support is opt-in.
|
|
8
|
+
|
|
9
|
+
## Status
|
|
10
|
+
|
|
11
|
+
**m35 is implemented in the `ushman` orchestrator** (`ushman characterize …`, `src/core/characterize/`). This repo is the standalone, link-ready extraction target for `ushman-characterize`.
|
|
12
|
+
**Runtime:** mixed. Instrumentation, replay, purity-classification, and generate-replay are pure Node. `capture` requires Puppeteer.
|
|
13
|
+
|
|
14
|
+
## What this package is NOT
|
|
15
|
+
|
|
16
|
+
- Not a code-coverage tool. `c8` / Istanbul tell us "this line was hit"; refactor safety needs "this function still produces the same output."
|
|
17
|
+
- Not a parity harness. That stays in `ushman` orchestrator's `parity/`.
|
|
18
|
+
- Not a snapshot tester. We do not write `expect.toMatchSnapshot()` files; we capture **traces** that survive normal refactoring.
|
|
19
|
+
- Not a mutation tester.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
bun install --global ushman-characterize
|
|
25
|
+
bun add ushman-characterize
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Breaking Changes
|
|
29
|
+
|
|
30
|
+
- v4 fixtures moved from `.ushman/test-harness/` to `.lab/characterize/`.
|
|
31
|
+
- Generated tests now live under `tests/{pure,scene,replay}/`.
|
|
32
|
+
- `capture` now boots the candidate via Vite preview by default.
|
|
33
|
+
- v3 workspaces are rejected with the standard v4 cutover stub hint.
|
|
34
|
+
|
|
35
|
+
## Local Dev
|
|
36
|
+
|
|
37
|
+
- This repo currently depends on a sibling checkout of `ushman-ledger` via `file:../ushman-ledger`.
|
|
38
|
+
- For local installs to work, keep `ushman-characterize` and `ushman-ledger` as sibling directories under the same parent.
|
|
39
|
+
|
|
40
|
+
## Quick start (the 4-phase loop)
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
ushman-characterize stub-pure <ws> --bundle=public/assets/bundle.js # Phase A
|
|
44
|
+
ushman-characterize stub-states <ws> # Phase B
|
|
45
|
+
ushman-characterize instrument --bundle=<in> --output=<out> --source-map=external # Phase C.1
|
|
46
|
+
ushman-characterize capture <ws> --bundle=<in> --states=0-lobby,1-game # Phase C.2
|
|
47
|
+
ushman-characterize generate-replay <ws> --bundle=<in> # Phase C.3
|
|
48
|
+
bun test tests/replay/ # run the generated tests
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
After the LLM (or operator) refactors a class out of the monolith:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
ushman-characterize rebind <ws> --map='.lab/characterize/modules/CameraController.mjs:CameraController=src/camera/CameraController.js'
|
|
55
|
+
bun test tests/replay/CameraController.*.test.ts
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Phases
|
|
59
|
+
|
|
60
|
+
| Phase | What | Effort to enable | Yield |
|
|
61
|
+
|-------|------|------------------|-------|
|
|
62
|
+
| A | Pure-function snapshot tests | <1 day | ~200+ tests on Capybara |
|
|
63
|
+
| B | Scene-state snapshot tests | <1 day | ~one test per captured state |
|
|
64
|
+
| C | Trace-and-replay (the main lift) | 5–7 days | ~400+ tests on Capybara |
|
|
65
|
+
| D | Module-contract rebind after extraction | <1 day | Tests follow the refactor |
|
|
66
|
+
|
|
67
|
+
## Public API
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
classifyPureTopLevelFunctions(opts): PureClassificationResult
|
|
71
|
+
readCapturedTraceState(opts): Promise<CapturedTraceRecord[]>
|
|
72
|
+
scaffoldPureCharacterizationTests(opts): Promise<{ pureFunctionCount, written, ... }>
|
|
73
|
+
scaffoldSceneCharacterizationTests(opts): Promise<{ written }>
|
|
74
|
+
populateScaffolds({
|
|
75
|
+
workspaceDir,
|
|
76
|
+
modulePathPrefix?,
|
|
77
|
+
symbolFilter?,
|
|
78
|
+
}): Promise<{ sidecarPath, written, skipped, synthesizedTraces, toolingGaps }>
|
|
79
|
+
populateSmokeScaffolds({
|
|
80
|
+
workspaceDir,
|
|
81
|
+
}): Promise<{ sidecarPath, written, skipped, toolingGaps }>
|
|
82
|
+
runStubPureCommand({
|
|
83
|
+
workspaceRoot,
|
|
84
|
+
bundlePath?,
|
|
85
|
+
regenStale?,
|
|
86
|
+
log,
|
|
87
|
+
}): Promise<...>
|
|
88
|
+
runStubStatesCommand({
|
|
89
|
+
workspaceRoot,
|
|
90
|
+
regenStale?,
|
|
91
|
+
log,
|
|
92
|
+
}): Promise<...>
|
|
93
|
+
canonicalizeSceneTree(value, opts?): unknown
|
|
94
|
+
instrumentBundle(opts): Promise<InstrumentResult>
|
|
95
|
+
captureCharacterization(opts): Promise<CaptureCharacterizationResult>
|
|
96
|
+
generateReplayCharacterization(opts): Promise<{ writtenTests, writtenFixtures }>
|
|
97
|
+
rewriteReplayImports(opts): Promise<{ files }>
|
|
98
|
+
createConsoleLogger(): Logger
|
|
99
|
+
canonicalizeTrace(value, opts?): unknown
|
|
100
|
+
type CaptureServerHost
|
|
101
|
+
type SceneInspectorDriver
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## CLI
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
ushman-characterize --help
|
|
108
|
+
ushman-characterize stub-pure <workspace> --bundle=<path>
|
|
109
|
+
ushman-characterize stub-pure <workspace> --regen-stale
|
|
110
|
+
ushman-characterize stub-states <workspace>
|
|
111
|
+
ushman-characterize stub-states <workspace> --regen-stale
|
|
112
|
+
ushman-characterize instrument --bundle=<path> --output=<path> [--source-map=external|inline|off]
|
|
113
|
+
ushman-characterize capture <workspace> [--bundle=...] [--states=a,b] [--scene-only] [--capture-side-effects] [--mode=preview|dev]
|
|
114
|
+
ushman-characterize generate-replay <workspace> --bundle=<path> [--max-cases=10]
|
|
115
|
+
ushman-characterize replay <workspace> [--strict|--lenient] [--filter=<pattern>]
|
|
116
|
+
ushman-characterize rebind <workspace> --map='symbol=path' [--map='workspace/module.mjs:symbol=path'] [--map=...] [--dry-run] [--yes]
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Notes:
|
|
120
|
+
|
|
121
|
+
- Normal `capture` requires `--bundle=<path>`. `--scene-only` is the only mode that can omit it.
|
|
122
|
+
- Characterize fixtures now live under `<ws>/.lab/characterize/` and derived tests live under `<ws>/tests/{pure,scene,replay}/`.
|
|
123
|
+
- `stub-pure --regen-stale` populates seed-time `tests/pure/**/*.test.ts` scaffolds only when the current file hash still matches `tests/.seed-fingerprints.json`. Operator-edited files are skipped unchanged.
|
|
124
|
+
- `stub-pure --regen-stale` consumes canonical module traces from `<ws>/.lab/characterize/traces/<module>.jsonl`. If only legacy state traces exist, characterize synthesizes the module trace once from unambiguous symbol matches and then reuses that file on later runs.
|
|
125
|
+
- `stub-pure --regen-stale` trusts the symbol list declared by existing `test.todo(...)` blocks. The populator does not re-enumerate exports from source files.
|
|
126
|
+
- Legacy state trace synthesis is intentionally conservative. If the same symbol name appears in more than one managed scaffold, characterize leaves those scaffolds untouched and records a tooling-gap instead of guessing which module owns the trace rows.
|
|
127
|
+
- `stub-states --regen-stale` populates seed-time `tests/smoke/<state>.test.ts` scaffolds when `<ws>/parity/baseline/<state>.png` exists, `tests/smoke/{config,diff,drive}.ts` already exist, and the target scaffold hash still matches the sidecar.
|
|
128
|
+
- Editing any auto-managed scaffold, even comments or whitespace, changes its fingerprint and makes future `--regen-stale` runs skip that file on purpose.
|
|
129
|
+
- V4 capture reads the state DAG only from `<ws>/.lab/state-dag.json`.
|
|
130
|
+
- `capture` boots the candidate through `vite build && vite preview` by default. Use `--mode=dev` only for a faster local smoke loop.
|
|
131
|
+
- `instrument` preserves source maps. The standalone CLI defaults to adjacent `.map` files; in-process capture uses inline maps so browser stack traces point back at the pre-instrumented bundle.
|
|
132
|
+
- `scene-only` capture needs a `SceneInspectorDriver`. The CLI will auto-load one from `ushman-threejs-tools/inspector` when that peer is installed.
|
|
133
|
+
- `capture` is partial-success aware: successful earlier states are kept, failed later states are reported with a yellow validator verdict, and oversized existing trace files fail only the affected state. The result now surfaces `attemptedStates`, `completedStates`, and `skippedStates` alongside `failures`.
|
|
134
|
+
- `replay` writes a versioned report to `.lab/characterize/reports/verify-report.json`.
|
|
135
|
+
- Trace JSONL records and replay fixture JSON files are explicitly versioned. The generated replay harness rejects incompatible fixture/support versions with a regeneration hint.
|
|
136
|
+
- `capture`, `rebind`, and seed-scaffold population emit `validator-result` ledger entries through `ushman-ledger` on every run. Missing traces or smoke baselines are recorded as `tooling-gap` notes instead of silently generating empty tests.
|
|
137
|
+
- `rebind --dry-run` prints before/after import previews for generated replay tests.
|
|
138
|
+
- Mixed replay imports are split per destination. Unmapped specifiers remain on the generated module import.
|
|
139
|
+
- Shorthand `rebind --map='symbol=path'` only works when that symbol is unambiguous across generated replay imports. Use file-scoped mappings when the same symbol name appears in more than one generated module.
|
|
140
|
+
- Anonymous default-exported callables are traced and replayed under a synthetic binding name. The default synthetic name is `defaultExport`; if that name is already taken in the bundle, generation picks `defaultExport_2`, `defaultExport_3`, and so on.
|
|
141
|
+
- Async side-effect attribution survives native `await` inside traced top-level functions, plus synchronous callbacks registered from an active traced chunk through `Promise.then`, `queueMicrotask`, `setTimeout`, `setInterval`, and `requestAnimationFrame`.
|
|
142
|
+
- Unsupported async attribution is explicit: callbacks that later suspend with their own `await`, `for await...of` / async-iterator control flow, DOM/event-listener callbacks fired by unrelated user activity, worker/message boundaries, and shared callback registries are not treated as reliable ownership signals.
|
|
143
|
+
|
|
144
|
+
Seed scaffold sidecar format:
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"schemaVersion": "shibuk-seed-fingerprints/v1",
|
|
149
|
+
"scaffolds": {
|
|
150
|
+
"tests/pure/util/math.test.ts": "<sha256>",
|
|
151
|
+
"tests/smoke/lobby.test.ts": "<sha256>"
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Serializer Contract
|
|
157
|
+
|
|
158
|
+
Trace canonicalization is shared across Node, the browser tracer, and the generated replay harness. The serializer currently covers:
|
|
159
|
+
|
|
160
|
+
- special numeric values (`NaN`, `Infinity`, `-Infinity`, `-0`)
|
|
161
|
+
- cycles and repeated references
|
|
162
|
+
- `ArrayBuffer` and typed-array views
|
|
163
|
+
- `Map`, `Set`, plain objects, and instance-like objects
|
|
164
|
+
- Three.js-like values including `Vector2`, `Vector3`, `Vector4`, `Euler`, `Quaternion`, `Matrix4`, `Color`, `Object3D`, `BufferGeometry`, `Material`, and `Texture`
|
|
165
|
+
|
|
166
|
+
By default floats are rounded to 4 decimals, object `uuid` fields are stripped from generic object payloads, each state trace is capped at 50 MB, and capture refuses to load more than 25,000 unique trace records for a single state without operator intervention.
|
|
167
|
+
|
|
168
|
+
Trace deduplication keys include the function identity, canonicalized arguments, receiver snapshot, return/throw payload, and captured side-effect shape. Calls that keep the same args and return value but arrive with a different `thisArg` snapshot or a different side-effect sequence are preserved as separate replay cases.
|
|
169
|
+
|
|
170
|
+
Trace JSONL records carry `ushman.characterize.trace-record@1`, replay fixtures carry `ushman.characterize.replay-fixture@1`, and both also store an explicit support version. When serializer or harness semantics change, bump the support version in lockstep with the replay report schema whenever old generated fixtures or harness files would no longer be trustworthy without regeneration.
|
|
171
|
+
|
|
172
|
+
## Format Version Migration
|
|
173
|
+
|
|
174
|
+
When serializer, capture, or replay semantics change in a way that makes older traces untrustworthy:
|
|
175
|
+
|
|
176
|
+
1. Bump `CHARACTERIZE_SUPPORT_VERSION` in [src/constants.ts](/Users/rhaq/workspace/ushman-characterize/src/constants.ts).
|
|
177
|
+
2. Re-run `ushman-characterize capture <ws> ...` to regenerate trace JSONL files.
|
|
178
|
+
3. Re-run `ushman-characterize generate-replay <ws> ...` to regenerate replay fixtures and generated tests.
|
|
179
|
+
4. Re-run `ushman-characterize replay <ws> --strict` to confirm the regenerated harness is green.
|
|
180
|
+
|
|
181
|
+
Schema-version bumps are reserved for structural format changes to the trace or replay fixture documents. Support-version bumps cover semantic/runtime changes where old files still parse but should no longer be trusted without regeneration.
|
|
182
|
+
|
|
183
|
+
## Why this exists separate from `ushman`
|
|
184
|
+
|
|
185
|
+
The decomposition campaign is **regular SWE work post-Ushman, enabled by m35** (per `IMPLEMENTATION-PLAN.md`'s `m35` row). Cloud LLMs and CI integrations that drive refactors should be able to import the trace harness without cloning the entire reverse-engineering pipeline.
|
|
186
|
+
|
|
187
|
+
## Where this fits in the family
|
|
188
|
+
|
|
189
|
+
| | |
|
|
190
|
+
|---|---|
|
|
191
|
+
| **Depends on** | `ushman-threejs-tools` (peer — for `Vector3` / `Quaternion` / `Object3D` brand checks in the trace serializer) |
|
|
192
|
+
| **Verified by** | `ushman-verify` Tier 0c+0d (the instrumented bundle must Babel-parse and have no undefined references) |
|
|
193
|
+
| **Runs alongside** | `ushman parity` for the full safety net during a decomposition campaign |
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ushman-characterize CLI wrapper. Symlinked from ~/.bun/bin/ushman-characterize
|
|
3
|
+
# after `bun link` so operators can invoke `ushman-characterize <args>` directly.
|
|
4
|
+
# Resolves this script's real location through any symlink chain and hands off to
|
|
5
|
+
# bun, which executes the TypeScript entrypoint.
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
SELF="$0"
|
|
9
|
+
while [ -L "$SELF" ]; do
|
|
10
|
+
LINK_TARGET="$(readlink "$SELF")"
|
|
11
|
+
case "$LINK_TARGET" in
|
|
12
|
+
/*) SELF="$LINK_TARGET" ;;
|
|
13
|
+
*) SELF="$(dirname "$SELF")/$LINK_TARGET" ;;
|
|
14
|
+
esac
|
|
15
|
+
done
|
|
16
|
+
HERE="$(cd "$(dirname "$SELF")" && pwd)"
|
|
17
|
+
PACKAGE_ROOT="$(cd "$HERE/.." && pwd)"
|
|
18
|
+
|
|
19
|
+
exec "${BUN_BIN:-bun}" run "$PACKAGE_ROOT/src/cli.ts" "$@"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type * as t from '@babel/types';
|
|
2
|
+
export declare const BABEL_PLUGINS: readonly ["asyncGenerators", "classPrivateMethods", "classPrivateProperties", "classProperties", "dynamicImport", "importAssertions", "jsx", "topLevelAwait", "typescript"];
|
|
3
|
+
export declare const parseModuleAst: ({ source, sourcePath }: {
|
|
4
|
+
readonly source: string;
|
|
5
|
+
readonly sourcePath: string;
|
|
6
|
+
}) => t.File;
|
|
7
|
+
//# sourceMappingURL=babel-config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"babel-config.d.ts","sourceRoot":"","sources":["../src/babel-config.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,CAAC,MAAM,cAAc,CAAC;AAEvC,eAAO,MAAM,aAAa,6KAUhB,CAAC;AAEX,eAAO,MAAM,cAAc,GAAI,wBAAwB;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAAE,KAKrG,CAAC,CAAC,IAAI,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { parse } from '@babel/parser';
|
|
2
|
+
export const BABEL_PLUGINS = [
|
|
3
|
+
'asyncGenerators',
|
|
4
|
+
'classPrivateMethods',
|
|
5
|
+
'classPrivateProperties',
|
|
6
|
+
'classProperties',
|
|
7
|
+
'dynamicImport',
|
|
8
|
+
'importAssertions',
|
|
9
|
+
'jsx',
|
|
10
|
+
'topLevelAwait',
|
|
11
|
+
'typescript',
|
|
12
|
+
];
|
|
13
|
+
export const parseModuleAst = ({ source, sourcePath }) => parse(source, {
|
|
14
|
+
plugins: [...BABEL_PLUGINS],
|
|
15
|
+
sourceFilename: sourcePath,
|
|
16
|
+
sourceType: 'module',
|
|
17
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { CaptureServerHost } from './types.ts';
|
|
2
|
+
type CaptureServerMode = 'dev' | 'preview';
|
|
3
|
+
type CaptureServerProcess = {
|
|
4
|
+
readonly exited: Promise<number>;
|
|
5
|
+
readonly kill: () => void;
|
|
6
|
+
readonly stderr?: null | ReadableStream<Uint8Array>;
|
|
7
|
+
readonly stdout?: null | ReadableStream<Uint8Array>;
|
|
8
|
+
};
|
|
9
|
+
type OutputTracker = {
|
|
10
|
+
readonly pending: readonly Promise<void>[];
|
|
11
|
+
readonly urls: Set<string>;
|
|
12
|
+
text: string;
|
|
13
|
+
};
|
|
14
|
+
export declare const createViteCaptureServerHost: ({ mode, }?: {
|
|
15
|
+
readonly mode?: CaptureServerMode;
|
|
16
|
+
}) => CaptureServerHost;
|
|
17
|
+
export declare const __testOnly: {
|
|
18
|
+
buildServeCommand: ({ mode, port }: {
|
|
19
|
+
readonly mode: CaptureServerMode;
|
|
20
|
+
readonly port?: number;
|
|
21
|
+
}) => string[];
|
|
22
|
+
closeProcess: (process: CaptureServerProcess) => Promise<void>;
|
|
23
|
+
waitForServerReady: ({ fallbackUrl, process, timeoutMs, tracker, }: {
|
|
24
|
+
readonly fallbackUrl: null | string;
|
|
25
|
+
readonly process: CaptureServerProcess;
|
|
26
|
+
readonly timeoutMs?: number;
|
|
27
|
+
readonly tracker: OutputTracker;
|
|
28
|
+
}) => Promise<string>;
|
|
29
|
+
};
|
|
30
|
+
export {};
|
|
31
|
+
//# sourceMappingURL=capture-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capture-server.d.ts","sourceRoot":"","sources":["../src/capture-server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAQpD,KAAK,iBAAiB,GAAG,KAAK,GAAG,SAAS,CAAC;AAC3C,KAAK,oBAAoB,GAAG;IACxB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,IAAI,CAAC;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,IAAI,GAAG,cAAc,CAAC,UAAU,CAAC,CAAC;IACpD,QAAQ,CAAC,MAAM,CAAC,EAAE,IAAI,GAAG,cAAc,CAAC,UAAU,CAAC,CAAC;CACvD,CAAC;AACF,KAAK,aAAa,GAAG;IACjB,QAAQ,CAAC,OAAO,EAAE,SAAS,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;IAC3C,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;CAChB,CAAC;AAoNF,eAAO,MAAM,2BAA2B,GAAI,YAEzC;IACC,QAAQ,CAAC,IAAI,CAAC,EAAE,iBAAiB,CAAC;CAChC,KAAG,iBAmCP,CAAC;AAEH,eAAO,MAAM,UAAU;wCAlPoB;QAAE,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;QAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE;4BAkClE,oBAAoB;wEAqItD;QACC,QAAQ,CAAC,WAAW,EAAE,IAAI,GAAG,MAAM,CAAC;QACpC,QAAQ,CAAC,OAAO,EAAE,oBAAoB,CAAC;QACvC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAC5B,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;KACnC;CA0EA,CAAC"}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
const BUILD_TIMEOUT_MS = 5 * 60 * 1000;
|
|
2
|
+
const DEFAULT_READY_TIMEOUT_MS = 20_000;
|
|
3
|
+
const HOSTNAME = '127.0.0.1';
|
|
4
|
+
const READY_POLL_MS = 100;
|
|
5
|
+
const SERVER_URL_PATTERN = /https?:\/\/(?:127\.0\.0\.1|localhost):\d+\/?/gu;
|
|
6
|
+
const decoder = new TextDecoder();
|
|
7
|
+
const toNormalizedUrl = (value) => (value.endsWith('/') ? value : `${value}/`);
|
|
8
|
+
const buildServeCommand = ({ mode, port }) => {
|
|
9
|
+
const args = ['bun', 'run', mode === 'preview' ? 'preview' : 'dev', '--', '--host', HOSTNAME];
|
|
10
|
+
if (typeof port === 'number' && port > 0) {
|
|
11
|
+
args.push('--port', `${port}`);
|
|
12
|
+
if (mode === 'preview') {
|
|
13
|
+
args.push('--strictPort');
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return args;
|
|
17
|
+
};
|
|
18
|
+
const renderSpawnFailure = ({ command, cwd, output, result, }) => {
|
|
19
|
+
const suffix = output.length > 0 ? `\n${output}` : '';
|
|
20
|
+
return `${command.join(' ')} failed in ${cwd} with exit code ${result.exitCode}.${suffix}`;
|
|
21
|
+
};
|
|
22
|
+
const readProcessOutput = async (process) => {
|
|
23
|
+
const [stdout, stderr] = await Promise.all([
|
|
24
|
+
process.stdout ? new Response(process.stdout).text() : Promise.resolve(''),
|
|
25
|
+
process.stderr ? new Response(process.stderr).text() : Promise.resolve(''),
|
|
26
|
+
]);
|
|
27
|
+
return [stdout, stderr].filter(Boolean).join('\n').trim();
|
|
28
|
+
};
|
|
29
|
+
const closeProcess = async (process) => {
|
|
30
|
+
try {
|
|
31
|
+
process.kill();
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// process may already be gone
|
|
35
|
+
}
|
|
36
|
+
await process.exited.catch(() => undefined);
|
|
37
|
+
};
|
|
38
|
+
const runPreviewBuild = async (workspaceRoot) => {
|
|
39
|
+
const command = ['bun', 'run', 'build'];
|
|
40
|
+
const process = Bun.spawn(command, {
|
|
41
|
+
cwd: workspaceRoot,
|
|
42
|
+
stderr: 'pipe',
|
|
43
|
+
stdin: 'ignore',
|
|
44
|
+
stdout: 'pipe',
|
|
45
|
+
});
|
|
46
|
+
let timedOut = false;
|
|
47
|
+
const timeoutId = setTimeout(() => {
|
|
48
|
+
timedOut = true;
|
|
49
|
+
void closeProcess(process);
|
|
50
|
+
}, BUILD_TIMEOUT_MS);
|
|
51
|
+
const exitCode = await process.exited;
|
|
52
|
+
clearTimeout(timeoutId);
|
|
53
|
+
if (exitCode === 0) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const output = await readProcessOutput(process);
|
|
57
|
+
if (timedOut) {
|
|
58
|
+
throw new Error(`bun run build timed out in ${workspaceRoot} after ${BUILD_TIMEOUT_MS}ms.${output ? `\n${output}` : ''}`);
|
|
59
|
+
}
|
|
60
|
+
throw new Error(renderSpawnFailure({
|
|
61
|
+
command,
|
|
62
|
+
cwd: workspaceRoot,
|
|
63
|
+
output,
|
|
64
|
+
result: {
|
|
65
|
+
exitCode,
|
|
66
|
+
},
|
|
67
|
+
}));
|
|
68
|
+
};
|
|
69
|
+
const appendOutput = (tracker, chunk) => {
|
|
70
|
+
if (chunk.length === 0) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
tracker.text += chunk;
|
|
74
|
+
for (const match of tracker.text.matchAll(SERVER_URL_PATTERN)) {
|
|
75
|
+
const value = match[0];
|
|
76
|
+
if (value) {
|
|
77
|
+
tracker.urls.add(toNormalizedUrl(value));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
const consumeStream = async (stream, tracker) => {
|
|
82
|
+
const reader = stream.getReader();
|
|
83
|
+
try {
|
|
84
|
+
while (true) {
|
|
85
|
+
const { done, value } = await reader.read();
|
|
86
|
+
if (done) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
appendOutput(tracker, decoder.decode(value, { stream: true }));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
reader.releaseLock();
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const createOutputTracker = (process) => {
|
|
97
|
+
const tracker = {
|
|
98
|
+
pending: [],
|
|
99
|
+
text: '',
|
|
100
|
+
urls: new Set(),
|
|
101
|
+
};
|
|
102
|
+
const pending = [
|
|
103
|
+
process.stdout ? consumeStream(process.stdout, tracker) : Promise.resolve(),
|
|
104
|
+
process.stderr ? consumeStream(process.stderr, tracker) : Promise.resolve(),
|
|
105
|
+
];
|
|
106
|
+
return {
|
|
107
|
+
...tracker,
|
|
108
|
+
pending,
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
const wait = async (ms) => {
|
|
112
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
113
|
+
};
|
|
114
|
+
const collectCandidateUrls = ({ fallbackUrl, tracker, }) => {
|
|
115
|
+
const candidates = new Set(fallbackUrl ? [fallbackUrl] : []);
|
|
116
|
+
for (const url of tracker.urls) {
|
|
117
|
+
candidates.add(url);
|
|
118
|
+
}
|
|
119
|
+
return candidates;
|
|
120
|
+
};
|
|
121
|
+
const firstReachableUrl = async (candidateUrls) => {
|
|
122
|
+
for (const candidateUrl of candidateUrls) {
|
|
123
|
+
try {
|
|
124
|
+
const response = await fetch(candidateUrl);
|
|
125
|
+
if (response.status < 500) {
|
|
126
|
+
return candidateUrl;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// ignore transient startup failures while the server boots
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
};
|
|
135
|
+
const formatTrackerOutput = (tracker) => {
|
|
136
|
+
const text = tracker.text.trim();
|
|
137
|
+
return text.length > 0 ? `\n${text}` : '';
|
|
138
|
+
};
|
|
139
|
+
const waitForServerReady = async ({ fallbackUrl, process, timeoutMs = DEFAULT_READY_TIMEOUT_MS, tracker, }) => {
|
|
140
|
+
let exitCode = null;
|
|
141
|
+
void process.exited.then((code) => {
|
|
142
|
+
exitCode = code;
|
|
143
|
+
});
|
|
144
|
+
const deadline = Date.now() + timeoutMs;
|
|
145
|
+
while (Date.now() < deadline) {
|
|
146
|
+
const reachableUrl = await firstReachableUrl(collectCandidateUrls({
|
|
147
|
+
fallbackUrl,
|
|
148
|
+
tracker,
|
|
149
|
+
}));
|
|
150
|
+
if (reachableUrl) {
|
|
151
|
+
return reachableUrl;
|
|
152
|
+
}
|
|
153
|
+
if (exitCode !== null) {
|
|
154
|
+
throw new Error(`Capture server exited before becoming ready (exitCode=${exitCode}).${formatTrackerOutput(tracker)}`);
|
|
155
|
+
}
|
|
156
|
+
await wait(READY_POLL_MS);
|
|
157
|
+
}
|
|
158
|
+
throw new Error(`Timed out waiting for capture server.${formatTrackerOutput(tracker)}`);
|
|
159
|
+
};
|
|
160
|
+
export const createViteCaptureServerHost = ({ mode = 'preview', } = {}) => ({
|
|
161
|
+
async serve(workspaceRoot, port) {
|
|
162
|
+
if (mode === 'preview') {
|
|
163
|
+
await runPreviewBuild(workspaceRoot);
|
|
164
|
+
}
|
|
165
|
+
const requestedPort = port > 0 ? port : undefined;
|
|
166
|
+
const fallbackUrl = requestedPort ? `http://${HOSTNAME}:${requestedPort}/` : null;
|
|
167
|
+
const process = Bun.spawn(buildServeCommand({ mode, port: requestedPort }), {
|
|
168
|
+
cwd: workspaceRoot,
|
|
169
|
+
stderr: 'pipe',
|
|
170
|
+
stdin: 'ignore',
|
|
171
|
+
stdout: 'pipe',
|
|
172
|
+
});
|
|
173
|
+
const tracker = createOutputTracker(process);
|
|
174
|
+
try {
|
|
175
|
+
const url = await waitForServerReady({
|
|
176
|
+
fallbackUrl,
|
|
177
|
+
process,
|
|
178
|
+
tracker,
|
|
179
|
+
});
|
|
180
|
+
return {
|
|
181
|
+
close: async () => {
|
|
182
|
+
await closeProcess(process);
|
|
183
|
+
await Promise.allSettled(tracker.pending);
|
|
184
|
+
},
|
|
185
|
+
url,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
await closeProcess(process);
|
|
190
|
+
await Promise.allSettled(tracker.pending);
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
export const __testOnly = {
|
|
196
|
+
buildServeCommand,
|
|
197
|
+
closeProcess,
|
|
198
|
+
waitForServerReady,
|
|
199
|
+
};
|