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.
Files changed (99) hide show
  1. package/AGENTS.md +110 -0
  2. package/CHANGELOG.md +41 -0
  3. package/LICENSE.md +21 -0
  4. package/README.md +193 -0
  5. package/bin/ushman-characterize +19 -0
  6. package/dist/babel-config.d.ts +7 -0
  7. package/dist/babel-config.d.ts.map +1 -0
  8. package/dist/babel-config.js +17 -0
  9. package/dist/capture-server.d.ts +31 -0
  10. package/dist/capture-server.d.ts.map +1 -0
  11. package/dist/capture-server.js +199 -0
  12. package/dist/capture.d.ts +97 -0
  13. package/dist/capture.d.ts.map +1 -0
  14. package/dist/capture.js +620 -0
  15. package/dist/cli/logger.d.ts +7 -0
  16. package/dist/cli/logger.d.ts.map +1 -0
  17. package/dist/cli/logger.js +14 -0
  18. package/dist/cli/parse-flags.d.ts +8 -0
  19. package/dist/cli/parse-flags.d.ts.map +1 -0
  20. package/dist/cli/parse-flags.js +60 -0
  21. package/dist/cli.d.ts +39 -0
  22. package/dist/cli.d.ts.map +1 -0
  23. package/dist/cli.js +439 -0
  24. package/dist/constants.d.ts +20 -0
  25. package/dist/constants.d.ts.map +1 -0
  26. package/dist/constants.js +19 -0
  27. package/dist/dedupe-contract.d.ts +26 -0
  28. package/dist/dedupe-contract.d.ts.map +1 -0
  29. package/dist/dedupe-contract.js +12 -0
  30. package/dist/default-export.d.ts +6 -0
  31. package/dist/default-export.d.ts.map +1 -0
  32. package/dist/default-export.js +52 -0
  33. package/dist/format-contract.d.ts +25 -0
  34. package/dist/format-contract.d.ts.map +1 -0
  35. package/dist/format-contract.js +96 -0
  36. package/dist/function-utils.d.ts +6 -0
  37. package/dist/function-utils.d.ts.map +1 -0
  38. package/dist/function-utils.js +22 -0
  39. package/dist/generate-replay.d.ts +18 -0
  40. package/dist/generate-replay.d.ts.map +1 -0
  41. package/dist/generate-replay.js +158 -0
  42. package/dist/index.d.ts +13 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +11 -0
  45. package/dist/instrument.d.ts +39 -0
  46. package/dist/instrument.d.ts.map +1 -0
  47. package/dist/instrument.js +605 -0
  48. package/dist/ledger.d.ts +19 -0
  49. package/dist/ledger.d.ts.map +1 -0
  50. package/dist/ledger.js +50 -0
  51. package/dist/puppeteer-harness.d.ts +74 -0
  52. package/dist/puppeteer-harness.d.ts.map +1 -0
  53. package/dist/puppeteer-harness.js +248 -0
  54. package/dist/purity-classifier.d.ts +28 -0
  55. package/dist/purity-classifier.d.ts.map +1 -0
  56. package/dist/purity-classifier.js +363 -0
  57. package/dist/rebind.d.ts +26 -0
  58. package/dist/rebind.d.ts.map +1 -0
  59. package/dist/rebind.js +356 -0
  60. package/dist/replay-report.d.ts +18 -0
  61. package/dist/replay-report.d.ts.map +1 -0
  62. package/dist/replay-report.js +12 -0
  63. package/dist/scene.d.ts +24 -0
  64. package/dist/scene.d.ts.map +1 -0
  65. package/dist/scene.js +235 -0
  66. package/dist/schema-types.d.ts +40 -0
  67. package/dist/schema-types.d.ts.map +1 -0
  68. package/dist/schema-types.js +32 -0
  69. package/dist/seed-scaffolds.d.ts +31 -0
  70. package/dist/seed-scaffolds.d.ts.map +1 -0
  71. package/dist/seed-scaffolds.js +96 -0
  72. package/dist/shared.d.ts +36 -0
  73. package/dist/shared.d.ts.map +1 -0
  74. package/dist/shared.js +390 -0
  75. package/dist/state-dag.d.ts +5 -0
  76. package/dist/state-dag.d.ts.map +1 -0
  77. package/dist/state-dag.js +27 -0
  78. package/dist/stub-pure.d.ts +57 -0
  79. package/dist/stub-pure.d.ts.map +1 -0
  80. package/dist/stub-pure.js +987 -0
  81. package/dist/time.d.ts +3 -0
  82. package/dist/time.d.ts.map +1 -0
  83. package/dist/time.js +10 -0
  84. package/dist/trace-format.d.ts +24 -0
  85. package/dist/trace-format.d.ts.map +1 -0
  86. package/dist/trace-format.js +213 -0
  87. package/dist/trace-serializer.d.ts +94 -0
  88. package/dist/trace-serializer.d.ts.map +1 -0
  89. package/dist/trace-serializer.js +607 -0
  90. package/dist/tracer-runtime.d.ts +25 -0
  91. package/dist/tracer-runtime.d.ts.map +1 -0
  92. package/dist/tracer-runtime.js +291 -0
  93. package/dist/types.d.ts +13 -0
  94. package/dist/types.d.ts.map +1 -0
  95. package/dist/types.js +0 -0
  96. package/dist/workspace-paths.d.ts +64 -0
  97. package/dist/workspace-paths.d.ts.map +1 -0
  98. package/dist/workspace-paths.js +288 -0
  99. package/package.json +86 -0
