hoi4-agent-tools 3.9.0 → 4.0.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 +118 -0
- package/README.md +115 -147
- package/dist/bin/http.js +12 -0
- package/dist/bin/setup.js +3 -2
- package/dist/bin/stdio.js +12 -0
- package/dist/hoi4_agent_tools/core/artifacts.js +40 -5
- package/dist/hoi4_agent_tools/core/canonical.js +18 -4
- package/dist/hoi4_agent_tools/core/configuration.js +7 -0
- package/dist/hoi4_agent_tools/core/domain-scan-patterns.js +12 -0
- package/dist/hoi4_agent_tools/core/domain-tools.js +65 -0
- package/dist/hoi4_agent_tools/core/domain-validation.js +5 -2
- package/dist/hoi4_agent_tools/core/engine.js +56 -10
- package/dist/hoi4_agent_tools/core/index.js +45 -20
- package/dist/hoi4_agent_tools/core/job-executor.js +14 -1
- package/dist/hoi4_agent_tools/core/job-operation-registration.js +15 -7
- package/dist/hoi4_agent_tools/core/job-store.js +78 -5
- package/dist/hoi4_agent_tools/core/job-transactions.js +31 -2
- package/dist/hoi4_agent_tools/core/job-worker-host.js +215 -116
- package/dist/hoi4_agent_tools/core/job-worker.js +90 -36
- package/dist/hoi4_agent_tools/core/operation-result.js +123 -10
- package/dist/hoi4_agent_tools/core/operation-tasks.js +49 -89
- package/dist/hoi4_agent_tools/core/persistent-analysis-cache.js +25 -21
- package/dist/hoi4_agent_tools/core/server-registry.js +155 -0
- package/dist/hoi4_agent_tools/core/server-status-resource.js +22 -0
- package/dist/hoi4_agent_tools/core/shared-request-capacity.js +26 -1
- package/dist/hoi4_agent_tools/core/source/cache.js +108 -29
- package/dist/hoi4_agent_tools/core/source/lexer.js +11 -3
- package/dist/hoi4_agent_tools/core/transactions.js +114 -23
- package/dist/hoi4_agent_tools/core/workspace.js +84 -8
- package/dist/hoi4_agent_tools/decision/ai.js +1 -1
- package/dist/hoi4_agent_tools/decision/service.js +28 -0
- package/dist/hoi4_agent_tools/decision/source-inventory.js +7 -5
- package/dist/hoi4_agent_tools/event/job-operations.js +9 -5
- package/dist/hoi4_agent_tools/event/operations.js +14 -1
- package/dist/hoi4_agent_tools/event/service.js +7 -0
- package/dist/hoi4_agent_tools/focus/job-operations.js +1 -2
- package/dist/hoi4_agent_tools/focus/tool-operations.js +1 -1
- package/dist/hoi4_agent_tools/gui/scenario-generator.js +11 -3
- package/dist/hoi4_agent_tools/gui/studio.js +20 -7
- package/dist/hoi4_agent_tools/gui/validators.js +8 -6
- package/dist/hoi4_agent_tools/map/bmp.js +1 -1
- package/dist/hoi4_agent_tools/map/derived.js +748 -0
- package/dist/hoi4_agent_tools/map/job-operations.js +7 -3
- package/dist/hoi4_agent_tools/map/model.js +103 -0
- package/dist/hoi4_agent_tools/map/operations.js +427 -58
- package/dist/hoi4_agent_tools/map/references.js +483 -0
- package/dist/hoi4_agent_tools/map/service.js +136 -61
- package/dist/hoi4_agent_tools/map/tool-operations.js +2 -1
- package/dist/hoi4_agent_tools/map/validation.js +3 -0
- package/dist/hoi4_agent_tools/map/world.js +1280 -0
- package/dist/hoi4_agent_tools/mcp/resources/register-modern.js +13 -1
- package/dist/hoi4_agent_tools/mcp/resources/register.js +6 -0
- package/dist/hoi4_agent_tools/mcp/server/argument-errors.js +72 -0
- package/dist/hoi4_agent_tools/mcp/server/create-modern-operations.js +23 -5
- package/dist/hoi4_agent_tools/mcp/server/create.js +7 -0
- package/dist/hoi4_agent_tools/mcp/server/request-lifecycle.js +24 -30
- package/dist/hoi4_agent_tools/mcp/server/tool-listing.js +45 -0
- package/dist/hoi4_agent_tools/mcp/tools/analysis.js +25 -5
- package/dist/hoi4_agent_tools/mcp/tools/event.js +18 -29
- package/dist/hoi4_agent_tools/mcp/tools/focus.js +4 -12
- package/dist/hoi4_agent_tools/mcp/tools/gui.js +3 -3
- package/dist/hoi4_agent_tools/mcp/tools/job.js +2 -2
- package/dist/hoi4_agent_tools/mcp/tools/map.js +13 -16
- package/dist/hoi4_agent_tools/mcp/tools/probability.js +42 -48
- package/dist/hoi4_agent_tools/mcp/tools/reference.js +22 -5
- package/dist/hoi4_agent_tools/mcp/tools/technology.js +16 -26
- package/dist/hoi4_agent_tools/mcp/transports/http.js +28 -4
- package/dist/hoi4_agent_tools/mcp/transports/stdio-lifetime.js +37 -18
- package/dist/hoi4_agent_tools/probability/job-operations.js +4 -9
- package/dist/hoi4_agent_tools/probability/operations.js +175 -13
- package/dist/hoi4_agent_tools/probability/source-analysis.js +24 -1
- package/dist/hoi4_agent_tools/reference/error-log.js +359 -0
- package/dist/hoi4_agent_tools/reference/known-mistakes.js +307 -0
- package/dist/hoi4_agent_tools/reference/known-mistakes.json +154 -0
- package/dist/hoi4_agent_tools/reference/mod-index.js +101 -0
- package/dist/hoi4_agent_tools/reference/operations.js +88 -19
- package/dist/hoi4_agent_tools/reference/player-text.js +4 -3
- package/dist/hoi4_agent_tools/reference/script-validation.js +19 -1
- package/dist/hoi4_agent_tools/reference/service.js +6 -0
- package/dist/hoi4_agent_tools/schemas/event.js +78 -19
- package/dist/hoi4_agent_tools/schemas/focus-requests.js +10 -0
- package/dist/hoi4_agent_tools/schemas/lenient-arguments.js +266 -0
- package/dist/hoi4_agent_tools/schemas/map-requests.js +63 -20
- package/dist/hoi4_agent_tools/schemas/map.js +122 -3
- package/dist/hoi4_agent_tools/schemas/mod-tools.js +181 -0
- package/dist/hoi4_agent_tools/schemas/probability-requests.js +119 -9
- package/dist/hoi4_agent_tools/schemas/reference.js +13 -0
- package/dist/hoi4_agent_tools/schemas/scenarios.js +1 -3
- package/dist/hoi4_agent_tools/schemas/script-validation.js +13 -6
- package/dist/hoi4_agent_tools/schemas/technology.js +83 -23
- package/dist/hoi4_agent_tools/technology/job-operations.js +3 -4
- package/dist/hoi4_agent_tools/technology/operations.js +14 -1
- package/dist/hoi4_agent_tools/technology/service.js +7 -0
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/README.md +7 -4
- package/docs/analysis.md +13 -0
- package/docs/configuration.md +281 -0
- package/docs/development.md +24 -2
- package/docs/events.md +27 -3
- package/docs/examples.md +6 -6
- package/docs/focus.md +125 -4
- package/docs/gui.md +98 -17
- package/docs/helper-expansion.md +8 -8
- package/docs/http.md +3 -1
- package/docs/jobs.md +19 -13
- package/docs/map.md +197 -6
- package/docs/mechanics.md +4 -0
- package/docs/probability.md +42 -15
- package/docs/reference.md +71 -4
- package/docs/setup.md +45 -11
- package/docs/technology.md +26 -1
- package/docs/third-party-notices.md +11 -0
- package/docs/tools.md +151 -42
- package/docs/troubleshooting.md +61 -0
- package/examples/clients/codex-config.toml +5 -2
- package/examples/clients/generic.json +2 -2
- package/examples/config/config.json +7 -0
- package/npm-shrinkwrap.json +161 -162
- package/package.json +13 -8
- package/schemas/configuration.schema.json +144 -128
- 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 +55 -0
- 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 +597 -126
- package/schemas/mechanic-test-input.schema.json +1 -1
- package/schemas/mod-index-input.schema.json +51 -0
- package/schemas/operation-result.schema.json +2 -2
- 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 +3 -5
- package/schemas/scenario-test-input.schema.json +3 -5
- 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 +3 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { emptyServiceResult } from '../core/result.js';
|
|
2
2
|
import { setInlineFilesScanned } from '../core/operation-result.js';
|
|
3
|
-
import { probabilityCompareRequestSchema, probabilityEvaluateRequestSchema, probabilityInspectRequestSchema, probabilityRenderRequestSchema, probabilitySequenceRequestSchema, probabilitySimulateRequestSchema, probabilitySweepRequestSchema, } from '../schemas/probability-requests.js';
|
|
3
|
+
import { probabilityCompareRequestSchema, probabilityEvaluateRequestSchema, probabilityInspectRequestSchema, probabilityRenderRequestSchema, probabilitySequenceRequestSchema, probabilitySimulateRequestSchema, probabilitySweepRequestSchema, probabilityAnalyzeRequestSchema, splitProbabilityAnalyzeRequest, } from '../schemas/probability-requests.js';
|
|
4
4
|
function runtime(context, refresh) {
|
|
5
5
|
return {
|
|
6
6
|
workspaceId: context.workspaceId,
|
|
@@ -9,6 +9,83 @@ function runtime(context, refresh) {
|
|
|
9
9
|
...(refresh === undefined ? {} : { refresh }),
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
|
+
const INLINE_RANKING_SCENARIOS = 4;
|
|
13
|
+
const INLINE_RANKING_CANDIDATES = 12;
|
|
14
|
+
const INLINE_MISSING_INPUTS = 16;
|
|
15
|
+
const INLINE_COMPARISON_CHANGES = 12;
|
|
16
|
+
function rounded(value, digits = 6) {
|
|
17
|
+
if (value === null || value === undefined || !Number.isFinite(value))
|
|
18
|
+
return null;
|
|
19
|
+
const scale = 10 ** digits;
|
|
20
|
+
return Math.round(value * scale) / scale;
|
|
21
|
+
}
|
|
22
|
+
function compareIds(left, right) {
|
|
23
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
24
|
+
}
|
|
25
|
+
/** The answer itself, inline: the strongest candidates of the first scenarios. */
|
|
26
|
+
function inlineRanking(result) {
|
|
27
|
+
return result.scenarios.slice(0, INLINE_RANKING_SCENARIOS).map((scenario) => {
|
|
28
|
+
const ranked = [...scenario.candidates].sort((left, right) => (right.conditionalProbability ?? -1) - (left.conditionalProbability ?? -1) ||
|
|
29
|
+
(right.rawValue?.value ?? Number.NEGATIVE_INFINITY) -
|
|
30
|
+
(left.rawValue?.value ?? Number.NEGATIVE_INFINITY) ||
|
|
31
|
+
compareIds(left.id, right.id));
|
|
32
|
+
return {
|
|
33
|
+
scenarioId: scenario.id,
|
|
34
|
+
poolComplete: scenario.poolComplete,
|
|
35
|
+
candidates: ranked.slice(0, INLINE_RANKING_CANDIDATES).map((candidate) => ({
|
|
36
|
+
id: candidate.id,
|
|
37
|
+
eligible: candidate.eligibility,
|
|
38
|
+
weight: rounded(candidate.rawValue?.value),
|
|
39
|
+
...((candidate.rawValue === undefined || candidate.rawValue === null) &&
|
|
40
|
+
candidate.rawInterval !== undefined
|
|
41
|
+
? {
|
|
42
|
+
weightRange: [rounded(candidate.rawInterval.min), rounded(candidate.rawInterval.max)],
|
|
43
|
+
}
|
|
44
|
+
: {}),
|
|
45
|
+
probability: rounded(candidate.conditionalProbability),
|
|
46
|
+
...(candidate.effectiveMtthDays === undefined
|
|
47
|
+
? {}
|
|
48
|
+
: { mtthDays: rounded(candidate.effectiveMtthDays, 2) }),
|
|
49
|
+
})),
|
|
50
|
+
...(ranked.length > INLINE_RANKING_CANDIDATES
|
|
51
|
+
? { omittedCandidates: ranked.length - INLINE_RANKING_CANDIDATES }
|
|
52
|
+
: {}),
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/** Facts the scenarios did not declare but a condition or value needs. */
|
|
57
|
+
function missingInputs(result) {
|
|
58
|
+
const inputs = new Set();
|
|
59
|
+
for (const { code, path, details } of result.unresolved) {
|
|
60
|
+
if (typeof details?.scenarioInput === 'string')
|
|
61
|
+
inputs.add(details.scenarioInput);
|
|
62
|
+
else if (code === 'VALUE_UNRESOLVED' && path !== undefined && !/^(?:@|constant:)/u.test(path))
|
|
63
|
+
inputs.add(path);
|
|
64
|
+
}
|
|
65
|
+
return [...inputs].sort(compareIds).slice(0, INLINE_MISSING_INPUTS);
|
|
66
|
+
}
|
|
67
|
+
function inlineComparison(result) {
|
|
68
|
+
if (result.comparison === undefined)
|
|
69
|
+
return [];
|
|
70
|
+
return [...result.comparison.scenarioChanges]
|
|
71
|
+
.sort((left, right) => Math.abs(right.probabilityDelta ?? 0) - Math.abs(left.probabilityDelta ?? 0) ||
|
|
72
|
+
Math.abs(right.rawDelta ?? 0) - Math.abs(left.rawDelta ?? 0) ||
|
|
73
|
+
compareIds(left.candidateId, right.candidateId))
|
|
74
|
+
.slice(0, INLINE_COMPARISON_CHANGES)
|
|
75
|
+
.map((change) => ({
|
|
76
|
+
scenarioId: change.scenarioId,
|
|
77
|
+
candidateId: change.candidateId,
|
|
78
|
+
...(change.rawDelta === undefined ? {} : { weightDelta: rounded(change.rawDelta) }),
|
|
79
|
+
...(change.probabilityDelta === undefined
|
|
80
|
+
? {}
|
|
81
|
+
: { probabilityDelta: rounded(change.probabilityDelta) }),
|
|
82
|
+
...(change.eligibilityChange === undefined
|
|
83
|
+
? {}
|
|
84
|
+
: {
|
|
85
|
+
eligibility: `${change.eligibilityChange.before}->${change.eligibilityChange.after}`,
|
|
86
|
+
}),
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
12
89
|
export function probabilityAnalysisData(result) {
|
|
13
90
|
return {
|
|
14
91
|
operation: result.operation,
|
|
@@ -41,6 +118,9 @@ export function probabilityAnalysisData(result) {
|
|
|
41
118
|
: { comparisonChanges: result.comparison.scenarioChanges.length }),
|
|
42
119
|
visualResources: result.resources.filter(({ mimeType }) => mimeType.startsWith('image/'))
|
|
43
120
|
.length,
|
|
121
|
+
ranking: inlineRanking(result),
|
|
122
|
+
missingInputs: missingInputs(result),
|
|
123
|
+
...(result.comparison === undefined ? {} : { changes: inlineComparison(result) }),
|
|
44
124
|
};
|
|
45
125
|
}
|
|
46
126
|
export function probabilityAnalysisServiceResult(workspaceId, result) {
|
|
@@ -88,19 +168,82 @@ export function normalizeProbabilityCompareRequest(input) {
|
|
|
88
168
|
export function normalizeProbabilityRenderRequest(input) {
|
|
89
169
|
return probabilityRenderRequestSchema.parse(input);
|
|
90
170
|
}
|
|
171
|
+
const DEFAULT_SCENARIO_SET = {
|
|
172
|
+
schemaVersion: '1.0',
|
|
173
|
+
id: 'source-defaults',
|
|
174
|
+
description: 'One scenario with no declared facts; conditions that need facts stay unresolved.',
|
|
175
|
+
scenarios: [{ id: 'no-facts', state: {} }],
|
|
176
|
+
};
|
|
177
|
+
/** Discover the weighted surface and, unless evaluate is false, evaluate it in the same call. */
|
|
91
178
|
export async function inspectProbabilities(analyzer, input, context) {
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
179
|
+
let source = input.source;
|
|
180
|
+
const candidatePool = input.candidatePool ?? [];
|
|
181
|
+
// A pool alone names what to inspect: its first ID finds the defining file.
|
|
182
|
+
if (source === undefined && input.customPoolManifest === undefined && candidatePool.length > 0)
|
|
183
|
+
source = { identifier: candidatePool[0] };
|
|
184
|
+
const customPoolManifest = input.customPoolManifest;
|
|
185
|
+
let inspected = await analyzer.inspect(runtime(context, input.refresh), input.adapter, source, candidatePool, customPoolManifest);
|
|
186
|
+
// A wrong adapter guess, such as a mission named as a decision, should not cost another call.
|
|
187
|
+
const suggested = inspected.discovery?.suggestedAdapter;
|
|
188
|
+
if (input.evaluate !== false &&
|
|
189
|
+
inspected.surface === undefined &&
|
|
190
|
+
suggested !== undefined &&
|
|
191
|
+
suggested !== input.adapter &&
|
|
192
|
+
(inspected.discovery?.reason === 'candidate_pool_not_found' ||
|
|
193
|
+
inspected.discovery?.reason === 'requested_adapter_empty' ||
|
|
194
|
+
inspected.discovery?.reason === 'identifier_not_found'))
|
|
195
|
+
inspected = await analyzer.inspect(runtime(context), suggested, source, candidatePool, customPoolManifest);
|
|
196
|
+
const surface = inspected.surface;
|
|
197
|
+
if (input.evaluate !== false &&
|
|
198
|
+
surface !== undefined &&
|
|
199
|
+
(source !== undefined || customPoolManifest !== undefined)) {
|
|
200
|
+
const analyzed = await analyzer.evaluate({
|
|
201
|
+
...runtime(context),
|
|
202
|
+
adapter: surface.adapter.id,
|
|
203
|
+
...(source === undefined ? {} : { source }),
|
|
204
|
+
...(customPoolManifest === undefined ? {} : { customPoolManifest }),
|
|
205
|
+
...(candidatePool.length === 0 ? {} : { candidatePool }),
|
|
206
|
+
scenarioSet: (input.scenarioSet ??
|
|
207
|
+
DEFAULT_SCENARIO_SET),
|
|
208
|
+
...(input.horizonDays === undefined ? {} : { horizonDays: input.horizonDays }),
|
|
209
|
+
...(input.metrics === undefined ? {} : { metrics: input.metrics }),
|
|
210
|
+
...(input.acceptanceBands === undefined
|
|
211
|
+
? {}
|
|
212
|
+
: {
|
|
213
|
+
acceptanceBands: input.acceptanceBands,
|
|
214
|
+
}),
|
|
215
|
+
...(input.diagnosticThresholds === undefined
|
|
216
|
+
? {}
|
|
217
|
+
: {
|
|
218
|
+
diagnosticThresholds: input.diagnosticThresholds,
|
|
219
|
+
}),
|
|
220
|
+
...(input.outputs === undefined ? {} : { outputs: input.outputs }),
|
|
221
|
+
});
|
|
222
|
+
const result = probabilityAnalysisServiceResult(context.workspaceId, analyzed);
|
|
223
|
+
result.artifacts = [...result.artifacts, ...inspected.artifacts];
|
|
224
|
+
if (input.scenarioSet === undefined)
|
|
225
|
+
result.diagnostics = [
|
|
226
|
+
{
|
|
227
|
+
code: 'PROBABILITY_DEFAULT_SCENARIO',
|
|
228
|
+
severity: 'info',
|
|
229
|
+
category: 'configuration',
|
|
230
|
+
message: 'Evaluated without declared facts; pass scenarioSet with the facts in missingInputs to resolve the remaining conditions',
|
|
231
|
+
},
|
|
232
|
+
...result.diagnostics,
|
|
233
|
+
];
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
const sourceRequiredInputPaths = (surface?.requiredInputs ?? []).filter((path) => path !== 'focus.external_factors_complete' && path !== 'technology.external_factors_complete');
|
|
237
|
+
const adapterRequiredInputPaths = (surface?.requiredInputs ?? []).filter((path) => path === 'focus.external_factors_complete' || path === 'technology.external_factors_complete');
|
|
95
238
|
const result = emptyServiceResult(context.workspaceId, {
|
|
96
239
|
adapters: inspected.adapters.length,
|
|
97
|
-
...(
|
|
240
|
+
...(surface === undefined
|
|
98
241
|
? {}
|
|
99
242
|
: {
|
|
100
|
-
adapterId:
|
|
101
|
-
sourceRevision:
|
|
102
|
-
sourceHash:
|
|
103
|
-
poolComplete:
|
|
243
|
+
adapterId: surface.adapter.id,
|
|
244
|
+
sourceRevision: surface.sourceRevision,
|
|
245
|
+
sourceHash: surface.sourceHash,
|
|
246
|
+
poolComplete: surface.poolComplete,
|
|
104
247
|
}),
|
|
105
248
|
...(inspected.discovery === undefined
|
|
106
249
|
? {}
|
|
@@ -115,7 +258,7 @@ export async function inspectProbabilities(analyzer, input, context) {
|
|
|
115
258
|
sourceRevision: inspected.discovery.sourceRevision,
|
|
116
259
|
sourceHash: inspected.discovery.sourceHash,
|
|
117
260
|
}),
|
|
118
|
-
candidates:
|
|
261
|
+
candidates: surface?.candidateCount ?? 0,
|
|
119
262
|
availableCandidates: inspected.discovery?.availableAdapters.reduce((sum, { candidateCount }) => sum + candidateCount, 0) ?? 0,
|
|
120
263
|
availableAdapters: inspected.discovery?.availableAdapters.map((available) => ({
|
|
121
264
|
adapterId: available.adapterId,
|
|
@@ -124,14 +267,14 @@ export async function inspectProbabilities(analyzer, input, context) {
|
|
|
124
267
|
candidatePoolMatches: available.candidatePoolMatchCount,
|
|
125
268
|
})) ?? [],
|
|
126
269
|
candidateExamples: inspected.discovery?.exampleCandidateIds ?? [],
|
|
127
|
-
requiredInputs:
|
|
270
|
+
requiredInputs: surface?.requiredInputs.length ?? 0,
|
|
128
271
|
requiredInputPaths: sourceRequiredInputPaths.slice(0, 32),
|
|
129
272
|
requiredInputPathsTruncated: sourceRequiredInputPaths.length > 32,
|
|
130
273
|
adapterRequiredInputPaths,
|
|
131
|
-
unresolved:
|
|
274
|
+
unresolved: surface?.unsupported.length ?? 0,
|
|
132
275
|
});
|
|
133
276
|
result.code =
|
|
134
|
-
|
|
277
|
+
surface !== undefined
|
|
135
278
|
? 'PROBABILITY_SOURCE_INSPECTED'
|
|
136
279
|
: inspected.discovery !== undefined
|
|
137
280
|
? 'PROBABILITY_SOURCE_DISCOVERED'
|
|
@@ -140,6 +283,25 @@ export async function inspectProbabilities(analyzer, input, context) {
|
|
|
140
283
|
setInlineFilesScanned(result, inspected.filesScanned);
|
|
141
284
|
return result;
|
|
142
285
|
}
|
|
286
|
+
/** Compare, sweep, simulate, sequence, or render through one entry point. */
|
|
287
|
+
export async function analyzeProbabilities(analyzer, input, context) {
|
|
288
|
+
const split = splitProbabilityAnalyzeRequest(input);
|
|
289
|
+
switch (split.analysis) {
|
|
290
|
+
case 'compare':
|
|
291
|
+
return compareProbabilities(analyzer, split.request, context);
|
|
292
|
+
case 'sweep':
|
|
293
|
+
return sweepProbabilities(analyzer, split.request, context);
|
|
294
|
+
case 'simulate':
|
|
295
|
+
return simulateProbabilities(analyzer, split.request, context);
|
|
296
|
+
case 'sequence':
|
|
297
|
+
return sequenceProbabilities(analyzer, split.request, context);
|
|
298
|
+
case 'render':
|
|
299
|
+
return renderProbabilities(analyzer, split.request, context);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
export function normalizeProbabilityAnalyzeRequest(input) {
|
|
303
|
+
return probabilityAnalyzeRequestSchema.parse(input);
|
|
304
|
+
}
|
|
143
305
|
export async function evaluateProbabilities(analyzer, input, context) {
|
|
144
306
|
const analyzed = await analyzer.evaluate({
|
|
145
307
|
...runtime(context, input.refresh),
|
|
@@ -388,6 +388,28 @@ function samePool(candidate, selected) {
|
|
|
388
388
|
return candidate.metadata.randomListId === selected.metadata.randomListId;
|
|
389
389
|
return candidate.id === selected.id;
|
|
390
390
|
}
|
|
391
|
+
/**
|
|
392
|
+
* A pool entry that names no candidate but names a categorical pool, such as an event ID for
|
|
393
|
+
* its options or a random_list ID, stands for every candidate of that pool.
|
|
394
|
+
*/
|
|
395
|
+
export function expandCandidatePool(candidates, candidatePool) {
|
|
396
|
+
if (candidatePool.length === 0)
|
|
397
|
+
return [];
|
|
398
|
+
const ids = new Set(candidates.map(({ id }) => id));
|
|
399
|
+
const expanded = new Set();
|
|
400
|
+
for (const entry of candidatePool) {
|
|
401
|
+
if (ids.has(entry)) {
|
|
402
|
+
expanded.add(entry);
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
const members = candidates.filter(({ metadata }) => metadata.eventId === entry || metadata.randomListId === entry);
|
|
406
|
+
if (members.length === 0)
|
|
407
|
+
expanded.add(entry);
|
|
408
|
+
for (const { id } of members)
|
|
409
|
+
expanded.add(id);
|
|
410
|
+
}
|
|
411
|
+
return [...expanded];
|
|
412
|
+
}
|
|
391
413
|
function candidatesAtSourceLine(candidates, adapterId, line) {
|
|
392
414
|
if (line === undefined)
|
|
393
415
|
return candidates;
|
|
@@ -421,11 +443,11 @@ function candidatesAtSourceLine(candidates, adapterId, line) {
|
|
|
421
443
|
}
|
|
422
444
|
export function inventoryWeightedSurfaces(snapshot, source, candidatePool = [], adapterIds = sourceBackedAdapterIds) {
|
|
423
445
|
const contexts = sourceContexts(snapshot, source);
|
|
424
|
-
const wanted = new Set(candidatePool);
|
|
425
446
|
const adapters = adapterIds
|
|
426
447
|
.filter((adapterId) => adapterId !== 'custom_weighted_pool')
|
|
427
448
|
.map((adapterId) => {
|
|
428
449
|
const candidates = candidatesAtSourceLine(contexts.flatMap((context) => candidatesFor(context, adapterId)), adapterId, source.line).sort((left, right) => compareCodeUnits(left.id, right.id));
|
|
450
|
+
const wanted = new Set(expandCandidatePool(candidates, candidatePool));
|
|
429
451
|
return {
|
|
430
452
|
adapterId,
|
|
431
453
|
candidateCount: candidates.length,
|
|
@@ -457,6 +479,7 @@ export function discoverWeightedSurface(snapshot, adapterId, source, candidatePo
|
|
|
457
479
|
let candidates = contexts.flatMap((context) => candidatesFor(context, adapterId));
|
|
458
480
|
candidates.sort((left, right) => compareCodeUnits(left.id, right.id));
|
|
459
481
|
candidates = candidatesAtSourceLine(candidates, adapterId, source.line);
|
|
482
|
+
candidatePool = expandCandidatePool(candidates, candidatePool);
|
|
460
483
|
const localCategoricalPool = adapterId === 'event_option_ai_chance' || adapterId === 'random_list';
|
|
461
484
|
let completeLocalPoolIds;
|
|
462
485
|
if (source.identifier !== undefined) {
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir, open, readFile, rename, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { ServiceError } from '../core/result.js';
|
|
6
|
+
import { knownMistakeForLogMessage } from './known-mistakes.js';
|
|
7
|
+
/** The newest part of a very large log is read; the game writes it anew on every launch. */
|
|
8
|
+
const MAX_LOG_BYTES = 16 * 1024 * 1024;
|
|
9
|
+
const MAX_MESSAGE_CHARACTERS = 600;
|
|
10
|
+
const SOURCE_EXTENSIONS = 'txt|gui|gfx|yml|csv|asset|lua|mod|shader|fxh|json|map|bmp|dds|tga|png';
|
|
11
|
+
const headerPattern = /^\[(\d{2}:\d{2}:\d{2})\]\[([^\]:]+?)(?:\.cpp)?(?::\d+)?\]:\s?(.*)$/u;
|
|
12
|
+
const quotedFilePattern = new RegExp(`(?:in |)file:?\\s*"([^"]+\\.(?:${SOURCE_EXTENSIONS}))"`, 'giu');
|
|
13
|
+
const barePathPattern = new RegExp(`((?:[A-Za-z]:)?[\\w .\\-/\\\\]*?(?:common|events|history|interface|gfx|localisation|localisation_synced|map|music|sound|portraits|tutorial)[/\\\\][^\\s"',;:]+?\\.(?:${SOURCE_EXTENSIONS}))\\b`, 'gu');
|
|
14
|
+
const linePattern = /(?:near\s+)?line:?\s*(\d{1,7})/giu;
|
|
15
|
+
/** Rules in order; the first that matches classifies an entry. */
|
|
16
|
+
const categoryRules = [
|
|
17
|
+
[
|
|
18
|
+
'syntax',
|
|
19
|
+
/unexpected (?:token|end)|missing (?:closing |opening )?(?:brace|bracket)|parse error|malformed|unbalanced/iu,
|
|
20
|
+
],
|
|
21
|
+
[
|
|
22
|
+
'duplicate',
|
|
23
|
+
/duplicate|already (?:exists|defined|registered)|defined (?:twice|multiple times)/iu,
|
|
24
|
+
],
|
|
25
|
+
['localisation', /locali[sz]/iu],
|
|
26
|
+
['effect', /\beffect\b/iu],
|
|
27
|
+
['trigger', /\btrigger\b|\bcondition\b/iu],
|
|
28
|
+
['scope', /\bscope\b/iu],
|
|
29
|
+
['graphics', /texture|sprite|\.dds|\.tga|\.png|gfx|\bfont\b|\bmesh\b|\bentity\b|animation/iu],
|
|
30
|
+
['interface', /\bgui\b|window|\bwidget\b|scripted ?gui/iu],
|
|
31
|
+
['map', /province|strategic ?region|\bstate\b|adjacenc|supply|railway|\bmap\b/iu],
|
|
32
|
+
[
|
|
33
|
+
'missing_reference',
|
|
34
|
+
/not found|unknown|doesn'?t exist|does not exist|invalid|missing|could not find|failed to (?:find|load)/iu,
|
|
35
|
+
],
|
|
36
|
+
];
|
|
37
|
+
const moduleCategories = [
|
|
38
|
+
[/locali[sz]/iu, 'localisation'],
|
|
39
|
+
[/^(?:effect|effectbase|effects)/iu, 'effect'],
|
|
40
|
+
[/^(?:trigger|triggerbase|triggers)/iu, 'trigger'],
|
|
41
|
+
[/gfx|texture|sprite|font|graphic/iu, 'graphics'],
|
|
42
|
+
[/gui|window|widget/iu, 'interface'],
|
|
43
|
+
[/province|map|state|region|supply|railway|adjacenc/iu, 'map'],
|
|
44
|
+
[/persistent|parse|pdx_?data|script/iu, 'syntax'],
|
|
45
|
+
];
|
|
46
|
+
/** What to do about each kind of entry; names the tool that narrows it down. */
|
|
47
|
+
const categoryHints = {
|
|
48
|
+
syntax: 'Check braces, quotes and assignments near the named line; hoi4.script_validate with the file path pinpoints it.',
|
|
49
|
+
duplicate: 'Two active definitions share a name: rename one, or remove the copy that overrides the other.',
|
|
50
|
+
localisation: 'Add the key to a UTF-8 with BOM .yml under localisation/<language>/ (key: "Text", no :0), or correct the key name.',
|
|
51
|
+
effect: 'Check that the effect exists and takes these arguments in this scope: hoi4.script_validate on the file, hoi4.reference_search for its documentation.',
|
|
52
|
+
trigger: 'Check that the trigger exists and takes these arguments in this scope: hoi4.script_validate on the file, hoi4.reference_search for its documentation.',
|
|
53
|
+
scope: 'A command runs in the wrong scope: check the enclosing scope changes with hoi4.script_validate.',
|
|
54
|
+
graphics: 'Register the sprite in a .gfx file, and check the texture path, file name case and DDS format.',
|
|
55
|
+
interface: 'Check the .gui element and sprite names; hoi4.gui_inspect shows the window and its missing assets.',
|
|
56
|
+
map: 'Check the named province, state or region with hoi4.map_inspect; map errors can stop the game loading without debug mode.',
|
|
57
|
+
missing_reference: 'The named object is not defined in any active layer: hoi4.source_lookup confirms, then define it or fix the name.',
|
|
58
|
+
other: 'Read the message and the named file; hoi4.source_lookup locates the definition.',
|
|
59
|
+
};
|
|
60
|
+
/** Reduce a message to its shape: names, numbers, quoted values and paths become placeholders. */
|
|
61
|
+
export function errorLogPattern(message) {
|
|
62
|
+
return (message
|
|
63
|
+
.toLowerCase()
|
|
64
|
+
.replace(/"[^"]*"/gu, '<v>')
|
|
65
|
+
.replace(new RegExp(`[\\w./\\\\:-]+\\.(?:${SOURCE_EXTENSIONS})\\b`, 'gu'), '<file>')
|
|
66
|
+
.replace(/\b[a-z_][\w]*\.\d+\b/gu, '<id>')
|
|
67
|
+
// snake_case script names (keys, effects, sprites) differ between otherwise equal messages.
|
|
68
|
+
.replace(/\b[a-z0-9]+(?:_[a-z0-9]+)+\b/gu, '<name>')
|
|
69
|
+
.replace(/-?\b\d+(?:\.\d+)?\b/gu, '<n>')
|
|
70
|
+
.replace(/\s+/gu, ' ')
|
|
71
|
+
.trim()
|
|
72
|
+
.slice(0, 300));
|
|
73
|
+
}
|
|
74
|
+
const HISTORY_FILE = 'error-log-history.json';
|
|
75
|
+
const MAX_HISTORY_ENTRIES = 5_000;
|
|
76
|
+
function entryKey(module, message) {
|
|
77
|
+
return createHash('sha256').update(`${module}\0${message}`).digest('hex').slice(0, 24);
|
|
78
|
+
}
|
|
79
|
+
async function readHistory(file) {
|
|
80
|
+
try {
|
|
81
|
+
const parsed = JSON.parse(await readFile(file, 'utf8'));
|
|
82
|
+
return typeof parsed === 'object' && parsed !== null ? parsed : {};
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return {};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Remember the entries of each log this server reads, keyed by the log's write time, so the
|
|
90
|
+
* next launch's log can say which errors are new and which a fix removed. The history lives
|
|
91
|
+
* in the server's own cache folder, never in the mod.
|
|
92
|
+
*/
|
|
93
|
+
async function compareWithPreviousLaunch(workspace, modifiedAt, entries) {
|
|
94
|
+
const file = path.join(workspace.cacheRoot, HISTORY_FILE);
|
|
95
|
+
const history = await readHistory(file);
|
|
96
|
+
const current = { modifiedAt, entries: {} };
|
|
97
|
+
for (const entry of entries.slice(0, MAX_HISTORY_ENTRIES))
|
|
98
|
+
current.entries[entryKey(entry.module, entry.message)] = {
|
|
99
|
+
category: entry.category,
|
|
100
|
+
message: entry.message.slice(0, 300),
|
|
101
|
+
...(entry.relativePath === undefined ? {} : { relativePath: entry.relativePath }),
|
|
102
|
+
};
|
|
103
|
+
if (history.current?.modifiedAt === modifiedAt)
|
|
104
|
+
return { ...(history.previous === undefined ? {} : { previous: history.previous }), current };
|
|
105
|
+
const next = {
|
|
106
|
+
...(history.current === undefined ? {} : { previous: history.current }),
|
|
107
|
+
current,
|
|
108
|
+
};
|
|
109
|
+
try {
|
|
110
|
+
await mkdir(workspace.cacheRoot, { recursive: true });
|
|
111
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
112
|
+
await writeFile(temporary, JSON.stringify(next));
|
|
113
|
+
await rename(temporary, file);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// The comparison is a convenience; an unwritable cache leaves the read itself intact.
|
|
117
|
+
}
|
|
118
|
+
return { ...(next.previous === undefined ? {} : { previous: next.previous }), current };
|
|
119
|
+
}
|
|
120
|
+
function classify(module, message) {
|
|
121
|
+
for (const [category, pattern] of categoryRules)
|
|
122
|
+
if (category !== 'missing_reference' && pattern.test(message))
|
|
123
|
+
return category;
|
|
124
|
+
for (const [pattern, category] of moduleCategories)
|
|
125
|
+
if (pattern.test(module))
|
|
126
|
+
return category;
|
|
127
|
+
return categoryRules.at(-1)[1].test(message) ? 'missing_reference' : 'other';
|
|
128
|
+
}
|
|
129
|
+
/** The reviewed known mistake a logged message reports, when there is one. */
|
|
130
|
+
function knownMistakeOf(message) {
|
|
131
|
+
const rule = knownMistakeForLogMessage(message);
|
|
132
|
+
return rule === undefined ? {} : { knownMistake: { id: rule.id, fix: rule.fix } };
|
|
133
|
+
}
|
|
134
|
+
/** The user-data folder that holds `logs/`, in order of preference. */
|
|
135
|
+
function userDataCandidates(workspace, configured) {
|
|
136
|
+
const candidates = [];
|
|
137
|
+
if (configured !== undefined)
|
|
138
|
+
candidates.push(path.resolve(configured));
|
|
139
|
+
const modParent = path.dirname(workspace.modRoot);
|
|
140
|
+
if (path.basename(modParent).toLowerCase() === 'mod')
|
|
141
|
+
candidates.push(path.dirname(modParent));
|
|
142
|
+
const home = os.homedir();
|
|
143
|
+
const userFolder = path.join('Paradox Interactive', 'Hearts of Iron IV');
|
|
144
|
+
if (process.platform === 'win32') {
|
|
145
|
+
if (process.env.OneDrive !== undefined)
|
|
146
|
+
candidates.push(path.join(process.env.OneDrive, 'Documents', userFolder));
|
|
147
|
+
candidates.push(path.join(home, 'Documents', userFolder));
|
|
148
|
+
}
|
|
149
|
+
else if (process.platform === 'darwin') {
|
|
150
|
+
candidates.push(path.join(home, 'Documents', userFolder));
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
candidates.push(path.join(home, '.local', 'share', userFolder));
|
|
154
|
+
}
|
|
155
|
+
return [...new Set(candidates)];
|
|
156
|
+
}
|
|
157
|
+
async function readTail(file, size) {
|
|
158
|
+
const start = Math.max(0, size - MAX_LOG_BYTES);
|
|
159
|
+
const handle = await open(file, 'r');
|
|
160
|
+
try {
|
|
161
|
+
const buffer = Buffer.alloc(size - start);
|
|
162
|
+
await handle.read(buffer, 0, buffer.length, start);
|
|
163
|
+
let text = buffer.toString('utf8');
|
|
164
|
+
// Drop the partial first line of a truncated read.
|
|
165
|
+
if (start > 0)
|
|
166
|
+
text = text.slice(text.indexOf('\n') + 1);
|
|
167
|
+
return { text, truncated: start > 0 };
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
await handle.close();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/** The last source file a message names, relative to its source root, and its line. */
|
|
174
|
+
function locate(message, roots) {
|
|
175
|
+
const files = [
|
|
176
|
+
...[...message.matchAll(quotedFilePattern)].map((match) => match[1]),
|
|
177
|
+
...[...message.matchAll(barePathPattern)].map((match) => match[1].trim()),
|
|
178
|
+
];
|
|
179
|
+
const raw = files.at(-1);
|
|
180
|
+
if (raw === undefined)
|
|
181
|
+
return undefined;
|
|
182
|
+
let normalized = raw.replaceAll('\\', '/').replace(/^\.\//u, '');
|
|
183
|
+
for (const root of roots) {
|
|
184
|
+
const prefix = `${root.replaceAll('\\', '/').replace(/\/$/u, '')}/`;
|
|
185
|
+
if (normalized.toLowerCase().startsWith(prefix.toLowerCase())) {
|
|
186
|
+
normalized = normalized.slice(prefix.length);
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Paths under any mod folder are made relative to that mod.
|
|
191
|
+
const modMatch = /\/mod\/[^/]+\/(.+)$/u.exec(normalized);
|
|
192
|
+
if (modMatch !== null && path.isAbsolute(raw))
|
|
193
|
+
normalized = modMatch[1];
|
|
194
|
+
const lines = [...message.matchAll(linePattern)].map((match) => Number(match[1]));
|
|
195
|
+
return { relativePath: normalized, ...(lines.length === 0 ? {} : { line: lines.at(-1) }) };
|
|
196
|
+
}
|
|
197
|
+
async function layerOf(workspace, relativePath, logModified) {
|
|
198
|
+
const layers = [
|
|
199
|
+
['mod', workspace.modRoot],
|
|
200
|
+
...workspace.dependencyRoots.map((root) => [
|
|
201
|
+
'dependency',
|
|
202
|
+
root,
|
|
203
|
+
]),
|
|
204
|
+
...(workspace.gameRoot === undefined
|
|
205
|
+
? []
|
|
206
|
+
: [['game', workspace.gameRoot]]),
|
|
207
|
+
];
|
|
208
|
+
for (const [layer, root] of layers) {
|
|
209
|
+
const candidate = path.resolve(root, relativePath);
|
|
210
|
+
if (!candidate.startsWith(path.resolve(root) + path.sep))
|
|
211
|
+
continue;
|
|
212
|
+
const info = await stat(candidate).catch(() => undefined);
|
|
213
|
+
if (info?.isFile() === true)
|
|
214
|
+
return { layer, changedSinceLog: info.mtimeMs > logModified };
|
|
215
|
+
}
|
|
216
|
+
return { layer: 'unknown', changedSinceLog: false };
|
|
217
|
+
}
|
|
218
|
+
export async function readErrorLog(workspace, input, configuredUserRoot, signal) {
|
|
219
|
+
let found;
|
|
220
|
+
for (const root of userDataCandidates(workspace, configuredUserRoot)) {
|
|
221
|
+
const file = path.join(root, 'logs', 'error.log');
|
|
222
|
+
const info = await stat(file).catch(() => undefined);
|
|
223
|
+
if (info?.isFile() === true) {
|
|
224
|
+
found = { file, size: info.size, modified: info.mtimeMs };
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (found === undefined)
|
|
229
|
+
throw new ServiceError('ERROR_LOG_NOT_FOUND', 'No logs/error.log was found in the game user-data folder; launch the game once, or set gameUserRoot in the server configuration');
|
|
230
|
+
signal?.throwIfAborted();
|
|
231
|
+
const { text, truncated } = await readTail(found.file, found.size);
|
|
232
|
+
const roots = [workspace.modRoot, ...workspace.dependencyRoots, workspace.gameRoot ?? ''].filter(Boolean);
|
|
233
|
+
// Join continuation lines to the entry they belong to.
|
|
234
|
+
const raw = [];
|
|
235
|
+
text.split(/\r?\n/u).forEach((line, index) => {
|
|
236
|
+
const header = headerPattern.exec(line);
|
|
237
|
+
if (header !== null)
|
|
238
|
+
raw.push({ time: header[1], module: header[2], message: header[3], logLine: index + 1 });
|
|
239
|
+
else if (line.trim() !== '' && raw.length > 0)
|
|
240
|
+
raw.at(-1).message += ` ${line.trim()}`;
|
|
241
|
+
else if (line.trim() !== '')
|
|
242
|
+
raw.push({ module: '', message: line.trim(), logLine: index + 1 });
|
|
243
|
+
});
|
|
244
|
+
const grouped = new Map();
|
|
245
|
+
for (const [order, item] of raw.entries()) {
|
|
246
|
+
signal?.throwIfAborted();
|
|
247
|
+
const message = item.message.slice(0, MAX_MESSAGE_CHARACTERS);
|
|
248
|
+
const key = `${item.module}\0${message}`;
|
|
249
|
+
const existing = grouped.get(key);
|
|
250
|
+
if (existing !== undefined) {
|
|
251
|
+
existing.count += 1;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
const located = locate(message, roots);
|
|
255
|
+
const source = located === undefined
|
|
256
|
+
? undefined
|
|
257
|
+
: {
|
|
258
|
+
relativePath: located.relativePath.slice(0, 1024),
|
|
259
|
+
...(located.line === undefined ? {} : { line: located.line }),
|
|
260
|
+
...(await layerOf(workspace, located.relativePath, found.modified)),
|
|
261
|
+
};
|
|
262
|
+
grouped.set(key, {
|
|
263
|
+
category: classify(item.module, message),
|
|
264
|
+
module: item.module.slice(0, 128),
|
|
265
|
+
message,
|
|
266
|
+
count: 1,
|
|
267
|
+
firstLogLine: item.logLine,
|
|
268
|
+
...(item.time === undefined ? {} : { time: item.time }),
|
|
269
|
+
...(source === undefined ? {} : { source }),
|
|
270
|
+
order,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
const layerRank = { mod: 0, dependency: 1, unknown: 2, game: 3 };
|
|
274
|
+
const all = [...grouped.values()].sort((left, right) => layerRank[left.source?.layer ?? 'unknown'] - layerRank[right.source?.layer ?? 'unknown'] ||
|
|
275
|
+
left.order - right.order);
|
|
276
|
+
const selected = all.filter((entry) => (input.scope === 'all' ||
|
|
277
|
+
(input.scope === 'mod' && entry.source?.layer === 'mod') ||
|
|
278
|
+
(input.scope === 'unlocated' && entry.source === undefined)) &&
|
|
279
|
+
(input.category === undefined || entry.category === input.category) &&
|
|
280
|
+
(input.query === undefined ||
|
|
281
|
+
entry.message.toLowerCase().includes(input.query.toLowerCase()) ||
|
|
282
|
+
entry.source?.relativePath.toLowerCase().includes(input.query.toLowerCase()) === true));
|
|
283
|
+
const launches = await compareWithPreviousLaunch(workspace, new Date(found.modified).toISOString(), all.map((entry) => ({
|
|
284
|
+
module: entry.module,
|
|
285
|
+
message: entry.message,
|
|
286
|
+
category: entry.category,
|
|
287
|
+
...(entry.source === undefined ? {} : { relativePath: entry.source.relativePath }),
|
|
288
|
+
})));
|
|
289
|
+
const previousKeys = launches.previous === undefined ? undefined : new Set(Object.keys(launches.previous.entries));
|
|
290
|
+
const currentKeys = new Set(Object.keys(launches.current.entries));
|
|
291
|
+
const patterns = new Map();
|
|
292
|
+
for (const entry of selected) {
|
|
293
|
+
const pattern = errorLogPattern(entry.message);
|
|
294
|
+
const totals = patterns.get(pattern) ?? {
|
|
295
|
+
category: entry.category,
|
|
296
|
+
distinct: 0,
|
|
297
|
+
entries: 0,
|
|
298
|
+
example: entry.message,
|
|
299
|
+
};
|
|
300
|
+
totals.distinct += 1;
|
|
301
|
+
totals.entries += entry.count;
|
|
302
|
+
patterns.set(pattern, totals);
|
|
303
|
+
}
|
|
304
|
+
const categories = new Map();
|
|
305
|
+
for (const entry of selected) {
|
|
306
|
+
const totals = categories.get(entry.category) ?? { entries: 0, distinct: 0 };
|
|
307
|
+
totals.entries += entry.count;
|
|
308
|
+
totals.distinct += 1;
|
|
309
|
+
categories.set(entry.category, totals);
|
|
310
|
+
}
|
|
311
|
+
const page = selected.slice(input.offset, input.offset + input.limit);
|
|
312
|
+
return {
|
|
313
|
+
log: {
|
|
314
|
+
path: 'user:logs/error.log',
|
|
315
|
+
modifiedAt: new Date(found.modified).toISOString(),
|
|
316
|
+
bytes: found.size,
|
|
317
|
+
truncated,
|
|
318
|
+
entries: raw.length,
|
|
319
|
+
},
|
|
320
|
+
scope: input.scope,
|
|
321
|
+
totalEntries: selected.reduce((sum, entry) => sum + entry.count, 0),
|
|
322
|
+
distinctEntries: selected.length,
|
|
323
|
+
modEntries: all.filter((entry) => entry.source?.layer === 'mod').length,
|
|
324
|
+
changedSinceLog: all.filter((entry) => entry.source?.changedSinceLog === true).length,
|
|
325
|
+
categories: [...categories]
|
|
326
|
+
.map(([category, totals]) => ({ category, ...totals, hint: categoryHints[category] }))
|
|
327
|
+
.sort((left, right) => right.entries - left.entries),
|
|
328
|
+
patterns: [...patterns]
|
|
329
|
+
.filter(([, totals]) => totals.distinct > 1)
|
|
330
|
+
.sort(([, left], [, right]) => right.distinct - left.distinct || right.entries - left.entries)
|
|
331
|
+
.slice(0, 8)
|
|
332
|
+
.map(([pattern, totals]) => ({ pattern, ...totals })),
|
|
333
|
+
...(launches.previous === undefined || previousKeys === undefined
|
|
334
|
+
? {}
|
|
335
|
+
: {
|
|
336
|
+
sinceLastLaunch: {
|
|
337
|
+
previousLogAt: launches.previous.modifiedAt,
|
|
338
|
+
newEntries: [...currentKeys].filter((key) => !previousKeys.has(key)).length,
|
|
339
|
+
persistingEntries: [...currentKeys].filter((key) => previousKeys.has(key)).length,
|
|
340
|
+
resolvedEntries: [...previousKeys].filter((key) => !currentKeys.has(key)).length,
|
|
341
|
+
resolvedExamples: Object.entries(launches.previous.entries)
|
|
342
|
+
.filter(([key]) => !currentKeys.has(key))
|
|
343
|
+
.slice(0, 5)
|
|
344
|
+
.map(([, entry]) => entry),
|
|
345
|
+
},
|
|
346
|
+
}),
|
|
347
|
+
entries: page.map(({ order: _order, ...entry }) => ({
|
|
348
|
+
...entry,
|
|
349
|
+
...knownMistakeOf(entry.message),
|
|
350
|
+
...(previousKeys === undefined
|
|
351
|
+
? {}
|
|
352
|
+
: { new: !previousKeys.has(entryKey(entry.module, entry.message)) }),
|
|
353
|
+
})),
|
|
354
|
+
offset: input.offset,
|
|
355
|
+
...(input.offset + page.length < selected.length
|
|
356
|
+
? { nextOffset: input.offset + page.length }
|
|
357
|
+
: {}),
|
|
358
|
+
};
|
|
359
|
+
}
|