@kanzo-tech/graph 0.2.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.
package/README.md CHANGED
@@ -28,8 +28,15 @@ pnpm add @kanzo-tech/graph @cosmos.gl/graph
28
28
  ```
29
29
 
30
30
  `@cosmos.gl/graph` is a required peer: this is a renderer, and there is nothing left of it without
31
- one. The Mosaic peers are **optional** — `duckBoundedSource()` and `openCorpus()` need them, the
32
- rendering hooks do not, and a host drawing arrays it already has should not pay for a database.
31
+ one. The Mosaic peers and fossil's corpus reader are **optional** — `openCorpus()` needs them and
32
+ the rendering hooks do not.
33
+
34
+ **So the root barrel draws nothing, and that is the shape rather than an oversight.** The reason for
35
+ the split used to be *a host drawing arrays it already has should not pay for a database*, and that
36
+ host no longer exists here: `memorySource` is deleted and this package sends **one** source, the
37
+ corpus'. What the split protects now is a barrel that is a rendering surface — hooks, looks,
38
+ identity, the buffers a slice implies — for a host whose slices arrive from somewhere else, its own
39
+ `BoundedSource` included. A picture costs `@kanzo-tech/graph/duckdb`.
33
40
 
34
41
  ## The two halves
35
42
 
@@ -40,11 +47,12 @@ is **sampled** rather than truncated — one row every `ceil(matched / limit)` o
40
47
  Morton-ordered `dense_id`, which spreads the marks over the window instead of drawing a corner of
41
48
  it. So a view of everything is still a few thousand marks, and they are still everywhere.
42
49
 
43
- `duckBoundedSource` — on `@kanzo-tech/graph/duckdb`, because that is the half that needs Mosaic —
44
- answers over two DuckDB relations and takes the column names as options, so pointing it at another
45
- corpus is a change of argument, not of code. A host that already holds its arrays takes
46
- `memorySource` and pays for no database; under `limit` either one is asked once and never again, so
47
- a graph that fits pays for nothing.
50
+ `openCorpus` — on `@kanzo-tech/graph/duckdb`, because that is the half that needs Mosaic and
51
+ fossil's reader — takes where a corpus is and hands back both halves: the source the canvas draws
52
+ from, and the relations registered under their own names for the charts, the crossfilter and the
53
+ verbs. It takes no column names and no type index; those come off the manifest or they do not come.
54
+ Under `limit` it is asked once for everything and never again, so a corpus that fits pays for
55
+ nothing.
48
56
 
49
57
  **A point is addressed by index and identified by pair.** cosmos.gl numbers points by their position
50
58
  in the arrays it was last handed, so index 7 is whatever the current answer put seventh. A vertex is
package/dist/bounded.d.ts CHANGED
@@ -102,20 +102,15 @@ export interface Slice {
102
102
  }
103
103
  /**
104
104
  * A **region** is a map question: what is inside this rectangle. It suits an overview, a minimap, a
105
- * reader panning across a laid-out corpus, and it is what every source can answer.
105
+ * reader panning across a laid-out corpus, and it is what a corpus can answer by address.
106
106
  *
107
- * A **neighbourhood** is the graph question, and it lives on [`ExploringSource`] rather than here.
108
- * A network has no spatial "near"; it has topological near, and a rectangle cannot express "two hops
109
- * from this node" no matter how it is positioned. Leaving it out of the render contract was a real
110
- * design error — a contract that only spoke rectangles imposed a map metaphor on a network — and
111
- * putting it back as a *variant of the same call* was a second one, which is what this shape fixes.
112
- *
113
- * **The three ways a source used to say "not that question".** A member of a query union, a
114
- * `supports(kind)` predicate, and a `throw` at the top of `slice`. Three spellings of one idea, and
115
- * the only one a caller could act on before making the call was the middle one — so asking a
116
- * relational source for a neighbourhood was a runtime error that typechecked. It is a separate,
117
- * optional method now: a source that cannot walk edges does not have it, and asking is a compile
118
- * error rather than a promise that rejects.
107
+ * **It is the only question this contract asks, and that is a narrowing rather than the natural
108
+ * shape.** A network has no spatial "near"; it has topological near, and a rectangle cannot express
109
+ * "two hops from this node" no matter how it is positioned — so a contract that only speaks
110
+ * rectangles imposes the map metaphor on a network. `ExploringSource` said the second question here
111
+ * and is deleted with the source that answered it; `index.test.ts` carries the tombstone and the
112
+ * seam it named. What it comes back as is fossil's `expand`, addressed rather than joined — not a
113
+ * second variant of this call.
119
114
  */
120
115
  export interface SliceRequest {
121
116
  /** The rectangle. */
@@ -313,26 +308,6 @@ export declare function abortError(message: string): DOMException;
313
308
  * cancelled a question the loop had not aborted.
314
309
  */
315
310
  export declare function isAbort(error: unknown): boolean;
316
- /**
317
- * A source that can also be asked a topological question.
318
- *
319
- * Separate from [`BoundedSource`] rather than optional on it, because *can you walk edges* is a fact
320
- * about a source that a caller should learn from the type rather than from a predicate. A relation
321
- * with `x`/`y` and a spatial index answers regions and nothing else; one that holds adjacency — or
322
- * that can reach fossil's `expand` — answers both.
323
- *
324
- * `useQueryLoop` narrows with `"explore" in source`, which is the check a caller writes once.
325
- */
326
- export interface ExploringSource extends BoundedSource {
327
- explore(request: ExploreRequest): Promise<Slice>;
328
- }
329
- /** Where to start and how far out. Everything else is the same bounding as a region. */
330
- export interface ExploreRequest extends Omit<SliceRequest, "view"> {
331
- /** Where to start, as identities — not buffer indices, which do not survive an answer. */
332
- seeds: VertexId[];
333
- /** How many hops out. One is the ego network; beyond three is usually the whole graph. */
334
- depth: number;
335
- }
336
311
  /**
337
312
  * Whether this graph should be sliced at all.
338
313
  *
@@ -1 +1 @@
1
- {"version":3,"file":"bounded.d.ts","sourceRoot":"","sources":["../src/bounded.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;;;;;;GAOG;AACH,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,KAAK;IACpB,CAAC,EAAE,MAAM,CAAC;IACV;;;;;;;;;;;;;;;;;;;OAmBG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;;;;;;;;OAcG;IACH,QAAQ,EAAE,cAAc,CAAC;IACzB;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,8FAA8F;IAC9F,SAAS,EAAE,YAAY,CAAC;IACxB,mDAAmD;IACnD,KAAK,EAAE,YAAY,CAAC;IACpB,8CAA8C;IAC9C,UAAU,EAAE,WAAW,CAAC;IACxB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,YAAY,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,YAAY;IAC3B,qBAAqB;IACrB,IAAI,EAAE,QAAQ,CAAC;IACf;;;;;;;;;;;;OAYG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,CAAC,CAAC,EAAE,MAAM,CAAC;IACX;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,QAAQ,EAAE,CAAC;IACpB;;;;;;;;;;OAUG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;;;;;;;;;OAeG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;IAC7C;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1B;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC7B;;;;;;;;;;;;;OAaG;IACH,KAAK,CAAC,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACtD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,CAExD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE/C;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,eAAgB,SAAQ,aAAa;IACpD,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;CAClD;AAED,wFAAwF;AACxF,MAAM,WAAW,cAAe,SAAQ,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC;IAChE,0FAA0F;IAC1F,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,0FAA0F;IAC1F,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAE7E;AAED;;;;;;;;;;;;;;;;;GAiBG;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,QAAS,CAAC;AAEpC;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,uBAAuB,IAAI,CAAC"}
1
+ {"version":3,"file":"bounded.d.ts","sourceRoot":"","sources":["../src/bounded.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;;;;;;GAOG;AACH,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,KAAK;IACpB,CAAC,EAAE,MAAM,CAAC;IACV;;;;;;;;;;;;;;;;;;;OAmBG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;;;;;;;;OAcG;IACH,QAAQ,EAAE,cAAc,CAAC;IACzB;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,8FAA8F;IAC9F,SAAS,EAAE,YAAY,CAAC;IACxB,mDAAmD;IACnD,KAAK,EAAE,YAAY,CAAC;IACpB,8CAA8C;IAC9C,UAAU,EAAE,WAAW,CAAC;IACxB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,YAAY,CAAC;CACtB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,YAAY;IAC3B,qBAAqB;IACrB,IAAI,EAAE,QAAQ,CAAC;IACf;;;;;;;;;;;;OAYG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,CAAC,CAAC,EAAE,MAAM,CAAC;IACX;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,QAAQ,EAAE,CAAC;IACpB;;;;;;;;;;OAUG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;;;;;;;;;OAeG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;IAC7C;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1B;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC7B;;;;;;;;;;;;;OAaG;IACH,KAAK,CAAC,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACtD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,CAExD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE/C;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAE7E;AAED;;;;;;;;;;;;;;;;;GAiBG;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,QAAS,CAAC;AAEpC;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,uBAAuB,IAAI,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"bounded.js","sources":["../src/bounded.ts"],"sourcesContent":["/**\n * A graph you never hold all of.\n *\n * `load()` is the other kind: it reads a relation whole, turns it into typed arrays, and hands the\n * lot to the renderer. Measured, that costs 389 ms at 200,000 nodes and stops being viable somewhere\n * short of a million — the working set is N, so the ceiling is whatever N the machine can hold.\n *\n * This is the shape that has no such ceiling: something asks a bounded question — a rectangle, or a\n * neighbourhood a few hops wide — and the source answers with **at most `limit` points**. The\n * answer's size follows the question rather than the corpus. Moving re-asks. A window holding more\n * than `limit` is *sampled* rather than truncated, so a view of everything is still a few thousand\n * marks and they are still spread over everything — see `sampled` in `duck-source.ts`.\n *\n * **Positions are authority, not suggestion.** The corpus is written once and read many — OLAP, which\n * is what GraphAr is for — so the coordinates the batch emits are the index every spatial question\n * is asked against. That settles a tension an earlier draft of this file waved at rather than\n * resolved: re-laying-out a slice live would move points out from under the very coordinates the\n * next query is expressed in, and the camera would drift away from the index within one frame of\n * the first force. **Do not re-lay-out a slice.** If a layout is wrong, it is wrong upstream, and it\n * is fixed by recompiling — the graph is a compiler's output and so is its geometry.\n *\n * **Dragging is the exception, and it is a local overlay.** A reader can move a node; that changes\n * where it is *drawn*, never where it is *indexed*. The consequence is small and real: drag a node\n * far away, pan to where you dropped it, and the spatial query does not know it is there. Which is\n * why `pinned` exists below — the few points a reader has taken hold of ride along with every\n * slice, regardless of the rectangle.\n *\n * **Deliberately not a format.** A source is anything that can answer that question: Parquet\n * fetched by address, a plain relation with `x`/`y` columns and a spatial predicate, or an\n * in-memory index. This package renders; it does not learn a storage layout. The\n * wiring between a particular source and this contract belongs at the call site.\n *\n * **Dense indices, not database ids.** `links` refers to positions in `positions`, so a consumer\n * never pays for an id→index map — the 148 ms that mapping costs at 200,000 nodes is not optimised\n * here, it is designed away. Sources that already number their vertices densely (GraphAr's\n * `dense_id` does) hand this over for free.\n */\n\nimport type { VertexId } from \"./resident\";\n\n/**\n * What the camera is looking at, in the graph's own coordinate space.\n *\n * A rectangle and nothing else. It carried a `zoom` for one reader — the level-of-detail threshold\n * a source compared it against to decide whether to answer with super-nodes — and that branch is\n * gone, so the field went with it rather than staying as a number every caller has to invent and\n * nothing reads.\n */\nexport interface Viewport {\n xMin: number;\n yMin: number;\n xMax: number;\n yMax: number;\n}\n\n/**\n * One answer. Every array is parallel and indexed densely from zero.\n *\n * `n` is what *matched*, before `limit` cut it — the difference between the two is how a view says\n * \"there is more here than I am showing you\", which is the one honest thing a bounded renderer owes\n * its reader. When a window holds more than `limit`, what comes back is a **sample** of it rather\n * than its first `limit` rows; `n` reports the window either way.\n *\n * **A struct, and it used to be a tagged union.** `mode` picked between points and super-nodes and\n * only the second branch carried `weights`. Both are gone — see `/docs/design/graph` — and with one\n * branch left a discriminant is a\n * field with one legal value.\n */\nexport interface Slice {\n n: number;\n /**\n * How many of `positions` are **marks** — points a reader can see. Everything past it is an\n * *anchor*.\n *\n * An anchor is a real vertex at its real coordinates that the window did not return: it is in the\n * buffers so that an edge leaving the window has somewhere to end. It is never drawn — `buffers`\n * gives it radius zero and alpha zero, and `residentOf` stops here, so nothing hovers, selects or\n * frames one.\n *\n * **Why the far end is the vertex rather than a point on the border.** A stub clipped to the\n * viewport carries the right direction and *lies about the distance*, and a reader cannot tell a\n * stub that ends 1.1 window-widths out from one that ends 47 — measured over all the far ends a\n * window loses, 20–22% are past four semi-widths and the worst is 47.1\n * (`.planning/FAR-VIEW-AND-EDGES.md`). Drawing the vertex where it is cannot lie, and it is also\n * the cheaper of the two: a clipped stub is one point *per edge*, an anchor is one point per far\n * *vertex*.\n *\n * Equal to `positions.length / 2` for a source that answers with marks only, which is why it is\n * required rather than optional — a caller that reads it always gets the count it meant.\n */\n marks: number;\n /**\n * Who each returned point *is*, parallel to `positions` — the `(type_idx, dense_id)` pair packed\n * by `vertexId`.\n *\n * Present because a buffer index is **not stable across answers**: index 7 is a different vertex\n * after a pan. Anything that outlives one answer — a selection, a focused node, a pinned set — has\n * to be held as an identity and re-resolved through `residentOf` each time. Leaving this out was\n * how the first draft would have shipped a selection that silently pointed at the wrong nodes.\n *\n * `BigUint64Array` because the pair is 64 bits exactly — a `Uint32Array` cannot hold it at all,\n * and a `Float64Array` holds it only while the type index stays under 2²¹, which is a ceiling\n * nobody would find until they crossed it. Still a typed array, so it is still one allocation and\n * still transferable; only what it carries changed. A dense id on its own is not an identity: it\n * numbers within one vertex type, so a union of two types repeats every value.\n */\n vertices: BigUint64Array;\n /**\n * The subject IRI of each returned point, parallel to `vertices` — **opt-in, and absent by\n * default.**\n *\n * `vertices` says where a point *is*; this says which vertex it *is*. They are not the same thing\n * and the corpus is explicit about it: redoing a layout renumbers every vertex, so a `dense_id`\n * held outside the corpus names a different vertex after the next write. Anything that has to\n * survive a recompile — a bookmark, a link out, a row in somebody else's database — keys on the\n * IRI. A selection held as `VertexId` survives a pan and does not survive a rebuild.\n *\n * **Absent by default because it costs 1.87× the tile, measured on the corpus side.** Compressed\n * bytes per row at five million: `subject` 8.016 against `dense_id` 4.000, `x` 2.717, `y` 2.501.\n * The four drawing columns are 9.23 B/row and become 17.25 with it. So the drawing path carries\n * addresses, and a host asks for names when something has to be *named* rather than painted.\n *\n * A `string[]` rather than a typed array, because that is what an IRI is. It is the one thing in a\n * `Slice` that does not go to the GPU, which is exactly why it is optional: a host that never\n * names a vertex should not pay to move it.\n */\n subjects?: string[];\n /** `[x0, y0, x1, y1, …]`, one pair per returned point — `marks` of them, then the anchors. */\n positions: Float32Array;\n /** `[src, dst, …]` as indices into `positions`. */\n links: Float32Array;\n /** Per-point category ordinal, for colour. */\n categories: Uint16Array;\n /**\n * What the size ramp is spent on, per point — a degree, a count, whatever the corpus ranks by.\n *\n * Optional because a source may not have one, and a graph drawn at one radius is a legitimate\n * picture. But without it a look's `form.size` range has only one end, so a source that can afford\n * the column should send it: it is the difference between seeing a hub and counting one.\n */\n sizes?: Float32Array;\n}\n\n/**\n * A **region** is a map question: what is inside this rectangle. It suits an overview, a minimap, a\n * reader panning across a laid-out corpus, and it is what every source can answer.\n *\n * A **neighbourhood** is the graph question, and it lives on [`ExploringSource`] rather than here.\n * A network has no spatial \"near\"; it has topological near, and a rectangle cannot express \"two hops\n * from this node\" no matter how it is positioned. Leaving it out of the render contract was a real\n * design error — a contract that only spoke rectangles imposed a map metaphor on a network — and\n * putting it back as a *variant of the same call* was a second one, which is what this shape fixes.\n *\n * **The three ways a source used to say \"not that question\".** A member of a query union, a\n * `supports(kind)` predicate, and a `throw` at the top of `slice`. Three spellings of one idea, and\n * the only one a caller could act on before making the call was the middle one — so asking a\n * relational source for a neighbourhood was a runtime error that typechecked. It is a separate,\n * optional method now: a source that cannot walk edges does not have it, and asking is a compile\n * error rather than a promise that rejects.\n */\nexport interface SliceRequest {\n /** The rectangle. */\n view: Viewport;\n /**\n * Which column colours a point — Plot's channel name, and Plot's meaning.\n *\n * **On the request rather than on the source, and that is the whole shape.** A source says *where\n * the bytes are*; a request says *what I want to draw*, and which column colours is the second.\n * Baked into a source at construction — which is where it used to live — changing what a graph is\n * coloured by meant building a new source, and the two are not the same question.\n *\n * It is the reference's own arrangement: in Plot the **mark** carries the channels and the mark is\n * what produces the query, while the data source only says where rows come from.\n *\n * Defaults to `community`, which every corpus has because the layout pass writes it.\n */\n fill?: string;\n /**\n * Which column the size ramp is spent on — Plot's `r`.\n *\n * Omitted, every point is drawn at one radius, which is a legitimate picture: without a ramp a\n * look's `form.size` range has only one end.\n */\n r?: string;\n /**\n * Vertices that must come back whatever the query says.\n *\n * The set a reader has taken hold of — dragged, pinned, selected, focused. Their drawn positions\n * are a view-local overlay on coordinates that never move, so the index cannot find them where\n * they now appear. Carrying them explicitly is cheaper and more honest than making the index\n * mutable: it is a handful of identities, and the alternative is a spatial structure that has to be\n * rewritten every time somebody drags something.\n */\n pinned?: VertexId[];\n /**\n * The most points the source may return.\n *\n * **Above it a source samples the window; it does not take the front of it.** Which is the second\n * half of `n`'s honesty: `n` says how many matched, and this says the answer is a *sample* of\n * those rather than whichever ones an `ORDER BY` happened to put first.\n *\n * Required, and always filled: a host's `limit` is optional all the way down to `useQueryLoop`,\n * which resolves it before the question leaves. A source never has to know what this package\n * would have chosen.\n */\n limit: number;\n /**\n * The shortest edge worth a row, **in screen pixels** — multiply by `perPixel` for a length in the\n * graph's own space.\n *\n * **What it buys is on `perPixel` below**: two of every three edges a five-million-node window\n * draws are under one pixel long, and discarding everything under three sends a third of the rows\n * for an identical picture.\n *\n * Required for the reason `limit` is, and it is the field this whole shape exists for. It used to\n * live on an exported `BOUNDED_DEFAULTS` that a source read to find out what it was being asked —\n * so a request arrived incomplete and every source finished it in its own words. There were two,\n * one of them in another repository. Now the loop resolves it and a source reads it here.\n *\n * Meaningless without `perPixel`, and a source with no resolution discards nothing: a threshold in\n * pixels with no pixels is not a threshold.\n */\n minLinkPixels: number;\n /**\n * How much of the graph's own space one screen pixel covers — the resolution the answer is going\n * to be looked at.\n *\n * **What it buys: an edge shorter than three pixels is not sent.** Two of every three edges a\n * five-million-node window draws are under one pixel long — they are a dot on top of their own\n * endpoints, which the point layer has already drawn. Discarding everything under 3 px sends\n * 27.5–35.3% of the rows and leaves 99.9–100% of the inked pixels identical\n * (`.planning/FAR-VIEW-AND-EDGES.md`, measured 2026-08-17).\n *\n * **A row discard, not a fade.** cosmos.gl's `linkVisibilityDistanceRange` already dims a short\n * link, and dimming happens after the row has been joined, returned, uploaded and rasterised. This\n * is the same picture without the work.\n *\n * The rectangle alone cannot say it: the same rectangle over a 400-pixel canvas and a 4,000-pixel\n * one are different questions, and only the caller knows which. Omitted — a source is asked for\n * everything, or by something with no canvas — nothing is discarded, because a threshold in pixels\n * with no pixels is not a threshold.\n */\n perPixel?: number;\n /**\n * Stop: the caller does not want this answer any more.\n *\n * **A cancelled question rejects with `signal.reason`, and that is the whole contract.** The\n * camera moves faster than a database answers, so a source that can only hold one question at a\n * time is routinely asked a second before the first has landed. The first caller is still holding\n * a promise; dropping it leaves that caller's `finally` unrun, which in `useQueryLoop` reads as a\n * query permanently in flight for the rest of the session. So the stale promise is **settled**,\n * and this says with what.\n *\n * `AbortController.abort()` puts a `DOMException` named `AbortError` in `reason`, and `throw\n * signal.reason` is the whole implementation. A source that cancels for its own reasons — one\n * standing question re-aimed by a newer caller, a connection let go — throws an `AbortError` it\n * builds itself, which is what `abortError()` below is for.\n *\n * **This replaced a sentinel of ours, and the argument for the sentinel was real.** `SUPERSEDED`\n * was an exported `Symbol` with `isSuperseded` beside it, on the reasoning that *you moved on* and\n * *the database said no* are the two things a query loop must tell apart, and a string comparison\n * against a thrown value goes stale with nothing failing. All of that is true and none of it is an\n * argument for a *private* sentinel: `AbortError` is the name the platform already gives that\n * distinction, `fetch` rejects with it, and every third-party async primitive a source is written\n * over — `fetch`, `AbortSignal.timeout`, a WHATWG stream — produces one without being told to.\n * Ours meant a source had to import a symbol from us to be cancellable at all, and a source that\n * simply passed the signal to `fetch` did the standard thing and was reported to the host as a\n * failure. The comparison is against `error.name`, which is the platform's contract rather than a\n * message.\n */\n signal?: AbortSignal;\n}\n\n/**\n * Anything that can answer \"what is in this rectangle, at this zoom, in at most this many marks\".\n *\n * One method, one question. A source that also wants search, aggregation or paths is describing a\n * query surface rather than a render path, and there is already one of those — fossil's verbs. The\n * line: this answers *what should I draw*, and nothing about *what does it mean*.\n */\nexport interface BoundedSource {\n slice(request: SliceRequest): Promise<Slice>;\n /**\n * How many vertices there are in total, if the source knows cheaply.\n *\n * The reason this exists is the one real cost of bounding: **a graph that fits pays for nothing.**\n * Unbounded loads once and then pans on the GPU for free; bounded issues a query per camera move.\n * At 200,000 nodes that trade is overwhelmingly worth it — 1,017 ms of first paint against roughly\n * 130 ms — but at 2,000 it is pure loss, tens of milliseconds of latency per pan buying a ceiling\n * nobody was near.\n *\n * So a consumer asks first. Under the limit, take one slice covering everything and never ask\n * again: same code path, and panning is free exactly when it can be.\n */\n total?(): Promise<number>;\n /**\n * The rectangle the corpus occupies, when the source can say cheaply.\n *\n * **Framing the opening view is not the host's job, and treating it as one is measured.** The\n * archive occupies `x ∈ [1843, 2253]` of a 4,096-wide space — 1% of its area — and a camera that\n * opens on the space rather than on the data draws 1,543 points into a tenth of the viewport, at\n * two to nine pixels each, under a fog of links. Every point is uploaded and none is legible, which\n * a reader reports as *the nodes are not rendering*.\n *\n * It matters more for a bounded source than for a whole one: the first question a sliced graph\n * asks is *what is the camera over*, so a camera pointing at empty space is a first paint of\n * nothing. Framing before asking is the difference between one query and none.\n *\n * Optional, because a source over an unlaid-out relation has no answer — and cheap where it\n * exists: a corpus reads it off tile footers it was going to read anyway, and a relation with\n * `x`/`y` gets it from four aggregates.\n */\n extent?(): Promise<Viewport>;\n /**\n * Say when the answer changes for a reason the camera cannot see, and hold the source's resources\n * for as long as anybody is listening.\n *\n * **A whole slice rather than a nudge to ask again, and that is what makes it worth having.** The\n * reason a bounded answer changes on its own is that the page filtered something — somebody\n * brushed a histogram, a panel picked a value — and a source that lives inside a crossfilter is\n * *told* by the coordinator, which has already re-run the reads with the new predicate by the time\n * this fires. Asking again would issue the same two queries a second time to learn what is in hand.\n *\n * The returned function is also the release: it is where a source lets go of whatever it holds —\n * a client registration, a connection, a cache — so a loop that calls this is a loop that cannot\n * leak one. Optional, because a source over arrays holds nothing and changes for nothing.\n */\n watch?(answered: (slice: Slice) => void): () => void;\n}\n\n/**\n * A cancellation this source is raising itself, in the platform's own shape.\n *\n * For the case a `signal` cannot express: a source holding **one** standing question, re-aimed by a\n * newer caller. There is no signal for the caller that lost — its request was never aborted, it was\n * simply overtaken — so the source builds the rejection, and it builds the same kind the platform\n * would have. `message` says what overtook it; `name` is what anybody tests.\n *\n * Not on the barrel. A source outside this package cancels by passing the `signal` it was handed to\n * whatever it is waiting on, or by `throw signal.reason` — both of which produce an `AbortError`\n * with nothing imported from us. This exists for the one case that has no signal to reach for, and\n * `new DOMException(message, \"AbortError\")` is the whole of it if a third source ever needs it too.\n */\nexport function abortError(message: string): DOMException {\n return new DOMException(message, \"AbortError\");\n}\n\n/**\n * Whether a rejection means *you moved on* rather than *the answer failed*.\n *\n * `error.name` and not `instanceof`: the same `AbortError` reaches here as a `DOMException` from\n * `AbortController`, as one of ours from `abortError`, and — for a source written over `fetch` in\n * another realm, an iframe or a worker — as an object no `instanceof` in this realm matches. The\n * name is what WHATWG specifies and what every producer agrees on.\n *\n * Not on the barrel either, for the same reason as `abortError`: a caller of this package awaits\n * `slice()` behind an `AbortController` it owns, so `controller.signal.aborted` already answers the\n * question for it. This is the branch `useQueryLoop` needs for the *other* half — a source that\n * cancelled a question the loop had not aborted.\n */\nexport function isAbort(error: unknown): boolean {\n return typeof error === \"object\" && error !== null && (error as { name?: unknown }).name === \"AbortError\";\n}\n\n/**\n * A source that can also be asked a topological question.\n *\n * Separate from [`BoundedSource`] rather than optional on it, because *can you walk edges* is a fact\n * about a source that a caller should learn from the type rather than from a predicate. A relation\n * with `x`/`y` and a spatial index answers regions and nothing else; one that holds adjacency — or\n * that can reach fossil's `expand` — answers both.\n *\n * `useQueryLoop` narrows with `\"explore\" in source`, which is the check a caller writes once.\n */\nexport interface ExploringSource extends BoundedSource {\n explore(request: ExploreRequest): Promise<Slice>;\n}\n\n/** Where to start and how far out. Everything else is the same bounding as a region. */\nexport interface ExploreRequest extends Omit<SliceRequest, \"view\"> {\n /** Where to start, as identities — not buffer indices, which do not survive an answer. */\n seeds: VertexId[];\n /** How many hops out. One is the ego network; beyond three is usually the whole graph. */\n depth: number;\n}\n\n/**\n * Whether this graph should be sliced at all.\n *\n * `undefined` when the source cannot say cheaply — in which case slice, because an unknown corpus is\n * more likely to be the large kind than not.\n */\nexport function shouldSlice(total: number | undefined, limit: number): boolean {\n return total === undefined || total > limit;\n}\n\n/**\n * What a question means when the host says nothing — **resolved before a source ever sees it.**\n *\n * These were `BOUNDED_DEFAULTS`, an exported table, and a source read it to find out what it was\n * being asked. That is the defect, stated plainly: a request that arrives unresolved is an\n * *incomplete* request, and every source outside this package had to know a constant of ours to\n * finish it. Two of them did — `duck-source.ts` here, and fossil's tile reader — and each finished\n * it in its own words, which is two chances to disagree about one number.\n *\n * `useQueryLoop` fills both in on every call now, so `limit` and `minLinkPixels` are **required**\n * fields of `SliceRequest`: a source reads `request.limit` and is done. The numbers stay module\n * scoped and off the barrel, because nobody outside needs to look them up any more.\n *\n * The camera→rectangle conversion that used to live beside them is gone for the sibling reason:\n * cosmos.gl owns the screen↔space transform and answers it through `screenToSpacePosition`, so\n * deriving the rectangle from the camera and the space size was a second implementation of the\n * renderer's own maths, free to drift from it. `useQueryLoop` asks the renderer instead.\n */\n\n/**\n * Twenty thousand marks.\n *\n * Above about 50,000 points a live layout stops being comfortable and the edge layer is already\n * fog well before that, so a limit far below the renderer's ceiling is not a compromise — it is\n * the legibility ceiling, which arrives first and is the one a reader actually meets.\n */\nexport const DEFAULT_LIMIT = 20_000;\n\n/**\n * The shortest edge worth a row — three screen pixels.\n *\n * Measured over the drawn edges of five windows per corpus, 2026-08-17: the median edge is 1.60 px\n * at 200k, 1.47 at a million and **0.52 at five million**, where 64.6% are under one pixel. An edge\n * that short is a dot on top of two dots the point layer has already drawn. Discarding under 3 px\n * sends 27.5–35.3% of the rows and leaves 99.9–100% of the inked pixels identical — the working is\n * in `.planning/FAR-VIEW-AND-EDGES.md`.\n *\n * Three rather than two because both were measured against the same five windows of\n * `docs/public/bench/1000000`: 2 px sends 5.6–25.8% more rows, mean 19%, for the same 0.1% of\n * image.\n *\n * **It is on `SliceRequest` now, and the argument against that has been overtaken.** It used to say:\n * one call site, and a knob with one call site is a knob nobody has an opinion about. There were\n * two call sites by then and one of them was in another repository, both reading the constant off\n * the barrel to reconstruct the same product. Whether it is a *knob* is still open — no caller\n * overrides it — but it is a **fact about the question**, and a question carries its own facts.\n */\nexport const DEFAULT_MIN_LINK_PIXELS = 3;\n"],"names":["abortError","message","isAbort","error","shouldSlice","total","limit","DEFAULT_LIMIT","DEFAULT_MIN_LINK_PIXELS"],"mappings":"AAuVO,SAASA,EAAWC,GAA+B;AACxD,SAAO,IAAI,aAAaA,GAAS,YAAY;AAC/C;AAeO,SAASC,EAAQC,GAAyB;AAC/C,SAAO,OAAOA,KAAU,YAAYA,MAAU,QAASA,EAA6B,SAAS;AAC/F;AA8BO,SAASC,EAAYC,GAA2BC,GAAwB;AAC7E,SAAOD,MAAU,UAAaA,IAAQC;AACxC;AA4BO,MAAMC,IAAgB,KAqBhBC,IAA0B;"}
1
+ {"version":3,"file":"bounded.js","sources":["../src/bounded.ts"],"sourcesContent":["/**\n * A graph you never hold all of.\n *\n * `load()` is the other kind: it reads a relation whole, turns it into typed arrays, and hands the\n * lot to the renderer. Measured, that costs 389 ms at 200,000 nodes and stops being viable somewhere\n * short of a million — the working set is N, so the ceiling is whatever N the machine can hold.\n *\n * This is the shape that has no such ceiling: something asks a bounded question — a rectangle, or a\n * neighbourhood a few hops wide — and the source answers with **at most `limit` points**. The\n * answer's size follows the question rather than the corpus. Moving re-asks. A window holding more\n * than `limit` is *sampled* rather than truncated, so a view of everything is still a few thousand\n * marks and they are still spread over everything — see `sampled` in `duck-source.ts`.\n *\n * **Positions are authority, not suggestion.** The corpus is written once and read many — OLAP, which\n * is what GraphAr is for — so the coordinates the batch emits are the index every spatial question\n * is asked against. That settles a tension an earlier draft of this file waved at rather than\n * resolved: re-laying-out a slice live would move points out from under the very coordinates the\n * next query is expressed in, and the camera would drift away from the index within one frame of\n * the first force. **Do not re-lay-out a slice.** If a layout is wrong, it is wrong upstream, and it\n * is fixed by recompiling — the graph is a compiler's output and so is its geometry.\n *\n * **Dragging is the exception, and it is a local overlay.** A reader can move a node; that changes\n * where it is *drawn*, never where it is *indexed*. The consequence is small and real: drag a node\n * far away, pan to where you dropped it, and the spatial query does not know it is there. Which is\n * why `pinned` exists below — the few points a reader has taken hold of ride along with every\n * slice, regardless of the rectangle.\n *\n * **Deliberately not a format.** A source is anything that can answer that question: Parquet\n * fetched by address, a plain relation with `x`/`y` columns and a spatial predicate, or an\n * in-memory index. This package renders; it does not learn a storage layout. The\n * wiring between a particular source and this contract belongs at the call site.\n *\n * **Dense indices, not database ids.** `links` refers to positions in `positions`, so a consumer\n * never pays for an id→index map — the 148 ms that mapping costs at 200,000 nodes is not optimised\n * here, it is designed away. Sources that already number their vertices densely (GraphAr's\n * `dense_id` does) hand this over for free.\n */\n\nimport type { VertexId } from \"./resident\";\n\n/**\n * What the camera is looking at, in the graph's own coordinate space.\n *\n * A rectangle and nothing else. It carried a `zoom` for one reader — the level-of-detail threshold\n * a source compared it against to decide whether to answer with super-nodes — and that branch is\n * gone, so the field went with it rather than staying as a number every caller has to invent and\n * nothing reads.\n */\nexport interface Viewport {\n xMin: number;\n yMin: number;\n xMax: number;\n yMax: number;\n}\n\n/**\n * One answer. Every array is parallel and indexed densely from zero.\n *\n * `n` is what *matched*, before `limit` cut it — the difference between the two is how a view says\n * \"there is more here than I am showing you\", which is the one honest thing a bounded renderer owes\n * its reader. When a window holds more than `limit`, what comes back is a **sample** of it rather\n * than its first `limit` rows; `n` reports the window either way.\n *\n * **A struct, and it used to be a tagged union.** `mode` picked between points and super-nodes and\n * only the second branch carried `weights`. Both are gone — see `/docs/design/graph` — and with one\n * branch left a discriminant is a\n * field with one legal value.\n */\nexport interface Slice {\n n: number;\n /**\n * How many of `positions` are **marks** — points a reader can see. Everything past it is an\n * *anchor*.\n *\n * An anchor is a real vertex at its real coordinates that the window did not return: it is in the\n * buffers so that an edge leaving the window has somewhere to end. It is never drawn — `buffers`\n * gives it radius zero and alpha zero, and `residentOf` stops here, so nothing hovers, selects or\n * frames one.\n *\n * **Why the far end is the vertex rather than a point on the border.** A stub clipped to the\n * viewport carries the right direction and *lies about the distance*, and a reader cannot tell a\n * stub that ends 1.1 window-widths out from one that ends 47 — measured over all the far ends a\n * window loses, 20–22% are past four semi-widths and the worst is 47.1\n * (`.planning/FAR-VIEW-AND-EDGES.md`). Drawing the vertex where it is cannot lie, and it is also\n * the cheaper of the two: a clipped stub is one point *per edge*, an anchor is one point per far\n * *vertex*.\n *\n * Equal to `positions.length / 2` for a source that answers with marks only, which is why it is\n * required rather than optional — a caller that reads it always gets the count it meant.\n */\n marks: number;\n /**\n * Who each returned point *is*, parallel to `positions` — the `(type_idx, dense_id)` pair packed\n * by `vertexId`.\n *\n * Present because a buffer index is **not stable across answers**: index 7 is a different vertex\n * after a pan. Anything that outlives one answer — a selection, a focused node, a pinned set — has\n * to be held as an identity and re-resolved through `residentOf` each time. Leaving this out was\n * how the first draft would have shipped a selection that silently pointed at the wrong nodes.\n *\n * `BigUint64Array` because the pair is 64 bits exactly — a `Uint32Array` cannot hold it at all,\n * and a `Float64Array` holds it only while the type index stays under 2²¹, which is a ceiling\n * nobody would find until they crossed it. Still a typed array, so it is still one allocation and\n * still transferable; only what it carries changed. A dense id on its own is not an identity: it\n * numbers within one vertex type, so a union of two types repeats every value.\n */\n vertices: BigUint64Array;\n /**\n * The subject IRI of each returned point, parallel to `vertices` — **opt-in, and absent by\n * default.**\n *\n * `vertices` says where a point *is*; this says which vertex it *is*. They are not the same thing\n * and the corpus is explicit about it: redoing a layout renumbers every vertex, so a `dense_id`\n * held outside the corpus names a different vertex after the next write. Anything that has to\n * survive a recompile — a bookmark, a link out, a row in somebody else's database — keys on the\n * IRI. A selection held as `VertexId` survives a pan and does not survive a rebuild.\n *\n * **Absent by default because it costs 1.87× the tile, measured on the corpus side.** Compressed\n * bytes per row at five million: `subject` 8.016 against `dense_id` 4.000, `x` 2.717, `y` 2.501.\n * The four drawing columns are 9.23 B/row and become 17.25 with it. So the drawing path carries\n * addresses, and a host asks for names when something has to be *named* rather than painted.\n *\n * A `string[]` rather than a typed array, because that is what an IRI is. It is the one thing in a\n * `Slice` that does not go to the GPU, which is exactly why it is optional: a host that never\n * names a vertex should not pay to move it.\n */\n subjects?: string[];\n /** `[x0, y0, x1, y1, …]`, one pair per returned point — `marks` of them, then the anchors. */\n positions: Float32Array;\n /** `[src, dst, …]` as indices into `positions`. */\n links: Float32Array;\n /** Per-point category ordinal, for colour. */\n categories: Uint16Array;\n /**\n * What the size ramp is spent on, per point — a degree, a count, whatever the corpus ranks by.\n *\n * Optional because a source may not have one, and a graph drawn at one radius is a legitimate\n * picture. But without it a look's `form.size` range has only one end, so a source that can afford\n * the column should send it: it is the difference between seeing a hub and counting one.\n */\n sizes?: Float32Array;\n}\n\n/**\n * A **region** is a map question: what is inside this rectangle. It suits an overview, a minimap, a\n * reader panning across a laid-out corpus, and it is what a corpus can answer by address.\n *\n * **It is the only question this contract asks, and that is a narrowing rather than the natural\n * shape.** A network has no spatial \"near\"; it has topological near, and a rectangle cannot express\n * \"two hops from this node\" no matter how it is positioned — so a contract that only speaks\n * rectangles imposes the map metaphor on a network. `ExploringSource` said the second question here\n * and is deleted with the source that answered it; `index.test.ts` carries the tombstone and the\n * seam it named. What it comes back as is fossil's `expand`, addressed rather than joined — not a\n * second variant of this call.\n */\nexport interface SliceRequest {\n /** The rectangle. */\n view: Viewport;\n /**\n * Which column colours a point — Plot's channel name, and Plot's meaning.\n *\n * **On the request rather than on the source, and that is the whole shape.** A source says *where\n * the bytes are*; a request says *what I want to draw*, and which column colours is the second.\n * Baked into a source at construction — which is where it used to live — changing what a graph is\n * coloured by meant building a new source, and the two are not the same question.\n *\n * It is the reference's own arrangement: in Plot the **mark** carries the channels and the mark is\n * what produces the query, while the data source only says where rows come from.\n *\n * Defaults to `community`, which every corpus has because the layout pass writes it.\n */\n fill?: string;\n /**\n * Which column the size ramp is spent on — Plot's `r`.\n *\n * Omitted, every point is drawn at one radius, which is a legitimate picture: without a ramp a\n * look's `form.size` range has only one end.\n */\n r?: string;\n /**\n * Vertices that must come back whatever the query says.\n *\n * The set a reader has taken hold of — dragged, pinned, selected, focused. Their drawn positions\n * are a view-local overlay on coordinates that never move, so the index cannot find them where\n * they now appear. Carrying them explicitly is cheaper and more honest than making the index\n * mutable: it is a handful of identities, and the alternative is a spatial structure that has to be\n * rewritten every time somebody drags something.\n */\n pinned?: VertexId[];\n /**\n * The most points the source may return.\n *\n * **Above it a source samples the window; it does not take the front of it.** Which is the second\n * half of `n`'s honesty: `n` says how many matched, and this says the answer is a *sample* of\n * those rather than whichever ones an `ORDER BY` happened to put first.\n *\n * Required, and always filled: a host's `limit` is optional all the way down to `useQueryLoop`,\n * which resolves it before the question leaves. A source never has to know what this package\n * would have chosen.\n */\n limit: number;\n /**\n * The shortest edge worth a row, **in screen pixels** — multiply by `perPixel` for a length in the\n * graph's own space.\n *\n * **What it buys is on `perPixel` below**: two of every three edges a five-million-node window\n * draws are under one pixel long, and discarding everything under three sends a third of the rows\n * for an identical picture.\n *\n * Required for the reason `limit` is, and it is the field this whole shape exists for. It used to\n * live on an exported `BOUNDED_DEFAULTS` that a source read to find out what it was being asked —\n * so a request arrived incomplete and every source finished it in its own words. There were two,\n * one of them in another repository. Now the loop resolves it and a source reads it here.\n *\n * Meaningless without `perPixel`, and a source with no resolution discards nothing: a threshold in\n * pixels with no pixels is not a threshold.\n */\n minLinkPixels: number;\n /**\n * How much of the graph's own space one screen pixel covers — the resolution the answer is going\n * to be looked at.\n *\n * **What it buys: an edge shorter than three pixels is not sent.** Two of every three edges a\n * five-million-node window draws are under one pixel long — they are a dot on top of their own\n * endpoints, which the point layer has already drawn. Discarding everything under 3 px sends\n * 27.5–35.3% of the rows and leaves 99.9–100% of the inked pixels identical\n * (`.planning/FAR-VIEW-AND-EDGES.md`, measured 2026-08-17).\n *\n * **A row discard, not a fade.** cosmos.gl's `linkVisibilityDistanceRange` already dims a short\n * link, and dimming happens after the row has been joined, returned, uploaded and rasterised. This\n * is the same picture without the work.\n *\n * The rectangle alone cannot say it: the same rectangle over a 400-pixel canvas and a 4,000-pixel\n * one are different questions, and only the caller knows which. Omitted — a source is asked for\n * everything, or by something with no canvas — nothing is discarded, because a threshold in pixels\n * with no pixels is not a threshold.\n */\n perPixel?: number;\n /**\n * Stop: the caller does not want this answer any more.\n *\n * **A cancelled question rejects with `signal.reason`, and that is the whole contract.** The\n * camera moves faster than a database answers, so a source that can only hold one question at a\n * time is routinely asked a second before the first has landed. The first caller is still holding\n * a promise; dropping it leaves that caller's `finally` unrun, which in `useQueryLoop` reads as a\n * query permanently in flight for the rest of the session. So the stale promise is **settled**,\n * and this says with what.\n *\n * `AbortController.abort()` puts a `DOMException` named `AbortError` in `reason`, and `throw\n * signal.reason` is the whole implementation. A source that cancels for its own reasons — one\n * standing question re-aimed by a newer caller, a connection let go — throws an `AbortError` it\n * builds itself, which is what `abortError()` below is for.\n *\n * **This replaced a sentinel of ours, and the argument for the sentinel was real.** `SUPERSEDED`\n * was an exported `Symbol` with `isSuperseded` beside it, on the reasoning that *you moved on* and\n * *the database said no* are the two things a query loop must tell apart, and a string comparison\n * against a thrown value goes stale with nothing failing. All of that is true and none of it is an\n * argument for a *private* sentinel: `AbortError` is the name the platform already gives that\n * distinction, `fetch` rejects with it, and every third-party async primitive a source is written\n * over — `fetch`, `AbortSignal.timeout`, a WHATWG stream — produces one without being told to.\n * Ours meant a source had to import a symbol from us to be cancellable at all, and a source that\n * simply passed the signal to `fetch` did the standard thing and was reported to the host as a\n * failure. The comparison is against `error.name`, which is the platform's contract rather than a\n * message.\n */\n signal?: AbortSignal;\n}\n\n/**\n * Anything that can answer \"what is in this rectangle, at this zoom, in at most this many marks\".\n *\n * One method, one question. A source that also wants search, aggregation or paths is describing a\n * query surface rather than a render path, and there is already one of those — fossil's verbs. The\n * line: this answers *what should I draw*, and nothing about *what does it mean*.\n */\nexport interface BoundedSource {\n slice(request: SliceRequest): Promise<Slice>;\n /**\n * How many vertices there are in total, if the source knows cheaply.\n *\n * The reason this exists is the one real cost of bounding: **a graph that fits pays for nothing.**\n * Unbounded loads once and then pans on the GPU for free; bounded issues a query per camera move.\n * At 200,000 nodes that trade is overwhelmingly worth it — 1,017 ms of first paint against roughly\n * 130 ms — but at 2,000 it is pure loss, tens of milliseconds of latency per pan buying a ceiling\n * nobody was near.\n *\n * So a consumer asks first. Under the limit, take one slice covering everything and never ask\n * again: same code path, and panning is free exactly when it can be.\n */\n total?(): Promise<number>;\n /**\n * The rectangle the corpus occupies, when the source can say cheaply.\n *\n * **Framing the opening view is not the host's job, and treating it as one is measured.** The\n * archive occupies `x ∈ [1843, 2253]` of a 4,096-wide space — 1% of its area — and a camera that\n * opens on the space rather than on the data draws 1,543 points into a tenth of the viewport, at\n * two to nine pixels each, under a fog of links. Every point is uploaded and none is legible, which\n * a reader reports as *the nodes are not rendering*.\n *\n * It matters more for a bounded source than for a whole one: the first question a sliced graph\n * asks is *what is the camera over*, so a camera pointing at empty space is a first paint of\n * nothing. Framing before asking is the difference between one query and none.\n *\n * Optional, because a source over an unlaid-out relation has no answer — and cheap where it\n * exists: a corpus reads it off tile footers it was going to read anyway, and a relation with\n * `x`/`y` gets it from four aggregates.\n */\n extent?(): Promise<Viewport>;\n /**\n * Say when the answer changes for a reason the camera cannot see, and hold the source's resources\n * for as long as anybody is listening.\n *\n * **A whole slice rather than a nudge to ask again, and that is what makes it worth having.** The\n * reason a bounded answer changes on its own is that the page filtered something — somebody\n * brushed a histogram, a panel picked a value — and a source that lives inside a crossfilter is\n * *told* by the coordinator, which has already re-run the reads with the new predicate by the time\n * this fires. Asking again would issue the same two queries a second time to learn what is in hand.\n *\n * The returned function is also the release: it is where a source lets go of whatever it holds —\n * a client registration, a connection, a cache — so a loop that calls this is a loop that cannot\n * leak one. Optional, because a source over arrays holds nothing and changes for nothing.\n */\n watch?(answered: (slice: Slice) => void): () => void;\n}\n\n/**\n * A cancellation this source is raising itself, in the platform's own shape.\n *\n * For the case a `signal` cannot express: a source holding **one** standing question, re-aimed by a\n * newer caller. There is no signal for the caller that lost — its request was never aborted, it was\n * simply overtaken — so the source builds the rejection, and it builds the same kind the platform\n * would have. `message` says what overtook it; `name` is what anybody tests.\n *\n * Not on the barrel. A source outside this package cancels by passing the `signal` it was handed to\n * whatever it is waiting on, or by `throw signal.reason` — both of which produce an `AbortError`\n * with nothing imported from us. This exists for the one case that has no signal to reach for, and\n * `new DOMException(message, \"AbortError\")` is the whole of it if a third source ever needs it too.\n */\nexport function abortError(message: string): DOMException {\n return new DOMException(message, \"AbortError\");\n}\n\n/**\n * Whether a rejection means *you moved on* rather than *the answer failed*.\n *\n * `error.name` and not `instanceof`: the same `AbortError` reaches here as a `DOMException` from\n * `AbortController`, as one of ours from `abortError`, and — for a source written over `fetch` in\n * another realm, an iframe or a worker — as an object no `instanceof` in this realm matches. The\n * name is what WHATWG specifies and what every producer agrees on.\n *\n * Not on the barrel either, for the same reason as `abortError`: a caller of this package awaits\n * `slice()` behind an `AbortController` it owns, so `controller.signal.aborted` already answers the\n * question for it. This is the branch `useQueryLoop` needs for the *other* half — a source that\n * cancelled a question the loop had not aborted.\n */\nexport function isAbort(error: unknown): boolean {\n return typeof error === \"object\" && error !== null && (error as { name?: unknown }).name === \"AbortError\";\n}\n\n/**\n * Whether this graph should be sliced at all.\n *\n * `undefined` when the source cannot say cheaply — in which case slice, because an unknown corpus is\n * more likely to be the large kind than not.\n */\nexport function shouldSlice(total: number | undefined, limit: number): boolean {\n return total === undefined || total > limit;\n}\n\n/**\n * What a question means when the host says nothing — **resolved before a source ever sees it.**\n *\n * These were `BOUNDED_DEFAULTS`, an exported table, and a source read it to find out what it was\n * being asked. That is the defect, stated plainly: a request that arrives unresolved is an\n * *incomplete* request, and every source outside this package had to know a constant of ours to\n * finish it. Two of them did — `duck-source.ts` here, and fossil's tile reader — and each finished\n * it in its own words, which is two chances to disagree about one number.\n *\n * `useQueryLoop` fills both in on every call now, so `limit` and `minLinkPixels` are **required**\n * fields of `SliceRequest`: a source reads `request.limit` and is done. The numbers stay module\n * scoped and off the barrel, because nobody outside needs to look them up any more.\n *\n * The camera→rectangle conversion that used to live beside them is gone for the sibling reason:\n * cosmos.gl owns the screen↔space transform and answers it through `screenToSpacePosition`, so\n * deriving the rectangle from the camera and the space size was a second implementation of the\n * renderer's own maths, free to drift from it. `useQueryLoop` asks the renderer instead.\n */\n\n/**\n * Twenty thousand marks.\n *\n * Above about 50,000 points a live layout stops being comfortable and the edge layer is already\n * fog well before that, so a limit far below the renderer's ceiling is not a compromise — it is\n * the legibility ceiling, which arrives first and is the one a reader actually meets.\n */\nexport const DEFAULT_LIMIT = 20_000;\n\n/**\n * The shortest edge worth a row — three screen pixels.\n *\n * Measured over the drawn edges of five windows per corpus, 2026-08-17: the median edge is 1.60 px\n * at 200k, 1.47 at a million and **0.52 at five million**, where 64.6% are under one pixel. An edge\n * that short is a dot on top of two dots the point layer has already drawn. Discarding under 3 px\n * sends 27.5–35.3% of the rows and leaves 99.9–100% of the inked pixels identical — the working is\n * in `.planning/FAR-VIEW-AND-EDGES.md`.\n *\n * Three rather than two because both were measured against the same five windows of\n * `docs/public/bench/1000000`: 2 px sends 5.6–25.8% more rows, mean 19%, for the same 0.1% of\n * image.\n *\n * **It is on `SliceRequest` now, and the argument against that has been overtaken.** It used to say:\n * one call site, and a knob with one call site is a knob nobody has an opinion about. There were\n * two call sites by then and one of them was in another repository, both reading the constant off\n * the barrel to reconstruct the same product. Whether it is a *knob* is still open — no caller\n * overrides it — but it is a **fact about the question**, and a question carries its own facts.\n */\nexport const DEFAULT_MIN_LINK_PIXELS = 3;\n"],"names":["abortError","message","isAbort","error","shouldSlice","total","limit","DEFAULT_LIMIT","DEFAULT_MIN_LINK_PIXELS"],"mappings":"AAkVO,SAASA,EAAWC,GAA+B;AACxD,SAAO,IAAI,aAAaA,GAAS,YAAY;AAC/C;AAeO,SAASC,EAAQC,GAAyB;AAC/C,SAAO,OAAOA,KAAU,YAAYA,MAAU,QAASA,EAA6B,SAAS;AAC/F;AAQO,SAASC,EAAYC,GAA2BC,GAAwB;AAC7E,SAAOD,MAAU,UAAaA,IAAQC;AACxC;AA4BO,MAAMC,IAAgB,KAqBhBC,IAA0B;"}
@@ -1,18 +1,20 @@
1
+ import { OpenOptions } from '@fossil-lang/corpus';
1
2
  import { Coordinator, Selection } from '@kanzo-tech/mosaic';
