loom-spec 0.3.0 → 0.4.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.
@@ -0,0 +1,25 @@
1
+ export interface ExportHtmlArgs {
2
+ /** Output file path. Default: "loom.html" in cwd. */
3
+ out: string;
4
+ /** Working directory root (walked up to find .loom/). */
5
+ root: string;
6
+ /** If set, only this diagram (plus any timelines that reference it). */
7
+ diagram?: string;
8
+ /** Skip all timelines. Useful for manuals where the static topology is
9
+ * the whole story. */
10
+ noTimelines: boolean;
11
+ /** Only export nodes carrying at least one of these tags. */
12
+ includeTags?: string[];
13
+ /** Drop nodes carrying any of these tags. */
14
+ excludeTags?: string[];
15
+ /** Named bundle from .loom/exports.json. Settings from the bundle become
16
+ * defaults; explicit CLI flags override. */
17
+ bundle?: string;
18
+ }
19
+ /**
20
+ * Used by the bundle-merge logic to tell "user passed --out" from "we are
21
+ * using the default". Exported so the CLI dispatcher can default-init the
22
+ * field once and stay consistent with this module's notion of "default".
23
+ */
24
+ export declare const DEFAULT_OUT = "loom.html";
25
+ export declare function runExportHtml(args: ExportHtmlArgs): Promise<void>;
@@ -0,0 +1,201 @@
1
+ import { readFile, writeFile, stat } from "node:fs/promises";
2
+ import { resolve, dirname } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { findLoomRoot } from "../server/findLoomRoot.js";
5
+ import { listDiagrams, readDiagram, listTimelines, readTimeline, readNodeTypes, } from "../server/fileOps.js";
6
+ import { applyFilter } from "../server/exportFilter.js";
7
+ import { loadExportsConfig } from "../server/exportConfig.js";
8
+ const here = dirname(fileURLToPath(import.meta.url));
9
+ async function fileExists(p) {
10
+ try {
11
+ await stat(p);
12
+ return true;
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }
18
+ /**
19
+ * Locate the export-mode view bundle. Tries both layouts:
20
+ * - Production: alongside cli/ in dist/ → ../view-export/
21
+ * - Dev (tsx running from src/cli/): ../../dist/view-export/
22
+ */
23
+ async function findExportBundle() {
24
+ const candidates = [
25
+ resolve(here, "../view-export"),
26
+ resolve(here, "../../dist/view-export"),
27
+ ];
28
+ for (const c of candidates) {
29
+ if (await fileExists(resolve(c, "index.html")))
30
+ return c;
31
+ }
32
+ throw new Error(`Could not find the export view bundle. Looked in:\n - ${candidates.join("\n - ")}\n` +
33
+ `Did you run 'pnpm build' (or 'pnpm build:export') to produce dist/view-export?`);
34
+ }
35
+ /**
36
+ * Used by the bundle-merge logic to tell "user passed --out" from "we are
37
+ * using the default". Exported so the CLI dispatcher can default-init the
38
+ * field once and stay consistent with this module's notion of "default".
39
+ */
40
+ export const DEFAULT_OUT = "loom.html";
41
+ async function loadAllData(args) {
42
+ const loomRoot = await findLoomRoot(args.root);
43
+ const nodeTypes = await readNodeTypes(loomRoot.loomPath);
44
+ // Diagrams — either one specific or all
45
+ const diagrams = {};
46
+ if (args.diagram) {
47
+ diagrams[args.diagram] = await readDiagram(loomRoot.loomPath, args.diagram);
48
+ }
49
+ else {
50
+ const summaries = await listDiagrams(loomRoot.loomPath);
51
+ for (const s of summaries) {
52
+ diagrams[s.id] = await readDiagram(loomRoot.loomPath, s.id);
53
+ }
54
+ }
55
+ // Timelines — include those that reference at least one diagram we exported
56
+ const timelines = {};
57
+ if (!args.noTimelines) {
58
+ const summaries = await listTimelines(loomRoot.loomPath);
59
+ for (const s of summaries) {
60
+ if (!(s.diagram in diagrams))
61
+ continue;
62
+ timelines[s.id] = await readTimeline(loomRoot.loomPath, s.id);
63
+ }
64
+ }
65
+ return {
66
+ generatedAt: new Date().toISOString(),
67
+ diagrams,
68
+ timelines,
69
+ nodeTypes,
70
+ };
71
+ }
72
+ /**
73
+ * Inline the view bundle (HTML + CSS + JS) and the exported data into a
74
+ * single self-contained HTML string.
75
+ *
76
+ * The export bundle's index.html looks roughly like:
77
+ * <html><head><link rel="stylesheet" href="/assets/bundle.css">
78
+ * <script type="module" crossorigin src="/assets/bundle.js"></script>
79
+ * </head><body><div id="root"></div></body></html>
80
+ *
81
+ * We rewrite the link/script tags to inline contents and inject a
82
+ * <script>window.__LOOM_DATA__ = {...}</script> before the bundle.
83
+ */
84
+ function buildHtml(bundleHtml, bundleCss, bundleJs, data, meta) {
85
+ // Sanitize the JSON for inlining inside a <script> tag.
86
+ // Replace </script> sequences and <!-- inside string values so they can't
87
+ // close the surrounding script element or confuse parsers.
88
+ const safeJson = JSON.stringify(data)
89
+ .replace(/<\/script/gi, "<\\/script")
90
+ .replace(/<!--/g, "<\\!--");
91
+ const inlineDataTag = `<script>window.__LOOM_DATA__ = ${safeJson};</script>`;
92
+ const inlineCssTag = `<style>${bundleCss}</style>`;
93
+ const inlineJsTag = `<script type="module">${bundleJs}</script>`;
94
+ let out = bundleHtml;
95
+ // CRITICAL: pass replacement as a function rather than a string. With a
96
+ // string second arg, JS treats `$$` as a literal `$`, which mangles any
97
+ // `$$typeof` / `$&` / `$'` in the JS bundle (React's reconciler uses
98
+ // `$$typeof` heavily). Function form bypasses the pattern processing.
99
+ out = out.replace(/<link\s+rel=["']stylesheet["'][^>]*>/i, () => inlineCssTag);
100
+ out = out.replace(/<script\s+type=["']module["'][^>]*><\/script>/i, () => inlineDataTag + inlineJsTag);
101
+ out = out.replace(/<title>[^<]*<\/title>/i, () => `<title>loom-spec export · ${escapeHtml(meta.sourceRoot)}</title>`);
102
+ return out;
103
+ }
104
+ function escapeHtml(s) {
105
+ return s
106
+ .replace(/&/g, "&amp;")
107
+ .replace(/</g, "&lt;")
108
+ .replace(/>/g, "&gt;")
109
+ .replace(/"/g, "&quot;");
110
+ }
111
+ export async function runExportHtml(args) {
112
+ // If a bundle name was given, resolve its settings from .loom/exports.json
113
+ // first. Explicit CLI args take precedence (for ad-hoc overrides).
114
+ let effective = args;
115
+ if (args.bundle) {
116
+ const loomRoot = await findLoomRoot(args.root);
117
+ const config = await loadExportsConfig(loomRoot.loomPath);
118
+ if (!config) {
119
+ console.error(`export-html: no .loom/exports.json found, but '${args.bundle}' looks like a named bundle.\n` +
120
+ ` Create .loom/exports.json with an 'exports.${args.bundle}' entry, or pass ad-hoc flags instead.`);
121
+ process.exit(1);
122
+ }
123
+ const bundle = config.exports[args.bundle];
124
+ if (!bundle) {
125
+ const available = Object.keys(config.exports).sort().join(", ") || "(none)";
126
+ console.error(`export-html: bundle '${args.bundle}' not found in .loom/exports.json. ` +
127
+ `Available: ${available}.`);
128
+ process.exit(1);
129
+ }
130
+ // Merge: explicit CLI args (in `args`) override config-supplied values.
131
+ effective = {
132
+ ...args,
133
+ out: args.out !== DEFAULT_OUT ? args.out : bundle.out ?? args.out,
134
+ diagram: args.diagram ?? bundle.diagram,
135
+ noTimelines: args.noTimelines || (bundle.noTimelines ?? false),
136
+ includeTags: args.includeTags ?? bundle.includeTags,
137
+ excludeTags: args.excludeTags ?? bundle.excludeTags,
138
+ };
139
+ }
140
+ const bundleDir = await findExportBundle();
141
+ const [bundleHtml, bundleCss, bundleJs] = await Promise.all([
142
+ readFile(resolve(bundleDir, "index.html"), "utf8"),
143
+ readFile(resolve(bundleDir, "assets/bundle.css"), "utf8"),
144
+ readFile(resolve(bundleDir, "assets/bundle.js"), "utf8"),
145
+ ]);
146
+ const data = await loadAllData(effective);
147
+ if (Object.keys(data.diagrams).length === 0) {
148
+ console.error("export-html: no diagrams found — refusing to write an empty export.");
149
+ process.exit(1);
150
+ }
151
+ const filterSpec = {
152
+ includeTags: effective.includeTags,
153
+ excludeTags: effective.excludeTags,
154
+ };
155
+ const { payload: filtered, summary } = applyFilter(data, filterSpec);
156
+ // After filtering, if everything's gone, bail. Better to fail loud than
157
+ // silently emit a blank HTML the user will wonder about.
158
+ const survivingNodes = Object.values(filtered.diagrams).reduce((n, d) => n + d.nodes.length, 0);
159
+ if (survivingNodes === 0) {
160
+ console.error("export-html: filter matched 0 nodes — refusing to write an empty export.");
161
+ if (filterSpec.includeTags?.length || filterSpec.excludeTags?.length) {
162
+ console.error(` filter was: include=[${(filterSpec.includeTags ?? []).join(", ")}], ` +
163
+ `exclude=[${(filterSpec.excludeTags ?? []).join(", ")}]`);
164
+ }
165
+ process.exit(1);
166
+ }
167
+ const finalData = {
168
+ ...data,
169
+ diagrams: filtered.diagrams,
170
+ timelines: filtered.timelines,
171
+ };
172
+ const html = buildHtml(bundleHtml, bundleCss, bundleJs, finalData, {
173
+ sourceRoot: effective.root,
174
+ });
175
+ const outPath = resolve(effective.out);
176
+ await writeFile(outPath, html, "utf8");
177
+ const sizeKb = Math.round(Buffer.byteLength(html, "utf8") / 1024);
178
+ const diagramCount = Object.keys(finalData.diagrams).length;
179
+ const timelineCount = Object.keys(finalData.timelines).length;
180
+ console.log(`Wrote ${outPath} (${sizeKb} kB): ` +
181
+ `${diagramCount} diagram${diagramCount === 1 ? "" : "s"}, ` +
182
+ `${timelineCount} timeline${timelineCount === 1 ? "" : "s"}.`);
183
+ const droppedParts = [];
184
+ if (summary.nodesDropped > 0)
185
+ droppedParts.push(`${summary.nodesDropped} nodes`);
186
+ if (summary.edgesDropped > 0)
187
+ droppedParts.push(`${summary.edgesDropped} edges`);
188
+ if (summary.groupsDropped > 0)
189
+ droppedParts.push(`${summary.groupsDropped} groups`);
190
+ if (summary.eventsDropped > 0)
191
+ droppedParts.push(`${summary.eventsDropped} events`);
192
+ if (summary.timelinesDropped > 0)
193
+ droppedParts.push(`${summary.timelinesDropped} timelines`);
194
+ if (summary.drillDownsCleared > 0)
195
+ droppedParts.push(`${summary.drillDownsCleared} drill-down refs`);
196
+ if (droppedParts.length > 0) {
197
+ console.log(`Filter dropped: ${droppedParts.join(", ")}.`);
198
+ }
199
+ console.log(`Open it directly in a browser, or drop it into any docs / wiki / GitHub-Pages site.`);
200
+ }
201
+ //# sourceMappingURL=exportHtml.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exportHtml.js","sourceRoot":"","sources":["../../src/cli/exportHtml.ts"],"names":[],"mappings":"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"}
package/dist/cli/index.js CHANGED
@@ -5,6 +5,7 @@ import { runValidate } from "./validate.js";
5
5
  import { runMcp } from "./mcp.js";
6
6
  import { runInstallMcp } from "./installMcp.js";
7
7
  import { runImportTrace } from "./importTrace.js";
8
+ import { runExportHtml, DEFAULT_OUT as EXPORT_DEFAULT_OUT } from "./exportHtml.js";
8
9
  const HELP = `loom-spec — node-based architecture spec for your repo
9
10
 
10
11
  Usage:
@@ -33,6 +34,23 @@ Usage:
33
34
  and timelines (loom_list_timelines, loom_add_event, …) — wire it
34
35
  into Claude Code's mcp.json (or any MCP-capable client).
35
36
 
37
+ loom-spec export-html [<bundle-name>] [--out <path>] [--diagram <id>]
38
+ [--no-timelines]
39
+ [--include-tag <comma-list>] [--exclude-tag <comma-list>]
40
+ [--root <dir>]
41
+ Build a standalone interactive HTML file from the spec — pan/zoom,
42
+ drill-down, switch diagrams, play timelines. Single self-contained
43
+ file, no server needed. Drop it into a manual, wiki, GitHub Pages
44
+ site, anywhere. Output defaults to ./loom.html. With --diagram, only
45
+ that diagram (and timelines referencing it) ship. With
46
+ --include-tag / --exclude-tag, only nodes whose 'tags' match
47
+ survive — edges, groups, drill-down chevrons, timeline events that
48
+ point at dropped nodes are cleaned up automatically.
49
+
50
+ Pass a <bundle-name> as the first positional arg to read settings
51
+ from a named bundle in .loom/exports.json. Explicit flags override
52
+ the bundle's values.
53
+
36
54
  loom-spec import-trace <trace.json> --as <timeline-id> --diagram <diagram-id>
37
55
  [--map <mapping.json>] [--append] [--root <dir>]
38
56
  Read an OpenTelemetry OTLP-JSON trace and generate a timeline that
@@ -46,6 +64,7 @@ Usage:
46
64
  `;
47
65
  function parseFlags(argv) {
48
66
  const flags = {};
67
+ const positional = [];
49
68
  for (let i = 0; i < argv.length; i++) {
50
69
  const a = argv[i];
51
70
  if (!a)
@@ -61,12 +80,15 @@ function parseFlags(argv) {
61
80
  flags[key] = true;
62
81
  }
63
82
  }
83
+ else {
84
+ positional.push(a);
85
+ }
64
86
  }
65
- return flags;
87
+ return { flags, positional };
66
88
  }
67
89
  async function main() {
68
90
  const [, , subcommand, ...rest] = process.argv;
69
- const flags = parseFlags(rest);
91
+ const { flags, positional } = parseFlags(rest);
70
92
  if (!subcommand || subcommand === "--help" || subcommand === "-h") {
71
93
  console.log(HELP);
72
94
  return;
@@ -106,9 +128,29 @@ async function main() {
106
128
  });
107
129
  return;
108
130
  }
131
+ if (subcommand === "export-html") {
132
+ const splitTags = (v) => {
133
+ if (typeof v !== "string" || !v.trim())
134
+ return undefined;
135
+ return v.split(",").map((s) => s.trim()).filter(Boolean);
136
+ };
137
+ // First positional arg, if any, is a named-bundle key from
138
+ // .loom/exports.json (e.g. `loom-spec export-html user-manual`).
139
+ const bundle = positional[0];
140
+ await runExportHtml({
141
+ out: flags.out ?? EXPORT_DEFAULT_OUT,
142
+ root: flags.root ?? process.cwd(),
143
+ diagram: typeof flags.diagram === "string" ? flags.diagram : undefined,
144
+ noTimelines: Boolean(flags["no-timelines"]),
145
+ includeTags: splitTags(flags["include-tag"]),
146
+ excludeTags: splitTags(flags["exclude-tag"]),
147
+ bundle,
148
+ });
149
+ return;
150
+ }
109
151
  if (subcommand === "import-trace") {
110
- // Positional arg: the trace file path (first non-flag in rest).
111
- const trace = rest.find((a) => a && !a.startsWith("--"));
152
+ // Positional arg: the trace file path.
153
+ const trace = positional[0];
112
154
  if (!trace) {
113
155
  console.error("import-trace: missing trace file path");
114
156
  console.log(HELP);
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Optional `.loom/exports.json` config — defines named export bundles so
3
+ * teams can keep export settings versioned in the repo instead of in shell
4
+ * commands. A new contributor runs `loom-spec export-html user-manual`
5
+ * and gets the same output as everyone else without having to remember
6
+ * which flags to pass.
7
+ *
8
+ * Shape:
9
+ *
10
+ * {
11
+ * "exports": {
12
+ * "user-manual": {
13
+ * "include-tags": ["public"],
14
+ * "exclude-tags": ["wip"],
15
+ * "diagram": "overview", // optional, single-diagram mode
16
+ * "no-timelines": true, // default false
17
+ * "out": "docs/architecture.html" // optional, default loom.html
18
+ * },
19
+ * "ops-runbook": {
20
+ * "include-tags": ["ops"]
21
+ * }
22
+ * }
23
+ * }
24
+ *
25
+ * All fields are optional. Unknown keys are tolerated (forward-compat).
26
+ * CLI flags override config values when both are present.
27
+ */
28
+ export interface NamedExport {
29
+ includeTags?: string[];
30
+ excludeTags?: string[];
31
+ diagram?: string;
32
+ noTimelines?: boolean;
33
+ out?: string;
34
+ }
35
+ export interface ExportsFile {
36
+ exports: Record<string, NamedExport>;
37
+ }
38
+ /**
39
+ * Try to read `.loom/exports.json`. Returns null when the file is missing
40
+ * (that's a normal, non-error state — exports config is opt-in).
41
+ */
42
+ export declare function loadExportsConfig(loomPath: string): Promise<ExportsFile | null>;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Optional `.loom/exports.json` config — defines named export bundles so
3
+ * teams can keep export settings versioned in the repo instead of in shell
4
+ * commands. A new contributor runs `loom-spec export-html user-manual`
5
+ * and gets the same output as everyone else without having to remember
6
+ * which flags to pass.
7
+ *
8
+ * Shape:
9
+ *
10
+ * {
11
+ * "exports": {
12
+ * "user-manual": {
13
+ * "include-tags": ["public"],
14
+ * "exclude-tags": ["wip"],
15
+ * "diagram": "overview", // optional, single-diagram mode
16
+ * "no-timelines": true, // default false
17
+ * "out": "docs/architecture.html" // optional, default loom.html
18
+ * },
19
+ * "ops-runbook": {
20
+ * "include-tags": ["ops"]
21
+ * }
22
+ * }
23
+ * }
24
+ *
25
+ * All fields are optional. Unknown keys are tolerated (forward-compat).
26
+ * CLI flags override config values when both are present.
27
+ */
28
+ import { readFile } from "node:fs/promises";
29
+ import { resolve } from "node:path";
30
+ function asStringArray(v, where) {
31
+ if (v === undefined)
32
+ return undefined;
33
+ if (!Array.isArray(v) || !v.every((s) => typeof s === "string")) {
34
+ throw new Error(`exports.json: ${where} must be an array of strings`);
35
+ }
36
+ return v;
37
+ }
38
+ function asString(v, where) {
39
+ if (v === undefined)
40
+ return undefined;
41
+ if (typeof v !== "string") {
42
+ throw new Error(`exports.json: ${where} must be a string`);
43
+ }
44
+ return v;
45
+ }
46
+ function asBool(v, where) {
47
+ if (v === undefined)
48
+ return undefined;
49
+ if (typeof v !== "boolean") {
50
+ throw new Error(`exports.json: ${where} must be a boolean`);
51
+ }
52
+ return v;
53
+ }
54
+ function normalize(raw) {
55
+ const out = { exports: {} };
56
+ const entries = Object.entries(raw.exports ?? {});
57
+ for (const [name, e] of entries) {
58
+ if (!/^[a-z0-9-]+$/.test(name)) {
59
+ throw new Error(`exports.json: bundle name '${name}' must match ^[a-z0-9-]+$`);
60
+ }
61
+ out.exports[name] = {
62
+ includeTags: asStringArray(e["include-tags"], `${name}.include-tags`),
63
+ excludeTags: asStringArray(e["exclude-tags"], `${name}.exclude-tags`),
64
+ diagram: asString(e.diagram, `${name}.diagram`),
65
+ noTimelines: asBool(e["no-timelines"], `${name}.no-timelines`),
66
+ out: asString(e.out, `${name}.out`),
67
+ };
68
+ }
69
+ return out;
70
+ }
71
+ /**
72
+ * Try to read `.loom/exports.json`. Returns null when the file is missing
73
+ * (that's a normal, non-error state — exports config is opt-in).
74
+ */
75
+ export async function loadExportsConfig(loomPath) {
76
+ try {
77
+ const raw = await readFile(resolve(loomPath, "exports.json"), "utf8");
78
+ const parsed = JSON.parse(raw);
79
+ return normalize(parsed);
80
+ }
81
+ catch (e) {
82
+ const code = e.code;
83
+ if (code === "ENOENT")
84
+ return null;
85
+ throw e;
86
+ }
87
+ }
88
+ //# sourceMappingURL=exportConfig.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exportConfig.js","sourceRoot":"","sources":["../../src/server/exportConfig.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Tag-based filter for HTML exports. Used by `loom-spec export-html` to
3
+ * produce scoped bundles (public manual, ops runbook, etc.) from the same
4
+ * `.loom/` source.
5
+ *
6
+ * Semantics:
7
+ *
8
+ * - A node "matches" iff
9
+ * (includeTags is empty OR node has at least one matching include tag)
10
+ * AND
11
+ * (excludeTags is empty OR node has none of the exclude tags)
12
+ *
13
+ * If both lists are empty the filter is a no-op (all nodes match).
14
+ *
15
+ * - Cascade rules (applied in order, so later rules see post-filter state):
16
+ * 1. Drop nodes that don't match.
17
+ * 2. Drop edges whose source or target node was dropped.
18
+ * 3. Drop or shrink groups: if any group's `children` are all dropped,
19
+ * drop the group; otherwise keep with surviving children.
20
+ * 4. Null out `drill_down` chevrons that target diagrams with zero
21
+ * surviving nodes after filtering.
22
+ * 5. Drop timeline events whose referenced node was dropped.
23
+ * 6. Drop timelines that end up with zero events.
24
+ * 7. Null out `triggered_by` refs pointing at dropped events.
25
+ *
26
+ * - Diagrams that end up empty are NOT dropped automatically — the caller
27
+ * decides (often via --diagram for explicit single-diagram exports).
28
+ * Empty diagrams still render an empty canvas, which is visible to the
29
+ * reader and gives them a clue something was filtered out.
30
+ */
31
+ import type { LoomDiagram } from "../types/diagram.js";
32
+ import type { LoomTimeline } from "../types/timeline.js";
33
+ import type { LoomNodeTypes } from "../types/node-types.js";
34
+ export interface FilterSpec {
35
+ includeTags?: string[];
36
+ excludeTags?: string[];
37
+ }
38
+ export interface LoomExportPayload {
39
+ diagrams: Record<string, LoomDiagram>;
40
+ timelines: Record<string, LoomTimeline>;
41
+ nodeTypes: LoomNodeTypes;
42
+ }
43
+ export interface FilterResult {
44
+ payload: LoomExportPayload;
45
+ /** Summary, used by the CLI to print "Dropped N nodes, M edges, …". */
46
+ summary: {
47
+ nodesDropped: number;
48
+ edgesDropped: number;
49
+ groupsDropped: number;
50
+ eventsDropped: number;
51
+ timelinesDropped: number;
52
+ drillDownsCleared: number;
53
+ };
54
+ }
55
+ /**
56
+ * Apply the filter cascade. Returns a deep-enough copy that mutating the
57
+ * result doesn't mutate the input.
58
+ */
59
+ export declare function applyFilter(payload: LoomExportPayload, spec: FilterSpec): FilterResult;