feat(electron): resolve stable local constant window options

Resolve stable local constant BrowserWindow options with retained provenance and shared early-exit mutation queries. Preserve source ordering and unknown results for relevant mutations or escapes.

Includes real Joplin correctness/cost receipts and regression coverage.

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
This commit is contained in:
YsopwnableandKaoru0822 authored and GitHub committed 2026-10-16 14:52:33 +08:00
1 parent 5a77acfbcd
commit d72d856f58
20 files changed
+4544 -64

No files matched your search

@@ -350,6 +350,17 @@ substitute version-dependent Electron defaults for omitted `webPreferences`.
Unresolved option objects, bridge keys, API objects, and IPC channel expressions
remain unknown and make coverage partial.
BrowserWindow options and `webPreferences` can follow local `const` aliases
whose initialization precedes the use. The shared lexical and mutation index
checks the selected `webPreferences` path, so an unrelated `options.icon` write
does not discard these facts. Reassignment, relevant writes, prior escapes,
ambiguous initialization, and unresolved spreads remain unknown. The binding
association is an inference: the window observation retains the constructor
location and `resolution_sources` identifies the original object literals in
the same artifact. Inline option syntax keeps its observed Evidence. This
does not execute code or establish runtime configuration; popup overrides from
`setWindowOpenHandler` are not covered by this constant-options resolution.
Recognized literal module-table bootstraps also support bounded lexical
substitution through module exports, import helper bodies, local wrapper calls,
returned closures, object spreads, and callback parameters. Recovered Electron
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,72 @@
# Local constant BrowserWindow options — Joplin verification
This record covers #4366, part of #3536 and #4027. The unchanged official Joplin desktop 3.7.21 ASAR contains `const windowOptions = {...}` at `ElectronAppWrapper.js:238–263`, an unrelated conditional `icon` assignment, and `new BrowserWindow(windowOptions)` at line 268. The scoped public workflow analyzes that source plus `package.json`, then reads the window through `inspect_analysis_view`. It does not execute Joplin.
The baseline returns one dynamic window with zero explicit preferences. The candidate retains four literal booleans (`nodeIntegration: true`, `contextIsolation: false`, `spellcheck: true`, `enableRemoteModule: true`) and the explicit dynamic `session` expression. The original window identity and constructor location are preserved. Constant association is inferred, with the original object's source range; omitted preferences do not acquire Electron defaults. Popup overrides and literal spreads remain separate work under #3536.
The [compact measurements](javascript-window-options-2026-10-16.json) retain both trial orders, exact inputs, runtime/harness/source fingerprints, original result rows, CLI/MCP Evidence parity and observer receipts. ASAR SHA256 is `767df409b18effe60421fbefcca873de237c7e813eac744ecdeebbc5e825a33a`; the 39,972-byte source is `dcd2260e46dd1b4922449090e268773b052d58ba24ff977b08b978bee6cc9da1`. Download/recreation provenance is in the [earlier Joplin record](javascript-retained-facts-2026-10-15.md). Downloadable applications and complete exports are not committed.
## Correctness
Both trial pairs match the independent export-and-filter oracle and exact complete CLI/MCP view Evidence. The pinned [Joplin checker](../../experiments/analysis-efficiency/joplin-window-options-check.py) verifies exact settings, explicit unknowns, source ranges and stable window identity. Manual negative controls reject a wrong boolean, numeric0 substituted for false, a changed identity and observed instead of inferred provenance.
A raw semantic graph digest changes because the semantic graph commits its parent `application_graph_id`. The final record separately checks complete semantic content after excluding exactly that foreign key and its dependent top-level `graph_id`; original raw digests remain recorded and linked to the independent complete CLI observations. These exclusions follow the canonical semantic graph schema and builder, rather than filtering differing nodes or relations.
## Costs and limitations
| Trial order | Variant | First answer (s) | CPU (s) | Peak cgroup bytes at answer | Projected task BPE |
| ----------------------- | --------- | ---------------: | ------: | --------------------------: | -----------------: |
| Baseline then candidate | Baseline | 70.283 | 69.258 | 1578196992 | 13337 |
| Baseline then candidate | Candidate | 42.701 | 42.437 | 1523793920 | 13432 |
| Candidate then baseline | Candidate | 44.793 | 44.766 | 1527357440 | 13432 |
| Candidate then baseline | Baseline | 42.958 | 42.946 | 1533308928 | 13337 |
The baseline tool workflow cannot supply these settings; additional manual source-reading costs are unmeasured. This is a correctness gain with +95 projected structured-task tokens (+203 when counting complete tool replies). Both use the same 8,069-token selected catalog. The first apparent latency gain does not repeat: the reversed pair makes the candidate about 1.8 s slower. These samples establish no speedup or general performance result. First view retrievals are 106–121 ms; subsequent within-session reads are 22–35 ms. No persistent analysis cache was added.
All costly jobs run serially with one CPU 10, an 8 GiB hard limit, 6 GiB early stop, no swap and 192 tasks, pinned Node 24.18.0/npm 11.16.0. Cgroup memory includes file cache and is not RSS. At-answer counters precede repeated reads, diagnostic export, full-graph hashing, CLI parity and tokenization. Whole-verifier observer receipts measure that extra work separately. The observer reported no OOM and verified payload cleanup in each scoped run.
Each task starts a fresh MCP process; the input hash warms file cache, and build/OS caches are uncontrolled. Three retrieval samples are not three cold analyses. `cl100k_base`/tiktoken 0.12.0 projections include preparation, first answer and selected input schemas; output schemas, actual billing, reasoning, peak agent context, host rendering and system-cold behavior were not measured. No live-model or Electron runtime claim is made.
## Validation and reproducibility
The integrated focused invocation passed 156/157 cases across 9 files. An unchanged upstream generator CLI/MCP case exceeded its original 30 s deadline in grouped execution; isolated runs at that same deadline passed on both main and candidate (18.590/18.034 s). The grouped failure is preserved; its root cause is not established and its deadline was not changed. Own CLI/MCP/fact-view, source mutation/escape/identity and capacity regressions passed. Static/document checks are recorded in the JSON validation receipts.
An initial real-app verifier attempt failed because its coverage assertion handled only artifact paths. The independent selector and tool already supported source-range paths. The verifier assertion was corrected before both valid pairs; that failed attempt is excluded from performance samples. The source oracle's initially transcribed closing line 264 was corrected to 263 after rereading the unchanged source. Neither correction changes production analysis.
After building the pinned baseline and candidate in separate checkouts, run this same command serially in baseline→candidate and candidate→baseline order, using the matching harness hash recorded for each pair:
```sh
query='{"kind":"javascript-facts","where":{"paths":["ElectronAppWrapper.js"],"kinds":["browser-window"]},"property_keys":["options_status","web_preferences_status","web_preferences","preload_path","resolution_sources"],"offset":0,"limit":64}'
node scripts/verify/javascript/retained-facts.mjs \
--artifact /absolute/path/to/app.asar --mode view --query "$query" \
--source-path ElectronAppWrapper.js --source-path package.json \
--rounds 3 --cli-parity --tokenizer-python /absolute/python-with-tiktoken-0.12.0
python3 experiments/analysis-efficiency/joplin-window-options-check.py \
docs/verification/javascript-window-options-2026-10-16.json
```
Apply the linked resource observer from the earlier Joplin record to each costly command. Complete CLI observations use the same source selection through `analyze-javascript-application --format json`. The preserved [content comparator](../../experiments/analysis-efficiency/compare-javascript-semantic-content.mjs) takes the baseline CLI JSON, candidate CLI JSON and this measurement record, then emits the semantic proof while retaining both original graph identifiers and raw digests. It reads sequentially and reports a concise failure rather than printing full graphs. The baseline backend is unchanged main `2c7583599b9ac1c847bd77b1940930fd8d4594c7`; only the matching verification harness is applied there. Source and runtime pins distinguish measurement versions from later documentation changes.
## Ordering review follow-up
Codex review identified an introduced producer-ordering regression: inline windows were appended during traversal while deferred constants were appended afterward. The new mixed constant/inline/unresolved regression reproduced `[4, 3, 5, 6]` instead of source order `[3, 4, 5, 6]`. The producing collection now restores order from retained source offsets before module attribution and public graph consumers; no downstream adapter repair was added. A reused options object remains dynamic after it escapes to the first constructor.
After the fix, all 49 targeted cases across five files passed, including the public CLI/MCP/fact-view workflow and capacity/identity cases. One additional real Joplin run at source revision `3fd71cba2` returned exactly the same complete answer and semantic fingerprint as the original candidate, with complete CLI/MCP view Evidence parity. It took 44.028 s at answer / 43.960 CPU s / 1,524,555,776 peak cgroup bytes, with the same 13,432 projected task tokens. This is a follow-up validation, not an additional paired latency comparison. The JSON stores its runtime and current source pins separately; the original trial receipts and historical source pins remain unchanged.
## Shared query performance review follow-up
The stored-alias example raised in review already returns unknown on the reviewed production code: the exact negative source case passes, and the public CLI returns one dynamic window with zero explicit preferences. The shared `reference-store` producer records the assignment escape before construction. No mutation rule was changed; the negative regression is retained.
The repeated-effects cost was reproducible. Candidate discovery needed only lexical identity but collected full mutation reports; each later capture also materialized every effect before checking for a prior escape. The shared index now offers lexical-only lookup and an early-exit predicate. Windows and request analysis use those operations. The same owner still supplies complete effects to the require-source consumer, which needs filtered effect kinds; no second interpretation or persistent cache was added.
The [preserved scaling control](../../experiments/analysis-efficiency/window-option-scaling.mjs) measures real production source analysis on synthetic capacity inputs; it is not a real-application or agent task. All samples and module hashes are retained. CPU medians across three sequential samples were:
| Shared-options constructors | Before (CPU s) | After (CPU s) |
| --------------------------- | -------------: | ------------: |
| 256 | 0.29703 | 0.17821 |
| 512 | 0.96105 | 0.08709 |
| 1024 | 4.41298 | 0.20032 |
The first 256 sample is the first algorithm execution in each process; later rounds and larger sizes can use warmed V8 code. These medians mix that progression and do not establish cold-task scaling across applications. A deterministic regression separately bounds escaped-effect reads for 128 captures to at most 4N while requiring unindexed unknowns, a valid first capture and unknown reused contents. Timing is not a test gate. Reproduce the capacity control, after building each pinned checkout, with `node experiments/analysis-efficiency/window-option-scaling.mjs LABEL 256,512,1024` under the same resource observer.
After this refactor, 107 cases across 10 focused files passed, including request CLI/MCP, require-source, worker-schema, identity, capacity and retained-fact workflows. A new real Joplin run at `9207ece2090208c272d2689c4002438ceda41145` preserves the exact whole answer and semantic fingerprint, including CLI/MCP parity and the same 13,432 projected task tokens. It measured 44.607s / 43.883 CPU s / 1,666,560,000 peak cgroup bytes. Thus the capacity improvement is clear for the tested pattern, while no general real-app speedup is claimed. The new stage stores current source/runtime pins and validation separately; all previous receipts remain unchanged.
@@ -0,0 +1,52 @@
#!/usr/bin/env node
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import { isDeepStrictEqual } from "node:util";
import canonicalize from "canonicalize";
const [baselinePath, candidatePath, recordPath] = process.argv.slice(2);
assert.ok(baselinePath && candidatePath && recordPath);
const record = JSON.parse(await readFile(recordPath, "utf8"));
const fingerprinted = record.trials.at(-1);
const digest = (value) =>
createHash("sha256").update(canonicalize(value)).digest("hex");
const readSemantic = async (path, label) => {
const evidence = JSON.parse(await readFile(path, "utf8"));
const analysis = evidence.normalized_result;
assert.equal(analysis.root_artifact_sha256, record.input.asar_sha256);
const semantic = analysis.semantic_graph;
assert.equal(semantic.application_graph_id, analysis.graph.graph_id);
const rawSha = digest(semantic);
assert.equal(
rawSha,
fingerprinted[label].profile.analysis_fingerprints.semantic_graph_sha256,
);
const { application_graph_id, graph_id, ...content } = semantic;
return { content, rawSha, application_graph_id, graph_id };
};
// Read sequentially; retain semantic content rather than complete parent graphs.
const before = await readSemantic(baselinePath, "baseline");
const after = await readSemantic(candidatePath, "candidate");
assert.ok(
isDeepStrictEqual(before.content, after.content),
"Semantic content differs after excluding only the two parent commitments",
);
console.log(
JSON.stringify({
excluded_top_level_fields: ["application_graph_id", "graph_id"],
content_deep_equal: true,
baseline_raw_sha256: before.rawSha,
candidate_raw_sha256: after.rawSha,
baseline_content_sha256: digest(before.content),
candidate_content_sha256: digest(after.content),
baseline_application_graph_id: before.application_graph_id,
candidate_application_graph_id: after.application_graph_id,
baseline_graph_id: before.graph_id,
candidate_graph_id: after.graph_id,
compared_fields: Object.keys(before.content).sort(),
serialization:
"RFC 8785 canonical JSON; arrays and all remaining fields retained",
}),
);
@@ -0,0 +1,106 @@
#!/usr/bin/env python3
"""Check pinned Joplin static evidence from retained-facts.mjs reports.
The oracle comes from the unchanged published ElectronAppWrapper.js, not from
the production resolver. It establishes static reconstruction, not execution.
"""
import argparse
import json
def check(baseline, candidate):
assert baseline["query"] == candidate["query"]
archive_sha = "767df409b18effe60421fbefcca873de237c7e813eac744ecdeebbc5e825a33a"
source_sha = "dcd2260e46dd1b4922449090e268773b052d58ba24ff977b08b978bee6cc9da1"
for report in (baseline, candidate):
assert report["artifact_sha256"] == archive_sha
assert report["source_paths"] == ["ElectronAppWrapper.js", "package.json"]
assert report["mode"] == "view"
assert report["cli_parity"]["exact_evidence_parity"] is True
assert report["answer"]["total"] == 1
assert len(report["answer"]["rows"]) == 1
before = baseline["answer"]["rows"][0]
after = candidate["answer"]["rows"][0]
assert before["node_id"] == after["node_id"]
assert before["identity"] == after["identity"] == {
"strategy": "artifact-local-key",
"stability": "artifact-version",
"artifact_sha256": source_sha,
"namespace": "electron-browser-window",
"key": "268:20-268:63",
}
assert before["properties"] == {
"options_status": "dynamic",
"web_preferences_status": "dynamic",
"web_preferences": [],
"preload_path": None,
}
properties = after["properties"]
assert properties["options_status"] == "object-literal"
assert properties["web_preferences_status"] == "object-literal"
assert properties["preload_path"] is None
assert properties["resolution_sources"] == [{
"source": "ElectronAppWrapper.js",
"start": {"line": 238, "column": 30},
"end": {"line": 263, "column": 9},
}]
preferences = {item["name"]: item["value"] for item in properties["web_preferences"]}
assert len(preferences) == len(properties["web_preferences"]) == 5
expected = {
"nodeIntegration": True,
"contextIsolation": False,
"spellcheck": True,
"enableRemoteModule": True,
}
for name, value in expected.items():
assert preferences[name] == {"status": "literal", "value": value, "expression": None}
assert preferences[name]["value"] is value
assert preferences["session"] == {
"status": "dynamic", "value": None, "expression": "this.joplinSession_",
}
assert before["evidence"]["state"] == "observed"
assert after["evidence"]["state"] == "inferred"
assert after["evidence"]["authority"] == "static-relationship-inference"
assert before["evidence"]["location"] == after["evidence"]["location"]
if "analysis_fingerprints" in baseline or "analysis_fingerprints" in candidate:
before_analysis = baseline["analysis_fingerprints"]
after_analysis = candidate["analysis_fingerprints"]
assert before_analysis["summary"]["explicit_web_preferences"] == 0
assert after_analysis["summary"]["explicit_web_preferences"] == 5
assert before_analysis["summary"] == {
**after_analysis["summary"], "explicit_web_preferences": 0,
}
return {"static_oracle": "passed", "stable_window_identity": True,
"explicit_preferences_before": 0, "explicit_preferences_after": 5,
"runtime_configuration_verified": False}
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("record", help="Combined baseline/candidate verification JSON")
arguments = parser.parse_args()
with open(arguments.record, encoding="utf-8") as record_file:
record = json.load(record_file)
assert record["trials"]
receipts = [check(trial["baseline"]["profile"], trial["candidate"]["profile"])
for trial in record["trials"]]
assert any("analysis_fingerprints" in trial["baseline"]["profile"]
and "analysis_fingerprints" in trial["candidate"]["profile"]
for trial in record["trials"])
proof = record["semantic_content_comparison"]
assert proof["excluded_top_level_fields"] == ["application_graph_id", "graph_id"]
assert proof["content_deep_equal"] is True
assert proof["baseline_content_sha256"] == proof["candidate_content_sha256"]
fingerprinted = record["trials"][-1]
for label in ("baseline", "candidate"):
assert proof[label + "_raw_sha256"] == fingerprinted[label]["profile"]["analysis_fingerprints"]["semantic_graph_sha256"]
for stage in ("ordering_review_followup", "effect_query_review_followup"):
followup = record.get(stage)
if followup is not None:
current = followup["profile"]
assert current["answer"] == record["trials"][0]["candidate"]["profile"]["answer"]
assert current["analysis_fingerprints"] == fingerprinted["candidate"]["profile"]["analysis_fingerprints"]
assert current["cli_parity"]["exact_evidence_parity"] is True
print(json.dumps({"trials": len(receipts), "semantic_content_unchanged": True,
"checks": receipts}, sort_keys=True))
@@ -0,0 +1,68 @@
#!/usr/bin/env node
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import { performance } from "node:perf_hooks";
import { analyzeJavaScriptStaticSource } from "../../dist/domain/javascript/javascriptStaticAnalysis.js";
const label = process.argv[2] ?? "unlabelled";
const counts = (process.argv[3] ?? "256,512,1024").split(",").map(Number);
assert.ok(counts.every((count) => Number.isSafeInteger(count) && count > 0));
const rows = [];
for (const count of counts) {
const source = [
'const {BrowserWindow}=require("electron");',
"const options={webPreferences:{sandbox:true}};",
...Array.from({ length: count }, () => "new BrowserWindow(options);"),
].join("\n");
const samples = [];
for (let round = 0; round < 3; round++) {
const cpu = process.cpuUsage();
const began = performance.now();
const result = analyzeJavaScriptStaticSource(source);
const cpuUsed = process.cpuUsage(cpu);
const elapsedMs = performance.now() - began;
const windows = result.electron.browser_windows;
assert.equal(windows.length, count);
assert.deepEqual(
windows.map(({ location }) => location.start.line),
Array.from({ length: count }, (_, index) => index + 3),
);
assert.equal(windows[0].options_status, "object-literal");
assert.ok(
windows
.slice(1)
.every(({ options_status }) => options_status === "dynamic"),
);
samples.push({
round,
elapsed_ms: elapsedMs,
cpu_seconds: (cpuUsed.user + cpuUsed.system) / 1e6,
});
}
rows.push({ count, source_bytes: Buffer.byteLength(source), samples });
}
const modules = [
"javascriptStaticAnalysis",
"electronStaticOptionObjects",
"javascriptSemanticAnalysis",
"javascriptSemanticMemberMutations",
];
const moduleHashes = {};
for (const name of modules)
moduleHashes[name] = createHash("sha256")
.update(
await readFile(
new URL(`../../dist/domain/javascript/${name}.js`, import.meta.url),
),
)
.digest("hex");
console.log(
JSON.stringify({
label,
rows,
module_sha256: moduleHashes,
conditions:
"Synthetic capacity/scaling control, not a real-app or agent task. Three sequential source analyses per size in one process; V8 and module caches may warm, no retained analysis cache. Timing assertions are not test gates. Run under the documented resource observer; real Joplin tasks are measured separately.",
}),
);
+8 -1
View File
@@ -258,7 +258,10 @@ try {
evidence.location.available &&
evidence.location.value.kind === "artifact-path"
? evidence.location.value.path
: null,
: evidence.location.available &&
evidence.location.value.kind === "source-range"
? evidence.location.value.source
: null,
),
);
for (const path of query.where?.paths ?? [])
@@ -333,6 +336,10 @@ try {
answer_sha256: digest(first),
export_ms: exportMs,
cli_parity: cliParity,
analysis_fingerprints: {
semantic_graph_sha256: digest(parent.normalized_result.semantic_graph),
summary: parent.normalized_result.summary,
},
token_costs: tokenCosts,
conditions:
"Fresh MCP and input hashing (file cache warmed); build/OS caches uncontrolled. First export sample includes export plus caller projection code, later samples reuse that immutable export. View samples precede diagnostic export. Task token projections include preparation, first answer, selected/full name/description/inputSchema catalog and the exact baseline program; exec tool schema, host rendering, output schemas, reasoning and billing unmeasured. At-answer cgroup counters include observer/startup overhead, before repeats/export diagnostics/parity/tokenization; final external observer measures all validation work. No application or native provider executes.",
@@ -98,13 +98,28 @@ const addBrowserWindow = (
web_preferences: value.web_preferences,
preload_path: value.preload_path,
preload_resolution_context: value.preload_resolution_context,
...(value.resolution_sources === undefined
? {}
: {
resolution_sources: value.resolution_sources.map(
(location) => ({
source: file.path,
...location,
}),
),
}),
absence_means_default: false,
},
evidence: astObservationEvidence({
evidence: (value.resolution_sources === undefined
? astObservationEvidence
: staticInferenceEvidence)({
sha256: file.sha256,
path: file.path,
range: value.location,
operation: "observe-browser-window-construction",
operation:
value.resolution_sources === undefined
? "observe-browser-window-construction"
: "resolve-browser-window-constant-options",
coverage,
limitations: [
"Only explicitly present webPreferences are reported; omitted properties are not interpreted as Electron defaults.",
@@ -30,40 +30,123 @@ import {
unwrapJavaScriptExpression,
} from "./javascriptAstValues.js";
import type { JavaScriptFindingContext } from "./javascriptStaticAnalysisState.js";
import type { JavaScriptBindingIndex } from "./javascriptSemanticState.js";
import {
hasElectronOptionObjectCandidate,
resolveElectronOptionObject,
} from "./electronStaticOptionObjects.js";
/** Inspect BrowserWindow, contextBridge, and utility-process syntax. */
export const inspectElectronBrowserNode = (
node: t.Node,
context: JavaScriptFindingContext,
): void => {
if (t.isNewExpression(node)) inspectBrowserWindow(node, context);
if (t.isNewExpression(node)) {
const path = electronCalleePath(node.callee, context);
if (path?.length === 1 && path[0] === "BrowserWindow") {
if (needsConstantOptions(node))
context.accumulator.pendingBrowserWindows.push(node);
else inspectBrowserWindow(node, context);
}
}
if (!t.isCallExpression(node) && !t.isOptionalCallExpression(node)) return;
inspectContextBridge(node, context);
inspectUtilityProcess(node, context);
};
const needsConstantOptions = (node: t.NewExpression): boolean => {
const argument = argumentNode(node.arguments[0]);
const options = argument && unwrapJavaScriptExpression(argument).node;
if (t.isIdentifier(options)) return true;
const lookup = objectProperty(options, "webPreferences");
return (
lookup.status === "explicit" &&
t.isIdentifier(unwrapJavaScriptExpression(lookup.property.value).node)
);
};
/** Batch deferred options after the shared mutation index can yield safely. */
export function* finishElectronBrowserWindowsSteps(
context: JavaScriptFindingContext,
index: JavaScriptBindingIndex,
): Generator<void, void> {
const pending = context.accumulator.pendingBrowserWindows;
if (pending.length === 0) return;
if (
pending.some((node) => {
const argument = argumentNode(node.arguments[0]);
const options = argument && unwrapJavaScriptExpression(argument).node;
if (t.isIdentifier(options))
return hasElectronOptionObjectCandidate(options, index);
const lookup = objectProperty(options, "webPreferences");
return (
lookup.status === "explicit" &&
hasElectronOptionObjectCandidate(lookup.property.value, index)
);
})
)
yield* index.indexMemberMutationFactsSteps();
for (const node of pending) {
inspectBrowserWindow(node, context, index);
yield;
}
// Inline findings arrive during traversal; deferred findings join that same
// source-ordered collection before module attribution and public projection.
context.accumulator.browserWindows.sort(
(left, right) => left.offset - right.offset,
);
pending.length = 0;
}
const inspectBrowserWindow = (
node: t.NewExpression,
context: JavaScriptFindingContext,
index?: JavaScriptBindingIndex,
): void => {
const path = electronCalleePath(node.callee, context);
if (path?.length !== 1 || path[0] !== "BrowserWindow") return;
const argument = argumentNode(node.arguments[0]);
const options =
argument === undefined
? undefined
: unwrapJavaScriptExpression(argument).node;
const collected = collectWindowOptions(context.source, options);
const resolvedOptions =
(index &&
resolveElectronOptionObject(options, node, index, ["webPreferences"])) ??
options;
const lookup = objectProperty(resolvedOptions, "webPreferences");
const declaredPreferences =
lookup.status === "explicit"
? unwrapJavaScriptExpression(lookup.property.value).node
: undefined;
const resolvedPreferences =
(index && resolveElectronOptionObject(declaredPreferences, node, index)) ??
declaredPreferences;
const resolutionSources = [
...(resolvedOptions !== options && resolvedOptions !== undefined
? [range(resolvedOptions)]
: []),
...(resolvedPreferences !== declaredPreferences &&
resolvedPreferences !== undefined
? [range(resolvedPreferences)]
: []),
];
const collected = collectWindowOptions(
context.source,
lookup.status,
resolvedPreferences,
);
context.accumulator.unknownFindings += collected.unknown;
const finding: ElectronBrowserWindowFinding = {
options_status:
options === undefined
resolvedOptions === undefined
? "missing"
: t.isObjectExpression(options)
: t.isObjectExpression(resolvedOptions)
? "object-literal"
: "dynamic",
web_preferences_status: collected.status,
web_preferences: collected.preferences,
...(resolutionSources.length > 0
? { resolution_sources: resolutionSources }
: {}),
...collected.preload,
module_key: null,
location: range(node),
@@ -78,24 +161,20 @@ const inspectBrowserWindow = (
const collectWindowOptions = (
source: string,
options: t.Node | undefined,
lookupStatus: "explicit" | "missing" | "dynamic",
preferences: t.Node | undefined,
): {
readonly status: ElectronBrowserWindowFinding["web_preferences_status"];
readonly preferences: readonly ElectronWebPreference[];
readonly preload: ElectronBrowserWindowPreload;
readonly unknown: number;
} => {
const lookup = objectProperty(options, "webPreferences");
const preferences =
lookup.status === "explicit"
? unwrapJavaScriptExpression(lookup.property.value).node
: undefined;
if (lookup.status !== "explicit" || !t.isObjectExpression(preferences))
if (lookupStatus !== "explicit" || !t.isObjectExpression(preferences))
return {
status: lookup.status === "missing" ? "missing" : "dynamic",
status: lookupStatus === "missing" ? "missing" : "dynamic",
preferences: [],
preload: { preload_path: null, preload_resolution_context: null },
unknown: lookup.status === "missing" ? 0 : 1,
unknown: lookupStatus === "missing" ? 0 : 1,
};
return collectWebPreferences(source, preferences);
};
@@ -2,6 +2,128 @@ import { describe, expect, it } from "vitest";
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
describe("Electron constant option evidence", () => {
it("keeps mixed constant, inline and unresolved windows in construction order", () => {
const analysis = analyzeJavaScriptStaticSource(
[
'const { BrowserWindow } = require("electron");',
"const options = { webPreferences: { sandbox: true } };",
"new BrowserWindow(options);",
"new BrowserWindow({ webPreferences: { sandbox: false } });",
"new BrowserWindow(dynamic);",
"new BrowserWindow(options);",
].join("\n"),
);
expect(
analysis.electron.browser_windows.map(
({ location }) => location.start.line,
),
).toEqual([3, 4, 5, 6]);
expect(
analysis.electron.browser_windows.map(
({ options_status }) => options_status,
),
).toEqual([
"object-literal",
"object-literal",
"dynamic",
// The first constructor already received this object; later contents
// remain unknown even though the declarations retain their source order.
"dynamic",
]);
});
it("resolves local constant options despite an unrelated option write", () => {
const analysis = analyzeJavaScriptStaticSource(`
const { BrowserWindow } = require("electron");
function open() {
const preferences = { nodeIntegration: true, contextIsolation: false };
const options = { webPreferences: preferences };
if (linux) options.icon = "icon.png";
new BrowserWindow(options);
}
`);
expect(analysis.electron.browser_windows).toMatchObject([
{
options_status: "object-literal",
web_preferences_status: "object-literal",
web_preferences: [
{
name: "contextIsolation",
value: { status: "literal", value: false },
},
{
name: "nodeIntegration",
value: { status: "literal", value: true },
},
],
resolution_sources: [expect.any(Object), expect.any(Object)],
},
]);
});
it.each([
"options.webPreferences.sandbox = false;",
"const alias = options.webPreferences; alias.sandbox = false;",
"mutate(options);",
"const { webPreferences: alias } = options; mutate(alias);",
"Object.assign(options, unknown);",
"const box = {}; box.options = options; box.options.webPreferences.sandbox = false;",
"options = replacement;",
])("keeps changed or escaped constant options unknown: %s", (mutation) => {
const analysis = analyzeJavaScriptStaticSource(`
const { BrowserWindow } = require("electron");
const options = { webPreferences: { sandbox: true } };
${mutation}
new BrowserWindow(options);
`);
expect(analysis.electron.browser_windows).toMatchObject([
{
options_status: "dynamic",
web_preferences_status: "dynamic",
web_preferences: [],
preload_path: null,
},
]);
});
it.each([
"new BrowserWindow(options); const options = { webPreferences: { sandbox: true } };",
"const options = alias; const alias = { webPreferences: { sandbox: true } }; new BrowserWindow(options);",
"const options = alias; const alias = options; new BrowserWindow(options);",
"const options = { webPreferences: { sandbox: true } }; { const options = unknown; new BrowserWindow(options); }",
"let options = { webPreferences: { sandbox: true } }; new BrowserWindow(options);",
])("does not invent a constant initializer: %s", (source) => {
const analysis = analyzeJavaScriptStaticSource(`
const { BrowserWindow } = require("electron"); ${source}
`);
expect(analysis.electron.browser_windows).toMatchObject([
{
options_status: "dynamic",
web_preferences_status: "dynamic",
web_preferences: [],
},
]);
});
it("preserves constants selected before later mutation and resolves preload context", () => {
const analysis = analyzeJavaScriptStaticSource(`
const { BrowserWindow } = require("electron");
const original = { webPreferences: { preload: require("path").join(__dirname, "preload.js"), sandbox: true } };
const options = original;
new BrowserWindow(options);
original.webPreferences.sandbox = false;
`);
expect(analysis.electron.browser_windows).toMatchObject([
{
options_status: "object-literal",
preload_path: "preload.js",
preload_resolution_context: "filesystem-expression",
},
]);
});
});
describe("Electron option evidence", () => {
it.each([
'({ webPreferences: { preload: "./preload.js", sandbox: true } } as const)',
@@ -38,6 +38,8 @@ export type ElectronBrowserWindowFinding = ElectronBrowserWindowPreload & {
readonly options_status: "object-literal" | "dynamic" | "missing";
readonly web_preferences_status: "object-literal" | "dynamic" | "missing";
readonly web_preferences: readonly ElectronWebPreference[];
/** Object syntax supporting constant substitution, separate from construction. */
readonly resolution_sources?: readonly JavaScriptSourceRange[];
readonly module_key: string | null;
/** Recovered factory runtime; absent in older or unbundled findings. */
readonly module_runtime?: string;
@@ -0,0 +1,65 @@
import * as t from "@babel/types";
import { unwrapJavaScriptExpression } from "./javascriptAstValues.js";
import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js";
import {
semanticStableInitializer,
type JavaScriptBindingIndex,
} from "./javascriptSemanticState.js";
/** Lexical candidates alone do not establish that object contents are intact. */
const optionObjectCandidate = (
input: t.Node | undefined,
index: JavaScriptBindingIndex,
):
| {
readonly object: t.ObjectExpression;
readonly references: readonly t.Identifier[];
}
| undefined => {
if (input === undefined) return undefined;
let node = unwrapJavaScriptExpression(input).node;
const seen = new Set<t.Node>();
const references: t.Identifier[] = [];
while (t.isIdentifier(node)) {
if (seen.has(node)) return undefined;
seen.add(node);
const binding = index.bindingForReferenceAt(node);
if (binding === undefined) return undefined;
const initializer = semanticStableInitializer(binding, index.bindings);
if (
binding.mutable ||
binding.declarationKinds.size !== 1 ||
!binding.declarationKinds.has("const") ||
initializer === undefined ||
initializer.projection.length !== 0 ||
!semanticBindingAtRead(binding, node, index.bindings.parentsByNode)
.hasDefiniteInitializer
)
return undefined;
references.push(node);
node = unwrapJavaScriptExpression(initializer.node).node;
}
return t.isObjectExpression(node) ? { object: node, references } : undefined;
};
/** Avoid building mutation facts for parameters, missing or unresolved objects. */
export const hasElectronOptionObjectCandidate = (
input: t.Node | undefined,
index: JavaScriptBindingIndex,
): boolean => optionObjectCandidate(input, index) !== undefined;
/** Select an inert const object using the shared lexical and mutation owner. */
export const resolveElectronOptionObject = (
input: t.Node | undefined,
site: t.Node,
index: JavaScriptBindingIndex,
propertyPath: readonly string[] = [],
): t.ObjectExpression | undefined => {
const candidate = optionObjectCandidate(input, index);
if (candidate === undefined) return undefined;
for (const reference of candidate.references) {
if (index.referenceMayHaveChangedAt(site, reference, propertyPath))
return undefined;
}
return candidate.object;
};
@@ -175,13 +175,16 @@ function* indexSemanticMemberMutationFactsSteps(
indexedMemberMutationStates.add(state);
}
/** Resolve one identifier or member use and summarize its source-site effects. */
const semanticReferenceEffectsAtSite = (
/** One owner projects source-site effects for complete reports or early exit. */
const visitSemanticReferenceEffectsAtSite = (
state: JavaScriptSemanticAnalysisState,
site: t.Node,
referenceNode: t.Node,
visit: (
effect: JavaScriptSemanticReferenceEffectsAtSite["effects"][number],
) => boolean,
requestedPath?: JavaScriptSemanticPropertyPath,
): JavaScriptSemanticReferenceEffectsAtSite => {
): Omit<JavaScriptSemanticReferenceEffectsAtSite, "effects"> => {
const { root, path: inferredPath } = semanticReferenceRoot(referenceNode);
const memberEffectsIndexed = indexedMemberMutationStates.has(state);
if (!t.isIdentifier(root))
@@ -189,7 +192,6 @@ const semanticReferenceEffectsAtSite = (
bindingId: null,
resolution: "unknown",
memberEffectsIndexed,
effects: [],
};
const lookup = semanticBindingLookupAt(state, root, root.name);
const binding = "binding" in lookup ? lookup.binding : undefined;
@@ -197,16 +199,19 @@ const semanticReferenceEffectsAtSite = (
const resolution =
lookup.kind === "binding" ? "resolved" : blocked ? "unknown" : "unbound";
const path = requestedPath ?? inferredPath;
const effects: JavaScriptSemanticReferenceEffectsAtSite["effects"][number][] =
[];
const metadata: Omit<JavaScriptSemanticReferenceEffectsAtSite, "effects"> = {
bindingId: binding?.bindingId ?? null,
resolution,
memberEffectsIndexed,
};
const retain = (
kind: JavaScriptSemanticReferenceEffectsAtSite["effects"][number]["kind"],
node: t.Node | undefined,
effectPath?: JavaScriptSemanticPropertyPath,
): void => {
): boolean => {
if (effectPath !== undefined && !semanticPathsOverlap(path, effectPath))
return;
effects.push({
return true;
return visit({
kind,
...(effectPath === undefined ? {} : { path: effectPath }),
timing: semanticEffectTiming(site, node, state),
@@ -214,23 +219,40 @@ const semanticReferenceEffectsAtSite = (
};
if (binding !== undefined) {
for (const node of state.bindingWrites.get(binding.bindingId) ?? [])
retain("binding-write", node);
if (!retain("binding-write", node)) return metadata;
if (indexedMemberMutationStates.has(state)) {
for (const effect of binding.memberWrites)
retain("member-write", effect.node, effect.path);
if (!retain("member-write", effect.node, effect.path)) return metadata;
for (const effect of binding.escapedPaths)
retain("escape", effect.node, effect.path);
if (!retain("escape", effect.node, effect.path)) return metadata;
}
} else if (lookup.kind === "ambient" || lookup.kind === "uncertain-ambient") {
for (const node of state.ambientBindingWrites.get(root.name) ?? [])
retain("ambient-write", node);
if (!retain("ambient-write", node)) return metadata;
}
return {
bindingId: binding?.bindingId ?? null,
resolution,
memberEffectsIndexed,
effects,
};
return metadata;
};
/** Preserve the complete diagnostic contract for consumers needing every effect. */
const semanticReferenceEffectsAtSite = (
state: JavaScriptSemanticAnalysisState,
site: t.Node,
referenceNode: t.Node,
requestedPath?: JavaScriptSemanticPropertyPath,
): JavaScriptSemanticReferenceEffectsAtSite => {
const effects: JavaScriptSemanticReferenceEffectsAtSite["effects"][number][] =
[];
const metadata = visitSemanticReferenceEffectsAtSite(
state,
site,
referenceNode,
(effect) => {
effects.push(effect);
return true;
},
requestedPath,
);
return { ...metadata, effects };
};
const semanticReferenceRoot = (
@@ -421,6 +443,12 @@ export function* indexJavaScriptBindingsWithReceiversSteps(
*indexMemberMutationFactsSteps() {
yield* indexSemanticMemberMutationFactsSteps(state);
},
bindingForReferenceAt(referenceNode) {
const { root } = semanticReferenceRoot(referenceNode);
return t.isIdentifier(root)
? resolveSemanticBindingState(state, root, root.name)
: undefined;
},
effectsForReferenceAt(site, referenceNode, propertyPath) {
return semanticReferenceEffectsAtSite(
state,
@@ -429,6 +457,21 @@ export function* indexJavaScriptBindingsWithReceiversSteps(
propertyPath,
);
},
referenceMayHaveChangedAt(site, referenceNode, propertyPath) {
if (!indexedMemberMutationStates.has(state)) return true;
let changed = false;
const metadata = visitSemanticReferenceEffectsAtSite(
state,
site,
referenceNode,
(effect) => {
changed = effect.timing !== "after";
return !changed;
},
propertyPath,
);
return metadata.resolution !== "resolved" || changed;
},
*referencesSteps() {
references ??= yield* collectSemanticReferencesSteps(program, state);
return references;
@@ -1,4 +1,5 @@
import { expect, it } from "vitest";
import * as t from "@babel/types";
import {
analyzeParsedJavaScriptSemantics,
@@ -15,6 +16,51 @@ import {
traverseJavaScriptAstSteps,
} from "./javascriptSemanticTraversal.js";
it("bounds repeated capture checks while preserving unindexed and escaped unknowns", () => {
const count = 128;
const parsed = parseJavaScriptSource(
[
"const options = {webPreferences: {sandbox: true}};",
...Array.from({ length: count }, () => "new BrowserWindow(options);"),
].join("\n"),
);
if (parsed === null) throw new Error("Expected parsed source");
const index = completeSemanticSteps(
indexJavaScriptBindingsWithReceiversSteps(parsed.program),
);
const sites = parsed.program.body.flatMap((node) =>
t.isExpressionStatement(node) && t.isNewExpression(node.expression)
? [node.expression]
: [],
);
const first = sites[0];
if (first === undefined || !t.isIdentifier(first.arguments[0]))
throw new Error("Expected capture");
expect(index.referenceMayHaveChangedAt(first, first.arguments[0])).toBe(true);
completeSemanticSteps(index.indexMemberMutationFactsSteps());
const binding = index.bindingForReferenceAt(first.arguments[0]);
if (binding === undefined) throw new Error("Expected local binding");
let reads = 0;
const effects = new Proxy(binding.escapedPaths, {
get(target, key, receiver) {
if (typeof key === "string" && /^\d+$/u.test(key)) reads += 1;
return Reflect.get(target, key, receiver);
},
});
Object.defineProperty(binding, "escapedPaths", { value: effects });
const changed = sites.map((site) => {
const reference = site.arguments[0];
if (!t.isIdentifier(reference)) throw new Error("Expected local reference");
return index.referenceMayHaveChangedAt(site, reference, ["webPreferences"]);
});
expect(changed).toEqual([
false,
...Array.from({ length: count - 1 }, () => true),
]);
// Allow implementation headroom while rejecting a full N-by-N effect report.
expect(reads).toBeLessThanOrEqual(count * 4);
});
it.each([false, true])(
"checkpoints suppressed GraphQL sources, including alias proof (aliases: %s)",
(aliased) => {
@@ -163,11 +163,21 @@ export interface JavaScriptBindingIndex {
readonly bindings: JavaScriptSemanticAnalysisState;
indexAmbientWritesSteps(): Generator<void, void>;
indexMemberMutationFactsSteps(): Generator<void, void>;
/** Resolve certain lexical ownership without collecting mutation reports. */
bindingForReferenceAt(
referenceNode: t.Node,
): JavaScriptSemanticBindingState | undefined;
effectsForReferenceAt(
site: t.Node,
referenceNode: t.Node,
propertyPath?: JavaScriptSemanticPropertyPath,
): JavaScriptSemanticReferenceEffectsAtSite;
/** Stop at the first relevant prior/uncertain effect without retaining a report. */
referenceMayHaveChangedAt(
site: t.Node,
referenceNode: t.Node,
propertyPath?: JavaScriptSemanticPropertyPath,
): boolean;
referencesSteps(): Generator<void, readonly JavaScriptSemanticReference[]>;
}
@@ -11,6 +11,7 @@ import {
} from "./javascriptSemanticTraversal.js";
import { inspectElectronStaticNode } from "./electronStaticAnalysis.js";
import { finishElectronBrowserWindowsSteps } from "./electronStaticAnalysisBrowser.js";
import {
detectVendors,
failedJavaScriptStaticAnalysis,
@@ -99,6 +100,7 @@ export function* analyzeParsedJavaScriptStaticSourceSteps(
rpcScan: createJavaScriptRpcScan(source, receivers.rpcClientRoots, index),
};
yield* traverseStaticSourceSteps(file, findings, bindings);
yield* finishElectronBrowserWindowsSteps(findings, index);
if (accumulator.pendingNetworkCalls.length > 0) {
yield* index.indexMemberMutationFactsSteps();
finishPartialNetworkEndpoints(findings, bindings, index);
@@ -571,8 +571,7 @@ const resolvedRequestTarget = (
// A toString result is selected before the enclosing request invocation.
const expression = unwrapJavaScriptExpression(node).node;
const capture = reference === expression ? site : expression;
if (referenceMayHaveChangedAtSite(reference, capture, index))
return undefined;
if (index.referenceMayHaveChangedAt(capture, reference)) return undefined;
if (!requestInitializerPrecedesSite(reference, capture, bindings, index))
return undefined;
const evaluated = evaluateSemanticExpression(node, bindings);
@@ -587,9 +586,7 @@ const requestInitializerPrecedesSite = (
bindings: JavaScriptSemanticAnalysisState,
index: JavaScriptBindingIndex,
): boolean => {
const effects = index.effectsForReferenceAt(site, reference);
if (effects.bindingId === null) return false;
const binding = bindings.bindingsById.get(effects.bindingId);
const binding = index.bindingForReferenceAt(reference);
const initializer = semanticStableInitializer(binding, bindings);
return (
initializer !== undefined &&
@@ -601,19 +598,6 @@ const requestInitializerPrecedesSite = (
);
};
const referenceMayHaveChangedAtSite = (
reference: t.Node,
site: t.Node,
index: JavaScriptBindingIndex,
): boolean => {
const effects = index.effectsForReferenceAt(site, reference);
return (
!effects.memberEffectsIndexed ||
effects.resolution !== "resolved" ||
effects.effects.some(({ timing }) => timing !== "after")
);
};
const requestReferenceNode = (node: t.Node): t.Node => {
const target = unwrapJavaScriptExpression(node).node;
if (
@@ -652,16 +636,9 @@ const urlObjectRequestTarget = (
)
target = unwrapJavaScriptExpression(callee.object).node;
}
if (
!t.isIdentifier(target) ||
referenceMayHaveChangedAtSite(target, site, index)
)
if (!t.isIdentifier(target) || index.referenceMayHaveChangedAt(site, target))
return undefined;
const effects = index.effectsForReferenceAt(site, target);
const binding =
effects.bindingId === null
? undefined
: bindings.bindingsById.get(effects.bindingId);
const binding = index.bindingForReferenceAt(target);
if (binding === undefined || seen.has(binding.bindingId)) return undefined;
seen.add(binding.bindingId);
const stable = semanticStableInitializer(binding, bindings);
@@ -55,6 +55,7 @@ export interface JavaScriptAnalysisAccumulator {
readonly sourceMaps: JavaScriptStaticAnalysis["source_map_urls"][number][];
readonly registrations: JavaScriptBundlerRegistration[];
readonly browserWindows: LocatedJavaScriptFinding<ElectronBrowserWindowFinding>[];
readonly pendingBrowserWindows: t.NewExpression[];
readonly contextBridgeApis: LocatedJavaScriptFinding<ElectronContextBridgeFinding>[];
readonly ipc: LocatedJavaScriptFinding<ElectronIpcFinding>[];
readonly senderValidations: LocatedJavaScriptFinding<ElectronSenderValidationFinding>[];
@@ -120,6 +121,7 @@ export const createJavaScriptAnalysisAccumulator = (
sourceMaps: [],
registrations: [],
browserWindows: [],
pendingBrowserWindows: [],
contextBridgeApis: [],
ipc: [],
senderValidations: [],
@@ -140,6 +140,7 @@ const windowShape = {
web_preferences: z.array(
z.strictObject({ name: z.string(), value: staticValue }),
),
resolution_sources: z.array(range).exactOptional(),
};
const bridgeShape = {
resolution_basis: z.literal("wrapper-substitution").optional(),
@@ -0,0 +1,109 @@
import { expect } from "vitest";
import { cliTest } from "../../support/cli/cliFixture.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
import { writeFixtureFiles } from "../../support/javascriptApplicationFixture.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js";
cliTest(
"retains constant window preferences and inferred source provenance through CLI, MCP and fact views",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-window-options-");
await writeFixtureFiles(root, {
"main.js": [
'throw new Error("Static input must never execute");',
'const { BrowserWindow } = require("electron");',
"function open() {",
' const prefs = {nodeIntegration: true, contextIsolation: false, preload: "./preload.js"};',
" const options = {webPreferences: prefs};",
' if (linux) options.icon = "icon.png";',
" new BrowserWindow(options);",
" const escaped = {webPreferences: {sandbox: true}};",
" patch(escaped); new BrowserWindow(escaped);",
" new BrowserWindow({webPreferences: {sandbox: true}});",
"}",
].join("\n"),
"preload.js": "// Inert preload source",
});
const cliResponse = await cli.run({
arguments: ["analyze-javascript-application", root, "--format", "json"],
});
expect(cliResponse.exitCode, JSON.stringify(cliResponse.json)).toBe(0);
const cliResult = javascriptApplicationAnalysisResultSchema.parse(
parseEvidence(cliResponse.json).normalized_result,
);
const { call } = await connectLocalToolsMcp();
const response = await call("analyze_javascript_application", {
input_path: root,
format: "directory",
});
expect(response.isError).not.toBe(true);
const parent = parseEvidence(response.structuredContent);
const result = javascriptApplicationAnalysisResultSchema.parse(
parent.normalized_result,
);
expect(result.graph).toEqual(cliResult.graph);
expect(result.summary).toEqual(cliResult.summary);
expect(result.summary).toMatchObject({
browser_windows: 3,
explicit_web_preferences: 4,
preload_entrypoints: 1,
});
const windows = result.graph.nodes.filter(
(node) => node.kind === "browser-window",
);
const resolved = windows.find(
(node) =>
node.observations[0]?.properties.resolution_sources !== undefined,
);
expect(resolved?.observations[0]).toMatchObject({
properties: {
source_path: "main.js",
options_status: "object-literal",
resolution_sources: [
{ source: "main.js", start: { line: 5 } },
{ source: "main.js", start: { line: 4 } },
],
},
evidence: {
state: "inferred",
authority: "static-relationship-inference",
location: { value: { source: "main.js", start: { line: 7 } } },
},
});
expect(
windows.filter(
(node) =>
node.observations[0]?.properties.web_preferences_status === "dynamic",
),
).toHaveLength(1);
expect(
windows.filter(
(node) => node.observations[0]?.evidence.state === "observed",
),
).toHaveLength(2);
const viewResponse = await call("inspect_analysis_view", {
source: { kind: "retained-evidence", evidence_id: parent.evidence_id },
view: {
kind: "javascript-facts",
where: { paths: ["main.js"], kinds: ["browser-window"] },
property_keys: ["web_preferences", "resolution_sources"],
},
});
expect(viewResponse.isError).not.toBe(true);
const view = analysisViewResultSchema.parse(
parseEvidence(viewResponse.structuredContent).normalized_result,
);
if (view.kind !== "javascript-facts") throw new Error("Expected fact view");
expect(view.rows).toHaveLength(3);
const row = view.rows.find((row) => row.node_id === resolved?.node_id);
expect(row?.evidence).toEqual(resolved?.observations[0]?.evidence);
expect(row?.properties).toEqual({
web_preferences: resolved?.observations[0]?.properties.web_preferences,
resolution_sources:
resolved?.observations[0]?.properties.resolution_sources,
});
},
);