2
3
  import { BoundedSource } from './bounded';
3
4
  import { VertexId } from './resident';
4
5
  /**
5
- * A `BoundedSource` over two ordinary relations in DuckDB.
6
+ * The source: a corpus fossil wrote, read through DuckDB.
6
7
  *
7
- * On a subpath because Mosaic is an optional peer and this is the half that needs it: a host drawing
8
- * arrays it already holds takes `memorySource` and pays for no database. Splitting them is what lets
9
- * that promise be true rather than merely stated.
8
+ * On a subpath because Mosaic and fossil's reader are optional peers and this is the half that needs
9
+ * them. The root barrel ships no source at all, which is the whole of what that split now means: a
10
+ * host that installs neither gets the rendering surface and draws nothing.
10
11
  *
11
- * **Neutral about storage, and that is the point.** The neutrality let bounded be measured against
12
- * unbounded before anything committed to a layout on disk — and it is the reason this file survived
13
- * a decision on the other side of the seam. The verb it was written to sit beside never landed:
14
- * `viewport` was dropped and GraphAr with it, because the camera is addressed rather than queried.
15
- * What replaces it is a tile fetched by a computed URL, which is another source.
12
+ * **There used to be two here and the second was neutral about storage.** `duckBoundedSource` took
13
+ * two ordinary relations and the column names that made sense of them, and that neutrality earned
14
+ * its keep once — it let bounded be measured against unbounded before anything committed to a layout
15
+ * on disk. What it cost afterwards is the reason it is gone: a corpus already declares those names
16
+ * in its manifest, so the second source was this side writing down what the other side owns, and the
17
+ * two answered the same question in two dialects of the same SQL.
16
18
  *
17
19
  * **Every query in this file goes through a `SliceRead`, and there is no other path.** `onceQuery`
18
20
  * was the other one — a throwaway client per query, on this subpath, re-exported for the two
@@ -39,62 +41,8 @@ export interface DuckSource extends BoundedSource {
39
41
  */
