@variance-authority/core 0.9.0 → 0.10.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 +17 -0
- package/README.md +46 -20
- package/dist/attribute/boundary.d.ts +21 -14
- package/dist/attribute/boundary.js +23 -29
- package/dist/attribute/boundary.js.map +1 -1
- package/dist/attribute/component-hash.d.ts +21 -0
- package/dist/attribute/component-hash.js +48 -3
- package/dist/attribute/component-hash.js.map +1 -1
- package/dist/attribute/index.d.ts +3 -3
- package/dist/attribute/index.js +1 -1
- package/dist/attribute/index.js.map +1 -1
- package/dist/attribute/lexicon.d.ts +37 -0
- package/dist/attribute/lexicon.js +40 -12
- package/dist/attribute/lexicon.js.map +1 -1
- package/dist/attribute/rename.d.ts +18 -0
- package/dist/attribute/rename.js +24 -0
- package/dist/attribute/rename.js.map +1 -0
- package/dist/compare/cascade.d.ts +2 -0
- package/dist/compare/cascade.js +5 -1
- package/dist/compare/cascade.js.map +1 -1
- package/dist/format/hash.d.ts +20 -0
- package/dist/format/hash.js +27 -1
- package/dist/format/hash.js.map +1 -1
- package/dist/format/index.d.ts +1 -1
- package/dist/format/index.js +1 -1
- package/dist/format/index.js.map +1 -1
- package/dist/format/snapshot.d.ts +15 -0
- package/dist/format/snapshot.js.map +1 -1
- package/dist/share/http-line.d.ts +30 -0
- package/dist/share/http-line.js +164 -0
- package/dist/share/http-line.js.map +1 -0
- package/dist/share/index.d.ts +9 -126
- package/dist/share/index.js +9 -168
- package/dist/share/index.js.map +1 -1
- package/dist/share/line.d.ts +153 -0
- package/dist/share/line.js +184 -0
- package/dist/share/line.js.map +1 -0
- package/dist/share/publish.d.ts +79 -0
- package/dist/share/publish.js +231 -0
- package/dist/share/publish.js.map +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @variance-authority/core
|
|
2
2
|
|
|
3
|
+
## 0.10.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- c743d3a: A share holds lines only. `SharedCache`, `SharedHit`, `neverFails`, `shareKey`, `firstShared`, `memoryShare`, `httpShare` and `HttpShareOptions` are removed from `@variance-authority/core/share`, and `createDirectoryShare` from `@variance-authority/store/share`. Use `httpLineCell`, `memoryLineCell`, `createDirectoryLineCell` or `createGitLineCell` with `publishLine` and `readLine`: a line keeps the latest entries a mainline or a branch published, and a miss says why. `neverFails` in `@variance-authority/raster` is unchanged.
|
|
8
|
+
- 6e5d53a: A file-backed baseline store files each baseline under a directory named for the renderer identity that painted it: a digest of the renderer, engine, platform, scale and fonts. That directory is now named `v1-<hex>`. It was named with the digest as written, `v1:<hex>`. NTFS refuses a colon in a file name, so a baseline root that still holds a `v1:` directory cannot be checked out on Windows, and `actions/upload-artifact` refuses to upload it. The render cache, which is in your cache directory unless `cacheRoot` says otherwise, now uses `v1-` for its identity directories and for each entry's file name.
|
|
9
|
+
|
|
10
|
+
A store treats `v1:<hex>` and `v1-<hex>` as the same identity and reads both. When `v1-<hex>` has no baseline for a subject, the store looks in `v1:<hex>`. A baseline under another machine's identity, in either spelling, still makes the subject `incomparable`.
|
|
11
|
+
|
|
12
|
+
`variance accept` writes each subject it accepts under `v1-<hex>` and deletes that subject's copy under `v1:<hex>`. It does not touch a subject whose pixels did not change, so a baseline that never changes stays under the old name, and the root stays unreadable on Windows until you move it. For each `v1:` directory, `variance doctor` prints how many baselines it holds and the `v1-` directory to move them into. Move the files with `git mv`. Render-cache entries under the old names are never read again, and the sweep every run applies to the cache deletes them.
|
|
13
|
+
|
|
14
|
+
`@variance-authority/core/format` exports `digestFileName`, which spells a digest as a path segment, and `digestOfFileName`, which reads a digest back from either spelling.
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- 11c1619: `httpLineCell` reads a 4xx answer as `refused`, not only 401 and 403, because every 4xx is the store saying no: a key it will not hold, a path it does not route, a token it does not take. The detail carries the store's own reason when the body gives one, as the `error` of a JSON body or as plain text, on one line and at most 1000 characters: `<url>: HTTP 422: the object key … names "feature" …`. A 404 is still `absent`. 408, 429 and every 5xx are `unreachable`, because they say the store could not take the request now, and they carry the reason the same way.
|
|
19
|
+
|
|
3
20
|
## 0.9.0
|
|
4
21
|
|
|
5
22
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -157,7 +157,7 @@ specifier exports only the capture artifact.
|
|
|
157
157
|
| `core/plan` | the whole configuration of a run — profile, ruleset version, viewport, policy, interventions — as one value, plus the identity digest derived from it |
|
|
158
158
|
| `core/relate` | what rests on what: a file graph in adjacency form, the components a change reaches, and a closure digest over each one |
|
|
159
159
|
| `core/segment` | columnar bytes: named columns, interned strings, and the validation a decode performs before it believes a file |
|
|
160
|
-
| `core/share` | leaving those bytes where another machine finds them,
|
|
160
|
+
| `core/share` | leaving those bytes where another machine finds them, as the latest of a mainline or a branch |
|
|
161
161
|
|
|
162
162
|
Five of those are the order an answer travels through: `format`, `rules`,
|
|
163
163
|
`compare`, `attribute`, `judge`. `plan` and `relate` are asked before anything
|
|
@@ -292,35 +292,61 @@ run that recorded no component provenance gets `unread` instead.
|
|
|
292
292
|
|
|
293
293
|
What a suite is made of is a fact about a commit rather than about a run.
|
|
294
294
|
`core/segment` writes such a thing as columns — equal facts encode to equal
|
|
295
|
-
bytes, which is what lets a transport skip an upload — and `core/share`
|
|
296
|
-
the bytes.
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
295
|
+
bytes, which is what lets a transport skip an upload — and `core/share` keeps
|
|
296
|
+
the latest of those bytes on a line. A checkout asks *what is mainline now*,
|
|
297
|
+
and a line answers that: one mainline or one branch, holding the latest entry
|
|
298
|
+
of each kind — the run report, one record per suite — and the images those
|
|
299
|
+
entries name, by digest.
|
|
300
|
+
|
|
301
|
+
Excerpt: `token` is your store's credential, `report` is the entry's bytes, and
|
|
302
|
+
`digests` are the images it names. `core/share` stores an entry's bytes and
|
|
303
|
+
never reads them, so their format is yours. The CLI's `report-v1` entry is
|
|
304
|
+
two parts: the run report's JSON, and a JSON table from each image path the
|
|
305
|
+
report names to that image's digest.
|
|
300
306
|
|
|
301
307
|
```ts
|
|
302
|
-
import {
|
|
308
|
+
import { findEntry, httpLineCell, publishLine, readLine } from '@variance-authority/core/share';
|
|
303
309
|
|
|
304
|
-
const
|
|
310
|
+
const cell = httpLineCell({
|
|
305
311
|
endpoint: 'https://objects.example.com/variance',
|
|
306
312
|
headers: { authorization: `Bearer ${token}` },
|
|
307
313
|
method: 'PUT',
|
|
308
314
|
});
|
|
315
|
+
const main = { kind: 'mainline', name: 'main' } as const;
|
|
316
|
+
|
|
317
|
+
await publishLine(cell, main, [
|
|
318
|
+
{ name: 'report-v1', commit, bytes: report, images: digests },
|
|
319
|
+
], { descends, image: (digest) => readImage(digest) });
|
|
309
320
|
|
|
310
|
-
|
|
321
|
+
const held = await readLine(cell, main);
|
|
322
|
+
const entry = 'manifest' in held ? findEntry(held.manifest, 'report-v1') : held;
|
|
311
323
|
```
|
|
312
324
|
|
|
313
|
-
`
|
|
314
|
-
|
|
315
|
-
`
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
325
|
+
`cell` is the backend: a manifest it replaces only against the version you
|
|
326
|
+
read, and the blobs the manifest names. `httpLineCell` writes the manifest with
|
|
327
|
+
`If-Match`, which S3, GCS and R2 honour. `endpoint` is a base URL every path is
|
|
328
|
+
appended to. `headers` is sent on every request, which is where a bucket's
|
|
329
|
+
`Authorization` or a deployment's token goes. `method` is the verb a write uses
|
|
330
|
+
— `PUT` for a bucket, `POST` for a deployment that routes on it. `timeoutMs`
|
|
331
|
+
is how long one request may take, reading its body included, before the store
|
|
332
|
+
is `unreachable` with a message that says it timed out; it is 60 000 when you
|
|
333
|
+
leave it out. A presigned base needs only the endpoint. `memoryLineCell()`
|
|
334
|
+
holds a line in this process, for tests and for a run that shares with itself.
|
|
335
|
+
|
|
336
|
+
`descends` is git's answer to whether one commit strictly descends from
|
|
337
|
+
another, and `undefined` when the history is not at hand. `image` supplies the
|
|
338
|
+
bytes of an image the line does not hold yet, so an unchanged image is sent
|
|
339
|
+
once. `attempts` bounds how many times a publish re-reads a line other writers
|
|
340
|
+
keep moving.
|
|
341
|
+
|
|
342
|
+
A publish replaces only the entries it carries, so suites from different jobs
|
|
343
|
+
land on one line. On a mainline, a held entry whose commit descends from yours
|
|
344
|
+
stays: that is an older run finishing last. A held entry in a newer format stays
|
|
345
|
+
on any line. A line reports why it missed — `absent`, `newer`, `refused`,
|
|
346
|
+
`unreachable` or `unreadable` — because each asks you for a different action.
|
|
347
|
+
Everything a line holds can be derived again, so a miss costs you the
|
|
348
|
+
derivation. [Sharing an evaluation](https://variance-authority.dev/docs/sharing)
|
|
349
|
+
is the operator's side of it.
|
|
324
350
|
|
|
325
351
|
## What it refuses
|
|
326
352
|
|
|
@@ -2,6 +2,8 @@ import type { CanonicalValue } from '../format/canonical.js';
|
|
|
2
2
|
import type { Rect } from '../format/capture.js';
|
|
3
3
|
import type { Digest } from '../format/hash.js';
|
|
4
4
|
import type { SemanticNode } from '../format/snapshot.js';
|
|
5
|
+
import { type Rename } from './rename.js';
|
|
6
|
+
export type { Rename } from './rename.js';
|
|
5
7
|
/**
|
|
6
8
|
* Where one component's nodes stop and the next component's begin.
|
|
7
9
|
*
|
|
@@ -149,19 +151,24 @@ export interface Shape {
|
|
|
149
151
|
* theme, so a subject-level list names them all and explains nothing.
|
|
150
152
|
*/
|
|
151
153
|
readonly tokens: readonly string[];
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
154
|
+
/**
|
|
155
|
+
* Every distinct value a declaration set on this boundary's own nodes, per
|
|
156
|
+
* property, and every value a custom property resolved to, under its `--name`.
|
|
157
|
+
* Not part of any digest, and present only when the caller passed what was
|
|
158
|
+
* declared.
|
|
159
|
+
*
|
|
160
|
+
* The `style` digest can say a component's declarations are not what they were
|
|
161
|
+
* and never say which. These are the values it was built from, kept so two
|
|
162
|
+
* revisions can be compared by property — `padding-left 8px → 14px` — on a
|
|
163
|
+
* path that holds two sidecars and no documents.
|
|
164
|
+
*
|
|
165
|
+
* Declared, not computed: a node's `style` is the whole allowlist, about a
|
|
166
|
+
* hundred properties sitting at their initial values, and recording those
|
|
167
|
+
* multiplied a sidecar's size by six to say nothing anybody wrote. Layout
|
|
168
|
+
* output is left out for the reason {@link LAYOUT_OUTPUT} gives: it moves with
|
|
169
|
+
* every box, and `box` already says by how much.
|
|
170
|
+
*/
|
|
171
|
+
readonly values?: Readonly<Record<string, readonly string[]>>;
|
|
165
172
|
}
|
|
166
173
|
/**
|
|
167
174
|
* One boundary's content, stopping at nested boundaries.
|
|
@@ -187,5 +194,5 @@ export interface Rename {
|
|
|
187
194
|
* Paths are not hashed. A path is an address that shifts when an unrelated
|
|
188
195
|
* sibling is inserted, so hashing one reports a change nobody made.
|
|
189
196
|
*/
|
|
190
|
-
export declare function shapeOf(boundary: Boundary, layout: boolean, rename?: Rename): Shape;
|
|
197
|
+
export declare function shapeOf(boundary: Boundary, layout: boolean, rename?: Rename, declared?: ReadonlySet<string>): Shape;
|
|
191
198
|
//# sourceMappingURL=boundary.d.ts.map
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { declares } from '../compare/cascade.js';
|
|
2
|
+
import { renamedAlias, renamedAttributes, renamedTokens } from './rename.js';
|
|
1
3
|
/**
|
|
2
4
|
* Where one component's nodes stop and the next component's begin.
|
|
3
5
|
*
|
|
@@ -201,7 +203,7 @@ export function boundaries(root) {
|
|
|
201
203
|
* Paths are not hashed. A path is an address that shifts when an unrelated
|
|
202
204
|
* sibling is inserted, so hashing one reports a change nobody made.
|
|
203
205
|
*/
|
|
204
|
-
export function shapeOf(boundary, layout, rename) {
|
|
206
|
+
export function shapeOf(boundary, layout, rename, declared) {
|
|
205
207
|
const { node, component, rung } = boundary;
|
|
206
208
|
const style = [];
|
|
207
209
|
const geometry = [];
|
|
@@ -210,21 +212,33 @@ export function shapeOf(boundary, layout, rename) {
|
|
|
210
212
|
const wiring = [];
|
|
211
213
|
const renders = [];
|
|
212
214
|
const tokens = new Set();
|
|
215
|
+
const values = new Map();
|
|
216
|
+
const note = (property, value) => {
|
|
217
|
+
const seen = values.get(property) ?? new Set();
|
|
218
|
+
seen.add(value);
|
|
219
|
+
values.set(property, seen);
|
|
220
|
+
};
|
|
213
221
|
let nodes = 0;
|
|
214
222
|
const walk = (current) => {
|
|
215
223
|
nodes += 1;
|
|
216
|
-
for (const token of Object.
|
|
224
|
+
for (const [token, value] of Object.entries(current.tokens ?? {})) {
|
|
217
225
|
tokens.add(token);
|
|
218
|
-
|
|
226
|
+
if (declared !== undefined)
|
|
227
|
+
note(token, value);
|
|
228
|
+
}
|
|
229
|
+
const own = {};
|
|
219
230
|
const measured = {};
|
|
220
231
|
for (const [property, value] of Object.entries(current.style)) {
|
|
221
232
|
const resolved = rename === undefined ? value : rename.style(value);
|
|
222
233
|
if (layout && LAYOUT_OUTPUT.has(property))
|
|
223
234
|
measured[property] = resolved;
|
|
224
|
-
else
|
|
225
|
-
|
|
235
|
+
else {
|
|
236
|
+
own[property] = resolved;
|
|
237
|
+
if (declared !== undefined && declares(declared, current.path, property))
|
|
238
|
+
note(property, value);
|
|
239
|
+
}
|
|
226
240
|
}
|
|
227
|
-
style.push({ style:
|
|
241
|
+
style.push({ style: own, tokens: renamedTokens(current, rename) });
|
|
228
242
|
// An entry per node, always. `null` for a node with no rect keeps position in
|
|
229
243
|
// the list meaningful: dropping the entry would let two different trees agree
|
|
230
244
|
// by coincidence.
|
|
@@ -300,29 +314,9 @@ export function shapeOf(boundary, layout, rename) {
|
|
|
300
314
|
renders,
|
|
301
315
|
nodes,
|
|
302
316
|
tokens: [...tokens].sort(),
|
|
317
|
+
...(declared === undefined
|
|
318
|
+
? {}
|
|
319
|
+
: { values: Object.fromEntries([...values].map(([property, seen]) => [property, [...seen]])) }),
|
|
303
320
|
};
|
|
304
321
|
}
|
|
305
|
-
function renamedAlias(node, rename) {
|
|
306
|
-
if (rename === undefined || node.alias === undefined)
|
|
307
|
-
return node.alias;
|
|
308
|
-
return rename.alias(node.alias);
|
|
309
|
-
}
|
|
310
|
-
function renamedAttributes(node, rename) {
|
|
311
|
-
if (rename === undefined)
|
|
312
|
-
return node.attributes;
|
|
313
|
-
const renamed = {};
|
|
314
|
-
for (const [name, value] of Object.entries(node.attributes)) {
|
|
315
|
-
renamed[name] = rename.attribute(name, value);
|
|
316
|
-
}
|
|
317
|
-
return renamed;
|
|
318
|
-
}
|
|
319
|
-
function renamedTokens(node, rename) {
|
|
320
|
-
if (rename === undefined || node.tokens === undefined)
|
|
321
|
-
return node.tokens;
|
|
322
|
-
const renamed = {};
|
|
323
|
-
for (const [name, value] of Object.entries(node.tokens)) {
|
|
324
|
-
renamed[name] = rename.style(value);
|
|
325
|
-
}
|
|
326
|
-
return renamed;
|
|
327
|
-
}
|
|
328
322
|
//# sourceMappingURL=boundary.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"boundary.js","sourceRoot":"","sources":["../../src/attribute/boundary.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CanonicalValue } from '../format/canonical.js';\nimport type { Rect } from '../format/capture.js';\nimport type { Digest } from '../format/hash.js';\nimport type { SemanticNode } from '../format/snapshot.js';\n\n/**\n * Where one component's nodes stop and the next component's begin.\n *\n * Two relations run upward out of a rendered node and they are not the same\n * relation. **Parent** is what is above it — React's `return` chain, the boxes\n * it ended up inside. **Owner** is what rendered it — React's `_debugOwner`, the\n * code that wrote the element. A layout wrapper is the parent of everything\n * handed to it and the owner of none of it.\n *\n * Both are needed here, for different halves of the same answer:\n *\n * - **Parent decides nesting.** A boundary owns a contiguous region of the\n * document, and what encloses that region is a fact about the tree. Reading\n * the owner for this would produce a boundary set that is not a partition.\n * - **Owner decides membership and naming.** A component's own content is what\n * *it* wrote. Content it was handed is a hole in its output — present, sized,\n * positioned by it, and authored somewhere else.\n *\n * Reading the parent for both — which is what a walk over `owners[0]` does — has\n * two consequences, and they are the two shapes a design system is made of:\n *\n * - **A component that renders only components disappears.** It authors no host\n * node, so it is never any node's nearest enclosing composite, so it is a\n * boundary nowhere. That is every variant wrapper (`DangerButton` returning a\n * `Button`) and every page-level assembly. The information is not missing —\n * the composite is in the chain, one rung up — it is discarded by reading only\n * the head of it. {@link boundaries} enters every rung the chain crosses.\n * - **A container absorbs its caller's content.** `Card` renders one `div` and\n * whatever it was given; hashed by enclosure, its digest moves whenever a\n * caller passes something else, while its props digest — which excludes\n * `children` — says its inputs held. {@link shapeOf} names a child boundary\n * only when this component placed it, and leaves an anonymous hole otherwise.\n *\n * The owner half is a development-build artefact: React populates `_debugOwner`\n * from `element._owner`, and a production bundle does not. Absent, both rules\n * degrade to naming everything, which is the enclosure answer — coarser, never\n * wrong in a new direction.\n *\n * Nothing here is exported from the package. The callers are siblings.\n */\n\n/** A node whose provenance chain broke. Not a filler category; see `RootKind`. */\nexport const UNATTRIBUTED = '(unattributed)';\n\n/**\n * Properties whose *computed* value is layout output rather than authored input.\n *\n * The distinction this list exists for is not stylistic. Under a profile with a\n * layout engine the snapshot carries the engine's resolved values, so a block\n * element's computed `height` is whatever its contents made it — and a button\n * two levels down growing by six pixels moves the computed height of every\n * ancestor. Hashed as *style*, that reports every enclosing component as having\n * changed, which is precisely the \"area ranks the displaced above the displacer\"\n * failure the cause hashes exist to fix, arriving through a different door.\n *\n * Measured on `cases/storybook-case`: one padding edit inside `Button` made\n * `Tokens`, `Stack`, `Card` and the unattributed root all report a moved style\n * hash, so *every* component in every affected story was named a cause.\n *\n * They are folded into `geometry` instead, where \"this component's box is a\n * different size\" already belongs and where it correctly does not make a cause.\n * Under a profile *without* layout there are no computed values, so these are\n * authored declarations like any other and stay in `style` — that tier has no\n * `geometry` digest to move them to, and a declared `width: 100px` really is the\n * component's own content.\n */\nexport const LAYOUT_OUTPUT: ReadonlySet<string> = new Set([\n 'width',\n 'height',\n 'min-width',\n 'min-height',\n 'max-width',\n 'max-height',\n // The one nobody guesses, and the one that was actually doing the damage.\n // `transform-origin` computes to half the border box — `512px 41px` — so it\n // moves whenever the box does, on every element, including ones that declare\n // no transform at all. Removing `width` and `height` changed nothing on the\n // case measurement; removing this is what made a padding edit inside `Button`\n // stop naming `Tokens`, `Stack`, `Card` and the root as causes.\n 'transform-origin',\n // Chromium returns *used* track sizes here — `\"70.39px 953.61px\"` — never the\n // author's `auto 1fr`, so nothing declared survives to be lost. That is what\n // makes these two safe on a list, and it is exactly what is not true of the\n // properties named below.\n 'grid-template-columns',\n 'grid-template-rows',\n]);\n\n/**\n * Five more shapes leak, and a longer list is the wrong fix. Measured.\n *\n * Editing only a descendant's padding in a real Chromium moves the computed\n * value of an ancestor's `padding-*` (percentage padding in a shrink-to-fit\n * box), `margin-*` (an `auto` margin centring a `fit-content` block),\n * `top`/`right`/`bottom`/`left` (an absolutely positioned element anchored to an\n * edge that moved) and `transform` (a `translate(-50%,-50%)` matrix, the exact\n * sibling of the `transform-origin` case above). Each one makes an enclosing\n * component a false cause.\n *\n * Adding them here was tried and is worse than the disease: with `padding-*` on\n * the list, `causes.test.ts`'s headline case — a padding edit *inside* `Button`\n * — stops naming `Button` at all. For these properties the computed value **is**\n * the authored value in the ordinary case, and a property name cannot tell the\n * two apart. Only a *value* can, and the only place that knows is `resolveStyle`\n * in `rules/normalize/cascade.ts`, which sees both the cascade's winner and the\n * engine's override and currently keeps no record of which superseded which.\n *\n * So the limit is stated rather than half-fixed: a component whose own\n * declarations use percentage padding, auto margins, edge-anchored absolute\n * positioning or a percentage translate can be named a cause by a change that\n * was not its own. The failure is a *false* cause, never a missed one, which is\n * the direction that costs a reader attention rather than a regression.\n */\n\n/**\n * Every component holding a node, outermost first, and who placed each one.\n *\n * The whole chain, not its head. `TodoFooter` rendering nothing but a `Stack`\n * appears here at the rung below `Stack`, which is the only place it appears at\n * all — it authors no element, so no node has it as a nearest enclosing\n * composite, and a walk that reads `owners[0]` cannot see it.\n *\n * `placedBy[i]` is who wrote the element for `stack[i]`: `OwnerFrame.createdBy`,\n * which is the component that decided a `<Stack>` belongs at this point, as\n * distinct from `Stack` itself. Absent per rung on a production build.\n *\n * A host node whose author is not the innermost component is content that was\n * handed in: it sits inside the container's box and belongs to whoever wrote it,\n * so the author is pushed as a further rung. That keeps the boundary set a\n * partition of the document — the slotted region is nested inside the container\n * rather than lifted out of it — while attributing it to the code responsible.\n */\ninterface Ownership {\n readonly stack: readonly string[];\n readonly placedBy: readonly (string | undefined)[];\n readonly props: readonly (Digest | undefined)[];\n}\n\nfunction ownershipOf(node: SemanticNode): Ownership {\n const owners = node.provenance?.owners;\n if (owners === undefined || owners.length === 0) {\n return { stack: [UNATTRIBUTED], placedBy: [undefined], props: [undefined] };\n }\n\n const stack: string[] = [];\n const placedBy: (string | undefined)[] = [];\n const props: (Digest | undefined)[] = [];\n for (let index = owners.length - 1; index >= 0; index -= 1) {\n const frame = owners[index]!;\n stack.push(frame.name);\n placedBy.push(frame.createdBy);\n props.push(frame.propsDigest);\n }\n\n // FIXME: what should this name when the author is a slot?\n //\n // Radix `Slot` under `asChild` merges the caller's className onto the child's\n // own host node, so `<Button asChild><Link/></Button>` renders one `<a>` whose\n // author is next/link's `LinkComponent` and whose every styling decision came\n // from `Button`. The owner rule then reports a name out of `node_modules` for a\n // region an edit to `button.tsx` moved, and both halves are individually\n // correct: `Button` really did author no host node, and `LinkComponent` really\n // did write the one that moved.\n //\n // Open, because the naive fix is worse. Preferring the nearest caller whenever\n // the author is unfamiliar would credit every container with its children's\n // changes, which is exactly the enclosure answer the owner rule exists to\n // refuse. Whether a slot is distinguishable from an ordinary hand-off at this\n // rung has not been read — `Slot` is a component like any other from here.\n const author = node.provenance?.createdBy;\n if (author !== undefined && author !== stack.at(-1)) {\n // A slotted region received no props of its own: it is markup, handed over\n // whole. Absent rather than the container's digest, which would claim the\n // author was called with inputs it never saw.\n stack.push(author);\n placedBy.push(author);\n props.push(undefined);\n }\n\n return { stack, placedBy, props };\n}\n\n/** How many rungs two stacks agree on, from the outside in. */\nfunction sharedRungs(a: readonly string[], b: readonly string[]): number {\n const limit = Math.min(a.length, b.length);\n let shared = 0;\n while (shared < limit && a[shared] === b[shared]) shared += 1;\n return shared;\n}\n\nexport interface Boundary {\n readonly node: SemanticNode;\n readonly component: string;\n /** The enclosing boundary's component, absent on the subject root. */\n readonly within?: string;\n /** Boundaries between this one and the subject root. The root is `0`. */\n readonly depth: number;\n /**\n * This component's rung in the node's ownership stack.\n *\n * Distinct from `depth`, which counts enclosing boundaries. They diverge\n * wherever a subject root has no provenance: the `(unattributed)` container is\n * a boundary at depth 0 holding a stack of one, and the application inside it\n * starts again at rung 0 while sitting at depth 1.\n */\n readonly rung: number;\n /**\n * Digest of the props this boundary received, from its own `OwnerFrame`.\n *\n * Read at this rung rather than from `owners[0]`, which is the innermost\n * component and belongs to whichever boundary is deepest at this node — the\n * distinction only exists because several boundaries can share a root.\n */\n readonly props?: Digest;\n /** The component whose JSX placed this one. Absent on a production build. */\n readonly placedBy?: string;\n}\n\n/** Whether a node's own content belongs to this boundary rather than one below. */\nexport function holds(boundary: Boundary, node: SemanticNode): boolean {\n const { stack } = ownershipOf(node);\n return stack.length === boundary.rung + 1 && stack[boundary.rung] === boundary.component;\n}\n\n/**\n * Every boundary root in the subject, in document order.\n *\n * A boundary opens at each rung a node's ownership stack adds to its parent's,\n * plus the subject root. Several can open at one node — a page assembly, the\n * card it returns and the stack inside that card all begin at the same element —\n * which is what makes a component that renders only components visible.\n *\n * Collected in a separate pre-order pass rather than discovered during hashing,\n * so that instance order is document order exactly. Discovering them while\n * walking would order them by boundary depth instead, and \"the second instance\"\n * would mean something different in a nested tree.\n */\nexport function boundaries(root: SemanticNode): readonly Boundary[] {\n const found: Boundary[] = [];\n\n const visit = (\n node: SemanticNode,\n parent: readonly string[] | null,\n enclosing: string | null,\n depth: number,\n ): void => {\n const { stack, placedBy, props } = ownershipOf(node);\n const opened = parent === null ? 0 : sharedRungs(parent, stack);\n\n let within = enclosing;\n let below = depth;\n for (let rung = opened; rung < stack.length; rung += 1) {\n const digest = props[rung];\n const placer = placedBy[rung];\n found.push({\n node,\n component: stack[rung]!,\n ...(within === null ? {} : { within }),\n depth: below,\n rung,\n ...(digest === undefined ? {} : { props: digest }),\n ...(placer === undefined ? {} : { placedBy: placer }),\n });\n within = stack[rung]!;\n below += 1;\n }\n\n for (const child of node.children) visit(child, stack, within, below);\n };\n\n visit(root, null, null, 0);\n return found;\n}\n\nexport interface Shape {\n readonly structure: CanonicalValue;\n readonly semantics: CanonicalValue;\n readonly text: CanonicalValue;\n readonly style: CanonicalValue;\n readonly geometry: CanonicalValue;\n\n /**\n * How the framework holds this boundary, per node that reports it.\n *\n * Its own band because it answers its own question. `structure` through\n * `geometry` all read what the renderer produced; this reads what the\n * component *is* — its hook shape, its wrappers, the contexts it subscribes\n * to, the keys it is reconciled under. Two components can agree on all five\n * content bands and disagree here, and when they do, they behave differently\n * under every change that follows.\n */\n readonly wiring: CanonicalValue;\n\n /**\n * The box this boundary's root node occupies. Not part of any digest.\n *\n * Beside the digests rather than inside `geometry`, which is the whole subtree:\n * this is the only thing on the record that can say a control grew eight pixels\n * rather than merely stopped being what it was. `null` where nothing was laid out.\n */\n readonly box: Rect | null;\n\n /** Child boundaries encountered, in document order. Not part of any digest. */\n readonly renders: readonly string[];\n /** Nodes this boundary owns, counting its own root. Not part of any digest. */\n readonly nodes: number;\n /**\n * Custom properties this boundary's own nodes resolved through, sorted.\n *\n * Not part of any digest — the *values* are already inside `style`, and the\n * names are carried beside it so a token that moved can be joined to the\n * components that read it. Off the boundary rather than off the subject on\n * purpose: every subject on a themed page resolves through every token in the\n * theme, so a subject-level list names them all and explains nothing.\n */\n readonly tokens: readonly string[];\n}\n\n/**\n * How a value that may carry a structural alias is rewritten before hashing.\n *\n * `undefined` for the per-name hashes, which have always hashed the alias the\n * normalizer assigned and must keep doing so byte for byte. Supplied by the\n * per-instance hashes, which need a boundary-local alias space — see\n * {@link ./instances.js}, where the argument for it lives.\n */\nexport interface Rename {\n readonly attribute: (name: string, value: string) => string;\n readonly style: (value: string) => string;\n readonly alias: (alias: string) => string;\n}\n\n/**\n * One boundary's content, stopping at nested boundaries.\n *\n * Where a child belongs to another component, this records a placeholder rather\n * than descending. That is the whole design: a component's hash moves when *its\n * own* code changes, and what a child renders internally is not that. Hashing\n * whole subtrees instead would move every ancestor on any leaf edit, and the\n * page root would change on every commit.\n *\n * **The placeholder names the child only when this component placed it.** A\n * component that wrote `<Stack>` owns that choice, and swapping it for a `<Card>`\n * is its own change; a container that was handed a `<Stack>` chose nothing, and\n * naming it would make the container's digest a function of its callers. So a\n * slot is anonymous, and `Card` renders the same bytes on every page that uses\n * it, whatever it was given. The cost is stated in ADR-0035: the *number* of\n * slotted children still reaches the digest, so a caller passing three where it\n * passed two moves the container.\n *\n * Falls back to naming every child where `_debugOwner` is absent, which is the\n * enclosure answer this replaced.\n *\n * Paths are not hashed. A path is an address that shifts when an unrelated\n * sibling is inserted, so hashing one reports a change nobody made.\n */\nexport function shapeOf(boundary: Boundary, layout: boolean, rename?: Rename): Shape {\n const { node, component, rung } = boundary;\n const style: CanonicalValue[] = [];\n const geometry: CanonicalValue[] = [];\n const semantics: CanonicalValue[] = [];\n const text: CanonicalValue[] = [];\n const wiring: CanonicalValue[] = [];\n const renders: string[] = [];\n const tokens = new Set<string>();\n let nodes = 0;\n\n const walk = (current: SemanticNode): CanonicalValue => {\n nodes += 1;\n for (const token of Object.keys(current.tokens ?? {})) tokens.add(token);\n const declared: Record<string, string> = {};\n const measured: Record<string, string> = {};\n\n for (const [property, value] of Object.entries(current.style)) {\n const resolved = rename === undefined ? value : rename.style(value);\n if (layout && LAYOUT_OUTPUT.has(property)) measured[property] = resolved;\n else declared[property] = resolved;\n }\n\n style.push({ style: declared, tokens: renamedTokens(current, rename) });\n // An entry per node, always. `null` for a node with no rect keeps position in\n // the list meaningful: dropping the entry would let two different trees agree\n // by coincidence.\n geometry.push({\n rect: current.rect ? { ...current.rect } : null,\n // Beside the rect, not instead of it. A computed `max-height` that changed\n // while the box did not is still a fact about this element's geometry, and\n // dropping it would make the two digests disagree about what a box is.\n ...(Object.keys(measured).length > 0 ? { measured } : {}),\n });\n\n // One entry per node in each list, always — `null` rather than omitted, for\n // the reason the geometry list gives: dropping an entry lets two different\n // trees agree by coincidence, and here it would let a heading losing its\n // name look like a heading that never had one.\n semantics.push({\n role: current.role ?? null,\n name: current.name ?? null,\n state: (current.state ?? null) as CanonicalValue,\n });\n text.push(current.text ?? null);\n // `null` for a node that reports none, for the reason every list here gives:\n // an omitted entry lets two different trees agree by coincidence. Most nodes\n // report `null` — wiring attaches to a component's root node and not to the\n // elements beneath it (see `wiringOf`), so a boundary's band is a short\n // signal in a long run of nulls, and the nulls are what make its position\n // mean anything.\n wiring.push((current.wiring ?? null) as CanonicalValue);\n\n return {\n tag: current.tag,\n alias: renamedAlias(current, rename),\n portalled: current.portalled,\n attributes: renamedAttributes(current, rename),\n children: current.children.map((child) => entry(child)),\n };\n };\n\n /**\n * Every node inside one boundary carries the same stack, by construction.\n *\n * So the comparison a child is measured against is fixed for the whole walk,\n * and a zero-node boundary — a component that renders only components, whose\n * root node already belongs to the boundary below it — measures against the\n * same thing.\n */\n const home = ownershipOf(node).stack.slice(0, rung + 1);\n\n /**\n * A node's contribution: its own content, or a stand-in for what is beneath.\n *\n * The rung a child leaves at is where its stack stops agreeing with `home` —\n * normally the next rung down, and shallower wherever a subject root carries\n * no provenance and the application starts again inside it.\n */\n const entry = (child: SemanticNode): CanonicalValue => {\n if (holds(boundary, child)) return walk(child);\n\n const own = ownershipOf(child);\n const left = sharedRungs(home, own.stack);\n const beneath = own.stack[left] ?? own.stack.at(-1) ?? UNATTRIBUTED;\n // Kept whichever way the digest goes: the edge is a true fact about the\n // document, and the graph wants it even where the hash must not carry it.\n renders.push(beneath);\n\n const placer = own.placedBy[left];\n return placer === undefined || placer === component ? { boundary: beneath } : { slot: null };\n };\n\n const structure = entry(node);\n return {\n structure,\n semantics,\n text,\n style,\n geometry,\n wiring,\n box: node.rect ?? null,\n renders,\n nodes,\n tokens: [...tokens].sort(),\n };\n}\n\nfunction renamedAlias(node: SemanticNode, rename?: Rename): string | undefined {\n if (rename === undefined || node.alias === undefined) return node.alias;\n return rename.alias(node.alias);\n}\n\nfunction renamedAttributes(\n node: SemanticNode,\n rename?: Rename,\n): Readonly<Record<string, string>> {\n if (rename === undefined) return node.attributes;\n\n const renamed: Record<string, string> = {};\n for (const [name, value] of Object.entries(node.attributes)) {\n renamed[name] = rename.attribute(name, value);\n }\n return renamed;\n}\n\nfunction renamedTokens(\n node: SemanticNode,\n rename?: Rename,\n): Readonly<Record<string, string>> | undefined {\n if (rename === undefined || node.tokens === undefined) return node.tokens;\n\n const renamed: Record<string, string> = {};\n for (const [name, value] of Object.entries(node.tokens)) {\n renamed[name] = rename.style(value);\n }\n return renamed;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"boundary.js","sourceRoot":"","sources":["../../src/attribute/boundary.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CanonicalValue } from '../format/canonical.js';\nimport type { Rect } from '../format/capture.js';\nimport type { Digest } from '../format/hash.js';\nimport type { SemanticNode } from '../format/snapshot.js';\nimport { declares } from '../compare/cascade.js';\nimport { renamedAlias, renamedAttributes, renamedTokens, type Rename } from './rename.js';\n\nexport type { Rename } from './rename.js';\n\n/**\n * Where one component's nodes stop and the next component's begin.\n *\n * Two relations run upward out of a rendered node and they are not the same\n * relation. **Parent** is what is above it — React's `return` chain, the boxes\n * it ended up inside. **Owner** is what rendered it — React's `_debugOwner`, the\n * code that wrote the element. A layout wrapper is the parent of everything\n * handed to it and the owner of none of it.\n *\n * Both are needed here, for different halves of the same answer:\n *\n * - **Parent decides nesting.** A boundary owns a contiguous region of the\n * document, and what encloses that region is a fact about the tree. Reading\n * the owner for this would produce a boundary set that is not a partition.\n * - **Owner decides membership and naming.** A component's own content is what\n * *it* wrote. Content it was handed is a hole in its output — present, sized,\n * positioned by it, and authored somewhere else.\n *\n * Reading the parent for both — which is what a walk over `owners[0]` does — has\n * two consequences, and they are the two shapes a design system is made of:\n *\n * - **A component that renders only components disappears.** It authors no host\n * node, so it is never any node's nearest enclosing composite, so it is a\n * boundary nowhere. That is every variant wrapper (`DangerButton` returning a\n * `Button`) and every page-level assembly. The information is not missing —\n * the composite is in the chain, one rung up — it is discarded by reading only\n * the head of it. {@link boundaries} enters every rung the chain crosses.\n * - **A container absorbs its caller's content.** `Card` renders one `div` and\n * whatever it was given; hashed by enclosure, its digest moves whenever a\n * caller passes something else, while its props digest — which excludes\n * `children` — says its inputs held. {@link shapeOf} names a child boundary\n * only when this component placed it, and leaves an anonymous hole otherwise.\n *\n * The owner half is a development-build artefact: React populates `_debugOwner`\n * from `element._owner`, and a production bundle does not. Absent, both rules\n * degrade to naming everything, which is the enclosure answer — coarser, never\n * wrong in a new direction.\n *\n * Nothing here is exported from the package. The callers are siblings.\n */\n\n/** A node whose provenance chain broke. Not a filler category; see `RootKind`. */\nexport const UNATTRIBUTED = '(unattributed)';\n\n/**\n * Properties whose *computed* value is layout output rather than authored input.\n *\n * The distinction this list exists for is not stylistic. Under a profile with a\n * layout engine the snapshot carries the engine's resolved values, so a block\n * element's computed `height` is whatever its contents made it — and a button\n * two levels down growing by six pixels moves the computed height of every\n * ancestor. Hashed as *style*, that reports every enclosing component as having\n * changed, which is precisely the \"area ranks the displaced above the displacer\"\n * failure the cause hashes exist to fix, arriving through a different door.\n *\n * Measured on `cases/storybook-case`: one padding edit inside `Button` made\n * `Tokens`, `Stack`, `Card` and the unattributed root all report a moved style\n * hash, so *every* component in every affected story was named a cause.\n *\n * They are folded into `geometry` instead, where \"this component's box is a\n * different size\" already belongs and where it correctly does not make a cause.\n * Under a profile *without* layout there are no computed values, so these are\n * authored declarations like any other and stay in `style` — that tier has no\n * `geometry` digest to move them to, and a declared `width: 100px` really is the\n * component's own content.\n */\nexport const LAYOUT_OUTPUT: ReadonlySet<string> = new Set([\n 'width',\n 'height',\n 'min-width',\n 'min-height',\n 'max-width',\n 'max-height',\n // The one nobody guesses, and the one that was actually doing the damage.\n // `transform-origin` computes to half the border box — `512px 41px` — so it\n // moves whenever the box does, on every element, including ones that declare\n // no transform at all. Removing `width` and `height` changed nothing on the\n // case measurement; removing this is what made a padding edit inside `Button`\n // stop naming `Tokens`, `Stack`, `Card` and the root as causes.\n 'transform-origin',\n // Chromium returns *used* track sizes here — `\"70.39px 953.61px\"` — never the\n // author's `auto 1fr`, so nothing declared survives to be lost. That is what\n // makes these two safe on a list, and it is exactly what is not true of the\n // properties named below.\n 'grid-template-columns',\n 'grid-template-rows',\n]);\n\n/**\n * Five more shapes leak, and a longer list is the wrong fix. Measured.\n *\n * Editing only a descendant's padding in a real Chromium moves the computed\n * value of an ancestor's `padding-*` (percentage padding in a shrink-to-fit\n * box), `margin-*` (an `auto` margin centring a `fit-content` block),\n * `top`/`right`/`bottom`/`left` (an absolutely positioned element anchored to an\n * edge that moved) and `transform` (a `translate(-50%,-50%)` matrix, the exact\n * sibling of the `transform-origin` case above). Each one makes an enclosing\n * component a false cause.\n *\n * Adding them here was tried and is worse than the disease: with `padding-*` on\n * the list, `causes.test.ts`'s headline case — a padding edit *inside* `Button`\n * — stops naming `Button` at all. For these properties the computed value **is**\n * the authored value in the ordinary case, and a property name cannot tell the\n * two apart. Only a *value* can, and the only place that knows is `resolveStyle`\n * in `rules/normalize/cascade.ts`, which sees both the cascade's winner and the\n * engine's override and currently keeps no record of which superseded which.\n *\n * So the limit is stated rather than half-fixed: a component whose own\n * declarations use percentage padding, auto margins, edge-anchored absolute\n * positioning or a percentage translate can be named a cause by a change that\n * was not its own. The failure is a *false* cause, never a missed one, which is\n * the direction that costs a reader attention rather than a regression.\n */\n\n/**\n * Every component holding a node, outermost first, and who placed each one.\n *\n * The whole chain, not its head. `TodoFooter` rendering nothing but a `Stack`\n * appears here at the rung below `Stack`, which is the only place it appears at\n * all — it authors no element, so no node has it as a nearest enclosing\n * composite, and a walk that reads `owners[0]` cannot see it.\n *\n * `placedBy[i]` is who wrote the element for `stack[i]`: `OwnerFrame.createdBy`,\n * which is the component that decided a `<Stack>` belongs at this point, as\n * distinct from `Stack` itself. Absent per rung on a production build.\n *\n * A host node whose author is not the innermost component is content that was\n * handed in: it sits inside the container's box and belongs to whoever wrote it,\n * so the author is pushed as a further rung. That keeps the boundary set a\n * partition of the document — the slotted region is nested inside the container\n * rather than lifted out of it — while attributing it to the code responsible.\n */\ninterface Ownership {\n readonly stack: readonly string[];\n readonly placedBy: readonly (string | undefined)[];\n readonly props: readonly (Digest | undefined)[];\n}\n\nfunction ownershipOf(node: SemanticNode): Ownership {\n const owners = node.provenance?.owners;\n if (owners === undefined || owners.length === 0) {\n return { stack: [UNATTRIBUTED], placedBy: [undefined], props: [undefined] };\n }\n\n const stack: string[] = [];\n const placedBy: (string | undefined)[] = [];\n const props: (Digest | undefined)[] = [];\n for (let index = owners.length - 1; index >= 0; index -= 1) {\n const frame = owners[index]!;\n stack.push(frame.name);\n placedBy.push(frame.createdBy);\n props.push(frame.propsDigest);\n }\n\n // FIXME: what should this name when the author is a slot?\n //\n // Radix `Slot` under `asChild` merges the caller's className onto the child's\n // own host node, so `<Button asChild><Link/></Button>` renders one `<a>` whose\n // author is next/link's `LinkComponent` and whose every styling decision came\n // from `Button`. The owner rule then reports a name out of `node_modules` for a\n // region an edit to `button.tsx` moved, and both halves are individually\n // correct: `Button` really did author no host node, and `LinkComponent` really\n // did write the one that moved.\n //\n // Open, because the naive fix is worse. Preferring the nearest caller whenever\n // the author is unfamiliar would credit every container with its children's\n // changes, which is exactly the enclosure answer the owner rule exists to\n // refuse. Whether a slot is distinguishable from an ordinary hand-off at this\n // rung has not been read — `Slot` is a component like any other from here.\n const author = node.provenance?.createdBy;\n if (author !== undefined && author !== stack.at(-1)) {\n // A slotted region received no props of its own: it is markup, handed over\n // whole. Absent rather than the container's digest, which would claim the\n // author was called with inputs it never saw.\n stack.push(author);\n placedBy.push(author);\n props.push(undefined);\n }\n\n return { stack, placedBy, props };\n}\n\n/** How many rungs two stacks agree on, from the outside in. */\nfunction sharedRungs(a: readonly string[], b: readonly string[]): number {\n const limit = Math.min(a.length, b.length);\n let shared = 0;\n while (shared < limit && a[shared] === b[shared]) shared += 1;\n return shared;\n}\n\nexport interface Boundary {\n readonly node: SemanticNode;\n readonly component: string;\n /** The enclosing boundary's component, absent on the subject root. */\n readonly within?: string;\n /** Boundaries between this one and the subject root. The root is `0`. */\n readonly depth: number;\n /**\n * This component's rung in the node's ownership stack.\n *\n * Distinct from `depth`, which counts enclosing boundaries. They diverge\n * wherever a subject root has no provenance: the `(unattributed)` container is\n * a boundary at depth 0 holding a stack of one, and the application inside it\n * starts again at rung 0 while sitting at depth 1.\n */\n readonly rung: number;\n /**\n * Digest of the props this boundary received, from its own `OwnerFrame`.\n *\n * Read at this rung rather than from `owners[0]`, which is the innermost\n * component and belongs to whichever boundary is deepest at this node — the\n * distinction only exists because several boundaries can share a root.\n */\n readonly props?: Digest;\n /** The component whose JSX placed this one. Absent on a production build. */\n readonly placedBy?: string;\n}\n\n/** Whether a node's own content belongs to this boundary rather than one below. */\nexport function holds(boundary: Boundary, node: SemanticNode): boolean {\n const { stack } = ownershipOf(node);\n return stack.length === boundary.rung + 1 && stack[boundary.rung] === boundary.component;\n}\n\n/**\n * Every boundary root in the subject, in document order.\n *\n * A boundary opens at each rung a node's ownership stack adds to its parent's,\n * plus the subject root. Several can open at one node — a page assembly, the\n * card it returns and the stack inside that card all begin at the same element —\n * which is what makes a component that renders only components visible.\n *\n * Collected in a separate pre-order pass rather than discovered during hashing,\n * so that instance order is document order exactly. Discovering them while\n * walking would order them by boundary depth instead, and \"the second instance\"\n * would mean something different in a nested tree.\n */\nexport function boundaries(root: SemanticNode): readonly Boundary[] {\n const found: Boundary[] = [];\n\n const visit = (\n node: SemanticNode,\n parent: readonly string[] | null,\n enclosing: string | null,\n depth: number,\n ): void => {\n const { stack, placedBy, props } = ownershipOf(node);\n const opened = parent === null ? 0 : sharedRungs(parent, stack);\n\n let within = enclosing;\n let below = depth;\n for (let rung = opened; rung < stack.length; rung += 1) {\n const digest = props[rung];\n const placer = placedBy[rung];\n found.push({\n node,\n component: stack[rung]!,\n ...(within === null ? {} : { within }),\n depth: below,\n rung,\n ...(digest === undefined ? {} : { props: digest }),\n ...(placer === undefined ? {} : { placedBy: placer }),\n });\n within = stack[rung]!;\n below += 1;\n }\n\n for (const child of node.children) visit(child, stack, within, below);\n };\n\n visit(root, null, null, 0);\n return found;\n}\n\nexport interface Shape {\n readonly structure: CanonicalValue;\n readonly semantics: CanonicalValue;\n readonly text: CanonicalValue;\n readonly style: CanonicalValue;\n readonly geometry: CanonicalValue;\n\n /**\n * How the framework holds this boundary, per node that reports it.\n *\n * Its own band because it answers its own question. `structure` through\n * `geometry` all read what the renderer produced; this reads what the\n * component *is* — its hook shape, its wrappers, the contexts it subscribes\n * to, the keys it is reconciled under. Two components can agree on all five\n * content bands and disagree here, and when they do, they behave differently\n * under every change that follows.\n */\n readonly wiring: CanonicalValue;\n\n /**\n * The box this boundary's root node occupies. Not part of any digest.\n *\n * Beside the digests rather than inside `geometry`, which is the whole subtree:\n * this is the only thing on the record that can say a control grew eight pixels\n * rather than merely stopped being what it was. `null` where nothing was laid out.\n */\n readonly box: Rect | null;\n\n /** Child boundaries encountered, in document order. Not part of any digest. */\n readonly renders: readonly string[];\n /** Nodes this boundary owns, counting its own root. Not part of any digest. */\n readonly nodes: number;\n /**\n * Custom properties this boundary's own nodes resolved through, sorted.\n *\n * Not part of any digest — the *values* are already inside `style`, and the\n * names are carried beside it so a token that moved can be joined to the\n * components that read it. Off the boundary rather than off the subject on\n * purpose: every subject on a themed page resolves through every token in the\n * theme, so a subject-level list names them all and explains nothing.\n */\n readonly tokens: readonly string[];\n\n /**\n * Every distinct value a declaration set on this boundary's own nodes, per\n * property, and every value a custom property resolved to, under its `--name`.\n * Not part of any digest, and present only when the caller passed what was\n * declared.\n *\n * The `style` digest can say a component's declarations are not what they were\n * and never say which. These are the values it was built from, kept so two\n * revisions can be compared by property — `padding-left 8px → 14px` — on a\n * path that holds two sidecars and no documents.\n *\n * Declared, not computed: a node's `style` is the whole allowlist, about a\n * hundred properties sitting at their initial values, and recording those\n * multiplied a sidecar's size by six to say nothing anybody wrote. Layout\n * output is left out for the reason {@link LAYOUT_OUTPUT} gives: it moves with\n * every box, and `box` already says by how much.\n */\n readonly values?: Readonly<Record<string, readonly string[]>>;\n}\n\n/**\n * One boundary's content, stopping at nested boundaries.\n *\n * Where a child belongs to another component, this records a placeholder rather\n * than descending. That is the whole design: a component's hash moves when *its\n * own* code changes, and what a child renders internally is not that. Hashing\n * whole subtrees instead would move every ancestor on any leaf edit, and the\n * page root would change on every commit.\n *\n * **The placeholder names the child only when this component placed it.** A\n * component that wrote `<Stack>` owns that choice, and swapping it for a `<Card>`\n * is its own change; a container that was handed a `<Stack>` chose nothing, and\n * naming it would make the container's digest a function of its callers. So a\n * slot is anonymous, and `Card` renders the same bytes on every page that uses\n * it, whatever it was given. The cost is stated in ADR-0035: the *number* of\n * slotted children still reaches the digest, so a caller passing three where it\n * passed two moves the container.\n *\n * Falls back to naming every child where `_debugOwner` is absent, which is the\n * enclosure answer this replaced.\n *\n * Paths are not hashed. A path is an address that shifts when an unrelated\n * sibling is inserted, so hashing one reports a change nobody made.\n */\nexport function shapeOf(\n boundary: Boundary,\n layout: boolean,\n rename?: Rename,\n declared?: ReadonlySet<string>,\n): Shape {\n const { node, component, rung } = boundary;\n const style: CanonicalValue[] = [];\n const geometry: CanonicalValue[] = [];\n const semantics: CanonicalValue[] = [];\n const text: CanonicalValue[] = [];\n const wiring: CanonicalValue[] = [];\n const renders: string[] = [];\n const tokens = new Set<string>();\n const values = new Map<string, Set<string>>();\n const note = (property: string, value: string): void => {\n const seen = values.get(property) ?? new Set<string>();\n seen.add(value);\n values.set(property, seen);\n };\n let nodes = 0;\n\n const walk = (current: SemanticNode): CanonicalValue => {\n nodes += 1;\n for (const [token, value] of Object.entries(current.tokens ?? {})) {\n tokens.add(token);\n if (declared !== undefined) note(token, value);\n }\n const own: Record<string, string> = {};\n const measured: Record<string, string> = {};\n\n for (const [property, value] of Object.entries(current.style)) {\n const resolved = rename === undefined ? value : rename.style(value);\n if (layout && LAYOUT_OUTPUT.has(property)) measured[property] = resolved;\n else {\n own[property] = resolved;\n if (declared !== undefined && declares(declared, current.path, property)) note(property, value);\n }\n }\n\n style.push({ style: own, tokens: renamedTokens(current, rename) });\n // An entry per node, always. `null` for a node with no rect keeps position in\n // the list meaningful: dropping the entry would let two different trees agree\n // by coincidence.\n geometry.push({\n rect: current.rect ? { ...current.rect } : null,\n // Beside the rect, not instead of it. A computed `max-height` that changed\n // while the box did not is still a fact about this element's geometry, and\n // dropping it would make the two digests disagree about what a box is.\n ...(Object.keys(measured).length > 0 ? { measured } : {}),\n });\n\n // One entry per node in each list, always — `null` rather than omitted, for\n // the reason the geometry list gives: dropping an entry lets two different\n // trees agree by coincidence, and here it would let a heading losing its\n // name look like a heading that never had one.\n semantics.push({\n role: current.role ?? null,\n name: current.name ?? null,\n state: (current.state ?? null) as CanonicalValue,\n });\n text.push(current.text ?? null);\n // `null` for a node that reports none, for the reason every list here gives:\n // an omitted entry lets two different trees agree by coincidence. Most nodes\n // report `null` — wiring attaches to a component's root node and not to the\n // elements beneath it (see `wiringOf`), so a boundary's band is a short\n // signal in a long run of nulls, and the nulls are what make its position\n // mean anything.\n wiring.push((current.wiring ?? null) as CanonicalValue);\n\n return {\n tag: current.tag,\n alias: renamedAlias(current, rename),\n portalled: current.portalled,\n attributes: renamedAttributes(current, rename),\n children: current.children.map((child) => entry(child)),\n };\n };\n\n /**\n * Every node inside one boundary carries the same stack, by construction.\n *\n * So the comparison a child is measured against is fixed for the whole walk,\n * and a zero-node boundary — a component that renders only components, whose\n * root node already belongs to the boundary below it — measures against the\n * same thing.\n */\n const home = ownershipOf(node).stack.slice(0, rung + 1);\n\n /**\n * A node's contribution: its own content, or a stand-in for what is beneath.\n *\n * The rung a child leaves at is where its stack stops agreeing with `home` —\n * normally the next rung down, and shallower wherever a subject root carries\n * no provenance and the application starts again inside it.\n */\n const entry = (child: SemanticNode): CanonicalValue => {\n if (holds(boundary, child)) return walk(child);\n\n const own = ownershipOf(child);\n const left = sharedRungs(home, own.stack);\n const beneath = own.stack[left] ?? own.stack.at(-1) ?? UNATTRIBUTED;\n // Kept whichever way the digest goes: the edge is a true fact about the\n // document, and the graph wants it even where the hash must not carry it.\n renders.push(beneath);\n\n const placer = own.placedBy[left];\n return placer === undefined || placer === component ? { boundary: beneath } : { slot: null };\n };\n\n const structure = entry(node);\n return {\n structure,\n semantics,\n text,\n style,\n geometry,\n wiring,\n box: node.rect ?? null,\n renders,\n nodes,\n tokens: [...tokens].sort(),\n ...(declared === undefined\n ? {}\n : { values: Object.fromEntries([...values].map(([property, seen]) => [property, [...seen]])) }),\n };\n}\n"]}
|
|
@@ -167,6 +167,27 @@ export interface ComponentBands {
|
|
|
167
167
|
readonly width: number;
|
|
168
168
|
readonly height: number;
|
|
169
169
|
};
|
|
170
|
+
/**
|
|
171
|
+
* Which declared values changed, by property, when both sides recorded them.
|
|
172
|
+
*
|
|
173
|
+
* `token` says a declaration under here moved; this names it. `from` holds the
|
|
174
|
+
* values only the baseline had and `to` the values only this run has, so a
|
|
175
|
+
* component rendered as primary and secondary that changed one background
|
|
176
|
+
* reports that background and not the other. An empty side is a property no
|
|
177
|
+
* node declared on that side, which was measured and is not the same as absent.
|
|
178
|
+
*
|
|
179
|
+
* Absent when either sidecar predates the record, and when the style digest
|
|
180
|
+
* moved but no property's set of values did — a value that moved from one
|
|
181
|
+
* node to another is a real change this cannot describe, and an empty list
|
|
182
|
+
* would say there was none. Present is never empty.
|
|
183
|
+
*/
|
|
184
|
+
readonly changed?: readonly ValueChange[];
|
|
185
|
+
}
|
|
186
|
+
/** One property whose set of declared values differs between two revisions. */
|
|
187
|
+
export interface ValueChange {
|
|
188
|
+
readonly property: string;
|
|
189
|
+
readonly from: readonly string[];
|
|
190
|
+
readonly to: readonly string[];
|
|
170
191
|
}
|
|
171
192
|
/**
|
|
172
193
|
* Every component whose hashes differ, with the bands it differs in.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BANDS } from '../compare/band.js';
|
|
2
|
+
import { declaredIn } from '../compare/cascade.js';
|
|
2
3
|
import { digestValue } from '../format/hash.js';
|
|
3
4
|
import { boundaries, shapeOf, UNATTRIBUTED } from './boundary.js';
|
|
4
5
|
export { UNATTRIBUTED } from './boundary.js';
|
|
@@ -25,9 +26,10 @@ export { UNATTRIBUTED } from './boundary.js';
|
|
|
25
26
|
*/
|
|
26
27
|
export function hashComponents(snapshot) {
|
|
27
28
|
const layout = snapshot.profile.layout;
|
|
29
|
+
const declared = declaredIn(snapshot);
|
|
28
30
|
const accumulated = new Map();
|
|
29
31
|
for (const boundary of boundaries(snapshot.root)) {
|
|
30
|
-
const shape = shapeOf(boundary, layout);
|
|
32
|
+
const shape = shapeOf(boundary, layout, undefined, declared);
|
|
31
33
|
const entry = accumulated.get(boundary.component) ?? {
|
|
32
34
|
structure: [],
|
|
33
35
|
semantics: [],
|
|
@@ -35,6 +37,7 @@ export function hashComponents(snapshot) {
|
|
|
35
37
|
style: [],
|
|
36
38
|
geometry: [],
|
|
37
39
|
boxes: [],
|
|
40
|
+
values: new Map(),
|
|
38
41
|
};
|
|
39
42
|
entry.structure.push(shape.structure);
|
|
40
43
|
entry.semantics.push(shape.semantics);
|
|
@@ -42,6 +45,12 @@ export function hashComponents(snapshot) {
|
|
|
42
45
|
entry.style.push(shape.style);
|
|
43
46
|
entry.geometry.push(shape.geometry);
|
|
44
47
|
entry.boxes.push(shape.box);
|
|
48
|
+
for (const [property, values] of Object.entries(shape.values ?? {})) {
|
|
49
|
+
const seen = entry.values.get(property) ?? new Set();
|
|
50
|
+
for (const value of values)
|
|
51
|
+
seen.add(value);
|
|
52
|
+
entry.values.set(property, seen);
|
|
53
|
+
}
|
|
45
54
|
accumulated.set(boundary.component, entry);
|
|
46
55
|
}
|
|
47
56
|
return [...accumulated.entries()]
|
|
@@ -57,13 +66,25 @@ export function hashComponents(snapshot) {
|
|
|
57
66
|
// nulls is a measurement nobody took written as one they did. Absent
|
|
58
67
|
// instead, which is the same rule `geometry` above obeys.
|
|
59
68
|
...(layout ? { boxes: entry.boxes } : {}),
|
|
69
|
+
values: sortedValues(entry.values),
|
|
60
70
|
}))
|
|
61
71
|
// Code-unit order, not `localeCompare`. The doc above promises byte-identical
|
|
62
72
|
// output for an unchanged subject, and a locale-aware comparison makes that a
|
|
63
73
|
// promise about the machine's `LANG` — which was tolerable while these were
|
|
64
74
|
// internal and is not now that they are written into a sidecar, committed
|
|
65
75
|
// beside a baseline, and read back on someone else's runner.
|
|
66
|
-
.sort((a, b) => (a.component
|
|
76
|
+
.sort((a, b) => byCodeUnit(a.component, b.component));
|
|
77
|
+
}
|
|
78
|
+
/** Properties and their values in code-unit order, so the sidecar is byte-stable. */
|
|
79
|
+
function sortedValues(values) {
|
|
80
|
+
const sorted = {};
|
|
81
|
+
for (const property of [...values.keys()].sort(byCodeUnit)) {
|
|
82
|
+
sorted[property] = [...values.get(property)].sort(byCodeUnit);
|
|
83
|
+
}
|
|
84
|
+
return sorted;
|
|
85
|
+
}
|
|
86
|
+
function byCodeUnit(a, b) {
|
|
87
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
67
88
|
}
|
|
68
89
|
/**
|
|
69
90
|
* Which components *caused* a change, given both revisions' hashes.
|
|
@@ -257,11 +278,13 @@ export function movedBandsBetween(before, after) {
|
|
|
257
278
|
if (bands.length === 0)
|
|
258
279
|
continue;
|
|
259
280
|
const grew = grewBetween(was.boxes, entry.boxes);
|
|
281
|
+
const changed = was.style === entry.style ? undefined : changedBetween(was.values, entry.values);
|
|
260
282
|
moved.push({
|
|
261
283
|
component: entry.component,
|
|
262
284
|
bands,
|
|
263
285
|
cause: ownContentMoved(was, entry),
|
|
264
286
|
...(grew === undefined ? {} : { grew }),
|
|
287
|
+
...(changed === undefined ? {} : { changed }),
|
|
265
288
|
});
|
|
266
289
|
}
|
|
267
290
|
for (const entry of before) {
|
|
@@ -271,7 +294,29 @@ export function movedBandsBetween(before, after) {
|
|
|
271
294
|
continue;
|
|
272
295
|
moved.push({ component: entry.component, bands: ['geometry'], cause: true, presence: 'removed' });
|
|
273
296
|
}
|
|
274
|
-
return moved.sort((left, right) => left.component
|
|
297
|
+
return moved.sort((left, right) => byCodeUnit(left.component, right.component));
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Every property whose values differ, or nothing when that cannot be said.
|
|
301
|
+
*
|
|
302
|
+
* Read only when the `style` digest moved, which is the digest these values
|
|
303
|
+
* were declared into: a component whose declarations hashed the same has
|
|
304
|
+
* nothing here to name, whatever else about it changed.
|
|
305
|
+
*/
|
|
306
|
+
function changedBetween(before, after) {
|
|
307
|
+
if (before === undefined || after === undefined)
|
|
308
|
+
return undefined;
|
|
309
|
+
const changed = [];
|
|
310
|
+
const properties = new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
311
|
+
for (const property of [...properties].sort(byCodeUnit)) {
|
|
312
|
+
const was = before[property] ?? [];
|
|
313
|
+
const now = after[property] ?? [];
|
|
314
|
+
const from = was.filter((value) => !now.includes(value));
|
|
315
|
+
const to = now.filter((value) => !was.includes(value));
|
|
316
|
+
if (from.length > 0 || to.length > 0)
|
|
317
|
+
changed.push({ property, from, to });
|
|
318
|
+
}
|
|
319
|
+
return changed.length === 0 ? undefined : changed;
|
|
275
320
|
}
|
|
276
321
|
/**
|
|
277
322
|
* One size delta both sides agree on, or nothing.
|