@michaelmagan/dnumb 0.1.1 → 0.1.2

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/AGENTS.md CHANGED
@@ -8,13 +8,13 @@ in a browser. Every output is text a model reads.
8
8
  `dnumb` turns a rendered page into measurements: element geometry, overflow, clipping, and the
9
9
  CSS that says what those measurements mean. It renders them as a linear text digest — one line per
10
10
  element that stands on its own, and one fold line standing in for a whole run of repeated siblings
11
- at once, each still addressable by id (see [`docs/digest.md`](https://github.com/michaelmagan/dnumb/blob/main/docs/digest.md#the-fold-line)) — and
11
+ at once, each still addressable by id (see [`docs/digest.md`](docs/digest.md#the-fold-line)) — and
12
12
  writes them to a self-contained JSON bundle you can query later on a machine with no app and no
13
13
  browser.
14
14
 
15
15
  **Live example** (`fixture=test/fixtures/flex-min-width-ellipsis.html line=e2`) — one real line out
16
16
  of a real capture, checked by `test/browser/doc-examples.test.ts`; the full capture is in
17
- [`docs/digest.md`](https://github.com/michaelmagan/dnumb/blob/main/docs/digest.md#a-truncated-heading).
17
+ [`docs/digest.md`](docs/digest.md#a-truncated-heading).
18
18
 
19
19
  ```text
20
20
  e2 h1.heading "Quarterly revenue review for t…" @0,0 668x28 [overflow-x +147px; clip, ellipsis]
@@ -40,7 +40,7 @@ Stay with the screenshot for *paint* — imagery, typography, and overall visual
40
40
  anything painted rather than styled (a photo, a canvas, video, a gradient's true value at the
41
41
  glyphs). Color is the one item that moved off this list: dnumb now measures the ink and the
42
42
  composited backdrop text is actually drawn on, and reports a WCAG contrast ratio, never a
43
- pass/fail — see [`docs/digest.md`](https://github.com/michaelmagan/dnumb/blob/main/docs/digest.md#color).
43
+ pass/fail — see [`docs/digest.md`](docs/digest.md#color).
44
44
  The two tools still compose well: use the image to form a suspicion and the numbers to check it.
45
45
 
46
46
  Honest cost: on the predecessor to this tool, tool-grounded review cost **25–30% more tokens**
@@ -83,9 +83,9 @@ problem:
83
83
  element with its own text gets an *effective backdrop* (the real, composited color behind it,
84
84
  not its own usually-transparent `background-color`) and, where both ink and backdrop resolve, a
85
85
  WCAG contrast ratio — never a pass/fail. Reachable via the digest's `# colors:` header line, `q
86
- color`, and `q describe`, per [`docs/digest.md`](https://github.com/michaelmagan/dnumb/blob/main/docs/digest.md#color).
86
+ color`, and `q describe`, per [`docs/digest.md`](docs/digest.md#color).
87
87
 
88
- Full grammar, every token, in [`docs/digest.md`](https://github.com/michaelmagan/dnumb/blob/main/docs/digest.md).
88
+ Full grammar, every token, in [`docs/digest.md`](docs/digest.md).
89
89
 
90
90
  **Do not treat a fact as a verdict.** Do not report `[overflow-x +147px]` as "an overflow bug"
91
91
  without the consequence tokens beside it, and do not treat the absence of facts as a clean bill of
@@ -96,8 +96,7 @@ can check the inference separately from the measurement.
96
96
 
97
97
  The predecessor to this tool died of exactly this. It measured `scrollWidth: 815, clientWidth:
98
98
  668` on a real truncated heading, then discarded the finding because the rule that owned the
99
- measurement read `text-overflow: ellipsis` as absolution. The full account is in
100
- [`docs/01-POSTMORTEM.md`](https://github.com/michaelmagan/dnumb/blob/main/docs/01-POSTMORTEM.md).
99
+ measurement read `text-overflow: ellipsis` as absolution.
101
100
 
102
101
  ## Two things that will trip you up
103
102
 
@@ -111,7 +110,7 @@ space for the ellipsis glyph and counts it in `scrollWidth`. Measured at +10px a
111
110
  +17px at font-size 20; the amount is that glyph's advance width, so it tracks font-size and varies
112
111
  with font-family. When a line says `ellipsis`, treat the number as an upper bound on the hidden
113
112
  content, not an exact width, and do not subtract a constant. Full detail in
114
- [`docs/digest.md`](https://github.com/michaelmagan/dnumb/blob/main/docs/digest.md).
113
+ [`docs/digest.md`](docs/digest.md).
115
114
 
116
115
  ## The library surface that exists today
117
116
 
@@ -136,7 +135,7 @@ import {
136
135
 
137
136
  `diffCaptures` is the other core verb — what changed between two captures, and why, at the same
138
137
  "report, do not adjudicate" register as everything else in this package. See the CLI's `diff` verb
139
- and [Reading a diff](https://github.com/michaelmagan/dnumb/blob/main/README.md#reading-a-diff) in
138
+ and [Reading a diff](README.md#reading-a-diff) in
140
139
  the README for the grammar and a worked example.
141
140
 
142
141
  Plus the geometry primitives — `intersect`, `contains`, `gapAlong`, `area`, `center` — all pure
@@ -169,8 +168,8 @@ does not.
169
168
  surface above is built and tested. The CLI has a stable verb set — `capture`, `look`, `diff`, and
170
169
  `q describe|find|at|within|distance|nearest|facts|color`
171
170
  — documented with worked examples in
172
- [README § From the CLI](https://github.com/michaelmagan/dnumb/blob/main/README.md#from-the-cli) and
173
- [§ Reading a diff](https://github.com/michaelmagan/dnumb/blob/main/README.md#reading-a-diff). This
171
+ [README § From the CLI](README.md#from-the-cli) and
172
+ [§ Reading a diff](README.md#reading-a-diff). This
174
173
  file still does not restate flags, on purpose: run `dnumb --help` for the exhaustive, versioned
175
174
  list of what your installed copy actually supports, and treat it as authoritative over any prose
176
175
  here or there.
@@ -187,7 +186,5 @@ unscoped `dnumb` package on npm (permanently blocked by npm's similarity filter)
187
186
  scoped form above, or install first: `npm install --save-dev @michaelmagan/dnumb` / `bun add -d
188
187
  @michaelmagan/dnumb`.
189
188
 
190
- The digest format is specified in
191
- [`docs/digest.md`](https://github.com/michaelmagan/dnumb/blob/main/docs/digest.md). Build progress,
192
- including what was tried and what was wrong, is in
193
- [`LOG.md`](https://github.com/michaelmagan/dnumb/blob/main/LOG.md).
189
+ The digest format is specified in [`docs/digest.md`](docs/digest.md), shipped alongside this file
190
+ in the installed package — no network access needed to read it.
package/README.md CHANGED
@@ -290,7 +290,7 @@ empty body and a page that did not change would otherwise look identical.
290
290
 
291
291
  | Doc | What it is |
292
292
  |---|---|
293
- | [docs/digest.md](docs/digest.md) | **The digest format.** Every token, every fact form, the fold and collapse syntax, the parser contract |
293
+ | [docs/digest.md](docs/digest.md) | **The digest format.** Every token, every fact form, the fold and collapse syntax, the parser contract. Ships in the tarball |
294
294
  | [AGENTS.md](AGENTS.md) | For the model that will use it. Ships in the tarball |
295
295
  | [skills/dnumb/SKILL.md](skills/dnumb/SKILL.md) | The agent skill. Ships in the tarball, discoverable via `@tanstack/intent` |
296
296
  | [LOG.md](LOG.md) | Build journal, one entry per step |
@@ -151,10 +151,86 @@ function edgeDistance(nodeStart, nodeSize, prevStart, prevSize) {
151
151
  return -(nodeEnd - prevStart);
152
152
  return nodeStart - prevEnd;
153
153
  }
154
+ /**
155
+ * Which axis `gapToken` should measure and print, for one sibling pair.
156
+ *
157
+ * For `row` and `column` flex containers this is not a judgment call: flexbox
158
+ * has exactly one main axis, fixed by `flex-direction`, and every child is
159
+ * placed along it by the spec regardless of what the cross axis happens to
160
+ * measure between any two of them. `docs/digest.md`'s own pinned example
161
+ * (`clipped-menu.html`) is a real case where trusting the cross axis instead
162
+ * would have been wrong: `e5` (a dropdown flown out below the toolbar) and
163
+ * `e4` (a toolbar button) sit in a `[row]` container, genuinely 5px apart on
164
+ * y (the dropdown starts below the row, not overlapping it) and 110px
165
+ * overlapping on x -- the row's own axis. The declared axis wins there
166
+ * unconditionally, which is why this function never second-guesses `row` or
167
+ * `column`.
168
+ *
169
+ * `grid` is different in kind, not degree: CSS Grid is two-dimensional, and
170
+ * `analysis/neighbors.ts#siblingGaps` and `analysis/uniformity.ts#
171
+ * consecutiveGaps` already encode this ("`flow: 'grid'` has no single
172
+ * well-defined axis") by skipping grid containers outright for the analyses
173
+ * that need one global axis per container. The element line cannot skip --
174
+ * every non-first sibling gets a gap token -- so it asks the only question
175
+ * that has an honest answer for a *specific pair*: are these two boxes
176
+ * actually adjacent along x, along y, or is the relationship genuinely
177
+ * ambiguous? Two consecutive DOM-order children of a `grid-template-columns:
178
+ * repeat(N, ...)` row (the common case -- a stat-card row, a two-column
179
+ * article layout) sit at the same y with the same height and a real x gutter
180
+ * between them; before this function existed, `gapToken` always measured
181
+ * grid children vertically, so that real horizontal gutter (`ar.wikipedia.org`,
182
+ * real capture: a 24px column gutter between the TOC sidebar and the main
183
+ * content) came out as `↓-11195` and `↓-11167` -- values with the *shape* of
184
+ * a fabricated near-total vertical overlap, which is exactly the mechanism
185
+ * `docs/notes/07-refusal.md` §6 and a blind-read rater on `dashboard-
186
+ * defect.html`'s 4-column `stats-grid` (`↓-112`, one card's full height) both
187
+ * hit: a direction-agnostic negative number, printed on the one flow kind
188
+ * with no declared axis to check it against, read as a literal overlap
189
+ * claim.
190
+ *
191
+ * The rule, applied only when `flow === 'grid'`:
192
+ *
193
+ * - One axis shows a real separation (no overlap, i.e. `>= 0`) and the
194
+ * other shows overlap: the boxes are unambiguously adjacent along the
195
+ * separated axis (aligned/overlapping on the other is what "in the same
196
+ * row/column" looks like) -- print that axis. This is the fix for the
197
+ * stat-card and Wikipedia cases above.
198
+ * - Both axes show a real separation: a diagonal case with no overlap on
199
+ * either axis at all, the shape of a grid-auto-flow row-wrap boundary
200
+ * (last card of row N to first card of row N+1) -- the smaller of the two
201
+ * magnitudes is kept, since the wrap boundary's "distance" back across the
202
+ * whole row width is an artifact of printing one linear sequence for a
203
+ * 2-D layout, while the real row-to-row gap is usually the smaller number
204
+ * and is what a reader means by "the gap to the next one".
205
+ * - Both axes overlap: no separation anywhere to anchor a choice on (a
206
+ * decorative full-bleed overlay sibling, e.g., `linear-app`'s `e347`) --
207
+ * falls back to the same vertical default every other ambiguous flow kind
208
+ * (`column`, `none`) already uses, unchanged from before this fix.
209
+ *
210
+ * None of this touches `row`, `column`, or `none`: their gap arithmetic and
211
+ * axis were already correct (see `edgeDistance`'s own doc comment for the
212
+ * direction-agnostic RTL fix), and `07-refusal.md` §6 names the fabrication
213
+ * as specific to grid/row-flow siblings, not flex row/column.
214
+ */
215
+ function resolveGapAxis(node, previous, flow) {
216
+ if (flow !== 'grid')
217
+ return flow !== 'row';
218
+ const h = edgeDistance(node.rect.x, node.rect.width, previous.rect.x, previous.rect.width);
219
+ const v = edgeDistance(node.rect.y, node.rect.height, previous.rect.y, previous.rect.height);
220
+ const hSeparated = Number.isFinite(h) && h >= 0;
221
+ const vSeparated = Number.isFinite(v) && v >= 0;
222
+ if (hSeparated && !vSeparated)
223
+ return false;
224
+ if (vSeparated && !hSeparated)
225
+ return true;
226
+ if (hSeparated && vSeparated)
227
+ return Math.abs(v) <= Math.abs(h);
228
+ return true; // both overlap: no separated axis to anchor on, keep the old default.
229
+ }
154
230
  function gapToken(node, previous, flow) {
155
231
  if (!previous)
156
232
  return '';
157
- const vertical = flow !== 'row';
233
+ const vertical = resolveGapAxis(node, previous, flow);
158
234
  const value = vertical
159
235
  ? edgeDistance(node.rect.y, node.rect.height, previous.rect.y, previous.rect.height)
160
236
  : edgeDistance(node.rect.x, node.rect.width, previous.rect.x, previous.rect.width);
@@ -147,10 +147,86 @@ function edgeDistance(nodeStart, nodeSize, prevStart, prevSize) {
147
147
  return -(nodeEnd - prevStart);
148
148
  return nodeStart - prevEnd;
149
149
  }
150
+ /**
151
+ * Which axis `gapToken` should measure and print, for one sibling pair.
152
+ *
153
+ * For `row` and `column` flex containers this is not a judgment call: flexbox
154
+ * has exactly one main axis, fixed by `flex-direction`, and every child is
155
+ * placed along it by the spec regardless of what the cross axis happens to
156
+ * measure between any two of them. `docs/digest.md`'s own pinned example
157
+ * (`clipped-menu.html`) is a real case where trusting the cross axis instead
158
+ * would have been wrong: `e5` (a dropdown flown out below the toolbar) and
159
+ * `e4` (a toolbar button) sit in a `[row]` container, genuinely 5px apart on
160
+ * y (the dropdown starts below the row, not overlapping it) and 110px
161
+ * overlapping on x -- the row's own axis. The declared axis wins there
162
+ * unconditionally, which is why this function never second-guesses `row` or
163
+ * `column`.
164
+ *
165
+ * `grid` is different in kind, not degree: CSS Grid is two-dimensional, and
166
+ * `analysis/neighbors.ts#siblingGaps` and `analysis/uniformity.ts#
167
+ * consecutiveGaps` already encode this ("`flow: 'grid'` has no single
168
+ * well-defined axis") by skipping grid containers outright for the analyses
169
+ * that need one global axis per container. The element line cannot skip --
170
+ * every non-first sibling gets a gap token -- so it asks the only question
171
+ * that has an honest answer for a *specific pair*: are these two boxes
172
+ * actually adjacent along x, along y, or is the relationship genuinely
173
+ * ambiguous? Two consecutive DOM-order children of a `grid-template-columns:
174
+ * repeat(N, ...)` row (the common case -- a stat-card row, a two-column
175
+ * article layout) sit at the same y with the same height and a real x gutter
176
+ * between them; before this function existed, `gapToken` always measured
177
+ * grid children vertically, so that real horizontal gutter (`ar.wikipedia.org`,
178
+ * real capture: a 24px column gutter between the TOC sidebar and the main
179
+ * content) came out as `↓-11195` and `↓-11167` -- values with the *shape* of
180
+ * a fabricated near-total vertical overlap, which is exactly the mechanism
181
+ * `docs/notes/07-refusal.md` §6 and a blind-read rater on `dashboard-
182
+ * defect.html`'s 4-column `stats-grid` (`↓-112`, one card's full height) both
183
+ * hit: a direction-agnostic negative number, printed on the one flow kind
184
+ * with no declared axis to check it against, read as a literal overlap
185
+ * claim.
186
+ *
187
+ * The rule, applied only when `flow === 'grid'`:
188
+ *
189
+ * - One axis shows a real separation (no overlap, i.e. `>= 0`) and the
190
+ * other shows overlap: the boxes are unambiguously adjacent along the
191
+ * separated axis (aligned/overlapping on the other is what "in the same
192
+ * row/column" looks like) -- print that axis. This is the fix for the
193
+ * stat-card and Wikipedia cases above.
194
+ * - Both axes show a real separation: a diagonal case with no overlap on
195
+ * either axis at all, the shape of a grid-auto-flow row-wrap boundary
196
+ * (last card of row N to first card of row N+1) -- the smaller of the two
197
+ * magnitudes is kept, since the wrap boundary's "distance" back across the
198
+ * whole row width is an artifact of printing one linear sequence for a
199
+ * 2-D layout, while the real row-to-row gap is usually the smaller number
200
+ * and is what a reader means by "the gap to the next one".
201
+ * - Both axes overlap: no separation anywhere to anchor a choice on (a
202
+ * decorative full-bleed overlay sibling, e.g., `linear-app`'s `e347`) --
203
+ * falls back to the same vertical default every other ambiguous flow kind
204
+ * (`column`, `none`) already uses, unchanged from before this fix.
205
+ *
206
+ * None of this touches `row`, `column`, or `none`: their gap arithmetic and
207
+ * axis were already correct (see `edgeDistance`'s own doc comment for the
208
+ * direction-agnostic RTL fix), and `07-refusal.md` §6 names the fabrication
209
+ * as specific to grid/row-flow siblings, not flex row/column.
210
+ */
211
+ function resolveGapAxis(node, previous, flow) {
212
+ if (flow !== 'grid')
213
+ return flow !== 'row';
214
+ const h = edgeDistance(node.rect.x, node.rect.width, previous.rect.x, previous.rect.width);
215
+ const v = edgeDistance(node.rect.y, node.rect.height, previous.rect.y, previous.rect.height);
216
+ const hSeparated = Number.isFinite(h) && h >= 0;
217
+ const vSeparated = Number.isFinite(v) && v >= 0;
218
+ if (hSeparated && !vSeparated)
219
+ return false;
220
+ if (vSeparated && !hSeparated)
221
+ return true;
222
+ if (hSeparated && vSeparated)
223
+ return Math.abs(v) <= Math.abs(h);
224
+ return true; // both overlap: no separated axis to anchor on, keep the old default.
225
+ }
150
226
  function gapToken(node, previous, flow) {
151
227
  if (!previous)
152
228
  return '';
153
- const vertical = flow !== 'row';
229
+ const vertical = resolveGapAxis(node, previous, flow);
154
230
  const value = vertical
155
231
  ? edgeDistance(node.rect.y, node.rect.height, previous.rect.y, previous.rect.height)
156
232
  : edgeDistance(node.rect.x, node.rect.width, previous.rect.x, previous.rect.width);