40
42
  publish(vertices: readonly VertexId[] | null): void;
41
43
  }
42
- export interface DuckSourceOptions {
43
- coordinator: Coordinator;
44
- /**
45
- * The crossfilter this graph draws inside.
46
- *
47
- * Given, the page's predicate rides in the slice query and the canvas draws **what survives**.
48
- * Omitted, the source is a reader of a relation and nothing else — which is what a graph with no
49
- * charts beside it is.
50
- */
51
- filterBy?: Selection;
52
- /** The node relation. */
53
- nodes: string;
54
- /** The edge relation, as pairs of node ids. */
55
- edges: string;
56
- /**
57
- * Which vertex type this relation is.
58
- *
59
- * Required, and with no default, because the source is the only thing that knows: a `dense_id`
60
- * numbers within one type, so the identity a slice carries is only completed here. A corpus of one
61
- * type is type `0` and has to say so — a defaulted `0` would let a second relation ship the same
62
- * identities as the first with nothing raised.
63
- */
64
- typeIndex: number;
65
- /**
66
- * A **dense** integer id — `0..n-1`, no gaps.
67
- *
68
- * Dense because `links` refers to positions rather than to ids, so a consumer never pays for an
69
- * id→index map. GraphAr's `dense_id` is this column by another name.
70
- */
71
- idField?: string;
72
- /**
73
- * The identity column — the subject IRI. **Omitted, a slice carries addresses only.**
74
- *
75
- * A `dense_id` says where a vertex is; the IRI says which vertex it is, and only the second
76
- * survives the layout being redone. The corpus writes it as `subject`, non-null and unique within
77
- * a type, which is why that is the name here — but it stays opt-in rather than defaulted, because
78
- * reading it costs 1.87× the drawing tile and most points are painted rather than named.
79
- *
80
- * Set it when something outlives a session: a bookmark, a link out, a selection that has to mean
81
- * the same thing after the next `fossil run`.
82
- */
83
- subjectField?: string;
84
- xField?: string;
85
- yField?: string;
86
- /**
87
- * **What colours and what sizes are not here**, and their absence is the shape rather than an
88
- * omission: they are `fill` and `r` on the request, because a channel is what the caller wants
89
- * drawn now and this object is where the bytes are. Given here too, recolouring meant building a
90
- * second source — and two places deciding one colour is the state that move ended.
91
- */
92
- sourceField?: string;
93
- targetField?: string;
94
- }
95
- export declare function duckBoundedSource(options: DuckSourceOptions): DuckSource;
96
44
  /**
97
- * A corpus that fossil wrote, read by address.
45
+ * A corpus that fossil wrote, read by address — **and the addressing is fossil's.**
98
46
  *
99
47
  * **The five things a call site used to know, and now does not.** Drawing a corpus meant deriving
100
48
  * the chunk URLs from a `chunk_size` copied by hand, knowing how a tile is named, knowing what the
@@ -107,18 +55,28 @@ export declare function duckBoundedSource(options: DuckSourceOptions): DuckSourc
107
55
  * The consumer knows one thing: **where the corpus is.**
108
56
  *
109
57
  * ```ts
110
- * const { source } = await openCorpus({ coordinator, dest: "/bench/1000000" });
58
+ * const { source } = await openCorpus({ coordinator, dest: "/bench/1000000", wasmUrl });
111
59
  * ```
112
60
  *
113
- * **Addressed, not queried.** The manifest and the per-tile boxes are read once and kept; after that
114
- * a camera move is arithmetic over boxes and a list of URLs. There is deliberately no request on the
115
- * path between the camera moving and a URL being computable — the moment there is one, this has
116
- * become the `viewport` verb fossil deleted.
61
+ * **And this side no longer knows the conventions either, which is the change.** It used to remove
62
+ * that defect for its callers by committing it one level down — a YAML line-scanner, a
63
+ * `chunk{k}.parquet` spelling, a `by_source/tile{k}.parquet` spelling and a `HEAD`-probing search
64
+ * for a tile count — and by the time those were deleted all four were **wrong**: fossil writes
65
+ * `container: rowgroups`, one `tiles.parquet` whose row groups are the tiles, and `vertex_count`
66
+ * had been in the manifest the whole time the search was probing for it. Fossil's `open` is
67
+ * `fossil_graph::plan` compiled to wasm32: the arithmetic the native reader runs, not a second
68
+ * implementation of it that agrees until it does not.
69
+ *
70
+ * **Addressed, not queried.** The manifests and the per-tile boxes are read once and kept; after
71
+ * that a camera move is arithmetic over boxes and a list of URLs. There is deliberately no request
72
+ * on the path between the camera moving and a URL being computable — the moment there is one, this
73
+ * has become the `viewport` verb fossil deleted.
117
74
  *
118
75
  * **What it is not.** It takes no column names and no type index. Those come from the manifest or
119
- * they do not come: a corpus reader that also accepts `idField` is `duckBoundedSource` with extra
120
- * steps, and there is already one of those for the case this is not — an arbitrary relation with
121
- * `x`/`y` that nobody wrote as a corpus.
76
+ * they do not come. There used to be a second source here for the case this is not — an arbitrary
77
+ * relation with `x`/`y` that nobody wrote as a corpus — and taking `idField` here would have been
78
+ * that one with extra steps. It is gone, so the rule is simpler than the guard against it: a column
79
+ * name reaching this door is a name the manifest should have carried.
122
80
  */
