pond-ts 0.48.0 → 0.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -8,7 +8,9 @@ The `@pond-ts` packages — `pond-ts`, `@pond-ts/react`, `@pond-ts/charts`,
8
8
  tag, so this file covers them all. Pre-1.0: minor bumps may include new features
9
9
  and type-level changes; patch bumps are strictly additive.
10
10
 
11
- [Unreleased]: https://github.com/pond-ts/pond/compare/v0.48.0...HEAD
11
+ [Unreleased]: https://github.com/pond-ts/pond/compare/v0.49.0...HEAD
12
+ [0.49.0]: https://github.com/pond-ts/pond/compare/v0.48.1...v0.49.0
13
+ [0.48.1]: https://github.com/pond-ts/pond/compare/v0.48.0...v0.48.1
12
14
  [0.48.0]: https://github.com/pond-ts/pond/compare/v0.47.0...v0.48.0
13
15
  [0.47.0]: https://github.com/pond-ts/pond/compare/v0.46.0...v0.47.0
14
16
  [0.46.0]: https://github.com/pjm17971/pond-ts/compare/v0.45.0...v0.46.0
@@ -47,6 +49,218 @@ and type-level changes; patch bumps are strictly additive.
47
49
 
48
50
  ## [Unreleased]
49
51
 
52
+ ## [0.49.0] — 2026-07-21
53
+
54
+ ### Added
55
+
56
+ - **charts:** **Annotation theme roles** (#508 item 3, Tidal vol-surface
57
+ friction). `theme.annotation` gains an optional **`roles`** map (role name →
58
+ `{ color, fillOpacity? }`), and `<Baseline>` / `<Marker>` / `<Region>` gain a
59
+ **`role`** prop that recolours that mark from `roles[role]` while keeping the
60
+ shared depth ramp — so a smile can place a green ATM baseline, a distinct
61
+ reference marker, and an amber zone at once without the whole register
62
+ shifting together. Resolves `roles[role] ?? annotation` (an unknown/unset
63
+ role is the base register). **Colour stays a theme concern** — there is no
64
+ per-mark colour prop (the one-styling-channel discipline; consistent with the
65
+ per-box red/green reject). `cssVarTheme` carries the `roles` map through
66
+ unchanged (deep-merged). `theme.annotation.roles` is optional, so existing
67
+ themes are unaffected.
68
+ - **charts:** **`<BoxPlot id>` — box selection** (#508 item 5, Tidal
69
+ vol-surface friction). A `BoxPlot` with an `id` is now clickable on the same
70
+ id-gated contract `<BarChart>` / `<ScatterChart>` carry
71
+ (`selected`/`onSelect`, `hovered`/`onHover`): a click anywhere on a box —
72
+ body or whisker, a range-only bid→ask segment included — selects it via
73
+ rect-containment (`boxAt`, the interval-mark analog of `barAt`, **not** the
74
+ continuous nearest-point threshold), and the selected box outlines (hovered
75
+ fainter, reusing `theme.box.stroke`, no new token). `key` is the box's `x`
76
+ (its span begin). Without an `id` the box stays display-only. Independent of
77
+ the box's `cursorFlag` cursor opt-out — selection rides the separate
78
+ `hitTest` path.
79
+ - **charts:** **`<YAxis tickCount>` + height-derived tick density** (#508
80
+ item 4, Tidal vol-surface friction). The y axis's auto-tick count now
81
+ follows the **row height** by default — a short strip (e.g. a 72px
82
+ histogram lane) gets fewer ticks than a tall row instead of crushing the
83
+ same ~5 labels into the space (mirrors the width-derived trading-time x
84
+ axis). A new **`tickCount`** prop pins an explicit target; explicit
85
+ `ticks` still overrides both. The count is resolved once per axis on the
86
+ row (`resolveYTickCount`) and shared by the axis labels, the cursor-readout
87
+ formatter, and the row gridlines, so a label / its gridline / its readout
88
+ stay on the same `ticks(count)` (replaces three hardcoded `5`s that agreed
89
+ by convention).
90
+
91
+ - **charts:** **`<Legend>` — the series key** (#508 item 2, Tidal vol-surface
92
+ friction; design per the sender's sketch on the issue). Every draw layer now
93
+ registers its readout identity (`as ?? column`) plus its **resolved** style
94
+ as a `SwatchSpec` (line stroke+dash, area fill+line, band envelope, scatter
95
+ dot, box whisker, bar fill, candle up/down pair), and `<Legend />` renders
96
+ the registry as a small card anchored to a corner of the rows block
97
+ (`placement`, default `top-right`) — so the key can never drift from the
98
+ plot. Rows follow chart-row → declaration order; identities collapse
99
+ (`id ?? label`, as the tracker readout merges keys); a **stacked bar
100
+ registers one row per group** with its resolved fill. Per-layer control via
101
+ a new `legend` prop on all seven marks: `false` opts out, a string renames.
102
+ Interactions are **id-gated** (the shipped selection contract): rows whose
103
+ layer has an `id` echo hover into the container and toggle selection on
104
+ click; `onRowHover` / `onRowClick` take over when provided; series
105
+ show/hide deliberately stays consumer-side. **Scope follows placement:** at
106
+ the container level the card lists every row; placed inside a `<Layers>` it
107
+ scopes to that `<ChartRow>` and anchors to that row's plot (a per-row legend
108
+ needs no prop). `<Legend items={…}>` renders explicit rows — inside a
109
+ container or standalone (a dashboard-side key).
110
+ New optional `theme.legend` slot (background/border/text; derives from
111
+ `chip`/`axis` tokens when absent, so hand-built themes keep compiling).
112
+ **Headless variant:** `useChartLegend()` serves the entries as data —
113
+ `rows` (**items grouped by chart row**; each item is
114
+ `label` + resolved swatch + `id` + live `selected`/`hovered`; a flat list
115
+ is `rows.flatMap((r) => r.items)`) plus chart-synced `hover`/`select`
116
+ verbs, the container's axis `gutters` (for aligning a custom layout to the
117
+ plot), and **`cursorTime`** (the cursor's axis instant, `null` when idle) —
118
+ the seams for a legend that is a design of its own: horizontal chip rows,
119
+ ticker-compare with the secondary dimmed, and **current-or-cursor values**
120
+ per item (`series.nearest(cursorTime)`, else the latest sample; item
121
+ labels also match the tracker's sample labels, so an `onTrackerChanged`
122
+ merge is a label-keyed join); `<Legend>` itself renders through the same
123
+ core, so the two can't disagree. **Scope follows placement** for both the
124
+ card and the hook: inside a `<Layers>` they scope to that `<ChartRow>` and
125
+ the card anchors to that row's plot; container-level stays all-rows. Card
126
+ polish per the first design pass: plot-area-inset placement, selection
127
+ reads by contrast (the selected item bold, others dulled — the
128
+ ticker-compare treatment), a dashed line's swatch hand-renders as a
129
+ canonical three-dash glyph, and a bar's swatch is a centred rounded square.
130
+ New exports: `Legend`, `LegendProps`, `LegendPlacement`, `SwatchSpec`,
131
+ `LegendItemInput`, `useChartLegend`, `ChartLegend`, `LegendRow`,
132
+ `LegendItem`.
133
+ - **charts:** **`cursorFormat` reaches value axes** (#508 item 1, Tidal
134
+ vol-surface friction) — the container's readout channel now applies on a
135
+ value x axis exactly as on a time axis: a **string** is a d3 _number_
136
+ specifier there, a **function** receives
137
+ `(value, { grain: undefined, defaultText })` (no time grain to hand over;
138
+ `defaultText` is the axis-default text). The readout also gains its
139
+ documented precedence on the axis strip on **every** axis kind:
140
+ `cursorFormat → axis format → container`, so an explicit `<XAxis format>`
141
+ keeps ticks terse while `cursorFormat` makes the cursor pill / marker
142
+ indicators / annotation auto-labels precise (`+2.0σ` ticks, `+1.83σ` pill).
143
+ A `transform`ed axis is exempt (its pill speaks the derived unit); a
144
+ category axis reads names and ignores `cursorFormat`. Type change:
145
+ `CursorFormat`'s function form widens `ctx.grain` from `TimeGrain` to
146
+ `TimeGrain | undefined`; the frame gains an optional `formatReadout`
147
+ channel (see Fixed below for the `formatTime` clarification).
148
+ - **charts:** **M4 line decimation** (charts decimator wave, Phase 3) — the
149
+ `<LineChart>` now draws from the per-device-pixel-column min/max/first/last
150
+ (pond's `binBy(…, 'minMaxFirstLast')`) once the visible data exceeds ~2 samples
151
+ per pixel, so a **fully-visible** dense series (which viewport culling can't
152
+ help) draws from O(plot width) points instead of every sample: a 1M-point line
153
+ drops from ~34 ms/frame to ~3.4 ms, under the 60 fps budget. It is **visually
154
+ lossless** (an e2e pixel-diff bounds the decimated-vs-full difference to a thin
155
+ sub-pixel AA seam) and **preserves single-sample anomalies** (the min/max
156
+ channel keeps the spike M4 is chosen over LTTB to keep). Bucket edges are the
157
+ scale's pixel range inverted back to key space, so each bucket is one pixel
158
+ column on **any** scale, including a non-affine `TradingTimeScale`. Auto-on with
159
+ an opt-out / tuning prop
160
+ `decimate={false | { threshold }}` (new `DecimateOption` export). **All gap
161
+ modes** decimate: a §2.2 **gap-edge union** folds each ≥1-pixel interior gap's
162
+ boundaries into the bucket edges, so the gap reduces to its own empty (`NaN`)
163
+ bucket with exact pre/post-gap values — `'empty'` breaks precisely, `'none'`
164
+ bridges, and the `dashed`/`step`/`fade` connectors still draw across the gap.
165
+ **`sessionBreaks` decimate too** — the same union folds each trading-axis
166
+ session-break instant into the bucket edges so no bucket merges two sessions'
167
+ extremes across the discontinuity, and `sessionRuns` then splits the decimated
168
+ series into clean per-session subpaths (a dense intraday line on a trading-time
169
+ axis decimates _and_ breaks at each session open). Still gated off only a
170
+ non-linear `curve` (documented backlog). Interaction reads the source series
171
+ (§2.3), so readouts/selection are unaffected.
172
+ - **charts:** M4 decimation extended to **`<AreaChart>` and `<BandChart>`** (same
173
+ auto-on `decimate={false | { threshold }}` prop). An **area** reuses the line M4
174
+ on its outline (gap-edge union included) with the fill following under the
175
+ full-series gradient — so a dense filled area shrinks its fill + outline work to
176
+ O(plot width). A **band** decimates to the per-pixel-column **min(`lower`) /
177
+ max(`upper`)** — the widest envelope the samples span, so it can never invert
178
+ (§2.5) and covers the same pixels; the win is the canvas fill (≈W vertices vs
179
+ every sample). Both gated off a non-linear `curve`; interaction unaffected.
180
+ - **charts:** **Viewport culling** (charts decimator wave, Phase 2). Line, area,
181
+ and band layers now clip to the **visible slice** of their key column — plus
182
+ one entry/exit point each side so the segment crossing each plot edge still
183
+ draws — before any path work, so a pan/zoom repaint costs O(visible) instead
184
+ of O(N). On a large series zoomed in, the per-frame `drawLine` path-generation
185
+ cost drops from ~29 ms at 1M points (over the 16.67 ms/60 fps budget — the
186
+ cause of the #256 pan-FPS collapse) to ~0.06 ms; a fully-visible series is
187
+ unchanged (culling no-ops, keeping that draw byte-identical — the baseline the
188
+ Phase 3 M4 decimator addresses). Interaction is unaffected: `sampleAt` /
189
+ `hitTest` read the full source series, so a hover readout or selection never
190
+ shifts when the window resizes (the decimator RFC §2.3 invariant). Culling is
191
+ automatic and internal — no API change.
192
+ - **charts:** Viewport culling extended to the **per-mark** layers — scatter,
193
+ bars, candles, and boxes. These loop over independent marks with index-keyed
194
+ accessors (a scatter's `colorAt(i)`, a bar's `begin[i]` selection match), so
195
+ rather than a subarray view they cull by **restricting the draw loop to the
196
+ visible index range** (the original `i` is preserved, so every accessor and
197
+ selection/hover match stays correct). Point marks (scatter) use the same
198
+ entry/exit window as lines; interval marks (bars / candles / boxes) use a
199
+ span-overlap window that keeps a wide mark crossing a plot edge. Same
200
+ automatic, internal, interaction-safe behaviour — a 500k-bar chart zoomed in
201
+ drops from ~23 ms/frame to ~0.16 ms.
202
+ - **charts:** Scatter culling is now **radius-aware**. The per-mark index margin
203
+ above measures neighbours in sample count, but a scatter mark's disc has a
204
+ pixel radius independent of sample spacing — so a dense scatter of fat marks
205
+ could drop an edge bubble whose _centre_ sits several samples off-screen while
206
+ its _disc_ still overlaps the plot edge (a subtle flicker under pan). The
207
+ scatter draw now widens the visible window by its max drawn radius (converted
208
+ px→data through the scale, plus any `offsetPx` nudge) before culling, so every
209
+ mark that can paint into the plot is kept. Small-radius scatters are unaffected
210
+ (they re-expand by a few pixels — usually the same window). Interval marks
211
+ (bars / candles / boxes) don't need this — their width _is_ their x-span.
212
+
213
+ ### Changed
214
+
215
+ - **charts:** **Tracker labels key on `as` across the multi-value marks**
216
+ (F-charts-8 §3, the `<Legend>` label-source prerequisite). With a semantic
217
+ `as`, a `<BandChart>`'s edge samples now read `"<as> lower"` / `"<as>
218
+ upper"` and a `<Candlestick showOHLC>`'s quote pills read `"<as> high"` /
219
+ `"<as> open"` / … — the series-name + role convention `<BoxPlot>` already
220
+ shipped (`iv upper`) — so readout/legend merge keys are the series
221
+ identity, not raw column names or bare role words. Without an `as`,
222
+ labels are unchanged (column names / role words). Consumers keying
223
+ `onTrackerChanged` readouts on the old labels while setting `as` should
224
+ key on the new `"<as> <role>"` form.
225
+
226
+ ### Fixed
227
+
228
+ - **charts:** **`cursorFormat` no longer leaks into tick labels** — setting
229
+ `timeFormat` _and_ `cursorFormat` together on a time axis rendered the
230
+ **tick labels** with `cursorFormat` (the ladder was disqualified by
231
+ `timeFormat`, and the tick fallback read the merged readout formatter).
232
+ The frame now carries two channels: `formatTime` is the label channel
233
+ (`timeFormat`-shaped, never `cursorFormat` — restoring its documented
234
+ meaning), and the new optional `formatReadout` carries `cursorFormat` to
235
+ the readout consumers (crosshair pill + in-plot cursor time, marker
236
+ indicators, annotation auto-labels). Frame consumers that read
237
+ `ContainerFrame.formatTime` for a readout should use
238
+ `formatReadout ?? formatTime`.
239
+ - **charts:** **Region drag-select no longer races the pointer stream**
240
+ (#508 item 7) — the drag anchor was container **state** read back through
241
+ the rendered frame at `pointerup`, so a batched/untrusted pointer stream
242
+ (automation, jsdom — plausibly a very fast flick under load) could deliver
243
+ `down→up` before the anchor committed: the select was **silently dropped**
244
+ and the late-committing anchor **leaked**, leaving the band stuck.
245
+ Human-paced trusted input hid this (React flushes trusted discrete events
246
+ synchronously). Gesture logic now reads a ref (the same ref+state
247
+ discipline the annotation-create drag already used); the state stays
248
+ paint-only. Deterministic regression tests cover both pacings.
249
+
250
+ ## [0.48.1] — 2026-07-19
251
+
252
+ ### Fixed
253
+
254
+ - `smooth(column, 'loess', …)` is now numerically stable on absolute epoch-ms
255
+ timestamps. The local weighted regression is now conditioned on `x` centred at
256
+ the query point; previously the un-centred normal equations lost precision to
257
+ floating-point cancellation (anchors ~1.7e12 → `x²` terms ~3e24). Severity
258
+ scales with (anchor magnitude / window width)²: on finely-spaced series
259
+ (second-level cadence) the fit overshot wildly outside the data range instead
260
+ of smoothing, while coarser day-spaced series erred only subtly. Output is now
261
+ shift-invariant to the time origin. Series built from small integer anchors
262
+ (e.g. in unit tests) were unaffected, which is why this hid.
263
+
50
264
  ## [0.48.0] — 2026-07-17
51
265
 
52
266
  ### Added
@@ -53,6 +53,14 @@ type PrefixesForSeriesTuple<T extends SeriesTuple> = {
53
53
  [I in keyof T]: string;
54
54
  } extends infer Result ? Result extends readonly [string, ...string[]] ? Result : never : never;
55
55
  export declare function toKey(value: KeyLike): EventKey;
56
+ /**
57
+ * An immutable, schema-typed, ordered collection of events — the batch
58
+ * layer's core primitive. A series is constructed whole from complete data
59
+ * and never mutated: every transform (`filter`, `align`, `rollup`, …)
60
+ * returns a new `TimeSeries`, so the full analytical surface can sort,
61
+ * scan, or index freely. Example:
62
+ * `new TimeSeries({ name, schema, rows })`.
63
+ */
56
64
  export declare class TimeSeries<S extends SeriesSchema> {
57
65
  #private;
58
66
  readonly name: string;
@@ -187,10 +187,16 @@ function loessAt(x, anchors, values, span) {
187
187
  let sumWY = 0;
188
188
  let sumWXX = 0;
189
189
  let sumWXY = 0;
190
+ // Regress in local coordinates centred on the query point (`dx = pointX - x`).
191
+ // Anchors are typically absolute epoch-ms (~1.7e12), so accumulating
192
+ // `pointX * pointX` (~3e24) into the normal equations loses all precision to
193
+ // floating-point cancellation and the fit overshoots wildly. Centring keeps
194
+ // every term small; the fitted value at the query point is then just the
195
+ // intercept, since `dx = 0` there.
190
196
  for (let index = start; index < end; index++) {
191
- const pointX = anchors[index];
197
+ const dx = anchors[index] - x;
192
198
  const pointY = values[index];
193
- const distance = Math.abs(pointX - x);
199
+ const distance = Math.abs(dx);
194
200
  const ratio = distance / bandwidth;
195
201
  const weight = ratio >= 1 ? 0 : (1 - ratio ** 3) ** 3;
196
202
  if (weight === 0) {
@@ -198,10 +204,10 @@ function loessAt(x, anchors, values, span) {
198
204
  }
199
205
  weightedCount += 1;
200
206
  sumW += weight;
201
- sumWX += weight * pointX;
207
+ sumWX += weight * dx;
202
208
  sumWY += weight * pointY;
203
- sumWXX += weight * pointX * pointX;
204
- sumWXY += weight * pointX * pointY;
209
+ sumWXX += weight * dx * dx;
210
+ sumWXY += weight * dx * pointY;
205
211
  }
206
212
  if (weightedCount === 0 || sumW === 0) {
207
213
  return undefined;
@@ -210,9 +216,8 @@ function loessAt(x, anchors, values, span) {
210
216
  if (Math.abs(denominator) < Number.EPSILON) {
211
217
  return sumWY / sumW;
212
218
  }
213
- const intercept = (sumWY * sumWXX - sumWX * sumWXY) / denominator;
214
- const slope = (sumW * sumWXY - sumWX * sumWY) / denominator;
215
- return intercept + slope * x;
219
+ // Value at the query point: intercept + slope * dx, with dx = 0.
220
+ return (sumWY * sumWXX - sumWX * sumWXY) / denominator;
216
221
  }
217
222
  function lowerBound(values, target) {
218
223
  let low = 0;
@@ -447,6 +452,14 @@ function prepareSeriesForJoin(series, options) {
447
452
  * actually runs).
448
453
  */
449
454
  const TRUSTED_STORE_SENTINEL = Symbol('TimeSeries.trustedStoreSentinel');
455
+ /**
456
+ * An immutable, schema-typed, ordered collection of events — the batch
457
+ * layer's core primitive. A series is constructed whole from complete data
458
+ * and never mutated: every transform (`filter`, `align`, `rollup`, …)
459
+ * returns a new `TimeSeries`, so the full analytical surface can sort,
460
+ * scan, or index freely. Example:
461
+ * `new TimeSeries({ name, schema, rows })`.
462
+ */
450
463
  export class TimeSeries {
451
464
  name;
452
465
  schema;
@@ -41,6 +41,14 @@ export type LiveSeriesOptions<S extends SeriesSchema> = {
41
41
  type EventListener<S extends SeriesSchema> = (event: EventForSchema<S>) => void;
42
42
  type BatchListener<S extends SeriesSchema> = (events: ReadonlyArray<EventForSchema<S>>) => void;
43
43
  type EvictListener<S extends SeriesSchema> = (events: ReadonlyArray<EventForSchema<S>>) => void;
44
+ /**
45
+ * A mutable, append-optimized event buffer for live ingestion — the entry
46
+ * point to the live layer. `push` validates rows against the same schema
47
+ * as `TimeSeries`, retention (`maxEvents` / `maxAge`) bounds the buffer,
48
+ * and subscribers react on the `'event'`, `'batch'`, and `'evict'`
49
+ * channels; snapshot to the batch layer at any time with `toTimeSeries()`.
50
+ * Example: `new LiveSeries({ name, schema, retention: { maxEvents: 10_000 } })`.
51
+ */
44
52
  export declare class LiveSeries<S extends SeriesSchema> {
45
53
  #private;
46
54
  readonly name: string;
@@ -86,6 +86,14 @@ function assertCellKind(kind, value, name) {
86
86
  }
87
87
  }
88
88
  // ── LiveSeries ──────────────────────────────────────────────────
89
+ /**
90
+ * A mutable, append-optimized event buffer for live ingestion — the entry
91
+ * point to the live layer. `push` validates rows against the same schema
92
+ * as `TimeSeries`, retention (`maxEvents` / `maxAge`) bounds the buffer,
93
+ * and subscribers react on the `'event'`, `'batch'`, and `'evict'`
94
+ * channels; snapshot to the batch layer at any time with `toTimeSeries()`.
95
+ * Example: `new LiveSeries({ name, schema, retention: { maxEvents: 10_000 } })`.
96
+ */
89
97
  export class LiveSeries {
90
98
  /** @internal */
91
99
  [EMITS_EVICT] = true;
@@ -37,6 +37,15 @@ type ViewOptions<S extends SeriesSchema> = {
37
37
  * Reuses `KeyColumnForSchema`, gated on the time variant.
38
38
  */
39
39
  type TimeKeyOnly<S extends SeriesSchema> = KeyColumnForSchema<S> extends TimeKeyColumn ? KeyColumnForSchema<S> : 'LiveView.keyColumn() supports time-keyed views only — use toTimeSeries() for other key kinds';
40
+ /**
41
+ * A derived, subscribable live projection — the result of chainable live
42
+ * operators such as `filter`, `map`, `select`, `fill`, `diff`, and `rate`.
43
+ * A view subscribes to its source `LiveSource`, runs each event through
44
+ * its process function as it arrives (emitting a transformed event or
45
+ * skipping it), and mirrors source eviction — you obtain one from an
46
+ * operator rather than constructing it directly. Example:
47
+ * `live.filter((e) => (e.get('value') as number) > 5)`.
48
+ */
40
49
  export declare class LiveView<S extends SeriesSchema> implements LiveSource<S> {
41
50
  #private;
42
51
  readonly name: string;
@@ -10,6 +10,15 @@ import { float64ColumnFromArray } from '../columnar/column.js';
10
10
  import { TimeKeyColumn } from '../columnar/key-column.js';
11
11
  import { EMITS_EVICT, } from '../schema/index.js';
12
12
  import { parseDuration } from '../core/duration.js';
13
+ /**
14
+ * A derived, subscribable live projection — the result of chainable live
15
+ * operators such as `filter`, `map`, `select`, `fill`, `diff`, and `rate`.
16
+ * A view subscribes to its source `LiveSource`, runs each event through
17
+ * its process function as it arrives (emitting a transformed event or
18
+ * skipping it), and mirrors source eviction — you obtain one from an
19
+ * operator rather than constructing it directly. Example:
20
+ * `live.filter((e) => (e.get('value') as number) > 5)`.
21
+ */
13
22
  export class LiveView {
14
23
  /** @internal */
15
24
  [EMITS_EVICT] = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pond-ts",
3
- "version": "0.48.0",
3
+ "version": "0.49.0",
4
4
  "description": "TypeScript-first time series primitives",
5
5
  "license": "MIT",
6
6
  "repository": {