@weatherboard/annotation 0.4.0 → 0.6.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 (60) hide show
  1. package/ADOPTING.md +97 -6
  2. package/COMPATIBILITY.md +15 -0
  3. package/README.md +33 -8
  4. package/dist/anchor.cjs +1 -1
  5. package/dist/anchor.cjs.map +2 -2
  6. package/dist/anchor.mjs +1 -1
  7. package/dist/anchor.mjs.map +2 -2
  8. package/dist/arming.d.cts +1 -3
  9. package/dist/arming.d.mts +1 -3
  10. package/dist/arming.d.ts +1 -3
  11. package/dist/bootstrap-runtime.d.cts +30 -0
  12. package/dist/bootstrap-runtime.d.mts +30 -0
  13. package/dist/bootstrap-runtime.d.ts +30 -0
  14. package/dist/bootstrap.cjs +2 -0
  15. package/dist/bootstrap.cjs.map +7 -0
  16. package/dist/bootstrap.d.cts +7 -0
  17. package/dist/bootstrap.d.mts +7 -0
  18. package/dist/bootstrap.d.ts +7 -0
  19. package/dist/bootstrap.mjs +2 -0
  20. package/dist/bootstrap.mjs.map +7 -0
  21. package/dist/host.cjs +1 -1
  22. package/dist/host.cjs.map +3 -3
  23. package/dist/host.mjs +1 -1
  24. package/dist/host.mjs.map +3 -3
  25. package/dist/index.cjs +4 -4
  26. package/dist/index.cjs.map +4 -4
  27. package/dist/index.d.cts +1 -1
  28. package/dist/index.d.mts +1 -1
  29. package/dist/index.d.ts +1 -1
  30. package/dist/index.mjs +4 -4
  31. package/dist/index.mjs.map +4 -4
  32. package/dist/markers.cjs +2 -2
  33. package/dist/markers.cjs.map +2 -2
  34. package/dist/markers.mjs +2 -2
  35. package/dist/markers.mjs.map +2 -2
  36. package/dist/protocol.cjs +1 -1
  37. package/dist/protocol.cjs.map +3 -3
  38. package/dist/protocol.d.cts +7 -0
  39. package/dist/protocol.d.mts +7 -0
  40. package/dist/protocol.d.ts +7 -0
  41. package/dist/protocol.mjs +1 -1
  42. package/dist/protocol.mjs.map +3 -3
  43. package/dist/read.cjs +1 -1
  44. package/dist/read.cjs.map +2 -2
  45. package/dist/read.mjs +1 -1
  46. package/dist/read.mjs.map +2 -2
  47. package/dist/selection.cjs +5 -0
  48. package/dist/selection.cjs.map +7 -0
  49. package/dist/selection.d.cts +27 -0
  50. package/dist/selection.d.mts +27 -0
  51. package/dist/selection.d.ts +27 -0
  52. package/dist/selection.mjs +5 -0
  53. package/dist/selection.mjs.map +7 -0
  54. package/dist/session.d.cts +10 -0
  55. package/dist/session.d.mts +10 -0
  56. package/dist/session.d.ts +10 -0
  57. package/dist/transport.d.cts +10 -0
  58. package/dist/transport.d.mts +10 -0
  59. package/dist/transport.d.ts +10 -0
  60. package/package.json +35 -10