@@ -0,0 +1,987 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ import { mkdir } from 'node:fs/promises';
10
+ import path from 'node:path';
11
+ import { pathToFileURL } from 'node:url';
12
+ import traverse from '@babel/traverse';
13
+ import * as t from '@babel/types';
14
+ import { parseModuleAst } from "./babel-config.js";
15
+ import { parseTraceRecord, stampTraceRecord } from "./format-contract.js";
16
+ import { recordCharacterizeToolingGap, recordCharacterizeValidatorResult } from "./ledger.js";
17
+ import { classifyPureTopLevelFunctionsFromBundle } from "./purity-classifier.js";
18
+ import { hashSeedScaffoldContent, listFilesRecursive, loadSeedFingerprintSidecar, toWorkspaceRelativePath, updateSeedFingerprintSidecar, } from "./seed-scaffolds.js";
19
+ import { buildStandaloneSymbolModule, parseBundleAst, parseJsonLines, relativeImportPath, sanitizeSymbolName, stableJsonStringify, toJsonLines, } from "./shared.js";
20
+ import { canonicalizeTraceValue, ensureWorkspaceTraceHarness } from "./trace-format.js";
21
+ import { assertV4Workspace, atomicWriteText } from "./workspace-paths.js";
22
+ // Keep automatic pure-function sampling small enough that stub generation remains fast.
23
+ // Operators can expand coverage manually after the initial scaffold lands.
24
+ const MAX_AUTO_CASES = 5;
25
+ // Prevent combinatorial explosion when heuristic argument candidates are cartesian-producted.
26
+ const MAX_CANDIDATE_ARG_PRODUCTS = 10;
27
+ // Promise-returning helpers that do not settle quickly are treated as non-deterministic samples.
28
+ const PROMISE_SAMPLE_TIMEOUT_MS = 100;
29
+ // Best-effort heuristic inference for seed-time sample generation. This is
30
+ // intentionally conservative and only drives the legacy manual stub-pure flow.
31
+ const inferKind = (parameterName) => {
32
+ const name = parameterName.toLowerCase();
33
+ const isScalarShortName = /^(ms|n|t|x|y|z)$/u.test(name);
34
+ if (/^(is|has|should|can)[a-z]/u.test(parameterName) || /enabled|flag|visible|open|closed/u.test(name)) {
35
+ return 'boolean';
36
+ }
37
+ if (/array|items|list|children|values|points/u.test(name)) {
38
+ return 'array';
39
+ }
40
+ if (/name|label|text|path|slug|key|id|url|message/u.test(name)) {
41
+ return 'string';
42
+ }
43
+ if (isScalarShortName ||
44
+ /value|count|index|offset|min|max|width|height|size|scale|angle|yaw|pitch|roll/u.test(name)) {
45
+ return 'number';
46
+ }
47
+ if (/options|config|meta|props|state|data/u.test(name)) {
48
+ return 'object';
49
+ }
50
+ return 'unknown';
51
+ };
52
+ const product = (items, limit) => {
53
+ const out = [[]];
54
+ for (const values of items) {
55
+ const next = [];
56
+ for (const prefix of out) {
57
+ for (const value of values) {
58
+ next.push([...prefix, value]);
59
+ if (next.length >= limit) {
60
+ return next;
61
+ }
62
+ }
63
+ }
64
+ out.splice(0, out.length, ...next);
65
+ }
66
+ return out;
67
+ };
68
+ const candidateValuesForKind = (kind) => {
69
+ switch (kind) {
70
+ case 'array':
71
+ return [[], [0], [1, 2], ['a', 'b']];
72
+ case 'boolean':
73
+ return [false, true];
74
+ case 'number':
75
+ return [-1, 0, 1, 5, 10];
76
+ case 'object':
77
+ return [{}, { value: 1 }, { max: 10, min: 0 }];
78
+ case 'string':
79
+ return ['', 'a', 'abc', 'value'];
80
+ default:
81
+ return [0, 1, 'a', true, {}, []];
82
+ }
83
+ };
84
+ const buildCandidateArgs = (info) => {
85
+ if (info.parameterNames.length === 0) {
86
+ return [[]];
87
+ }
88
+ if (info.parameterNames.length === 3 &&
89
+ inferKind(info.parameterNames[0] ?? '') === 'number' &&
90
+ inferKind(info.parameterNames[1] ?? '') === 'number' &&
91
+ inferKind(info.parameterNames[2] ?? '') === 'number') {
92
+ return [
93
+ [-1, 0, 10],
94
+ [0, 0, 10],
95
+ [5, 0, 10],
96
+ [11, 0, 10],
97
+ [1, 2, 3],
98
+ ];
99
+ }
100
+ const perParameter = info.parameterNames.map((name) => candidateValuesForKind(inferKind(name)));
101
+ return product(perParameter, MAX_CANDIDATE_ARG_PRODUCTS);
102
+ };
103
+ const renderArrayLiteral = (value) => {
104
+ const rendered = value.map((entry) => renderLiteral(entry));
105
+ return rendered.every((entry) => entry !== null) ? `[${rendered.join(', ')}]` : null;
106
+ };
107
+ const renderObjectLiteral = (value) => {
108
+ const entries = Object.entries(value).map(([key, entry]) => [key, renderLiteral(entry)]);
109
+ if (entries.some(([, entry]) => entry === null)) {
110
+ return null;
111
+ }
112
+ return `{ ${entries.map(([key, entry]) => `${JSON.stringify(key)}: ${entry}`).join(', ')} }`;
113
+ };
114
+ const renderLiteral = (value) => {
115
+ if (value === undefined) {
116
+ return 'undefined';
117
+ }
118
+ if (typeof value === 'number') {
119
+ if (Number.isNaN(value)) {
120
+ return 'Number.NaN';
121
+ }
122
+ if (value === Number.POSITIVE_INFINITY) {
123
+ return 'Number.POSITIVE_INFINITY';
124
+ }
125
+ if (value === Number.NEGATIVE_INFINITY) {
126
+ return 'Number.NEGATIVE_INFINITY';
127
+ }
128
+ if (Object.is(value, -0)) {
129
+ return '-0';
130
+ }
131
+ return String(value);
132
+ }
133
+ if (typeof value === 'string') {
134
+ return JSON.stringify(value);
135
+ }
136
+ if (typeof value === 'boolean' || value === null) {
137
+ return JSON.stringify(value);
138
+ }
139
+ if (Array.isArray(value)) {
140
+ return renderArrayLiteral(value);
141
+ }
142
+ if (typeof value === 'object') {
143
+ return renderObjectLiteral(value);
144
+ }
145
+ return null;
146
+ };
147
+ const isCaseRenderable = (value) => renderLiteral(value.expected) !== null && value.args.every((entry) => renderLiteral(entry) !== null);
148
+ const hasDetachedTextureLoadHazard = (source) => {
149
+ const ast = parseModuleAst({
150
+ source,
151
+ sourcePath: 'inline://stub-pure-hazard-check.js',
152
+ });
153
+ let sawTextureLoader = false;
154
+ let sawPromiseConstructor = false;
155
+ traverse(ast, {
156
+ Identifier(identifierPath) {
157
+ if (identifierPath.node.name === 'TextureLoader') {
158
+ sawTextureLoader = true;
159
+ }
160
+ if (identifierPath.node.name === 'Promise' &&
161
+ identifierPath.parentPath.isNewExpression() &&
162
+ identifierPath.parentPath.node.callee === identifierPath.node) {
163
+ sawPromiseConstructor = true;
164
+ }
165
+ if (sawTextureLoader && sawPromiseConstructor) {
166
+ identifierPath.stop();
167
+ }
168
+ },
169
+ NewExpression(newExpressionPath) {
170
+ if (t.isIdentifier(newExpressionPath.node.callee, { name: 'Promise' })) {
171
+ sawPromiseConstructor = true;
172
+ if (sawTextureLoader) {
173
+ newExpressionPath.stop();
174
+ }
175
+ }
176
+ },
177
+ });
178
+ return sawTextureLoader && sawPromiseConstructor;
179
+ };
180
+ const isPromiseLike = (value) => Boolean(value) && typeof value === 'object' && typeof value.then === 'function';
181
+ const settlePromiseSample = async (value) => {
182
+ let timeoutId;
183
+ try {
184
+ return await Promise.race([
185
+ Promise.resolve(value).then((resolved) => ({ ok: true, value: resolved }), () => ({ ok: false })),
186
+ new Promise((resolve) => {
187
+ timeoutId = setTimeout(() => resolve({ ok: false }), PROMISE_SAMPLE_TIMEOUT_MS);
188
+ }),
189
+ ]);
190
+ }
191
+ finally {
192
+ if (timeoutId) {
193
+ clearTimeout(timeoutId);
194
+ }
195
+ }
196
+ };
197
+ const executeSampleSafely = async (run) => {
198
+ let asyncFault = false;
199
+ const markAsyncFault = () => {
200
+ asyncFault = true;
201
+ };
202
+ process.on('unhandledRejection', markAsyncFault);
203
+ process.on('uncaughtException', markAsyncFault);
204
+ try {
205
+ let value;
206
+ try {
207
+ value = run();
208
+ }
209
+ catch {
210
+ return { ok: false };
211
+ }
212
+ const settled = isPromiseLike(value) ? await settlePromiseSample(value) : { ok: true, value };
213
+ if (!settled.ok) {
214
+ return settled;
215
+ }
216
+ await new Promise((resolve) => setTimeout(resolve, 0));
217
+ return asyncFault ? { ok: false } : settled;
218
+ }
219
+ finally {
220
+ process.off('unhandledRejection', markAsyncFault);
221
+ process.off('uncaughtException', markAsyncFault);
222
+ }
223
+ };
224
+ const loadFunctionCases = async ({ info, modulePath, }) => {
225
+ const moduleSource = await Bun.file(modulePath).text();
226
+ if (hasDetachedTextureLoadHazard(moduleSource)) {
227
+ return [];
228
+ }
229
+ const moduleUrl = `${pathToFileURL(modulePath).href}?cacheBust=${Date.now()}`;
230
+ const mod = (await import(__rewriteRelativeImportExtension(moduleUrl)));
231
+ const fn = mod[info.bindingName];
232
+ if (typeof fn !== 'function') {
233
+ return [];
234
+ }
235
+ const cases = [];
236
+ const seen = new Set();
237
+ for (const args of buildCandidateArgs(info)) {
238
+ try {
239
+ const expected = await executeSampleSafely(() => fn(...args));
240
+ if (!expected.ok) {
241
+ continue;
242
+ }
243
+ const canonicalExpected = canonicalizeTraceValue(expected.value);
244
+ const key = JSON.stringify({ args, expected: canonicalExpected });
245
+ if (seen.has(key)) {
246
+ continue;
247
+ }
248
+ seen.add(key);
249
+ const sample = { args, expected: canonicalExpected };
250
+ if (isCaseRenderable(sample)) {
251
+ cases.push(sample);
252
+ }
253
+ }
254
+ catch {
255
+ continue;
256
+ }
257
+ if (cases.length >= MAX_AUTO_CASES) {
258
+ break;
259
+ }
260
+ }
261
+ return cases;
262
+ };
263
+ const renderTestSource = ({ info, fixtureName, harnessPath, modulePath, testFile, }) => {
264
+ const harnessImportPath = relativeImportPath({ fromFile: testFile, toFile: harnessPath });
265
+ const importPath = relativeImportPath({ fromFile: testFile, toFile: modulePath });
266
+ return `// AUTO-GENERATED by ushman-characterize stub-pure. Do not hand-edit.
267
+ import { expect, test } from 'bun:test';
268
+ import { fixture, registerTraceMatchers } from '${harnessImportPath}';
269
+ import { ${info.bindingName} } from '${importPath}';
270
+
271
+ registerTraceMatchers();
272
+
273
+ const cases = await fixture('pure', '${fixtureName}');
274
+
275
+ for (const [index, entry] of cases.entries()) {
276
+ test('${info.name}: case #' + (index + 1), () => {
277
+ expect(${info.bindingName}(...entry.args)).toMatchTrace(entry.expected);
278
+ });
279
+ }
280
+
281
+ test.todo('${info.name}: add operator-supplied edge cases');
282
+ `;
283
+ };
284
+ const isRelativeSpecifier = (value) => value.startsWith('./') || value.startsWith('../');
285
+ const uniqueSymbolsInOrder = (symbols) => {
286
+ const seen = new Set();
287
+ return symbols.filter((symbol) => {
288
+ if (seen.has(symbol)) {
289
+ return false;
290
+ }
291
+ seen.add(symbol);
292
+ return true;
293
+ });
294
+ };
295
+ const collectManagedImportCandidateSymbols = (node) => node.specifiers
296
+ .map((specifier) => {
297
+ if (t.isImportSpecifier(specifier)) {
298
+ return t.isIdentifier(specifier.imported) ? specifier.imported.name : specifier.imported.value;
299
+ }
300
+ if (t.isImportDefaultSpecifier(specifier)) {
301
+ return 'default';
302
+ }
303
+ if (t.isImportNamespaceSpecifier(specifier)) {
304
+ return '*';
305
+ }
306
+ return null;
307
+ })
308
+ .filter((symbol) => symbol !== null);
309
+ const selectManagedImportCandidate = ({ candidates, filePath, todoSymbols, }) => {
310
+ const matchingCandidates = candidates.filter((candidate) => todoSymbols.every((symbol) => candidate.exportedSymbols.includes(symbol)));
311
+ if (matchingCandidates.length === 1) {
312
+ return matchingCandidates[0];
313
+ }
314
+ if (matchingCandidates.length > 1) {
315
+ throw new Error(`Managed pure scaffold ${filePath} matched multiple source-module imports for symbols: ${todoSymbols.join(', ')}.`);
316
+ }
317
+ throw new Error(`Managed pure scaffold ${filePath} could not resolve a unique source-module import for symbols: ${todoSymbols.join(', ')}.`);
318
+ };
319
+ const parseManagedPureScaffold = ({ filePath, source, workspaceDir, }) => {
320
+ const ast = parseModuleAst({
321
+ source,
322
+ sourcePath: filePath,
323
+ });
324
+ const todoSymbols = [];
325
+ const importCandidates = [];
326
+ const srcRoot = path.join(workspaceDir, 'src');
327
+ traverse(ast, {
328
+ CallExpression(callPath) {
329
+ if (t.isMemberExpression(callPath.node.callee) &&
330
+ t.isIdentifier(callPath.node.callee.object, { name: 'test' }) &&
331
+ t.isIdentifier(callPath.node.callee.property, { name: 'todo' })) {
332
+ const [firstArgument] = callPath.node.arguments;
333
+ if (t.isStringLiteral(firstArgument)) {
334
+ todoSymbols.push(firstArgument.value);
335
+ }
336
+ }
337
+ },
338
+ ImportDeclaration(importPathNode) {
339
+ if (!isRelativeSpecifier(importPathNode.node.source.value)) {
340
+ return;
341
+ }
342
+ const resolvedImportPath = path.resolve(path.dirname(filePath), importPathNode.node.source.value);
343
+ if (!resolvedImportPath.startsWith(`${srcRoot}${path.sep}`) && resolvedImportPath !== srcRoot) {
344
+ return;
345
+ }
346
+ const exportedSymbols = collectManagedImportCandidateSymbols(importPathNode.node);
347
+ if (exportedSymbols.length === 0) {
348
+ return;
349
+ }
350
+ importCandidates.push({
351
+ exportedSymbols,
352
+ importPath: importPathNode.node.source.value,
353
+ moduleFilePath: resolvedImportPath,
354
+ });
355
+ },
356
+ });
357
+ const managedSymbols = uniqueSymbolsInOrder(todoSymbols);
358
+ if (managedSymbols.length === 0) {
359
+ throw new Error(`Unable to find any managed export symbols in ${filePath}.`);
360
+ }
361
+ const selectedImport = selectManagedImportCandidate({
362
+ candidates: importCandidates,
363
+ filePath,
364
+ todoSymbols: managedSymbols,
365
+ });
366
+ if (!selectedImport) {
367
+ throw new Error(`Unable to find a managed source-module import in ${filePath}.`);
368
+ }
369
+ const moduleRelativePath = path
370
+ .relative(srcRoot, selectedImport.moduleFilePath)
371
+ .split(path.sep)
372
+ .join('/')
373
+ .replace(/\.[^.]+$/u, '');
374
+ if (moduleRelativePath.startsWith('../')) {
375
+ throw new Error(`Managed pure scaffold ${filePath} points outside ${srcRoot}.`);
376
+ }
377
+ return {
378
+ filePath,
379
+ importPath: selectedImport.importPath,
380
+ moduleFilePath: selectedImport.moduleFilePath,
381
+ moduleRelativePath,
382
+ relativeTestPath: toWorkspaceRelativePath({
383
+ filePath,
384
+ workspaceDir,
385
+ }),
386
+ symbols: managedSymbols,
387
+ };
388
+ };
389
+ const collectManagedPureScaffolds = async (workspaceDir) => {
390
+ const testsDir = path.join(workspaceDir, 'tests', 'pure');
391
+ const files = await listFilesRecursive({
392
+ dir: testsDir,
393
+ suffix: '.test.ts',
394
+ });
395
+ const parsed = await Promise.all(files.map(async (filePath) => {
396
+ try {
397
+ return {
398
+ error: null,
399
+ filePath,
400
+ scaffold: parseManagedPureScaffold({
401
+ filePath,
402
+ source: await Bun.file(filePath).text(),
403
+ workspaceDir,
404
+ }),
405
+ };
406
+ }
407
+ catch (error) {
408
+ return {
409
+ error: error instanceof Error ? error.message : String(error),
410
+ filePath,
411
+ scaffold: null,
412
+ };
413
+ }
414
+ }));
415
+ return {
416
+ parseFailures: parsed
417
+ .filter((entry) => entry.error !== null)
418
+ .map((entry) => ({
419
+ filePath: entry.filePath,
420
+ message: entry.error,
421
+ }))
422
+ .sort((left, right) => left.filePath.localeCompare(right.filePath)),
423
+ scaffolds: parsed
424
+ .flatMap((entry) => (entry.scaffold ? [entry.scaffold] : []))
425
+ .sort((left, right) => left.filePath.localeCompare(right.filePath)),
426
+ };
427
+ };
428
+ const isScaffoldUnmodified = ({ currentHash, trackedHash, }) => trackedHash !== undefined && trackedHash === currentHash;
429
+ const matchesPopulateFilters = ({ modulePathPrefix, scaffold, symbolFilter, }) => (modulePathPrefix === undefined || scaffold.moduleRelativePath.startsWith(modulePathPrefix)) &&
430
+ (symbolFilter === undefined ||
431
+ scaffold.symbols.some((symbol) => symbolFilter(symbol, scaffold.moduleRelativePath)));
432
+ const buildSymbolOwnershipMap = (scaffolds) => {
433
+ const ownershipCounts = new Map();
434
+ for (const scaffold of scaffolds) {
435
+ for (const symbol of new Set(scaffold.symbols)) {
436
+ const owners = ownershipCounts.get(symbol) ?? new Set();
437
+ owners.add(scaffold.relativeTestPath);
438
+ ownershipCounts.set(symbol, owners);
439
+ }
440
+ }
441
+ return new Map([...ownershipCounts.entries()].map(([symbol, owners]) => [
442
+ symbol,
443
+ [...owners].sort((left, right) => left.localeCompare(right)),
444
+ ]));
445
+ };
446
+ const buildPureModuleTracePath = ({ moduleRelativePath, workspaceDir, }) => path.join(workspaceDir, '.lab', 'characterize', 'traces', `${moduleRelativePath}.jsonl`);
447
+ const readTraceRecords = async ({ contextPrefix, filePath, }) => {
448
+ try {
449
+ return parseJsonLines(await Bun.file(filePath).text()).map((record, index) => parseTraceRecord({
450
+ context: `${contextPrefix}:${index + 1}`,
451
+ value: record,
452
+ }));
453
+ }
454
+ catch (error) {
455
+ throw new Error(`Trace file ${filePath} is invalid or incompatible. ${error instanceof Error ? error.message : String(error)}`);
456
+ }
457
+ };
458
+ const sortStableStrings = (values) => [...values].sort((left, right) => left.localeCompare(right));
459
+ const recordMatchesSymbol = ({ record, symbol }) => record.bindingName === symbol || record.functionName === symbol || record.methodName === symbol;
460
+ const buildPureCases = ({ records, symbol, }) => {
461
+ const deduped = new Map();
462
+ for (const record of records) {
463
+ if (!recordMatchesSymbol({ record, symbol })) {
464
+ continue;
465
+ }
466
+ const nextCase = {
467
+ args: record.args,
468
+ expected: record.expected,
469
+ state: record.state,
470
+ thisArg: record.thisArg,
471
+ threw: record.threw,
472
+ };
473
+ const key = stableJsonStringify({
474
+ args: nextCase.args,
475
+ expected: nextCase.expected,
476
+ thisArg: nextCase.thisArg,
477
+ threw: nextCase.threw,
478
+ });
479
+ if (!deduped.has(key)) {
480
+ deduped.set(key, nextCase);
481
+ }
482
+ }
483
+ return [...deduped.values()].sort((left, right) => stableJsonStringify(left).localeCompare(stableJsonStringify(right)));
484
+ };
485
+ const renderPureCaseConst = ({ cases, symbol, }) => `const ${sanitizeSymbolName(symbol)}Cases = ${stableJsonStringify(cases, 2)} as const;\n`;
486
+ const renderPureSymbolBlock = ({ cases, symbol, }) => {
487
+ if (cases.length === 0) {
488
+ return ` test.todo('${symbol}');\n`;
489
+ }
490
+ const caseConstName = `${sanitizeSymbolName(symbol)}Cases`;
491
+ return cases
492
+ .map((entry, index) => [
493
+ ` test('${symbol} case #${index + 1} [${entry.state}]', async () => {`,
494
+ ` const observed = await invokeManagedTraceCase(${symbol}, ${caseConstName}[${index}]!);`,
495
+ ` const traceEntry = ${caseConstName}[${index}]!;`,
496
+ ' if (traceEntry.threw !== null) {',
497
+ ' expect(observed.threw).not.toBeNull();',
498
+ ' expect(canonicalizeThrownValue(observed.threw)).toEqual(traceEntry.threw);',
499
+ ' return;',
500
+ ' }',
501
+ ' expect(observed.threw).toBeNull();',
502
+ ' assertObservedTraceValue(observed.value, traceEntry.expected, { floatDigits: FLOAT_TOLERANCE_DIGITS });',
503
+ ' });',
504
+ '',
505
+ ].join('\n'))
506
+ .join('\n');
507
+ };
508
+ const renderPopulatedPureTestSource = ({ harnessPath, scaffold, symbolCases, }) => {
509
+ const harnessImportPath = relativeImportPath({
510
+ fromFile: scaffold.filePath,
511
+ toFile: harnessPath,
512
+ });
513
+ const importPath = relativeImportPath({
514
+ fromFile: scaffold.filePath,
515
+ toFile: scaffold.moduleFilePath,
516
+ });
517
+ const managedSymbols = scaffold.symbols.filter((symbol) => symbolCases.has(symbol));
518
+ return [
519
+ '// AUTO-GENERATED by ushman-characterize populateScaffolds. Do not hand-edit.',
520
+ "import { describe, expect, test } from 'bun:test';",
521
+ `import { assertObservedTraceValue, canonicalizeThrownValue, invokeManagedTraceCase } from '${harnessImportPath}';`,
522
+ `import { ${managedSymbols.join(', ')} } from '${importPath}';`,
523
+ '',
524
+ 'const FLOAT_TOLERANCE_DIGITS = 4;',
525
+ '',
526
+ ...scaffold.symbols
527
+ .filter((symbol) => (symbolCases.get(symbol) ?? []).length > 0)
528
+ .map((symbol) => renderPureCaseConst({
529
+ cases: symbolCases.get(symbol) ?? [],
530
+ symbol,
531
+ })),
532
+ `describe('${scaffold.moduleRelativePath}', () => {`,
533
+ ...scaffold.symbols.map((symbol) => renderPureSymbolBlock({
534
+ cases: symbolCases.get(symbol) ?? [],
535
+ symbol,
536
+ })),
537
+ '});',
538
+ '',
539
+ ].join('\n');
540
+ };
541
+ const listLegacyStateTraceFiles = async (workspaceDir) => {
542
+ const tracesDir = path.join(workspaceDir, '.lab', 'characterize', 'traces');
543
+ const files = await listFilesRecursive({
544
+ dir: tracesDir,
545
+ suffix: '.jsonl',
546
+ });
547
+ return files.filter((filePath) => path.dirname(filePath) === tracesDir);
548
+ };
549
+ const indexLegacyStateTraceRecords = async (workspaceDir) => {
550
+ const topLevelTraceFiles = await listLegacyStateTraceFiles(workspaceDir);
551
+ const groupedRecords = new Map();
552
+ const parseFailures = [];
553
+ for (const filePath of topLevelTraceFiles) {
554
+ let records;
555
+ try {
556
+ records = await readTraceRecords({
557
+ contextPrefix: `Trace record ${path.basename(filePath)}`,
558
+ filePath,
559
+ });
560
+ }
561
+ catch (error) {
562
+ parseFailures.push(error instanceof Error ? error.message : String(error));
563
+ continue;
564
+ }
565
+ for (const record of records) {
566
+ for (const symbol of uniqueSymbolsInOrder([record.bindingName, record.functionName, record.methodName].filter((entry) => typeof entry === 'string' && entry.length > 0))) {
567
+ const current = groupedRecords.get(symbol) ?? [];
568
+ current.push(record);
569
+ groupedRecords.set(symbol, current);
570
+ }
571
+ }
572
+ }
573
+ return {
574
+ parseFailures,
575
+ recordsBySymbol: new Map([...groupedRecords.entries()].map(([symbol, records]) => [
576
+ symbol,
577
+ [...records].sort((left, right) => stableJsonStringify(left).localeCompare(stableJsonStringify(right))),
578
+ ])),
579
+ topLevelTraceFiles,
580
+ };
581
+ };
582
+ const synthesizeModuleTraceFromLegacyStateTraces = async ({ legacyTraceIndex, ownershipMap, scaffold, targetPath, }) => {
583
+ const ambiguousSymbols = sortStableStrings(scaffold.symbols.filter((symbol) => (ownershipMap.get(symbol) ?? []).length > 1));
584
+ if (ambiguousSymbols.length > 0) {
585
+ return {
586
+ ambiguity: ambiguousSymbols.map((symbol) => ({
587
+ owners: ownershipMap.get(symbol) ?? [],
588
+ symbol,
589
+ })),
590
+ written: false,
591
+ };
592
+ }
593
+ const matchedRecords = scaffold.symbols.flatMap((symbol) => legacyTraceIndex.recordsBySymbol.get(symbol) ?? []);
594
+ if (matchedRecords.length === 0) {
595
+ return {
596
+ ambiguity: [],
597
+ written: false,
598
+ };
599
+ }
600
+ const deduped = new Map();
601
+ for (const record of matchedRecords.sort((left, right) => stableJsonStringify(left).localeCompare(stableJsonStringify(right)))) {
602
+ const versioned = stampTraceRecord(record);
603
+ deduped.set(stableJsonStringify(versioned), versioned);
604
+ }
605
+ await atomicWriteText(targetPath, `${toJsonLines([...deduped.values()])}\n`);
606
+ return {
607
+ ambiguity: [],
608
+ written: true,
609
+ };
610
+ };
611
+ const resolveModuleTracePath = async ({ legacyTraceIndex, ownershipMap, scaffold, workspaceDir, }) => {
612
+ const moduleTracePath = buildPureModuleTracePath({
613
+ moduleRelativePath: scaffold.moduleRelativePath,
614
+ workspaceDir,
615
+ });
616
+ if (await Bun.file(moduleTracePath).exists()) {
617
+ return {
618
+ kind: 'found',
619
+ moduleTracePath,
620
+ synthesized: false,
621
+ };
622
+ }
623
+ const synthesized = await synthesizeModuleTraceFromLegacyStateTraces({
624
+ legacyTraceIndex,
625
+ ownershipMap,
626
+ scaffold,
627
+ targetPath: moduleTracePath,
628
+ });
629
+ if (synthesized.written) {
630
+ return {
631
+ kind: 'found',
632
+ moduleTracePath,
633
+ synthesized: true,
634
+ };
635
+ }
636
+ if (synthesized.ambiguity.length > 0) {
637
+ return {
638
+ kind: 'gap',
639
+ result: {
640
+ body: `Cannot synthesize ${moduleTracePath} from legacy state traces because these symbols are ambiguous across scaffolds: ${synthesized.ambiguity
641
+ .map(({ owners, symbol }) => `${symbol} (${owners.join(', ')})`)
642
+ .join('; ')}`,
643
+ summary: `ambiguous legacy trace synthesis for ${scaffold.relativeTestPath}`,
644
+ },
645
+ };
646
+ }
647
+ return {
648
+ kind: 'missing',
649
+ moduleTracePath,
650
+ };
651
+ };
652
+ const summarizePopulateScaffolds = ({ skipped, toolingGaps, written, }) => [
653
+ `populated ${written.length} pure scaffold file(s)`,
654
+ skipped.length > 0 ? `skipped ${skipped.length} operator-managed file(s)` : null,
655
+ toolingGaps.length > 0 ? `${toolingGaps.length} tooling gap(s)` : null,
656
+ ]
657
+ .filter((value) => value !== null)
658
+ .join('; ');
659
+ export const scaffoldPureCharacterizationTests = async ({ bundlePath, dryRun = false, workspaceRoot, }) => {
660
+ await assertV4Workspace(workspaceRoot);
661
+ const source = await Bun.file(bundlePath).text();
662
+ const bundle = parseBundleAst({ source, sourcePath: bundlePath });
663
+ const classification = classifyPureTopLevelFunctionsFromBundle(bundle);
664
+ const testsDir = path.join(workspaceRoot, 'tests', 'pure');
665
+ const harnessPaths = await ensureWorkspaceTraceHarness(workspaceRoot);
666
+ const moduleDir = harnessPaths.modulesDir;
667
+ await mkdir(testsDir, { recursive: true });
668
+ await mkdir(moduleDir, { recursive: true });
669
+ await mkdir(harnessPaths.pureFixturesDir, { recursive: true });
670
+ const written = [];
671
+ let autoAssertionCount = 0;
672
+ let stubCount = 0;
673
+ for (const info of classification.pureFunctions) {
674
+ const modulePath = path.join(moduleDir, `${sanitizeSymbolName(info.name)}.mjs`);
675
+ const moduleSource = buildStandaloneSymbolModule({
676
+ bindingName: info.bindingName,
677
+ bundle,
678
+ outputFilePath: modulePath,
679
+ });
680
+ if (!dryRun) {
681
+ await Bun.write(modulePath, `${moduleSource.trimEnd()}\n`);
682
+ }
683
+ const cases = dryRun ? [] : await loadFunctionCases({ info, modulePath });
684
+ autoAssertionCount += cases.length;
685
+ if (cases.length === 0) {
686
+ stubCount += 1;
687
+ }
688
+ const fixtureName = sanitizeSymbolName(info.name);
689
+ const fixturePath = path.join(harnessPaths.pureFixturesDir, `${fixtureName}.json`);
690
+ if (!dryRun) {
691
+ await Bun.write(fixturePath, `${JSON.stringify(cases, null, 2)}\n`);
692
+ }
693
+ const testFile = path.join(testsDir, `${sanitizeSymbolName(info.name)}.test.ts`);
694
+ const sourceText = renderTestSource({
695
+ fixtureName,
696
+ harnessPath: harnessPaths.harnessFile,
697
+ info,
698
+ modulePath,
699
+ testFile,
700
+ });
701
+ if (!dryRun) {
702
+ await Bun.write(testFile, sourceText);
703
+ }
704
+ written.push(testFile);
705
+ }
706
+ return {
707
+ autoAssertionCount,
708
+ pureFunctionCount: classification.pureFunctions.length,
709
+ stubCount,
710
+ totalTopLevelFunctions: classification.totalTopLevelFunctions,
711
+ written,
712
+ };
713
+ };
714
+ const recordPureToolingGap = async ({ body, summary, toolingGaps, workspaceDir, }) => {
715
+ toolingGaps.push(body);
716
+ await recordCharacterizeToolingGap({
717
+ body,
718
+ summary,
719
+ workspaceRoot: workspaceDir,
720
+ });
721
+ };
722
+ const resolvePureScaffoldUpdateMode = async ({ scaffold, sidecarScaffolds, }) => {
723
+ const trackedHash = sidecarScaffolds[scaffold.relativeTestPath];
724
+ if (trackedHash === undefined) {
725
+ return {
726
+ kind: 'gap',
727
+ result: {
728
+ body: `Cannot manage ${scaffold.relativeTestPath} because tests/.seed-fingerprints.json does not track it. Re-run \`ushman go --stage=seed\` for this workspace, or add the scaffold to the sidecar before retrying population.`,
729
+ summary: `untracked pure scaffold ${scaffold.relativeTestPath}`,
730
+ },
731
+ };
732
+ }
733
+ const currentSource = await Bun.file(scaffold.filePath).text();
734
+ const currentHash = hashSeedScaffoldContent(currentSource);
735
+ if (!isScaffoldUnmodified({
736
+ currentHash,
737
+ trackedHash,
738
+ })) {
739
+ return {
740
+ kind: 'skip',
741
+ };
742
+ }
743
+ return {
744
+ kind: 'populate',
745
+ };
746
+ };
747
+ const readPureScaffoldRecords = async ({ moduleTracePath, scaffold, }) => {
748
+ try {
749
+ return {
750
+ kind: 'records',
751
+ records: await readTraceRecords({
752
+ contextPrefix: `Trace record ${scaffold.moduleRelativePath}.jsonl`,
753
+ filePath: moduleTracePath,
754
+ }),
755
+ };
756
+ }
757
+ catch (error) {
758
+ return {
759
+ kind: 'gap',
760
+ result: {
761
+ body: `${error instanceof Error ? error.message : String(error)}\nRe-run \`ushman-characterize capture\` and then \`ushman-characterize stub-pure --regen-stale\` after regenerating stale traces.`,
762
+ summary: `invalid trace file for ${scaffold.relativeTestPath}`,
763
+ },
764
+ };
765
+ }
766
+ };
767
+ const buildPureScaffoldSymbolCases = ({ moduleTracePath, records, scaffold, }) => {
768
+ const symbolCases = new Map(scaffold.symbols.map((symbol) => [
769
+ symbol,
770
+ buildPureCases({
771
+ records,
772
+ symbol,
773
+ }),
774
+ ]));
775
+ const populatedCaseCount = [...symbolCases.values()].reduce((total, cases) => total + cases.length, 0);
776
+ if (populatedCaseCount === 0) {
777
+ return {
778
+ kind: 'gap',
779
+ result: {
780
+ body: `Missing trace cases for ${scaffold.relativeTestPath}. Trace file ${moduleTracePath} did not contain any records for ${scaffold.symbols.join(', ')}.\nRe-run \`ushman-characterize capture\` for the missing module coverage, or leave the operator-owned \`test.todo\` blocks in place until that symbol is exercised.`,
781
+ summary: `missing trace cases for ${scaffold.relativeTestPath}`,
782
+ },
783
+ };
784
+ }
785
+ return {
786
+ kind: 'cases',
787
+ symbolCases,
788
+ };
789
+ };
790
+ const populateSinglePureScaffold = async ({ harnessPath, legacyTraceIndex, ownershipMap, scaffold, sidecarScaffolds, workspaceDir, }) => {
791
+ const updateMode = await resolvePureScaffoldUpdateMode({
792
+ scaffold,
793
+ sidecarScaffolds,
794
+ });
795
+ if (updateMode.kind === 'gap') {
796
+ return {
797
+ toolingGap: updateMode.result,
798
+ };
799
+ }
800
+ if (updateMode.kind === 'skip') {
801
+ return {
802
+ skippedFilePath: scaffold.filePath,
803
+ };
804
+ }
805
+ const moduleTracePath = await resolveModuleTracePath({
806
+ legacyTraceIndex,
807
+ ownershipMap,
808
+ scaffold,
809
+ workspaceDir,
810
+ });
811
+ if (moduleTracePath.kind === 'gap') {
812
+ return {
813
+ toolingGap: moduleTracePath.result,
814
+ };
815
+ }
816
+ if (moduleTracePath.kind === 'missing' || !(await Bun.file(moduleTracePath.moduleTracePath).exists())) {
817
+ return {
818
+ toolingGap: {
819
+ body: `Missing trace for ${scaffold.relativeTestPath}. Expected ${buildPureModuleTracePath({
820
+ moduleRelativePath: scaffold.moduleRelativePath,
821
+ workspaceDir,
822
+ })}. Capture the module trace before re-running populateScaffolds.`,
823
+ summary: `missing trace for ${scaffold.relativeTestPath}`,
824
+ },
825
+ };
826
+ }
827
+ const recordsResult = await readPureScaffoldRecords({
828
+ moduleTracePath: moduleTracePath.moduleTracePath,
829
+ scaffold,
830
+ });
831
+ if (recordsResult.kind === 'gap') {
832
+ return {
833
+ toolingGap: recordsResult.result,
834
+ };
835
+ }
836
+ const casesResult = buildPureScaffoldSymbolCases({
837
+ moduleTracePath: moduleTracePath.moduleTracePath,
838
+ records: recordsResult.records,
839
+ scaffold,
840
+ });
841
+ if (casesResult.kind === 'gap') {
842
+ return {
843
+ toolingGap: casesResult.result,
844
+ };
845
+ }
846
+ const nextSource = renderPopulatedPureTestSource({
847
+ harnessPath,
848
+ scaffold,
849
+ symbolCases: casesResult.symbolCases,
850
+ });
851
+ await atomicWriteText(scaffold.filePath, `${nextSource.endsWith('\n') ? nextSource : `${nextSource}\n`}`);
852
+ return {
853
+ fingerprintEntry: [scaffold.relativeTestPath, hashSeedScaffoldContent(nextSource)],
854
+ synthesizedTracePath: moduleTracePath.synthesized ? moduleTracePath.moduleTracePath : undefined,
855
+ writtenFilePath: scaffold.filePath,
856
+ };
857
+ };
858
+ const recordPurePreflightFailures = async ({ legacyTraceIndex, parseFailures, toolingGaps, workspaceDir, }) => {
859
+ for (const failure of parseFailures) {
860
+ await recordCharacterizeToolingGap({
861
+ body: `${failure.message}\nFix the scaffold syntax or rerun \`ushman go --stage=seed\` to restore the managed scaffold before retrying population.`,
862
+ summary: `invalid pure scaffold ${toWorkspaceRelativePath({
863
+ filePath: failure.filePath,
864
+ workspaceDir,
865
+ })}`,
866
+ workspaceRoot: workspaceDir,
867
+ });
868
+ }
869
+ for (const failure of legacyTraceIndex.parseFailures) {
870
+ await recordCharacterizeToolingGap({
871
+ body: `${failure}\nRe-run \`ushman-characterize capture\` to refresh the malformed state trace before retrying legacy synthesis.`,
872
+ summary: 'invalid legacy state trace',
873
+ workspaceRoot: workspaceDir,
874
+ });
875
+ }
876
+ toolingGaps.push(...parseFailures.map((failure) => failure.message), ...legacyTraceIndex.parseFailures);
877
+ };
878
+ const applyPureScaffoldProcessingResult = async ({ nextFingerprints, result, skipped, synthesizedTraces, toolingGaps, workspaceDir, written, }) => {
879
+ if (result.toolingGap) {
880
+ await recordPureToolingGap({
881
+ body: result.toolingGap.body,
882
+ summary: result.toolingGap.summary,
883
+ toolingGaps,
884
+ workspaceDir,
885
+ });
886
+ }
887
+ if (result.skippedFilePath) {
888
+ skipped.push(result.skippedFilePath);
889
+ }
890
+ if (result.synthesizedTracePath) {
891
+ synthesizedTraces.push(result.synthesizedTracePath);
892
+ }
893
+ if (result.writtenFilePath) {
894
+ written.push(result.writtenFilePath);
895
+ }
896
+ if (result.fingerprintEntry) {
897
+ nextFingerprints[result.fingerprintEntry[0]] = result.fingerprintEntry[1];
898
+ }
899
+ };
900
+ export const populateScaffolds = async ({ modulePathPrefix, symbolFilter, workspaceDir, }) => {
901
+ await assertV4Workspace(workspaceDir);
902
+ const harnessPaths = await ensureWorkspaceTraceHarness(workspaceDir);
903
+ const sidecar = await loadSeedFingerprintSidecar(workspaceDir);
904
+ const { parseFailures, scaffolds } = await collectManagedPureScaffolds(workspaceDir);
905
+ const ownershipMap = buildSymbolOwnershipMap(scaffolds);
906
+ const legacyTraceIndex = await indexLegacyStateTraceRecords(workspaceDir);
907
+ const nextFingerprints = {};
908
+ const skipped = [];
909
+ const synthesizedTraces = [];
910
+ const toolingGaps = [];
911
+ const written = [];
912
+ await recordPurePreflightFailures({
913
+ legacyTraceIndex,
914
+ parseFailures,
915
+ toolingGaps,
916
+ workspaceDir,
917
+ });
918
+ for (const scaffold of scaffolds) {
919
+ if (!matchesPopulateFilters({
920
+ modulePathPrefix,
921
+ scaffold,
922
+ symbolFilter,
923
+ })) {
924
+ continue;
925
+ }
926
+ const result = await populateSinglePureScaffold({
927
+ harnessPath: harnessPaths.harnessFile,
928
+ legacyTraceIndex,
929
+ ownershipMap,
930
+ scaffold,
931
+ sidecarScaffolds: sidecar.scaffolds,
932
+ workspaceDir,
933
+ });
934
+ await applyPureScaffoldProcessingResult({
935
+ nextFingerprints,
936
+ result,
937
+ skipped,
938
+ synthesizedTraces,
939
+ toolingGaps,
940
+ workspaceDir,
941
+ written,
942
+ });
943
+ }
944
+ const affectedFiles = [...written, ...synthesizedTraces].map((filePath) => toWorkspaceRelativePath({
945
+ filePath,
946
+ workspaceDir,
947
+ }));
948
+ const sidecarPath = Object.keys(nextFingerprints).length > 0
949
+ ? await updateSeedFingerprintSidecar({
950
+ entries: nextFingerprints,
951
+ workspaceDir,
952
+ })
953
+ : sidecar.filePath;
954
+ await recordCharacterizeValidatorResult({
955
+ affectedFiles,
956
+ metrics: {
957
+ skippedCount: skipped.length,
958
+ synthesizedTraceCount: synthesizedTraces.length,
959
+ toolingGapCount: toolingGaps.length,
960
+ writtenCount: written.length,
961
+ },
962
+ summary: summarizePopulateScaffolds({
963
+ skipped,
964
+ toolingGaps,
965
+ written,
966
+ }),
967
+ verdict: toolingGaps.length > 0 ? 'yellow' : 'green',
968
+ workspaceRoot: workspaceDir,
969
+ });
970
+ return {
971
+ sidecarPath,
972
+ skipped,
973
+ synthesizedTraces,
974
+ toolingGaps,
975
+ written,
976
+ };
977
+ };
978
+ export const __testOnly = {
979
+ buildCandidateArgs,
980
+ executeSampleSafely,
981
+ hasDetachedTextureLoadHazard,
982
+ inferKind,
983
+ isCaseRenderable,
984
+ loadFunctionCases,
985
+ renderLiteral,
986
+ settlePromiseSample,
987
+ };