@weatherboard/annotation 0.2.2 → 0.4.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 +96 -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 +64 -0
  24. package/dist/markers.cjs.map +7 -0
  25. package/dist/markers.d.cts +283 -0
  26. package/dist/markers.d.mts +283 -0
  27. package/dist/markers.d.ts +283 -0
  28. package/dist/markers.mjs +64 -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 +194 -0
  40. package/dist/read.d.mts +194 -0
  41. package/dist/read.d.ts +194 -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,283 @@
1
+ import { resolveAnchor } from "./anchor.mjs";
2
+ import type { AnnotationMarker, AnnotationReader } from "./read.mjs";
3
+ /**
4
+ * Markers on the components a reviewer annotated, and the panel behind them
5
+ * (A-277).
6
+ *
7
+ * ===========================================================================
8
+ * WHY THIS IS A SIXTH ENTRY POINT AND NOT PART OF `.`
9
+ * ===========================================================================
10
+ *
11
+ * `./anchor` and `./read` both make the same argument and it applies here with
12
+ * the most weight: this runs only in a product that RENDERS markers, and the SDK
13
+ * that only captures does not need a byte of it. Folding it into `index` would
14
+ * put the marker layer, the panel and its stylesheet into the bundle of every
15
+ * page of every managed product, including the ones that will never show one.
16
+ *
17
+ * ===========================================================================
18
+ * THE COUNT LINE IS THE FEATURE. THE ICONS ARE THE GARNISH.
19
+ * ===========================================================================
20
+ *
21
+ * On a read path **zero markers is exactly what "working, nothing here" looks
22
+ * like**, and the runbook lists twelve distinct causes that produce it. So an
23
+ * armed page ALWAYS renders the line, before and after the read, whatever the
24
+ * answer — *"3 annotations here, 1 shown"* — and that is not a status indicator,
25
+ * it is the only thing that makes an anchor which cannot place VISIBLE rather
26
+ * than silent.
27
+ *
28
+ * The sentence is built from the PAGE's own tally, with the envelope's `total`
29
+ * as the denominator. It is deliberately not built from the envelope's
30
+ * `anchored`: the server has no DOM and cannot know whether a marker lands, and
31
+ * two numbers called "placed" meaning different things is the shape this
32
+ * repository keeps getting burned by. `anchored` is a diagnostic for the
33
+ * runbook — it separates *the console has nothing for this page* from *the
34
+ * console has three and your DOM matched none of them* — and it is exposed on
35
+ * `MarkerRender` rather than rendered as though it were the page's answer.
36
+ *
37
+ * ===========================================================================
38
+ * MOST MARKERS ON A LIST PAGE ARE ROUTE-LEVEL, AND THAT IS A DESIGNED STATE
39
+ * ===========================================================================
40
+ *
41
+ * MEASURED by A-275 against live staging pages: on a listing page only about
42
+ * **25%** of annotated elements resolve uniquely, and about 75% are ambiguous
43
+ * identical siblings. That is not a defect to be worked around — a source ref
44
+ * names a COMPONENT, and two identical cards are genuinely indistinguishable
45
+ * from one. Wrong-placement was **0** across every mutation tested, and it is
46
+ * zero because the resolver refuses rather than guesses.
47
+ *
48
+ * So this layer is built for the refusing case rather than treating it as
49
+ * degraded:
50
+ *
51
+ * - an `"ambiguous"` anchor NEVER gets a marker. A marker on the wrong element
52
+ * does not degrade this feature, it inverts it.
53
+ * - markers that resolve to the SAME element are drawn as one icon carrying a
54
+ * count, and the panel lists all of them. Without that, a route anchor — the
55
+ * honest home for a region capture and for any page with no source refs —
56
+ * would stack every marker on the page at one point, and three captures would
57
+ * render as one icon with two hidden underneath it.
58
+ * - a route anchor resolves to `document.body`, so its cluster sits in the
59
+ * page's own top-right corner and reads as *"about this page"*. That is what
60
+ * the anchor means.
61
+ *
62
+ * ===========================================================================
63
+ * IT NEVER RE-RESOLVES SOMETHING IT DID NOT READ FROM THE CONSOLE
64
+ * ===========================================================================
65
+ *
66
+ * `resolveAnchor` is IMPORTED from `./anchor` and nothing here reimplements any
67
+ * part of it. That is A-275's whole point: the function that measured the scheme
68
+ * and the function that places the markers must be one function, or the
69
+ * measurement is about code nobody runs.
70
+ *
71
+ * ===========================================================================
72
+ * THIS RUNS INSIDE SOMEBODY ELSE'S PRODUCTION SITE
73
+ * ===========================================================================
74
+ *
75
+ * Everything rendered here came off the network. `read.ts` sanitises it — bounded
76
+ * strings, control characters dropped, a marker that cannot be read skipped — and
77
+ * this file adds the second half of that guarantee: every value reaches the DOM
78
+ * through `textContent` and never through `innerHTML`, so a console that has been
79
+ * compromised or has merely gone wrong cannot put markup into a customer's page.
80
+ * There is no `href` anywhere in this layer, which is why `safeIssueUrl` has no
81
+ * caller here.
82
+ *
83
+ * The whole layer lives in a shadow root, so the product's stylesheet cannot
84
+ * reach in and this cannot reach out.
85
+ */
86
+ /** The layer's host node. Namespaced, and never product-named (A-98). */
87
+ export declare const MARKERS_ID = "ai-annotation-markers";
88
+ export interface AnnotationMarkerLabels {
89
+ /** The region the whole layer announces itself as. */
90
+ region: string;
91
+ /**
92
+ * Built with the three counts: `(total, shown, resolvedHidden) => string`.
93
+ *
94
+ * `resolvedHidden` is a third argument rather than a separate line because a
95
+ * count in one place and a caveat in another is how a reader concludes the
96
+ * first number was the whole answer. A consumer overriding this with a
97
+ * two-parameter function still typechecks and still gets the first two
98
+ * numbers — but it will not say what is being held back, which is the one
99
+ * thing this argument exists for.
100
+ */
101
+ count: (total: number, shown: number, resolvedHidden: number) => string;
102
+ /**
103
+ * The control that widens the read, and the label it carries in each state.
104
+ *
105
+ * A count with no way to act on it is a fact a reviewer can do nothing with;
106
+ * a control with no count is one nobody knows to press. Both, or neither.
107
+ */
108
+ showResolved: (resolvedHidden: number) => string;
109
+ hideResolved: string;
110
+ /** Shown before the first read completes. */
111
+ loading: string;
112
+ /** The console answered and refused. `reason` is the envelope's, or empty. */
113
+ refused: (reason: string) => string;
114
+ /** The read did not complete at all — no status, no body. */
115
+ unreachable: string;
116
+ /** No read token, so nothing was even attempted. */
117
+ noCredential: string;
118
+ /** Freshness, from `asOf`. */
119
+ asOf: (age: string) => string;
120
+ freshnessUnknown: string;
121
+ close: string;
122
+ }
123
+ export declare const DEFAULT_MARKER_LABELS: AnnotationMarkerLabels;
124
+ export interface AnnotationMarkersOptions {
125
+ /**
126
+ * The read client, from `createAnnotationReader`. Required: a marker layer
127
+ * with no way to read is a layer that renders an empty page confidently.
128
+ */
129
+ reader: AnnotationReader | null;
130
+ /**
131
+ * May this page render markers at all?
132
+ *
133
+ * `false` renders NOTHING — no host node, no shadow root, no stylesheet —
134
+ * exactly as `createAnnotationToolbar` does, and for the same reason: an
135
+ * unarmed page that shipped this code must leave no residue in a customer's
136
+ * DOM. Every method still answers, so a caller does not have to branch.
137
+ */
138
+ armed?: boolean;
139
+ doc?: Document;
140
+ win?: Window;
141
+ /**
142
+ * The path to ask about. Defaults to `location.pathname`.
143
+ *
144
+ * The RESOLVED path and never the route pattern. A pattern is one value for
145
+ * every page of a dynamic route, so asking by pattern returns another page's
146
+ * captures — measured at 161 of 414 anchors placing confidently on the wrong
147
+ * `[slug]`, with zero refusals, in `docs/decisions/a-275-anchor-placement.md`.
148
+ */
149
+ path?: string;
150
+ labels?: Partial<AnnotationMarkerLabels>;
151
+ /**
152
+ * Start with the resolved captures showing (A-288/A-323). Defaults to `false`.
153
+ *
154
+ * The default matches the console's, deliberately: a layer that asked for
155
+ * everything on every page would put the default back where A-288 decided it
156
+ * should not be, from the other side of the wire, and nobody would see it in
157
+ * the console's code.
158
+ */
159
+ showResolved?: boolean;
160
+ /** For the age in the freshness line. Defaults to `Date.now`. */
161
+ now?: () => number;
162
+ }
163
+ /** What one render actually put on the page. Returned so a caller can assert on it. */
164
+ export interface MarkerRender {
165
+ /** The envelope's `total` — what the console holds for this path. */
166
+ total: number;
167
+ /** Markers whose anchor resolved to exactly one element, and were drawn. */
168
+ shown: number;
169
+ /** Anchors that resolved to more than one element. NEVER drawn. */
170
+ ambiguous: number;
171
+ /** Anchors that matched nothing in this document. */
172
+ unresolved: number;
173
+ /**
174
+ * The CONSOLE's count of anchors it could serve as placeable at all.
175
+ *
176
+ * Carried through for the runbook's checklist and deliberately NOT rendered as
177
+ * though it were `shown`. See this file's header.
178
+ */
179
+ anchored: number;
180
+ /**
181
+ * The envelope's `resolvedHidden` — finished captures the console held back.
182
+ *
183
+ * Zero while `showingResolved` is true, because nothing is then being held
184
+ * back. That is the console's number and not an inference: a layer that
185
+ * remembered the previous read's figure would keep reporting it after the
186
+ * capture had been archived.
187
+ */
188
+ resolvedHidden: number;
189
+ /** Whether this render asked for `?resolved=include`. */
190
+ showingResolved: boolean;
191
+ /** Distinct elements drawn on. Lower than `shown` wherever markers clustered. */
192
+ clusters: number;
193
+ /** The envelope's `asOf` — the console's Linear cursor, or `""`. */
194
+ asOf: string;
195
+ /** The line a human reads. Exactly the text in the DOM. */
196
+ line: string;
197
+ }
198
+ export interface AnnotationMarkers {
199
+ /** Read, resolve, draw. Never rejects: a failure is a rendered sentence. */
200
+ refresh(): Promise<MarkerRender>;
201
+ /** Is this node ours? For an SDK that must not capture its own furniture. */
202
+ owns(node: unknown): boolean;
203
+ /** The last render, or null before the first. */
204
+ last(): MarkerRender | null;
205
+ destroy(): void;
206
+ }
207
+ export interface Spot {
208
+ left: number;
209
+ top: number;
210
+ }
211
+ /**
212
+ * A position for this pin that no already-placed pin occupies — or, when there
213
+ * is none, the ideal one, OVERLAPPING.
214
+ *
215
+ * ===========================================================================
216
+ * IT GIVES UP. THAT IS THE WHOLE POINT OF THIS FUNCTION EXISTING.
217
+ * ===========================================================================
218
+ *
219
+ * This was a bare `while` loop with no exit but success, and it could not
220
+ * terminate. Once `left` floored at `MARGIN` the wrap computed
221
+ * `Math.max(MARGIN, MARGIN - STEP) === MARGIN` and `top = MARGIN`, so the search
222
+ * returned to a position it had already rejected and tried it again, forever.
223
+ *
224
+ * REPRODUCED, not reasoned about: two clusters on two distinct elements with
225
+ * `innerWidth` and `innerHeight` both 0 — `refresh()` never returned, killed at
226
+ * 20s. `position()` is called by `draw()` and by every `scroll` and `resize`
227
+ * listener, so that is **a locked main thread inside a customer's production
228
+ * page**, which is strictly worse than the thrown read `refresh()` refuses to
229
+ * allow.
230
+ *
231
+ * A zero-sized viewport is not exotic: a hidden iframe, a prerender, a host that
232
+ * stubs `win`. Two pins are then enough. A short embedded viewport reaches it
233
+ * honestly too — 300×200 offers about 77 slots against a possible 100 pins.
234
+ *
235
+ * And the irony is the useful part. `linkedom` returns zeros for every rect,
236
+ * which is exactly why the clamp above had to be found in a browser — and it is
237
+ * exactly why no fixture test saw this. **The zero-rect case is a layout hazard
238
+ * AND a termination hazard, and the first fix addressed only the layout.**
239
+ *
240
+ * So the walk is bounded twice over: `MAX_PROBES`, and a `left` that has run out
241
+ * of columns. When it gives up, the pin goes at its clamped ideal and is allowed
242
+ * to sit on top of another. **An overlapping marker is a visible imperfection; a
243
+ * locked main thread is the product broken.**
244
+ *
245
+ * Exported so `markers.test.ts` can assert termination directly, under
246
+ * `linkedom`, with no browser: only the QUALITY of the placement needs one.
247
+ */
248
+ export declare function freeSpot(ideal: Spot, taken: readonly Spot[], maxTop: number): Spot;
249
+ /**
250
+ * A duration a human reads. Two units at most, mirroring `fleet/freshness.ts` —
251
+ * the point of the line is "is this minutes or hours", and the seconds in
252
+ * "2h 10m 4s" are noise that makes the answer take longer to find.
253
+ *
254
+ * Reimplemented rather than imported because that module is server-side and this
255
+ * package must not depend on the console. It is eleven lines of arithmetic with
256
+ * a test on it, which is the honest version of that trade.
257
+ */
258
+ export declare function formatMarkerAge(ms: number): string;
259
+ /**
260
+ * Group markers by the element each one resolves to, refusing the ambiguous.
261
+ *
262
+ * Exported and pure so the placement rule is testable over a parsed fixture
263
+ * without a browser — the same split `anchor.test.ts` uses, and the reason this
264
+ * layer's most important behaviour (`ambiguous` is never drawn) is covered by
265
+ * `pnpm verify` rather than only by a browser nobody schedules.
266
+ */
267
+ export declare function placeMarkers(markers: readonly AnnotationMarker[], doc: Parameters<typeof resolveAnchor>[1]): {
268
+ clusters: {
269
+ element: unknown;
270
+ markers: AnnotationMarker[];
271
+ }[];
272
+ ambiguous: number;
273
+ unresolved: number;
274
+ };
275
+ /**
276
+ * Mount the marker layer.
277
+ *
278
+ * Does not read anything until `refresh()` is called, so a caller decides when
279
+ * the page is settled enough to measure. The count line renders `loading`
280
+ * immediately, because an armed page with nothing on it at all is the state this
281
+ * whole layer exists to make impossible.
282
+ */
283
+ export declare function mountAnnotationMarkers(options: AnnotationMarkersOptions): AnnotationMarkers;
@@ -0,0 +1,283 @@
1
+ import { resolveAnchor } from "./anchor.js";
2
+ import type { AnnotationMarker, AnnotationReader } from "./read.js";
3
+ /**
4
+ * Markers on the components a reviewer annotated, and the panel behind them
5
+ * (A-277).
6
+ *
7
+ * ===========================================================================
8
+ * WHY THIS IS A SIXTH ENTRY POINT AND NOT PART OF `.`
9
+ * ===========================================================================
10
+ *
11
+ * `./anchor` and `./read` both make the same argument and it applies here with
12
+ * the most weight: this runs only in a product that RENDERS markers, and the SDK
13
+ * that only captures does not need a byte of it. Folding it into `index` would
14
+ * put the marker layer, the panel and its stylesheet into the bundle of every
15
+ * page of every managed product, including the ones that will never show one.
16
+ *
17
+ * ===========================================================================
18
+ * THE COUNT LINE IS THE FEATURE. THE ICONS ARE THE GARNISH.
19
+ * ===========================================================================
20
+ *
21
+ * On a read path **zero markers is exactly what "working, nothing here" looks
22
+ * like**, and the runbook lists twelve distinct causes that produce it. So an
23
+ * armed page ALWAYS renders the line, before and after the read, whatever the
24
+ * answer — *"3 annotations here, 1 shown"* — and that is not a status indicator,
25
+ * it is the only thing that makes an anchor which cannot place VISIBLE rather
26
+ * than silent.
27
+ *
28
+ * The sentence is built from the PAGE's own tally, with the envelope's `total`
29
+ * as the denominator. It is deliberately not built from the envelope's
30
+ * `anchored`: the server has no DOM and cannot know whether a marker lands, and
31
+ * two numbers called "placed" meaning different things is the shape this
32
+ * repository keeps getting burned by. `anchored` is a diagnostic for the
33
+ * runbook — it separates *the console has nothing for this page* from *the
34
+ * console has three and your DOM matched none of them* — and it is exposed on
35
+ * `MarkerRender` rather than rendered as though it were the page's answer.
36
+ *
37
+ * ===========================================================================
38
+ * MOST MARKERS ON A LIST PAGE ARE ROUTE-LEVEL, AND THAT IS A DESIGNED STATE
39
+ * ===========================================================================
40
+ *
41
+ * MEASURED by A-275 against live staging pages: on a listing page only about
42
+ * **25%** of annotated elements resolve uniquely, and about 75% are ambiguous
43
+ * identical siblings. That is not a defect to be worked around — a source ref
44
+ * names a COMPONENT, and two identical cards are genuinely indistinguishable
45
+ * from one. Wrong-placement was **0** across every mutation tested, and it is
46
+ * zero because the resolver refuses rather than guesses.
47
+ *
48
+ * So this layer is built for the refusing case rather than treating it as
49
+ * degraded:
50
+ *
51
+ * - an `"ambiguous"` anchor NEVER gets a marker. A marker on the wrong element
52
+ * does not degrade this feature, it inverts it.
53
+ * - markers that resolve to the SAME element are drawn as one icon carrying a
54
+ * count, and the panel lists all of them. Without that, a route anchor — the
55
+ * honest home for a region capture and for any page with no source refs —
56
+ * would stack every marker on the page at one point, and three captures would
57
+ * render as one icon with two hidden underneath it.
58
+ * - a route anchor resolves to `document.body`, so its cluster sits in the
59
+ * page's own top-right corner and reads as *"about this page"*. That is what
60
+ * the anchor means.
61
+ *
62
+ * ===========================================================================
63
+ * IT NEVER RE-RESOLVES SOMETHING IT DID NOT READ FROM THE CONSOLE
64
+ * ===========================================================================
65
+ *
66
+ * `resolveAnchor` is IMPORTED from `./anchor` and nothing here reimplements any
67
+ * part of it. That is A-275's whole point: the function that measured the scheme
68
+ * and the function that places the markers must be one function, or the
69
+ * measurement is about code nobody runs.
70
+ *
71
+ * ===========================================================================
72
+ * THIS RUNS INSIDE SOMEBODY ELSE'S PRODUCTION SITE
73
+ * ===========================================================================
74
+ *
75
+ * Everything rendered here came off the network. `read.ts` sanitises it — bounded
76
+ * strings, control characters dropped, a marker that cannot be read skipped — and
77
+ * this file adds the second half of that guarantee: every value reaches the DOM
78
+ * through `textContent` and never through `innerHTML`, so a console that has been
79
+ * compromised or has merely gone wrong cannot put markup into a customer's page.
80
+ * There is no `href` anywhere in this layer, which is why `safeIssueUrl` has no
81
+ * caller here.
82
+ *
83
+ * The whole layer lives in a shadow root, so the product's stylesheet cannot
84
+ * reach in and this cannot reach out.
85
+ */
86
+ /** The layer's host node. Namespaced, and never product-named (A-98). */
87
+ export declare const MARKERS_ID = "ai-annotation-markers";
88
+ export interface AnnotationMarkerLabels {
89
+ /** The region the whole layer announces itself as. */
90
+ region: string;
91
+ /**
92
+ * Built with the three counts: `(total, shown, resolvedHidden) => string`.
93
+ *
94
+ * `resolvedHidden` is a third argument rather than a separate line because a
95
+ * count in one place and a caveat in another is how a reader concludes the
96
+ * first number was the whole answer. A consumer overriding this with a
97
+ * two-parameter function still typechecks and still gets the first two
98
+ * numbers — but it will not say what is being held back, which is the one
99
+ * thing this argument exists for.
100
+ */
101
+ count: (total: number, shown: number, resolvedHidden: number) => string;
102
+ /**
103
+ * The control that widens the read, and the label it carries in each state.
104
+ *
105
+ * A count with no way to act on it is a fact a reviewer can do nothing with;
106
+ * a control with no count is one nobody knows to press. Both, or neither.
107
+ */
108
+ showResolved: (resolvedHidden: number) => string;
109
+ hideResolved: string;
110
+ /** Shown before the first read completes. */
111
+ loading: string;
112
+ /** The console answered and refused. `reason` is the envelope's, or empty. */
113
+ refused: (reason: string) => string;
114
+ /** The read did not complete at all — no status, no body. */
115
+ unreachable: string;
116
+ /** No read token, so nothing was even attempted. */
117
+ noCredential: string;
118
+ /** Freshness, from `asOf`. */
119
+ asOf: (age: string) => string;
120
+ freshnessUnknown: string;
121
+ close: string;
122
+ }
123
+ export declare const DEFAULT_MARKER_LABELS: AnnotationMarkerLabels;
124
+ export interface AnnotationMarkersOptions {
125
+ /**
126
+ * The read client, from `createAnnotationReader`. Required: a marker layer
127
+ * with no way to read is a layer that renders an empty page confidently.
128
+ */
129
+ reader: AnnotationReader | null;
130
+ /**
131
+ * May this page render markers at all?
132
+ *
133
+ * `false` renders NOTHING — no host node, no shadow root, no stylesheet —
134
+ * exactly as `createAnnotationToolbar` does, and for the same reason: an
135
+ * unarmed page that shipped this code must leave no residue in a customer's
136
+ * DOM. Every method still answers, so a caller does not have to branch.
137
+ */
138
+ armed?: boolean;
139
+ doc?: Document;
140
+ win?: Window;
141
+ /**
142
+ * The path to ask about. Defaults to `location.pathname`.
143
+ *
144
+ * The RESOLVED path and never the route pattern. A pattern is one value for
145
+ * every page of a dynamic route, so asking by pattern returns another page's
146
+ * captures — measured at 161 of 414 anchors placing confidently on the wrong
147
+ * `[slug]`, with zero refusals, in `docs/decisions/a-275-anchor-placement.md`.
148
+ */
149
+ path?: string;
150
+ labels?: Partial<AnnotationMarkerLabels>;
151
+ /**
152
+ * Start with the resolved captures showing (A-288/A-323). Defaults to `false`.
153
+ *
154
+ * The default matches the console's, deliberately: a layer that asked for
155
+ * everything on every page would put the default back where A-288 decided it
156
+ * should not be, from the other side of the wire, and nobody would see it in
157
+ * the console's code.
158
+ */
159
+ showResolved?: boolean;
160
+ /** For the age in the freshness line. Defaults to `Date.now`. */
161
+ now?: () => number;
162
+ }
163
+ /** What one render actually put on the page. Returned so a caller can assert on it. */
164
+ export interface MarkerRender {
165
+ /** The envelope's `total` — what the console holds for this path. */
166
+ total: number;
167
+ /** Markers whose anchor resolved to exactly one element, and were drawn. */
168
+ shown: number;
169
+ /** Anchors that resolved to more than one element. NEVER drawn. */
170
+ ambiguous: number;
171
+ /** Anchors that matched nothing in this document. */
172
+ unresolved: number;
173
+ /**
174
+ * The CONSOLE's count of anchors it could serve as placeable at all.
175
+ *
176
+ * Carried through for the runbook's checklist and deliberately NOT rendered as
177
+ * though it were `shown`. See this file's header.
178
+ */
179
+ anchored: number;
180
+ /**
181
+ * The envelope's `resolvedHidden` — finished captures the console held back.
182
+ *
183
+ * Zero while `showingResolved` is true, because nothing is then being held
184
+ * back. That is the console's number and not an inference: a layer that
185
+ * remembered the previous read's figure would keep reporting it after the
186
+ * capture had been archived.
187
+ */
188
+ resolvedHidden: number;
189
+ /** Whether this render asked for `?resolved=include`. */
190
+ showingResolved: boolean;
191
+ /** Distinct elements drawn on. Lower than `shown` wherever markers clustered. */
192
+ clusters: number;
193
+ /** The envelope's `asOf` — the console's Linear cursor, or `""`. */
194
+ asOf: string;
195
+ /** The line a human reads. Exactly the text in the DOM. */
196
+ line: string;
197
+ }
198
+ export interface AnnotationMarkers {
199
+ /** Read, resolve, draw. Never rejects: a failure is a rendered sentence. */
200
+ refresh(): Promise<MarkerRender>;
201
+ /** Is this node ours? For an SDK that must not capture its own furniture. */
202
+ owns(node: unknown): boolean;
203
+ /** The last render, or null before the first. */
204
+ last(): MarkerRender | null;
205
+ destroy(): void;
206
+ }
207
+ export interface Spot {
208
+ left: number;
209
+ top: number;
210
+ }
211
+ /**
212
+ * A position for this pin that no already-placed pin occupies — or, when there
213
+ * is none, the ideal one, OVERLAPPING.
214
+ *
215
+ * ===========================================================================
216
+ * IT GIVES UP. THAT IS THE WHOLE POINT OF THIS FUNCTION EXISTING.
217
+ * ===========================================================================
218
+ *
219
+ * This was a bare `while` loop with no exit but success, and it could not
220
+ * terminate. Once `left` floored at `MARGIN` the wrap computed
221
+ * `Math.max(MARGIN, MARGIN - STEP) === MARGIN` and `top = MARGIN`, so the search
222
+ * returned to a position it had already rejected and tried it again, forever.
223
+ *
224
+ * REPRODUCED, not reasoned about: two clusters on two distinct elements with
225
+ * `innerWidth` and `innerHeight` both 0 — `refresh()` never returned, killed at
226
+ * 20s. `position()` is called by `draw()` and by every `scroll` and `resize`
227
+ * listener, so that is **a locked main thread inside a customer's production
228
+ * page**, which is strictly worse than the thrown read `refresh()` refuses to
229
+ * allow.
230
+ *
231
+ * A zero-sized viewport is not exotic: a hidden iframe, a prerender, a host that
232
+ * stubs `win`. Two pins are then enough. A short embedded viewport reaches it
233
+ * honestly too — 300×200 offers about 77 slots against a possible 100 pins.
234
+ *
235
+ * And the irony is the useful part. `linkedom` returns zeros for every rect,
236
+ * which is exactly why the clamp above had to be found in a browser — and it is
237
+ * exactly why no fixture test saw this. **The zero-rect case is a layout hazard
238
+ * AND a termination hazard, and the first fix addressed only the layout.**
239
+ *
240
+ * So the walk is bounded twice over: `MAX_PROBES`, and a `left` that has run out
241
+ * of columns. When it gives up, the pin goes at its clamped ideal and is allowed
242
+ * to sit on top of another. **An overlapping marker is a visible imperfection; a
243
+ * locked main thread is the product broken.**
244
+ *
245
+ * Exported so `markers.test.ts` can assert termination directly, under
246
+ * `linkedom`, with no browser: only the QUALITY of the placement needs one.
247
+ */
248
+ export declare function freeSpot(ideal: Spot, taken: readonly Spot[], maxTop: number): Spot;
249
+ /**
250
+ * A duration a human reads. Two units at most, mirroring `fleet/freshness.ts` —
251
+ * the point of the line is "is this minutes or hours", and the seconds in
252
+ * "2h 10m 4s" are noise that makes the answer take longer to find.
253
+ *
254
+ * Reimplemented rather than imported because that module is server-side and this
255
+ * package must not depend on the console. It is eleven lines of arithmetic with
256
+ * a test on it, which is the honest version of that trade.
257
+ */
258
+ export declare function formatMarkerAge(ms: number): string;
259
+ /**
260
+ * Group markers by the element each one resolves to, refusing the ambiguous.
261
+ *
262
+ * Exported and pure so the placement rule is testable over a parsed fixture
263
+ * without a browser — the same split `anchor.test.ts` uses, and the reason this
264
+ * layer's most important behaviour (`ambiguous` is never drawn) is covered by
265
+ * `pnpm verify` rather than only by a browser nobody schedules.
266
+ */
267
+ export declare function placeMarkers(markers: readonly AnnotationMarker[], doc: Parameters<typeof resolveAnchor>[1]): {
268
+ clusters: {
269
+ element: unknown;
270
+ markers: AnnotationMarker[];
271
+ }[];
272
+ ambiguous: number;
273
+ unresolved: number;
274
+ };
275
+ /**
276
+ * Mount the marker layer.
277
+ *
278
+ * Does not read anything until `refresh()` is called, so a caller decides when
279
+ * the page is settled enough to measure. The count line renders `loading`
280
+ * immediately, because an armed page with nothing on it at all is the state this
281
+ * whole layer exists to make impossible.
282
+ */
283
+ export declare function mountAnnotationMarkers(options: AnnotationMarkersOptions): AnnotationMarkers;
@@ -0,0 +1,64 @@
1
+ var C="data-src";var B=8;function S(e){if(!e)return null;let t=e.lastIndexOf(":");if(t<=0)return null;let n=e.lastIndexOf(":",t-1);if(n<=0)return null;let r=e.slice(0,n),o=Number(e.slice(n+1,t)),i=Number(e.slice(t+1));return!r||!Number.isFinite(o)||!Number.isFinite(i)?null:{file:r,line:o,column:i}}function j(e,t=B){let n=[],r=e;for(;r&&n.length<t;){let o=S(r.getAttribute(C));if(o){let i=n[n.length-1];i&&i.file===o.file&&i.line===o.line||n.push(o)}r=r.parentElement}return n}function q(e){let t=e;for(;t;){if(S(t.getAttribute(C)))return t;t=t.parentElement}return null}function H(e,t){return e.file!==t.file||e.line!==t.line?!1:e.column===void 0||t.column===void 0?!0:e.column===t.column}function J(e,t){if(e.length===0)return!1;if(e.length<B){if(t.length!==e.length)return!1}else if(t.length<e.length)return!1;return e.every((n,r)=>{let o=t[r];return o!==void 0&&H(n,o)})}function Z(e,t){let n={file:e.file,line:e.line};e.column!==void 0&&(n.column=e.column);let r=t.querySelectorAll(`[${C}]`),o=[];for(let i=0;i<r.length;i++){let s=r[i],c=S(s.getAttribute(C));if(!c||!H(n,c))continue;let d=S(q(s.parentElement)?.getAttribute(C)??null);d&&H(d,c)||o.push(s)}return o}function V(e,t){switch(e.kind){case"route":{if(!e.path)return"none";let n=t.location?.pathname;return!n||n!==e.path?"none":t.body??"none"}case"dom-path":{if(!e.path)return"none";let n;try{n=t.querySelectorAll(e.path)}catch{return"none"}return n.length===0?"none":n.length>1?"ambiguous":n[0]}case"source-ref":{if(e.chain.length===0)return"none";let n=Z(e,t).filter(r=>J(e.chain,j(r)));return n.length===0?"none":n.length>1?"ambiguous":n[0]}default:return"none"}}var X="ai-annotation-markers",Q="2147483002",ee={region:"Annotations on this page",count:(e,t,n)=>{let r=e===0?"No annotations on this page":`${e} annotation${e===1?"":"s"} here, ${t} shown`;return n>0?`${r} \xB7 ${n} resolved`:r},showResolved:e=>`Show ${e} resolved annotation${e===1?"":"s"}`,hideResolved:"Hide resolved annotations",loading:"Looking for annotations\u2026",refused:e=>e==="retired-link"?"This annotation link is an older format the console no longer accepts. Open Request changes again.":e==="rate-limited"?"The console is rate-limiting this page's reads. Wait a moment.":"The console refused this page's read.",unreachable:"Could not reach the console for this page's annotations.",noCredential:"No read credential, so annotations were not requested.",asOf:e=>`status as of ${e} ago`,freshnessUnknown:"the console cannot say how fresh these statuses are",close:"Close"},K={total:0,shown:0,ambiguous:0,unresolved:0,anchored:0,resolvedHidden:0,showingResolved:!1,clusters:0,asOf:"",line:""},ne=`
2
+ :host{
3
+ --ai-bg:#fff;
4
+ --ai-fg:#18181b;
5
+ --ai-line:#d4d4d8;
6
+ --ai-line-soft:#e4e4e7;
7
+ --ai-muted:#52525b;
8
+ --ai-accent:#2563eb;
9
+ --ai-accent-line:#1d4ed8;
10
+ --ai-invert-bg:#18181b;
11
+ --ai-invert-fg:#fafafa;
12
+ --ai-space-1:4px;
13
+ --ai-space-2:6px;
14
+ --ai-space-3:8px;
15
+ --ai-space-4:10px;
16
+ --ai-space-5:12px;
17
+ --ai-radius-control:6px;
18
+ --ai-radius-card:8px;
19
+ --ai-radius-pill:999px;
20
+ --ai-pin:22px;
21
+ --ai-edge:16px;
22
+ all:initial;position:fixed;inset:0;z-index:${Q};pointer-events:none;
23
+ font:13px/1.45 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
24
+ color:var(--ai-fg);color-scheme:light}
25
+ *,*::before,*::after{box-sizing:border-box}
26
+ [hidden]{display:none!important}
27
+ .count{
28
+ position:absolute;left:var(--ai-edge);bottom:var(--ai-edge);pointer-events:auto;
29
+ background:var(--ai-invert-bg);color:var(--ai-invert-fg);
30
+ border-radius:var(--ai-radius-pill);
31
+ padding:var(--ai-space-2) var(--ai-space-5);max-width:min(60vw,420px);
32
+ display:flex;align-items:center;gap:var(--ai-space-3);flex-wrap:wrap}
33
+ .toggle{
34
+ pointer-events:auto;font:inherit;font-size:12px;white-space:nowrap;
35
+ background:var(--ai-invert-bg);color:var(--ai-invert-fg);
36
+ border:1px solid var(--ai-invert-fg);border-radius:var(--ai-radius-pill);
37
+ padding:0 var(--ai-space-3);cursor:pointer}
38
+ .toggle:focus-visible{outline:2px solid var(--ai-invert-fg);outline-offset:2px}
39
+ .pin{
40
+ position:absolute;pointer-events:auto;
41
+ width:var(--ai-pin);height:var(--ai-pin);
42
+ display:flex;align-items:center;justify-content:center;
43
+ border:1px solid var(--ai-accent-line);border-radius:var(--ai-radius-pill);
44
+ background:var(--ai-accent);color:var(--ai-invert-fg);
45
+ font:600 11px/1 ui-sans-serif,system-ui,sans-serif;cursor:pointer;padding:0}
46
+ .pin:focus-visible{outline:2px solid var(--ai-fg);outline-offset:2px}
47
+ .panel{
48
+ position:absolute;pointer-events:auto;width:min(320px,80vw);
49
+ background:var(--ai-bg);border:1px solid var(--ai-line);
50
+ border-radius:var(--ai-radius-card);padding:var(--ai-space-5);
51
+ max-height:50vh;overflow:auto}
52
+ .entry+.entry{margin-top:var(--ai-space-5);border-top:1px solid var(--ai-line-soft);
53
+ padding-top:var(--ai-space-5)}
54
+ .who{color:var(--ai-muted);font-size:12px;margin:0 0 var(--ai-space-1)}
55
+ .what{margin:0 0 var(--ai-space-2);white-space:pre-wrap;overflow-wrap:anywhere}
56
+ .state{margin:0;font-size:12px;color:var(--ai-muted)}
57
+ .fresh{margin:var(--ai-space-2) 0 0;font-size:11px;color:var(--ai-muted)}
58
+ .close{
59
+ pointer-events:auto;margin-top:var(--ai-space-4);font:inherit;
60
+ background:var(--ai-bg);color:var(--ai-fg);border:1px solid var(--ai-line);
61
+ border-radius:var(--ai-radius-control);
62
+ padding:var(--ai-space-1) var(--ai-space-4);cursor:pointer}
63
+ `,T=22,v=4,W=(e,t,n)=>Math.min(Math.max(e,t),n),G=T+2,te=128;function re(e,t,n){let{left:r,top:o}=e,i=(s,c)=>t.some(d=>Math.abs(d.left-s)<T&&Math.abs(d.top-c)<T);for(let s=0;s<te;s+=1){if(!i(r,o))return{left:r,top:o};if(o+=G,o>n&&(o=v,r-=G,r<v))break}return{left:e.left,top:e.top}}function u(e,t,n){let r=e.createElement(t);if(n)for(let o of Object.keys(n))r.setAttribute(o,n[o]??"");return r}function oe(e){let t=Math.max(0,Math.floor(e/1e3));if(t<60)return`${t}s`;let n=Math.floor(t/60);if(n<60)return`${n}m`;let r=Math.floor(n/60);if(r<24){let s=n%60;return s===0?`${r}h`:`${r}h ${s}m`}let o=Math.floor(r/24),i=r%24;return i===0?`${o}d`:`${o}d ${i}h`}function ae(e,t){let n=new Map,r=0,o=0;for(let i of e){let s=V(i.anchor,t);if(s==="ambiguous"){r+=1;continue}if(s==="none"){o+=1;continue}let c=n.get(s);c?c.push(i):n.set(s,[i])}return{clusters:[...n].map(([i,s])=>({element:i,markers:s})),ambiguous:r,unresolved:o}}function fe(e){let t={...ee,...e.labels};if(e.armed===!1)return{refresh:()=>Promise.resolve(K),owns:()=>!1,last:()=>null,destroy:()=>{}};let n=e.doc??document,r=e.win??n.defaultView??window,o=e.now??Date.now,i=u(n,"div",{id:X}),s=i.attachShadow({mode:"open"}),c=u(n,"style");c.textContent=ne,s.appendChild(c);let d=u(n,"style");s.appendChild(d);let O=u(n,"div",{class:"count",role:"status","aria-label":t.region}),k=u(n,"span",{class:"count-text"});k.textContent=t.loading,O.appendChild(k);let A=u(n,"button",{type:"button",class:"toggle",hidden:""});O.appendChild(A),s.appendChild(O);let h=u(n,"div",{class:"panel",role:"dialog","aria-modal":"false","aria-label":t.region,hidden:""});s.appendChild(h),n.body.appendChild(i);let M=null,E=[],N=!1,R=e.showResolved===!0;function _(a){if(R)A.textContent=t.hideResolved;else if(a>0)A.textContent=t.showResolved(a);else{A.setAttribute("hidden",""),A.textContent="";return}A.setAttribute("aria-pressed",R?"true":"false"),A.removeAttribute("hidden")}function P(){if(E.length===0){d.textContent="";return}let a=r.innerWidth||0,p=r.innerHeight||0,g=[],b=[];E.forEach(({element:w},l)=>{let y=w.getBoundingClientRect(),f=W(Math.round(y.right-11),v,Math.max(v,a-T-v)),m=W(Math.round(y.top-11),v,Math.max(v,p-T-v)),x=re({left:f,top:m},g,p-T-v);g.push(x),b.push(`.pin[data-i="${l}"]{left:${x.left}px;top:${x.top}px}`)}),d.textContent=b.join("")}let L=()=>{N||P()};r.addEventListener("scroll",L,!0),r.addEventListener("resize",L);function I(){for(h.setAttribute("hidden","");h.firstChild;)h.removeChild(h.firstChild)}function Y(a,p){I();for(let f of a){let m=u(n,"div",{class:"entry"}),x=u(n,"p",{class:"who"}),D=f.createdAt?new Date(f.createdAt):null;x.textContent=D!==null&&!Number.isNaN(D.getTime())?`${f.reviewer} \xB7 ${D.toISOString().slice(0,10)}`:f.reviewer,m.appendChild(x);let U=u(n,"p",{class:"what"});U.textContent=f.comment,m.appendChild(U);let z=u(n,"p",{class:"state"});z.textContent=f.status.display,m.appendChild(z),h.appendChild(m)}let g=u(n,"p",{class:"fresh"}),b=M?.asOf??"",w=b===""?NaN:Date.parse(b);g.textContent=Number.isNaN(w)?t.freshnessUnknown:t.asOf(oe(o()-w)),h.appendChild(g);let l=u(n,"button",{type:"button",class:"close"});l.textContent=t.close,l.addEventListener("click",()=>I()),h.appendChild(l);let y=p.getBoundingClientRect();d.textContent=`${d.textContent??""}.panel{left:${Math.round(Math.max(8,y.right-320))}px;top:${Math.round(y.top+20)}px}`,h.removeAttribute("hidden")}function $(a,p){for(let{pin:l}of E)l.remove();if(E=[],I(),a===null){k.textContent=p??t.unreachable,_(0);let l={...K,showingResolved:R,line:k.textContent};return M=l,P(),l}let g=ae(a.annotations,n),b=0;g.clusters.forEach((l,y)=>{let f=l.element,m=u(n,"button",{type:"button",class:"pin","data-i":String(y),"aria-label":`${l.markers.length} annotation${l.markers.length===1?"":"s"}`});m.textContent=l.markers.length===1?"1":String(l.markers.length),m.addEventListener("click",x=>{x.preventDefault(),x.stopPropagation(),Y(l.markers,f)}),s.appendChild(m),E.push({pin:m,element:f}),b+=l.markers.length}),k.textContent=a.ok||a.reason===void 0?t.count(a.total,b,a.resolvedHidden):t.refused(a.reason),_(a.resolvedHidden);let w={total:a.total,shown:b,ambiguous:g.ambiguous,unresolved:g.unresolved,anchored:a.anchored,resolvedHidden:a.resolvedHidden,showingResolved:R,clusters:g.clusters.length,asOf:a.asOf,line:k.textContent??""};return M=w,P(),w}A.addEventListener("click",a=>{a.preventDefault(),a.stopPropagation(),!N&&(R=!R,_(M?.resolvedHidden??0),k.textContent=t.loading,F.refresh())});let F={async refresh(){if(N)return K;if(e.reader===null)return $(null,t.noCredential);let a=e.path??n.location?.pathname??"/";try{let p=await e.reader.read(a,{includeResolved:R});return $(p,null)}catch{return $(null,t.unreachable)}},owns(a){if(a===i)return!0;let p=a;return typeof p?.closest=="function"?p.closest(`#${X}`)===i:!1},last:()=>M,destroy(){N=!0,r.removeEventListener("scroll",L,!0),r.removeEventListener("resize",L);for(let{pin:a}of E)a.remove();E=[],i.remove()}};return F}export{ee as DEFAULT_MARKER_LABELS,X as MARKERS_ID,oe as formatMarkerAge,re as freeSpot,fe as mountAnnotationMarkers,ae as placeMarkers};
64
+ //# sourceMappingURL=markers.mjs.map