@@ -0,0 +1,27 @@
1
+ /** Host-neutral authored-content selection. No capture token, transport or production arming. */
2
+ export interface RelativePoint {
3
+ x: number;
4
+ y: number;
5
+ width: number;
6
+ height: number;
7
+ }
8
+ export interface TextSelectionAnchor {
9
+ path: string;
10
+ start: number;
11
+ end: number;
12
+ quote: string;
13
+ prefix: string;
14
+ suffix: string;
15
+ }
16
+ export type TextPlacement = {
17
+ state: "exact";
18
+ range: Range;
19
+ } | {
20
+ state: "missing" | "ambiguous" | "stale";
21
+ reason: string;
22
+ };
23
+ export declare function relativePoint(box: Pick<DOMRect, "left" | "top" | "width" | "height">, x: number, y: number): RelativePoint | null;
24
+ export declare function captureTextSelection(scope: Element, selection: Selection | null): TextSelectionAnchor | null;
25
+ /** A snapshot resolver batches DOM traversal for all annotations in one frame. */
26
+ export declare function prepareTextSelections(scope: Element): (anchor: TextSelectionAnchor, unchanged: boolean) => TextPlacement;
27
+ export declare function resolveTextSelection(scope: Element, anchor: TextSelectionAnchor, unchanged: boolean): TextPlacement;
@@ -0,0 +1,5 @@
1
+ function A(e,t,i){return![e.left,e.top,e.width,e.height,t,i].every(Number.isFinite)||e.width<=0||e.height<=0||t<e.left||t>e.left+e.width||i<e.top||i>e.top+e.height?null:{x:(t-e.left)/e.width,y:(i-e.top)/e.height,width:e.width,height:e.height}}function M(e,t){let i=Array.from(e.querySelectorAll("[data-annotation-text]")).filter(u=>t==="slide"||u.dataset.annotationText===t);if(t!=="slide"&&i.length!==1)return null;let o=[],n="",a=null,f=0;for(let u of i){let d=e.ownerDocument.createTreeWalker(u,4),r=[],l=d.nextNode();for(;l;){if(++f>1e4)return null;l.parentElement?.closest("[data-annotation-text]")===u&&r.push(l),l=d.nextNode()}n.length&&r.length&&t==="slide"&&(n+=`
2
+ `,o.push({node:r[0],offset:0}));for(let s of r){for(let c=0;c<s.data.length;c++){let x=s.data[c];if(o.push({node:s,offset:c}),n+=x==="\r"?`
3
+ `:x,x==="\r"&&s.data[c+1]===`
4
+ `&&c++,n.length>25e4)return null}a={node:s,offset:s.data.length}}}return{text:n,positions:o,end:a}}function T(e,t,i){if(e.end?.node===t&&e.end.offset===i)return e.text.length;let o=e.positions.findIndex(n=>n.node===t&&n.offset===i);if(o>=0)return o;if(t.nodeType===3&&i===t.textContent?.length){for(let n=e.positions.length-1;n>=0;n--)if(e.positions[n]?.node===t)return n+1}return null}function P(e,t){if(!t||t.isCollapsed||t.rangeCount!==1)return null;let i=t.getRangeAt(0);if(!e.contains(i.startContainer)||!e.contains(i.endContainer))return null;let o=(c,x,g)=>{if(c.nodeType===3)return{node:c,offset:x};let m=c.childNodes[g?x-1:x];if(!m)return null;let h=m;for(;g?h.lastChild:h.firstChild;)h=g?h.lastChild:h.firstChild;return h.nodeType===3?{node:h,offset:g?h.textContent.length:0}:null},n=o(i.startContainer,i.startOffset,!1),a=o(i.endContainer,i.endOffset,!0);if(!n||!a)return null;let f=n.node.parentElement?.closest("[data-annotation-text]"),u=a.node.parentElement?.closest("[data-annotation-text]");if(!f||!u)return null;let d=f===u?f.dataset.annotationText:"slide",r=M(e,d);if(!r)return null;let l=T(r,n.node,n.offset),s=T(r,a.node,a.offset);return l===null||s===null||l>=s||s-l>1024?null:{path:d,start:l,end:s,quote:r.text.slice(l,s),prefix:r.text.slice(Math.max(0,l-128),l),suffix:r.text.slice(s,s+128)}}function p(e,t,i,o){let n=t.positions[i],a=o===t.text.length?t.end:t.positions[o];if(!n||!a)return null;let f=e.ownerDocument.createRange();return f.setStart(n.node,n.offset),f.setEnd(a.node,a.offset),f}function E(e){let t=new Map,i=n=>(t.has(n)||t.set(n,M(e,n)),t.get(n)??null),o=Array.from(new Set(Array.from(e.querySelectorAll("[data-annotation-text]"),n=>n.dataset.annotationText)));return(n,a)=>w(e,n,a,i,o)}function S(e,t,i){return E(e)(t,i)}function w(e,t,i,o,n){if(!t.quote.length)return{state:"missing",reason:"No selected text was recorded."};let a=o(t.path);if(i&&a?.text.slice(t.start,t.end)===t.quote){let d=p(e,a,t.start,t.end);if(d)return{state:"exact",range:d}}let f=t.path==="slide"?["slide"]:n,u=[];for(let d of f){let r=o(d);if(!r)continue;let l=r.text.indexOf(t.quote);for(;l>=0;){if(r.text.slice(Math.max(0,l-t.prefix.length),l)===t.prefix&&r.text.slice(l+t.quote.length,l+t.quote.length+t.suffix.length)===t.suffix){let s=p(e,r,l,l+t.quote.length);if(s&&u.push(s),u.length>1)return{state:"ambiguous",reason:"This text matches more than one location."}}l=r.text.indexOf(t.quote,l+1)}}return u[0]?{state:"exact",range:u[0]}:{state:"stale",reason:"The selected text has changed. The original quote is retained."}}export{P as captureTextSelection,E as prepareTextSelections,A as relativePoint,S as resolveTextSelection};
5
+ //# sourceMappingURL=selection.mjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/selection.ts"],
4
+ "sourcesContent": ["/** Host-neutral authored-content selection. No capture token, transport or production arming. */\nexport interface RelativePoint { x: number; y: number; width: number; height: number }\nexport interface TextSelectionAnchor {\n path: string; start: number; end: number; quote: string; prefix: string; suffix: string;\n}\nexport type TextPlacement =\n | { state: \"exact\"; range: Range }\n | { state: \"missing\" | \"ambiguous\" | \"stale\"; reason: string };\ninterface Position { node: Text; offset: number }\ninterface TextMap { text: string; positions: Position[]; end: Position | null }\nconst MAX_NODES = 10000;\nconst MAX_TEXT = 250000;\n\nexport function relativePoint(box: Pick<DOMRect, \"left\" | \"top\" | \"width\" | \"height\">, x: number, y: number): RelativePoint | null {\n if (![box.left, box.top, box.width, box.height, x, y].every(Number.isFinite) || box.width <= 0 || box.height <= 0 ||\n x < box.left || x > box.left + box.width || y < box.top || y > box.top + box.height) return null;\n return { x: (x - box.left) / box.width, y: (y - box.top) / box.height, width: box.width, height: box.height };\n}\n\n/** Maps original UTF-16 offsets, folding CRLF as HTML does. Newlines separate authored fields. */\nfunction textMap(scope: Element, path: string): TextMap | null {\n const fields = Array.from(scope.querySelectorAll<HTMLElement>(\"[data-annotation-text]\"))\n .filter((field) => path === \"slide\" || field.dataset.annotationText === path);\n if (path !== \"slide\" && fields.length !== 1) return null;\n const positions: Position[] = [];\n let text = \"\";\n let end: Position | null = null;\n let nodes = 0;\n for (const field of fields) {\n const walker = scope.ownerDocument.createTreeWalker(field, 4);\n const fieldNodes: Text[] = [];\n let next = walker.nextNode();\n while (next) {\n if (++nodes > MAX_NODES) return null;\n if (next.parentElement?.closest(\"[data-annotation-text]\") === field) fieldNodes.push(next as Text);\n next = walker.nextNode();\n }\n if (text.length && fieldNodes.length && path === \"slide\") {\n text += \"\\n\";\n positions.push({ node: fieldNodes[0]!, offset: 0 });\n }\n for (const node of fieldNodes) {\n for (let offset = 0; offset < node.data.length; offset++) {\n const character = node.data[offset]!;\n positions.push({ node, offset });\n text += character === \"\\r\" ? \"\\n\" : character;\n if (character === \"\\r\" && node.data[offset + 1] === \"\\n\") offset++;\n if (text.length > MAX_TEXT) return null;\n }\n end = { node, offset: node.data.length };\n }\n }\n return { text, positions, end };\n}\nfunction offsetAt(map: TextMap, node: Node, offset: number): number | null {\n if (map.end?.node === node && map.end.offset === offset) return map.text.length;\n const found = map.positions.findIndex((position) => position.node === node && position.offset === offset);\n if (found >= 0) return found;\n // Selection can end exactly after a text node before the next field.\n if (node.nodeType === 3 && offset === node.textContent?.length) {\n for (let i = map.positions.length - 1; i >= 0; i--) if (map.positions[i]?.node === node) return i + 1;\n }\n return null;\n}\n\nexport function captureTextSelection(scope: Element, selection: Selection | null): TextSelectionAnchor | null {\n if (!selection || selection.isCollapsed || selection.rangeCount !== 1) return null;\n const range = selection.getRangeAt(0);\n if (!scope.contains(range.startContainer) || !scope.contains(range.endContainer)) return null;\n // Browser selection may use element boundaries (select-all, keyboard), not\n // just text nodes. Resolve those boundaries to the authored text they enclose.\n const boundary = (node: Node, offset: number, ending: boolean): Position | null => {\n if (node.nodeType === 3) return { node: node as Text, offset };\n const child = node.childNodes[ending ? offset - 1 : offset];\n if (!child) return null;\n let leaf: Node = child;\n while (ending ? leaf.lastChild : leaf.firstChild) leaf = (ending ? leaf.lastChild : leaf.firstChild)!;\n return leaf.nodeType === 3 ? { node: leaf as Text, offset: ending ? leaf.textContent!.length : 0 } : null;\n };\n const a = boundary(range.startContainer, range.startOffset, false);\n const b = boundary(range.endContainer, range.endOffset, true);\n if (!a || !b) return null;\n const first = a.node.parentElement?.closest<HTMLElement>(\"[data-annotation-text]\");\n const last = b.node.parentElement?.closest<HTMLElement>(\"[data-annotation-text]\");\n if (!first || !last) return null;\n const path = first === last ? first.dataset.annotationText! : \"slide\";\n const map = textMap(scope, path);\n if (!map) return null;\n const start = offsetAt(map, a.node, a.offset);\n const end = offsetAt(map, b.node, b.offset);\n if (start === null || end === null || start >= end || end - start > 1024) return null;\n return { path, start, end, quote: map.text.slice(start, end),\n prefix: map.text.slice(Math.max(0, start - 128), start), suffix: map.text.slice(end, end + 128) };\n}\n\nfunction rangeAt(scope: Element, map: TextMap, start: number, end: number): Range | null {\n const a = map.positions[start];\n const b = end === map.text.length ? map.end : map.positions[end];\n if (!a || !b) return null;\n const range = scope.ownerDocument.createRange();\n range.setStart(a.node, a.offset);\n range.setEnd(b.node, b.offset);\n return range;\n}\n/** A snapshot resolver batches DOM traversal for all annotations in one frame. */\nexport function prepareTextSelections(scope: Element): (anchor: TextSelectionAnchor, unchanged: boolean) => TextPlacement {\n const maps = new Map<string, TextMap | null>();\n const getMap = (path: string) => {\n if (!maps.has(path)) maps.set(path, textMap(scope, path));\n return maps.get(path) ?? null;\n };\n const authoredPaths = Array.from(new Set(Array.from(scope.querySelectorAll<HTMLElement>(\"[data-annotation-text]\"), field => field.dataset.annotationText!)));\n return (anchor, unchanged) => resolvePrepared(scope, anchor, unchanged, getMap, authoredPaths);\n}\nexport function resolveTextSelection(scope: Element, anchor: TextSelectionAnchor, unchanged: boolean): TextPlacement {\n return prepareTextSelections(scope)(anchor, unchanged);\n}\nfunction resolvePrepared(scope: Element, anchor: TextSelectionAnchor, unchanged: boolean, getMap: (path: string) => TextMap | null, authoredPaths: string[]): TextPlacement {\n if (!anchor.quote.length) return { state: \"missing\", reason: \"No selected text was recorded.\" };\n const original = getMap(anchor.path);\n if (unchanged && original?.text.slice(anchor.start, anchor.end) === anchor.quote) {\n const range = rangeAt(scope, original, anchor.start, anchor.end);\n if (range) return { state: \"exact\", range };\n }\n // Positional field paths are valid only at the captured revision. Search all\n // authored fields after edits; two identical matches must remain ambiguous.\n const paths = anchor.path === \"slide\" ? [\"slide\"] : authoredPaths;\n const matches: Range[] = [];\n for (const path of paths) {\n const map = getMap(path);\n if (!map) continue;\n let index = map.text.indexOf(anchor.quote);\n while (index >= 0) {\n if (map.text.slice(Math.max(0, index - anchor.prefix.length), index) === anchor.prefix &&\n map.text.slice(index + anchor.quote.length, index + anchor.quote.length + anchor.suffix.length) === anchor.suffix) {\n const range = rangeAt(scope, map, index, index + anchor.quote.length);\n if (range) matches.push(range);\n if (matches.length > 1) return { state: \"ambiguous\", reason: \"This text matches more than one location.\" };\n }\n index = map.text.indexOf(anchor.quote, index + 1);\n }\n }\n if (matches[0]) return { state: \"exact\", range: matches[0] };\n return { state: \"stale\", reason: \"The selected text has changed. The original quote is retained.\" };\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["relativePoint", "box", "x", "y", "textMap", "scope", "path", "fields", "field", "positions", "text", "end", "nodes", "walker", "fieldNodes", "next", "node", "offset", "character", "offsetAt", "map", "found", "position", "i", "captureTextSelection", "selection", "range", "boundary", "ending", "child", "leaf", "a", "b", "first", "last", "start", "rangeAt", "prepareTextSelections", "maps", "getMap", "authoredPaths", "anchor", "unchanged", "resolvePrepared", "resolveTextSelection", "original", "paths", "matches", "index"]
7
+ }
@@ -0,0 +1,10 @@
1
+ /** Per-tab state. None of these values are cookies or sent by the browser. */
2
+ export declare const CAPTURE_TOKEN_STORAGE_KEY = "ai.annotation.capture";
3
+ export declare const READ_TOKEN_STORAGE_KEY = "ai.annotation.read";
4
+ export declare const REVIEW_RUN_STORAGE_KEY = "ai.annotation.run";
5
+ /** A credential-free record that an annotation deep link was opened. */
6
+ export declare const ANNOTATION_ATTEMPT_STORAGE_KEY = "ai.annotation.attempt";
7
+ export declare const ANNOTATION_ATTEMPT_GLOBAL = "__weatherboardAnnotationAttemptV1";
8
+ export declare const ANNOTATION_STARTUP_STATUS_ID = "ai-annotation-startup-status";
9
+ /** Small enough to remain in the root entry point for Done's full teardown. */
10
+ export declare function clearAnnotationBootstrap(win: Window | null, storage: Pick<Storage, "removeItem"> | null, doc?: Document): void;
@@ -0,0 +1,10 @@
1
+ /** Per-tab state. None of these values are cookies or sent by the browser. */
2
+ export declare const CAPTURE_TOKEN_STORAGE_KEY = "ai.annotation.capture";
3
+ export declare const READ_TOKEN_STORAGE_KEY = "ai.annotation.read";
4
+ export declare const REVIEW_RUN_STORAGE_KEY = "ai.annotation.run";
5
+ /** A credential-free record that an annotation deep link was opened. */
6
+ export declare const ANNOTATION_ATTEMPT_STORAGE_KEY = "ai.annotation.attempt";
7
+ export declare const ANNOTATION_ATTEMPT_GLOBAL = "__weatherboardAnnotationAttemptV1";
8
+ export declare const ANNOTATION_STARTUP_STATUS_ID = "ai-annotation-startup-status";
9
+ /** Small enough to remain in the root entry point for Done's full teardown. */
10
+ export declare function clearAnnotationBootstrap(win: Window | null, storage: Pick<Storage, "removeItem"> | null, doc?: Document): void;
@@ -0,0 +1,10 @@
1
+ /** Per-tab state. None of these values are cookies or sent by the browser. */
2
+ export declare const CAPTURE_TOKEN_STORAGE_KEY = "ai.annotation.capture";
3
+ export declare const READ_TOKEN_STORAGE_KEY = "ai.annotation.read";
4
+ export declare const REVIEW_RUN_STORAGE_KEY = "ai.annotation.run";
5
+ /** A credential-free record that an annotation deep link was opened. */
6
+ export declare const ANNOTATION_ATTEMPT_STORAGE_KEY = "ai.annotation.attempt";
7
+ export declare const ANNOTATION_ATTEMPT_GLOBAL = "__weatherboardAnnotationAttemptV1";
8
+ export declare const ANNOTATION_STARTUP_STATUS_ID = "ai-annotation-startup-status";
9
+ /** Small enough to remain in the root entry point for Done's full teardown. */
10
+ export declare function clearAnnotationBootstrap(win: Window | null, storage: Pick<Storage, "removeItem"> | null, doc?: Document): void;
@@ -49,6 +49,11 @@ export type FrameMessageKind = FrameToParent["type"];
49
49
  * successful send and never a failed one.
