@weatherboard/annotation 0.2.2 → 0.3.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/ADOPTING.md +52 -0
- package/README.md +9 -1
- package/dist/anchor.cjs +2 -0
- package/dist/anchor.cjs.map +7 -0
- package/dist/anchor.d.cts +295 -0
- package/dist/anchor.d.mts +295 -0
- package/dist/anchor.d.ts +295 -0
- package/dist/anchor.mjs +2 -0
- package/dist/anchor.mjs.map +7 -0
- package/dist/arming.d.cts +30 -2
- package/dist/arming.d.mts +30 -2
- package/dist/arming.d.ts +30 -2
- package/dist/host.cjs +1 -1
- package/dist/host.cjs.map +1 -1
- package/dist/host.mjs.map +1 -1
- package/dist/index.cjs +4 -4
- package/dist/index.cjs.map +3 -3
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +4 -4
- package/dist/index.mjs.map +3 -3
- package/dist/markers.cjs +57 -0
- package/dist/markers.cjs.map +7 -0
- package/dist/markers.d.cts +246 -0
- package/dist/markers.d.mts +246 -0
- package/dist/markers.d.ts +246 -0
- package/dist/markers.mjs +57 -0
- package/dist/markers.mjs.map +7 -0
- package/dist/protocol.cjs +1 -1
- package/dist/protocol.cjs.map +3 -3
- package/dist/protocol.d.cts +29 -0
- package/dist/protocol.d.mts +29 -0
- package/dist/protocol.d.ts +29 -0
- package/dist/protocol.mjs +1 -1
- package/dist/protocol.mjs.map +3 -3
- package/dist/read.cjs +2 -0
- package/dist/read.cjs.map +7 -0
- package/dist/read.d.cts +149 -0
- package/dist/read.d.mts +149 -0
- package/dist/read.d.ts +149 -0
- package/dist/read.mjs +2 -0
- package/dist/read.mjs.map +7 -0
- package/dist/transport.d.cts +7 -1
- package/dist/transport.d.mts +7 -1
- package/dist/transport.d.ts +7 -1
- package/package.json +48 -5
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Putting a capture back on the element it came from (A-275).
|
|
3
|
+
*
|
|
4
|
+
* ===========================================================================
|
|
5
|
+
* WHY THIS IS A PACKAGE EXPORT AND NOT A MEASUREMENT HARNESS
|
|
6
|
+
* ===========================================================================
|
|
7
|
+
*
|
|
8
|
+
* The slice that produced this file was originally scoped as "measure whether
|
|
9
|
+
* anchoring can work", with rendering explicitly out of scope. That was wrong,
|
|
10
|
+
* and the reason is A-268's shape: resolving an anchor against a live DOM **is**
|
|
11
|
+
* the marker-placement code. Measuring with one implementation and shipping
|
|
12
|
+
* another later gives two implementations of one primitive, and two
|
|
13
|
+
* implementations of one primitive is a way for them to disagree — which is the
|
|
14
|
+
* principle `capture-token.ts` already names, and the defect A-268 already was.
|
|
15
|
+
*
|
|
16
|
+
* So `resolveAnchor` is the export the marker layer (A-277) imports, and the
|
|
17
|
+
* measurement in A-275 is that same function called on staging pages.
|
|
18
|
+
*
|
|
19
|
+
* **The measurement and the decision it produced are in
|
|
20
|
+
* `docs/decisions/a-275-anchor-placement.md`**, and `anchor-measure.mjs` runs it
|
|
21
|
+
* again. Read the decision before changing anything here.
|
|
22
|
+
*
|
|
23
|
+
* Every figure in this file is quoted from that document with its denominator,
|
|
24
|
+
* or is a property of the code rather than a measurement and says so. That rule
|
|
25
|
+
* exists because it was broken here: two comments claimed `MEASURED: 105 of 200`
|
|
26
|
+
* — a stale figure, from a SAMPLE the decision document explicitly retires, for a
|
|
27
|
+
* branch it was not even a statistic about. **A number labelled MEASURED that no
|
|
28
|
+
* checked-in artifact can reproduce is worse than no number**, which this
|
|
29
|
+
* package's own size-budget note already says about a different number.
|
|
30
|
+
*
|
|
31
|
+
* The headline, so it is not paraphrased loosely elsewhere: **139 of 546
|
|
32
|
+
* annotated elements on the staging home page resolve to exactly one element,
|
|
33
|
+
* and 0 of 546 resolve to the wrong one.** Every plausible improvement to the
|
|
34
|
+
* first figure is a way of trading the second one away.
|
|
35
|
+
*
|
|
36
|
+
* ===========================================================================
|
|
37
|
+
* WHY `source_file:source_line` IS NOT AN ANCHOR
|
|
38
|
+
* ===========================================================================
|
|
39
|
+
*
|
|
40
|
+
* A source ref identifies a **component**, not an **instance**. Six cards
|
|
41
|
+
* rendered from one `<PubCard/>` all carry the same `data-src`, and the file and
|
|
42
|
+
* line say nothing about which one a reviewer clicked.
|
|
43
|
+
*
|
|
44
|
+
* The first draft of this scheme fixed that with an ordinal — the element's
|
|
45
|
+
* index among the matches, in document order. A reviewer showed that this is
|
|
46
|
+
* **wrong rather than merely incomplete**, and the argument is worth keeping
|
|
47
|
+
* because it is not obvious:
|
|
48
|
+
*
|
|
49
|
+
* An ordinal is POSITIONAL. Delete the second card, and a capture anchored to
|
|
50
|
+
* ordinal 3 resolves to what used to be card 4. Exactly one match. A marker is
|
|
51
|
+
* drawn, on the wrong element, and NOTHING ANYWHERE HAS A COMPLAINT — a
|
|
52
|
+
* "cannot be confidently placed" rule never fires, because the failure IS a
|
|
53
|
+
* confident single match.
|
|
54
|
+
*
|
|
55
|
+
* A marker on the wrong element does not degrade this feature, it inverts it:
|
|
56
|
+
* the entire value of putting the conversation on the component is that
|
|
57
|
+
* *"this one, here"* stops being ambiguous.
|
|
58
|
+
*
|
|
59
|
+
* ===========================================================================
|
|
60
|
+
* SO: A DISCONFIRMING SIGNAL, NOT A UNIQUENESS TEST
|
|
61
|
+
* ===========================================================================
|
|
62
|
+
*
|
|
63
|
+
* Uniqueness is satisfied by every silent failure above, so uniqueness cannot be
|
|
64
|
+
* the test. What the anchor stores instead is the **whole source chain** —
|
|
65
|
+
* `TargetInfo.sourceChain`, every `data-src` from the clicked element up to the
|
|
66
|
+
* root, innermost first, which the capture has carried since A-33 and which the
|
|
67
|
+
* ingest has persisted since A-287 — and a candidate must match the chain, not
|
|
68
|
+
* just the leaf.
|
|
69
|
+
*
|
|
70
|
+
* A reordered sibling still matches (its ancestors did not change). An element
|
|
71
|
+
* that MOVED does not (its ancestors did). The chain is a claim the DOM can
|
|
72
|
+
* contradict, and that is the only kind of claim worth storing.
|
|
73
|
+
*
|
|
74
|
+
* It does not make repeated siblings distinguishable, and it is not supposed to:
|
|
75
|
+
* two identical cards in one list have identical chains, so an anchor to either
|
|
76
|
+
* resolves `"ambiguous"`. That is the SAFE failure — the marker is withheld and
|
|
77
|
+
* the count says so — and it is the one an ordinal would have converted into a
|
|
78
|
+
* confident wrong answer.
|
|
79
|
+
*
|
|
80
|
+
* ===========================================================================
|
|
81
|
+
* A TAGGED UNION FROM DAY ONE
|
|
82
|
+
* ===========================================================================
|
|
83
|
+
*
|
|
84
|
+
* Not speculative generality: two capture kinds that cannot carry a usable
|
|
85
|
+
* source ref ALREADY SHIP. `TargetInfo.domPath` is the documented fallback for a
|
|
86
|
+
* page whose elements have no refs, and region captures (A-179) exist, where
|
|
87
|
+
* `sourceChain` is the siblings a rectangle crossed rather than ancestors —
|
|
88
|
+
* a chain that contains nothing of itself and would be silently read as an
|
|
89
|
+
* ancestor chain by anything that assumed one shape.
|
|
90
|
+
*
|
|
91
|
+
* Retrofitting a union onto flat columns after a year of rows is the expensive
|
|
92
|
+
* version of this, and the read response (A-276) is additive-only forever, so
|
|
93
|
+
* `kind` is what lets an older SDK skip a marker it does not understand rather
|
|
94
|
+
* than misplace it.
|
|
95
|
+
*/
|
|
96
|
+
import { type ElementLike } from "./target.mjs";
|
|
97
|
+
import { type SourceRef, type TargetInfo } from "./protocol.mjs";
|
|
98
|
+
/**
|
|
99
|
+
* An element identified by the source location that rendered it, plus the chain
|
|
100
|
+
* of source locations above it.
|
|
101
|
+
*
|
|
102
|
+
* `chain` is innermost-first and INCLUDES the leaf, exactly as
|
|
103
|
+
* `collectSourceChain` produces it — one array, not a leaf plus a tail, because
|
|
104
|
+
* two representations of one list is a way for them to disagree about whether
|
|
105
|
+
* entry zero is the element or its parent.
|
|
106
|
+
*/
|
|
107
|
+
export interface SourceRefAnchor {
|
|
108
|
+
kind: "source-ref";
|
|
109
|
+
file: string;
|
|
110
|
+
line: number;
|
|
111
|
+
column?: number;
|
|
112
|
+
chain: SourceRef[];
|
|
113
|
+
/**
|
|
114
|
+
* The element's index among the leaf-matching candidates at capture time.
|
|
115
|
+
*
|
|
116
|
+
* **Recorded, and deliberately never used to select.** It is here because
|
|
117
|
+
* knowing "this was the 3rd of 6" is genuinely useful when a human is looking
|
|
118
|
+
* at an unresolved anchor and asking what happened — and it is *not* here as a
|
|
119
|
+
* tie-break, for the reason set out at length in this file's header. If you
|
|
120
|
+
* are about to reach for it to turn an `"ambiguous"` into an element, that is
|
|
121
|
+
* the bug this whole scheme was rewritten to avoid; `anchor.test.ts` has a
|
|
122
|
+
* test that fails if you do.
|
|
123
|
+
*/
|
|
124
|
+
ordinal?: number;
|
|
125
|
+
}
|
|
126
|
+
/** A CSS-ish path, for elements no build transform gave a ref. */
|
|
127
|
+
export interface DomPathAnchor {
|
|
128
|
+
kind: "dom-path";
|
|
129
|
+
path: string;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* "Something on THIS page."
|
|
133
|
+
*
|
|
134
|
+
* First-class rather than a fallback of last resort: it is the honest home for a
|
|
135
|
+
* region capture, for a canvas, for a PDF, and for any product that is not a JSX
|
|
136
|
+
* tree.
|
|
137
|
+
*
|
|
138
|
+
* ===========================================================================
|
|
139
|
+
* `path` IS NOT OPTIONAL DECORATION — IT IS WHAT MAKES "THIS PAGE" MEAN ANYTHING
|
|
140
|
+
* ===========================================================================
|
|
141
|
+
*
|
|
142
|
+
* The first version of this interface had no fields at all and `resolveAnchor`
|
|
143
|
+
* returned `doc.body` unconditionally, with a comment calling it *"always
|
|
144
|
+
* placeable"*. A reviewer pointed out that this reintroduces, through the route
|
|
145
|
+
* branch, exactly the failure the source-ref scheme was rewritten to prevent:
|
|
146
|
+
*
|
|
147
|
+
* A capture taken on `/guides/a` resolves CONFIDENTLY on `/guides/b`. One
|
|
148
|
+
* match, marker drawn, wrong page, counted as `placed`. No signal anywhere.
|
|
149
|
+
*
|
|
150
|
+
* That is not a rate to be estimated. `return doc.body` has no input but the
|
|
151
|
+
* document, so under it **every** route anchor placed on **every** page it was
|
|
152
|
+
* shown — 100%, by construction, and there is nothing to measure. The reason it
|
|
153
|
+
* matters is `payload.page.route`: it is the matched PATTERN rather than the
|
|
154
|
+
* URL, so "the page this came from" was never pinned in the first place. And
|
|
155
|
+
* `anchorFromTarget` sends every region capture down this branch, so it is the
|
|
156
|
+
* common case rather than the edge.
|
|
157
|
+
*
|
|
158
|
+
* The measured neighbour is the SOURCE-REF branch meeting the same problem:
|
|
159
|
+
* 414 anchors captured on one guide page resolve **identically** after a real
|
|
160
|
+
* navigation to another guide page — 161 placed, 253 ambiguous, 0 refused, the
|
|
161
|
+
* same three numbers on both — recorded with its method and denominator in
|
|
162
|
+
* `docs/decisions/a-275-anchor-placement.md` § *The limit this does not fix*.
|
|
163
|
+
* It is a different branch and a different number, cited here as evidence that
|
|
164
|
+
* the pattern-not-URL problem is real, not as a figure for this one.
|
|
165
|
+
*
|
|
166
|
+
* So a route anchor carries the RESOLVED path it was captured on, and
|
|
167
|
+
* `resolveAnchor` refuses when the document is somewhere else.
|
|
168
|
+
*
|
|
169
|
+
* ===========================================================================
|
|
170
|
+
* AND ABSENT MEANS "CANNOT PLACE", NEVER "PLACE ANYWAY"
|
|
171
|
+
* ===========================================================================
|
|
172
|
+
*
|
|
173
|
+
* `path` is optional because the field is newer than the wire, and the read-back
|
|
174
|
+
* plan's versioning rule for exactly this case is that a new anchor field is
|
|
175
|
+
* optional and **absent means cannot place** — the precedent `selection?:` set.
|
|
176
|
+
* A route anchor with no `path`, or a document that cannot say where it is,
|
|
177
|
+
* therefore resolves to `"none"`.
|
|
178
|
+
*
|
|
179
|
+
* That is a deliberate choice to under-place rather than mis-place, and it costs
|
|
180
|
+
* nothing today because no route anchor has ever been stored: markers have not
|
|
181
|
+
* shipped. It would be a real cost if this were retrofitted a year from now,
|
|
182
|
+
* which is the argument for doing it here.
|
|
183
|
+
*
|
|
184
|
+
* WHAT IT DOES NOT FIX: a SOURCE-REF anchor still resolves happily on a
|
|
185
|
+
* different `[id]` of the same route pattern — the markup is identical, so there
|
|
186
|
+
* is nothing for the chain to disagree with, and no amount of anchor design
|
|
187
|
+
* closes it. That one is the read endpoint's (A-276): scope the query on the
|
|
188
|
+
* resolved path, not on `route`. Stated here because an unstated limit gets read
|
|
189
|
+
* as a covered one.
|
|
190
|
+
*/
|
|
191
|
+
export interface RouteAnchor {
|
|
192
|
+
kind: "route";
|
|
193
|
+
/** `location.pathname` at capture time. Absent means the anchor cannot be placed. */
|
|
194
|
+
path?: string;
|
|
195
|
+
}
|
|
196
|
+
export type Anchor = SourceRefAnchor | DomPathAnchor | RouteAnchor;
|
|
197
|
+
/**
|
|
198
|
+
* Where a marker goes, or why it does not go anywhere.
|
|
199
|
+
*
|
|
200
|
+
* Three outcomes and not two, because `"none"` and `"ambiguous"` are different
|
|
201
|
+
* facts about the page and a caller counting them together loses the one that
|
|
202
|
+
* says the markup moved.
|
|
203
|
+
*/
|
|
204
|
+
export type AnchorResolution<E extends ElementLike = ElementLike> = E | "none" | "ambiguous";
|
|
205
|
+
/**
|
|
206
|
+
* The narrowest slice of `Document` this module reads. A real `Document`
|
|
207
|
+
* satisfies it, which `anchor.test.ts` asserts at compile time.
|
|
208
|
+
*
|
|
209
|
+
* `location` is here for the route branch above. It is optional because a parsed
|
|
210
|
+
* fixture has no location, and a document that cannot say where it is makes a
|
|
211
|
+
* route anchor unplaceable rather than placeable-by-default.
|
|
212
|
+
*/
|
|
213
|
+
export interface DocumentLike<E extends ElementLike = ElementLike> {
|
|
214
|
+
querySelectorAll(selectors: string): ArrayLike<E>;
|
|
215
|
+
body?: E | null;
|
|
216
|
+
location?: {
|
|
217
|
+
pathname: string;
|
|
218
|
+
} | null;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Resolve an anchor against a document.
|
|
222
|
+
*
|
|
223
|
+
* Pure: it reads the document and returns an element or a refusal, and it never
|
|
224
|
+
* mutates, measures layout, or draws anything. That is what lets the same
|
|
225
|
+
* function be a unit test over a checked-in HTML fixture, a number in a
|
|
226
|
+
* measurement run, and the marker layer's placement call.
|
|
227
|
+
*
|
|
228
|
+
* A `kind` this version does not recognise resolves to `"none"` — the read
|
|
229
|
+
* response is additive-only forever (A-276), so an SDK meeting a newer console
|
|
230
|
+
* must SKIP a marker it cannot place rather than guess at one.
|
|
231
|
+
*/
|
|
232
|
+
export declare function resolveAnchor<E extends ElementLike>(anchor: Anchor, doc: DocumentLike<E>): AnchorResolution<E>;
|
|
233
|
+
export interface AnchorFromTargetOptions {
|
|
234
|
+
/** Document order index among leaf-matching candidates, when the caller has a document. */
|
|
235
|
+
ordinal?: number;
|
|
236
|
+
/**
|
|
237
|
+
* `location.pathname` at capture time — the RESOLVED path, not the route
|
|
238
|
+
* pattern.
|
|
239
|
+
*
|
|
240
|
+
* Pass it. A route anchor built without it can never be placed, by design; see
|
|
241
|
+
* `RouteAnchor`. It is optional here rather than required because
|
|
242
|
+
* `anchorFromTarget` is also called for element captures, where it is unused.
|
|
243
|
+
*/
|
|
244
|
+
path?: string;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Build the anchor for a capture.
|
|
248
|
+
*
|
|
249
|
+
* The capture side of the same primitive, here rather than in `target.ts` so the
|
|
250
|
+
* union and the function that produces it cannot drift apart.
|
|
251
|
+
*
|
|
252
|
+
* **A region capture becomes a `route` anchor.** Its `sourceChain` is the
|
|
253
|
+
* siblings a rectangle crossed, which contain nothing of each other; handing
|
|
254
|
+
* that to `chainMatches` would compare a sibling list against an ancestor list
|
|
255
|
+
* and refuse — the right answer for the wrong reason, and one that would report
|
|
256
|
+
* every region capture as "the markup moved". Route-level is what a region
|
|
257
|
+
* capture actually means: something on this page, in that area.
|
|
258
|
+
*/
|
|
259
|
+
export declare function anchorFromTarget(target: Pick<TargetInfo, "selection" | "source" | "sourceChain" | "domPath">, options?: AnchorFromTargetOptions): Anchor;
|
|
260
|
+
/**
|
|
261
|
+
* The counts a caller reports, so that "0 markers" is never the same string as
|
|
262
|
+
* "working, nothing here" (rule 1 of the read-back plan).
|
|
263
|
+
*
|
|
264
|
+
* ===========================================================================
|
|
265
|
+
* `wrong` IS NOT IN HERE AND CANNOT BE, AND THAT IS A GAP RATHER THAN A DESIGN
|
|
266
|
+
* ===========================================================================
|
|
267
|
+
*
|
|
268
|
+
* Whether a resolved element is the RIGHT element is not knowable from the live
|
|
269
|
+
* page alone — it takes a before/after comparison, which is what A-275's
|
|
270
|
+
* mutation battery does and what no runtime caller can do.
|
|
271
|
+
*
|
|
272
|
+
* So `placed` means "resolved to exactly one element", NOT "resolved
|
|
273
|
+
* correctly", and there is one measured channel where those differ: a
|
|
274
|
+
* source-ref anchor captured on one `[id]` resolves on another `[id]` of the
|
|
275
|
+
* same route pattern, because the two pages are structurally identical.
|
|
276
|
+
*
|
|
277
|
+
* MEASURED at **161 of 414 anchors** placing on a different `[id]` of the same
|
|
278
|
+
* route pattern, with zero refusals — indistinguishable, count for count, from
|
|
279
|
+
* resolving against the page they were captured on. The method, the denominator
|
|
280
|
+
* and the pages are in `docs/decisions/a-275-anchor-placement.md` § *The limit
|
|
281
|
+
* this does not fix*, and `node anchor-measure.mjs --url … --compare-url …`
|
|
282
|
+
* re-runs it in one command. Nothing in this module can detect it; the
|
|
283
|
+
* fix is for the read endpoint to scope on the resolved path (A-276), so a
|
|
284
|
+
* caller must not read `placed` as a correctness figure until it does.
|
|
285
|
+
*
|
|
286
|
+
* The route branch closes its own half of that (`RouteAnchor.path`). This
|
|
287
|
+
* comment is the record that the source-ref half is still open.
|
|
288
|
+
*/
|
|
289
|
+
export interface AnchorTally {
|
|
290
|
+
total: number;
|
|
291
|
+
placed: number;
|
|
292
|
+
ambiguous: number;
|
|
293
|
+
unresolved: number;
|
|
294
|
+
}
|
|
295
|
+
export declare function tallyAnchors<E extends ElementLike>(anchors: readonly Anchor[], doc: DocumentLike<E>): AnchorTally;
|
package/dist/anchor.d.ts
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Putting a capture back on the element it came from (A-275).
|
|
3
|
+
*
|
|
4
|
+
* ===========================================================================
|
|
5
|
+
* WHY THIS IS A PACKAGE EXPORT AND NOT A MEASUREMENT HARNESS
|
|
6
|
+
* ===========================================================================
|
|
7
|
+
*
|
|
8
|
+
* The slice that produced this file was originally scoped as "measure whether
|
|
9
|
+
* anchoring can work", with rendering explicitly out of scope. That was wrong,
|
|
10
|
+
* and the reason is A-268's shape: resolving an anchor against a live DOM **is**
|
|
11
|
+
* the marker-placement code. Measuring with one implementation and shipping
|
|
12
|
+
* another later gives two implementations of one primitive, and two
|
|
13
|
+
* implementations of one primitive is a way for them to disagree — which is the
|
|
14
|
+
* principle `capture-token.ts` already names, and the defect A-268 already was.
|
|
15
|
+
*
|
|
16
|
+
* So `resolveAnchor` is the export the marker layer (A-277) imports, and the
|
|
17
|
+
* measurement in A-275 is that same function called on staging pages.
|
|
18
|
+
*
|
|
19
|
+
* **The measurement and the decision it produced are in
|
|
20
|
+
* `docs/decisions/a-275-anchor-placement.md`**, and `anchor-measure.mjs` runs it
|
|
21
|
+
* again. Read the decision before changing anything here.
|
|
22
|
+
*
|
|
23
|
+
* Every figure in this file is quoted from that document with its denominator,
|
|
24
|
+
* or is a property of the code rather than a measurement and says so. That rule
|
|
25
|
+
* exists because it was broken here: two comments claimed `MEASURED: 105 of 200`
|
|
26
|
+
* — a stale figure, from a SAMPLE the decision document explicitly retires, for a
|
|
27
|
+
* branch it was not even a statistic about. **A number labelled MEASURED that no
|
|
28
|
+
* checked-in artifact can reproduce is worse than no number**, which this
|
|
29
|
+
* package's own size-budget note already says about a different number.
|
|
30
|
+
*
|
|
31
|
+
* The headline, so it is not paraphrased loosely elsewhere: **139 of 546
|
|
32
|
+
* annotated elements on the staging home page resolve to exactly one element,
|
|
33
|
+
* and 0 of 546 resolve to the wrong one.** Every plausible improvement to the
|
|
34
|
+
* first figure is a way of trading the second one away.
|
|
35
|
+
*
|
|
36
|
+
* ===========================================================================
|
|
37
|
+
* WHY `source_file:source_line` IS NOT AN ANCHOR
|
|
38
|
+
* ===========================================================================
|
|
39
|
+
*
|
|
40
|
+
* A source ref identifies a **component**, not an **instance**. Six cards
|
|
41
|
+
* rendered from one `<PubCard/>` all carry the same `data-src`, and the file and
|
|
42
|
+
* line say nothing about which one a reviewer clicked.
|
|
43
|
+
*
|
|
44
|
+
* The first draft of this scheme fixed that with an ordinal — the element's
|
|
45
|
+
* index among the matches, in document order. A reviewer showed that this is
|
|
46
|
+
* **wrong rather than merely incomplete**, and the argument is worth keeping
|
|
47
|
+
* because it is not obvious:
|
|
48
|
+
*
|
|
49
|
+
* An ordinal is POSITIONAL. Delete the second card, and a capture anchored to
|
|
50
|
+
* ordinal 3 resolves to what used to be card 4. Exactly one match. A marker is
|
|
51
|
+
* drawn, on the wrong element, and NOTHING ANYWHERE HAS A COMPLAINT — a
|
|
52
|
+
* "cannot be confidently placed" rule never fires, because the failure IS a
|
|
53
|
+
* confident single match.
|
|
54
|
+
*
|
|
55
|
+
* A marker on the wrong element does not degrade this feature, it inverts it:
|
|
56
|
+
* the entire value of putting the conversation on the component is that
|
|
57
|
+
* *"this one, here"* stops being ambiguous.
|
|
58
|
+
*
|
|
59
|
+
* ===========================================================================
|
|
60
|
+
* SO: A DISCONFIRMING SIGNAL, NOT A UNIQUENESS TEST
|
|
61
|
+
* ===========================================================================
|
|
62
|
+
*
|
|
63
|
+
* Uniqueness is satisfied by every silent failure above, so uniqueness cannot be
|
|
64
|
+
* the test. What the anchor stores instead is the **whole source chain** —
|
|
65
|
+
* `TargetInfo.sourceChain`, every `data-src` from the clicked element up to the
|
|
66
|
+
* root, innermost first, which the capture has carried since A-33 and which the
|
|
67
|
+
* ingest has persisted since A-287 — and a candidate must match the chain, not
|
|
68
|
+
* just the leaf.
|
|
69
|
+
*
|
|
70
|
+
* A reordered sibling still matches (its ancestors did not change). An element
|
|
71
|
+
* that MOVED does not (its ancestors did). The chain is a claim the DOM can
|
|
72
|
+
* contradict, and that is the only kind of claim worth storing.
|
|
73
|
+
*
|
|
74
|
+
* It does not make repeated siblings distinguishable, and it is not supposed to:
|
|
75
|
+
* two identical cards in one list have identical chains, so an anchor to either
|
|
76
|
+
* resolves `"ambiguous"`. That is the SAFE failure — the marker is withheld and
|
|
77
|
+
* the count says so — and it is the one an ordinal would have converted into a
|
|
78
|
+
* confident wrong answer.
|
|
79
|
+
*
|
|
80
|
+
* ===========================================================================
|
|
81
|
+
* A TAGGED UNION FROM DAY ONE
|
|
82
|
+
* ===========================================================================
|
|
83
|
+
*
|
|
84
|
+
* Not speculative generality: two capture kinds that cannot carry a usable
|
|
85
|
+
* source ref ALREADY SHIP. `TargetInfo.domPath` is the documented fallback for a
|
|
86
|
+
* page whose elements have no refs, and region captures (A-179) exist, where
|
|
87
|
+
* `sourceChain` is the siblings a rectangle crossed rather than ancestors —
|
|
88
|
+
* a chain that contains nothing of itself and would be silently read as an
|
|
89
|
+
* ancestor chain by anything that assumed one shape.
|
|
90
|
+
*
|
|
91
|
+
* Retrofitting a union onto flat columns after a year of rows is the expensive
|
|
92
|
+
* version of this, and the read response (A-276) is additive-only forever, so
|
|
93
|
+
* `kind` is what lets an older SDK skip a marker it does not understand rather
|
|
94
|
+
* than misplace it.
|
|
95
|
+
*/
|
|
96
|
+
import { type ElementLike } from "./target.js";
|
|
97
|
+
import { type SourceRef, type TargetInfo } from "./protocol.js";
|
|
98
|
+
/**
|
|
99
|
+
* An element identified by the source location that rendered it, plus the chain
|
|
100
|
+
* of source locations above it.
|
|
101
|
+
*
|
|
102
|
+
* `chain` is innermost-first and INCLUDES the leaf, exactly as
|
|
103
|
+
* `collectSourceChain` produces it — one array, not a leaf plus a tail, because
|
|
104
|
+
* two representations of one list is a way for them to disagree about whether
|
|
105
|
+
* entry zero is the element or its parent.
|
|
106
|
+
*/
|
|
107
|
+
export interface SourceRefAnchor {
|
|
108
|
+
kind: "source-ref";
|
|
109
|
+
file: string;
|
|
110
|
+
line: number;
|
|
111
|
+
column?: number;
|
|
112
|
+
chain: SourceRef[];
|
|
113
|
+
/**
|
|
114
|
+
* The element's index among the leaf-matching candidates at capture time.
|
|
115
|
+
*
|
|
116
|
+
* **Recorded, and deliberately never used to select.** It is here because
|
|
117
|
+
* knowing "this was the 3rd of 6" is genuinely useful when a human is looking
|
|
118
|
+
* at an unresolved anchor and asking what happened — and it is *not* here as a
|
|
119
|
+
* tie-break, for the reason set out at length in this file's header. If you
|
|
120
|
+
* are about to reach for it to turn an `"ambiguous"` into an element, that is
|
|
121
|
+
* the bug this whole scheme was rewritten to avoid; `anchor.test.ts` has a
|
|
122
|
+
* test that fails if you do.
|
|
123
|
+
*/
|
|
124
|
+
ordinal?: number;
|
|
125
|
+
}
|
|
126
|
+
/** A CSS-ish path, for elements no build transform gave a ref. */
|
|
127
|
+
export interface DomPathAnchor {
|
|
128
|
+
kind: "dom-path";
|
|
129
|
+
path: string;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* "Something on THIS page."
|
|
133
|
+
*
|
|
134
|
+
* First-class rather than a fallback of last resort: it is the honest home for a
|
|
135
|
+
* region capture, for a canvas, for a PDF, and for any product that is not a JSX
|
|
136
|
+
* tree.
|
|
137
|
+
*
|
|
138
|
+
* ===========================================================================
|
|
139
|
+
* `path` IS NOT OPTIONAL DECORATION — IT IS WHAT MAKES "THIS PAGE" MEAN ANYTHING
|
|
140
|
+
* ===========================================================================
|
|
141
|
+
*
|
|
142
|
+
* The first version of this interface had no fields at all and `resolveAnchor`
|
|
143
|
+
* returned `doc.body` unconditionally, with a comment calling it *"always
|
|
144
|
+
* placeable"*. A reviewer pointed out that this reintroduces, through the route
|
|
145
|
+
* branch, exactly the failure the source-ref scheme was rewritten to prevent:
|
|
146
|
+
*
|
|
147
|
+
* A capture taken on `/guides/a` resolves CONFIDENTLY on `/guides/b`. One
|
|
148
|
+
* match, marker drawn, wrong page, counted as `placed`. No signal anywhere.
|
|
149
|
+
*
|
|
150
|
+
* That is not a rate to be estimated. `return doc.body` has no input but the
|
|
151
|
+
* document, so under it **every** route anchor placed on **every** page it was
|
|
152
|
+
* shown — 100%, by construction, and there is nothing to measure. The reason it
|
|
153
|
+
* matters is `payload.page.route`: it is the matched PATTERN rather than the
|
|
154
|
+
* URL, so "the page this came from" was never pinned in the first place. And
|
|
155
|
+
* `anchorFromTarget` sends every region capture down this branch, so it is the
|
|
156
|
+
* common case rather than the edge.
|
|
157
|
+
*
|
|
158
|
+
* The measured neighbour is the SOURCE-REF branch meeting the same problem:
|
|
159
|
+
* 414 anchors captured on one guide page resolve **identically** after a real
|
|
160
|
+
* navigation to another guide page — 161 placed, 253 ambiguous, 0 refused, the
|
|
161
|
+
* same three numbers on both — recorded with its method and denominator in
|
|
162
|
+
* `docs/decisions/a-275-anchor-placement.md` § *The limit this does not fix*.
|
|
163
|
+
* It is a different branch and a different number, cited here as evidence that
|
|
164
|
+
* the pattern-not-URL problem is real, not as a figure for this one.
|
|
165
|
+
*
|
|
166
|
+
* So a route anchor carries the RESOLVED path it was captured on, and
|
|
167
|
+
* `resolveAnchor` refuses when the document is somewhere else.
|
|
168
|
+
*
|
|
169
|
+
* ===========================================================================
|
|
170
|
+
* AND ABSENT MEANS "CANNOT PLACE", NEVER "PLACE ANYWAY"
|
|
171
|
+
* ===========================================================================
|
|
172
|
+
*
|
|
173
|
+
* `path` is optional because the field is newer than the wire, and the read-back
|
|
174
|
+
* plan's versioning rule for exactly this case is that a new anchor field is
|
|
175
|
+
* optional and **absent means cannot place** — the precedent `selection?:` set.
|
|
176
|
+
* A route anchor with no `path`, or a document that cannot say where it is,
|
|
177
|
+
* therefore resolves to `"none"`.
|
|
178
|
+
*
|
|
179
|
+
* That is a deliberate choice to under-place rather than mis-place, and it costs
|
|
180
|
+
* nothing today because no route anchor has ever been stored: markers have not
|
|
181
|
+
* shipped. It would be a real cost if this were retrofitted a year from now,
|
|
182
|
+
* which is the argument for doing it here.
|
|
183
|
+
*
|
|
184
|
+
* WHAT IT DOES NOT FIX: a SOURCE-REF anchor still resolves happily on a
|
|
185
|
+
* different `[id]` of the same route pattern — the markup is identical, so there
|
|
186
|
+
* is nothing for the chain to disagree with, and no amount of anchor design
|
|
187
|
+
* closes it. That one is the read endpoint's (A-276): scope the query on the
|
|
188
|
+
* resolved path, not on `route`. Stated here because an unstated limit gets read
|
|
189
|
+
* as a covered one.
|
|
190
|
+
*/
|
|
191
|
+
export interface RouteAnchor {
|
|
192
|
+
kind: "route";
|
|
193
|
+
/** `location.pathname` at capture time. Absent means the anchor cannot be placed. */
|
|
194
|
+
path?: string;
|
|
195
|
+
}
|
|
196
|
+
export type Anchor = SourceRefAnchor | DomPathAnchor | RouteAnchor;
|
|
197
|
+
/**
|
|
198
|
+
* Where a marker goes, or why it does not go anywhere.
|
|
199
|
+
*
|
|
200
|
+
* Three outcomes and not two, because `"none"` and `"ambiguous"` are different
|
|
201
|
+
* facts about the page and a caller counting them together loses the one that
|
|
202
|
+
* says the markup moved.
|
|
203
|
+
*/
|
|
204
|
+
export type AnchorResolution<E extends ElementLike = ElementLike> = E | "none" | "ambiguous";
|
|
205
|
+
/**
|
|
206
|
+
* The narrowest slice of `Document` this module reads. A real `Document`
|
|
207
|
+
* satisfies it, which `anchor.test.ts` asserts at compile time.
|
|
208
|
+
*
|
|
209
|
+
* `location` is here for the route branch above. It is optional because a parsed
|
|
210
|
+
* fixture has no location, and a document that cannot say where it is makes a
|
|
211
|
+
* route anchor unplaceable rather than placeable-by-default.
|
|
212
|
+
*/
|
|
213
|
+
export interface DocumentLike<E extends ElementLike = ElementLike> {
|
|
214
|
+
querySelectorAll(selectors: string): ArrayLike<E>;
|
|
215
|
+
body?: E | null;
|
|
216
|
+
location?: {
|
|
217
|
+
pathname: string;
|
|
218
|
+
} | null;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Resolve an anchor against a document.
|
|
222
|
+
*
|
|
223
|
+
* Pure: it reads the document and returns an element or a refusal, and it never
|
|
224
|
+
* mutates, measures layout, or draws anything. That is what lets the same
|
|
225
|
+
* function be a unit test over a checked-in HTML fixture, a number in a
|
|
226
|
+
* measurement run, and the marker layer's placement call.
|
|
227
|
+
*
|
|
228
|
+
* A `kind` this version does not recognise resolves to `"none"` — the read
|
|
229
|
+
* response is additive-only forever (A-276), so an SDK meeting a newer console
|
|
230
|
+
* must SKIP a marker it cannot place rather than guess at one.
|
|
231
|
+
*/
|
|
232
|
+
export declare function resolveAnchor<E extends ElementLike>(anchor: Anchor, doc: DocumentLike<E>): AnchorResolution<E>;
|
|
233
|
+
export interface AnchorFromTargetOptions {
|
|
234
|
+
/** Document order index among leaf-matching candidates, when the caller has a document. */
|
|
235
|
+
ordinal?: number;
|
|
236
|
+
/**
|
|
237
|
+
* `location.pathname` at capture time — the RESOLVED path, not the route
|
|
238
|
+
* pattern.
|
|
239
|
+
*
|
|
240
|
+
* Pass it. A route anchor built without it can never be placed, by design; see
|
|
241
|
+
* `RouteAnchor`. It is optional here rather than required because
|
|
242
|
+
* `anchorFromTarget` is also called for element captures, where it is unused.
|
|
243
|
+
*/
|
|
244
|
+
path?: string;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Build the anchor for a capture.
|
|
248
|
+
*
|
|
249
|
+
* The capture side of the same primitive, here rather than in `target.ts` so the
|
|
250
|
+
* union and the function that produces it cannot drift apart.
|
|
251
|
+
*
|
|
252
|
+
* **A region capture becomes a `route` anchor.** Its `sourceChain` is the
|
|
253
|
+
* siblings a rectangle crossed, which contain nothing of each other; handing
|
|
254
|
+
* that to `chainMatches` would compare a sibling list against an ancestor list
|
|
255
|
+
* and refuse — the right answer for the wrong reason, and one that would report
|
|
256
|
+
* every region capture as "the markup moved". Route-level is what a region
|
|
257
|
+
* capture actually means: something on this page, in that area.
|
|
258
|
+
*/
|
|
259
|
+
export declare function anchorFromTarget(target: Pick<TargetInfo, "selection" | "source" | "sourceChain" | "domPath">, options?: AnchorFromTargetOptions): Anchor;
|
|
260
|
+
/**
|
|
261
|
+
* The counts a caller reports, so that "0 markers" is never the same string as
|
|
262
|
+
* "working, nothing here" (rule 1 of the read-back plan).
|
|
263
|
+
*
|
|
264
|
+
* ===========================================================================
|
|
265
|
+
* `wrong` IS NOT IN HERE AND CANNOT BE, AND THAT IS A GAP RATHER THAN A DESIGN
|
|
266
|
+
* ===========================================================================
|
|
267
|
+
*
|
|
268
|
+
* Whether a resolved element is the RIGHT element is not knowable from the live
|
|
269
|
+
* page alone — it takes a before/after comparison, which is what A-275's
|
|
270
|
+
* mutation battery does and what no runtime caller can do.
|
|
271
|
+
*
|
|
272
|
+
* So `placed` means "resolved to exactly one element", NOT "resolved
|
|
273
|
+
* correctly", and there is one measured channel where those differ: a
|
|
274
|
+
* source-ref anchor captured on one `[id]` resolves on another `[id]` of the
|
|
275
|
+
* same route pattern, because the two pages are structurally identical.
|
|
276
|
+
*
|
|
277
|
+
* MEASURED at **161 of 414 anchors** placing on a different `[id]` of the same
|
|
278
|
+
* route pattern, with zero refusals — indistinguishable, count for count, from
|
|
279
|
+
* resolving against the page they were captured on. The method, the denominator
|
|
280
|
+
* and the pages are in `docs/decisions/a-275-anchor-placement.md` § *The limit
|
|
281
|
+
* this does not fix*, and `node anchor-measure.mjs --url … --compare-url …`
|
|
282
|
+
* re-runs it in one command. Nothing in this module can detect it; the
|
|
283
|
+
* fix is for the read endpoint to scope on the resolved path (A-276), so a
|
|
284
|
+
* caller must not read `placed` as a correctness figure until it does.
|
|
285
|
+
*
|
|
286
|
+
* The route branch closes its own half of that (`RouteAnchor.path`). This
|
|
287
|
+
* comment is the record that the source-ref half is still open.
|
|
288
|
+
*/
|
|
289
|
+
export interface AnchorTally {
|
|
290
|
+
total: number;
|
|
291
|
+
placed: number;
|
|
292
|
+
ambiguous: number;
|
|
293
|
+
unresolved: number;
|
|
294
|
+
}
|
|
295
|
+
export declare function tallyAnchors<E extends ElementLike>(anchors: readonly Anchor[], doc: DocumentLike<E>): AnchorTally;
|
package/dist/anchor.mjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var u="data-src";function p(e){return e.selection==="region"?"region":"element"}var f=8;function s(e){if(!e)return null;let n=e.lastIndexOf(":");if(n<=0)return null;let t=e.lastIndexOf(":",n-1);if(t<=0)return null;let o=e.slice(0,t),r=Number(e.slice(t+1,n)),i=Number(e.slice(n+1));return!o||!Number.isFinite(r)||!Number.isFinite(i)?null:{file:o,line:r,column:i}}function g(e,n=f){let t=[],o=e;for(;o&&t.length<n;){let r=s(o.getAttribute(u));if(r){let i=t[t.length-1];i&&i.file===r.file&&i.line===r.line||t.push(r)}o=o.parentElement}return t}function d(e){let n=e;for(;n;){if(s(n.getAttribute(u)))return n;n=n.parentElement}return null}function l(e,n){return e.file!==n.file||e.line!==n.line?!1:e.column===void 0||n.column===void 0?!0:e.column===n.column}function h(e,n){if(e.length===0)return!1;if(e.length<f){if(n.length!==e.length)return!1}else if(n.length<e.length)return!1;return e.every((t,o)=>{let r=n[o];return r!==void 0&&l(t,r)})}function A(e,n){let t={file:e.file,line:e.line};e.column!==void 0&&(t.column=e.column);let o=n.querySelectorAll(`[${u}]`),r=[];for(let i=0;i<o.length;i++){let a=o[i],c=s(a.getAttribute(u));if(!c||!l(t,c))continue;let m=s(d(a.parentElement)?.getAttribute(u)??null);m&&l(m,c)||r.push(a)}return r}function x(e,n){switch(e.kind){case"route":{if(!e.path)return"none";let t=n.location?.pathname;return!t||t!==e.path?"none":n.body??"none"}case"dom-path":{if(!e.path)return"none";let t;try{t=n.querySelectorAll(e.path)}catch{return"none"}return t.length===0?"none":t.length>1?"ambiguous":t[0]}case"source-ref":{if(e.chain.length===0)return"none";let t=A(e,n).filter(o=>h(e.chain,g(o)));return t.length===0?"none":t.length>1?"ambiguous":t[0]}default:return"none"}}function N(e,n={}){let t=()=>n.path===void 0?{kind:"route"}:{kind:"route",path:n.path};if(p(e)==="region")return t();let o=e.sourceChain??[],r=o[0],i=e.source??r;if(i&&r&&l(i,r)){let a={kind:"source-ref",file:i.file,line:i.line,chain:o};return i.column!==void 0&&(a.column=i.column),n.ordinal!==void 0&&(a.ordinal=n.ordinal),a}return e.domPath?{kind:"dom-path",path:e.domPath}:t()}function S(e,n){let t={total:e.length,placed:0,ambiguous:0,unresolved:0};for(let o of e){let r=x(o,n);r==="ambiguous"?t.ambiguous+=1:r==="none"?t.unresolved+=1:t.placed+=1}return t}export{N as anchorFromTarget,x as resolveAnchor,S as tallyAnchors};
|
|
2
|
+
//# sourceMappingURL=anchor.mjs.map
|