@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 +12 -15
- package/README.md +1 -1
- package/dist/digest/render.cjs +77 -1
- package/dist/digest/render.js +77 -1
- package/docs/digest.md +1600 -0
- package/package.json +4 -2
- package/skills/dnumb/SKILL.md +7 -7
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`](
|
|
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`](
|
|
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`](
|
|
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`](
|
|
86
|
+
color`, and `q describe`, per [`docs/digest.md`](docs/digest.md#color).
|
|
87
87
|
|
|
88
|
-
Full grammar, every token, in [`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.
|
|
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`](
|
|
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](
|
|
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](
|
|
173
|
-
[§ Reading a diff](
|
|
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
|
-
|
|
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 |
|
package/dist/digest/render.cjs
CHANGED
|
@@ -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 =
|
|
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);
|
package/dist/digest/render.js
CHANGED
|
@@ -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 =
|
|
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);
|