50
50
  */
51
51
  export interface SendReceipt {
52
+ /**
53
+ * Durable console status for this capture's workflow. This is an opaque URL
54
+ * supplied by the console, not a client-derived run identifier.
55
+ */
56
+ progressUrl?: string;
52
57
  /**
53
58
  * The issue this capture became, when the far end names one.
54
59
  *
@@ -57,6 +62,11 @@ export interface SendReceipt {
57
62
  * crossing the network boundary is put through before it gets to be one.
58
63
  */
59
64
  issueUrl?: string;
65
+ saved?: {
66
+ feedbackId: string;
67
+ recoveryUrl?: string;
68
+ message: string;
69
+ };
60
70
  }
61
71
  /**
62
72
  * A URL from the network that is safe to put in somebody else's page (A-240).
@@ -49,6 +49,11 @@ export type FrameMessageKind = FrameToParent["type"];
49
49
  * successful send and never a failed one.
50
50
  */
51
51
  export interface SendReceipt {
52
+ /**
53
+ * Durable console status for this capture's workflow. This is an opaque URL
54
+ * supplied by the console, not a client-derived run identifier.
55
+ */
56
+ progressUrl?: string;
52
57
  /**
53
58
  * The issue this capture became, when the far end names one.
54
59
  *
@@ -57,6 +62,11 @@ export interface SendReceipt {
57
62
  * crossing the network boundary is put through before it gets to be one.
58
63
  */
59
64
  issueUrl?: string;
65
+ saved?: {
66
+ feedbackId: string;
67
+ recoveryUrl?: string;
68
+ message: string;
69
+ };
60
70
  }