123
81
  export interface OpenCorpusOptions {
124
82
  coordinator: Coordinator;
@@ -130,6 +88,17 @@ export interface OpenCorpusOptions {
130
88
  filterBy?: Selection;
131
89
  /** Where the corpus lives, without a trailing slash — the directory holding `graph.graph.yml`. */
132
90
  dest: string;
91
+ /**
92
+ * Where `fossil_graph_wasm_bg.wasm` is.
93
+ *
94
+ * **The one thing about fossil's reader a caller still has to say, and not ours to default.** The
95
+ * addressing runs in WASM, so the module has to be up before a URL can be composed, and only the
96
+ * caller knows how its bundler resolves an asset — `?url` under Vite, an asset import under Next,
97
+ * a `Response` over the bytes in Node. Passed straight through, spelled as fossil spells it.
98
+ * Omitted, the boot is left to whoever already did it: it is memoised for the session, so a host
99
+ * on its second corpus need not say it again.
100
+ */
101
+ wasmUrl?: OpenOptions["wasmUrl"];
133
102
  /**
134
103
  * Which vertex type to draw, when a corpus carries more than one.
135
104
  *
@@ -143,6 +112,25 @@ export interface OpenCorpusOptions {
143
112
  * asks for names when something has to be *named* rather than painted.
144
113
  */
145
114
  subjects?: boolean;
115
+ /**
116
+ * How a manifest is read, when a plain `fetch` of its URL is not how this host reads one.
117
+ *
118
+ * **The default is `manifest` below, and it is the whole of what this file knows about reading a
119
+ * corpus** — right for a corpus served off an origin the page can already read, and wrong for a
120
+ * host whose blobs sit behind a signature. There the URL fossil composes is correct and
121
+ * unreadable, and nothing else on these options carries a credential.
122
+ *
123
+ * Passed straight through to fossil's `open`, which is where the capability belongs: `open`
124
+ * composes every address and lends the reader to each one, so a host that signs a URL signs the
125
+ * index and the per-type manifests the index names **without knowing which files those are**.
126
+ * That is the point of lending a reader rather than handing over bytes — and it is what keeps a
127
+ * signing host on this door, because the alternative it otherwise reaches for is composing the
128
+ * addresses itself, which is the convention-copying `openCorpus` exists to end.
129
+ *
130
+ * It reads manifests and nothing else. The payload is read by the coordinator's own connector,
131
+ * which is the host's already.
132
+ */
133
+ readText?: OpenOptions["readText"];
146
134
  }
147
135
  /**
148
136
  * An opened corpus: the half that **draws** and the half that **answers**.
@@ -175,8 +163,51 @@ export interface OpenedCorpus {
175
163
  * crossfilter serve the canvas and the charts.
176
164
  */
177
165
  nodes: string;
178
- /** The source-ordered edge relation, or `undefined` when the corpus declares no edge for this type. */
179
- edges: string | undefined;
166
+ /**
167
+ * The source-ordered edge relations this canvas draws, registered one view per relation.
168
+ *
169
+ * **A list, because a corpus declares a list.** This was one name, picked with `.find` over the
170
+ * relations whose source is this type — so a corpus declaring two edge labels registered the
171
+ * first and dropped the second with nothing raised, and a chart over `corpus_Person_edges` was a
172
+ * chart over half the graph. `frame` on the other side takes every one of them
173
+ * (`packages/corpus/src/corpus.ts`, `edges.filter((e) => e.srcType === address.type)`), and
174
+ * fossil's own `verbs()` registers a view per relation under `{src}_{edge}_{dst}` rather than
175
+ * unioning them — which is also what keeps two relations' differing property columns from having
176
+ * to agree on one schema.
177
+ *
178
+ * Empty when the corpus declares no relation this source can draw; {@link OpenedCorpus.undrawn}
179
+ * is then where the ones it declared went.
180
+ */
181
+ edges: readonly EdgeRelation[];
182
+ /**
183
+ * The relations incident to this type that the canvas does **not** read, and why.
184
+ *
185
+ * A single-type canvas can draw a relation only where *both* endpoints are numbered in its own
186
+ * `dense_id` space. The rest are dropped from the read rather than mixed into it, and reported
187
+ * here rather than dropped in silence.
188
+ */
189
+ undrawn: readonly UndrawnRelation[];
190
+ }
191
+ /** One edge relation of the drawn type, and the view its source-ordered adjacency is registered under. */
192
+ export interface EdgeRelation {
193
+ readonly edgeType: string;
194
+ readonly srcType: string;
195
+ readonly dstType: string;
196
+ /** The registered view name — `corpus_{src}_{edge}_{dst}`. */
197
+ readonly view: string;
198
+ }
199
+ /** One relation incident to the drawn type that this source cannot read, and why. */
200
+ export interface UndrawnRelation {
201
+ readonly edgeType: string;
202
+ readonly srcType: string;
203
+ readonly dstType: string;
204
+ /**
205
+ * `other-space`: an endpoint is another vertex type, so the `dense_id` on that side numbers a
206
+ * different set of vertices and this source holds no coordinates for any of them.
207
+ * `not-declared`: both endpoints are this type and the corpus publishes no source-ordered
208
+ * adjacency to read.
209
+ */
210
+ readonly reason: "other-space" | "not-declared";
180
211
  }
