@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.
Files changed (47) hide show
  1. package/ADOPTING.md +52 -0
  2. package/README.md +9 -1
  3. package/dist/anchor.cjs +2 -0
  4. package/dist/anchor.cjs.map +7 -0
  5. package/dist/anchor.d.cts +295 -0
  6. package/dist/anchor.d.mts +295 -0
  7. package/dist/anchor.d.ts +295 -0
  8. package/dist/anchor.mjs +2 -0
  9. package/dist/anchor.mjs.map +7 -0
  10. package/dist/arming.d.cts +30 -2
  11. package/dist/arming.d.mts +30 -2
  12. package/dist/arming.d.ts +30 -2
  13. package/dist/host.cjs +1 -1
  14. package/dist/host.cjs.map +1 -1
  15. package/dist/host.mjs.map +1 -1
  16. package/dist/index.cjs +4 -4
  17. package/dist/index.cjs.map +3 -3
  18. package/dist/index.d.cts +2 -2
  19. package/dist/index.d.mts +2 -2
  20. package/dist/index.d.ts +2 -2
  21. package/dist/index.mjs +4 -4
  22. package/dist/index.mjs.map +3 -3
  23. package/dist/markers.cjs +57 -0
  24. package/dist/markers.cjs.map +7 -0
  25. package/dist/markers.d.cts +246 -0
  26. package/dist/markers.d.mts +246 -0
  27. package/dist/markers.d.ts +246 -0
  28. package/dist/markers.mjs +57 -0
  29. package/dist/markers.mjs.map +7 -0
  30. package/dist/protocol.cjs +1 -1
  31. package/dist/protocol.cjs.map +3 -3
  32. package/dist/protocol.d.cts +29 -0
  33. package/dist/protocol.d.mts +29 -0
  34. package/dist/protocol.d.ts +29 -0
  35. package/dist/protocol.mjs +1 -1
  36. package/dist/protocol.mjs.map +3 -3
  37. package/dist/read.cjs +2 -0
  38. package/dist/read.cjs.map +7 -0
  39. package/dist/read.d.cts +149 -0
  40. package/dist/read.d.mts +149 -0
  41. package/dist/read.d.ts +149 -0
  42. package/dist/read.mjs +2 -0
  43. package/dist/read.mjs.map +7 -0
  44. package/dist/transport.d.cts +7 -1
  45. package/dist/transport.d.mts +7 -1
  46. package/dist/transport.d.ts +7 -1
  47. 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;
@@ -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;
@@ -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