@milaboratories/pl-flight-recorder 0.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/README.md +119 -0
- package/dist/analyze.d.ts +133 -0
- package/dist/analyze.d.ts.map +1 -0
- package/dist/analyze.js +525 -0
- package/dist/analyze.js.map +1 -0
- package/dist/data_summary.d.ts +28 -0
- package/dist/data_summary.d.ts.map +1 -0
- package/dist/data_summary.js +73 -0
- package/dist/data_summary.js.map +1 -0
- package/dist/digest.d.ts +28 -0
- package/dist/digest.d.ts.map +1 -0
- package/dist/digest.js +55 -0
- package/dist/digest.js.map +1 -0
- package/dist/events.d.ts +89 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +12 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +13 -0
- package/dist/instrument.d.ts +82 -0
- package/dist/instrument.d.ts.map +1 -0
- package/dist/instrument.js +313 -0
- package/dist/instrument.js.map +1 -0
- package/dist/recorder.d.ts +82 -0
- package/dist/recorder.d.ts.map +1 -0
- package/dist/recorder.js +293 -0
- package/dist/recorder.js.map +1 -0
- package/dist/redact.d.ts +53 -0
- package/dist/redact.d.ts.map +1 -0
- package/dist/redact.js +145 -0
- package/dist/redact.js.map +1 -0
- package/dist/report.d.ts +6 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +377 -0
- package/dist/report.js.map +1 -0
- package/dist/rules.d.ts +73 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +245 -0
- package/dist/rules.js.map +1 -0
- package/dist/sampler.d.ts +22 -0
- package/dist/sampler.d.ts.map +1 -0
- package/dist/sampler.js +33 -0
- package/dist/sampler.js.map +1 -0
- package/dist/sampler_thread.d.ts +1 -0
- package/dist/sampler_thread.js +41 -0
- package/dist/sampler_thread.js.map +1 -0
- package/dist/session.d.ts +40 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +50 -0
- package/dist/session.js.map +1 -0
- package/dist/supervisor.d.ts +58 -0
- package/dist/supervisor.d.ts.map +1 -0
- package/dist/supervisor.js +108 -0
- package/dist/supervisor.js.map +1 -0
- package/package.json +43 -0
- package/src/analyze.test.ts +539 -0
- package/src/analyze.ts +795 -0
- package/src/data_summary.ts +110 -0
- package/src/digest.ts +49 -0
- package/src/events.ts +102 -0
- package/src/index.ts +104 -0
- package/src/instrument.ts +442 -0
- package/src/recorder.ts +397 -0
- package/src/redact.test.ts +155 -0
- package/src/redact.ts +213 -0
- package/src/report.ts +512 -0
- package/src/rules.test.ts +182 -0
- package/src/rules.ts +383 -0
- package/src/sampler.ts +40 -0
- package/src/sampler_thread.ts +45 -0
- package/src/session.ts +69 -0
- package/src/supervisor.ts +150 -0
package/dist/report.js
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { readSession } from "./recorder.js";
|
|
2
|
+
import { REDACTION } from "./digest.js";
|
|
3
|
+
import { axesUnder, axisKey, isJoinNode, joinChildren, joinShapes } from "./rules.js";
|
|
4
|
+
import { formatBytes, formatCount } from "./analyze.js";
|
|
5
|
+
//#region src/report.ts
|
|
6
|
+
/** Renders an analysis as the markdown report a developer reads. */
|
|
7
|
+
function renderReport(analysis) {
|
|
8
|
+
const { records } = readSession(analysis.file);
|
|
9
|
+
const culprit = findCulpritJoin(records, analysis);
|
|
10
|
+
const offHeap = (analysis.memory.externalGrowth ?? 0) + (analysis.memory.arrayBuffersGrowth ?? 0);
|
|
11
|
+
return [
|
|
12
|
+
"# Platforma OOM flight report",
|
|
13
|
+
"",
|
|
14
|
+
`**Verdict — ${analysis.verdict.likelyCause ?? "no rule matched"}**`,
|
|
15
|
+
"",
|
|
16
|
+
analysis.verdict.summary,
|
|
17
|
+
"",
|
|
18
|
+
table("Where it stopped", [
|
|
19
|
+
row("Outcome", analysis.verdict.outcome),
|
|
20
|
+
row("Last unfinished operation", analysis.verdict.where),
|
|
21
|
+
row("Memory region that grew", analysis.verdict.memoryRegion ?? "not classified"),
|
|
22
|
+
row("Peak RSS", formatBytes(analysis.memory.peakRss)),
|
|
23
|
+
row("RSS at last sample", formatBytes(analysis.memory.rssAtDeath)),
|
|
24
|
+
row("JS heap at last record", `${formatBytes(analysis.memory.heapUsedAtDeath)} of ${formatBytes(analysis.memory.heapLimit)}` + (analysis.memory.heapPressure ? ` (${Math.round(analysis.memory.heapPressure * 100)}%)` : "")),
|
|
25
|
+
row("Off-heap allocated (external + ArrayBuffers)", `${formatBytes(offHeap)} — allocated size, which exceeds resident memory when pages are never written`),
|
|
26
|
+
row("Sampler series", analysis.memory.samplerPresent ? `${analysis.memory.sampleCount} samples` : "absent (in-thread records only)"),
|
|
27
|
+
row("Worst recorded thread stall", `${Math.round(analysis.memory.worstStallMs)} ms`),
|
|
28
|
+
row("Log tail truncated by the kill", String(analysis.truncatedTail)),
|
|
29
|
+
row("Log rotations", analysis.rotations === 0 ? "none — the whole session is present" : `${analysis.rotations} — operations older than the retained segments are absent`),
|
|
30
|
+
row("Supervisor crash marker", analysis.crashMarker ? `${analysis.crashMarker.reason}${analysis.crashMarker.errorCode ? ` (${analysis.crashMarker.errorCode})` : ""}` : "none — the parent process did not record the cause")
|
|
31
|
+
]),
|
|
32
|
+
"",
|
|
33
|
+
"## RSS over the session",
|
|
34
|
+
"",
|
|
35
|
+
"```",
|
|
36
|
+
sparkline(analysis.memory.rssSeries),
|
|
37
|
+
"```",
|
|
38
|
+
"",
|
|
39
|
+
findingsSection(analysis),
|
|
40
|
+
"",
|
|
41
|
+
attributionSection(analysis),
|
|
42
|
+
"",
|
|
43
|
+
culpritSection(culprit),
|
|
44
|
+
"",
|
|
45
|
+
rendersSection(analysis),
|
|
46
|
+
"",
|
|
47
|
+
timelineSection(analysis),
|
|
48
|
+
"",
|
|
49
|
+
nextStepsSection(analysis),
|
|
50
|
+
"",
|
|
51
|
+
table("Environment", [
|
|
52
|
+
row("Role", analysis.role),
|
|
53
|
+
row("Node", analysis.env?.node),
|
|
54
|
+
row("Platform", analysis.env?.platform),
|
|
55
|
+
row("Machine memory", formatBytes(analysis.env?.totalMemory)),
|
|
56
|
+
row("CPUs", analysis.env?.cpus),
|
|
57
|
+
row("V8 heap limit", formatBytes(analysis.env?.heapLimit)),
|
|
58
|
+
row("execArgv", (analysis.env?.execArgv ?? []).join(" ") || "(none)"),
|
|
59
|
+
row("App meta", JSON.stringify(analysis.meta ?? {})),
|
|
60
|
+
row("Session", analysis.sessionId),
|
|
61
|
+
row("Records", analysis.recordCount)
|
|
62
|
+
]),
|
|
63
|
+
"",
|
|
64
|
+
"## What this report contains",
|
|
65
|
+
"",
|
|
66
|
+
`Kept: ${REDACTION.kept.join(", ")}.`,
|
|
67
|
+
"",
|
|
68
|
+
`Never recorded: ${REDACTION.dropped.join(", ")}.`,
|
|
69
|
+
""
|
|
70
|
+
].join("\n");
|
|
71
|
+
}
|
|
72
|
+
function findingsSection(analysis) {
|
|
73
|
+
if (analysis.findings.length === 0) return "## Findings\n\nNo rule fired.";
|
|
74
|
+
const lines = [
|
|
75
|
+
"## Findings",
|
|
76
|
+
"",
|
|
77
|
+
"| Severity | Rule | Detail |",
|
|
78
|
+
"| --- | --- | --- |"
|
|
79
|
+
];
|
|
80
|
+
for (const finding of analysis.findings) lines.push(`| ${finding.severity} | \`${finding.rule}\` | ${escapeCell(finding.detail)} |`);
|
|
81
|
+
return lines.join("\n");
|
|
82
|
+
}
|
|
83
|
+
function attributionSection(analysis) {
|
|
84
|
+
if (analysis.attribution.length === 0) return "## Memory attribution\n\nNo completed operation carried a memory delta.";
|
|
85
|
+
const lines = [
|
|
86
|
+
"## Memory attribution — RSS growth per completed operation",
|
|
87
|
+
"",
|
|
88
|
+
"| Op | seq | Duration | RSS delta | Heap delta |",
|
|
89
|
+
"| --- | --- | --- | --- | --- |"
|
|
90
|
+
];
|
|
91
|
+
for (const op of analysis.attribution) lines.push(`| ${op.op} | ${op.seq} | ${op.ms ?? "?"} ms | ${formatBytes(op.rssDelta)} | ${formatBytes(op.heapDelta ?? 0)} |`);
|
|
92
|
+
lines.push("", "The operation that never completed does not appear here — it is in the verdict above.");
|
|
93
|
+
return lines.join("\n");
|
|
94
|
+
}
|
|
95
|
+
function culpritSection(culprit) {
|
|
96
|
+
if (!culprit) return "## Definition in flight\n\nNo definition was recorded for the failing operation.";
|
|
97
|
+
const digest = culprit.def;
|
|
98
|
+
const def = digest?.def;
|
|
99
|
+
const lines = [
|
|
100
|
+
`## Definition in flight (seq ${culprit.seq}, \`${culprit.type}\`, ${digest?.kind ?? "unknown shape"})`,
|
|
101
|
+
"",
|
|
102
|
+
"```",
|
|
103
|
+
renderDefTree(def, ""),
|
|
104
|
+
"```"
|
|
105
|
+
];
|
|
106
|
+
const elided = describeElisions(digest?.redaction);
|
|
107
|
+
if (elided) lines.push("", elided);
|
|
108
|
+
return lines.join("\n");
|
|
109
|
+
}
|
|
110
|
+
function describeElisions(redaction) {
|
|
111
|
+
if (!redaction) return void 0;
|
|
112
|
+
const notes = [];
|
|
113
|
+
if (redaction.omittedItems) notes.push(`${redaction.omittedItems} array item(s) omitted across ${redaction.truncatedArrays} array(s)`);
|
|
114
|
+
if (redaction.depthCapped) notes.push(`${redaction.depthCapped} subtree(s) cut at the depth cap`);
|
|
115
|
+
if (redaction.budgetExhausted) notes.push("the node budget was exhausted, so the tail is missing");
|
|
116
|
+
const all = [redaction.bytes !== void 0 ? formatBytes(redaction.bytes) : void 0, ...notes].filter(Boolean);
|
|
117
|
+
return all.length > 0 ? `Recorded shape: ${all.join("; ")}.` : void 0;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Renders the recorded definition as a tree.
|
|
121
|
+
*
|
|
122
|
+
* The walk is driven by the shape rather than by a known definition type: a node
|
|
123
|
+
* with a join discriminator gets the join line, a node carrying a column spec
|
|
124
|
+
* gets the column line, and anything else with a discriminator is printed as a
|
|
125
|
+
* pass-through step. The same code therefore renders both the original tree API
|
|
126
|
+
* and the V2 query API.
|
|
127
|
+
*/
|
|
128
|
+
function renderDefTree(node, indent, depth = 0) {
|
|
129
|
+
if (depth > 24) return `${indent}…`;
|
|
130
|
+
if (node === null || node === void 0) return `${indent}(none)`;
|
|
131
|
+
if (Array.isArray(node)) return node.map((child) => renderDefTree(child, indent, depth + 1)).join("\n");
|
|
132
|
+
if (typeof node !== "object") return `${indent}${String(node)}`;
|
|
133
|
+
const record = node;
|
|
134
|
+
const payload = columnPayload(record);
|
|
135
|
+
if (payload) return `${indent}${columnLine(record, payload)}`;
|
|
136
|
+
if (isJoinNode(record)) {
|
|
137
|
+
const shape = joinShapes(record)[0];
|
|
138
|
+
const children = joinChildren(record);
|
|
139
|
+
return [
|
|
140
|
+
`${indent}${shape?.join ?? "join"} children=${children.length} sharedAxes=${shape?.sharedAxes.length ?? 0}` + (shape?.disjointPairs.length ? ` !! DISJOINT ${JSON.stringify(shape.disjointPairs)}` : "") + ` inputRowsMax=${formatCount(shape?.inputRowsMax)}` + (shape?.rowsUpperBound ? ` rowsUpperBound=${formatCount(shape.rowsUpperBound)}` : ""),
|
|
141
|
+
`${indent} axisUnion: ${(shape?.axisUnion ?? []).join(" ") || "(none)"}`,
|
|
142
|
+
...children.map((child) => renderDefTree(child, `${indent} `, depth + 1))
|
|
143
|
+
].join("\n");
|
|
144
|
+
}
|
|
145
|
+
const discriminator = typeof record.type === "string" ? record.type : void 0;
|
|
146
|
+
const interesting = Object.entries(record).filter(([, value]) => isStructural(value));
|
|
147
|
+
if (discriminator) {
|
|
148
|
+
const detail = [filtersNote(record), typeof record.$omitted === "number" ? `omitted=${record.$omitted}` : void 0].filter(Boolean).join(" ");
|
|
149
|
+
return [`${indent}${discriminator}${detail ? ` ${detail}` : ""}`, ...interesting.map(([, value]) => renderDefTree(value, `${indent} `, depth + 1))].join("\n");
|
|
150
|
+
}
|
|
151
|
+
return interesting.length > 0 ? interesting.map(([, value]) => renderDefTree(value, indent, depth + 1)).join("\n") : `${indent}${describeLeafObject(record)}`;
|
|
152
|
+
}
|
|
153
|
+
function filtersNote(record) {
|
|
154
|
+
const parts = [];
|
|
155
|
+
for (const key of [
|
|
156
|
+
"filters",
|
|
157
|
+
"partitionFilters",
|
|
158
|
+
"sorting"
|
|
159
|
+
]) {
|
|
160
|
+
const value = record[key];
|
|
161
|
+
if (Array.isArray(value) && value.length > 0) parts.push(`${key}=${value.length}`);
|
|
162
|
+
}
|
|
163
|
+
const predicate = record.predicate;
|
|
164
|
+
if (typeof predicate?.operator === "string") parts.push(`op=${predicate.operator}`);
|
|
165
|
+
return parts.length > 0 ? parts.join(" ") : void 0;
|
|
166
|
+
}
|
|
167
|
+
function columnLine(outer, payload) {
|
|
168
|
+
const spec = payload.spec ?? {};
|
|
169
|
+
const data = payload.data ?? payload.dataInfo ?? {};
|
|
170
|
+
const axes = axesUnder(payload).map(axisKey).join(" , ");
|
|
171
|
+
const rows = data.rows ?? data.entries;
|
|
172
|
+
return `${typeof outer.type === "string" ? outer.type : "column"} ${asText(spec.name)} : ${asText(spec.valueType)} axes=[${axes}] data=${data.kind ?? "?"}` + (data.parts !== void 0 ? ` parts=${data.parts}` : "") + (rows !== void 0 ? ` rows=${formatCount(rows)}` : " rows=unknown") + (data.bytes !== void 0 ? ` bytes=${formatBytes(data.bytes)}` : "");
|
|
173
|
+
}
|
|
174
|
+
/** The record carrying a column spec: the node itself, or the column it wraps. */
|
|
175
|
+
function columnPayload(record) {
|
|
176
|
+
if (carriesSpec(record)) return record;
|
|
177
|
+
const inner = record.column;
|
|
178
|
+
return carriesSpec(inner) ? inner : void 0;
|
|
179
|
+
}
|
|
180
|
+
function carriesSpec(value) {
|
|
181
|
+
if (typeof value !== "object" || value === null) return false;
|
|
182
|
+
const spec = value.spec;
|
|
183
|
+
return Array.isArray(spec?.axesSpec);
|
|
184
|
+
}
|
|
185
|
+
function isStructural(value) {
|
|
186
|
+
return typeof value === "object" && value !== null;
|
|
187
|
+
}
|
|
188
|
+
function describeLeafObject(record) {
|
|
189
|
+
if (typeof record.$omitted === "number") return `… ${record.$omitted} more omitted`;
|
|
190
|
+
if (record.$depth !== void 0) return "… cut at the depth cap";
|
|
191
|
+
if (record.$budget !== void 0) return "… node budget exhausted";
|
|
192
|
+
if (typeof record.$opaque === "string") return `(${record.$opaque})`;
|
|
193
|
+
const keys = Object.keys(record);
|
|
194
|
+
return keys.length > 0 ? `{ ${keys.join(", ")} }` : "{}";
|
|
195
|
+
}
|
|
196
|
+
function asText(value) {
|
|
197
|
+
return typeof value === "string" ? value : "?";
|
|
198
|
+
}
|
|
199
|
+
function rendersSection(analysis) {
|
|
200
|
+
if (analysis.renders.length === 0) return "## Model renders\n\nNone recorded.";
|
|
201
|
+
const lines = [
|
|
202
|
+
"## Model renders",
|
|
203
|
+
"",
|
|
204
|
+
"| Block | seq | Finished | ms | Sandbox bytes out |",
|
|
205
|
+
"| --- | --- | --- | --- | --- |"
|
|
206
|
+
];
|
|
207
|
+
for (const render of analysis.renders.slice(-12)) {
|
|
208
|
+
const name = render.blockId ?? render.block ?? render.key ?? "?";
|
|
209
|
+
const finished = render.end ? render.failed ? "error" : "yes" : "**NO**";
|
|
210
|
+
const serOut = render.stats?.serOutBytes !== void 0 ? formatBytes(render.stats.serOutBytes) : "-";
|
|
211
|
+
lines.push(`| ${name} | ${render.seq} | ${finished} | ${render.ms ?? "-"} | ${serOut} |`);
|
|
212
|
+
}
|
|
213
|
+
return lines.join("\n");
|
|
214
|
+
}
|
|
215
|
+
function timelineSection(analysis) {
|
|
216
|
+
const lines = [
|
|
217
|
+
"## Last records before the log ends",
|
|
218
|
+
"",
|
|
219
|
+
"```"
|
|
220
|
+
];
|
|
221
|
+
for (const record of analysis.timeline) {
|
|
222
|
+
const type = record.type;
|
|
223
|
+
if (type === "mem-sampler" || type === "mem-self") continue;
|
|
224
|
+
const rss = record.rss;
|
|
225
|
+
const time = new Date(record.wall).toISOString().slice(11, 23);
|
|
226
|
+
lines.push(`${String(record.seq).padStart(5)} ${time} ${(rss !== void 0 ? formatBytes(rss) : "").padStart(9)} ${type}` + extraFields(record));
|
|
227
|
+
}
|
|
228
|
+
lines.push("```");
|
|
229
|
+
return lines.join("\n");
|
|
230
|
+
}
|
|
231
|
+
const TIMELINE_FIELDS = [
|
|
232
|
+
"blockId",
|
|
233
|
+
"handle",
|
|
234
|
+
"rows",
|
|
235
|
+
"columns",
|
|
236
|
+
"amplification",
|
|
237
|
+
"returnedBytes",
|
|
238
|
+
"unbounded",
|
|
239
|
+
"tableRows",
|
|
240
|
+
"columnCount",
|
|
241
|
+
"ms",
|
|
242
|
+
"error"
|
|
243
|
+
];
|
|
244
|
+
function extraFields(record) {
|
|
245
|
+
const parts = [];
|
|
246
|
+
for (const key of TIMELINE_FIELDS) {
|
|
247
|
+
const value = record[key];
|
|
248
|
+
if (value === void 0) continue;
|
|
249
|
+
parts.push(`${key}=${key === "returnedBytes" ? formatBytes(value) : value}`);
|
|
250
|
+
}
|
|
251
|
+
const range = record.range;
|
|
252
|
+
if (range) parts.push(`range=${range.offset}+${range.length}`);
|
|
253
|
+
if (record.defSummary) parts.push(`def=${JSON.stringify(record.defSummary)}`);
|
|
254
|
+
return parts.length > 0 ? ` ${parts.join(" ")}` : "";
|
|
255
|
+
}
|
|
256
|
+
const NEXT_STEPS = [
|
|
257
|
+
{
|
|
258
|
+
rule: "cross-join",
|
|
259
|
+
step: "The join tree has siblings with no axis in common. Find where the model assembles that join and check the axis specs of the columns it pulls from the result pool — output size is the product of the inputs."
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
rule: "axis-domain-mismatch",
|
|
263
|
+
step: "The same axis name and type appears with different domains inside one join, so the join key does not match. Compare the domains listed in the findings against what the producing block writes."
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
rule: "unbounded-getData",
|
|
267
|
+
step: "A getData call has no row range. Page it, or drive it from the table viewport."
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
rule: "join-amplification",
|
|
271
|
+
step: "Output rows far exceed the largest input. Log the axis union at each join node and find the level where the count explodes."
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
rule: "huge-inline-column",
|
|
275
|
+
step: "The model built a large inline column inside the sandbox. Move that work into the workflow so it never crosses the QuickJS boundary."
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
rule: "js-heap-exhaustion-confirmed",
|
|
279
|
+
step: "The supervisor confirmed a JS heap limit breach in the middle-layer thread, so the growth is in JavaScript objects, not the native engine. Reproduce with --heapsnapshot-near-heap-limit=1 on that worker to get the retaining set."
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
rule: "js-heap-exhaustion",
|
|
283
|
+
step: "Growth is inside the JS heap. A heap snapshot from the next reproduction would name the retaining objects."
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
rule: "off-heap-buffer-growth",
|
|
287
|
+
step: "Growth is outside the JS heap, so raising --max-old-space-size will not help. Ask for a pframes engine heap profile (pprofDump) alongside this report."
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
rule: "native-allocation-growth",
|
|
291
|
+
step: "Growth is outside the JS heap, so raising --max-old-space-size will not help. Ask for a pframes engine heap profile (pprofDump) alongside this report."
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
rule: "unattributed-crash-marker",
|
|
295
|
+
step: "A crash marker could not be attributed because more than one session in the directory stopped writing around the same time. Spawn the middle-layer worker with an assigned flight session id, which removes the ambiguity entirely."
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
rule: "heap-reading-stale",
|
|
299
|
+
step: "The heap was not sampled near the crash because the thread was blocked. Treat the heap numbers in this report as a floor, not a measurement."
|
|
300
|
+
}
|
|
301
|
+
];
|
|
302
|
+
function nextStepsSection(analysis) {
|
|
303
|
+
const rules = new Set(analysis.findings.map((finding) => finding.rule));
|
|
304
|
+
const steps = NEXT_STEPS.filter((entry) => rules.has(entry.rule)).map((entry) => entry.step);
|
|
305
|
+
if (analysis.rotations > 1) steps.push("This session rotated more than once, so only the last two segments are on disk and the earliest operations are gone. The verdict above rests on the tail, which is intact; treat the operation list as partial.");
|
|
306
|
+
if (!analysis.memory.samplerPresent) steps.push("No sampler series in this session: the RSS curve came from in-thread records only and may have gaps where the thread was blocked.");
|
|
307
|
+
if (steps.length === 0) steps.push("No rule fired. Send the raw flight log so the thresholds can be revisited.");
|
|
308
|
+
return [
|
|
309
|
+
"## Next steps",
|
|
310
|
+
"",
|
|
311
|
+
...steps.map((step, index) => `${index + 1}. ${step}`)
|
|
312
|
+
].join("\n");
|
|
313
|
+
}
|
|
314
|
+
function findCulpritJoin(records, analysis) {
|
|
315
|
+
const bySeq = new Map(records.map((record) => [record.seq, record]));
|
|
316
|
+
const inFlight = analysis.inFlightAtDeath;
|
|
317
|
+
if (inFlight) {
|
|
318
|
+
const beginRecord = bySeq.get(inFlight.seq);
|
|
319
|
+
if (beginRecord?.def) return {
|
|
320
|
+
seq: inFlight.seq,
|
|
321
|
+
type: inFlight.op,
|
|
322
|
+
def: beginRecord.def
|
|
323
|
+
};
|
|
324
|
+
const joinSeq = beginRecord?.joinSeq;
|
|
325
|
+
const join = joinSeq === void 0 ? void 0 : bySeq.get(joinSeq);
|
|
326
|
+
if (join?.def) return {
|
|
327
|
+
seq: join.seq,
|
|
328
|
+
type: opName(join.type),
|
|
329
|
+
def: join.def
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
const fallback = records.findLast((record) => record.def && record.findings?.length) ?? records.findLast((record) => record.def);
|
|
333
|
+
return fallback ? {
|
|
334
|
+
seq: fallback.seq,
|
|
335
|
+
type: opName(fallback.type),
|
|
336
|
+
def: fallback.def
|
|
337
|
+
} : void 0;
|
|
338
|
+
}
|
|
339
|
+
function opName(recordType) {
|
|
340
|
+
return recordType.replace(/-(begin|end|error)$/, "");
|
|
341
|
+
}
|
|
342
|
+
function sparkline(series, width = 72) {
|
|
343
|
+
if (series.length === 0) return "(no samples)";
|
|
344
|
+
const values = series.map((sample) => sample.rss ?? 0);
|
|
345
|
+
const max = Math.max(...values);
|
|
346
|
+
const min = Math.min(...values);
|
|
347
|
+
const chars = "▁▂▃▄▅▆▇█";
|
|
348
|
+
const step = Math.max(1, Math.ceil(values.length / width));
|
|
349
|
+
let line = "";
|
|
350
|
+
for (let i = 0; i < values.length; i += step) {
|
|
351
|
+
const bucket = values.slice(i, i + step);
|
|
352
|
+
const peak = Math.max(...bucket);
|
|
353
|
+
const index = max === min ? 0 : Math.round((peak - min) / (max - min) * 7);
|
|
354
|
+
line += chars[index];
|
|
355
|
+
}
|
|
356
|
+
const spanMs = (series[series.length - 1].wall ?? 0) - (series[0].wall ?? 0);
|
|
357
|
+
return `${line}\n${formatBytes(min)} → ${formatBytes(max)} over ${(spanMs / 1e3).toFixed(1)}s (${values.length} samples)`;
|
|
358
|
+
}
|
|
359
|
+
function table(title, rows) {
|
|
360
|
+
return [
|
|
361
|
+
`## ${title}`,
|
|
362
|
+
"",
|
|
363
|
+
"| | |",
|
|
364
|
+
"| --- | --- |",
|
|
365
|
+
...rows
|
|
366
|
+
].join("\n");
|
|
367
|
+
}
|
|
368
|
+
function row(key, value) {
|
|
369
|
+
return `| ${key} | ${value ?? "unknown"} |`;
|
|
370
|
+
}
|
|
371
|
+
function escapeCell(value) {
|
|
372
|
+
return String(value ?? "").replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
373
|
+
}
|
|
374
|
+
//#endregion
|
|
375
|
+
export { renderReport };
|
|
376
|
+
|
|
377
|
+
//# sourceMappingURL=report.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"report.js","names":[],"sources":["../src/report.ts"],"sourcesContent":["import { readSession } from \"./recorder\";\nimport { REDACTION } from \"./digest\";\nimport { axesUnder, axisKey, isJoinNode, joinChildren, joinShapes } from \"./rules\";\nimport { formatBytes, formatCount, type SessionAnalysis } from \"./analyze\";\nimport type { FlightRecord } from \"./events\";\n\n/** Renders an analysis as the markdown report a developer reads. */\nexport function renderReport(analysis: SessionAnalysis): string {\n const { records } = readSession(analysis.file);\n const culprit = findCulpritJoin(records, analysis);\n const offHeap = (analysis.memory.externalGrowth ?? 0) + (analysis.memory.arrayBuffersGrowth ?? 0);\n\n return [\n \"# Platforma OOM flight report\",\n \"\",\n `**Verdict — ${analysis.verdict.likelyCause ?? \"no rule matched\"}**`,\n \"\",\n analysis.verdict.summary,\n \"\",\n table(\"Where it stopped\", [\n row(\"Outcome\", analysis.verdict.outcome),\n row(\"Last unfinished operation\", analysis.verdict.where),\n row(\"Memory region that grew\", analysis.verdict.memoryRegion ?? \"not classified\"),\n row(\"Peak RSS\", formatBytes(analysis.memory.peakRss)),\n row(\"RSS at last sample\", formatBytes(analysis.memory.rssAtDeath)),\n row(\n \"JS heap at last record\",\n `${formatBytes(analysis.memory.heapUsedAtDeath)} of ${formatBytes(analysis.memory.heapLimit)}` +\n (analysis.memory.heapPressure\n ? ` (${Math.round(analysis.memory.heapPressure * 100)}%)`\n : \"\"),\n ),\n row(\n \"Off-heap allocated (external + ArrayBuffers)\",\n `${formatBytes(offHeap)} — allocated size, which exceeds resident memory when pages are never written`,\n ),\n row(\n \"Sampler series\",\n analysis.memory.samplerPresent\n ? `${analysis.memory.sampleCount} samples`\n : \"absent (in-thread records only)\",\n ),\n row(\"Worst recorded thread stall\", `${Math.round(analysis.memory.worstStallMs)} ms`),\n row(\"Log tail truncated by the kill\", String(analysis.truncatedTail)),\n row(\n \"Log rotations\",\n analysis.rotations === 0\n ? \"none — the whole session is present\"\n : `${analysis.rotations} — operations older than the retained segments are absent`,\n ),\n row(\n \"Supervisor crash marker\",\n analysis.crashMarker\n ? `${analysis.crashMarker.reason}${\n analysis.crashMarker.errorCode ? ` (${analysis.crashMarker.errorCode})` : \"\"\n }`\n : \"none — the parent process did not record the cause\",\n ),\n ]),\n \"\",\n \"## RSS over the session\",\n \"\",\n \"```\",\n sparkline(analysis.memory.rssSeries),\n \"```\",\n \"\",\n findingsSection(analysis),\n \"\",\n attributionSection(analysis),\n \"\",\n culpritSection(culprit),\n \"\",\n rendersSection(analysis),\n \"\",\n timelineSection(analysis),\n \"\",\n nextStepsSection(analysis),\n \"\",\n table(\"Environment\", [\n row(\"Role\", analysis.role),\n row(\"Node\", analysis.env?.node),\n row(\"Platform\", analysis.env?.platform),\n row(\"Machine memory\", formatBytes(analysis.env?.totalMemory)),\n row(\"CPUs\", analysis.env?.cpus),\n row(\"V8 heap limit\", formatBytes(analysis.env?.heapLimit)),\n row(\"execArgv\", (analysis.env?.execArgv ?? []).join(\" \") || \"(none)\"),\n row(\"App meta\", JSON.stringify(analysis.meta ?? {})),\n row(\"Session\", analysis.sessionId),\n row(\"Records\", analysis.recordCount),\n ]),\n \"\",\n \"## What this report contains\",\n \"\",\n `Kept: ${REDACTION.kept.join(\", \")}.`,\n \"\",\n `Never recorded: ${REDACTION.dropped.join(\", \")}.`,\n \"\",\n ].join(\"\\n\");\n}\n\n// Internals\n\ntype Culprit = { seq: number; type: string; def: Record<string, unknown> };\n\nfunction findingsSection(analysis: SessionAnalysis): string {\n if (analysis.findings.length === 0) return \"## Findings\\n\\nNo rule fired.\";\n const lines = [\"## Findings\", \"\", \"| Severity | Rule | Detail |\", \"| --- | --- | --- |\"];\n for (const finding of analysis.findings) {\n lines.push(`| ${finding.severity} | \\`${finding.rule}\\` | ${escapeCell(finding.detail)} |`);\n }\n return lines.join(\"\\n\");\n}\n\nfunction attributionSection(analysis: SessionAnalysis): string {\n if (analysis.attribution.length === 0) {\n return \"## Memory attribution\\n\\nNo completed operation carried a memory delta.\";\n }\n const lines = [\n \"## Memory attribution — RSS growth per completed operation\",\n \"\",\n \"| Op | seq | Duration | RSS delta | Heap delta |\",\n \"| --- | --- | --- | --- | --- |\",\n ];\n for (const op of analysis.attribution) {\n lines.push(\n `| ${op.op} | ${op.seq} | ${op.ms ?? \"?\"} ms | ${formatBytes(op.rssDelta)} | ${formatBytes(\n op.heapDelta ?? 0,\n )} |`,\n );\n }\n lines.push(\n \"\",\n \"The operation that never completed does not appear here — it is in the verdict above.\",\n );\n return lines.join(\"\\n\");\n}\n\nfunction culpritSection(culprit: Culprit | undefined): string {\n if (!culprit) {\n return \"## Definition in flight\\n\\nNo definition was recorded for the failing operation.\";\n }\n const digest = culprit.def as\n | { kind?: string; def?: unknown; redaction?: RedactionSummary }\n | undefined;\n const def = digest?.def;\n const lines = [\n `## Definition in flight (seq ${culprit.seq}, \\`${culprit.type}\\`, ${digest?.kind ?? \"unknown shape\"})`,\n \"\",\n \"```\",\n renderDefTree(def, \"\"),\n \"```\",\n ];\n\n const elided = describeElisions(digest?.redaction);\n if (elided) lines.push(\"\", elided);\n return lines.join(\"\\n\");\n}\n\ntype RedactionSummary = {\n bytes?: number;\n hashedStrings?: number;\n truncatedArrays?: number;\n omittedItems?: number;\n depthCapped?: number;\n opaqueObjects?: number;\n budgetExhausted?: boolean;\n};\n\nfunction describeElisions(redaction: RedactionSummary | undefined): string | undefined {\n if (!redaction) return undefined;\n const notes: string[] = [];\n if (redaction.omittedItems) {\n notes.push(\n `${redaction.omittedItems} array item(s) omitted across ${redaction.truncatedArrays} array(s)`,\n );\n }\n if (redaction.depthCapped) notes.push(`${redaction.depthCapped} subtree(s) cut at the depth cap`);\n if (redaction.budgetExhausted)\n notes.push(\"the node budget was exhausted, so the tail is missing\");\n const size = redaction.bytes !== undefined ? formatBytes(redaction.bytes) : undefined;\n const all = [size, ...notes].filter(Boolean);\n return all.length > 0 ? `Recorded shape: ${all.join(\"; \")}.` : undefined;\n}\n\n/**\n * Renders the recorded definition as a tree.\n *\n * The walk is driven by the shape rather than by a known definition type: a node\n * with a join discriminator gets the join line, a node carrying a column spec\n * gets the column line, and anything else with a discriminator is printed as a\n * pass-through step. The same code therefore renders both the original tree API\n * and the V2 query API.\n */\nfunction renderDefTree(node: unknown, indent: string, depth = 0): string {\n if (depth > 24) return `${indent}…`;\n if (node === null || node === undefined) return `${indent}(none)`;\n if (Array.isArray(node)) {\n return node.map((child) => renderDefTree(child, indent, depth + 1)).join(\"\\n\");\n }\n if (typeof node !== \"object\") return `${indent}${String(node)}`;\n\n const record = node as Record<string, unknown>;\n // Both APIs wrap a column as `{ type: \"column\", column: … }`, so the wrapper\n // and the column it carries are printed as one line rather than two.\n const payload = columnPayload(record);\n if (payload) return `${indent}${columnLine(record, payload)}`;\n\n if (isJoinNode(record)) {\n const shape = joinShapes(record)[0];\n const children = joinChildren(record);\n const head =\n `${indent}${shape?.join ?? \"join\"} children=${children.length}` +\n ` sharedAxes=${shape?.sharedAxes.length ?? 0}` +\n (shape?.disjointPairs.length ? ` !! DISJOINT ${JSON.stringify(shape.disjointPairs)}` : \"\") +\n ` inputRowsMax=${formatCount(shape?.inputRowsMax)}` +\n (shape?.rowsUpperBound ? ` rowsUpperBound=${formatCount(shape.rowsUpperBound)}` : \"\");\n const axes = `${indent} axisUnion: ${(shape?.axisUnion ?? []).join(\" \") || \"(none)\"}`;\n return [\n head,\n axes,\n ...children.map((child) => renderDefTree(child, `${indent} `, depth + 1)),\n ].join(\"\\n\");\n }\n\n const discriminator = typeof record.type === \"string\" ? record.type : undefined;\n const interesting = Object.entries(record).filter(([, value]) => isStructural(value));\n if (discriminator) {\n const detail = [\n filtersNote(record),\n typeof record.$omitted === \"number\" ? `omitted=${record.$omitted}` : undefined,\n ]\n .filter(Boolean)\n .join(\" \");\n const head = `${indent}${discriminator}${detail ? ` ${detail}` : \"\"}`;\n return [\n head,\n ...interesting.map(([, value]) => renderDefTree(value, `${indent} `, depth + 1)),\n ].join(\"\\n\");\n }\n // A transparent wrapper (the V2 `{ entry }` shape, or a plain container):\n // print nothing for it and keep the indentation of its parent.\n return interesting.length > 0\n ? interesting.map(([, value]) => renderDefTree(value, indent, depth + 1)).join(\"\\n\")\n : `${indent}${describeLeafObject(record)}`;\n}\n\nfunction filtersNote(record: Record<string, unknown>): string | undefined {\n const parts: string[] = [];\n for (const key of [\"filters\", \"partitionFilters\", \"sorting\"]) {\n const value = record[key];\n if (Array.isArray(value) && value.length > 0) parts.push(`${key}=${value.length}`);\n }\n const predicate = record.predicate as { operator?: unknown } | undefined;\n if (typeof predicate?.operator === \"string\") parts.push(`op=${predicate.operator}`);\n return parts.length > 0 ? parts.join(\" \") : undefined;\n}\n\nfunction columnLine(outer: Record<string, unknown>, payload: Record<string, unknown>): string {\n const spec = (payload.spec ?? {}) as {\n name?: unknown;\n valueType?: unknown;\n axesSpec?: unknown[];\n };\n const data = (payload.data ?? payload.dataInfo ?? {}) as {\n kind?: string;\n rows?: number;\n bytes?: number;\n parts?: number;\n entries?: number;\n };\n const axes = axesUnder(payload).map(axisKey).join(\" , \");\n const rows = data.rows ?? data.entries;\n return (\n `${typeof outer.type === \"string\" ? outer.type : \"column\"} ` +\n `${asText(spec.name)} : ${asText(spec.valueType)}` +\n ` axes=[${axes}]` +\n ` data=${data.kind ?? \"?\"}` +\n (data.parts !== undefined ? ` parts=${data.parts}` : \"\") +\n (rows !== undefined ? ` rows=${formatCount(rows)}` : \" rows=unknown\") +\n (data.bytes !== undefined ? ` bytes=${formatBytes(data.bytes)}` : \"\")\n );\n}\n\n/** The record carrying a column spec: the node itself, or the column it wraps. */\nfunction columnPayload(record: Record<string, unknown>): Record<string, unknown> | undefined {\n if (carriesSpec(record)) return record;\n const inner = record.column;\n return carriesSpec(inner) ? (inner as Record<string, unknown>) : undefined;\n}\n\nfunction carriesSpec(value: unknown): boolean {\n if (typeof value !== \"object\" || value === null) return false;\n const spec = (value as { spec?: { axesSpec?: unknown } }).spec;\n return Array.isArray(spec?.axesSpec);\n}\n\nfunction isStructural(value: unknown): boolean {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction describeLeafObject(record: Record<string, unknown>): string {\n if (typeof record.$omitted === \"number\") return `… ${record.$omitted} more omitted`;\n if (record.$depth !== undefined) return \"… cut at the depth cap\";\n if (record.$budget !== undefined) return \"… node budget exhausted\";\n if (typeof record.$opaque === \"string\") return `(${record.$opaque})`;\n const keys = Object.keys(record);\n return keys.length > 0 ? `{ ${keys.join(\", \")} }` : \"{}\";\n}\n\nfunction asText(value: unknown): string {\n return typeof value === \"string\" ? value : \"?\";\n}\n\nfunction rendersSection(analysis: SessionAnalysis): string {\n if (analysis.renders.length === 0) return \"## Model renders\\n\\nNone recorded.\";\n const lines = [\n \"## Model renders\",\n \"\",\n \"| Block | seq | Finished | ms | Sandbox bytes out |\",\n \"| --- | --- | --- | --- | --- |\",\n ];\n for (const render of analysis.renders.slice(-12)) {\n const name = render.blockId ?? render.block ?? render.key ?? \"?\";\n const finished = render.end ? (render.failed ? \"error\" : \"yes\") : \"**NO**\";\n const serOut =\n render.stats?.serOutBytes !== undefined ? formatBytes(render.stats.serOutBytes) : \"-\";\n lines.push(`| ${name} | ${render.seq} | ${finished} | ${render.ms ?? \"-\"} | ${serOut} |`);\n }\n return lines.join(\"\\n\");\n}\n\nfunction timelineSection(analysis: SessionAnalysis): string {\n const lines = [\"## Last records before the log ends\", \"\", \"```\"];\n for (const record of analysis.timeline) {\n const type = record.type as string;\n if (type === \"mem-sampler\" || type === \"mem-self\") continue;\n const rss = record.rss as number | undefined;\n const time = new Date(record.wall as number).toISOString().slice(11, 23);\n lines.push(\n `${String(record.seq).padStart(5)} ${time} ` +\n `${(rss !== undefined ? formatBytes(rss) : \"\").padStart(9)} ${type}` +\n extraFields(record),\n );\n }\n lines.push(\"```\");\n return lines.join(\"\\n\");\n}\n\nconst TIMELINE_FIELDS = [\n \"blockId\",\n \"handle\",\n \"rows\",\n \"columns\",\n \"amplification\",\n \"returnedBytes\",\n \"unbounded\",\n \"tableRows\",\n \"columnCount\",\n \"ms\",\n \"error\",\n];\n\nfunction extraFields(record: Record<string, unknown>): string {\n const parts: string[] = [];\n for (const key of TIMELINE_FIELDS) {\n const value = record[key];\n if (value === undefined) continue;\n parts.push(`${key}=${key === \"returnedBytes\" ? formatBytes(value as number) : value}`);\n }\n const range = record.range as { offset: number; length: number } | null | undefined;\n if (range) parts.push(`range=${range.offset}+${range.length}`);\n if (record.defSummary) parts.push(`def=${JSON.stringify(record.defSummary)}`);\n return parts.length > 0 ? ` ${parts.join(\" \")}` : \"\";\n}\n\nconst NEXT_STEPS: { rule: string; step: string }[] = [\n {\n rule: \"cross-join\",\n step: \"The join tree has siblings with no axis in common. Find where the model assembles that join and check the axis specs of the columns it pulls from the result pool — output size is the product of the inputs.\",\n },\n {\n rule: \"axis-domain-mismatch\",\n step: \"The same axis name and type appears with different domains inside one join, so the join key does not match. Compare the domains listed in the findings against what the producing block writes.\",\n },\n {\n rule: \"unbounded-getData\",\n step: \"A getData call has no row range. Page it, or drive it from the table viewport.\",\n },\n {\n rule: \"join-amplification\",\n step: \"Output rows far exceed the largest input. Log the axis union at each join node and find the level where the count explodes.\",\n },\n {\n rule: \"huge-inline-column\",\n step: \"The model built a large inline column inside the sandbox. Move that work into the workflow so it never crosses the QuickJS boundary.\",\n },\n {\n rule: \"js-heap-exhaustion-confirmed\",\n step: \"The supervisor confirmed a JS heap limit breach in the middle-layer thread, so the growth is in JavaScript objects, not the native engine. Reproduce with --heapsnapshot-near-heap-limit=1 on that worker to get the retaining set.\",\n },\n {\n rule: \"js-heap-exhaustion\",\n step: \"Growth is inside the JS heap. A heap snapshot from the next reproduction would name the retaining objects.\",\n },\n {\n rule: \"off-heap-buffer-growth\",\n step: \"Growth is outside the JS heap, so raising --max-old-space-size will not help. Ask for a pframes engine heap profile (pprofDump) alongside this report.\",\n },\n {\n rule: \"native-allocation-growth\",\n step: \"Growth is outside the JS heap, so raising --max-old-space-size will not help. Ask for a pframes engine heap profile (pprofDump) alongside this report.\",\n },\n {\n rule: \"unattributed-crash-marker\",\n step: \"A crash marker could not be attributed because more than one session in the directory stopped writing around the same time. Spawn the middle-layer worker with an assigned flight session id, which removes the ambiguity entirely.\",\n },\n {\n rule: \"heap-reading-stale\",\n step: \"The heap was not sampled near the crash because the thread was blocked. Treat the heap numbers in this report as a floor, not a measurement.\",\n },\n];\n\nfunction nextStepsSection(analysis: SessionAnalysis): string {\n const rules = new Set(analysis.findings.map((finding) => finding.rule));\n const steps = NEXT_STEPS.filter((entry) => rules.has(entry.rule)).map((entry) => entry.step);\n if (analysis.rotations > 1) {\n steps.push(\n \"This session rotated more than once, so only the last two segments are on disk and the earliest operations are gone. The verdict above rests on the tail, which is intact; treat the operation list as partial.\",\n );\n }\n if (!analysis.memory.samplerPresent) {\n steps.push(\n \"No sampler series in this session: the RSS curve came from in-thread records only and may have gaps where the thread was blocked.\",\n );\n }\n if (steps.length === 0) {\n steps.push(\"No rule fired. Send the raw flight log so the thresholds can be revisited.\");\n }\n return [\"## Next steps\", \"\", ...steps.map((step, index) => `${index + 1}. ${step}`)].join(\"\\n\");\n}\n\nfunction findCulpritJoin(records: FlightRecord[], analysis: SessionAnalysis): Culprit | undefined {\n const bySeq = new Map(records.map((record) => [record.seq, record]));\n const inFlight = analysis.inFlightAtDeath;\n\n // An in-flight data call points back at the join that produced its handle.\n if (inFlight) {\n const beginRecord = bySeq.get(inFlight.seq);\n if (beginRecord?.def) {\n return {\n seq: inFlight.seq,\n type: inFlight.op,\n def: beginRecord.def as Record<string, unknown>,\n };\n }\n const joinSeq = beginRecord?.joinSeq as number | undefined;\n const join = joinSeq === undefined ? undefined : bySeq.get(joinSeq);\n if (join?.def) {\n return { seq: join.seq, type: opName(join.type), def: join.def as Record<string, unknown> };\n }\n }\n // Otherwise the most recent join carrying a structural finding.\n const flagged = records.findLast(\n (record) => record.def && (record.findings as unknown[] | undefined)?.length,\n );\n const fallback = flagged ?? records.findLast((record) => record.def);\n return fallback\n ? {\n seq: fallback.seq,\n type: opName(fallback.type),\n def: fallback.def as Record<string, unknown>,\n }\n : undefined;\n}\n\nfunction opName(recordType: string): string {\n return recordType.replace(/-(begin|end|error)$/, \"\");\n}\n\nfunction sparkline(series: { wall: number; rss?: number }[], width = 72): string {\n if (series.length === 0) return \"(no samples)\";\n const values = series.map((sample) => sample.rss ?? 0);\n const max = Math.max(...values);\n const min = Math.min(...values);\n const chars = \"▁▂▃▄▅▆▇█\";\n const step = Math.max(1, Math.ceil(values.length / width));\n let line = \"\";\n for (let i = 0; i < values.length; i += step) {\n const bucket = values.slice(i, i + step);\n const peak = Math.max(...bucket);\n const index = max === min ? 0 : Math.round(((peak - min) / (max - min)) * (chars.length - 1));\n line += chars[index];\n }\n const spanMs = (series[series.length - 1].wall ?? 0) - (series[0].wall ?? 0);\n return `${line}\\n${formatBytes(min)} → ${formatBytes(max)} over ${(spanMs / 1000).toFixed(1)}s (${\n values.length\n } samples)`;\n}\n\nfunction table(title: string, rows: string[]): string {\n return [`## ${title}`, \"\", \"| | |\", \"| --- | --- |\", ...rows].join(\"\\n\");\n}\n\nfunction row(key: string, value: unknown): string {\n return `| ${key} | ${value ?? \"unknown\"} |`;\n}\n\nfunction escapeCell(value: string): string {\n return String(value ?? \"\")\n .replace(/\\|/g, \"\\\\|\")\n .replace(/\\n/g, \" \");\n}\n"],"mappings":";;;;;;AAOA,SAAgB,aAAa,UAAmC;CAC9D,MAAM,EAAE,YAAY,YAAY,SAAS,IAAI;CAC7C,MAAM,UAAU,gBAAgB,SAAS,QAAQ;CACjD,MAAM,WAAW,SAAS,OAAO,kBAAkB,MAAM,SAAS,OAAO,sBAAsB;CAE/F,OAAO;EACL;EACA;EACA,eAAe,SAAS,QAAQ,eAAe,kBAAkB;EACjE;EACA,SAAS,QAAQ;EACjB;EACA,MAAM,oBAAoB;GACxB,IAAI,WAAW,SAAS,QAAQ,OAAO;GACvC,IAAI,6BAA6B,SAAS,QAAQ,KAAK;GACvD,IAAI,2BAA2B,SAAS,QAAQ,gBAAgB,gBAAgB;GAChF,IAAI,YAAY,YAAY,SAAS,OAAO,OAAO,CAAC;GACpD,IAAI,sBAAsB,YAAY,SAAS,OAAO,UAAU,CAAC;GACjE,IACE,0BACA,GAAG,YAAY,SAAS,OAAO,eAAe,EAAE,MAAM,YAAY,SAAS,OAAO,SAAS,OACxF,SAAS,OAAO,eACb,KAAK,KAAK,MAAM,SAAS,OAAO,eAAe,GAAG,EAAE,MACpD,GACR;GACA,IACE,gDACA,GAAG,YAAY,OAAO,EAAE,8EAC1B;GACA,IACE,kBACA,SAAS,OAAO,iBACZ,GAAG,SAAS,OAAO,YAAY,YAC/B,iCACN;GACA,IAAI,+BAA+B,GAAG,KAAK,MAAM,SAAS,OAAO,YAAY,EAAE,IAAI;GACnF,IAAI,kCAAkC,OAAO,SAAS,aAAa,CAAC;GACpE,IACE,iBACA,SAAS,cAAc,IACnB,wCACA,GAAG,SAAS,UAAU,0DAC5B;GACA,IACE,2BACA,SAAS,cACL,GAAG,SAAS,YAAY,SACtB,SAAS,YAAY,YAAY,KAAK,SAAS,YAAY,UAAU,KAAK,OAE5E,oDACN;EACF,CAAC;EACD;EACA;EACA;EACA;EACA,UAAU,SAAS,OAAO,SAAS;EACnC;EACA;EACA,gBAAgB,QAAQ;EACxB;EACA,mBAAmB,QAAQ;EAC3B;EACA,eAAe,OAAO;EACtB;EACA,eAAe,QAAQ;EACvB;EACA,gBAAgB,QAAQ;EACxB;EACA,iBAAiB,QAAQ;EACzB;EACA,MAAM,eAAe;GACnB,IAAI,QAAQ,SAAS,IAAI;GACzB,IAAI,QAAQ,SAAS,KAAK,IAAI;GAC9B,IAAI,YAAY,SAAS,KAAK,QAAQ;GACtC,IAAI,kBAAkB,YAAY,SAAS,KAAK,WAAW,CAAC;GAC5D,IAAI,QAAQ,SAAS,KAAK,IAAI;GAC9B,IAAI,iBAAiB,YAAY,SAAS,KAAK,SAAS,CAAC;GACzD,IAAI,aAAa,SAAS,KAAK,YAAY,CAAC,EAAA,CAAG,KAAK,GAAG,KAAK,QAAQ;GACpE,IAAI,YAAY,KAAK,UAAU,SAAS,QAAQ,CAAC,CAAC,CAAC;GACnD,IAAI,WAAW,SAAS,SAAS;GACjC,IAAI,WAAW,SAAS,WAAW;EACrC,CAAC;EACD;EACA;EACA;EACA,SAAS,UAAU,KAAK,KAAK,IAAI,EAAE;EACnC;EACA,mBAAmB,UAAU,QAAQ,KAAK,IAAI,EAAE;EAChD;CACF,CAAC,CAAC,KAAK,IAAI;AACb;AAMA,SAAS,gBAAgB,UAAmC;CAC1D,IAAI,SAAS,SAAS,WAAW,GAAG,OAAO;CAC3C,MAAM,QAAQ;EAAC;EAAe;EAAI;EAAgC;CAAqB;CACvF,KAAK,MAAM,WAAW,SAAS,UAC7B,MAAM,KAAK,KAAK,QAAQ,SAAS,OAAO,QAAQ,KAAK,OAAO,WAAW,QAAQ,MAAM,EAAE,GAAG;CAE5F,OAAO,MAAM,KAAK,IAAI;AACxB;AAEA,SAAS,mBAAmB,UAAmC;CAC7D,IAAI,SAAS,YAAY,WAAW,GAClC,OAAO;CAET,MAAM,QAAQ;EACZ;EACA;EACA;EACA;CACF;CACA,KAAK,MAAM,MAAM,SAAS,aACxB,MAAM,KACJ,KAAK,GAAG,GAAG,KAAK,GAAG,IAAI,KAAK,GAAG,MAAM,IAAI,QAAQ,YAAY,GAAG,QAAQ,EAAE,KAAK,YAC7E,GAAG,aAAa,CAClB,EAAE,GACJ;CAEF,MAAM,KACJ,IACA,uFACF;CACA,OAAO,MAAM,KAAK,IAAI;AACxB;AAEA,SAAS,eAAe,SAAsC;CAC5D,IAAI,CAAC,SACH,OAAO;CAET,MAAM,SAAS,QAAQ;CAGvB,MAAM,MAAM,QAAQ;CACpB,MAAM,QAAQ;EACZ,gCAAgC,QAAQ,IAAI,MAAM,QAAQ,KAAK,MAAM,QAAQ,QAAQ,gBAAgB;EACrG;EACA;EACA,cAAc,KAAK,EAAE;EACrB;CACF;CAEA,MAAM,SAAS,iBAAiB,QAAQ,SAAS;CACjD,IAAI,QAAQ,MAAM,KAAK,IAAI,MAAM;CACjC,OAAO,MAAM,KAAK,IAAI;AACxB;AAYA,SAAS,iBAAiB,WAA6D;CACrF,IAAI,CAAC,WAAW,OAAO,KAAA;CACvB,MAAM,QAAkB,CAAC;CACzB,IAAI,UAAU,cACZ,MAAM,KACJ,GAAG,UAAU,aAAa,gCAAgC,UAAU,gBAAgB,UACtF;CAEF,IAAI,UAAU,aAAa,MAAM,KAAK,GAAG,UAAU,YAAY,iCAAiC;CAChG,IAAI,UAAU,iBACZ,MAAM,KAAK,uDAAuD;CAEpE,MAAM,MAAM,CADC,UAAU,UAAU,KAAA,IAAY,YAAY,UAAU,KAAK,IAAI,KAAA,GACzD,GAAG,KAAK,CAAC,CAAC,OAAO,OAAO;CAC3C,OAAO,IAAI,SAAS,IAAI,mBAAmB,IAAI,KAAK,IAAI,EAAE,KAAK,KAAA;AACjE;;;;;;;;;;AAWA,SAAS,cAAc,MAAe,QAAgB,QAAQ,GAAW;CACvE,IAAI,QAAQ,IAAI,OAAO,GAAG,OAAO;CACjC,IAAI,SAAS,QAAQ,SAAS,KAAA,GAAW,OAAO,GAAG,OAAO;CAC1D,IAAI,MAAM,QAAQ,IAAI,GACpB,OAAO,KAAK,KAAK,UAAU,cAAc,OAAO,QAAQ,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI;CAE/E,IAAI,OAAO,SAAS,UAAU,OAAO,GAAG,SAAS,OAAO,IAAI;CAE5D,MAAM,SAAS;CAGf,MAAM,UAAU,cAAc,MAAM;CACpC,IAAI,SAAS,OAAO,GAAG,SAAS,WAAW,QAAQ,OAAO;CAE1D,IAAI,WAAW,MAAM,GAAG;EACtB,MAAM,QAAQ,WAAW,MAAM,CAAC,CAAC;EACjC,MAAM,WAAW,aAAa,MAAM;EAQpC,OAAO;GANL,GAAG,SAAS,OAAO,QAAQ,OAAO,aAAa,SAAS,OAAA,eACxC,OAAO,WAAW,UAAU,OAC3C,OAAO,cAAc,SAAS,iBAAiB,KAAK,UAAU,MAAM,aAAa,MAAM,MACxF,kBAAkB,YAAY,OAAO,YAAY,OAChD,OAAO,iBAAiB,oBAAoB,YAAY,MAAM,cAAc,MAAM;GAInF,GAHc,OAAO,gBAAgB,OAAO,aAAa,CAAC,EAAA,CAAG,KAAK,IAAI,KAAK;GAI3E,GAAG,SAAS,KAAK,UAAU,cAAc,OAAO,GAAG,OAAO,OAAO,QAAQ,CAAC,CAAC;EAC7E,CAAC,CAAC,KAAK,IAAI;CACb;CAEA,MAAM,gBAAgB,OAAO,OAAO,SAAS,WAAW,OAAO,OAAO,KAAA;CACtE,MAAM,cAAc,OAAO,QAAQ,MAAM,CAAC,CAAC,QAAQ,GAAG,WAAW,aAAa,KAAK,CAAC;CACpF,IAAI,eAAe;EACjB,MAAM,SAAS,CACb,YAAY,MAAM,GAClB,OAAO,OAAO,aAAa,WAAW,WAAW,OAAO,aAAa,KAAA,CACvE,CAAC,CACE,OAAO,OAAO,CAAC,CACf,KAAK,IAAI;EAEZ,OAAO,CACL,GAFc,SAAS,gBAAgB,SAAS,KAAK,WAAW,MAGhE,GAAG,YAAY,KAAK,GAAG,WAAW,cAAc,OAAO,GAAG,OAAO,KAAK,QAAQ,CAAC,CAAC,CAClF,CAAC,CAAC,KAAK,IAAI;CACb;CAGA,OAAO,YAAY,SAAS,IACxB,YAAY,KAAK,GAAG,WAAW,cAAc,OAAO,QAAQ,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IACjF,GAAG,SAAS,mBAAmB,MAAM;AAC3C;AAEA,SAAS,YAAY,QAAqD;CACxE,MAAM,QAAkB,CAAC;CACzB,KAAK,MAAM,OAAO;EAAC;EAAW;EAAoB;CAAS,GAAG;EAC5D,MAAM,QAAQ,OAAO;EACrB,IAAI,MAAM,QAAQ,KAAK,KAAK,MAAM,SAAS,GAAG,MAAM,KAAK,GAAG,IAAI,GAAG,MAAM,QAAQ;CACnF;CACA,MAAM,YAAY,OAAO;CACzB,IAAI,OAAO,WAAW,aAAa,UAAU,MAAM,KAAK,MAAM,UAAU,UAAU;CAClF,OAAO,MAAM,SAAS,IAAI,MAAM,KAAK,GAAG,IAAI,KAAA;AAC9C;AAEA,SAAS,WAAW,OAAgC,SAA0C;CAC5F,MAAM,OAAQ,QAAQ,QAAQ,CAAC;CAK/B,MAAM,OAAQ,QAAQ,QAAQ,QAAQ,YAAY,CAAC;CAOnD,MAAM,OAAO,UAAU,OAAO,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,KAAK,KAAK;CACvD,MAAM,OAAO,KAAK,QAAQ,KAAK;CAC/B,OACE,GAAG,OAAO,MAAM,SAAS,WAAW,MAAM,OAAO,SAAS,IACvD,OAAO,KAAK,IAAI,EAAE,KAAK,OAAO,KAAK,SAAS,EAAA,UACpC,KAAK,UACN,KAAK,QAAQ,SACtB,KAAK,UAAU,KAAA,IAAY,UAAU,KAAK,UAAU,OACpD,SAAS,KAAA,IAAY,SAAS,YAAY,IAAI,MAAM,oBACpD,KAAK,UAAU,KAAA,IAAY,UAAU,YAAY,KAAK,KAAK,MAAM;AAEtE;;AAGA,SAAS,cAAc,QAAsE;CAC3F,IAAI,YAAY,MAAM,GAAG,OAAO;CAChC,MAAM,QAAQ,OAAO;CACrB,OAAO,YAAY,KAAK,IAAK,QAAoC,KAAA;AACnE;AAEA,SAAS,YAAY,OAAyB;CAC5C,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM,OAAO;CACxD,MAAM,OAAQ,MAA4C;CAC1D,OAAO,MAAM,QAAQ,MAAM,QAAQ;AACrC;AAEA,SAAS,aAAa,OAAyB;CAC7C,OAAO,OAAO,UAAU,YAAY,UAAU;AAChD;AAEA,SAAS,mBAAmB,QAAyC;CACnE,IAAI,OAAO,OAAO,aAAa,UAAU,OAAO,KAAK,OAAO,SAAS;CACrE,IAAI,OAAO,WAAW,KAAA,GAAW,OAAO;CACxC,IAAI,OAAO,YAAY,KAAA,GAAW,OAAO;CACzC,IAAI,OAAO,OAAO,YAAY,UAAU,OAAO,IAAI,OAAO,QAAQ;CAClE,MAAM,OAAO,OAAO,KAAK,MAAM;CAC/B,OAAO,KAAK,SAAS,IAAI,KAAK,KAAK,KAAK,IAAI,EAAE,MAAM;AACtD;AAEA,SAAS,OAAO,OAAwB;CACtC,OAAO,OAAO,UAAU,WAAW,QAAQ;AAC7C;AAEA,SAAS,eAAe,UAAmC;CACzD,IAAI,SAAS,QAAQ,WAAW,GAAG,OAAO;CAC1C,MAAM,QAAQ;EACZ;EACA;EACA;EACA;CACF;CACA,KAAK,MAAM,UAAU,SAAS,QAAQ,MAAM,GAAG,GAAG;EAChD,MAAM,OAAO,OAAO,WAAW,OAAO,SAAS,OAAO,OAAO;EAC7D,MAAM,WAAW,OAAO,MAAO,OAAO,SAAS,UAAU,QAAS;EAClE,MAAM,SACJ,OAAO,OAAO,gBAAgB,KAAA,IAAY,YAAY,OAAO,MAAM,WAAW,IAAI;EACpF,MAAM,KAAK,KAAK,KAAK,KAAK,OAAO,IAAI,KAAK,SAAS,KAAK,OAAO,MAAM,IAAI,KAAK,OAAO,GAAG;CAC1F;CACA,OAAO,MAAM,KAAK,IAAI;AACxB;AAEA,SAAS,gBAAgB,UAAmC;CAC1D,MAAM,QAAQ;EAAC;EAAuC;EAAI;CAAK;CAC/D,KAAK,MAAM,UAAU,SAAS,UAAU;EACtC,MAAM,OAAO,OAAO;EACpB,IAAI,SAAS,iBAAiB,SAAS,YAAY;EACnD,MAAM,MAAM,OAAO;EACnB,MAAM,OAAO,IAAI,KAAK,OAAO,IAAc,CAAC,CAAC,YAAY,CAAC,CAAC,MAAM,IAAI,EAAE;EACvE,MAAM,KACJ,GAAG,OAAO,OAAO,GAAG,CAAC,CAAC,SAAS,CAAC,EAAE,IAAI,KAAK,KACrC,QAAQ,KAAA,IAAY,YAAY,GAAG,IAAI,GAAA,CAAI,SAAS,CAAC,EAAE,IAAI,SAC/D,YAAY,MAAM,CACtB;CACF;CACA,MAAM,KAAK,KAAK;CAChB,OAAO,MAAM,KAAK,IAAI;AACxB;AAEA,MAAM,kBAAkB;CACtB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAEA,SAAS,YAAY,QAAyC;CAC5D,MAAM,QAAkB,CAAC;CACzB,KAAK,MAAM,OAAO,iBAAiB;EACjC,MAAM,QAAQ,OAAO;EACrB,IAAI,UAAU,KAAA,GAAW;EACzB,MAAM,KAAK,GAAG,IAAI,GAAG,QAAQ,kBAAkB,YAAY,KAAe,IAAI,OAAO;CACvF;CACA,MAAM,QAAQ,OAAO;CACrB,IAAI,OAAO,MAAM,KAAK,SAAS,MAAM,OAAO,GAAG,MAAM,QAAQ;CAC7D,IAAI,OAAO,YAAY,MAAM,KAAK,OAAO,KAAK,UAAU,OAAO,UAAU,GAAG;CAC5E,OAAO,MAAM,SAAS,IAAI,KAAK,MAAM,KAAK,GAAG,MAAM;AACrD;AAEA,MAAM,aAA+C;CACnD;EACE,MAAM;EACN,MAAM;CACR;CACA;EACE,MAAM;EACN,MAAM;CACR;CACA;EACE,MAAM;EACN,MAAM;CACR;CACA;EACE,MAAM;EACN,MAAM;CACR;CACA;EACE,MAAM;EACN,MAAM;CACR;CACA;EACE,MAAM;EACN,MAAM;CACR;CACA;EACE,MAAM;EACN,MAAM;CACR;CACA;EACE,MAAM;EACN,MAAM;CACR;CACA;EACE,MAAM;EACN,MAAM;CACR;CACA;EACE,MAAM;EACN,MAAM;CACR;CACA;EACE,MAAM;EACN,MAAM;CACR;AACF;AAEA,SAAS,iBAAiB,UAAmC;CAC3D,MAAM,QAAQ,IAAI,IAAI,SAAS,SAAS,KAAK,YAAY,QAAQ,IAAI,CAAC;CACtE,MAAM,QAAQ,WAAW,QAAQ,UAAU,MAAM,IAAI,MAAM,IAAI,CAAC,CAAC,CAAC,KAAK,UAAU,MAAM,IAAI;CAC3F,IAAI,SAAS,YAAY,GACvB,MAAM,KACJ,iNACF;CAEF,IAAI,CAAC,SAAS,OAAO,gBACnB,MAAM,KACJ,mIACF;CAEF,IAAI,MAAM,WAAW,GACnB,MAAM,KAAK,4EAA4E;CAEzF,OAAO;EAAC;EAAiB;EAAI,GAAG,MAAM,KAAK,MAAM,UAAU,GAAG,QAAQ,EAAE,IAAI,MAAM;CAAC,CAAC,CAAC,KAAK,IAAI;AAChG;AAEA,SAAS,gBAAgB,SAAyB,UAAgD;CAChG,MAAM,QAAQ,IAAI,IAAI,QAAQ,KAAK,WAAW,CAAC,OAAO,KAAK,MAAM,CAAC,CAAC;CACnE,MAAM,WAAW,SAAS;CAG1B,IAAI,UAAU;EACZ,MAAM,cAAc,MAAM,IAAI,SAAS,GAAG;EAC1C,IAAI,aAAa,KACf,OAAO;GACL,KAAK,SAAS;GACd,MAAM,SAAS;GACf,KAAK,YAAY;EACnB;EAEF,MAAM,UAAU,aAAa;EAC7B,MAAM,OAAO,YAAY,KAAA,IAAY,KAAA,IAAY,MAAM,IAAI,OAAO;EAClE,IAAI,MAAM,KACR,OAAO;GAAE,KAAK,KAAK;GAAK,MAAM,OAAO,KAAK,IAAI;GAAG,KAAK,KAAK;EAA+B;CAE9F;CAKA,MAAM,WAHU,QAAQ,UACrB,WAAW,OAAO,OAAQ,OAAO,UAAoC,MAEjD,KAAK,QAAQ,UAAU,WAAW,OAAO,GAAG;CACnE,OAAO,WACH;EACE,KAAK,SAAS;EACd,MAAM,OAAO,SAAS,IAAI;EAC1B,KAAK,SAAS;CAChB,IACA,KAAA;AACN;AAEA,SAAS,OAAO,YAA4B;CAC1C,OAAO,WAAW,QAAQ,uBAAuB,EAAE;AACrD;AAEA,SAAS,UAAU,QAA0C,QAAQ,IAAY;CAC/E,IAAI,OAAO,WAAW,GAAG,OAAO;CAChC,MAAM,SAAS,OAAO,KAAK,WAAW,OAAO,OAAO,CAAC;CACrD,MAAM,MAAM,KAAK,IAAI,GAAG,MAAM;CAC9B,MAAM,MAAM,KAAK,IAAI,GAAG,MAAM;CAC9B,MAAM,QAAQ;CACd,MAAM,OAAO,KAAK,IAAI,GAAG,KAAK,KAAK,OAAO,SAAS,KAAK,CAAC;CACzD,IAAI,OAAO;CACX,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK,MAAM;EAC5C,MAAM,SAAS,OAAO,MAAM,GAAG,IAAI,IAAI;EACvC,MAAM,OAAO,KAAK,IAAI,GAAG,MAAM;EAC/B,MAAM,QAAQ,QAAQ,MAAM,IAAI,KAAK,OAAQ,OAAO,QAAQ,MAAM,OAAS,CAAiB;EAC5F,QAAQ,MAAM;CAChB;CACA,MAAM,UAAU,OAAO,OAAO,SAAS,EAAE,CAAC,QAAQ,MAAM,OAAO,EAAE,CAAC,QAAQ;CAC1E,OAAO,GAAG,KAAK,IAAI,YAAY,GAAG,EAAE,KAAK,YAAY,GAAG,EAAE,SAAS,SAAS,IAAA,CAAM,QAAQ,CAAC,EAAE,KAC3F,OAAO,OACR;AACH;AAEA,SAAS,MAAM,OAAe,MAAwB;CACpD,OAAO;EAAC,MAAM;EAAS;EAAI;EAAS;EAAiB,GAAG;CAAI,CAAC,CAAC,KAAK,IAAI;AACzE;AAEA,SAAS,IAAI,KAAa,OAAwB;CAChD,OAAO,KAAK,IAAI,KAAK,SAAS,UAAU;AAC1C;AAEA,SAAS,WAAW,OAAuB;CACzC,OAAO,OAAO,SAAS,EAAE,CAAC,CACvB,QAAQ,OAAO,KAAK,CAAC,CACrB,QAAQ,OAAO,GAAG;AACvB"}
|
package/dist/rules.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
//#region src/rules.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Structural faults in a join, read from the recorded shape of its definition.
|
|
4
|
+
*
|
|
5
|
+
* Two faults make a join's output size unbounded and both are visible before any
|
|
6
|
+
* data is touched: siblings that share no axis at all, which is a cartesian
|
|
7
|
+
* product, and siblings whose axes agree on name and type but disagree on
|
|
8
|
+
* domain, where the join key silently fails to match.
|
|
9
|
+
*
|
|
10
|
+
* These run in the analyzer rather than at record time. Nothing is computed on
|
|
11
|
+
* the hot path, the thresholds and the rules can be revised against logs that
|
|
12
|
+
* already exist, and one implementation covers every definition shape: the join
|
|
13
|
+
* nodes are recognised by their discriminator and their children by position, so
|
|
14
|
+
* both the original tree API and the V2 query API are read by the same walk.
|
|
15
|
+
*/
|
|
16
|
+
export type FindingSeverity = "critical" | "high" | "medium" | "low";
|
|
17
|
+
export type StructuralFinding = {
|
|
18
|
+
rule: "cross-join" | "axis-domain-mismatch" | "partial-key-fan-out";
|
|
19
|
+
severity: FindingSeverity;
|
|
20
|
+
/** Position in the definition, e.g. `root/innerJoin[1]`. */
|
|
21
|
+
path: string;
|
|
22
|
+
join: string;
|
|
23
|
+
detail: string;
|
|
24
|
+
rowsUpperBound?: number;
|
|
25
|
+
domains?: {
|
|
26
|
+
domain: string;
|
|
27
|
+
children: number[];
|
|
28
|
+
}[];
|
|
29
|
+
missing?: {
|
|
30
|
+
index: number;
|
|
31
|
+
missing: string[];
|
|
32
|
+
}[];
|
|
33
|
+
};
|
|
34
|
+
export type AxisDescriptor = {
|
|
35
|
+
name: string;
|
|
36
|
+
type: string;
|
|
37
|
+
domain?: Record<string, string>;
|
|
38
|
+
};
|
|
39
|
+
export type JoinShape = {
|
|
40
|
+
join: string;
|
|
41
|
+
path: string;
|
|
42
|
+
childCount: number;
|
|
43
|
+
axisUnion: string[];
|
|
44
|
+
sharedAxes: string[];
|
|
45
|
+
disjointPairs: [number, number][];
|
|
46
|
+
inputRowsMax?: number;
|
|
47
|
+
/** Loose but true: no join of these inputs can exceed the product of their rows. */
|
|
48
|
+
rowsUpperBound?: number;
|
|
49
|
+
};
|
|
50
|
+
/** Structural findings for a recorded definition, most specific first. */
|
|
51
|
+
export declare function structuralFindings(def: unknown): StructuralFinding[];
|
|
52
|
+
/** Shape of every join node in a definition, outermost first. */
|
|
53
|
+
export declare function joinShapes(def: unknown): JoinShape[];
|
|
54
|
+
/**
|
|
55
|
+
* Largest input row count the definition declares, used to judge how much a
|
|
56
|
+
* join amplified. Unknown when no workflow recorded chunk statistics.
|
|
57
|
+
*/
|
|
58
|
+
export declare function inputRowsMax(def: unknown): number | undefined;
|
|
59
|
+
/** Canonical axis identity, matching how join keys are formed. */
|
|
60
|
+
export declare function axisKey(axis: AxisDescriptor): string;
|
|
61
|
+
/** Axis identity ignoring domain, used to spot near-miss axes that fail to join. */
|
|
62
|
+
export declare function axisNameKey(axis: AxisDescriptor): string;
|
|
63
|
+
/** True when a node is a join, by its discriminator. */
|
|
64
|
+
export declare function isJoinNode(node: unknown): boolean;
|
|
65
|
+
/**
|
|
66
|
+
* A join's children, by position. The V2 API wraps each child in `{ entry }`;
|
|
67
|
+
* that wrapper is left in place because every read here descends through it.
|
|
68
|
+
*/
|
|
69
|
+
export declare function joinChildren(node: unknown): unknown[];
|
|
70
|
+
/** Axis descriptors anywhere beneath a node, deduplicated by identity. */
|
|
71
|
+
export declare function axesUnder(node: unknown): AxisDescriptor[];
|
|
72
|
+
//#endregion
|
|
73
|
+
//# sourceMappingURL=rules.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rules.d.ts","names":[],"sources":["../src/rules.ts"],"mappings":";;;;;;;;;;;;;;;YAuBY;YAEA;EACV;EACA,UAAU;;EAEV;EACA;EACA;EACA;EACA;IAAY;IAAgB;;EAC5B;IAAY;IAAe;;;YAGjB;EACV;EACA;EACA,SAAS;;YAGC;EACV;EACA;EACA;EACA;EACA;EACA;EACA;;EAEA;;;wBA6Bc,mBAAmB,eAAe;;wBAOlC,WAAW,eAAe;;;;;wBAU1B,aAAa;;wBAMb,QAAQ,MAAM;;wBAKd,YAAY,MAAM;;wBAKlB,WAAW;;;;;wBAYX,aAAa;;wBASb,UAAU,gBAAgB"}
|