modality-ts 0.0.9 → 0.0.11
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/README.md +11 -7
- package/dist/check/diagnostics/bounds.d.ts +2 -1
- package/dist/check/diagnostics/bounds.d.ts.map +1 -1
- package/dist/check/diagnostics/bounds.js +3 -3
- package/dist/check/diagnostics/bounds.js.map +1 -1
- package/dist/check/engine/check-model.d.ts +3 -0
- package/dist/check/engine/check-model.d.ts.map +1 -1
- package/dist/check/engine/check-model.js +408 -43
- package/dist/check/engine/check-model.js.map +1 -1
- package/dist/check/engine/model-api.d.ts.map +1 -1
- package/dist/check/engine/model-api.js +5 -3
- package/dist/check/engine/model-api.js.map +1 -1
- package/dist/check/engine/stabilize.d.ts +2 -1
- package/dist/check/engine/stabilize.d.ts.map +1 -1
- package/dist/check/engine/stabilize.js +31 -9
- package/dist/check/engine/stabilize.js.map +1 -1
- package/dist/check/engine/state-utils.d.ts +3 -2
- package/dist/check/engine/state-utils.d.ts.map +1 -1
- package/dist/check/engine/state-utils.js +20 -7
- package/dist/check/engine/state-utils.js.map +1 -1
- package/dist/check/engine/transitions.d.ts +9 -1
- package/dist/check/engine/transitions.d.ts.map +1 -1
- package/dist/check/engine/transitions.js +32 -5
- package/dist/check/engine/transitions.js.map +1 -1
- package/dist/check/index.d.ts +1 -1
- package/dist/check/index.d.ts.map +1 -1
- package/dist/check/index.js.map +1 -1
- package/dist/check/properties/finalize.d.ts +4 -3
- package/dist/check/properties/finalize.d.ts.map +1 -1
- package/dist/check/properties/finalize.js +45 -9
- package/dist/check/properties/finalize.js.map +1 -1
- package/dist/check/properties/leads-to.d.ts.map +1 -1
- package/dist/check/properties/leads-to.js +4 -3
- package/dist/check/properties/leads-to.js.map +1 -1
- package/dist/check/properties/observe.d.ts +4 -3
- package/dist/check/properties/observe.d.ts.map +1 -1
- package/dist/check/properties/observe.js +5 -5
- package/dist/check/properties/observe.js.map +1 -1
- package/dist/check/properties/reachable-from.d.ts +4 -2
- package/dist/check/properties/reachable-from.d.ts.map +1 -1
- package/dist/check/properties/reachable-from.js +2 -2
- package/dist/check/properties/reachable-from.js.map +1 -1
- package/dist/check/slicing/slice-model.d.ts +1 -0
- package/dist/check/slicing/slice-model.d.ts.map +1 -1
- package/dist/check/slicing/slice-model.js +38 -12
- package/dist/check/slicing/slice-model.js.map +1 -1
- package/dist/check/traces/trace.d.ts +7 -2
- package/dist/check/traces/trace.d.ts.map +1 -1
- package/dist/check/traces/trace.js +9 -4
- package/dist/check/traces/trace.js.map +1 -1
- package/dist/check/types.d.ts +83 -3
- package/dist/check/types.d.ts.map +1 -1
- package/dist/cli/cli.js +134 -42
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/defaults.d.ts +13 -0
- package/dist/cli/defaults.d.ts.map +1 -0
- package/dist/cli/defaults.js +57 -0
- package/dist/cli/defaults.js.map +1 -0
- package/dist/cli/features/check/command.d.ts +7 -0
- package/dist/cli/features/check/command.d.ts.map +1 -1
- package/dist/cli/features/check/command.js +90 -13
- package/dist/cli/features/check/command.js.map +1 -1
- package/dist/cli/features/conform/command.d.ts.map +1 -1
- package/dist/cli/features/conform/command.js +3 -1
- package/dist/cli/features/conform/command.js.map +1 -1
- package/dist/cli/features/export/command.js +12 -4
- package/dist/cli/features/export/command.js.map +1 -1
- package/dist/cli/features/extract/command.d.ts +2 -1
- package/dist/cli/features/extract/command.d.ts.map +1 -1
- package/dist/cli/features/extract/command.js +375 -94
- package/dist/cli/features/extract/command.js.map +1 -1
- package/dist/cli/features/init/command.d.ts +9 -0
- package/dist/cli/features/init/command.d.ts.map +1 -0
- package/dist/cli/features/init/command.js +23 -0
- package/dist/cli/features/init/command.js.map +1 -0
- package/dist/cli/features/init/index.d.ts +3 -0
- package/dist/cli/features/init/index.d.ts.map +1 -0
- package/dist/cli/features/init/index.js +2 -0
- package/dist/cli/features/init/index.js.map +1 -0
- package/dist/cli/harness/index.d.ts.map +1 -1
- package/dist/cli/harness/index.js +17 -4
- package/dist/cli/harness/index.js.map +1 -1
- package/dist/cli/registry/index.d.ts +4 -4
- package/dist/cli/registry/index.d.ts.map +1 -1
- package/dist/cli/registry/index.js +6 -4
- package/dist/cli/registry/index.js.map +1 -1
- package/dist/core/ir/domains.d.ts +2 -0
- package/dist/core/ir/domains.d.ts.map +1 -1
- package/dist/core/ir/domains.js +71 -0
- package/dist/core/ir/domains.js.map +1 -1
- package/dist/core/ir/validator.d.ts +4 -1
- package/dist/core/ir/validator.d.ts.map +1 -1
- package/dist/core/ir/validator.js +45 -12
- package/dist/core/ir/validator.js.map +1 -1
- package/dist/core/props/index.d.ts.map +1 -1
- package/dist/core/props/index.js.map +1 -1
- package/dist/core/report/types.d.ts +68 -0
- package/dist/core/report/types.d.ts.map +1 -1
- package/dist/extract/engine/pipeline/index.d.ts +3 -1
- package/dist/extract/engine/pipeline/index.d.ts.map +1 -1
- package/dist/extract/engine/pipeline/index.js +10 -23
- package/dist/extract/engine/pipeline/index.js.map +1 -1
- package/dist/extract/engine/spi/index.d.ts +38 -7
- package/dist/extract/engine/spi/index.d.ts.map +1 -1
- package/dist/extract/engine/spi/index.js +1 -1
- package/dist/extract/engine/spi/index.js.map +1 -1
- package/dist/extract/engine/ts/domains.d.ts +1 -1
- package/dist/extract/engine/ts/domains.d.ts.map +1 -1
- package/dist/extract/engine/ts/domains.js +25 -17
- package/dist/extract/engine/ts/domains.js.map +1 -1
- package/dist/extract/engine/ts/react-source-transitions.d.ts +2 -1
- package/dist/extract/engine/ts/react-source-transitions.d.ts.map +1 -1
- package/dist/extract/engine/ts/react-source-transitions.js +17 -7
- package/dist/extract/engine/ts/react-source-transitions.js.map +1 -1
- package/dist/extract/engine/ts/routes.d.ts +2 -2
- package/dist/extract/engine/ts/routes.d.ts.map +1 -1
- package/dist/extract/engine/ts/routes.js +5 -24
- package/dist/extract/engine/ts/routes.js.map +1 -1
- package/dist/extract/engine/ts/static-navigation.d.ts +2 -1
- package/dist/extract/engine/ts/static-navigation.d.ts.map +1 -1
- package/dist/extract/engine/ts/static-navigation.js +30 -20
- package/dist/extract/engine/ts/static-navigation.js.map +1 -1
- package/dist/extract/engine/ts/transition/async.d.ts +3 -10
- package/dist/extract/engine/ts/transition/async.d.ts.map +1 -1
- package/dist/extract/engine/ts/transition/async.js +60 -59
- package/dist/extract/engine/ts/transition/async.js.map +1 -1
- package/dist/extract/engine/ts/transition/effects.d.ts +2 -23
- package/dist/extract/engine/ts/transition/effects.d.ts.map +1 -1
- package/dist/extract/engine/ts/transition/effects.js +10 -171
- package/dist/extract/engine/ts/transition/effects.js.map +1 -1
- package/dist/extract/engine/ts/transition/expressions.d.ts +1 -0
- package/dist/extract/engine/ts/transition/expressions.d.ts.map +1 -1
- package/dist/extract/engine/ts/transition/expressions.js +24 -1
- package/dist/extract/engine/ts/transition/expressions.js.map +1 -1
- package/dist/extract/engine/ts/transition/guards.d.ts.map +1 -1
- package/dist/extract/engine/ts/transition/guards.js +11 -2
- package/dist/extract/engine/ts/transition/guards.js.map +1 -1
- package/dist/extract/engine/ts/transition/handlers.d.ts +2 -23
- package/dist/extract/engine/ts/transition/handlers.d.ts.map +1 -1
- package/dist/extract/engine/ts/transition/handlers.js +46 -188
- package/dist/extract/engine/ts/transition/handlers.js.map +1 -1
- package/dist/extract/engine/ts/transition/locals.d.ts +4 -0
- package/dist/extract/engine/ts/transition/locals.d.ts.map +1 -1
- package/dist/extract/engine/ts/transition/locals.js +27 -16
- package/dist/extract/engine/ts/transition/locals.js.map +1 -1
- package/dist/extract/engine/ts/transition/navigation.d.ts +7 -5
- package/dist/extract/engine/ts/transition/navigation.d.ts.map +1 -1
- package/dist/extract/engine/ts/transition/navigation.js +85 -41
- package/dist/extract/engine/ts/transition/navigation.js.map +1 -1
- package/dist/extract/engine/ts/transition/plugin-calls.d.ts.map +1 -1
- package/dist/extract/engine/ts/transition/plugin-calls.js +1 -1
- package/dist/extract/engine/ts/transition/plugin-calls.js.map +1 -1
- package/dist/extract/engine/ts/transition/statement-summary.d.ts +34 -0
- package/dist/extract/engine/ts/transition/statement-summary.d.ts.map +1 -0
- package/dist/extract/engine/ts/transition/statement-summary.js +502 -0
- package/dist/extract/engine/ts/transition/statement-summary.js.map +1 -0
- package/dist/extract/engine/ts/transition/timers.d.ts.map +1 -1
- package/dist/extract/engine/ts/transition/timers.js +7 -17
- package/dist/extract/engine/ts/transition/timers.js.map +1 -1
- package/dist/extract/sources/jotai/domains.d.ts +2 -1
- package/dist/extract/sources/jotai/domains.d.ts.map +1 -1
- package/dist/extract/sources/jotai/domains.js +2 -164
- package/dist/extract/sources/jotai/domains.js.map +1 -1
- package/dist/extract/sources/jotai/harness.d.ts.map +1 -1
- package/dist/extract/sources/jotai/harness.js +5 -2
- package/dist/extract/sources/jotai/harness.js.map +1 -1
- package/dist/extract/sources/jotai/index.d.ts +3 -3
- package/dist/extract/sources/jotai/index.d.ts.map +1 -1
- package/dist/extract/sources/jotai/index.js +2 -18
- package/dist/extract/sources/jotai/index.js.map +1 -1
- package/dist/extract/sources/jotai/plugin.d.ts +4 -0
- package/dist/extract/sources/jotai/plugin.d.ts.map +1 -0
- package/dist/extract/sources/jotai/plugin.js +19 -0
- package/dist/extract/sources/jotai/plugin.js.map +1 -0
- package/dist/extract/sources/jotai/transitions.d.ts +15 -0
- package/dist/extract/sources/jotai/transitions.d.ts.map +1 -0
- package/dist/extract/sources/jotai/transitions.js +41 -0
- package/dist/extract/sources/jotai/transitions.js.map +1 -0
- package/dist/extract/sources/router/discover.d.ts +9 -0
- package/dist/extract/sources/router/discover.d.ts.map +1 -0
- package/dist/extract/sources/router/discover.js +135 -0
- package/dist/extract/sources/router/discover.js.map +1 -0
- package/dist/extract/sources/router/index.d.ts +7 -3
- package/dist/extract/sources/router/index.d.ts.map +1 -1
- package/dist/extract/sources/router/index.js +21 -12
- package/dist/extract/sources/router/index.js.map +1 -1
- package/dist/extract/sources/router/navigation.d.ts +3 -4
- package/dist/extract/sources/router/navigation.d.ts.map +1 -1
- package/dist/extract/sources/router/navigation.js +33 -1
- package/dist/extract/sources/router/navigation.js.map +1 -1
- package/dist/extract/sources/router/redirects.d.ts +4 -0
- package/dist/extract/sources/router/redirects.d.ts.map +1 -0
- package/dist/extract/sources/router/redirects.js +37 -0
- package/dist/extract/sources/router/redirects.js.map +1 -0
- package/dist/extract/sources/router/routes.d.ts +2 -2
- package/dist/extract/sources/router/routes.d.ts.map +1 -1
- package/dist/extract/sources/router/routes.js +24 -6
- package/dist/extract/sources/router/routes.js.map +1 -1
- package/dist/extract/sources/shared/react-transition-extract.d.ts +14 -0
- package/dist/extract/sources/shared/react-transition-extract.d.ts.map +1 -0
- package/dist/extract/sources/shared/react-transition-extract.js +23 -0
- package/dist/extract/sources/shared/react-transition-extract.js.map +1 -0
- package/dist/extract/sources/swr/domains.d.ts +3 -2
- package/dist/extract/sources/swr/domains.d.ts.map +1 -1
- package/dist/extract/sources/swr/domains.js +3 -96
- package/dist/extract/sources/swr/domains.js.map +1 -1
- package/dist/extract/sources/swr/harness.d.ts.map +1 -1
- package/dist/extract/sources/swr/harness.js +13 -6
- package/dist/extract/sources/swr/harness.js.map +1 -1
- package/dist/extract/sources/swr/index.d.ts +3 -3
- package/dist/extract/sources/swr/index.d.ts.map +1 -1
- package/dist/extract/sources/swr/index.js +2 -20
- package/dist/extract/sources/swr/index.js.map +1 -1
- package/dist/extract/sources/swr/plugin.d.ts +4 -0
- package/dist/extract/sources/swr/plugin.d.ts.map +1 -0
- package/dist/extract/sources/swr/plugin.js +21 -0
- package/dist/extract/sources/swr/plugin.js.map +1 -0
- package/dist/extract/sources/swr/template.js +1 -1
- package/dist/extract/sources/swr/template.js.map +1 -1
- package/dist/extract/sources/swr/transitions.d.ts +15 -0
- package/dist/extract/sources/swr/transitions.d.ts.map +1 -0
- package/dist/extract/sources/swr/transitions.js +43 -0
- package/dist/extract/sources/swr/transitions.js.map +1 -0
- package/dist/extract/sources/swr/writes.d.ts.map +1 -1
- package/dist/extract/sources/swr/writes.js +1 -1
- package/dist/extract/sources/swr/writes.js.map +1 -1
- package/dist/extract/sources/use-state/harness.d.ts.map +1 -1
- package/dist/extract/sources/use-state/harness.js +5 -2
- package/dist/extract/sources/use-state/harness.js.map +1 -1
- package/dist/extract/sources/use-state/index.d.ts.map +1 -1
- package/dist/extract/sources/use-state/index.js +1 -166
- package/dist/extract/sources/use-state/index.js.map +1 -1
- package/dist/extract/sources/use-state/transitions.d.ts.map +1 -1
- package/dist/extract/sources/use-state/transitions.js +17 -9
- package/dist/extract/sources/use-state/transitions.js.map +1 -1
- package/dist/extract/sources/use-state/types.d.ts +1 -0
- package/dist/extract/sources/use-state/types.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { dirname, join, parse, resolve } from "node:path";
|
|
3
|
+
import { dirname, extname, join, parse, resolve } from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
import * as ts from "typescript";
|
|
6
6
|
import { runExtractionPipeline } from "modality-ts/extract";
|
|
7
|
-
import { canonicalJson, parseModelArtifact, } from "modality-ts/core";
|
|
8
|
-
import { routerSource } from "modality-ts/extract/sources/router";
|
|
7
|
+
import { canonicalJson, collectTokenDomainPaths, domainCardinality, parseModelArtifact, } from "modality-ts/core";
|
|
8
|
+
import { parseReactRouterRoutes, routerSource, } from "modality-ts/extract/sources/router";
|
|
9
9
|
import { emitAppModel } from "../../codegen/model.js";
|
|
10
10
|
import { loadAndApplyOverlay, loadOverlaySpec } from "../../overlay.js";
|
|
11
11
|
import { createBuiltinModalityRegistry } from "../../registry/index.js";
|
|
12
12
|
export async function runExtractCommand(options) {
|
|
13
|
-
const
|
|
14
|
-
const
|
|
13
|
+
const sourcePaths = normalizedSourcePaths(options);
|
|
14
|
+
const projectBase = await loadExtractionProject(sourcePaths);
|
|
15
|
+
const config = await loadModalityConfig(options.configPath ?? (await findNearestConfig(projectBase.configStartDir)));
|
|
15
16
|
const route = options.route ?? config.route ?? "/";
|
|
16
17
|
const appModelPath = options.appModelPath ?? `${dirname(options.modelPath)}/app.model.ts`;
|
|
17
18
|
const packageJsonPath = options.packageJsonPath ??
|
|
18
19
|
config.packageJsonPath ??
|
|
19
|
-
(await findNearestPackageJson(
|
|
20
|
+
(await findNearestPackageJson(projectBase.configStartDir));
|
|
20
21
|
const dependencies = await readPackageDependencies(packageJsonPath);
|
|
21
22
|
const registry = createBuiltinModalityRegistry({
|
|
22
23
|
dependencies,
|
|
@@ -30,6 +31,9 @@ export async function runExtractCommand(options) {
|
|
|
30
31
|
],
|
|
31
32
|
routerPlugin: options.routerPlugin ?? config.routerPlugin,
|
|
32
33
|
});
|
|
34
|
+
const routerAdapter = registry.routerPlugin ?? routerSource();
|
|
35
|
+
const project = await attachRouteInventory(projectBase, routerAdapter);
|
|
36
|
+
const routePatterns = project.inventory.routes.map((node) => node.pattern);
|
|
33
37
|
const effectApis = uniqueStrings([
|
|
34
38
|
...(config.effectApis ?? []),
|
|
35
39
|
...(options.effectApis ?? []),
|
|
@@ -46,27 +50,15 @@ export async function runExtractCommand(options) {
|
|
|
46
50
|
sourceText: project.sourceText,
|
|
47
51
|
fileName: project.entryFile,
|
|
48
52
|
route,
|
|
49
|
-
routePatterns
|
|
53
|
+
routePatterns,
|
|
50
54
|
effectApis,
|
|
51
55
|
sourcePlugins: registry.sourcePlugins,
|
|
52
56
|
routerPlugin: registry.routerPlugin,
|
|
57
|
+
inventory: project.inventory,
|
|
53
58
|
});
|
|
54
59
|
const transitions = [...pipeline.transitions];
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
...project.routes,
|
|
58
|
-
...transitionNavigatedRoutes(transitions),
|
|
59
|
-
]);
|
|
60
|
-
const defaultRouter = routerSource();
|
|
61
|
-
const routeVars = registry.routerPlugin
|
|
62
|
-
? registry.routerPlugin.routeVars(discoveredRoutes, {
|
|
63
|
-
route,
|
|
64
|
-
bounds: { maxHistory: 4 },
|
|
65
|
-
})
|
|
66
|
-
: defaultRouter.routeVars(discoveredRoutes, {
|
|
67
|
-
route,
|
|
68
|
-
bounds: { maxHistory: 4 },
|
|
69
|
-
});
|
|
60
|
+
const lowering = buildLocationLowering(transitions, routerAdapter, project.inventory);
|
|
61
|
+
const routeVars = routerAdapter.locationVars(project.inventory, { route, bounds: { maxHistory: 4 } }, lowering);
|
|
70
62
|
const templateVars = pipeline.templateFragments.flatMap((fragment) => fragment.vars);
|
|
71
63
|
const stateVars = refineAssignedLiteralDomains([...pipeline.stateVars, ...templateVars], transitions);
|
|
72
64
|
const extractedModel = {
|
|
@@ -110,7 +102,7 @@ export async function runExtractCommand(options) {
|
|
|
110
102
|
extractionCaveats,
|
|
111
103
|
},
|
|
112
104
|
};
|
|
113
|
-
const report = createExtractionReport(project.sourceFiles, model, warnings, overlay.ignoredVars, options.now ?? new Date());
|
|
105
|
+
const report = createExtractionReport(project.sourceFiles, model, warnings, overlay.ignoredVars, options.now ?? new Date(), project.inventory);
|
|
114
106
|
await mkdir(dirname(options.modelPath), { recursive: true });
|
|
115
107
|
await writeFile(options.modelPath, `${canonicalJson(model)}\n`, "utf8");
|
|
116
108
|
await mkdir(dirname(appModelPath), { recursive: true });
|
|
@@ -122,11 +114,42 @@ export async function runExtractCommand(options) {
|
|
|
122
114
|
if (options.expectModelPath) {
|
|
123
115
|
await assertMatchesExpectedModel(model, options.expectModelPath);
|
|
124
116
|
}
|
|
117
|
+
const stateSpaceLine = (() => {
|
|
118
|
+
const contributors = report.stateContributors;
|
|
119
|
+
if (!contributors)
|
|
120
|
+
return undefined;
|
|
121
|
+
const { totalBits, topVars } = contributors;
|
|
122
|
+
const top = topVars
|
|
123
|
+
.slice(0, 3)
|
|
124
|
+
.map((v) => `${v.varId}(${v.bits.toFixed(1)})`)
|
|
125
|
+
.join(",");
|
|
126
|
+
return `state-space≈${totalBits.toFixed(1)}bits top:${top}`;
|
|
127
|
+
})();
|
|
128
|
+
const coarseDomainsLine = (() => {
|
|
129
|
+
const entries = report.coarseDomains ?? [];
|
|
130
|
+
if (entries.length === 0)
|
|
131
|
+
return undefined;
|
|
132
|
+
const count = entries.reduce((sum, entry) => sum + entry.paths.length, 0);
|
|
133
|
+
const first = entries[0];
|
|
134
|
+
if (!first)
|
|
135
|
+
return undefined;
|
|
136
|
+
const examplePath = first.paths[0];
|
|
137
|
+
return `coarse-domains=${count} e.g. ${first.varId}[${examplePath ?? ""}]`;
|
|
138
|
+
})();
|
|
139
|
+
const routeCoverageLine = (() => {
|
|
140
|
+
const coverage = report.routeCoverage;
|
|
141
|
+
if (!coverage || coverage.configured === 0)
|
|
142
|
+
return undefined;
|
|
143
|
+
return formatRouteCoverageLine(coverage);
|
|
144
|
+
})();
|
|
125
145
|
return {
|
|
126
146
|
model,
|
|
127
147
|
report,
|
|
128
148
|
lines: [
|
|
129
149
|
`extracted vars=${pipeline.stateVars.length + pipeline.templateFragments.flatMap((fragment) => fragment.vars).length} transitions=${transitions.length}`,
|
|
150
|
+
...(stateSpaceLine ? [stateSpaceLine] : []),
|
|
151
|
+
...(routeCoverageLine ? [routeCoverageLine] : []),
|
|
152
|
+
...(coarseDomainsLine ? [coarseDomainsLine] : []),
|
|
130
153
|
`plugins=${registry.plugins.map((plugin) => `${plugin.kind}:${plugin.id}@${plugin.version}`).join(",") || "none"}`,
|
|
131
154
|
`model=${options.modelPath}`,
|
|
132
155
|
`appModel=${appModelPath}`,
|
|
@@ -144,8 +167,22 @@ export async function runExtractCommand(options) {
|
|
|
144
167
|
],
|
|
145
168
|
};
|
|
146
169
|
}
|
|
147
|
-
|
|
148
|
-
const
|
|
170
|
+
function normalizedSourcePaths(options) {
|
|
171
|
+
const sourcePaths = options.sourcePaths ?? [];
|
|
172
|
+
const paths = [
|
|
173
|
+
...sourcePaths,
|
|
174
|
+
...(options.sourcePath ? [options.sourcePath] : []),
|
|
175
|
+
];
|
|
176
|
+
if (paths.length === 0)
|
|
177
|
+
throw new Error("Missing source.tsx path");
|
|
178
|
+
return uniqueStrings(paths.map((path) => resolve(path)));
|
|
179
|
+
}
|
|
180
|
+
async function loadExtractionProject(sourcePaths) {
|
|
181
|
+
if (sourcePaths.length > 1)
|
|
182
|
+
return loadMultiFileExtractionProject(sourcePaths);
|
|
183
|
+
const resolved = sourcePaths[0];
|
|
184
|
+
if (!resolved)
|
|
185
|
+
throw new Error("extract requires at least one source path");
|
|
149
186
|
const info = await stat(resolved);
|
|
150
187
|
if (!info.isDirectory()) {
|
|
151
188
|
const source = await readFile(resolved, "utf8");
|
|
@@ -156,7 +193,7 @@ async function loadExtractionProject(sourcePath) {
|
|
|
156
193
|
sourceText: imported.sources.map((entry) => entry.text).join("\n"),
|
|
157
194
|
sourceFiles: imported.sources.map((entry) => entry.path),
|
|
158
195
|
sources: imported.sources,
|
|
159
|
-
routes: [],
|
|
196
|
+
inventory: { routes: [] },
|
|
160
197
|
effectApis: fetchEffectApis(imported.sources.map((entry) => entry.text).join("\n")),
|
|
161
198
|
configStartDir: dirname(resolved),
|
|
162
199
|
};
|
|
@@ -166,6 +203,7 @@ async function loadExtractionProject(sourcePath) {
|
|
|
166
203
|
const rootPath = join(resolved, "app", "root.tsx");
|
|
167
204
|
const roots = await existingFiles([rootPath]);
|
|
168
205
|
const entries = [
|
|
206
|
+
{ path: routesPath, text: await readFile(routesPath, "utf8") },
|
|
169
207
|
...(await Promise.all(roots.map(async (path) => ({ path, text: await readFile(path, "utf8") })))),
|
|
170
208
|
...(await Promise.all(routeEntries.map(async (entry) => ({
|
|
171
209
|
path: resolve(dirname(routesPath), entry.file),
|
|
@@ -180,17 +218,79 @@ async function loadExtractionProject(sourcePath) {
|
|
|
180
218
|
sourceText,
|
|
181
219
|
sourceFiles: imported.sources.map((entry) => entry.path),
|
|
182
220
|
sources: imported.sources,
|
|
183
|
-
routes:
|
|
221
|
+
inventory: { routes: [] },
|
|
184
222
|
effectApis: fetchEffectApis(sourceText),
|
|
185
223
|
configStartDir: resolved,
|
|
186
224
|
};
|
|
187
225
|
}
|
|
226
|
+
async function loadMultiFileExtractionProject(sourcePaths) {
|
|
227
|
+
const projects = await Promise.all(sourcePaths.map((sourcePath) => loadExtractionProject([sourcePath])));
|
|
228
|
+
const sourcesByPath = new Map();
|
|
229
|
+
for (const project of projects) {
|
|
230
|
+
for (const source of project.sources) {
|
|
231
|
+
sourcesByPath.set(source.path, source);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const sources = [...sourcesByPath.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
235
|
+
const sourceText = sources.map((entry) => entry.text).join("\n");
|
|
236
|
+
return {
|
|
237
|
+
entryFile: projects.map((project) => project.entryFile).join(","),
|
|
238
|
+
sourceText,
|
|
239
|
+
sourceFiles: sources.map((entry) => entry.path),
|
|
240
|
+
sources,
|
|
241
|
+
inventory: { routes: [] },
|
|
242
|
+
effectApis: uniqueStrings([
|
|
243
|
+
...projects.flatMap((project) => project.effectApis),
|
|
244
|
+
...fetchEffectApis(sourceText),
|
|
245
|
+
]),
|
|
246
|
+
configStartDir: commonAncestor(projects.map((project) => project.configStartDir)),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
async function attachRouteInventory(project, adapter) {
|
|
250
|
+
const files = [...project.sources];
|
|
251
|
+
const manifestPath = files.find((file) => file.path.endsWith("routes.ts"))?.path ??
|
|
252
|
+
(await findNearestRoutesManifest(project.configStartDir));
|
|
253
|
+
if (manifestPath &&
|
|
254
|
+
!files.some((file) => resolve(file.path) === resolve(manifestPath))) {
|
|
255
|
+
files.push({
|
|
256
|
+
path: manifestPath,
|
|
257
|
+
text: await readFile(manifestPath, "utf8"),
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
const inventory = await adapter.discoverRoutes({
|
|
261
|
+
rootDir: project.configStartDir,
|
|
262
|
+
files,
|
|
263
|
+
readFile: (path) => readFile(path, "utf8"),
|
|
264
|
+
});
|
|
265
|
+
return { ...project, inventory };
|
|
266
|
+
}
|
|
267
|
+
async function findNearestRoutesManifest(startDir) {
|
|
268
|
+
let current = resolve(startDir);
|
|
269
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
270
|
+
const candidate = join(current, "app", "routes.ts");
|
|
271
|
+
try {
|
|
272
|
+
const info = await stat(candidate);
|
|
273
|
+
if (info.isFile())
|
|
274
|
+
return candidate;
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
// keep walking upward
|
|
278
|
+
}
|
|
279
|
+
const parent = dirname(current);
|
|
280
|
+
if (parent === current)
|
|
281
|
+
break;
|
|
282
|
+
current = parent;
|
|
283
|
+
}
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
188
286
|
async function sourceWithLocalImports(entries, tsconfig) {
|
|
189
287
|
const seen = new Set();
|
|
190
288
|
const sources = [];
|
|
191
289
|
const queue = [...entries];
|
|
192
290
|
while (queue.length > 0) {
|
|
193
291
|
const next = queue.shift();
|
|
292
|
+
if (!next)
|
|
293
|
+
break;
|
|
194
294
|
const canonical = resolve(next.path);
|
|
195
295
|
if (seen.has(canonical))
|
|
196
296
|
continue;
|
|
@@ -336,33 +436,6 @@ async function existingFiles(paths) {
|
|
|
336
436
|
}
|
|
337
437
|
return found;
|
|
338
438
|
}
|
|
339
|
-
function parseReactRouterRoutes(source) {
|
|
340
|
-
const routes = [];
|
|
341
|
-
const parsed = tsCreateSourceFile(source);
|
|
342
|
-
const visit = (node) => {
|
|
343
|
-
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
|
|
344
|
-
if (node.expression.text === "index" &&
|
|
345
|
-
ts.isStringLiteral(node.arguments[0])) {
|
|
346
|
-
routes.push({ pattern: "/", file: node.arguments[0].text });
|
|
347
|
-
}
|
|
348
|
-
if (node.expression.text === "route" &&
|
|
349
|
-
ts.isStringLiteral(node.arguments[0]) &&
|
|
350
|
-
ts.isStringLiteral(node.arguments[1])) {
|
|
351
|
-
routes.push({
|
|
352
|
-
pattern: reactRouterPathPattern(node.arguments[0].text),
|
|
353
|
-
file: node.arguments[1].text,
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
tsForEachChild(node, visit);
|
|
358
|
-
};
|
|
359
|
-
visit(parsed);
|
|
360
|
-
return routes;
|
|
361
|
-
}
|
|
362
|
-
function reactRouterPathPattern(pattern) {
|
|
363
|
-
const normalized = pattern.startsWith("/") ? pattern : `/${pattern}`;
|
|
364
|
-
return normalized.replace(/\$([A-Za-z0-9_]+)/g, ":$1").replace(/\*$/, "*");
|
|
365
|
-
}
|
|
366
439
|
function sourceHashes(sources) {
|
|
367
440
|
return Object.fromEntries(sources.map((source) => [source.path, sha256(source.text)]));
|
|
368
441
|
}
|
|
@@ -434,10 +507,26 @@ async function readOverlaySpec(model, overlayPath) {
|
|
|
434
507
|
async function loadModalityConfig(configPath) {
|
|
435
508
|
if (!configPath)
|
|
436
509
|
return {};
|
|
437
|
-
const module = (await
|
|
510
|
+
const module = (await importConfigModule(configPath));
|
|
438
511
|
const exported = module.default ?? module.config ?? {};
|
|
439
512
|
return typeof exported === "function" ? await exported() : exported;
|
|
440
513
|
}
|
|
514
|
+
async function importConfigModule(configPath) {
|
|
515
|
+
if (extname(configPath) === ".ts" || extname(configPath) === ".mts") {
|
|
516
|
+
const source = await readFile(configPath, "utf8");
|
|
517
|
+
const transpiled = ts.transpileModule(source, {
|
|
518
|
+
compilerOptions: {
|
|
519
|
+
module: ts.ModuleKind.ESNext,
|
|
520
|
+
target: ts.ScriptTarget.ES2022,
|
|
521
|
+
importsNotUsedAsValues: ts.ImportsNotUsedAsValues.Remove,
|
|
522
|
+
},
|
|
523
|
+
}).outputText;
|
|
524
|
+
const encoded = Buffer.from(transpiled).toString("base64");
|
|
525
|
+
const url = `data:text/javascript;base64,${encoded}`;
|
|
526
|
+
return import(url);
|
|
527
|
+
}
|
|
528
|
+
return import(`${pathToFileURL(configPath).href}?t=${Date.now()}`);
|
|
529
|
+
}
|
|
441
530
|
async function findNearestConfig(startDir) {
|
|
442
531
|
const names = [
|
|
443
532
|
"modality.config.ts",
|
|
@@ -467,6 +556,29 @@ async function findNearestConfig(startDir) {
|
|
|
467
556
|
function uniqueStrings(values) {
|
|
468
557
|
return [...new Set(values)].sort();
|
|
469
558
|
}
|
|
559
|
+
function commonAncestor(paths) {
|
|
560
|
+
if (paths.length === 0)
|
|
561
|
+
return process.cwd();
|
|
562
|
+
const [first, ...rest] = paths.map((path) => resolve(path).split(/[\\/]+/));
|
|
563
|
+
if (!first)
|
|
564
|
+
return process.cwd();
|
|
565
|
+
let length = first.length;
|
|
566
|
+
for (const parts of rest) {
|
|
567
|
+
length = Math.min(length, parts.length);
|
|
568
|
+
for (let index = 0; index < length; index += 1) {
|
|
569
|
+
if (first[index] !== parts[index]) {
|
|
570
|
+
length = index;
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
const prefix = first.slice(0, length).join("/");
|
|
576
|
+
if (prefix === "") {
|
|
577
|
+
const firstPath = paths[0];
|
|
578
|
+
return firstPath ? parse(firstPath).root : process.cwd();
|
|
579
|
+
}
|
|
580
|
+
return prefix;
|
|
581
|
+
}
|
|
470
582
|
async function findNearestPackageJson(startDir) {
|
|
471
583
|
let dir = startDir;
|
|
472
584
|
while (true) {
|
|
@@ -506,21 +618,50 @@ async function assertMatchesExpectedModel(model, expectedModelPath) {
|
|
|
506
618
|
throw new Error(`Extracted model differs from expected snapshot ${expectedModelPath}`);
|
|
507
619
|
}
|
|
508
620
|
}
|
|
509
|
-
function
|
|
621
|
+
function round2(n) {
|
|
622
|
+
return Math.round(n * 100) / 100;
|
|
623
|
+
}
|
|
624
|
+
function buildStateContributors(model, limit = 20) {
|
|
625
|
+
const contributors = model.vars.map((decl) => {
|
|
626
|
+
const cardinality = domainCardinality(decl.domain);
|
|
627
|
+
const bits = cardinality < 1 ? 0 : round2(Math.log2(cardinality));
|
|
628
|
+
const scope = decl.scope.kind === "global" ? "global" : decl.scope.route;
|
|
629
|
+
const origin = typeof decl.origin === "string" ? decl.origin : decl.origin.file;
|
|
630
|
+
return {
|
|
631
|
+
varId: decl.id,
|
|
632
|
+
domainKind: decl.domain.kind,
|
|
633
|
+
bits,
|
|
634
|
+
scope,
|
|
635
|
+
origin,
|
|
636
|
+
};
|
|
637
|
+
});
|
|
638
|
+
const totalBits = round2(contributors.reduce((sum, c) => sum + c.bits, 0));
|
|
639
|
+
const topVars = [...contributors]
|
|
640
|
+
.sort((a, b) => b.bits - a.bits || a.varId.localeCompare(b.varId))
|
|
641
|
+
.slice(0, limit);
|
|
642
|
+
const bySourceMap = new Map();
|
|
643
|
+
for (const c of contributors) {
|
|
644
|
+
bySourceMap.set(c.origin, round2((bySourceMap.get(c.origin) ?? 0) + c.bits));
|
|
645
|
+
}
|
|
646
|
+
const bySource = [...bySourceMap.entries()]
|
|
647
|
+
.map(([source, bits]) => ({ source, bits }))
|
|
648
|
+
.sort((a, b) => b.bits - a.bits || a.source.localeCompare(b.source));
|
|
649
|
+
return { totalBits, topVars, bySource };
|
|
650
|
+
}
|
|
651
|
+
function createExtractionReport(sourceFiles, model, warnings, ignoredVars, now, inventory) {
|
|
510
652
|
const caveats = model.metadata?.extractionCaveats ?? emptyExtractionCaveats();
|
|
653
|
+
const varDomains = new Map(model.vars.map((decl) => [decl.id, decl.domain]));
|
|
511
654
|
const transitionHandlers = model.transitions.map((transition) => ({
|
|
512
655
|
id: transition.id,
|
|
513
656
|
classification: transition.confidence === "manual"
|
|
514
657
|
? "overlay"
|
|
515
658
|
: transition.confidence,
|
|
516
659
|
reasons: transition.confidence === "over-approx"
|
|
517
|
-
? overApproxReasons(transition)
|
|
660
|
+
? overApproxReasons(transition, varDomains)
|
|
518
661
|
: [],
|
|
519
662
|
}));
|
|
520
663
|
const transitionIds = new Set(transitionHandlers.map((handler) => handler.id));
|
|
521
|
-
const unextractableHandlers = warnings
|
|
522
|
-
.map(unextractableHandlerFromWarning)
|
|
523
|
-
.filter((handler) => Boolean(handler))
|
|
664
|
+
const unextractableHandlers = dedupeUnextractableHandlers(warnings)
|
|
524
665
|
.filter((handler) => !transitionIds.has(handler.id))
|
|
525
666
|
.map((handler) => ({
|
|
526
667
|
id: handler.id,
|
|
@@ -531,6 +672,14 @@ function createExtractionReport(sourceFiles, model, warnings, ignoredVars, now)
|
|
|
531
672
|
const exactOrOverlay = handlers.filter((handler) => handler.classification === "exact" ||
|
|
532
673
|
handler.classification === "overlay").length;
|
|
533
674
|
const unextractable = handlers.filter((handler) => handler.classification === "unextractable").length;
|
|
675
|
+
const coarseDomains = model.vars
|
|
676
|
+
.map((decl) => ({
|
|
677
|
+
varId: decl.id,
|
|
678
|
+
paths: collectTokenDomainPaths(decl.domain),
|
|
679
|
+
}))
|
|
680
|
+
.filter((entry) => entry.paths.length > 0)
|
|
681
|
+
.sort((a, b) => a.varId.localeCompare(b.varId));
|
|
682
|
+
const routeCoverage = buildRouteCoverage(inventory, model);
|
|
534
683
|
return {
|
|
535
684
|
schemaVersion: 1,
|
|
536
685
|
kind: "extraction-report",
|
|
@@ -553,6 +702,9 @@ function createExtractionReport(sourceFiles, model, warnings, ignoredVars, now)
|
|
|
553
702
|
? "default-token"
|
|
554
703
|
: "type-derived"),
|
|
555
704
|
})),
|
|
705
|
+
...(coarseDomains.length > 0 ? { coarseDomains } : {}),
|
|
706
|
+
stateContributors: buildStateContributors(model),
|
|
707
|
+
...(routeCoverage ? { routeCoverage } : {}),
|
|
556
708
|
coverage: {
|
|
557
709
|
handlersTotal: handlers.length,
|
|
558
710
|
exactOrOverlay,
|
|
@@ -563,6 +715,106 @@ function createExtractionReport(sourceFiles, model, warnings, ignoredVars, now)
|
|
|
563
715
|
warnings,
|
|
564
716
|
};
|
|
565
717
|
}
|
|
718
|
+
function buildRouteCoverage(inventory, model) {
|
|
719
|
+
if (!inventory || inventory.routes.length === 0)
|
|
720
|
+
return undefined;
|
|
721
|
+
const routeVar = model.vars.find((decl) => decl.id === "sys:route");
|
|
722
|
+
const modeledValues = new Set(routeVar?.domain.kind === "enum" ? routeVar.domain.values : []);
|
|
723
|
+
const routes = inventory.routes
|
|
724
|
+
.map((node) => {
|
|
725
|
+
const modeled = modeledValues.has(node.pattern);
|
|
726
|
+
if (modeled)
|
|
727
|
+
return { pattern: node.pattern, modeled: true };
|
|
728
|
+
let classification;
|
|
729
|
+
let reason;
|
|
730
|
+
if (node.kind === "resource") {
|
|
731
|
+
classification = "api";
|
|
732
|
+
reason = "API/resource route excluded from client state";
|
|
733
|
+
}
|
|
734
|
+
else if (node.redirectTo) {
|
|
735
|
+
classification = "redirect-only";
|
|
736
|
+
reason = "Redirect-only route excluded from client state";
|
|
737
|
+
}
|
|
738
|
+
else if (node.pattern.includes("*")) {
|
|
739
|
+
classification = "unsupported";
|
|
740
|
+
reason = "Splat/wildcard route pattern not modeled";
|
|
741
|
+
}
|
|
742
|
+
else {
|
|
743
|
+
classification = "no-client-state";
|
|
744
|
+
reason = "No client-side state modeled for this route";
|
|
745
|
+
}
|
|
746
|
+
return { pattern: node.pattern, modeled: false, classification, reason };
|
|
747
|
+
})
|
|
748
|
+
.sort((left, right) => left.pattern.localeCompare(right.pattern));
|
|
749
|
+
const modeled = routes.filter((entry) => entry.modeled).length;
|
|
750
|
+
return { configured: inventory.routes.length, modeled, routes };
|
|
751
|
+
}
|
|
752
|
+
function formatRouteCoverageLine(coverage) {
|
|
753
|
+
const omitted = coverage.configured - coverage.modeled;
|
|
754
|
+
const counts = new Map();
|
|
755
|
+
for (const entry of coverage.routes) {
|
|
756
|
+
if (entry.modeled || !entry.classification)
|
|
757
|
+
continue;
|
|
758
|
+
counts.set(entry.classification, (counts.get(entry.classification) ?? 0) + 1);
|
|
759
|
+
}
|
|
760
|
+
const parts = [...counts.entries()]
|
|
761
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
762
|
+
.map(([classification, count]) => `${classification}=${count}`);
|
|
763
|
+
const suffix = parts.length > 0 ? ` [${parts.join(",")}]` : "";
|
|
764
|
+
return `routes configured=${coverage.configured} modeled=${coverage.modeled} omitted=${omitted}${suffix}`;
|
|
765
|
+
}
|
|
766
|
+
function buildLocationLowering(transitions, adapter, inventory) {
|
|
767
|
+
const pushTargets = new Set();
|
|
768
|
+
const pushOrigins = new Set();
|
|
769
|
+
let hasUnboundPush = false;
|
|
770
|
+
for (const transition of transitions) {
|
|
771
|
+
if (transition.id.startsWith("route:"))
|
|
772
|
+
continue;
|
|
773
|
+
const navigations = collectPushReplaceNavigations(transition.effect);
|
|
774
|
+
if (navigations.length === 0)
|
|
775
|
+
continue;
|
|
776
|
+
const component = transition.id.split(".")[0] ?? "";
|
|
777
|
+
const origin = adapter.routeForComponent?.(component, inventory);
|
|
778
|
+
for (const navigation of navigations) {
|
|
779
|
+
if (navigation.to)
|
|
780
|
+
pushTargets.add(navigation.to);
|
|
781
|
+
if (!origin)
|
|
782
|
+
hasUnboundPush = true;
|
|
783
|
+
else
|
|
784
|
+
pushOrigins.add(origin);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return {
|
|
788
|
+
pushTargets: [...pushTargets].sort(),
|
|
789
|
+
pushOrigins: [...pushOrigins].sort(),
|
|
790
|
+
hasUnboundPush,
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
function collectPushReplaceNavigations(effect) {
|
|
794
|
+
const navigations = [];
|
|
795
|
+
const visit = (current) => {
|
|
796
|
+
if (current.kind === "navigate" &&
|
|
797
|
+
(current.mode === "push" || current.mode === "replace")) {
|
|
798
|
+
const to = current.to?.kind === "lit" && typeof current.to.value === "string"
|
|
799
|
+
? current.to.value
|
|
800
|
+
: undefined;
|
|
801
|
+
navigations.push({
|
|
802
|
+
mode: current.mode,
|
|
803
|
+
...(to !== undefined ? { to } : {}),
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
if (current.kind === "seq") {
|
|
807
|
+
for (const child of current.effects)
|
|
808
|
+
visit(child);
|
|
809
|
+
}
|
|
810
|
+
if (current.kind === "if") {
|
|
811
|
+
visit(current.then);
|
|
812
|
+
visit(current.else);
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
visit(effect);
|
|
816
|
+
return navigations;
|
|
817
|
+
}
|
|
566
818
|
function emptyExtractionCaveats() {
|
|
567
819
|
return {
|
|
568
820
|
globalTaints: [],
|
|
@@ -585,10 +837,7 @@ function createExtractionCaveats(warnings) {
|
|
|
585
837
|
.map(unhandledRejectionFromWarning)
|
|
586
838
|
.filter(isCaveat)
|
|
587
839
|
.sort(compareCaveats),
|
|
588
|
-
unextractableHandlers: warnings
|
|
589
|
-
.map(unextractableHandlerFromWarning)
|
|
590
|
-
.filter(isCaveat)
|
|
591
|
-
.sort(compareCaveats),
|
|
840
|
+
unextractableHandlers: dedupeUnextractableHandlers(warnings),
|
|
592
841
|
};
|
|
593
842
|
}
|
|
594
843
|
function globalTaintFromWarning(warning) {
|
|
@@ -652,32 +901,15 @@ function firstSemverMajor(range) {
|
|
|
652
901
|
function pluginProvenance(plugins) {
|
|
653
902
|
return [...plugins.sources, ...(plugins.router ? [plugins.router] : [])].sort((left, right) => left.kind.localeCompare(right.kind) || left.id.localeCompare(right.id));
|
|
654
903
|
}
|
|
655
|
-
function
|
|
656
|
-
const routes = new Set();
|
|
657
|
-
const visit = (effect) => {
|
|
658
|
-
if (effect.kind === "navigate" &&
|
|
659
|
-
effect.to?.kind === "lit" &&
|
|
660
|
-
typeof effect.to.value === "string")
|
|
661
|
-
routes.add(effect.to.value);
|
|
662
|
-
if (effect.kind === "seq") {
|
|
663
|
-
for (const child of effect.effects)
|
|
664
|
-
visit(child);
|
|
665
|
-
}
|
|
666
|
-
if (effect.kind === "if") {
|
|
667
|
-
visit(effect.then);
|
|
668
|
-
visit(effect.else);
|
|
669
|
-
}
|
|
670
|
-
};
|
|
671
|
-
for (const transition of transitions)
|
|
672
|
-
visit(transition.effect);
|
|
673
|
-
return [...routes].sort();
|
|
674
|
-
}
|
|
675
|
-
function overApproxReasons(transition) {
|
|
904
|
+
function overApproxReasons(transition, varDomains = new Map()) {
|
|
676
905
|
const reasons = new Set();
|
|
677
906
|
if (transition.id.endsWith(".escaped"))
|
|
678
907
|
reasons.add("setter escaped to unanalyzed call");
|
|
679
|
-
for (const variable of havocWrites(transition.effect))
|
|
680
|
-
|
|
908
|
+
for (const variable of havocWrites(transition.effect)) {
|
|
909
|
+
const domain = varDomains.get(variable);
|
|
910
|
+
const prefix = domain?.kind === "bool" ? "safe local toggle" : "domain-wide havoc";
|
|
911
|
+
reasons.add(`${prefix}: havoc write to ${variable}`);
|
|
912
|
+
}
|
|
681
913
|
if (reasons.size === 0)
|
|
682
914
|
reasons.add("transition confidence is over-approx");
|
|
683
915
|
return [...reasons].sort();
|
|
@@ -728,17 +960,28 @@ function normalizeId(id) {
|
|
|
728
960
|
function editDistance(left, right) {
|
|
729
961
|
const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
730
962
|
for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
|
|
731
|
-
|
|
963
|
+
const startDiagonal = previous[0];
|
|
964
|
+
if (startDiagonal === undefined)
|
|
965
|
+
break;
|
|
966
|
+
let diagonal = startDiagonal;
|
|
732
967
|
previous[0] = leftIndex;
|
|
733
968
|
for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) {
|
|
734
|
-
const
|
|
735
|
-
const
|
|
969
|
+
const upCell = previous[rightIndex];
|
|
970
|
+
const leftCell = previous[rightIndex - 1];
|
|
971
|
+
if (upCell === undefined || leftCell === undefined)
|
|
972
|
+
break;
|
|
973
|
+
const up = upCell + 1;
|
|
974
|
+
const leftCost = leftCell + 1;
|
|
736
975
|
const subst = diagonal + (left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1);
|
|
737
|
-
|
|
976
|
+
const corner = previous[rightIndex];
|
|
977
|
+
if (corner === undefined)
|
|
978
|
+
break;
|
|
979
|
+
diagonal = corner;
|
|
738
980
|
previous[rightIndex] = Math.min(up, leftCost, subst);
|
|
739
981
|
}
|
|
740
982
|
}
|
|
741
|
-
|
|
983
|
+
const distance = previous[right.length];
|
|
984
|
+
return distance ?? 0;
|
|
742
985
|
}
|
|
743
986
|
function havocWrites(effect) {
|
|
744
987
|
if (effect.kind === "havoc")
|
|
@@ -749,9 +992,47 @@ function havocWrites(effect) {
|
|
|
749
992
|
return [...havocWrites(effect.then), ...havocWrites(effect.else)];
|
|
750
993
|
return [];
|
|
751
994
|
}
|
|
995
|
+
const GENERIC_UNEXTRACTABLE_CATEGORIES = new Set([
|
|
996
|
+
"no-extractable-effect",
|
|
997
|
+
"unextractable",
|
|
998
|
+
]);
|
|
999
|
+
function dedupeUnextractableHandlers(warnings) {
|
|
1000
|
+
const parsed = warnings.map(unextractableHandlerFromWarning).filter((handler) => Boolean(handler));
|
|
1001
|
+
const byId = new Map();
|
|
1002
|
+
for (const handler of parsed) {
|
|
1003
|
+
const existing = byId.get(handler.id);
|
|
1004
|
+
if (!existing) {
|
|
1005
|
+
byId.set(handler.id, handler);
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
const existingIsGeneric = GENERIC_UNEXTRACTABLE_CATEGORIES.has(existing.category);
|
|
1009
|
+
const incomingIsGeneric = GENERIC_UNEXTRACTABLE_CATEGORIES.has(handler.category);
|
|
1010
|
+
if (existingIsGeneric && !incomingIsGeneric)
|
|
1011
|
+
byId.set(handler.id, handler);
|
|
1012
|
+
}
|
|
1013
|
+
return [...byId.values()]
|
|
1014
|
+
.map(({ id, reason, source }) => source ? { id, reason, source } : { id, reason })
|
|
1015
|
+
.sort(compareCaveats);
|
|
1016
|
+
}
|
|
752
1017
|
function unextractableHandlerFromWarning(warning) {
|
|
753
|
-
const
|
|
754
|
-
|
|
1018
|
+
const rich = /^Unextractable handler (\S+) \[([^\]]+)\] \((.+)\)$/.exec(warning);
|
|
1019
|
+
if (rich) {
|
|
1020
|
+
const id = rich[1];
|
|
1021
|
+
const category = rich[2];
|
|
1022
|
+
const source = rich[3];
|
|
1023
|
+
if (!id || !category || !source)
|
|
1024
|
+
return undefined;
|
|
1025
|
+
return {
|
|
1026
|
+
id,
|
|
1027
|
+
category,
|
|
1028
|
+
reason: `${category} at ${source}`,
|
|
1029
|
+
source,
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
const bare = /^Unextractable handler (\S+)$/.exec(warning);
|
|
1033
|
+
return bare?.[1]
|
|
1034
|
+
? { id: bare[1], category: "unextractable", reason: bare[0] }
|
|
1035
|
+
: undefined;
|
|
755
1036
|
}
|
|
756
1037
|
function pendingVars(effectApis, transitions = [], vars = [], maxPending = 3) {
|
|
757
1038
|
const enqueues = transitions.flatMap((transition) => enqueueOps(transition.effect));
|