181
212
  export declare function openCorpus(options: OpenCorpusOptions): Promise<OpenedCorpus>;
182
213
  //# sourceMappingURL=duck-source.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"duck-source.d.ts","sourceRoot":"","sources":["../src/duck-source.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAc,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC7E,OAAO,KAAK,EAAE,aAAa,EAAiC,MAAM,WAAW,CAAC;AAE9E,OAAO,EAA6B,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtE;;;;;;;;;;;;;;;;;GAiBG;AAEH;;;;;;;GAOG;AACH,MAAM,WAAW,UAAW,SAAQ,aAAa;IAC/C;;;;;;;;OAQG;IACH,OAAO,CAAC,QAAQ,EAAE,SAAS,QAAQ,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;CACrD;AAED,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,WAAW,CAAC;IACzB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,yBAAyB;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;OAOG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;;;;;;OAUG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAuFD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,iBAAiB,GAAG,UAAU,CAmExE;AA4dD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,WAAW,CAAC;IACzB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,kGAAkG;IAClG,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAyCD;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,YAAY;IAC3B;;;;;;;OAOG;IACH,MAAM,EAAE,UAAU,CAAC;IACnB;;;;;;OAMG;IACH,KAAK,EAAE,MAAM,CAAC;IACd,uGAAuG;IACvG,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3B;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,YAAY,CAAC,CAmXlF"}
