hoi4-agent-tools 4.0.5 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +2 -0
- package/dist/hoi4_agent_tools/core/bounded-source.js +210 -0
- package/dist/hoi4_agent_tools/core/canonical.js +42 -0
- package/dist/hoi4_agent_tools/core/condition-evaluator.js +12 -8
- package/dist/hoi4_agent_tools/core/dependency-walk.js +21 -2
- package/dist/hoi4_agent_tools/core/diagnostic-rollup.js +91 -0
- package/dist/hoi4_agent_tools/core/domain-validation.js +12 -0
- package/dist/hoi4_agent_tools/core/engine.js +2 -1
- package/dist/hoi4_agent_tools/core/impact-graph.js +6 -2
- package/dist/hoi4_agent_tools/core/impact-semantic.js +12 -2
- package/dist/hoi4_agent_tools/core/index.js +1 -0
- package/dist/hoi4_agent_tools/core/job-executor.js +9 -0
- package/dist/hoi4_agent_tools/core/job-worker-host.js +12 -2
- package/dist/hoi4_agent_tools/core/managed-outputs.js +72 -0
- package/dist/hoi4_agent_tools/core/operation-result.js +28 -2
- package/dist/hoi4_agent_tools/core/scanner.js +17 -0
- package/dist/hoi4_agent_tools/core/server-state.js +9 -1
- package/dist/hoi4_agent_tools/core/temporary-directories.js +5 -1
- package/dist/hoi4_agent_tools/core/transaction-canonical.js +43 -0
- package/dist/hoi4_agent_tools/core/transaction-limits.js +7 -2
- package/dist/hoi4_agent_tools/core/transactions.js +355 -111
- package/dist/hoi4_agent_tools/decision/service.js +22 -1
- package/dist/hoi4_agent_tools/event/algorithms.js +3 -2
- package/dist/hoi4_agent_tools/event/graph.js +52 -35
- package/dist/hoi4_agent_tools/event/limits.js +4 -0
- package/dist/hoi4_agent_tools/event/operations.js +11 -0
- package/dist/hoi4_agent_tools/event/queries.js +13 -34
- package/dist/hoi4_agent_tools/event/render.js +4 -2
- package/dist/hoi4_agent_tools/event/service.js +93 -33
- package/dist/hoi4_agent_tools/event/source-analysis.js +94 -4
- package/dist/hoi4_agent_tools/event/state-keys.js +63 -0
- package/dist/hoi4_agent_tools/event/summary.js +130 -0
- package/dist/hoi4_agent_tools/impact/service.js +2 -1
- package/dist/hoi4_agent_tools/map/derived.js +21 -6
- package/dist/hoi4_agent_tools/map/job-operations.js +9 -3
- package/dist/hoi4_agent_tools/map/operations.js +72 -21
- package/dist/hoi4_agent_tools/map/reference-labels.js +238 -0
- package/dist/hoi4_agent_tools/map/reference-preparation.js +247 -0
- package/dist/hoi4_agent_tools/map/reference-world.js +1097 -0
- package/dist/hoi4_agent_tools/map/render.js +9 -1
- package/dist/hoi4_agent_tools/map/service.js +222 -18
- package/dist/hoi4_agent_tools/map/tool-operations.js +71 -2
- package/dist/hoi4_agent_tools/map/world-context.js +148 -0
- package/dist/hoi4_agent_tools/map/world-drainage.js +273 -0
- package/dist/hoi4_agent_tools/map/world-outputs.js +55 -0
- package/dist/hoi4_agent_tools/map/world-preview-style.js +157 -0
- package/dist/hoi4_agent_tools/map/world-preview.js +230 -0
- package/dist/hoi4_agent_tools/map/world-provinces.js +524 -0
- package/dist/hoi4_agent_tools/map/world.js +374 -253
- package/dist/hoi4_agent_tools/mcp/tools/analysis.js +1 -0
- package/dist/hoi4_agent_tools/mcp/tools/event.js +66 -0
- package/dist/hoi4_agent_tools/mcp/tools/map.js +15 -2
- package/dist/hoi4_agent_tools/reference/source-lookup.js +4 -2
- package/dist/hoi4_agent_tools/schemas/map-requests.js +4 -0
- package/dist/hoi4_agent_tools/schemas/map.js +103 -10
- package/dist/hoi4_agent_tools/schemas/transaction.js +20 -3
- package/dist/hoi4_agent_tools/version.js +2 -2
- package/docs/analysis.md +4 -0
- package/docs/events.md +12 -0
- package/docs/images/reference-maps/political.png +0 -0
- package/docs/images/reference-maps/sketch.png +0 -0
- package/docs/map.md +45 -0
- package/docs/third-party-notices.md +6 -0
- package/npm-shrinkwrap.json +124 -2
- package/package.json +4 -1
- package/schemas/configuration.schema.json +1 -1
- package/schemas/continuous-focus-palette.schema.json +1 -1
- package/schemas/custom-weighted-pool.schema.json +1 -1
- package/schemas/decision-inspect-input.schema.json +1 -1
- package/schemas/error-log-input.schema.json +1 -1
- package/schemas/event-feature-manifest.schema.json +1 -1
- package/schemas/focus-plan.schema.json +1 -1
- package/schemas/focus-planning-sidecar.schema.json +1 -1
- package/schemas/gui-animation-source.schema.json +1 -1
- package/schemas/gui-helper.schema.json +1 -1
- package/schemas/gui-scenario.schema.json +1 -1
- package/schemas/helper-expansion-request.schema.json +1 -1
- package/schemas/helper-expansion-summary.schema.json +1 -1
- package/schemas/impact-inspect-input.schema.json +1 -1
- package/schemas/map-operation.schema.json +373 -70
- package/schemas/mechanic-test-input.schema.json +1 -1
- package/schemas/mod-index-input.schema.json +1 -1
- package/schemas/operation-result.schema.json +1 -1
- package/schemas/package-check-input.schema.json +1 -1
- package/schemas/package-manifest.schema.json +1 -1
- package/schemas/probability-analysis-result.schema.json +1 -1
- package/schemas/probability-compare-input.schema.json +1 -1
- package/schemas/probability-evaluate-input.schema.json +1 -1
- package/schemas/probability-inspect-input.schema.json +1 -1
- package/schemas/probability-render-input.schema.json +1 -1
- package/schemas/probability-scenario-set.schema.json +1 -1
- package/schemas/probability-sequence-input.schema.json +1 -1
- package/schemas/probability-simulate-input.schema.json +1 -1
- package/schemas/probability-sweep-input.schema.json +1 -1
- package/schemas/reference-context-input.schema.json +1 -1
- package/schemas/reference-read-input.schema.json +1 -1
- package/schemas/reference-search-input.schema.json +1 -1
- package/schemas/scenario-suite.schema.json +1 -1
- package/schemas/scenario-test-input.schema.json +1 -1
- package/schemas/script-validate-input.schema.json +1 -1
- package/schemas/shared-scenario.schema.json +1 -1
- package/schemas/source-lookup-input.schema.json +1 -1
- package/server.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 4.2.0 - 2026-10-19
|
|
6
|
+
|
|
7
|
+
- Keep job workers running when a discovered mod folder is removed after server start. Workers now resolve only workspaces whose roots still exist, while a job for the removed workspace still fails with `WORKSPACE_ROOT_MISSING`.
|
|
8
|
+
- Stop `hoi4.event_inspect` `scan` from failing with `RangeError: Invalid string length` on large mods. The graph snapshot is measured before serialization and omitted above 128 MiB with the info diagnostic `EVENT_GRAPH_SNAPSHOT_OMITTED`.
|
|
9
|
+
- Return `data.summary` from event inspection: selection finding counts by classification and code with the ten most severe source-linked findings, trace sizes and event IDs, and root counts.
|
|
10
|
+
- Resolve `event_target:name` reads against `save_global_event_target_as` producers; `global_event_target:name` reads still require a global producer. This removes false `EVENT_TARGET_READ_BEFORE_SAVE` probable defects.
|
|
11
|
+
- Record temporary-variable and temporary-array writes, `round_variable`, `modulo_variable`, `randomize_variable`, `set_variable_to_random` and `resize_array`. Link variable reads to writers inside scripted helpers, scope-prefixed names and `array^index` elements, removing most false `EVENT_GATE_WITHOUT_WRITER` warnings.
|
|
12
|
+
- Stop inferring missing scripted helpers from native effect arguments, condition blocks of decisions, ideas, focuses and raids, trigger iterators and data fields of definition files.
|
|
13
|
+
- Report unresolved references in event analysis only for symbol kinds that analysis indexes; sprite and interface references are left to the full index.
|
|
14
|
+
- Keep distinct event findings in one owner separate instead of collapsing them to the first finding per code.
|
|
15
|
+
- Report impact dynamic references only for symbol kinds the traversal reaches, list affected files in `data.affectedFilePaths`, and leave `changedFiles` empty for this read-only analysis.
|
|
16
|
+
- Treat undeclared `has_variable` and `has_event_target` inputs as unresolved in open decision scenarios, matching flags; `closedFlags: true` keeps them absent.
|
|
17
|
+
- Complete workspace-wide event `lint`, `state_flow`, `impact` and unselected renders on large mods. Helper expansion stops at its fixed work ceiling with an `EVENT_HELPER_PROJECTION_LIMIT` finding instead of failing with `DEPENDENCY_WORK_LIMIT`, and dependency checkpoints are spaced by the size of the previous save so their cost stays linear.
|
|
18
|
+
- Remove quadratic passes from root discovery, event cycle checks, selector-scoped lint and terminal or unresolved renders. Root discovery on Chaos Redux with vanilla falls from minutes to under a second.
|
|
19
|
+
- Report each recursive scripted-helper loop once instead of once per caller, and read `event_target:founder.values` as the target `founder`.
|
|
20
|
+
- Return the overview and the branch renders that fit when an unselected overview spends its raster budget, with `omittedBranchRenders` and `coverage.budgetOmittedBranches`, instead of `RENDER_AGGREGATE_BLOCKED`.
|
|
21
|
+
- Add `data.summary` to `explain_path` (route found, steps, event IDs, failure reason) and event `impact` (reach counts); root summaries list the mod's own callerless events first with counts by layer, and finding summaries rank the mod's files first with `byLayer` counts.
|
|
22
|
+
- Keep decision gate, cost and lifecycle evidence when the installed game version is not verified for AI-weight adapters; the AI part is unresolved with a `PROBABILITY_GAME_VERSION_UNSUPPORTED` warning.
|
|
23
|
+
- Raise the semantic reference ceiling for impact analysis and source lookup to its supported maximum of 1,000,000, so a large mod with the base game is analyzed completely.
|
|
24
|
+
|
|
25
|
+
## 4.1.0 - 2026-10-15
|
|
26
|
+
|
|
27
|
+
- Add reference-image `create_world` requests with automatic dimensions, countries, biomes, coastline interpretation, drainage, offline English label recognition, optional aligned layers and reviewed semantic hints.
|
|
28
|
+
- Add `preview: true` to map rewrites. Previews validate and render without creating or applying a rewrite transaction; failed generation is never illustrated using the unchanged base-game map.
|
|
29
|
+
- Guard local reference inputs with canonical workspace checks, bounded decoding, fixed-memory original-source hashing and stale-source verification. OCR uses packaged local model data and bounded worker lifetimes.
|
|
30
|
+
- Generate smoother relief, connected terrain-dependent provinces and downhill rivers; use installed palette conventions, distinct country filenames and actual urban footprints.
|
|
31
|
+
- Cache map colors per province instead of hashing them for every rendered pixel. Retain deterministic pixels and artifact contents.
|
|
32
|
+
- Preserve reviewed city, state, sea and region names and report uncertain interpretation. Map generation remains an offline foundation, with live acceptance evidence reported separately.
|
|
33
|
+
- Add starter bookmarks, country histories, infantry templates, units, stockpiles and production; isolate old-world AI and startup hooks while retaining shared continent identifiers.
|
|
34
|
+
- Track generated outputs and safely remove obsolete files on regeneration; reject edited outputs and malformed inventories. Bound complete-world inline diffs so preview links remain available.
|
|
35
|
+
- Clean detached water speckles without turning lake islands into ocean pockets, preserve native province extent limits, and emit building positions without a spurious final record.
|
|
36
|
+
|
|
5
37
|
## 4.0.5 - 2026-10-14
|
|
6
38
|
|
|
7
39
|
- Keep child anchors independent of parent `origo`, so centered native windows place their controls inside the frame.
|
package/README.md
CHANGED
|
@@ -17,6 +17,8 @@ Make focus trees, events and decisions. Edit maps and generate a new world. Prev
|
|
|
17
17
|
## What it does
|
|
18
18
|
|
|
19
19
|
Your agent can build and edit focus trees, events, decisions and maps, and generate a new world.
|
|
20
|
+
|
|
21
|
+
For a new map, give it a cleaned local reference image and preview the interpreted geography before writing. The [map workflow](docs/map.md#generate-a-world-from-one-image) creates provinces, states, terrain, cities, countries, supply and a provisional starting scenario. Reviewed names and masks refine ambiguous artwork; offline validation is separate from [native campaign acceptance](https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.1.0/docs/evaluations/worldgen_acceptance_2026-10-15.md).
|
|
20
22
|
It previews focus trees, technology folders, windows and maps, finds the right rule in the game's documentation, fixes broken script and tunes AI choices.
|
|
21
23
|
You describe what you want; the agent picks the right tools.
|
|
22
24
|
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { ServiceError } from './result.js';
|
|
2
|
+
import { prepareSource, verifySource } from './scanner.js';
|
|
3
|
+
import { isWithin } from './workspace.js';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { lstat, mkdtemp, open, realpath, rmdir, unlink } from 'node:fs/promises';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import sharp from 'sharp';
|
|
9
|
+
import { TemporaryDirectoryLifetime, sweepTemporaryDirectories } from './temporary-directories.js';
|
|
10
|
+
export const REFERENCE_SOURCE_MAX_BYTES = 1_073_741_824;
|
|
11
|
+
export const REFERENCE_WORKING_MAX_SIDE = 3_072;
|
|
12
|
+
export const REFERENCE_WORKING_MAX_BYTES = 33_554_432;
|
|
13
|
+
function supportedRasterHeader(bytes) {
|
|
14
|
+
return ((bytes.length >= 8 &&
|
|
15
|
+
[137, 80, 78, 71, 13, 10, 26, 10].every((value, index) => bytes[index] === value)) ||
|
|
16
|
+
(bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) ||
|
|
17
|
+
(bytes.length >= 12 &&
|
|
18
|
+
[82, 73, 70, 70].every((value, index) => bytes[index] === value) &&
|
|
19
|
+
[87, 69, 66, 80].every((value, index) => bytes[index + 8] === value)));
|
|
20
|
+
}
|
|
21
|
+
function assertSnapshotIsolation(resolver, candidate, checkDescendants) {
|
|
22
|
+
for (const workspace of resolver.list())
|
|
23
|
+
for (const root of [
|
|
24
|
+
...workspace.roots,
|
|
25
|
+
{ path: workspace.artifactRoot },
|
|
26
|
+
{ path: workspace.cacheRoot },
|
|
27
|
+
])
|
|
28
|
+
if (isWithin(root.path, candidate) || (checkDescendants && isWithin(candidate, root.path)))
|
|
29
|
+
throw new ServiceError('REFERENCE_SNAPSHOT_UNSAFE', 'Reference snapshots must be outside workspace capability roots');
|
|
30
|
+
}
|
|
31
|
+
async function assertSnapshotDirectory(resolver, directory, parent) {
|
|
32
|
+
const canonical = path.normalize(await realpath(directory));
|
|
33
|
+
const metadata = await lstat(directory);
|
|
34
|
+
if (canonical !== path.normalize(directory) ||
|
|
35
|
+
!isWithin(parent, canonical) ||
|
|
36
|
+
canonical === parent ||
|
|
37
|
+
metadata.isSymbolicLink() ||
|
|
38
|
+
!metadata.isDirectory())
|
|
39
|
+
throw new ServiceError('REFERENCE_SNAPSHOT_UNSAFE', 'Reference snapshot directory is not a private canonical directory');
|
|
40
|
+
if (process.platform !== 'win32' &&
|
|
41
|
+
((metadata.mode & 0o077) !== 0 ||
|
|
42
|
+
(process.getuid !== undefined && metadata.uid !== process.getuid())))
|
|
43
|
+
throw new ServiceError('REFERENCE_SNAPSHOT_UNSAFE', 'Reference snapshot directory permissions are not private');
|
|
44
|
+
assertSnapshotIsolation(resolver, canonical, true);
|
|
45
|
+
}
|
|
46
|
+
async function createPrivateSnapshot(resolver) {
|
|
47
|
+
const parent = path.normalize(await realpath(resolver.serverState()?.root ?? tmpdir()));
|
|
48
|
+
assertSnapshotIsolation(resolver, parent, false);
|
|
49
|
+
const parentMetadata = await lstat(parent);
|
|
50
|
+
if (!parentMetadata.isDirectory() ||
|
|
51
|
+
parentMetadata.isSymbolicLink() ||
|
|
52
|
+
(process.platform !== 'win32' &&
|
|
53
|
+
(parentMetadata.mode & 0o022) !== 0 &&
|
|
54
|
+
(parentMetadata.mode & 0o1000) === 0))
|
|
55
|
+
throw new ServiceError('REFERENCE_SNAPSHOT_UNSAFE', 'Reference snapshot parent is not protected against directory replacement');
|
|
56
|
+
// Hidden directories are excluded by mod-root discovery even with a temporary-root fallback.
|
|
57
|
+
await sweepTemporaryDirectories(parent, {
|
|
58
|
+
referenceSnapshotsOnly: true,
|
|
59
|
+
staleMs: 0,
|
|
60
|
+
maxBytes: REFERENCE_SOURCE_MAX_BYTES,
|
|
61
|
+
protectedRoots: resolver
|
|
62
|
+
.list()
|
|
63
|
+
.flatMap((workspace) => [
|
|
64
|
+
...workspace.roots.map(({ path }) => path),
|
|
65
|
+
workspace.artifactRoot,
|
|
66
|
+
workspace.cacheRoot,
|
|
67
|
+
]),
|
|
68
|
+
});
|
|
69
|
+
const directory = await mkdtemp(path.join(parent, '.reference-snapshot-'));
|
|
70
|
+
const lifetime = new TemporaryDirectoryLifetime(REFERENCE_SOURCE_MAX_BYTES);
|
|
71
|
+
try {
|
|
72
|
+
await assertSnapshotDirectory(resolver, directory, parent);
|
|
73
|
+
await lifetime.own(directory);
|
|
74
|
+
const filePath = path.join(directory, 'source.raster');
|
|
75
|
+
const handle = await open(filePath, 'wx', 0o600);
|
|
76
|
+
try {
|
|
77
|
+
await assertSnapshotDirectory(resolver, directory, parent);
|
|
78
|
+
return { directory, filePath, handle, closed: false, lifetime };
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
await handle.close();
|
|
82
|
+
await unlink(filePath);
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
await lifetime.dispose();
|
|
88
|
+
if (await lstat(directory).catch(() => undefined))
|
|
89
|
+
await rmdir(directory);
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function removePrivateSnapshot(snapshot) {
|
|
94
|
+
try {
|
|
95
|
+
let closeFailed = false;
|
|
96
|
+
try {
|
|
97
|
+
if (!snapshot.closed)
|
|
98
|
+
await snapshot.handle.close();
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
closeFailed = true;
|
|
102
|
+
}
|
|
103
|
+
await snapshot.lifetime.dispose();
|
|
104
|
+
if (closeFailed || (await lstat(snapshot.directory).catch(() => undefined)))
|
|
105
|
+
throw new Error('Private snapshot ownership changed before cleanup');
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
throw new ServiceError('REFERENCE_SNAPSHOT_CLEANUP_FAILED', 'Private reference snapshot cleanup failed');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** Decode a private snapshot copied and hashed from the same verified FD, using fixed memory. */
|
|
112
|
+
export async function readReferenceImageSource(resolver, workspaceId, relativePath, principal, signal) {
|
|
113
|
+
if (!/\.(?:png|jpe?g|webp)$/iu.test(relativePath) || relativePath.includes(':'))
|
|
114
|
+
throw new ServiceError('REFERENCE_IMAGE_PATH_INVALID', 'Reference must name a local raster image');
|
|
115
|
+
const resolved = await resolver.resolvePath(workspaceId, relativePath, 'read', ['mod'], principal);
|
|
116
|
+
const source = await prepareSource(resolved.root, relativePath, signal);
|
|
117
|
+
let snapshot;
|
|
118
|
+
try {
|
|
119
|
+
if (!source.metadata.isFile())
|
|
120
|
+
throw new ServiceError('SOURCE_NOT_REGULAR_FILE', 'Reference source must be a regular file');
|
|
121
|
+
if (source.metadata.size < 1 || source.metadata.size > REFERENCE_SOURCE_MAX_BYTES)
|
|
122
|
+
throw new ServiceError('SOURCE_BYTE_LIMIT', 'Reference source exceeds its byte limit', {
|
|
123
|
+
limit: REFERENCE_SOURCE_MAX_BYTES,
|
|
124
|
+
});
|
|
125
|
+
signal?.throwIfAborted();
|
|
126
|
+
snapshot = await createPrivateSnapshot(resolver);
|
|
127
|
+
const hash = createHash('sha256');
|
|
128
|
+
const chunk = Buffer.alloc(Math.min(1_048_576, source.metadata.size));
|
|
129
|
+
let offset = 0;
|
|
130
|
+
while (offset < source.metadata.size) {
|
|
131
|
+
signal?.throwIfAborted();
|
|
132
|
+
const read = await source.handle.read(chunk, 0, Math.min(chunk.length, source.metadata.size - offset), offset);
|
|
133
|
+
if (read.bytesRead === 0)
|
|
134
|
+
throw new ServiceError('SCAN_SOURCE_CHANGED', 'Reference source changed while copying its snapshot');
|
|
135
|
+
if (offset === 0 && !supportedRasterHeader(chunk.subarray(0, read.bytesRead)))
|
|
136
|
+
throw new ServiceError('REFERENCE_IMAGE_DECODE_FAILED', 'Reference source is not a supported raster image');
|
|
137
|
+
hash.update(chunk.subarray(0, read.bytesRead));
|
|
138
|
+
let written = 0;
|
|
139
|
+
while (written < read.bytesRead) {
|
|
140
|
+
const result = await snapshot.handle.write(chunk, written, read.bytesRead - written, offset + written);
|
|
141
|
+
if (result.bytesWritten === 0)
|
|
142
|
+
throw new ServiceError('REFERENCE_SNAPSHOT_WRITE_FAILED', 'Reference snapshot could not be completed');
|
|
143
|
+
written += result.bytesWritten;
|
|
144
|
+
}
|
|
145
|
+
offset += read.bytesRead;
|
|
146
|
+
}
|
|
147
|
+
const originalHash = hash.digest('hex');
|
|
148
|
+
await snapshot.handle.close();
|
|
149
|
+
snapshot.closed = true;
|
|
150
|
+
await verifySource(resolved.root, source);
|
|
151
|
+
signal?.throwIfAborted();
|
|
152
|
+
sharp.cache({ memory: 32, files: 0, items: 32 });
|
|
153
|
+
const input = sharp(snapshot.filePath, {
|
|
154
|
+
sequentialRead: true,
|
|
155
|
+
limitInputPixels: 300_000_000,
|
|
156
|
+
failOn: 'warning',
|
|
157
|
+
});
|
|
158
|
+
const metadata = await input.metadata();
|
|
159
|
+
if (!metadata.width ||
|
|
160
|
+
!metadata.height ||
|
|
161
|
+
(metadata.pages ?? 1) > 1 ||
|
|
162
|
+
!['png', 'jpeg', 'webp'].includes(metadata.format))
|
|
163
|
+
throw new ServiceError('REFERENCE_IMAGE_UNSUPPORTED', 'Reference must be a single-frame supported raster image');
|
|
164
|
+
signal?.throwIfAborted();
|
|
165
|
+
const decoded = await input
|
|
166
|
+
.resize({
|
|
167
|
+
width: REFERENCE_WORKING_MAX_SIDE,
|
|
168
|
+
height: REFERENCE_WORKING_MAX_SIDE,
|
|
169
|
+
fit: 'inside',
|
|
170
|
+
withoutEnlargement: true,
|
|
171
|
+
})
|
|
172
|
+
.png()
|
|
173
|
+
.toBuffer({ resolveWithObject: true });
|
|
174
|
+
await verifySource(resolved.root, source);
|
|
175
|
+
signal?.throwIfAborted();
|
|
176
|
+
if (decoded.data.length > REFERENCE_WORKING_MAX_BYTES)
|
|
177
|
+
throw new ServiceError('REFERENCE_WORKING_BYTE_LIMIT', 'Reference working image exceeds its byte limit');
|
|
178
|
+
return {
|
|
179
|
+
source: {
|
|
180
|
+
absolutePath: source.canonicalPath,
|
|
181
|
+
displayPath: `mod:${relativePath.replaceAll('\\', '/')}`,
|
|
182
|
+
relativePath: relativePath.replaceAll('\\', '/'),
|
|
183
|
+
rootKind: resolved.root.kind,
|
|
184
|
+
loadOrder: resolved.root.loadOrder,
|
|
185
|
+
size: source.metadata.size,
|
|
186
|
+
modifiedMs: source.metadata.mtimeMs,
|
|
187
|
+
sha256: originalHash,
|
|
188
|
+
},
|
|
189
|
+
bytes: decoded.data,
|
|
190
|
+
originalWidth: metadata.width,
|
|
191
|
+
originalHeight: metadata.height,
|
|
192
|
+
workingWidth: decoded.info.width,
|
|
193
|
+
workingHeight: decoded.info.height,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
if (error instanceof ServiceError || signal?.aborted)
|
|
198
|
+
throw error;
|
|
199
|
+
throw new ServiceError('REFERENCE_IMAGE_DECODE_FAILED', 'Reference image could not be safely decoded');
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
try {
|
|
203
|
+
if (snapshot !== undefined)
|
|
204
|
+
await removePrivateSnapshot(snapshot);
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
await source.handle.close();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -40,6 +40,48 @@ export function canonicalize(value) {
|
|
|
40
40
|
export function canonicalJson(value) {
|
|
41
41
|
return JSON.stringify(canonicalize(value));
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* UTF-8 byte length of `canonicalJson(value)`, or `undefined` as soon as it would exceed
|
|
45
|
+
* `limit`. The value is measured without building the string, so a caller can decline output
|
|
46
|
+
* that would exceed a JavaScript string or an artifact ceiling before allocating it.
|
|
47
|
+
*/
|
|
48
|
+
export function canonicalJsonByteLength(value, limit) {
|
|
49
|
+
let total = 0;
|
|
50
|
+
const add = (bytes) => (total += bytes) <= limit;
|
|
51
|
+
const visit = (current) => {
|
|
52
|
+
if (current === null || typeof current === 'string' || typeof current === 'boolean') {
|
|
53
|
+
return add(Buffer.byteLength(JSON.stringify(current)));
|
|
54
|
+
}
|
|
55
|
+
if (typeof current === 'number') {
|
|
56
|
+
if (!Number.isFinite(current))
|
|
57
|
+
throw new TypeError('Canonical JSON cannot contain non-finite numbers');
|
|
58
|
+
return add(Buffer.byteLength(JSON.stringify(Object.is(current, -0) ? 0 : current)));
|
|
59
|
+
}
|
|
60
|
+
if (Array.isArray(current)) {
|
|
61
|
+
if (!add(2 + Math.max(0, current.length - 1)))
|
|
62
|
+
return false;
|
|
63
|
+
for (let index = 0; index < current.length; index += 1) {
|
|
64
|
+
// A hole stays a hole in the canonical copy, which JSON writes as null.
|
|
65
|
+
if (!(index in current) ? !add(4) : !visit(current[index]))
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
if (typeof current === 'object') {
|
|
71
|
+
const object = current;
|
|
72
|
+
const keys = Object.keys(object).filter((key) => object[key] !== undefined);
|
|
73
|
+
if (!add(2 + Math.max(0, keys.length - 1)))
|
|
74
|
+
return false;
|
|
75
|
+
for (const key of keys) {
|
|
76
|
+
if (!add(Buffer.byteLength(JSON.stringify(key)) + 1) || !visit(object[key]))
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
throw new TypeError(`Canonical JSON cannot contain ${typeof current}`);
|
|
82
|
+
};
|
|
83
|
+
return visit(value) ? total : undefined;
|
|
84
|
+
}
|
|
43
85
|
export function sha256Bytes(value) {
|
|
44
86
|
return createHash('sha256').update(value).digest('hex');
|
|
45
87
|
}
|
|
@@ -302,19 +302,23 @@ function evaluateAssignmentCore(assignment, scenario, candidate, definitions, he
|
|
|
302
302
|
return unresolved(assignment, candidate, `Scenario does not declare flag ${right}`, `flag.${right}`);
|
|
303
303
|
return { state: flags?.includes(right) === true ? 'true' : 'false', unresolved: [] };
|
|
304
304
|
}
|
|
305
|
+
// Like flags, an undeclared variable or event target is unknown in an open scenario and
|
|
306
|
+
// absent only when the scenario closes its world with `closedFlags`.
|
|
305
307
|
if (key === 'has_event_target') {
|
|
306
308
|
const eventTargets = scopeContext.binding.eventTargets ?? scenario.eventTargets;
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
309
|
+
if (Object.hasOwn(eventTargets ?? {}, right))
|
|
310
|
+
return { state: 'true', unresolved: [] };
|
|
311
|
+
if (scenario.closedFlags === false)
|
|
312
|
+
return unresolved(assignment, candidate, `Scenario does not declare event target ${right}`, `eventTarget.${right}`);
|
|
313
|
+
return { state: 'false', unresolved: [] };
|
|
311
314
|
}
|
|
312
315
|
if (key === 'has_variable') {
|
|
313
316
|
const state = scopeContext.expression === 'ROOT' ? scenario.state : scopeContext.binding.state;
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
317
|
+
if (Object.hasOwn(state, right) || Object.hasOwn(state, `variable.${right}`))
|
|
318
|
+
return { state: 'true', unresolved: [] };
|
|
319
|
+
if (scenario.closedFlags === false)
|
|
320
|
+
return unresolved(assignment, candidate, `Scenario does not declare variable ${right}`, `variable.${right}`);
|
|
321
|
+
return { state: 'false', unresolved: [] };
|
|
318
322
|
}
|
|
319
323
|
if (key === 'tag' || key === 'original_tag') {
|
|
320
324
|
const actor = scopeContext.expression === 'ROOT'
|
|
@@ -24,6 +24,8 @@ const stateSchema = z
|
|
|
24
24
|
current: z.object({ path: ordinal, edge: ordinal }).strict().nullable(),
|
|
25
25
|
started: z.boolean(),
|
|
26
26
|
complete: z.boolean(),
|
|
27
|
+
/** Set when a `truncate` walk stopped at its work ceiling before visiting every root. */
|
|
28
|
+
truncated: z.boolean().optional(),
|
|
27
29
|
})
|
|
28
30
|
.strict();
|
|
29
31
|
/** A bounded, serializable DFS frontier; paths share prefixes instead of copying source data. */
|
|
@@ -82,7 +84,10 @@ export class DependencyWalk {
|
|
|
82
84
|
const invalid = (message) => {
|
|
83
85
|
throw new ServiceError('DEPENDENCY_CHECKPOINT_INVALID', message);
|
|
84
86
|
};
|
|
85
|
-
if (
|
|
87
|
+
if ((this.state.truncated === true &&
|
|
88
|
+
(this.input.onWorkLimit !== 'truncate' || !this.state.complete)) ||
|
|
89
|
+
this.state.complete !==
|
|
90
|
+
(this.state.root === this.input.roots.length || this.state.truncated === true) ||
|
|
86
91
|
(!this.state.started && (this.state.current !== null || this.state.stack.length !== 0)) ||
|
|
87
92
|
(this.state.complete && this.state.started) ||
|
|
88
93
|
this.state.paths.length > this.state.steps ||
|
|
@@ -152,6 +157,15 @@ export class DependencyWalk {
|
|
|
152
157
|
const end = this.state.steps + maximumSteps;
|
|
153
158
|
while (!this.state.complete && this.state.steps < end) {
|
|
154
159
|
signal?.throwIfAborted();
|
|
160
|
+
if (this.state.steps >= this.input.maximumSteps && this.input.onWorkLimit === 'truncate') {
|
|
161
|
+
// Records found so far are complete paths; the unfinished frontier is dropped.
|
|
162
|
+
this.state.truncated = true;
|
|
163
|
+
this.state.complete = true;
|
|
164
|
+
this.state.started = false;
|
|
165
|
+
this.state.current = null;
|
|
166
|
+
this.state.stack = [];
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
155
169
|
if (this.state.steps >= this.input.maximumSteps)
|
|
156
170
|
throw new ServiceError('DEPENDENCY_WORK_LIMIT', 'Dependency expansion reached its explicit work ceiling', {
|
|
157
171
|
maximum: this.input.maximumSteps,
|
|
@@ -241,9 +255,14 @@ export async function walkDependenciesAsync(input, signal, checkpointSteps = 2_0
|
|
|
241
255
|
const restored = await checkpoints?.load(key, input.sourceRevision, stateSchema);
|
|
242
256
|
const walk = new DependencyWalk(input, restored);
|
|
243
257
|
let savedAt = walk.state.steps;
|
|
258
|
+
// Each save serializes the whole frontier. Spacing saves by the size written last time
|
|
259
|
+
// doubles the gaps as the frontier grows, so total checkpoint work stays linear.
|
|
260
|
+
let savedSize = 0;
|
|
244
261
|
while (!walk.state.complete) {
|
|
245
262
|
const complete = walk.advance(256, signal);
|
|
246
|
-
|
|
263
|
+
const interval = Math.max(checkpointSteps, savedSize);
|
|
264
|
+
if ((complete && savedAt > 0) || walk.state.steps - savedAt >= interval) {
|
|
265
|
+
savedSize = walk.state.paths.length + walk.state.records.length;
|
|
247
266
|
await checkpoints?.save(key, input.sourceRevision, walk.checkpoint(), walk.state.root, input.roots.length);
|
|
248
267
|
savedAt = walk.state.steps;
|
|
249
268
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { canonicalJson, compareCodeUnits } from './canonical.js';
|
|
2
|
+
import { sortDiagnostics } from './diagnostics.js';
|
|
3
|
+
const SAMPLE_LIMIT = 8;
|
|
4
|
+
/** Count source occurrences represented by an original diagnostic or a soft rollup. */
|
|
5
|
+
export function diagnosticOccurrenceCount(diagnostic) {
|
|
6
|
+
const value = diagnostic.details?.occurrenceCount;
|
|
7
|
+
return diagnostic.details?.rollup === 'repeated-soft-diagnostics.v1' &&
|
|
8
|
+
typeof value === 'number' &&
|
|
9
|
+
Number.isSafeInteger(value) &&
|
|
10
|
+
value > 0
|
|
11
|
+
? value
|
|
12
|
+
: 1;
|
|
13
|
+
}
|
|
14
|
+
/** Include omission evidence emitted by bounded validators without counting their marker twice. */
|
|
15
|
+
export function diagnosticSeverityCounts(diagnostics) {
|
|
16
|
+
const counts = { info: 0, warning: 0, error: 0, blocker: 0 };
|
|
17
|
+
for (const diagnostic of diagnostics) {
|
|
18
|
+
if (diagnostic.code !== 'MAP_DIAGNOSTICS_TRUNCATED') {
|
|
19
|
+
counts[diagnostic.severity] += diagnosticOccurrenceCount(diagnostic);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const omissions = diagnostic.details?.omittedBySeverity;
|
|
23
|
+
if (typeof omissions !== 'object' || omissions === null)
|
|
24
|
+
continue;
|
|
25
|
+
for (const severity of ['info', 'warning', 'error', 'blocker']) {
|
|
26
|
+
const value = omissions[severity];
|
|
27
|
+
if (typeof value === 'number' && Number.isSafeInteger(value) && value >= 0)
|
|
28
|
+
counts[severity] += value;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return counts;
|
|
32
|
+
}
|
|
33
|
+
/** Keep every hard diagnostic; summarize repeated informational and warning codes only. */
|
|
34
|
+
export function rollupRepeatedSoftDiagnostics(diagnostics, evidence) {
|
|
35
|
+
const hard = [];
|
|
36
|
+
const groups = new Map();
|
|
37
|
+
const ordered = diagnostics
|
|
38
|
+
.map((diagnostic) => ({ diagnostic, key: canonicalJson(diagnostic) }))
|
|
39
|
+
.sort((left, right) => compareCodeUnits(left.key, right.key))
|
|
40
|
+
.map(({ diagnostic }) => diagnostic);
|
|
41
|
+
for (const diagnostic of ordered) {
|
|
42
|
+
if (diagnostic.severity === 'error' ||
|
|
43
|
+
diagnostic.severity === 'blocker' ||
|
|
44
|
+
diagnostic.code === 'MAP_DIAGNOSTICS_TRUNCATED') {
|
|
45
|
+
hard.push(diagnostic);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const key = `${diagnostic.severity}:${diagnostic.code}`;
|
|
49
|
+
const group = groups.get(key) ?? [];
|
|
50
|
+
group.push(diagnostic);
|
|
51
|
+
groups.set(key, group);
|
|
52
|
+
}
|
|
53
|
+
const soft = [...groups.values()].map((group) => {
|
|
54
|
+
if (group.length === 1)
|
|
55
|
+
return group[0];
|
|
56
|
+
const first = group[0];
|
|
57
|
+
const occurrences = group.reduce((total, diagnostic) => total + diagnosticOccurrenceCount(diagnostic), 0);
|
|
58
|
+
const locations = new Map();
|
|
59
|
+
for (const diagnostic of group)
|
|
60
|
+
for (const location of [diagnostic.location, ...(diagnostic.related ?? [])])
|
|
61
|
+
if (location !== undefined)
|
|
62
|
+
locations.set(canonicalJson(location), location);
|
|
63
|
+
const samples = [...locations]
|
|
64
|
+
.sort(([left], [right]) => compareCodeUnits(left, right))
|
|
65
|
+
.slice(0, SAMPLE_LIMIT)
|
|
66
|
+
.map(([, location]) => location);
|
|
67
|
+
const operationIds = [
|
|
68
|
+
...new Set(group.flatMap(({ operationId }) => (operationId === undefined ? [] : [operationId]))),
|
|
69
|
+
].sort(compareCodeUnits);
|
|
70
|
+
return {
|
|
71
|
+
code: first.code,
|
|
72
|
+
severity: first.severity,
|
|
73
|
+
category: first.category,
|
|
74
|
+
message: `${occurrences} occurrences of ${first.code}; ${first.message}`.slice(0, 4096),
|
|
75
|
+
...(samples[0] === undefined ? {} : { location: samples[0] }),
|
|
76
|
+
...(samples.length < 2 ? {} : { related: samples.slice(1) }),
|
|
77
|
+
...(operationIds.length === 1 ? { operationId: operationIds[0] } : {}),
|
|
78
|
+
details: {
|
|
79
|
+
rollup: 'repeated-soft-diagnostics.v1',
|
|
80
|
+
occurrenceCount: occurrences,
|
|
81
|
+
retainedSamples: Math.min(SAMPLE_LIMIT, group.length),
|
|
82
|
+
samples: group.slice(0, SAMPLE_LIMIT),
|
|
83
|
+
operationCount: operationIds.length,
|
|
84
|
+
operationIds: operationIds.slice(0, SAMPLE_LIMIT),
|
|
85
|
+
locationCount: locations.size,
|
|
86
|
+
...(evidence === undefined ? {} : { completeDiagnosticEvidence: evidence }),
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
return sortDiagnostics([...hard, ...soft]);
|
|
91
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { boundedSourceHashEvidence, publicArtifactLink } from './artifacts.js';
|
|
2
2
|
import { canonicalJson } from './canonical.js';
|
|
3
3
|
import { exactChangedFilePatterns, focusDomainScanPatterns, mapChangeScanPatterns, } from './domain-scan-patterns.js';
|
|
4
|
+
import { rollupRepeatedSoftDiagnostics } from './diagnostic-rollup.js';
|
|
4
5
|
import { parseClausewitz } from './source/index.js';
|
|
5
6
|
import { TRANSACTION_MAX_DIAGNOSTICS } from './transaction-limits.js';
|
|
6
7
|
import { importContinuousFocusPalettes, importFocusTrees, layoutFocusTreeAsync, lintContinuousFocusPalette, lintFocusTree, } from '../focus/index.js';
|
|
@@ -158,6 +159,17 @@ export async function postValidateTransaction(engine, transaction, principal, si
|
|
|
158
159
|
.slice(0, retainedLimit - retainedHard.length);
|
|
159
160
|
const omittedHard = hard.length - retainedHard.length;
|
|
160
161
|
const artifactLink = publicArtifactLink(artifact);
|
|
162
|
+
if (transaction.operationKind === 'agent-nudger-map-changes') {
|
|
163
|
+
const rolled = rollupRepeatedSoftDiagnostics(diagnostics, artifactLink);
|
|
164
|
+
if (rolled.length <= availableDiagnostics) {
|
|
165
|
+
checks.push({
|
|
166
|
+
id: 'post-write-diagnostics-artifact',
|
|
167
|
+
passed: true,
|
|
168
|
+
message: `Complete bounded post-write diagnostics are stored at ${artifactLink.uri}`,
|
|
169
|
+
});
|
|
170
|
+
return { diagnostics: rolled, checks, artifacts: [artifactLink] };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
161
173
|
checks.push({
|
|
162
174
|
id: 'post-write-diagnostics-artifact',
|
|
163
175
|
passed: omittedHard === 0,
|
|
@@ -10,6 +10,7 @@ import { RequestScheduler } from './request-scheduler.js';
|
|
|
10
10
|
import { SharedRequestCapacity } from './shared-request-capacity.js';
|
|
11
11
|
import { JobStore } from './job-store.js';
|
|
12
12
|
import { ServiceError } from './result.js';
|
|
13
|
+
import { TRANSACTION_MAX_JOURNAL_BYTES } from './transaction-limits.js';
|
|
13
14
|
/**
|
|
14
15
|
* Completed scans kept per workspace. One tool call often scans several pattern sets (a GUI
|
|
15
16
|
* preview scans layouts, linked definitions and localisation), so a single slot made every
|
|
@@ -17,7 +18,7 @@ import { ServiceError } from './result.js';
|
|
|
17
18
|
*/
|
|
18
19
|
const SCAN_CACHE_ENTRIES_PER_WORKSPACE = 4;
|
|
19
20
|
const RECOVERY_TTL_SECONDS = 3_600;
|
|
20
|
-
const RECOVERY_MAX_BYTES =
|
|
21
|
+
const RECOVERY_MAX_BYTES = TRANSACTION_MAX_JOURNAL_BYTES;
|
|
21
22
|
const RECOVERY_MAX_RECORDS = 128;
|
|
22
23
|
function abortReason(signal) {
|
|
23
24
|
return signal.reason instanceof Error
|
|
@@ -263,6 +263,10 @@ export function inspectImpactGraph(snapshot, query) {
|
|
|
263
263
|
queue.push({ key: consumerKey, depth: depth + 1 });
|
|
264
264
|
}
|
|
265
265
|
}
|
|
266
|
+
// A dynamic reference can only reach a symbol of the kind it names, so references to other
|
|
267
|
+
// kinds neither hide consumers of this selection nor make its evidence incomplete.
|
|
268
|
+
const traversedKinds = new Set([...visited].map((key) => key.slice(0, key.indexOf('\0'))));
|
|
269
|
+
const dynamicReferences = semantic.unresolved.filter(({ toKind }) => traversedKinds.has(toKind));
|
|
266
270
|
const { cycles, omittedCycles } = reachedCycleEdges(visited, [
|
|
267
271
|
...directConsumers,
|
|
268
272
|
...transitiveConsumers,
|
|
@@ -276,14 +280,14 @@ export function inspectImpactGraph(snapshot, query) {
|
|
|
276
280
|
omittedCycles === 0 &&
|
|
277
281
|
!stoppedAtDepth &&
|
|
278
282
|
unresolved.length === 0 &&
|
|
279
|
-
|
|
283
|
+
dynamicReferences.length === 0,
|
|
280
284
|
definitions,
|
|
281
285
|
missingSymbols,
|
|
282
286
|
directConsumers: directConsumers.sort(edgeOrder),
|
|
283
287
|
transitiveConsumers: transitiveConsumers.sort(edgeOrder),
|
|
284
288
|
affectedFiles: [...affectedFiles].sort(compareCodeUnits),
|
|
285
289
|
unresolved,
|
|
286
|
-
dynamicReferences
|
|
290
|
+
dynamicReferences,
|
|
287
291
|
cycles,
|
|
288
292
|
coverage: {
|
|
289
293
|
scannedSymbols: snapshot.index.symbols.length,
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { compareCodeUnits } from './canonical.js';
|
|
2
2
|
import { assignments, firstScalar, nodeLocation, parseClausewitz, } from './source/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Semantic references retained per workspace scan. A large mod with the base game holds several
|
|
5
|
+
* hundred thousand (one measured mod with vanilla held about 660,000), so the ceiling is the supported
|
|
6
|
+
* maximum rather than a fraction of it.
|
|
7
|
+
*/
|
|
8
|
+
export const IMPACT_SEMANTIC_MAX_REFERENCES = 1_000_000;
|
|
3
9
|
const eventKeys = new Set([
|
|
4
10
|
'country_event',
|
|
5
11
|
'news_event',
|
|
@@ -120,8 +126,10 @@ function staticTarget(value) {
|
|
|
120
126
|
!/^(?:ROOT|FROM|PREV|THIS)$/iu.test(value));
|
|
121
127
|
}
|
|
122
128
|
/** Extracts typed script edges while preserving unknown dynamic references. */
|
|
123
|
-
export function scanImpactSemanticReferences(snapshot, maxReferences =
|
|
124
|
-
if (!Number.isSafeInteger(maxReferences) ||
|
|
129
|
+
export function scanImpactSemanticReferences(snapshot, maxReferences = IMPACT_SEMANTIC_MAX_REFERENCES, options = {}) {
|
|
130
|
+
if (!Number.isSafeInteger(maxReferences) ||
|
|
131
|
+
maxReferences < 1 ||
|
|
132
|
+
maxReferences > IMPACT_SEMANTIC_MAX_REFERENCES)
|
|
125
133
|
throw new RangeError('Semantic reference limit is outside the supported range');
|
|
126
134
|
const references = [];
|
|
127
135
|
const unresolved = [];
|
|
@@ -134,6 +142,7 @@ export function scanImpactSemanticReferences(snapshot, maxReferences = 200_000,
|
|
|
134
142
|
path: file.displayPath,
|
|
135
143
|
location: nodeLocation(document, node, owner.id),
|
|
136
144
|
reason: `Dynamic ${referenceKind} target ${target} cannot be resolved statically`,
|
|
145
|
+
toKind,
|
|
137
146
|
});
|
|
138
147
|
else
|
|
139
148
|
complete = false;
|
|
@@ -192,6 +201,7 @@ export function scanImpactSemanticReferences(snapshot, maxReferences = 200_000,
|
|
|
192
201
|
path: file.displayPath,
|
|
193
202
|
location: nodeLocation(document, assignment, current.id),
|
|
194
203
|
reason: `Cannot identify ${key} state key`,
|
|
204
|
+
toKind: stateOperation.kind,
|
|
195
205
|
});
|
|
196
206
|
else
|
|
197
207
|
complete = false;
|
|
@@ -1440,6 +1440,7 @@ export class SymbolIndex {
|
|
|
1440
1440
|
...(reference.location === undefined ? {} : { location: reference.location }),
|
|
1441
1441
|
details: {
|
|
1442
1442
|
referenceKind: reference.kind,
|
|
1443
|
+
toKind: reference.toKind,
|
|
1443
1444
|
from: reference.from,
|
|
1444
1445
|
...(mayResolveFromSkippedSource ? { skippedSourceCount: this.#skippedSourceCount } : {}),
|
|
1445
1446
|
},
|
|
@@ -34,6 +34,8 @@ export class JobOperations {
|
|
|
34
34
|
mutation: true,
|
|
35
35
|
prepare: async (input, context) => {
|
|
36
36
|
const prepared = await prepare(schema.parse(input), context);
|
|
37
|
+
if ('readOnlyPreview' in prepared)
|
|
38
|
+
return { readOnlyPreview: jsonObject.parse(prepared.readOnlyPreview) };
|
|
37
39
|
if (!('transaction' in prepared))
|
|
38
40
|
return { transaction: prepared };
|
|
39
41
|
return {
|
|
@@ -161,6 +163,13 @@ export class JobExecutor {
|
|
|
161
163
|
if (claimed.record.transaction !== undefined)
|
|
162
164
|
return writes.reconcile(workspaceId, id, owner.token, principal, undefined, true, (execution, reconciled, recipe) => operation.complete(initial.request.arguments, execution, reconciled, recipe, context));
|
|
163
165
|
const prepared = await operation.prepare(initial.request.arguments, context);
|
|
166
|
+
if ('readOnlyPreview' in prepared) {
|
|
167
|
+
controller.signal.throwIfAborted();
|
|
168
|
+
return this.jobs.store.updateOwned(scope, id, owner.token, {
|
|
169
|
+
status: 'completed',
|
|
170
|
+
result: prepared.readOnlyPreview,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
164
173
|
return writes.execute(workspaceId, id, owner.token, prepared.transaction, principal, controller.signal, prepared.recipe, (execution, reconciled, recipe) => operation.complete(initial.request.arguments, execution, reconciled, recipe, context));
|
|
165
174
|
}
|
|
166
175
|
const result = jsonObject.parse(await withAnalysisCheckpoints(jobAnalysisCheckpoints(context), () => operation.run(initial.request.arguments, context)));
|