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
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export const parseKeyValueFlags = (args, options = {}) => {
|
|
2
|
+
const positionals = [];
|
|
3
|
+
const flags = new Map();
|
|
4
|
+
const booleanFlags = new Set(options.booleanFlags ?? []);
|
|
5
|
+
const stringFlags = new Set(options.stringFlags ?? []);
|
|
6
|
+
const knownFlags = new Set([...booleanFlags, ...stringFlags]);
|
|
7
|
+
const parseInlineFlag = (arg) => {
|
|
8
|
+
if (!arg.startsWith('--') || !arg.includes('=')) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const [key, ...rest] = arg.slice(2).split('=');
|
|
12
|
+
if (!key) {
|
|
13
|
+
throw new Error(`Unknown flag: ${arg}`);
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
key,
|
|
17
|
+
value: rest.join('='),
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
const ensureKnownFlag = (key, rawArg) => {
|
|
21
|
+
if (!knownFlags.has(key)) {
|
|
22
|
+
throw new Error(`Unknown flag: ${rawArg}`);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const consumeSeparateValue = (key, index) => {
|
|
26
|
+
const next = args[index + 1];
|
|
27
|
+
if (!next || next.startsWith('-')) {
|
|
28
|
+
throw new Error(`Missing value for --${key}`);
|
|
29
|
+
}
|
|
30
|
+
return next;
|
|
31
|
+
};
|
|
32
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
33
|
+
const arg = args[index];
|
|
34
|
+
if (!arg.startsWith('-')) {
|
|
35
|
+
positionals.push(arg);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const inlineFlag = parseInlineFlag(arg);
|
|
39
|
+
if (inlineFlag) {
|
|
40
|
+
ensureKnownFlag(inlineFlag.key, arg);
|
|
41
|
+
if (booleanFlags.has(inlineFlag.key)) {
|
|
42
|
+
throw new Error(`Flag does not take a value: --${inlineFlag.key}`);
|
|
43
|
+
}
|
|
44
|
+
if (inlineFlag.value.length === 0) {
|
|
45
|
+
throw new Error(`Missing value for --${inlineFlag.key}`);
|
|
46
|
+
}
|
|
47
|
+
flags.set(inlineFlag.key, inlineFlag.value);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const key = arg.replace(/^-{1,2}/u, '');
|
|
51
|
+
ensureKnownFlag(key, arg);
|
|
52
|
+
if (booleanFlags.has(key)) {
|
|
53
|
+
flags.set(key, true);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
flags.set(key, consumeSeparateValue(key, index));
|
|
57
|
+
index += 1;
|
|
58
|
+
}
|
|
59
|
+
return { flags, positionals };
|
|
60
|
+
};
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type Logger } from './cli/logger.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Programmatic entry point for seed-time pure scaffold population and the legacy manual
|
|
4
|
+
* `stub-pure --bundle=...` flow.
|
|
5
|
+
*/
|
|
6
|
+
export declare const runStubPureCommand: ({ bundlePath, log, regenStale, workspaceRoot, }: {
|
|
7
|
+
readonly bundlePath?: string;
|
|
8
|
+
readonly log: Logger;
|
|
9
|
+
readonly regenStale?: boolean;
|
|
10
|
+
readonly workspaceRoot: string;
|
|
11
|
+
}) => Promise<import("./stub-pure.ts").PopulateScaffoldsResult | {
|
|
12
|
+
autoAssertionCount: number;
|
|
13
|
+
pureFunctionCount: number;
|
|
14
|
+
stubCount: number;
|
|
15
|
+
totalTopLevelFunctions: number;
|
|
16
|
+
written: string[];
|
|
17
|
+
} | {
|
|
18
|
+
sidecarPath: string;
|
|
19
|
+
skipped: string[];
|
|
20
|
+
toolingGaps: string[];
|
|
21
|
+
written: string[];
|
|
22
|
+
}>;
|
|
23
|
+
/**
|
|
24
|
+
* Programmatic entry point for seed-time smoke scaffold population and the legacy manual
|
|
25
|
+
* `stub-states` screenshot scaffold flow.
|
|
26
|
+
*/
|
|
27
|
+
export declare const runStubStatesCommand: ({ log, regenStale, workspaceRoot, }: {
|
|
28
|
+
readonly log: Logger;
|
|
29
|
+
readonly regenStale?: boolean;
|
|
30
|
+
readonly workspaceRoot: string;
|
|
31
|
+
}) => Promise<import("./scene.ts").PopulateSmokeScaffoldsResult | {
|
|
32
|
+
stale: string[];
|
|
33
|
+
written: string[];
|
|
34
|
+
}>;
|
|
35
|
+
/**
|
|
36
|
+
* Run the standalone characterize CLI.
|
|
37
|
+
*/
|
|
38
|
+
export declare const runCharacterizeCommand: (args: readonly string[]) => Promise<number>;
|
|
39
|
+
//# sourceMappingURL=cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAGA,OAAO,EAAuB,KAAK,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAsLnE;;;GAGG;AACH,eAAO,MAAM,kBAAkB,GAAU,iDAKtC;IACC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAClC;;;;;;;;aA7CkB,MAAM,EAAE;;aAER,MAAM,EAAE;EA4F1B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,oBAAoB,GAAU,qCAIxC;IACC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAClC;;;EA2CA,CAAC;AAuMF;;GAEG;AACH,eAAO,MAAM,sBAAsB,GAAU,MAAM,SAAS,MAAM,EAAE,KAAG,OAAO,CAAC,MAAM,CA2BpF,CAAC"}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import { mkdir } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { captureCharacterization } from "./capture.js";
|
|
4
|
+
import { createConsoleLogger } from "./cli/logger.js";
|
|
5
|
+
import { parseKeyValueFlags } from "./cli/parse-flags.js";
|
|
6
|
+
import { generateReplayCharacterization } from "./generate-replay.js";
|
|
7
|
+
import { instrumentBundle } from "./instrument.js";
|
|
8
|
+
import { recordCharacterizeToolingGap, recordCharacterizeValidatorResult } from "./ledger.js";
|
|
9
|
+
import { parseRebindMapping, rewriteReplayImports } from "./rebind.js";
|
|
10
|
+
import { createVerifyReport } from "./replay-report.js";
|
|
11
|
+
import { populateSmokeScaffolds, scaffoldSceneCharacterizationTests } from "./scene.js";
|
|
12
|
+
import { SEED_FINGERPRINT_SIDECAR_RELATIVE_PATH } from "./seed-scaffolds.js";
|
|
13
|
+
import { populateScaffolds, scaffoldPureCharacterizationTests } from "./stub-pure.js";
|
|
14
|
+
import { workspaceHarnessPaths } from "./trace-format.js";
|
|
15
|
+
import { assertV4Workspace, withPipelineLock } from "./workspace-paths.js";
|
|
16
|
+
const HELP_TEXT = `Usage: ushman-characterize <subcommand> [workspace] [options]
|
|
17
|
+
|
|
18
|
+
Subcommands:
|
|
19
|
+
stub-pure Generate pure-function characterization tests.
|
|
20
|
+
stub-states Generate scene characterization tests from screenshot baselines.
|
|
21
|
+
instrument Write an instrumented bundle to disk.
|
|
22
|
+
capture Capture live traces from a workspace stage.
|
|
23
|
+
generate-replay Generate replay fixtures and Bun tests from captured traces.
|
|
24
|
+
replay Run generated replay tests in strict or lenient mode.
|
|
25
|
+
rebind Rewrite generated replay imports to extracted source files.
|
|
26
|
+
|
|
27
|
+
Examples:
|
|
28
|
+
ushman-characterize stub-pure . --bundle=public/assets/index.js
|
|
29
|
+
ushman-characterize stub-pure . --regen-stale
|
|
30
|
+
ushman-characterize stub-states . --regen-stale
|
|
31
|
+
ushman-characterize capture . --bundle=public/assets/index.js --states=lobby --mode=preview
|
|
32
|
+
ushman-characterize instrument --bundle=public/assets/index.js --output=public/assets/index.instrumented.js --source-map=external
|
|
33
|
+
ushman-characterize rebind . --map='.lab/characterize/modules/CameraController.mjs:CameraController=src/camera/CameraController.js' --dry-run
|
|
34
|
+
|
|
35
|
+
Defaults:
|
|
36
|
+
capture --mode=preview
|
|
37
|
+
capture --capture-side-effects=false
|
|
38
|
+
generate-replay --max-cases=10
|
|
39
|
+
instrument --source-map=external
|
|
40
|
+
|
|
41
|
+
Notes:
|
|
42
|
+
capture, rebind, and scaffold population emit characterize validator-result ledger entries
|
|
43
|
+
`;
|
|
44
|
+
const VALID_SOURCE_MAP_MODES = ['external', 'inline', 'off'];
|
|
45
|
+
const parseStateList = (raw) => (raw ?? '')
|
|
46
|
+
.split(',')
|
|
47
|
+
.map((entry) => entry.trim())
|
|
48
|
+
.filter(Boolean);
|
|
49
|
+
const resolveFromWorkspace = (workspaceRoot, value) => path.isAbsolute(value) ? value : path.join(workspaceRoot, value);
|
|
50
|
+
const parseWorkspaceRoot = (positionals) => path.resolve(positionals[0] ?? process.cwd());
|
|
51
|
+
const resolveSourceMapMode = (value) => {
|
|
52
|
+
if (value === undefined) {
|
|
53
|
+
return 'external';
|
|
54
|
+
}
|
|
55
|
+
if (VALID_SOURCE_MAP_MODES.includes(value)) {
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
throw new Error(`Invalid --source-map="${value}". Use: ${VALID_SOURCE_MAP_MODES.join(', ')}.`);
|
|
59
|
+
};
|
|
60
|
+
const renderRebindPreview = ({ after, before, filePath, }) => `${filePath}
|
|
61
|
+
--- before
|
|
62
|
+
${before}
|
|
63
|
+
+++ after
|
|
64
|
+
${after}`;
|
|
65
|
+
const parseRebindMappings = (args) => {
|
|
66
|
+
const mappings = new Map();
|
|
67
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
68
|
+
const value = args[index];
|
|
69
|
+
if (!value) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (value === '--map') {
|
|
73
|
+
const pair = args[index + 1];
|
|
74
|
+
if (!pair || pair.startsWith('-')) {
|
|
75
|
+
throw new Error('Missing value for --map');
|
|
76
|
+
}
|
|
77
|
+
index += 1;
|
|
78
|
+
const mapping = parseRebindMapping(pair);
|
|
79
|
+
mappings.set(mapping.selector, mapping.target);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (value.startsWith('--map=')) {
|
|
83
|
+
const mapping = parseRebindMapping(value.slice('--map='.length));
|
|
84
|
+
mappings.set(mapping.selector, mapping.target);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return mappings;
|
|
88
|
+
};
|
|
89
|
+
const resolveReplayMode = ({ lenient, mode, strict, }) => {
|
|
90
|
+
const explicitModes = [strict, lenient, mode !== undefined].filter(Boolean).length;
|
|
91
|
+
if (strict && lenient) {
|
|
92
|
+
throw new Error('Choose either --strict or --lenient, not both.');
|
|
93
|
+
}
|
|
94
|
+
if (explicitModes > 1 && mode !== undefined) {
|
|
95
|
+
throw new Error('Use either --mode=<strict|lenient> or the boolean mode flags, not both.');
|
|
96
|
+
}
|
|
97
|
+
if (strict) {
|
|
98
|
+
return 'strict';
|
|
99
|
+
}
|
|
100
|
+
if (lenient) {
|
|
101
|
+
return 'lenient';
|
|
102
|
+
}
|
|
103
|
+
if (mode === undefined) {
|
|
104
|
+
return 'strict';
|
|
105
|
+
}
|
|
106
|
+
if (mode !== 'lenient' && mode !== 'strict') {
|
|
107
|
+
throw new Error(`Unsupported replay mode "${mode}". Use strict or lenient.`);
|
|
108
|
+
}
|
|
109
|
+
return mode;
|
|
110
|
+
};
|
|
111
|
+
const buildPopulateFailureResult = ({ summary, workspaceRoot, }) => ({
|
|
112
|
+
sidecarPath: path.join(workspaceRoot, SEED_FINGERPRINT_SIDECAR_RELATIVE_PATH),
|
|
113
|
+
skipped: [],
|
|
114
|
+
toolingGaps: [summary],
|
|
115
|
+
written: [],
|
|
116
|
+
});
|
|
117
|
+
const handlePopulateCommandFailure = async ({ error, log, subcommand, workspaceRoot, }) => {
|
|
118
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
119
|
+
const summary = `${subcommand} population failed: ${message}`;
|
|
120
|
+
await recordCharacterizeToolingGap({
|
|
121
|
+
body: `${summary}\nSeed should continue, but this workspace needs manual follow-up before characterize scaffolds can be trusted.`,
|
|
122
|
+
summary,
|
|
123
|
+
workspaceRoot,
|
|
124
|
+
});
|
|
125
|
+
await recordCharacterizeValidatorResult({
|
|
126
|
+
summary,
|
|
127
|
+
verdict: 'yellow',
|
|
128
|
+
workspaceRoot,
|
|
129
|
+
});
|
|
130
|
+
log.warn(summary);
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* Programmatic entry point for seed-time pure scaffold population and the legacy manual
|
|
134
|
+
* `stub-pure --bundle=...` flow.
|
|
135
|
+
*/
|
|
136
|
+
export const runStubPureCommand = async ({ bundlePath, log, regenStale = false, workspaceRoot, }) => {
|
|
137
|
+
if (regenStale) {
|
|
138
|
+
try {
|
|
139
|
+
const result = await withPipelineLock({
|
|
140
|
+
command: 'ushman-characterize stub-pure --regen-stale',
|
|
141
|
+
work: async () => populateScaffolds({
|
|
142
|
+
workspaceDir: workspaceRoot,
|
|
143
|
+
}),
|
|
144
|
+
workspaceRoot,
|
|
145
|
+
});
|
|
146
|
+
log.info(`populated ${result.written.length} pure scaffold file(s)`);
|
|
147
|
+
for (const gap of result.toolingGaps) {
|
|
148
|
+
log.warn(gap);
|
|
149
|
+
}
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (typeof error.exitCode === 'number') {
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
await handlePopulateCommandFailure({
|
|
157
|
+
error,
|
|
158
|
+
log,
|
|
159
|
+
subcommand: 'stub-pure',
|
|
160
|
+
workspaceRoot,
|
|
161
|
+
});
|
|
162
|
+
return buildPopulateFailureResult({
|
|
163
|
+
summary: `stub-pure population failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
164
|
+
workspaceRoot,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (!bundlePath) {
|
|
169
|
+
throw new Error('stub-pure requires --bundle=<path>.');
|
|
170
|
+
}
|
|
171
|
+
const result = await withPipelineLock({
|
|
172
|
+
command: 'ushman-characterize stub-pure',
|
|
173
|
+
work: async () => scaffoldPureCharacterizationTests({
|
|
174
|
+
bundlePath: resolveFromWorkspace(workspaceRoot, bundlePath),
|
|
175
|
+
workspaceRoot,
|
|
176
|
+
}),
|
|
177
|
+
workspaceRoot,
|
|
178
|
+
});
|
|
179
|
+
log.info(`classified ${result.pureFunctionCount} pure function(s) from ${result.totalTopLevelFunctions} top-level callables; ${result.autoAssertionCount} auto assertion(s) generated`);
|
|
180
|
+
return result;
|
|
181
|
+
};
|
|
182
|
+
/**
|
|
183
|
+
* Programmatic entry point for seed-time smoke scaffold population and the legacy manual
|
|
184
|
+
* `stub-states` screenshot scaffold flow.
|
|
185
|
+
*/
|
|
186
|
+
export const runStubStatesCommand = async ({ log, regenStale = false, workspaceRoot, }) => {
|
|
187
|
+
if (regenStale) {
|
|
188
|
+
try {
|
|
189
|
+
const result = await withPipelineLock({
|
|
190
|
+
command: 'ushman-characterize stub-states --regen-stale',
|
|
191
|
+
work: async () => populateSmokeScaffolds({
|
|
192
|
+
workspaceDir: workspaceRoot,
|
|
193
|
+
}),
|
|
194
|
+
workspaceRoot,
|
|
195
|
+
});
|
|
196
|
+
log.info(`populated ${result.written.length} smoke scaffold file(s)`);
|
|
197
|
+
for (const gap of result.toolingGaps) {
|
|
198
|
+
log.warn(gap);
|
|
199
|
+
}
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
if (typeof error.exitCode === 'number') {
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
await handlePopulateCommandFailure({
|
|
207
|
+
error,
|
|
208
|
+
log,
|
|
209
|
+
subcommand: 'stub-states',
|
|
210
|
+
workspaceRoot,
|
|
211
|
+
});
|
|
212
|
+
return buildPopulateFailureResult({
|
|
213
|
+
summary: `stub-states population failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
214
|
+
workspaceRoot,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const result = await withPipelineLock({
|
|
219
|
+
command: 'ushman-characterize stub-states',
|
|
220
|
+
work: async () => scaffoldSceneCharacterizationTests({
|
|
221
|
+
workspaceRoot,
|
|
222
|
+
}),
|
|
223
|
+
workspaceRoot,
|
|
224
|
+
});
|
|
225
|
+
log.info(`scaffolded ${result.written.length} scene characterization test file(s)`);
|
|
226
|
+
return result;
|
|
227
|
+
};
|
|
228
|
+
const handleStubPure = async ({ log, rest }) => {
|
|
229
|
+
const { flags, positionals } = parseKeyValueFlags(rest, {
|
|
230
|
+
booleanFlags: ['regen-stale'],
|
|
231
|
+
stringFlags: ['bundle'],
|
|
232
|
+
});
|
|
233
|
+
const workspaceRoot = parseWorkspaceRoot(positionals);
|
|
234
|
+
await runStubPureCommand({
|
|
235
|
+
bundlePath: flags.get('bundle'),
|
|
236
|
+
log,
|
|
237
|
+
regenStale: flags.get('regen-stale') === true,
|
|
238
|
+
workspaceRoot,
|
|
239
|
+
});
|
|
240
|
+
return 0;
|
|
241
|
+
};
|
|
242
|
+
const handleStubStates = async ({ log, rest }) => {
|
|
243
|
+
const { flags, positionals } = parseKeyValueFlags(rest, {
|
|
244
|
+
booleanFlags: ['regen-stale'],
|
|
245
|
+
});
|
|
246
|
+
const workspaceRoot = parseWorkspaceRoot(positionals);
|
|
247
|
+
await runStubStatesCommand({
|
|
248
|
+
log,
|
|
249
|
+
regenStale: flags.get('regen-stale') === true,
|
|
250
|
+
workspaceRoot,
|
|
251
|
+
});
|
|
252
|
+
return 0;
|
|
253
|
+
};
|
|
254
|
+
const handleInstrument = async ({ log, rest }) => {
|
|
255
|
+
const { flags } = parseKeyValueFlags(rest, {
|
|
256
|
+
stringFlags: ['bundle', 'output', 'source-map'],
|
|
257
|
+
});
|
|
258
|
+
const bundlePath = flags.get('bundle');
|
|
259
|
+
const outputPath = flags.get('output');
|
|
260
|
+
if (!bundlePath || !outputPath) {
|
|
261
|
+
throw new Error('instrument requires --bundle=<path> and --output=<path>.');
|
|
262
|
+
}
|
|
263
|
+
const sourceMapMode = resolveSourceMapMode(flags.get('source-map'));
|
|
264
|
+
const result = await instrumentBundle({
|
|
265
|
+
bundlePath: path.resolve(bundlePath),
|
|
266
|
+
outputPath: path.resolve(outputPath),
|
|
267
|
+
sourceMapMode,
|
|
268
|
+
});
|
|
269
|
+
log.info(`instrumented ${result.instrumentedSymbols.length} callable(s) -> ${result.outputPath}${result.mapPath ? ` (+ ${result.mapPath})` : ''}`);
|
|
270
|
+
return 0;
|
|
271
|
+
};
|
|
272
|
+
const handleCapture = async ({ log, rest }) => {
|
|
273
|
+
const { flags, positionals } = parseKeyValueFlags(rest, {
|
|
274
|
+
booleanFlags: ['capture-side-effects', 'scene-only'],
|
|
275
|
+
stringFlags: ['bundle', 'mode', 'states'],
|
|
276
|
+
});
|
|
277
|
+
const workspaceRoot = parseWorkspaceRoot(positionals);
|
|
278
|
+
const result = await withPipelineLock({
|
|
279
|
+
command: 'ushman-characterize capture',
|
|
280
|
+
work: async () => captureCharacterization({
|
|
281
|
+
bundlePath: flags.get('bundle')
|
|
282
|
+
? resolveFromWorkspace(workspaceRoot, flags.get('bundle'))
|
|
283
|
+
: null,
|
|
284
|
+
captureSideEffects: flags.get('capture-side-effects') === true,
|
|
285
|
+
mode: flags.get('mode') ?? 'preview',
|
|
286
|
+
sceneOnly: flags.get('scene-only') === true,
|
|
287
|
+
states: parseStateList(flags.get('states')),
|
|
288
|
+
workspaceRoot,
|
|
289
|
+
}),
|
|
290
|
+
workspaceRoot,
|
|
291
|
+
});
|
|
292
|
+
for (const state of result.states) {
|
|
293
|
+
log.info(`state=${state.state} rawCalls=${state.rawCalls} unique=${state.uniqueCalls} dropped=${state.droppedUniqueCalls} bytes=${state.traceBytes} fns=${state.functionCount} merged=${state.mergedExistingRecords}`);
|
|
294
|
+
}
|
|
295
|
+
if (result.failures.length > 0) {
|
|
296
|
+
for (const failure of result.failures) {
|
|
297
|
+
log.warn(`state=${failure.state} phase=${failure.phase} error=${failure.error}`);
|
|
298
|
+
}
|
|
299
|
+
return 2;
|
|
300
|
+
}
|
|
301
|
+
return 0;
|
|
302
|
+
};
|
|
303
|
+
const handleGenerateReplay = async ({ log, rest }) => {
|
|
304
|
+
const { flags, positionals } = parseKeyValueFlags(rest, {
|
|
305
|
+
stringFlags: ['bundle', 'max-cases'],
|
|
306
|
+
});
|
|
307
|
+
const workspaceRoot = parseWorkspaceRoot(positionals);
|
|
308
|
+
const bundlePath = flags.get('bundle');
|
|
309
|
+
if (!bundlePath) {
|
|
310
|
+
throw new Error('generate-replay requires --bundle=<path>.');
|
|
311
|
+
}
|
|
312
|
+
const result = await withPipelineLock({
|
|
313
|
+
command: 'ushman-characterize generate-replay',
|
|
314
|
+
work: async () => generateReplayCharacterization({
|
|
315
|
+
bundlePath: resolveFromWorkspace(workspaceRoot, bundlePath),
|
|
316
|
+
maxCases: Number.parseInt(flags.get('max-cases') ?? '10', 10),
|
|
317
|
+
workspaceRoot,
|
|
318
|
+
}),
|
|
319
|
+
workspaceRoot,
|
|
320
|
+
});
|
|
321
|
+
log.info(`generated ${result.writtenTests.length} replay test file(s) and ${result.writtenFixtures.length} fixture file(s)`);
|
|
322
|
+
if (result.skippedFunctions.length > 0) {
|
|
323
|
+
for (const skipped of result.skippedFunctions) {
|
|
324
|
+
log.warn(`skipped replay generation for ${skipped.functionName}: ${skipped.reason}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return 0;
|
|
328
|
+
};
|
|
329
|
+
const handleReplay = async ({ log, rest }) => {
|
|
330
|
+
const { flags, positionals } = parseKeyValueFlags(rest, {
|
|
331
|
+
booleanFlags: ['lenient', 'strict'],
|
|
332
|
+
stringFlags: ['filter', 'mode'],
|
|
333
|
+
});
|
|
334
|
+
const workspaceRoot = parseWorkspaceRoot(positionals);
|
|
335
|
+
await assertV4Workspace(workspaceRoot);
|
|
336
|
+
const mode = resolveReplayMode({
|
|
337
|
+
lenient: flags.get('lenient') === true,
|
|
338
|
+
mode: flags.get('mode'),
|
|
339
|
+
strict: flags.get('strict') === true,
|
|
340
|
+
});
|
|
341
|
+
const filter = flags.get('filter') ?? null;
|
|
342
|
+
const cmd = ['bun', 'test', 'tests/replay/*.test.ts'];
|
|
343
|
+
if (filter) {
|
|
344
|
+
cmd.push('--filter', filter);
|
|
345
|
+
}
|
|
346
|
+
const result = Bun.spawnSync(cmd, {
|
|
347
|
+
cwd: workspaceRoot,
|
|
348
|
+
env: {
|
|
349
|
+
...process.env,
|
|
350
|
+
USHMAN_CHARACTERIZE_MATCH_MODE: mode,
|
|
351
|
+
},
|
|
352
|
+
stderr: 'inherit',
|
|
353
|
+
stdout: 'inherit',
|
|
354
|
+
});
|
|
355
|
+
const harnessPaths = workspaceHarnessPaths(workspaceRoot);
|
|
356
|
+
await mkdir(harnessPaths.reportsDir, { recursive: true });
|
|
357
|
+
const report = createVerifyReport({
|
|
358
|
+
exitCode: result.exitCode,
|
|
359
|
+
filter,
|
|
360
|
+
mode,
|
|
361
|
+
workspaceRoot,
|
|
362
|
+
});
|
|
363
|
+
const reportPath = path.join(harnessPaths.reportsDir, 'verify-report.json');
|
|
364
|
+
await Bun.write(reportPath, `${JSON.stringify(report, null, 2)}\n`);
|
|
365
|
+
log.info(`wrote replay report to ${reportPath}`);
|
|
366
|
+
return result.exitCode;
|
|
367
|
+
};
|
|
368
|
+
const handleRebind = async ({ log, rest }) => {
|
|
369
|
+
const { flags, positionals } = parseKeyValueFlags(rest, {
|
|
370
|
+
booleanFlags: ['dry-run', 'yes'],
|
|
371
|
+
stringFlags: ['map'],
|
|
372
|
+
});
|
|
373
|
+
const workspaceRoot = parseWorkspaceRoot(positionals);
|
|
374
|
+
const mappings = parseRebindMappings(rest);
|
|
375
|
+
if (mappings.size === 0) {
|
|
376
|
+
throw new Error('rebind requires at least one --map=symbol=path entry.');
|
|
377
|
+
}
|
|
378
|
+
const dryRun = flags.get('dry-run') === true;
|
|
379
|
+
const result = await withPipelineLock({
|
|
380
|
+
command: 'ushman-characterize rebind',
|
|
381
|
+
disabled: dryRun,
|
|
382
|
+
work: async () => rewriteReplayImports({
|
|
383
|
+
dryRun,
|
|
384
|
+
mappings,
|
|
385
|
+
workspaceRoot,
|
|
386
|
+
yes: flags.get('yes') === true,
|
|
387
|
+
}),
|
|
388
|
+
workspaceRoot,
|
|
389
|
+
});
|
|
390
|
+
if (dryRun) {
|
|
391
|
+
for (const preview of result.previews) {
|
|
392
|
+
log.info(renderRebindPreview(preview));
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
log.info(`${dryRun ? 'would touch' : 'updated'} ${result.files.length} replay test file(s)`);
|
|
396
|
+
return 0;
|
|
397
|
+
};
|
|
398
|
+
const COMMAND_HANDLERS = {
|
|
399
|
+
capture: handleCapture,
|
|
400
|
+
'generate-replay': handleGenerateReplay,
|
|
401
|
+
instrument: handleInstrument,
|
|
402
|
+
rebind: handleRebind,
|
|
403
|
+
replay: handleReplay,
|
|
404
|
+
'stub-pure': handleStubPure,
|
|
405
|
+
'stub-states': handleStubStates,
|
|
406
|
+
};
|
|
407
|
+
/**
|
|
408
|
+
* Run the standalone characterize CLI.
|
|
409
|
+
*/
|
|
410
|
+
export const runCharacterizeCommand = async (args) => {
|
|
411
|
+
const log = createConsoleLogger();
|
|
412
|
+
const [subcommand, ...rest] = args;
|
|
413
|
+
if (!subcommand || subcommand === '--help' || subcommand === '-h') {
|
|
414
|
+
process.stdout.write(`${HELP_TEXT}\n`);
|
|
415
|
+
return subcommand ? 0 : 1;
|
|
416
|
+
}
|
|
417
|
+
const handler = COMMAND_HANDLERS[subcommand];
|
|
418
|
+
if (!handler) {
|
|
419
|
+
log.error(`Unknown characterize subcommand: ${subcommand}`);
|
|
420
|
+
process.stdout.write(`${HELP_TEXT}\n`);
|
|
421
|
+
return 1;
|
|
422
|
+
}
|
|
423
|
+
try {
|
|
424
|
+
return await handler({
|
|
425
|
+
log,
|
|
426
|
+
rest,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
log.error(error instanceof Error ? error.message : String(error));
|
|
431
|
+
return typeof error.exitCode === 'number'
|
|
432
|
+
? error.exitCode
|
|
433
|
+
: 1;
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
if (import.meta.main) {
|
|
437
|
+
const exitCode = await runCharacterizeCommand(process.argv.slice(2));
|
|
438
|
+
process.exit(exitCode);
|
|
439
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const FLOAT_PRECISION_SCALE = 10000;
|
|
2
|
+
export declare const DEFAULT_TRACE_DEPTH_LIMIT = 8;
|
|
3
|
+
export declare const DEFAULT_TRACE_BYTES_LIMIT: number;
|
|
4
|
+
export declare const DEFAULT_PER_FUNCTION_SHAPE_CAP = 25;
|
|
5
|
+
export declare const DEFAULT_MAX_UNIQUE_RECORDS_PER_STATE = 25000;
|
|
6
|
+
export declare const DEFAULT_CAPTURE_SETTLE_FRAMES = 8;
|
|
7
|
+
export declare const DEFAULT_CAPTURE_VIEWPORT: {
|
|
8
|
+
readonly height: 720;
|
|
9
|
+
readonly width: 1280;
|
|
10
|
+
};
|
|
11
|
+
export declare const EXEMPT_FIELD_MARKER = "__ushman_exempt__";
|
|
12
|
+
export declare const CHARACTERIZE_SUPPORT_VERSION = "2";
|
|
13
|
+
export declare const TRACE_RECORD_SCHEMA_NAME = "ushman.characterize.trace-record";
|
|
14
|
+
export declare const TRACE_RECORD_SCHEMA_VERSION = "1";
|
|
15
|
+
export declare const REPLAY_FIXTURE_SCHEMA_NAME = "ushman.characterize.replay-fixture";
|
|
16
|
+
export declare const REPLAY_FIXTURE_SCHEMA_VERSION = "1";
|
|
17
|
+
export declare const TRACE_RUNTIME_VERSION = 2;
|
|
18
|
+
export declare const VERIFY_REPORT_SCHEMA_NAME = "ushman.characterize.verify-report";
|
|
19
|
+
export declare const VERIFY_REPORT_SCHEMA_VERSION = "1";
|
|
20
|
+
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,qBAAqB,QAAS,CAAC;AAC5C,eAAO,MAAM,yBAAyB,IAAI,CAAC;AAC3C,eAAO,MAAM,yBAAyB,QAAmB,CAAC;AAC1D,eAAO,MAAM,8BAA8B,KAAK,CAAC;AACjD,eAAO,MAAM,oCAAoC,QAAS,CAAC;AAC3D,eAAO,MAAM,6BAA6B,IAAI,CAAC;AAC/C,eAAO,MAAM,wBAAwB;;;CAG3B,CAAC;AACX,eAAO,MAAM,mBAAmB,sBAAsB,CAAC;AACvD,eAAO,MAAM,4BAA4B,MAAM,CAAC;AAChD,eAAO,MAAM,wBAAwB,qCAAqC,CAAC;AAC3E,eAAO,MAAM,2BAA2B,MAAM,CAAC;AAC/C,eAAO,MAAM,0BAA0B,uCAAuC,CAAC;AAC/E,eAAO,MAAM,6BAA6B,MAAM,CAAC;AACjD,eAAO,MAAM,qBAAqB,IAAI,CAAC;AACvC,eAAO,MAAM,yBAAyB,sCAAsC,CAAC;AAC7E,eAAO,MAAM,4BAA4B,MAAM,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const FLOAT_PRECISION_SCALE = 10_000;
|
|
2
|
+
export const DEFAULT_TRACE_DEPTH_LIMIT = 8;
|
|
3
|
+
export const DEFAULT_TRACE_BYTES_LIMIT = 50 * 1024 * 1024;
|
|
4
|
+
export const DEFAULT_PER_FUNCTION_SHAPE_CAP = 25;
|
|
5
|
+
export const DEFAULT_MAX_UNIQUE_RECORDS_PER_STATE = 25_000;
|
|
6
|
+
export const DEFAULT_CAPTURE_SETTLE_FRAMES = 8;
|
|
7
|
+
export const DEFAULT_CAPTURE_VIEWPORT = {
|
|
8
|
+
height: 720,
|
|
9
|
+
width: 1280,
|
|
10
|
+
};
|
|
11
|
+
export const EXEMPT_FIELD_MARKER = '__ushman_exempt__';
|
|
12
|
+
export const CHARACTERIZE_SUPPORT_VERSION = '2';
|
|
13
|
+
export const TRACE_RECORD_SCHEMA_NAME = 'ushman.characterize.trace-record';
|
|
14
|
+
export const TRACE_RECORD_SCHEMA_VERSION = '1';
|
|
15
|
+
export const REPLAY_FIXTURE_SCHEMA_NAME = 'ushman.characterize.replay-fixture';
|
|
16
|
+
export const REPLAY_FIXTURE_SCHEMA_VERSION = '1';
|
|
17
|
+
export const TRACE_RUNTIME_VERSION = 2;
|
|
18
|
+
export const VERIFY_REPORT_SCHEMA_NAME = 'ushman.characterize.verify-report';
|
|
19
|
+
export const VERIFY_REPORT_SCHEMA_VERSION = '1';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
type TraceDedupeRecord = {
|
|
2
|
+
readonly args: unknown;
|
|
3
|
+
readonly bindingName: string;
|
|
4
|
+
readonly className: null | string;
|
|
5
|
+
readonly expected: unknown;
|
|
6
|
+
readonly functionName: string;
|
|
7
|
+
readonly memberKind: 'function' | 'method';
|
|
8
|
+
readonly methodName: null | string;
|
|
9
|
+
readonly sideEffects: readonly unknown[];
|
|
10
|
+
readonly thisArg: unknown;
|
|
11
|
+
readonly threw: unknown;
|
|
12
|
+
};
|
|
13
|
+
export declare const toTraceDeduplicationPayload: (record: TraceDedupeRecord) => {
|
|
14
|
+
args: unknown;
|
|
15
|
+
bindingName: string;
|
|
16
|
+
className: string | null;
|
|
17
|
+
expected: unknown;
|
|
18
|
+
functionName: string;
|
|
19
|
+
memberKind: "function" | "method";
|
|
20
|
+
methodName: string | null;
|
|
21
|
+
sideEffects: readonly unknown[];
|
|
22
|
+
thisArg: unknown;
|
|
23
|
+
threw: unknown;
|
|
24
|
+
};
|
|
25
|
+
export {};
|
|
26
|
+
//# sourceMappingURL=dedupe-contract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dedupe-contract.d.ts","sourceRoot":"","sources":["../src/dedupe-contract.ts"],"names":[],"mappings":"AAAA,KAAK,iBAAiB,GAAG;IACrB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,IAAI,GAAG,MAAM,CAAC;IAClC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,UAAU,GAAG,QAAQ,CAAC;IAC3C,QAAQ,CAAC,UAAU,EAAE,IAAI,GAAG,MAAM,CAAC;IACnC,QAAQ,CAAC,WAAW,EAAE,SAAS,OAAO,EAAE,CAAC;IACzC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF,eAAO,MAAM,2BAA2B,GAAI,QAAQ,iBAAiB;;;;;;;;;;;CAWnE,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const toTraceDeduplicationPayload = (record) => ({
|
|
2
|
+
args: record.args,
|
|
3
|
+
bindingName: record.bindingName,
|
|
4
|
+
className: record.className,
|
|
5
|
+
expected: record.expected,
|
|
6
|
+
functionName: record.functionName,
|
|
7
|
+
memberKind: record.memberKind,
|
|
8
|
+
methodName: record.methodName,
|
|
9
|
+
sideEffects: record.sideEffects,
|
|
10
|
+
thisArg: record.thisArg,
|
|
11
|
+
threw: record.threw,
|
|
12
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import * as t from '@babel/types';
|
|
2
|
+
export declare const isAnonymousDefaultExportCallableStatement: (statement: t.Statement | t.ModuleDeclaration) => boolean;
|
|
3
|
+
export declare const collectTopLevelBindingNames: (statements: readonly (t.Statement | t.ModuleDeclaration)[]) => Set<string>;
|
|
4
|
+
export declare const resolveAnonymousDefaultExportBindingName: (takenNames: ReadonlySet<string>) => string;
|
|
5
|
+
export declare const getAnonymousDefaultExportBindingName: (statements: readonly (t.Statement | t.ModuleDeclaration)[]) => string | null;
|
|
6
|
+
//# sourceMappingURL=default-export.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"default-export.d.ts","sourceRoot":"","sources":["../src/default-export.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,cAAc,CAAC;AAmBlC,eAAO,MAAM,yCAAyC,GAAI,WAAW,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,iBAAiB,YAI3C,CAAC;AAE5D,eAAO,MAAM,2BAA2B,GAAI,YAAY,SAAS,CAAC,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,iBAAiB,CAAC,EAAE,gBA0BrG,CAAC;AAEF,eAAO,MAAM,wCAAwC,GAAI,YAAY,WAAW,CAAC,MAAM,CAAC,WAUvF,CAAC;AAEF,eAAO,MAAM,oCAAoC,GAAI,YAAY,SAAS,CAAC,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,iBAAiB,CAAC,EAAE,kBAM9G,CAAC"}
|