1
+ {"version":3,"file":"duck-source.d.ts","sourceRoot":"","sources":["../src/duck-source.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAe,WAAW,EAAqB,MAAM,qBAAqB,CAAC;AAEvF,OAAO,KAAK,EAAE,WAAW,EAAc,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC7E,OAAO,KAAK,EAAE,aAAa,EAAiC,MAAM,WAAW,CAAC;AAE9E,OAAO,EAA6B,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtE;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;;;;;GAOG;AACH,MAAM,WAAW,UAAW,SAAQ,aAAa;IAC/C;;;;;;;;OAQG;IACH,OAAO,CAAC,QAAQ,EAAE,SAAS,QAAQ,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;CACrD;AAojBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,WAAW,CAAC;IACzB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,kGAAkG;IAClG,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IACjC;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,CAAC,EAAE,WAAW,CAAC,UAAU,CAAC,CAAC;CACpC;AA8FD;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,YAAY;IAC3B;;;;;;;OAOG;IACH,MAAM,EAAE,UAAU,CAAC;IACnB;;;;;;OAMG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;;;;;;;;OAcG;IACH,KAAK,EAAE,SAAS,YAAY,EAAE,CAAC;IAC/B;;;;;;OAMG;IACH,OAAO,EAAE,SAAS,eAAe,EAAE,CAAC;CACrC;AAED,0GAA0G;AAC1G,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,qFAAqF;AACrF,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,EAAE,aAAa,GAAG,cAAc,CAAC;CACjD;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,YAAY,CAAC,CA4ZlF"}