61
71
  /**
62
72
  * A URL from the network that is safe to put in somebody else's page (A-240).
@@ -49,6 +49,11 @@ export type FrameMessageKind = FrameToParent["type"];
49
49
  * successful send and never a failed one.
50
50
  */
51
51
  export interface SendReceipt {
52
+ /**
53
+ * Durable console status for this capture's workflow. This is an opaque URL
54
+ * supplied by the console, not a client-derived run identifier.
55
+ */
56
+ progressUrl?: string;
52
57
  /**
53
58
  * The issue this capture became, when the far end names one.
54
59
  *
@@ -57,6 +62,11 @@ export interface SendReceipt {
57
62
  * crossing the network boundary is put through before it gets to be one.
58
63
  */
59
64
  issueUrl?: string;
65
+ saved?: {
66
+ feedbackId: string;
67
+ recoveryUrl?: string;
68
+ message: string;
69
+ };
60
70
  }
61
71
  /**
62
72
  * A URL from the network that is safe to put in somebody else's page (A-240).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weatherboard/annotation",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Click an element in a running app, get the source location that rendered it.",
5
5
  "keywords": [
6
6
  "annotation",
@@ -12,7 +12,7 @@
12
12
  "license": "MIT",
13
13
  "repository": {
14
14
  "type": "git",
15
- "url": "git+https://github.com/Another-Iteration/another-iteration.git",
15
+ "url": "git+https://github.com/Weatherboard-Studio/weatherboard-studio.git",
16
16
  "directory": "packages/annotation"
17
17
  },
18
18
  "type": "module",
@@ -29,6 +29,16 @@
29
29
  "default": "./dist/index.cjs"
30
30
  }
31
31
  },
32
+ "./bootstrap": {
33
+ "import": {
34
+ "types": "./dist/bootstrap.d.mts",
35
+ "default": "./dist/bootstrap.mjs"
36
+ },
37
+ "require": {
38
+ "types": "./dist/bootstrap.d.cts",
39
+ "default": "./dist/bootstrap.cjs"
40
+ }
41
+ },
32
42
  "./protocol": {
33
43
  "import": {
34
44
  "types": "./dist/protocol.d.mts",
@@ -79,7 +89,17 @@
79
89
  "default": "./dist/markers.cjs"
80
90
  }
81
91
  },
82
- "./package.json": "./package.json"
92
+ "./package.json": "./package.json",
93
+ "./selection": {
94
+ "import": {
95
+ "types": "./dist/selection.d.mts",
96
+ "default": "./dist/selection.mjs"
97
+ },
98
+ "require": {
99
+ "types": "./dist/selection.d.cts",
100
+ "default": "./dist/selection.cjs"
101
+ }
102
+ }
83
103
  },
84
104
  "main": "./dist/index.cjs",
85
105
  "types": "./dist/index.d.ts",
@@ -98,14 +118,19 @@
98
118
  ],
99
119
  "//sizeBudget": "Gzipped bytes per published entry point, checked by `build.mjs` on every build and therefore in CI. A budget is here rather than in a separate config because the number is part of the package's contract: this code runs on every page of every managed product, and 'it grew a bit each time' is how a 4KB SDK becomes a 40KB one with nobody having decided. Raising a number is allowed; doing it in the same commit as the growth, where a reviewer sees both, is the point.",
100
120
  "//sizeBudget-anchor": "A-275, and the reason it is a FOURTH ENTRY POINT rather than a re-export from `.`: the marker layer (A-277) runs on the product page and needs `resolveAnchor`, but the SDK that only CAPTURES does not, and folding it into index would put it in the bundle of every managed product whether or not that product renders markers. THE NUMBER HERE WAS WRONG ONCE AND THAT IS WHY IT IS SPELLED OUT: the first commit of this note said 961, measured by bundling `src/anchor.ts` alone with esbuild, and `build.mjs` printed 996 for the same commit - the standalone bundle and the real build are not the same measurement, and a stated figure that disagrees with the gate is worse than no figure. MEASURED BY RUNNING `node build.mjs` AND READING ITS OUTPUT, which is the only number the budget is ever compared against: 1036 as shipped, after A-275's review cycle added `RouteAnchor.path` and the path check. Budget 1100, so headroom is 1100/1036 = 6.2%. Deliberately not raised to restore the slack the review spent: this entry point exists to grow when A-277 needs it to, and the budget's job is to make that a decision somebody makes. Looser in PROPORTION than index (0.83%) for the reason protocol is: 64 bytes is 6% here and would be 0.7% of index.",
101
- "//sizeBudget-index": "A-241 adds arming: 8700 -> 9400 for index and 700 -> 850 for protocol. MEASURED by rebuilding, four numbers. Baseline at A-240's merge commit fceb183: index 8562, protocol 646 - the two figures the previous note recorded, re-measured rather than trusted. As shipped: index 9281, protocol 788. And with the ./arming re-export block deleted from src/index.ts but everything else present: 8913. SO readArming AND ITS TYPES ARE 368 OF THE 719, AND 351 ARE NOT: 142 of that is protocol's own growth (armedCookie, clearedCookie, ANNOTATION_ARMED_VALUE, REVIEW_RUN_PARAM), which index pays too because it re-exports protocol whole, and the rest is isAnnotatableBuild with its ANNOTATABLE_ENVS allowlist, the two-gate expression in mountAnnotation, and Done's teardown. That 351 is the number worth knowing: a consumer who never calls readArming still pays it, because the kill-switch is not optional and must not be tree-shakeable. TWO REVIEW CYCLES MOVED IT AND BOTH COSTS ARE MEASURED: the fail-closed allowlist +27 (9239 -> 9266) and Done's teardown +15 (9266 -> 9281). HEADROOM IS PER ENTRY POINT AND THE TWO ARE STATED SEPARATELY, because one figure for both was false here once already: index 9400/9281 = 1.28%, against A-240's 8700/8562 = 1.61%, so tighter and DELIBERATELY NOT TOPPED UP - the budget's job is to make the next growth a decision, and quietly restoring slack each time is how it stops doing that; protocol 850/788 = 7.87%, against its previous 700/646 = 8.36%. protocol runs looser in PROPORTION only because it is small - 62 bytes is 7.87% there and would be 0.67% of index. The argument for the toolbar's own 2553 bytes is in docs/decisions/a-240-annotation-toolbar-size.md, extended there for this slice rather than restated here. A-254 MOVED IT AGAIN AND THE BUDGET IS DELIBERATELY NOT RAISED: index 9281 -> 9300, +19 gzipped bytes for the armed-before-persist early return in readArming, re-measured by running build.mjs before and after. Headroom is now 9400/9300 = 1.06%. That is tight on purpose - 19 bytes bought the end of an unarmed page storing an attacker-supplied capture token, and the next growth after it should be a decision somebody makes rather than slack somebody spends. A-277 MOVED IT AGAIN, TWICE, AND BOTH ARE MEASURED BY REBUILDING: index 9471 -> 9531, +60 gzipped bytes, and protocol 788 -> 800, +12. The 12 is READ_TOKEN_PARAM and its comment-free constant; the remaining 48 in index is arming carrying the read token - a fourth stripped parameter, a third storage key, a `readToken` field on `Arming`, and the read/write branches for it in `readArming`. THAT 48 IS PAID BY EVERY CONSUMER INCLUDING ONES THAT NEVER MOUNT A MARKER, and it is paid deliberately rather than moved into ./markers: the strip is the thing that gets a signed credential out of an address bar, it has to run in the first milliseconds of the page, and a product that mounts only the toolbar still receives an `ai_read` in its URL and still must not leave it there. Budgets 9550 -> 9650 and 850 -> 870. 9650 rather than 9600: at 9600 the headroom is 69 bytes, which is a budget that fails on the next sentence somebody writes rather than on the next decision somebody makes. 9650/9531 = 1.25% and 870/800 = 8.75%, so index is LOOSER than A-285 left it (0.83%) and that is stated rather than hidden - the slack was restored on purpose this once, because A-279 (replies) is the next slice into this file and a budget with 19 bytes in it makes that slice's first commit a budget argument instead of a design one. A-285 MOVED IT AGAIN AND THE BUDGET IS RAISED, WITH THE COST SPLIT AND MEASURED: index 9300 -> 9471, +171 gzipped bytes, by rebuilding three times. 9300 is the baseline at this branch point, re-measured rather than trusted. 9426 with transport.ts alone - safeRefusalReason, the IngestRefusal fields and the body read on the refusal branch, 126 bytes. 9471 with the toolbar rendering it and index re-exporting the two functions, a further 45. THAT 171 IS WHAT IT COSTS FOR A REFUSAL TO REACH A HUMAN AT ALL: before it, every sentence the ingest composed - the link issued for another deployment, the empty comment - was thrown away by the only client that could show it, and every failure read 'That did not send'. 9550 rather than 9500: 9500 would leave 29 bytes, which is a budget that fails on the next comment somebody writes rather than on the next decision somebody makes. 9550/9471 = 0.83%, against A-254's 9400/9300 = 1.06% - so still tighter than it was, and deliberately not topped up.",
121
+ "//sizeBudget-index": "A-385: the persistent progress receipt link measures 9919 gzip bytes under Node 22.19.0 (CI agrees), versus 9892 under Node 26.7.0. Budget 9900 -> 9950 accepts the 19-byte CI overage and leaves 31 bytes (0.31%) of headroom. Bounded receipt compaction did not improve the gzip measurement; keep the clear validation and receipt shape rather than distort them for compression. A-383: durable saved-capture receipt and persistent recovery link increase index to 9806 gzip bytes as measured by pnpm build; budget 9700 -> 9900. A-241 adds arming: 8700 -> 9400 for index and 700 -> 850 for protocol. MEASURED by rebuilding, four numbers. Baseline at A-240's merge commit fceb183: index 8562, protocol 646 - the two figures the previous note recorded, re-measured rather than trusted. As shipped: index 9281, protocol 788. And with the ./arming re-export block deleted from src/index.ts but everything else present: 8913. SO readArming AND ITS TYPES ARE 368 OF THE 719, AND 351 ARE NOT: 142 of that is protocol's own growth (armedCookie, clearedCookie, ANNOTATION_ARMED_VALUE, REVIEW_RUN_PARAM), which index pays too because it re-exports protocol whole, and the rest is isAnnotatableBuild with its ANNOTATABLE_ENVS allowlist, the two-gate expression in mountAnnotation, and Done's teardown. That 351 is the number worth knowing: a consumer who never calls readArming still pays it, because the kill-switch is not optional and must not be tree-shakeable. TWO REVIEW CYCLES MOVED IT AND BOTH COSTS ARE MEASURED: the fail-closed allowlist +27 (9239 -> 9266) and Done's teardown +15 (9266 -> 9281). HEADROOM IS PER ENTRY POINT AND THE TWO ARE STATED SEPARATELY, because one figure for both was false here once already: index 9400/9281 = 1.28%, against A-240's 8700/8562 = 1.61%, so tighter and DELIBERATELY NOT TOPPED UP - the budget's job is to make the next growth a decision, and quietly restoring slack each time is how it stops doing that; protocol 850/788 = 7.87%, against its previous 700/646 = 8.36%. protocol runs looser in PROPORTION only because it is small - 62 bytes is 7.87% there and would be 0.67% of index. The argument for the toolbar's own 2553 bytes is in docs/decisions/a-240-annotation-toolbar-size.md, extended there for this slice rather than restated here. A-254 MOVED IT AGAIN AND THE BUDGET IS DELIBERATELY NOT RAISED: index 9281 -> 9300, +19 gzipped bytes for the armed-before-persist early return in readArming, re-measured by running build.mjs before and after. Headroom is now 9400/9300 = 1.06%. That is tight on purpose - 19 bytes bought the end of an unarmed page storing an attacker-supplied capture token, and the next growth after it should be a decision somebody makes rather than slack somebody spends. A-277 MOVED IT AGAIN, TWICE, AND BOTH ARE MEASURED BY REBUILDING: index 9471 -> 9531, +60 gzipped bytes, and protocol 788 -> 800, +12. The 12 is READ_TOKEN_PARAM and its comment-free constant; the remaining 48 in index is arming carrying the read token - a fourth stripped parameter, a third storage key, a `readToken` field on `Arming`, and the read/write branches for it in `readArming`. THAT 48 IS PAID BY EVERY CONSUMER INCLUDING ONES THAT NEVER MOUNT A MARKER, and it is paid deliberately rather than moved into ./markers: the strip is the thing that gets a signed credential out of an address bar, it has to run in the first milliseconds of the page, and a product that mounts only the toolbar still receives an `ai_read` in its URL and still must not leave it there. Budgets 9550 -> 9650 and 850 -> 870. 9650 rather than 9600: at 9600 the headroom is 69 bytes, which is a budget that fails on the next sentence somebody writes rather than on the next decision somebody makes. 9650/9531 = 1.25% and 870/800 = 8.75%, so index is LOOSER than A-285 left it (0.83%) and that is stated rather than hidden - the slack was restored on purpose this once, because A-279 (replies) is the next slice into this file and a budget with 19 bytes in it makes that slice's first commit a budget argument instead of a design one. A-285 MOVED IT AGAIN AND THE BUDGET IS RAISED, WITH THE COST SPLIT AND MEASURED: index 9300 -> 9471, +171 gzipped bytes, by rebuilding three times. 9300 is the baseline at this branch point, re-measured rather than trusted. 9426 with transport.ts alone - safeRefusalReason, the IngestRefusal fields and the body read on the refusal branch, 126 bytes. 9471 with the toolbar rendering it and index re-exporting the two functions, a further 45. THAT 171 IS WHAT IT COSTS FOR A REFUSAL TO REACH A HUMAN AT ALL: before it, every sentence the ingest composed - the link issued for another deployment, the empty comment - was thrown away by the only client that could show it, and every failure read 'That did not send'. 9550 rather than 9500: 9500 would leave 29 bytes, which is a budget that fails on the next comment somebody writes rather than on the next decision somebody makes. 9550/9471 = 0.83%, against A-254's 9400/9300 = 1.06% - so still tighter than it was, and deliberately not topped up.",
122
+ "//sizeBudget-host": "A-359, and the first note this entry point has had. MEASURED BY RUNNING `node build.mjs` AND READING ITS OUTPUT, three entry points, before and after, on this branch rather than trusted from the last note: host 566 -> 649 (+83), protocol 800 -> 869 (+69), index 9531 -> 9599 (+68). WHAT THE BYTES ARE: `unhandledMessage` in protocol.ts, which is the whole of protocol's 69 and is re-exported into index for index's 68; host pays that 69 plus 14 for its own `default` branch. The `const unhandled: never` assertions cost NOTHING - they are erased at build, which is the good half of this trade. WHAT THEY BUY: host.ts and sdk.ts each dispatched on a discriminated union with no `default`, so a message type added to `ParentToFrame` or `FrameToParent` was DROPPED by whichever half had not been taught about it, with no error anywhere. Measured before the fix: adding a member to each union and running `tsc --noEmit` gave exit 0 and zero errors. These two files are the only things in the world that can disagree about what a message on this wire means, and they deploy from different repositories. THE FIRST DRAFT OF THE SENTENCE COST 230 CHARACTERS AND PUT ALL THREE ENTRY POINTS OVER AT ONCE - index 9717/9650, protocol 991/870, host 771/660 - which is the gate working, and is why the shipped string is one line: which half, what arrived, and that nothing was done with it. Nothing beyond that survived the budget, and nothing beyond that is load-bearing. BUDGETS RAISED: protocol 870 -> 890, index 9650 -> 9700, host 660 -> 680. Every one of the three was ALREADY UNDER its old budget after the string was trimmed - 869/870, 9599/9650 and 649/660 - so this is not a raise to make a failure go away, and the numbers above are the gate's own output at both ends. It is a raise because 1, 51 and 11 bytes of headroom is the state the A-277 note names as the thing to avoid: 'a budget that fails on the next sentence somebody writes rather than on the next decision somebody makes'. Protocol at one byte would have failed the next contributor over a comma. New headroom: protocol 890/869 = 2.4%, host 680/649 = 4.8%, index 9700/9599 = 1.05% - index tightest in proportion for the reason every note here gives, that a percentage of index is a lot of bytes, and the two small entry points looser for the reason protocol has always run looser, that 20 bytes is 2% there and would be 0.2% of index.",
123
+ "//sizeBudget-host-a359-cycle2": "The guard on `unhandledMessage`'s stringify, and this raise IS forced by the gate rather than headroom-tidying like the one above it. MEASURED BY RUNNING `node build.mjs`, every figure its own output: protocol 869 -> 908 (+39), host 649 -> 686 (+37), index 9599 -> 9615 (+16). protocol and host both went OVER their A-359 budgets (908/890, 686/680) and are raised to 940 and 720; index needs nothing and is NOT raised, so it stays at 9700 with 9615 against it. WHAT THE BYTES BUY: `unhandledMessage` interpolated `JSON.stringify(message)` unguarded, and postMessage serialises with STRUCTURED CLONE, which PRESERVES CYCLES where JSON cannot represent them - so a circular `event.data` made the function throw `Converting circular structure to JSON` from inside a `message` listener on a page this SDK is a guest on, once per message, at the sender's discretion. That is the exact outcome the forty-line comment on that function argues against; the implementation defeated its own argument. Both the circular case and a throwing-getter Proxy were confirmed RED against the previous implementation before the fix and are pinned in exhaustive.test.ts. THE EXPENSIVE ALTERNATIVE WAS TRIED AND MEASURED RATHER THAN REASONED ABOUT: reusing `describeArg` from state.ts - which solves the same problem and whose own comment names the same hazard - costs protocol 869 -> 1008 (+139) and host 649 -> 793 (+144), a hundred bytes per entry point more than the local version, because describeArg is built for console ARGUMENTS (strings, Errors, a prototype walk) and almost none of it is reachable for a wire message. It was reverted for a second reason that would stand even at equal cost: `protocol.ts` currently has ZERO imports, `state.ts` imports FROM it, and importing back would make the pure wire contract depend on the console-patching state collector and close a module cycle that is invisible only because state.ts's side is `import type`. The cost of that choice is two safe-stringify helpers in this package instead of one; they cannot drift in a way that matters, each being a total function with one behaviour and its own test, and the note on `safeJson` says what to do if a third ever appears. New headroom: protocol 940/908 = 3.5%, host 720/686 = 5.0%, index 9700/9615 = 0.88%.",
124
+ "//sizeBudget-a1375": "A-1375 adds an eighth, deliberately small ./bootstrap entry point for the synchronous pre-hydration script, arming reads and token-free startup status. It is separate so an ordinary visitor does not download the toolbar, state collector or transport before the page knows it is armed. REPRODUCED WITH NODE 22.19.0 by running `pnpm --filter @weatherboard/annotation build` on the unmodified origin/main baseline 4d1df98e and on this source, then reading build.mjs's ESM gzip rows: index 9919 -> 10082 (+163), protocol 908 -> 967 (+59), and bootstrap absent (0 emitted bytes) -> 2321 (+2321). The index increase pays for the hosted-mode constants re-export and Done clearing shared bootstrap state; protocol pays for the two token-free capability constants; bootstrap contains the new early runtime itself. The index budget moves 9950 -> 10150 (+200, 2.01%) and leaves 68 bytes, 0.67% of its current artifact. The protocol budget moves 940 -> 1000 (+60, 6.38%) and leaves 33 bytes, 3.41% of its current artifact, effectively preserving its previous 32-byte allowance. The new bootstrap budget is 2350 and leaves 29 bytes, 1.25% of its current artifact. Bootstrap is deliberately tighter than protocol because it runs synchronously in every adopting document; all three limits remain close enough that the next material feature requires an explicit increase rather than spending unrecorded slack.",
102
125
  "sizeBudget": {
103
- "protocol": 870,
104
- "index": 9650,
105
- "host": 660,
126
+ "bootstrap": 2350,
127
+ "protocol": 1000,
128
+ "index": 10150,
129
+ "host": 720,
106
130
  "anchor": 1100,
107
131
  "read": 1700,
108
- "markers": 4250
132
+ "markers": 4250,
133
+ "selection": 3500
109
134
  },
110
135
  "engines": {
111
136
  "node": ">=18"
@@ -122,9 +147,9 @@
122
147
  "//sizeBudget-read": "A-276, and a FIFTH ENTRY POINT for ./anchor reason: the read client runs only in a product that RENDERS markers, and folding it into index would tax every page of every managed product with code most of them never call. MEASURED BY RUNNING node build.mjs AND READING ITS OUTPUT, which is the only number the budget is ever compared against: 1597 as shipped. Budget 1700, so headroom is 1700/1597 = 6.4% - deliberately in line with anchor 1100/1036 = 6.2% rather than tighter, because A-277 is the slice that will grow this one and the budget job is to make that growth a decision somebody makes. It is looser in PROPORTION than index 0.83% for the reason protocol is: 100 bytes is 6% here and would be 1% of index. What is IN it: the envelope and marker types are erased at build, so the bytes are parseAnnotationsResponse and its three shape checks (the anchor union, the marker, the counts), createAnnotationReader, and assertConcreteEndpoint pulled in from transport.ts by tree-shaking. The sanitising is most of it and is not optional - comment and reviewer are rendered inside the customers own page, and safeIssueUrl header has the argument. A-323 MOVED IT AND THE BUDGET IS DELIBERATELY NOT RAISED: read 1593 -> 1652, +59 gzipped bytes, re-measured by running build.mjs before and after. What the 59 buys is the ONLY way an SDK can see a finished capture: the resolved=include search param behind an includeResolved option, the RESOLVED_INCLUDE constant, and resolvedHidden on the parser. The constant is not an indulgence - the console compares params.get('resolved') === 'include' and its own tests pin that true, 1, yes, Include, INCLUDE and a trailing space all get the DEFAULT, so a client sending any near-miss would widen nothing, report nothing wrong, and leave a page saying '2 resolved' beside a button that does not work. THE FIELD IS ADDITIVE AND wireVersion STAYS AT 1: parseAnnotationsResponse already ignores unknown fields and answers 0 for an absent count, so an older SDK meeting the A-288 console is unaffected and this SDK meeting a pre-A-288 console reads resolvedHidden: 0, which is the honest reading of a console that hides nothing. Headroom is now 1700/1652 = 2.9%, down from 6.4% - the slack A-276 left for A-277 has been spent by A-323 instead, which is what it was for.",
123
148
  "scripts": {
124
149
  "build": "node build.mjs",
125
- "typecheck": "tsc --noEmit",
150
+ "typecheck": "node ../../node_modules/@typescript/native/bin/tsc --noEmit --incremental --tsBuildInfoFile .typecheck.tsbuildinfo",
126
151
  "//lint": "A-97. The root `eslint.config.mjs` is the single rule set — ESLint finds it by walking up from here, so there is nothing per-package to keep in sync. It is a script in EVERY package on purpose: A-52's verify tier runs `pnpm -r --if-present lint` and counts how many packages DECLARED it, so a package without this line is invisible to that gate rather than merely unlinted. `--max-warnings 0` because the React and Next plugins report at `warn` by default, and a gate that exits 0 while printing problems is the exact failure this repository keeps finding.",
127
- "lint": "eslint . --max-warnings 0",
152
+ "lint": "eslint . --max-warnings 0 --cache --cache-strategy content",
128
153
  "//test": "`--test-timeout` is not decoration (A-240). Without it, breaking `httpTransport`'s abort did not turn this suite RED — it hung `pnpm test` forever, because the pending fetch keeps the event loop alive and node:test has no default deadline. CI times out, so it never read as a pass; but a wall-clock stall names no test and blames whatever was printed last, and this repository's standard is a NAMED FAILING TEST. 30s is a ceiling and not a budget: MEASURED, the slowest test in this suite is 203ms (`a 500 reaches the SDK's error handler`, which sleeps 200ms on purpose), so the limit is ~150x it and no honest test can reach it.",
129
154
  "test": "tsx --test --test-timeout=30000 src/*.test.ts",
130
155
  "e2e": "playwright test",