@weatherboard/annotation 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ADOPTING.md +360 -0
- package/LICENSE +21 -0
- package/README.md +109 -0
- package/dist/host.cjs +2 -0
- package/dist/host.cjs.map +7 -0
- package/dist/host.d.cts +60 -0
- package/dist/host.d.mts +60 -0
- package/dist/host.d.ts +60 -0
- package/dist/host.mjs +2 -0
- package/dist/host.mjs.map +7 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +7 -0
- package/dist/index.d.cts +34 -0
- package/dist/index.d.mts +34 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +7 -0
- package/dist/overlay.d.cts +31 -0
- package/dist/overlay.d.mts +31 -0
- package/dist/overlay.d.ts +31 -0
- package/dist/protocol.cjs +2 -0
- package/dist/protocol.cjs.map +7 -0
- package/dist/protocol.d.cts +309 -0
- package/dist/protocol.d.mts +309 -0
- package/dist/protocol.d.ts +309 -0
- package/dist/protocol.mjs +2 -0
- package/dist/protocol.mjs.map +7 -0
- package/dist/screenshot.d.cts +182 -0
- package/dist/screenshot.d.mts +182 -0
- package/dist/screenshot.d.ts +182 -0
- package/dist/sdk.d.cts +52 -0
- package/dist/sdk.d.mts +52 -0
- package/dist/sdk.d.ts +52 -0
- package/dist/state.d.cts +62 -0
- package/dist/state.d.mts +62 -0
- package/dist/state.d.ts +62 -0
- package/dist/target.d.cts +74 -0
- package/dist/target.d.mts +74 -0
- package/dist/target.d.ts +74 -0
- package/package.json +94 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/protocol.ts", "../src/overlay.ts", "../src/target.ts", "../src/screenshot.ts", "../src/sdk.ts", "../src/state.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * The annotation postMessage protocol (A-36).\n *\n * Mirrors `docs/contracts/types.ts`. That file is the\n * cross-component contract; this is the runtime copy the product bundle ships,\n * because the product cannot import from a docs directory. Changing a shape\n * here is a change to both \u2014 say so in the PR.\n */\n\nexport const SDK_VERSION = \"1\";\n\n/**\n * ===========================================================================\n * THE ATTRIBUTES \u2014 THE OTHER WIRE (A-186)\n * ===========================================================================\n *\n * These live in `protocol.ts`, not in `target.ts` where they are read, because\n * they are a contract between TWO PACKAGES that a consumer installs separately:\n * `@weatherboard/annotation-next` WRITES them at build time and this package\n * READS them at runtime. Two spellings of `data-src` is not a bug anybody\n * catches \u2014 the transform runs, the attributes appear, and every capture comes\n * back with no source ref at all, which is indistinguishable from the transform\n * not running.\n *\n * `annotation-next` imports these from `@weatherboard/annotation/protocol`\n * rather than declaring its own, which is why the subpath has no DOM in it: a\n * build tool importing the page SDK to learn two strings would be absurd.\n */\nexport const SRC_ATTR = \"data-src\";\nexport const NAME_ATTR = \"data-src-name\";\n\nexport interface SourceRef {\n file: string;\n line: number;\n column?: number;\n}\n\nexport interface BuildInfo {\n /** Mandatory. Feedback not pinned to a revision is ambiguous the moment someone merges. */\n gitSha: string;\n /**\n * Where this build came from.\n *\n * `\"development\"` was added in A-37, when the SDK was first mounted in a\n * real app and the two halves turned out not to agree: `resolveBuildInfo` in\n * `resolveBuildInfo` in the product's own Next config package \u2014 the module\n * that stamps the build-env variable, and the only producer of this field \u2014\n * emits\n * `production | preview | development`, mirroring `VERCEL_ENV`. This union\n * instead offered `\"staging\"`, which nothing produces: a staging deploy IS a\n * Vercel preview and reports itself as one. Widening rather than forcing the\n * mount to lie: a locally-annotated build calling itself a preview would put\n * someone's laptop in the corpus as though it were a deployment.\n */\n env: \"production\" | \"preview\" | \"staging\" | \"development\";\n branch?: string;\n /** Set when reviewing an agent's preview deploy, so feedback routes to that run's session. */\n previewRunId?: string;\n}\n\nexport interface ConsoleEntry {\n level: \"error\" | \"warn\";\n message: string;\n stack?: string;\n at: string;\n}\n\nexport interface NetworkEntry {\n url: string;\n method: string;\n status: number;\n at: string;\n}\n\nexport interface TargetInfo {\n /**\n * What the reviewer selected \u2014 one element, or an area (A-179).\n *\n * -------------------------------------------------------------------------\n * WHY THIS EXISTS RATHER THAN BEING INFERRED\n * -------------------------------------------------------------------------\n *\n * A region capture and an element capture are the same SHAPE: a bounding box,\n * a chain of source refs, a DOM path. Nothing in the values distinguishes\n * them, and the two mean opposite things about the chain \u2014 for an element it\n * is ANCESTORS, innermost first, so entry two contains entry one; for a\n * region it is SIBLINGS the rectangle happened to cross, which contain\n * nothing of each other.\n *\n * A reader that guesses will describe a region's siblings as a call-site\n * chain, which is a confident false statement about the page. `describeCapture`\n * hands that description to a model that writes acceptance criteria from it.\n *\n * -------------------------------------------------------------------------\n * WHY IT IS OPTIONAL, AND WHY ABSENT MEANS `element`\n * -------------------------------------------------------------------------\n *\n * The two halves of this protocol live in different repositories and deploy\n * independently (see the note on the legacy parameter names below). Every\n * capture in the store today, and every capture from a product bundle that\n * predates region selection, has no such field \u2014 and all of them are element\n * captures. Absent therefore has to mean `element`, or this field would\n * retroactively make the entire history ambiguous.\n *\n * Required-with-a-default would have the same effect and a worse failure: an\n * older bundle omits it, the type says it is there, and every reader believes\n * a value nobody sent.\n */\n selection?: \"element\" | \"region\";\n source?: SourceRef;\n /**\n * Every `data-src` from the clicked element up to the document root, innermost\n * first \u2014 an addition to the contract, and the reason is structural.\n *\n * Only host elements carry a ref (A-33), so `source` names the file that\n * *writes the markup*. For a composed design-system component that is\n * `MediaCard.tsx`, not the `GuideDetailCard.tsx` a human would say they\n * clicked. The chain is what lets the PM agent and the coding agent see both,\n * and pick the call site over the leaf when the feedback is about content\n * rather than the component itself.\n */\n sourceChain?: SourceRef[];\n componentName?: string;\n /** CSS selector fallback, for when no ancestor carries a ref at all. */\n domPath: string;\n boundingBox: { x: number; y: number; w: number; h: number };\n /** Truncated to 200 characters. */\n textContent?: string;\n}\n\nexport interface AnnotationPayload {\n schemaVersion: 1;\n capturedAt: string;\n build: BuildInfo;\n page: {\n url: string;\n /** The matched route pattern, not the resolved URL. */\n route: string;\n viewport: { w: number; h: number; dpr: number };\n userAgent: string;\n };\n target: TargetInfo;\n state: {\n consoleErrors: ConsoleEntry[];\n failedRequests: NetworkEntry[];\n featureFlags: Record<string, unknown>;\n };\n screenshot?: { dataUrl: string; region: \"element\" | \"viewport\" };\n /** The human's words, verbatim. The parent frame fills this in; we never rewrite it. */\n comment: string;\n}\n\nexport type ParentToFrame =\n | { type: \"annotate:enable\" }\n | { type: \"annotate:disable\" }\n | { type: \"annotate:ping\" };\n\nexport type FrameToParent =\n | { type: \"annotate:ready\"; sdkVersion: string; build: BuildInfo }\n | { type: \"annotate:hover\"; target: TargetInfo }\n | { type: \"annotate:capture\"; payload: AnnotationPayload }\n | { type: \"annotate:cancel\" };\n\nconst PARENT_TYPES = new Set([\"annotate:enable\", \"annotate:disable\", \"annotate:ping\"]);\n\n/**\n * Is this a message we recognise?\n *\n * Origin is checked separately and first \u2014 a well-formed message from the wrong\n * origin is the case this guard must not be trusted to catch.\n */\nexport function isParentMessage(data: unknown): data is ParentToFrame {\n if (typeof data !== \"object\" || data === null) return false;\n const type = (data as { type?: unknown }).type;\n return typeof type === \"string\" && PARENT_TYPES.has(type);\n}\n\n/**\n * Reject wildcards outright rather than treating them as \"any origin\".\n *\n * `postMessage(msg, \"*\")` broadcasts the payload to whatever happens to be\n * framing us, and an annotation payload carries page content, console errors and\n * failed request URLs. The spec's rule is no wildcards in either direction; this\n * makes it impossible to configure one by accident.\n */\nexport function assertConcreteOrigin(origin: string): string {\n if (!origin || origin === \"*\") {\n throw new Error(\n \"[annotation] a concrete parent origin is required; '*' would broadcast \" +\n \"annotation payloads to any embedder.\",\n );\n }\n return origin;\n}\n\n/**\n * The query parameter that arms the SDK in a managed product, and the cookie\n * that records it.\n *\n * THESE LIVE IN THE PROTOCOL BECAUSE THE TWO HALVES ARE NOW IN DIFFERENT\n * REPOSITORIES. The managed product's middleware looks for the parameter and\n * sets the cookie; the management app appends the parameter and the SDK reads\n * the cookie. While both halves were one monorepo, a test could read the other\n * side's source and assert the strings matched \u2014 that gate does not survive a\n * repository split, and a mismatch produces no error at all: the frame loads,\n * the cookie is never set, the SDK never boots, and the reviewer sees \"waiting\n * for the page to announce itself\" with nothing to go on.\n *\n * So they stop being two spellings that agree and become one exported\n * constant. A managed product that imports this package cannot drift from it.\n */\nexport const ANNOTATION_TOKEN_PARAM = \"ai_annotate\";\nexport const ANNOTATION_ARMED_COOKIE = \"ai_annotate_armed\";\n\n/**\n * The names these used to have (A-98).\n *\n * -------------------------------------------------------------------------\n * WHY THE RENAME, AND WHY IT COULD NOT JUST BE DONE\n * -------------------------------------------------------------------------\n *\n * They were `pits_annotate` and `pits_annotate_armed` \u2014 one managed product's\n * initialism, in a contract every managed product's bundle has to implement.\n * Onboarding a second product meant asking an unrelated codebase to adopt the\n * first product's name in its cookies and its query strings. That is the bug\n * A-98 is about, and it is a bug in a wire protocol rather than in a variable\n * name, which is what makes it expensive.\n *\n * The two halves live in DIFFERENT REPOSITORIES and deploy independently. A\n * clean rename is therefore a breaking change to every product bundle\n * simultaneously, and its failure mode is silence: the frame loads, the cookie\n * is never set, the SDK never boots, and the reviewer sees \"waiting for the\n * page to announce itself\" with nothing to go on. Nothing throws, nothing logs,\n * and the natural reading is that the agent broke the site.\n *\n * -------------------------------------------------------------------------\n * THE MIGRATION: READ BOTH, WRITE BOTH, THEN STOP WRITING THE OLD ONE\n * -------------------------------------------------------------------------\n *\n * 1. **Now.** Every reader accepts either name. Every writer emits BOTH, so a\n * product still running the old bundle arms on the legacy name and one\n * running the new bundle arms on the new one. Deploy order stops mattering,\n * which is the whole point \u2014 there is no window in which a reviewer sees the\n * silent failure above.\n * 2. **After every managed product's bundle is on this version** (A-102). Drop\n * the legacy names from the writers, then from the readers. Two steps, not\n * one: a reader that stops accepting the old name before the writers stop\n * emitting it re-creates the outage in the other direction.\n *\n * The legacy names are exported rather than inlined so the deletion in step 2\n * is `git grep LEGACY_` rather than an archaeology exercise.\n */\nexport const LEGACY_ANNOTATION_TOKEN_PARAM = \"pits_annotate\";\nexport const LEGACY_ANNOTATION_ARMED_COOKIE = \"pits_annotate_armed\";\n\n/**\n * ===========================================================================\n * AND THE NAMES ARE NOT CHANGING AGAIN FOR THE PUBLISHED PACKAGE (A-184)\n * ===========================================================================\n *\n * When this becomes `@weatherboard/annotation` the obvious next thought is that\n * the wire names should match the package \u2014 `weatherboard_annotate`. They\n * should not, and the reason is the migration described above rather than\n * taste.\n *\n * That migration is STILL IN FLIGHT. Readers accept both spellings; writers\n * emit both; step 2 waits on every managed product's bundle being current.\n * A third spelling introduced now does not replace the second, it joins it:\n * every reader accepts three names, every writer emits three, and the window in\n * which a product can be on any of them widens instead of closing.\n *\n * The cost is not the code. It is that each rename buys another instance of the\n * failure this file exists to prevent \u2014 the frame loads, the cookie is never\n * set, the SDK never boots, and the reviewer sees \"waiting for the page to\n * announce itself\" with nothing to go on. Twice is a migration. Three times is\n * a habit, and the third one would be paid by consumers who never asked for\n * either.\n *\n * `ai_annotate` is already product-neutral, which was the entire point of A-98.\n * A published package needs names that name no product; it does not need names\n * that name the package. Nobody reading a query string cares which npm scope\n * the library came from.\n *\n * The one real objection is collision: `ai_` is a common prefix and some other\n * tool could plausibly use it. That is survivable by construction \u2014 arming\n * requires the parameter to carry a token the product compares exactly, so a\n * coincidental `?ai_annotate=` from an unrelated tool arms nothing. A colliding\n * COOKIE cannot false-positive either; `isAnnotationArmed` matches on the\n * delimiter rather than by substring, for exactly this reason.\n *\n * If a rename is ever genuinely warranted, it happens ONCE, after step 2 has\n * landed and the reader accepts one name again \u2014 not layered on top of an\n * unfinished one.\n */\n\n/** Both spellings of the query parameter, current first. */\nexport const ANNOTATION_TOKEN_PARAMS = [\n ANNOTATION_TOKEN_PARAM,\n LEGACY_ANNOTATION_TOKEN_PARAM,\n] as const;\n\n/** Both spellings of the cookie, current first. */\nexport const ANNOTATION_ARMED_COOKIES = [\n ANNOTATION_ARMED_COOKIE,\n LEGACY_ANNOTATION_ARMED_COOKIE,\n] as const;\n\n/**\n * Whether a cookie jar carries the armed cookie, under either name.\n *\n * Delimiter-aware rather than a substring test: a cookie named\n * `not_ai_annotate_armed`, or another cookie whose VALUE contains the name,\n * satisfies `includes` \u2014 and arming an annotation channel on a coincidence is\n * not a failure anyone would think to look for.\n */\nexport function isAnnotationArmed(cookie: string): boolean {\n return cookie\n .split(\";\")\n .some((entry) => {\n const trimmed = entry.trimStart();\n return ANNOTATION_ARMED_COOKIES.some((name) => trimmed.startsWith(`${name}=`));\n });\n}\n\n/**\n * The arming token from a URL's query string, under either name.\n *\n * Current name wins when both are present. They should never disagree \u2014 the\n * management app writes both from one value \u2014 but if they ever do, the one this\n * version of the protocol owns is the one to believe.\n */\nexport function readAnnotationToken(params: URLSearchParams): string | null {\n for (const name of ANNOTATION_TOKEN_PARAMS) {\n const value = params.get(name);\n if (value !== null && value.length > 0) return value;\n }\n return null;\n}\n", "/**\n * The hover highlight, drawn inside the iframe.\n *\n * The parent frame does not draw this, and that is a deliberate call from\n * `spec/02-annotation-sdk.md`: translating coordinates across the frame boundary\n * means tracking the iframe's position, the page's scroll, and any transform on\n * either side, and getting it wrong puts the box next to the thing rather than\n * on it. Inside the frame, `getBoundingClientRect` is already in the right\n * coordinate space.\n */\n\n/**\n * The SDK'\"'\"'s own node, so this is not a cross-repository contract like the\n * cookie and the query parameter are \u2014 nothing outside this package creates it\n * or looks it up. Renamed with them anyway (A-98): a product whose stylesheet\n * happens to target the old id would be styling the overlay by accident, and\n * the new name is the one a reader will expect to find.\n */\nexport const OVERLAY_ID = \"ai-annotation-overlay\";\n\n/** Above app chrome without being a magic number nobody can justify. */\nconst Z_INDEX = \"2147483000\";\n\nexport interface Overlay {\n show(box: { x: number; y: number; w: number; h: number }, label: string | null): void;\n hide(): void;\n destroy(): void;\n /** The overlay's own nodes, so hit-testing can ignore them. */\n contains(node: unknown): boolean;\n}\n\nexport function createOverlay(doc: Document): Overlay {\n const root = doc.createElement(\"div\");\n root.id = OVERLAY_ID;\n // `pointer-events: none` is what keeps the overlay from hit-testing itself:\n // without it, `elementFromPoint` returns the highlight and the highlight\n // follows the highlight.\n root.style.cssText = [\n \"position:fixed\",\n \"top:0\",\n \"left:0\",\n \"pointer-events:none\",\n `z-index:${Z_INDEX}`,\n \"display:none\",\n \"box-sizing:border-box\",\n \"border:2px solid #2563eb\",\n \"background:rgba(37,99,235,0.12)\",\n \"border-radius:2px\",\n ].join(\";\");\n\n const tag = doc.createElement(\"div\");\n tag.style.cssText = [\n \"position:absolute\",\n \"left:0\",\n \"top:-20px\",\n \"font:11px/16px ui-monospace,SFMono-Regular,Menlo,monospace\",\n \"color:#fff\",\n \"background:#2563eb\",\n \"padding:1px 5px\",\n \"border-radius:2px\",\n \"white-space:nowrap\",\n \"max-width:60vw\",\n \"overflow:hidden\",\n \"text-overflow:ellipsis\",\n ].join(\";\");\n root.appendChild(tag);\n doc.body.appendChild(root);\n\n return {\n show(box, label) {\n root.style.display = \"block\";\n root.style.transform = `translate(${box.x}px, ${box.y}px)`;\n root.style.width = `${box.w}px`;\n root.style.height = `${box.h}px`;\n // A box at the very top of the viewport would render its label off-screen.\n tag.style.top = box.y < 20 ? \"100%\" : \"-20px\";\n tag.textContent = label ?? \"\";\n tag.style.display = label ? \"block\" : \"none\";\n },\n hide() {\n root.style.display = \"none\";\n },\n destroy() {\n root.remove();\n },\n contains(node) {\n return node instanceof Node && root.contains(node);\n },\n };\n}\n", "import { NAME_ATTR, SRC_ATTR, type SourceRef, type TargetInfo } from \"./protocol\";\n\n/**\n * Turning a clicked DOM element into something a coding agent can act on.\n *\n * Everything here is written against the narrowest possible slice of the DOM so\n * it can be tested without a browser \u2014 see `ElementLike`. The parts that need a\n * real browser (hit-testing, layout) live in `sdk.ts`.\n */\n\n/**\n * Re-exported, not declared. They moved to `protocol.ts` (A-186) because the\n * build transform in `@weatherboard/annotation-next` writes what this file\n * reads, and the two packages install separately. Kept exported from here so\n * every existing importer is unaffected.\n */\nexport { NAME_ATTR, SRC_ATTR };\n\nexport const MAX_TEXT = 200;\n/** Deep trees are common; a chain long enough to be useless is not worth sending. */\nexport const MAX_CHAIN = 8;\n\n/** The subset of `Element` this module reads. */\nexport interface ElementLike {\n tagName: string;\n id?: string;\n getAttribute(name: string): string | null;\n parentElement: ElementLike | null;\n classList?: { value?: string } | ArrayLike<string>;\n textContent?: string | null;\n}\n\n/**\n * Parse `path/to/File.tsx:42:7` into its parts.\n *\n * Split from the right: the line and column never contain a colon, and a path\n * conceivably might. Returns null rather than a half-parsed ref \u2014 a ref we\n * cannot read is worse than no ref, because downstream will believe it.\n */\nexport function parseSourceRef(value: string | null): SourceRef | null {\n if (!value) return null;\n\n const colIdx = value.lastIndexOf(\":\");\n if (colIdx <= 0) return null;\n const lineIdx = value.lastIndexOf(\":\", colIdx - 1);\n if (lineIdx <= 0) return null;\n\n const file = value.slice(0, lineIdx);\n const line = Number(value.slice(lineIdx + 1, colIdx));\n const column = Number(value.slice(colIdx + 1));\n\n if (!file || !Number.isFinite(line) || !Number.isFinite(column)) return null;\n return { file, line, column };\n}\n\n/**\n * Every source ref from `el` upwards, innermost first.\n *\n * Consecutive duplicates are collapsed: one JSX element often produces several\n * nested DOM nodes carrying the same ref, and repeating it says nothing.\n */\nexport function collectSourceChain(el: ElementLike | null, max = MAX_CHAIN): SourceRef[] {\n const chain: SourceRef[] = [];\n let node: ElementLike | null = el;\n\n while (node && chain.length < max) {\n const ref = parseSourceRef(node.getAttribute(SRC_ATTR));\n if (ref) {\n const prev = chain[chain.length - 1];\n const isRepeat = prev && prev.file === ref.file && prev.line === ref.line;\n if (!isRepeat) chain.push(ref);\n }\n node = node.parentElement;\n }\n\n return chain;\n}\n\n/** The nearest ancestor (or self) carrying a ref, which is the element a human means. */\nexport function findAnnotatedAncestor(el: ElementLike | null): ElementLike | null {\n let node: ElementLike | null = el;\n while (node) {\n if (parseSourceRef(node.getAttribute(SRC_ATTR))) return node;\n node = node.parentElement;\n }\n return null;\n}\n\nfunction classTokens(el: ElementLike): string[] {\n const list = el.classList as { value?: string } | undefined;\n const raw = typeof list?.value === \"string\" ? list.value : \"\";\n return raw.split(/\\s+/).filter(Boolean);\n}\n\n/**\n * A CSS-ish path to the element, used only when no ancestor carries a ref.\n *\n * Deliberately not guaranteed unique. It is a hint for a human reading the\n * payload, not a selector we promise to re-resolve \u2014 StyleX class names are\n * hashed and change between builds, so a path that looks precise would be\n * quietly wrong on the next deploy. IDs are kept because they survive; classes\n * are capped at two for readability.\n */\nexport function domPath(el: ElementLike | null, maxDepth = 6): string {\n const parts: string[] = [];\n let node: ElementLike | null = el;\n\n while (node && parts.length < maxDepth) {\n const tag = (node.tagName || \"\").toLowerCase();\n if (!tag || tag === \"html\") break;\n\n let part = tag;\n if (node.id) {\n // An id is unique on the page, so nothing above it adds information.\n parts.unshift(`${tag}#${node.id}`);\n return parts.join(\" > \");\n }\n const classes = classTokens(node).slice(0, 2);\n if (classes.length > 0) part += `.${classes.join(\".\")}`;\n parts.unshift(part);\n\n if (tag === \"body\") break;\n node = node.parentElement;\n }\n\n return parts.join(\" > \");\n}\n\n/** Collapse whitespace and truncate, so the payload carries a label rather than a page. */\nexport function summariseText(text: string | null | undefined): string | undefined {\n if (!text) return undefined;\n const collapsed = text.replace(/\\s+/g, \" \").trim();\n if (!collapsed) return undefined;\n return collapsed.length > MAX_TEXT ? `${collapsed.slice(0, MAX_TEXT - 1)}\u2026` : collapsed;\n}\n\nexport interface ResolveTargetOptions {\n boundingBox: { x: number; y: number; w: number; h: number };\n}\n\n/**\n * Build the `TargetInfo` for a clicked or hovered element.\n *\n * `source` is the nearest ref, which names the file that writes the markup \u2014\n * for a composed design-system component that is the design-system file, not\n * the call site. `sourceChain` carries the rest so the agent can choose.\n */\nexport function resolveTarget(el: ElementLike, options: ResolveTargetOptions): TargetInfo {\n const annotated = findAnnotatedAncestor(el);\n const chain = collectSourceChain(el);\n const source = chain[0];\n\n const target: TargetInfo = {\n domPath: domPath(el),\n boundingBox: options.boundingBox,\n };\n\n if (source) target.source = source;\n if (chain.length > 0) target.sourceChain = chain;\n\n const componentName = annotated?.getAttribute(NAME_ATTR);\n if (componentName) target.componentName = componentName;\n\n const text = summariseText(el.textContent);\n if (text) target.textContent = text;\n\n return target;\n}\n", "/**\n * Screenshot capture for the annotation payload (A-31).\n *\n * WHAT THIS ACTUALLY CAPTURES \u2014 READ THIS BEFORE BELIEVING THE FIELD NAME.\n * This is not a page rasteriser. It captures the bitmap of a single\n * `<img>`, `<canvas>` or `<video>` at or around the clicked element, and\n * nothing else. Click a heading and the payload has no screenshot.\n *\n * WHY NOT html2canvas, WHICH THE SPEC SUGGESTS. `spec/02` offers html2canvas or\n * \"`element.toDataURL` via an offscreen canvas where feasible\". html2canvas is a\n * runtime dependency, and this package deliberately has none \u2014 it ships\n * TypeScript source that two repositories compile into their own bundles, so a\n * dependency here is a dependency, a version and a licence in both of them, for\n * a field `spec/02` itself calls a nice-to-have (\"do not block Phase 1 on\n * screenshots; the source ref is what actually matters\"). The platform gives us\n * no way to rasterise arbitrary DOM: `SVGForeignObject` cannot load\n * cross-origin CSS or fonts, and `getDisplayMedia` prompts the user. So we\n * implement the honest subset rather than a convincing-looking approximation,\n * and the spec's server-side fallback \u2014 render `page.url` at `build.gitSha`\n * headless and crop to `target.boundingBox` \u2014 remains the route to a real\n * picture of the page.\n *\n * The subset is worth having on its own: \"this image is the wrong one\" and\n * \"this video poster is stretched\" are common reports, and for those the\n * element's own bitmap is the entire evidence.\n *\n * NOTHING HERE MAY BREAK A CAPTURE. Every entry point resolves rather than\n * rejects. A tainted canvas (any cross-origin image without CORS headers, which\n * is most of them) makes `toDataURL` throw `SecurityError`; a detached element\n * makes `drawImage` throw; a huge bitmap makes it slow. All of those must end\n * with `screenshot` absent from an otherwise complete payload, never with a\n * capture the reviewer loses.\n */\n\n/** The canvas surface this module needs. Narrow so tests need no browser. */\nexport interface CanvasLike {\n width: number;\n height: number;\n getContext(id: \"2d\"): CanvasContextLike | null;\n toDataURL(type?: string, quality?: number): string;\n}\n\nexport interface CanvasContextLike {\n drawImage(source: unknown, dx: number, dy: number, dw: number, dh: number): void;\n}\n\n/**\n * The subset of an element this module reads.\n *\n * Deliberately structural rather than `instanceof HTMLImageElement`: the SDK\n * runs inside an iframe, and an element from another document fails an\n * `instanceof` check against this realm's constructors even when it is exactly\n * the thing we want.\n */\nexport interface CaptureCandidate {\n tagName: string;\n parentElement?: CaptureCandidate | null;\n querySelector?: (selector: string) => CaptureCandidate | null;\n /** `<img>` */\n naturalWidth?: number;\n naturalHeight?: number;\n complete?: boolean;\n /** `<video>` */\n videoWidth?: number;\n videoHeight?: number;\n /** `<canvas>` */\n width?: number;\n height?: number;\n}\n\nexport type Screenshot = { dataUrl: string; region: \"element\" | \"viewport\" };\n\n/**\n * The byte budget for the encoded data URL.\n *\n * `apps/manager/src/lib/feedback/validate.ts` rejects the whole payload over\n * `MAX_PAYLOAD_BYTES` (128KB) and names screenshots as the usual cause. 48KB\n * leaves the rest of the payload \u2014 twenty console entries with stacks, twenty\n * failed requests, a source chain \u2014 room to be large without the server\n * refusing a report the reviewer has already written. A data URL is base64\n * ASCII, so `dataUrl.length` is its byte cost in the serialised payload, near\n * enough to compare directly.\n *\n * Over budget we drop the screenshot. Emitting it anyway would trade a payload\n * with no picture for a payload the server 413s, which loses the source ref \u2014\n * the only part that actually matters.\n */\nexport const MAX_SCREENSHOT_BYTES = 48 * 1024;\n\n/**\n * Longest edge, in device-independent pixels, of the downscaled capture.\n *\n * A 2000px-wide hero image encodes to well over the budget at any quality worth\n * looking at. 480px is enough to see that the wrong photo is on the card, which\n * is the question this field answers.\n */\nexport const MAX_SCREENSHOT_EDGE = 480;\n\n/**\n * A ceiling on how long we will wait, not on how long the work takes.\n *\n * `toDataURL` is synchronous and cannot be interrupted; this bounds the wait so\n * a pathological encode does not hold the capture open indefinitely, and the\n * payload goes out without the screenshot instead. Said plainly because a\n * timeout that cannot cancel its work is the kind of thing a reader assumes is\n * stronger than it is.\n */\nexport const SCREENSHOT_TIMEOUT_MS = 250;\n\n/**\n * Attempts, in order. The second is a genuine retry at a worse picture rather\n * than a give-up: a slightly muddy 240px thumbnail still answers \"is that the\n * right image\", and a dropped screenshot answers nothing.\n */\nconst ATTEMPTS: ReadonlyArray<{ edge: number; quality: number }> = [\n { edge: MAX_SCREENSHOT_EDGE, quality: 0.7 },\n { edge: MAX_SCREENSHOT_EDGE / 2, quality: 0.5 },\n];\n\n/**\n * WebP first, JPEG second. PNG is never asked for: it is lossless, and a\n * lossless photograph is exactly the thing that blows the byte budget. A canvas\n * that does not know a requested type silently returns PNG, so the result is\n * checked by prefix rather than trusted.\n */\nconst FORMATS = [\"image/webp\", \"image/jpeg\"] as const;\n\nexport interface ScreenshotOptions {\n /**\n * Off switch. `spec/02` treats screenshots as optional, so a host that would\n * rather not encode bitmaps on click \u2014 or that serves images this SDK would\n * only ever taint \u2014 sets this to `false`.\n */\n enabled?: boolean;\n maxBytes?: number;\n maxEdge?: number;\n timeoutMs?: number;\n /**\n * Canvas factory. Injected so the tests can drive the failure paths \u2014 taint,\n * an oversized encode, a context that is not there \u2014 none of which are\n * reachable from a fake DOM object alone.\n */\n createCanvas?: () => CanvasLike | null;\n}\n\ninterface ResolvedOptions {\n enabled: boolean;\n maxBytes: number;\n maxEdge: number;\n timeoutMs: number;\n createCanvas: () => CanvasLike | null;\n}\n\nexport function resolveScreenshotOptions(\n options: ScreenshotOptions | false | undefined,\n doc?: Document,\n): ResolvedOptions {\n /**\n * DEFAULT ON. The subset only fires on media elements, costs one downscaled\n * encode, and cannot enlarge a payload past the budget above; the failure\n * mode of defaulting off is that nobody remembers the flag exists and the\n * field is dead code. Hosts that do not want it pass `screenshot: false`.\n */\n const opts = options === false ? { enabled: false } : (options ?? {});\n return {\n enabled: opts.enabled ?? true,\n maxBytes: opts.maxBytes ?? MAX_SCREENSHOT_BYTES,\n maxEdge: opts.maxEdge ?? MAX_SCREENSHOT_EDGE,\n timeoutMs: opts.timeoutMs ?? SCREENSHOT_TIMEOUT_MS,\n createCanvas: opts.createCanvas ?? (() => doc?.createElement(\"canvas\") ?? null),\n };\n}\n\nconst MEDIA_TAGS = new Set([\"IMG\", \"CANVAS\", \"VIDEO\"]);\nconst MEDIA_SELECTOR = \"img,canvas,video\";\n/** How far up the tree to look. A card wraps its image; a page does not. */\nconst MAX_ANCESTOR_HOPS = 4;\n\nexport function isMediaElement(el: CaptureCandidate | null | undefined): boolean {\n return !!el && MEDIA_TAGS.has(el.tagName.toUpperCase());\n}\n\n/**\n * The nearest thing with a bitmap: the element itself, then a descendant, then\n * an ancestor.\n *\n * Descendant before ancestor because a click lands on whatever is topmost, and\n * on a card that is usually the overlay or the link wrapping the photo \u2014 the\n * photo the reviewer means is inside it, not above it.\n */\nexport function findCaptureSource(\n el: CaptureCandidate | null,\n maxHops = MAX_ANCESTOR_HOPS,\n): CaptureCandidate | null {\n if (!el) return null;\n if (isMediaElement(el)) return el;\n\n const descendant = el.querySelector?.(MEDIA_SELECTOR) ?? null;\n if (isMediaElement(descendant)) return descendant;\n\n let node = el.parentElement ?? null;\n for (let hops = 0; node && hops < maxHops; hops++) {\n if (isMediaElement(node)) return node;\n node = node.parentElement ?? null;\n }\n return null;\n}\n\n/**\n * The source's own pixel dimensions, or null if it has none.\n *\n * A broken `<img>` reports `naturalWidth === 0`, and that is the honest answer:\n * there is no bitmap to capture. The report still carries the source ref and\n * the DOM path, which is what a \"this image is broken\" ticket needs anyway.\n */\nexport function intrinsicSize(el: CaptureCandidate): { w: number; h: number } | null {\n const tag = el.tagName.toUpperCase();\n const pair =\n tag === \"IMG\"\n ? { w: el.naturalWidth, h: el.naturalHeight }\n : tag === \"VIDEO\"\n ? { w: el.videoWidth, h: el.videoHeight }\n : { w: el.width, h: el.height };\n\n const w = Math.floor(pair.w ?? 0);\n const h = Math.floor(pair.h ?? 0);\n if (!Number.isFinite(w) || !Number.isFinite(h) || w < 1 || h < 1) return null;\n if (tag === \"IMG\" && el.complete === false) return null;\n return { w, h };\n}\n\n/** Fit inside `maxEdge` without ever upscaling \u2014 enlarging invents detail. */\nexport function fitWithin(\n size: { w: number; h: number },\n maxEdge: number,\n): { w: number; h: number } {\n const scale = Math.min(1, maxEdge / Math.max(size.w, size.h));\n return { w: Math.max(1, Math.round(size.w * scale)), h: Math.max(1, Math.round(size.h * scale)) };\n}\n\n/**\n * Encode one attempt. Returns null for every failure, including the ones the\n * platform signals by throwing.\n */\nfunction encode(\n source: CaptureCandidate,\n size: { w: number; h: number },\n quality: number,\n options: ResolvedOptions,\n): string | null {\n let canvas: CanvasLike | null;\n try {\n canvas = options.createCanvas();\n } catch {\n return null;\n }\n if (!canvas) return null;\n\n try {\n canvas.width = size.w;\n canvas.height = size.h;\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return null;\n ctx.drawImage(source, 0, 0, size.w, size.h);\n\n for (const format of FORMATS) {\n // `toDataURL` throws `SecurityError` the moment the canvas is tainted by\n // a cross-origin bitmap, which is the single most likely outcome on a\n // real page. Per-format so a WebP-less browser still gets its JPEG.\n let dataUrl: string;\n try {\n dataUrl = canvas.toDataURL(format, quality);\n } catch {\n return null;\n }\n if (typeof dataUrl !== \"string\" || !dataUrl.startsWith(`data:${format}`)) continue;\n if (dataUrl.length <= options.maxBytes) return dataUrl;\n // Over budget in this format; a different container will not save an\n // encode this large, so let the next, smaller attempt try instead.\n return null;\n }\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * Capture synchronously, or return null. Exported for the tests, which need to\n * assert the drop decisions without a timer in the way.\n */\nexport function captureScreenshotSync(\n el: CaptureCandidate | null,\n options: ResolvedOptions,\n): Screenshot | null {\n if (!options.enabled) return null;\n\n const source = findCaptureSource(el);\n if (!source) return null;\n\n const natural = intrinsicSize(source);\n if (!natural) return null;\n\n for (const attempt of ATTEMPTS) {\n const size = fitWithin(natural, Math.min(options.maxEdge, attempt.edge));\n const dataUrl = encode(source, size, attempt.quality, options);\n /**\n * Always `\"element\"`, never `\"viewport\"`. The protocol allows both, but\n * this implementation has no way to rasterise a viewport, and a field that\n * claimed otherwise would send a reviewer looking for page context that is\n * not in the image. `\"viewport\"` is reserved for the server-side renderer.\n */\n if (dataUrl) return { dataUrl, region: \"element\" };\n }\n return null;\n}\n\n/**\n * The entry point the SDK calls. Resolves to a screenshot or to null, within\n * `timeoutMs`, and never rejects.\n */\nexport function captureScreenshot(\n el: CaptureCandidate | null,\n options: ResolvedOptions,\n): Promise<Screenshot | null> {\n if (!options.enabled) return Promise.resolve(null);\n\n return new Promise<Screenshot | null>((resolve) => {\n let settled = false;\n const settle = (value: Screenshot | null) => {\n if (settled) return;\n settled = true;\n resolve(value);\n };\n\n const timer = setTimeout(() => settle(null), options.timeoutMs);\n // `unref` where it exists: a pending timer must not keep a Node test\n // process alive after the payload has already gone out.\n (timer as unknown as { unref?: () => void }).unref?.();\n\n // A microtask, not a straight call: it keeps this function's own contract\n // uniform (always async) so the SDK has one code path, not two.\n void Promise.resolve()\n .then(() => captureScreenshotSync(el, options))\n .then(settle, () => settle(null))\n .finally(() => clearTimeout(timer));\n });\n}\n\n/**\n * Merge a capture into a payload, or hand the payload back untouched.\n *\n * The payload goes in already complete and comes out still complete. The\n * failure mode this shape exists to make impossible is a screenshot problem\n * costing the reviewer the source ref \u2014 so the payload is never awaited into\n * existence, only decorated, and `screenshot` is added as a key or not at all\n * rather than set to `undefined` (which `JSON.stringify` drops anyway, but\n * which reads as \"we tried and got nothing\" to anyone inspecting the object).\n */\nexport async function attachScreenshot<T extends object>(\n payload: T,\n el: CaptureCandidate | null,\n options: ResolvedOptions,\n): Promise<T & { screenshot?: Screenshot }> {\n let screenshot: Screenshot | null;\n try {\n screenshot = await captureScreenshot(el, options);\n } catch {\n // `captureScreenshot` does not reject; this is for a host-supplied\n // `createCanvas` that manages to break the chain in some way it cannot.\n screenshot = null;\n }\n return screenshot ? { ...payload, screenshot } : payload;\n}\n", "import {\n SDK_VERSION,\n assertConcreteOrigin,\n isParentMessage,\n type AnnotationPayload,\n type BuildInfo,\n type FrameToParent,\n type TargetInfo,\n} from \"./protocol\";\nimport { createOverlay, type Overlay } from \"./overlay\";\nimport { resolveTarget } from \"./target\";\nimport {\n attachScreenshot,\n resolveScreenshotOptions,\n type CaptureCandidate,\n type ScreenshotOptions,\n} from \"./screenshot\";\n\n/**\n * The annotation SDK (A-36).\n *\n * Lives in the product bundle, listens for the management app's instructions,\n * and turns a click into a payload pinned to a code location.\n */\n\nexport interface AnnotationState {\n consoleErrors: AnnotationPayload[\"state\"][\"consoleErrors\"];\n failedRequests: AnnotationPayload[\"state\"][\"failedRequests\"];\n featureFlags: Record<string, unknown>;\n}\n\nexport interface AnnotationSdkOptions {\n /**\n * The management app's origin. Required and concrete \u2014 see\n * `assertConcreteOrigin`. Messages from anywhere else are ignored, and we\n * never post to anywhere else.\n */\n parentOrigin: string;\n build: BuildInfo;\n /**\n * The matched route pattern, e.g. `/pub/[slug]`.\n *\n * Injected rather than derived: only the app knows its own route patterns,\n * and `location.pathname` is the resolved URL, which is what the spec\n * explicitly does not want \u2014 `/pub/the-eagle` and `/pub/the-crown` are the\n * same route and should group as one.\n */\n getRoute: () => string;\n /**\n * Page state at capture time. Injected so A-35's ring buffers can supply the\n * real thing without this module knowing how they are collected.\n */\n getState: () => AnnotationState;\n /**\n * Screenshot capture (A-31). Omit for the default, or pass `false` to turn it\n * off entirely. See `screenshot.ts` for what it can and cannot capture \u2014 the\n * short version is that it captures a media element's own bitmap and nothing\n * else, so most clicks produce no screenshot at all, by design.\n */\n screenshot?: ScreenshotOptions | false;\n}\n\nexport interface AnnotationSdk {\n enable(): void;\n disable(): void;\n isEnabled(): boolean;\n destroy(): void;\n}\n\nconst EMPTY_STATE: AnnotationState = {\n consoleErrors: [],\n failedRequests: [],\n featureFlags: {},\n};\n\n/** State until A-35 lands its ring buffers. Empty is honest; invented is not. */\nexport const emptyAnnotationState = (): AnnotationState => ({ ...EMPTY_STATE });\n\nexport function createAnnotationSdk(options: AnnotationSdkOptions): AnnotationSdk {\n const parentOrigin = assertConcreteOrigin(options.parentOrigin);\n const win = window;\n const doc = win.document;\n const screenshotOptions = resolveScreenshotOptions(options.screenshot, doc);\n\n let enabled = false;\n let overlay: Overlay | null = null;\n let hoverFrame = 0;\n let lastHovered: Element | null = null;\n\n const post = (message: FrameToParent) => {\n // Never a wildcard, in either direction.\n win.parent.postMessage(message, parentOrigin);\n };\n\n const boxOf = (el: Element) => {\n const r = el.getBoundingClientRect();\n return { x: r.x, y: r.y, w: r.width, h: r.height };\n };\n\n const targetFor = (el: Element): TargetInfo =>\n resolveTarget(el, { boundingBox: boxOf(el) });\n\n const labelFor = (target: TargetInfo): string | null => {\n if (target.componentName && target.source) {\n const file = target.source.file.split(\"/\").pop() ?? target.source.file;\n return `${target.componentName} \u00B7 ${file}:${target.source.line}`;\n }\n if (target.source) return `${target.source.file}:${target.source.line}`;\n return target.componentName ?? null;\n };\n\n const onMouseMove = (event: MouseEvent) => {\n if (!enabled) return;\n // Coalesce to one hit-test per frame: mousemove fires far faster than\n // anything downstream can use, and each one costs a layout read.\n if (hoverFrame) return;\n hoverFrame = win.requestAnimationFrame(() => {\n hoverFrame = 0;\n const el = doc.elementFromPoint(event.clientX, event.clientY);\n if (!el || (overlay && overlay.contains(el))) return;\n if (el === lastHovered) return;\n lastHovered = el;\n\n const target = targetFor(el);\n overlay?.show(target.boundingBox, labelFor(target));\n post({ type: \"annotate:hover\", target });\n });\n };\n\n const buildPayload = (el: Element, state: AnnotationState): AnnotationPayload => ({\n schemaVersion: 1,\n capturedAt: new Date().toISOString(),\n build: options.build,\n page: {\n url: win.location.href,\n route: options.getRoute(),\n viewport: {\n w: win.innerWidth,\n h: win.innerHeight,\n dpr: win.devicePixelRatio,\n },\n userAgent: win.navigator.userAgent,\n },\n target: targetFor(el),\n state: {\n consoleErrors: state.consoleErrors,\n failedRequests: state.failedRequests,\n featureFlags: state.featureFlags,\n },\n // The parent frame collects the human's words and merges them in verbatim.\n comment: \"\",\n });\n\n const onClick = (event: MouseEvent) => {\n if (!enabled) return;\n const el = event.target;\n if (!(el instanceof Element)) return;\n\n /**\n * Capture phase, and stop it dead. Annotation mode is pointed at a live\n * product: an un-prevented click navigates away, submits a form, or opens a\n * dialog, and the payload describes a page the reviewer is no longer on.\n */\n event.preventDefault();\n event.stopPropagation();\n\n /**\n * The payload is assembled *now*, synchronously, and the screenshot is\n * merged into it afterwards. Reading page state after an await would read a\n * page the click has already changed, and the whole point of the capture is\n * that it describes the moment of the click.\n */\n const payload = buildPayload(el, options.getState());\n\n if (!screenshotOptions.enabled) {\n post({ type: \"annotate:capture\", payload });\n return;\n }\n\n /**\n * The screenshot is a nice-to-have and is never allowed to cost us the\n * capture: `captureScreenshot` resolves within its own timeout and never\n * rejects, and the `catch` is belt-and-braces for a caller-supplied\n * `createCanvas` that manages to break the promise chain itself. Either way\n * the payload goes out \u2014 with `screenshot` absent.\n */\n void attachScreenshot(payload, el as unknown as CaptureCandidate, screenshotOptions).then(\n (withShot) => post({ type: \"annotate:capture\", payload: withShot }),\n () => post({ type: \"annotate:capture\", payload }),\n );\n };\n\n const onKeyDown = (event: KeyboardEvent) => {\n if (!enabled || event.key !== \"Escape\") return;\n post({ type: \"annotate:cancel\" });\n disable();\n };\n\n function enable() {\n if (enabled) return;\n enabled = true;\n overlay ??= createOverlay(doc);\n doc.addEventListener(\"mousemove\", onMouseMove, true);\n doc.addEventListener(\"click\", onClick, true);\n doc.addEventListener(\"keydown\", onKeyDown, true);\n }\n\n function disable() {\n if (!enabled) return;\n enabled = false;\n lastHovered = null;\n if (hoverFrame) {\n win.cancelAnimationFrame(hoverFrame);\n hoverFrame = 0;\n }\n doc.removeEventListener(\"mousemove\", onMouseMove, true);\n doc.removeEventListener(\"click\", onClick, true);\n doc.removeEventListener(\"keydown\", onKeyDown, true);\n overlay?.hide();\n }\n\n const onMessage = (event: MessageEvent) => {\n // Origin first, shape second. A well-formed message from the wrong origin is\n // exactly what this check exists to stop.\n if (event.origin !== parentOrigin) return;\n if (!isParentMessage(event.data)) return;\n\n switch (event.data.type) {\n case \"annotate:enable\":\n enable();\n break;\n case \"annotate:disable\":\n disable();\n break;\n case \"annotate:ping\":\n post({ type: \"annotate:ready\", sdkVersion: SDK_VERSION, build: options.build });\n break;\n }\n };\n\n win.addEventListener(\"message\", onMessage);\n\n // Announce ourselves: the parent may have finished loading before we did, in\n // which case its handshake ping has already been and gone.\n post({ type: \"annotate:ready\", sdkVersion: SDK_VERSION, build: options.build });\n\n return {\n enable,\n disable,\n isEnabled: () => enabled,\n destroy() {\n disable();\n win.removeEventListener(\"message\", onMessage);\n overlay?.destroy();\n overlay = null;\n },\n };\n}\n", "import type { ConsoleEntry, NetworkEntry } from \"./protocol\";\nimport type { AnnotationState } from \"./sdk\";\n\n/**\n * Page state, captured continuously (A-35).\n *\n * The timing is the whole point. By the time someone notices something is wrong\n * and clicks it, the interesting error has already happened \u2014 reading\n * `console` at capture time gets you nothing, because there is nothing to read.\n * So the buffers fill from page load and the click just takes a copy.\n *\n * Which means this code runs on every page, for every user in the enabled\n * environments, in front of the app's own error handling. It is written to be\n * boring: patch, record, call through, never throw.\n */\n\n/** Twenty each, per the spec. Enough for context, small enough to post. */\nexport const DEFAULT_LIMIT = 20;\n\n/** A stack is useful; a stack the size of the payload is not. */\nconst MAX_MESSAGE = 500;\nconst MAX_STACK = 2000;\n\n/**\n * A fixed-size window over the most recent N items.\n *\n * Oldest-out rather than newest-dropped: a page that logs steadily would\n * otherwise pin the buffer to whatever happened at boot and never show the\n * error the reviewer is looking at.\n */\nexport class RingBuffer<T> {\n readonly #items: T[] = [];\n readonly #limit: number;\n\n constructor(limit: number = DEFAULT_LIMIT) {\n this.#limit = Math.max(1, limit);\n }\n\n push(item: T): void {\n this.#items.push(item);\n if (this.#items.length > this.#limit) this.#items.shift();\n }\n\n /** A copy, so a consumer holding the payload cannot see it mutate underneath them. */\n toArray(): T[] {\n return [...this.#items];\n }\n\n get size(): number {\n return this.#items.length;\n }\n}\n\n/**\n * Render one console argument as text, without ever throwing.\n *\n * Console arguments are arbitrary: DOM nodes, circular objects, proxies that\n * throw on property access. Anything that fails here would fail *inside the\n * app's own `console.error` call*, turning a logged error into a thrown one.\n */\nexport function describeArg(arg: unknown): string {\n if (typeof arg === \"string\") return arg;\n if (arg instanceof Error) return `${arg.name}: ${arg.message}`;\n if (arg === null) return \"null\";\n if (arg === undefined) return \"undefined\";\n if (typeof arg === \"object\") {\n try {\n return JSON.stringify(arg) ?? String(arg);\n } catch {\n /**\n * Circular, or a getter that threw. Reaching for the constructor name is\n * tempting and is itself a property read \u2014 on a Proxy whose `get` trap\n * throws, the fallback throws too, outside the try that was meant to\n * contain it. Ask the prototype instead, which no `get` trap sees.\n */\n try {\n const proto = Object.getPrototypeOf(arg) as { constructor?: { name?: string } } | null;\n return `[${proto?.constructor?.name ?? \"object\"}]`;\n } catch {\n return \"[object]\";\n }\n }\n }\n try {\n return String(arg);\n } catch {\n return \"[unrenderable]\";\n }\n}\n\nconst truncate = (text: string, max: number) =>\n text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;\n\nexport function formatConsoleArgs(args: unknown[]): string {\n return truncate(args.map(describeArg).join(\" \"), MAX_MESSAGE);\n}\n\n/** The first `Error` among the arguments carries the only stack worth keeping. */\nexport function firstStack(args: unknown[]): string | undefined {\n for (const arg of args) {\n if (arg instanceof Error && typeof arg.stack === \"string\") {\n return truncate(arg.stack, MAX_STACK);\n }\n }\n return undefined;\n}\n\n/** A response is interesting when it is not 2xx. `0` marks a request that never got one. */\nexport function isFailedStatus(status: number): boolean {\n return status === 0 || status < 200 || status >= 300;\n}\n\nexport interface StateCollector {\n getState(): AnnotationState;\n /** Restore every patched global. Exposed mainly so tests can prove it is reversible. */\n destroy(): void;\n}\n\nexport interface StateCollectorOptions {\n limit: number;\n /** Feature flags are the app's to know; this module does not guess at them. */\n getFeatureFlags: () => Record<string, unknown>;\n now: () => string;\n}\n\nexport const defaultStateCollectorOptions = (): StateCollectorOptions => ({\n limit: DEFAULT_LIMIT,\n getFeatureFlags: () => ({}),\n now: () => new Date().toISOString(),\n});\n\n/**\n * Patch `console`, `fetch` and `XMLHttpRequest`, and record what goes wrong.\n *\n * Install this as early as the bundle allows \u2014 an error thrown before the patch\n * is an error the payload will not carry.\n */\nexport function installStateCollector(\n win: Window & typeof globalThis,\n options: StateCollectorOptions = defaultStateCollectorOptions(),\n): StateCollector {\n const consoleErrors = new RingBuffer<ConsoleEntry>(options.limit);\n const failedRequests = new RingBuffer<NetworkEntry>(options.limit);\n\n const record = (level: ConsoleEntry[\"level\"], args: unknown[]) => {\n consoleErrors.push({\n level,\n message: formatConsoleArgs(args),\n ...(firstStack(args) ? { stack: firstStack(args) } : {}),\n at: options.now(),\n });\n };\n\n // --- console -------------------------------------------------------------\n\n const originalError = win.console.error;\n const originalWarn = win.console.warn;\n\n win.console.error = function patchedError(...args: unknown[]) {\n try {\n record(\"error\", args);\n } catch {\n // Recording is best-effort. The app's log must go out regardless.\n }\n return originalError.apply(win.console, args as never);\n };\n\n win.console.warn = function patchedWarn(...args: unknown[]) {\n try {\n record(\"warn\", args);\n } catch {\n /* as above */\n }\n return originalWarn.apply(win.console, args as never);\n };\n\n // --- fetch ---------------------------------------------------------------\n\n /**\n * `unbound-method` is right in general and wrong here (A-97). This is a\n * patch-and-restore: the original is saved so it can be called with an\n * explicit receiver (`originalFetch.call(win, \u2026)`, below) and put back on\n * teardown. Binding it instead would make the restore install a bound copy\n * rather than the function the page started with.\n */\n // eslint-disable-next-line @typescript-eslint/unbound-method\n const originalFetch = win.fetch;\n\n const requestInfo = (input: unknown, init?: RequestInit) => {\n const url =\n typeof input === \"string\"\n ? input\n : input instanceof URL\n ? input.href\n : ((input as Request)?.url ?? String(input));\n const method = init?.method ?? (input as Request)?.method ?? \"GET\";\n return { url, method: method.toUpperCase() };\n };\n\n if (typeof originalFetch === \"function\") {\n win.fetch = async function patchedFetch(input: never, init?: RequestInit) {\n try {\n const response = await originalFetch.call(win, input, init);\n if (isFailedStatus(response.status)) {\n const { url, method } = requestInfo(input, init);\n failedRequests.push({ url, method, status: response.status, at: options.now() });\n }\n return response;\n } catch (error) {\n /**\n * A request that never got a response is recorded as status 0. The spec\n * says \"non-2xx\", and a connection that was refused or aborted is the\n * most interesting failure of all \u2014 it is exactly what a reviewer means\n * by \"nothing loaded\". Rethrown untouched: this is an observer.\n */\n const { url, method } = requestInfo(input, init);\n failedRequests.push({ url, method, status: 0, at: options.now() });\n throw error;\n }\n } as typeof win.fetch;\n }\n\n // --- XMLHttpRequest ------------------------------------------------------\n\n const XHR = win.XMLHttpRequest;\n // Saved to be `.apply`d with an explicit `this` and restored on teardown \u2014\n // see the note on `originalFetch` above.\n // eslint-disable-next-line @typescript-eslint/unbound-method\n const originalOpen = XHR?.prototype?.open;\n\n if (typeof originalOpen === \"function\") {\n XHR.prototype.open = function patchedOpen(\n this: XMLHttpRequest & { __aiAnnotateMethod?: string; __aiAnnotateUrl?: string },\n method: string,\n url: string | URL,\n ...rest: unknown[]\n ) {\n this.__aiAnnotateMethod = method.toUpperCase();\n this.__aiAnnotateUrl = String(url);\n\n // `loadend` fires for success, failure, abort and timeout alike, so one\n // listener covers every way a request can finish.\n this.addEventListener(\"loadend\", () => {\n try {\n if (isFailedStatus(this.status)) {\n failedRequests.push({\n url: this.__aiAnnotateUrl ?? \"\",\n method: this.__aiAnnotateMethod ?? \"GET\",\n status: this.status,\n at: options.now(),\n });\n }\n } catch {\n /* observing must never break the request */\n }\n });\n\n return originalOpen.apply(this, [method, url, ...rest] as never);\n };\n }\n\n return {\n getState(): AnnotationState {\n return {\n consoleErrors: consoleErrors.toArray(),\n failedRequests: failedRequests.toArray(),\n featureFlags: options.getFeatureFlags(),\n };\n },\n destroy() {\n win.console.error = originalError;\n win.console.warn = originalWarn;\n if (typeof originalFetch === \"function\") win.fetch = originalFetch;\n if (typeof originalOpen === \"function\") XHR.prototype.open = originalOpen;\n },\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["SDK_VERSION", "SRC_ATTR", "NAME_ATTR", "PARENT_TYPES", "isParentMessage", "data", "type", "assertConcreteOrigin", "origin", "ANNOTATION_TOKEN_PARAM", "ANNOTATION_ARMED_COOKIE", "LEGACY_ANNOTATION_TOKEN_PARAM", "LEGACY_ANNOTATION_ARMED_COOKIE", "ANNOTATION_TOKEN_PARAMS", "ANNOTATION_ARMED_COOKIES", "isAnnotationArmed", "cookie", "entry", "trimmed", "name", "readAnnotationToken", "params", "value", "OVERLAY_ID", "Z_INDEX", "createOverlay", "doc", "root", "tag", "box", "label", "node", "MAX_TEXT", "MAX_CHAIN", "parseSourceRef", "value", "colIdx", "lineIdx", "file", "line", "column", "collectSourceChain", "el", "max", "chain", "node", "ref", "SRC_ATTR", "prev", "findAnnotatedAncestor", "classTokens", "list", "domPath", "maxDepth", "parts", "tag", "part", "classes", "summariseText", "text", "collapsed", "resolveTarget", "options", "annotated", "source", "target", "componentName", "NAME_ATTR", "MAX_SCREENSHOT_BYTES", "MAX_SCREENSHOT_EDGE", "SCREENSHOT_TIMEOUT_MS", "ATTEMPTS", "FORMATS", "resolveScreenshotOptions", "options", "doc", "opts", "MEDIA_TAGS", "MEDIA_SELECTOR", "MAX_ANCESTOR_HOPS", "isMediaElement", "el", "findCaptureSource", "maxHops", "descendant", "node", "hops", "intrinsicSize", "tag", "pair", "w", "h", "fitWithin", "size", "maxEdge", "scale", "encode", "source", "quality", "canvas", "ctx", "format", "dataUrl", "captureScreenshotSync", "natural", "attempt", "captureScreenshot", "resolve", "settled", "settle", "value", "timer", "attachScreenshot", "payload", "screenshot", "EMPTY_STATE", "emptyAnnotationState", "createAnnotationSdk", "options", "parentOrigin", "assertConcreteOrigin", "win", "doc", "screenshotOptions", "resolveScreenshotOptions", "enabled", "overlay", "hoverFrame", "lastHovered", "post", "message", "boxOf", "el", "r", "targetFor", "resolveTarget", "labelFor", "target", "file", "onMouseMove", "event", "buildPayload", "state", "onClick", "payload", "attachScreenshot", "withShot", "onKeyDown", "disable", "enable", "createOverlay", "onMessage", "isParentMessage", "DEFAULT_LIMIT", "_items", "_limit", "RingBuffer", "limit", "__privateAdd", "__privateSet", "item", "__privateGet", "describeArg", "arg", "truncate", "text", "max", "formatConsoleArgs", "args", "firstStack", "isFailedStatus", "status", "defaultStateCollectorOptions", "installStateCollector", "win", "options", "consoleErrors", "failedRequests", "record", "level", "originalError", "originalWarn", "originalFetch", "requestInfo", "input", "init", "url", "method", "response", "error", "XHR", "originalOpen", "rest"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The hover highlight, drawn inside the iframe.
|
|
3
|
+
*
|
|
4
|
+
* The parent frame does not draw this, and that is a deliberate call from
|
|
5
|
+
* `spec/02-annotation-sdk.md`: translating coordinates across the frame boundary
|
|
6
|
+
* means tracking the iframe's position, the page's scroll, and any transform on
|
|
7
|
+
* either side, and getting it wrong puts the box next to the thing rather than
|
|
8
|
+
* on it. Inside the frame, `getBoundingClientRect` is already in the right
|
|
9
|
+
* coordinate space.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* The SDK'"'"'s own node, so this is not a cross-repository contract like the
|
|
13
|
+
* cookie and the query parameter are — nothing outside this package creates it
|
|
14
|
+
* or looks it up. Renamed with them anyway (A-98): a product whose stylesheet
|
|
15
|
+
* happens to target the old id would be styling the overlay by accident, and
|
|
16
|
+
* the new name is the one a reader will expect to find.
|
|
17
|
+
*/
|
|
18
|
+
export declare const OVERLAY_ID = "ai-annotation-overlay";
|
|
19
|
+
export interface Overlay {
|
|
20
|
+
show(box: {
|
|
21
|
+
x: number;
|
|
22
|
+
y: number;
|
|
23
|
+
w: number;
|
|
24
|
+
h: number;
|
|
25
|
+
}, label: string | null): void;
|
|
26
|
+
hide(): void;
|
|
27
|
+
destroy(): void;
|
|
28
|
+
/** The overlay's own nodes, so hit-testing can ignore them. */
|
|
29
|
+
contains(node: unknown): boolean;
|
|
30
|
+
}
|
|
31
|
+
export declare function createOverlay(doc: Document): Overlay;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The hover highlight, drawn inside the iframe.
|
|
3
|
+
*
|
|
4
|
+
* The parent frame does not draw this, and that is a deliberate call from
|
|
5
|
+
* `spec/02-annotation-sdk.md`: translating coordinates across the frame boundary
|
|
6
|
+
* means tracking the iframe's position, the page's scroll, and any transform on
|
|
7
|
+
* either side, and getting it wrong puts the box next to the thing rather than
|
|
8
|
+
* on it. Inside the frame, `getBoundingClientRect` is already in the right
|
|
9
|
+
* coordinate space.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* The SDK'"'"'s own node, so this is not a cross-repository contract like the
|
|
13
|
+
* cookie and the query parameter are — nothing outside this package creates it
|
|
14
|
+
* or looks it up. Renamed with them anyway (A-98): a product whose stylesheet
|
|
15
|
+
* happens to target the old id would be styling the overlay by accident, and
|
|
16
|
+
* the new name is the one a reader will expect to find.
|
|
17
|
+
*/
|
|
18
|
+
export declare const OVERLAY_ID = "ai-annotation-overlay";
|
|
19
|
+
export interface Overlay {
|
|
20
|
+
show(box: {
|
|
21
|
+
x: number;
|
|
22
|
+
y: number;
|
|
23
|
+
w: number;
|
|
24
|
+
h: number;
|
|
25
|
+
}, label: string | null): void;
|
|
26
|
+
hide(): void;
|
|
27
|
+
destroy(): void;
|
|
28
|
+
/** The overlay's own nodes, so hit-testing can ignore them. */
|
|
29
|
+
contains(node: unknown): boolean;
|
|
30
|
+
}
|
|
31
|
+
export declare function createOverlay(doc: Document): Overlay;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The hover highlight, drawn inside the iframe.
|
|
3
|
+
*
|
|
4
|
+
* The parent frame does not draw this, and that is a deliberate call from
|
|
5
|
+
* `spec/02-annotation-sdk.md`: translating coordinates across the frame boundary
|
|
6
|
+
* means tracking the iframe's position, the page's scroll, and any transform on
|
|
7
|
+
* either side, and getting it wrong puts the box next to the thing rather than
|
|
8
|
+
* on it. Inside the frame, `getBoundingClientRect` is already in the right
|
|
9
|
+
* coordinate space.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* The SDK'"'"'s own node, so this is not a cross-repository contract like the
|
|
13
|
+
* cookie and the query parameter are — nothing outside this package creates it
|
|
14
|
+
* or looks it up. Renamed with them anyway (A-98): a product whose stylesheet
|
|
15
|
+
* happens to target the old id would be styling the overlay by accident, and
|
|
16
|
+
* the new name is the one a reader will expect to find.
|
|
17
|
+
*/
|
|
18
|
+
export declare const OVERLAY_ID = "ai-annotation-overlay";
|
|
19
|
+
export interface Overlay {
|
|
20
|
+
show(box: {
|
|
21
|
+
x: number;
|
|
22
|
+
y: number;
|
|
23
|
+
w: number;
|
|
24
|
+
h: number;
|
|
25
|
+
}, label: string | null): void;
|
|
26
|
+
hide(): void;
|
|
27
|
+
destroy(): void;
|
|
28
|
+
/** The overlay's own nodes, so hit-testing can ignore them. */
|
|
29
|
+
contains(node: unknown): boolean;
|
|
30
|
+
}
|
|
31
|
+
export declare function createOverlay(doc: Document): Overlay;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var a=Object.defineProperty;var l=Object.getOwnPropertyDescriptor;var m=Object.getOwnPropertyNames;var d=Object.prototype.hasOwnProperty;var A=(t,e)=>{for(var n in e)a(t,n,{get:e[n],enumerable:!0})},f=(t,e,n,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of m(e))!d.call(t,r)&&r!==n&&a(t,r,{get:()=>e[r],enumerable:!(o=l(e,r))||o.enumerable});return t};var T=t=>f(a({},"__esModule",{value:!0}),t);var I={};A(I,{ANNOTATION_ARMED_COOKIE:()=>i,ANNOTATION_ARMED_COOKIES:()=>g,ANNOTATION_TOKEN_PARAM:()=>s,ANNOTATION_TOKEN_PARAMS:()=>u,LEGACY_ANNOTATION_ARMED_COOKIE:()=>p,LEGACY_ANNOTATION_TOKEN_PARAM:()=>c,NAME_ATTR:()=>x,SDK_VERSION:()=>N,SRC_ATTR:()=>y,assertConcreteOrigin:()=>b,isAnnotationArmed:()=>E,isParentMessage:()=>O,readAnnotationToken:()=>R});module.exports=T(I);var N="1",y="data-src",x="data-src-name",_=new Set(["annotate:enable","annotate:disable","annotate:ping"]);function O(t){if(typeof t!="object"||t===null)return!1;let e=t.type;return typeof e=="string"&&_.has(e)}function b(t){if(!t||t==="*")throw new Error("[annotation] a concrete parent origin is required; '*' would broadcast annotation payloads to any embedder.");return t}var s="ai_annotate",i="ai_annotate_armed",c="pits_annotate",p="pits_annotate_armed",u=[s,c],g=[i,p];function E(t){return t.split(";").some(e=>{let n=e.trimStart();return g.some(o=>n.startsWith(`${o}=`))})}function R(t){for(let e of u){let n=t.get(e);if(n!==null&&n.length>0)return n}return null}
|
|
2
|
+
//# sourceMappingURL=protocol.cjs.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/protocol.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * The annotation postMessage protocol (A-36).\n *\n * Mirrors `docs/contracts/types.ts`. That file is the\n * cross-component contract; this is the runtime copy the product bundle ships,\n * because the product cannot import from a docs directory. Changing a shape\n * here is a change to both \u2014 say so in the PR.\n */\n\nexport const SDK_VERSION = \"1\";\n\n/**\n * ===========================================================================\n * THE ATTRIBUTES \u2014 THE OTHER WIRE (A-186)\n * ===========================================================================\n *\n * These live in `protocol.ts`, not in `target.ts` where they are read, because\n * they are a contract between TWO PACKAGES that a consumer installs separately:\n * `@weatherboard/annotation-next` WRITES them at build time and this package\n * READS them at runtime. Two spellings of `data-src` is not a bug anybody\n * catches \u2014 the transform runs, the attributes appear, and every capture comes\n * back with no source ref at all, which is indistinguishable from the transform\n * not running.\n *\n * `annotation-next` imports these from `@weatherboard/annotation/protocol`\n * rather than declaring its own, which is why the subpath has no DOM in it: a\n * build tool importing the page SDK to learn two strings would be absurd.\n */\nexport const SRC_ATTR = \"data-src\";\nexport const NAME_ATTR = \"data-src-name\";\n\nexport interface SourceRef {\n file: string;\n line: number;\n column?: number;\n}\n\nexport interface BuildInfo {\n /** Mandatory. Feedback not pinned to a revision is ambiguous the moment someone merges. */\n gitSha: string;\n /**\n * Where this build came from.\n *\n * `\"development\"` was added in A-37, when the SDK was first mounted in a\n * real app and the two halves turned out not to agree: `resolveBuildInfo` in\n * `resolveBuildInfo` in the product's own Next config package \u2014 the module\n * that stamps the build-env variable, and the only producer of this field \u2014\n * emits\n * `production | preview | development`, mirroring `VERCEL_ENV`. This union\n * instead offered `\"staging\"`, which nothing produces: a staging deploy IS a\n * Vercel preview and reports itself as one. Widening rather than forcing the\n * mount to lie: a locally-annotated build calling itself a preview would put\n * someone's laptop in the corpus as though it were a deployment.\n */\n env: \"production\" | \"preview\" | \"staging\" | \"development\";\n branch?: string;\n /** Set when reviewing an agent's preview deploy, so feedback routes to that run's session. */\n previewRunId?: string;\n}\n\nexport interface ConsoleEntry {\n level: \"error\" | \"warn\";\n message: string;\n stack?: string;\n at: string;\n}\n\nexport interface NetworkEntry {\n url: string;\n method: string;\n status: number;\n at: string;\n}\n\nexport interface TargetInfo {\n /**\n * What the reviewer selected \u2014 one element, or an area (A-179).\n *\n * -------------------------------------------------------------------------\n * WHY THIS EXISTS RATHER THAN BEING INFERRED\n * -------------------------------------------------------------------------\n *\n * A region capture and an element capture are the same SHAPE: a bounding box,\n * a chain of source refs, a DOM path. Nothing in the values distinguishes\n * them, and the two mean opposite things about the chain \u2014 for an element it\n * is ANCESTORS, innermost first, so entry two contains entry one; for a\n * region it is SIBLINGS the rectangle happened to cross, which contain\n * nothing of each other.\n *\n * A reader that guesses will describe a region's siblings as a call-site\n * chain, which is a confident false statement about the page. `describeCapture`\n * hands that description to a model that writes acceptance criteria from it.\n *\n * -------------------------------------------------------------------------\n * WHY IT IS OPTIONAL, AND WHY ABSENT MEANS `element`\n * -------------------------------------------------------------------------\n *\n * The two halves of this protocol live in different repositories and deploy\n * independently (see the note on the legacy parameter names below). Every\n * capture in the store today, and every capture from a product bundle that\n * predates region selection, has no such field \u2014 and all of them are element\n * captures. Absent therefore has to mean `element`, or this field would\n * retroactively make the entire history ambiguous.\n *\n * Required-with-a-default would have the same effect and a worse failure: an\n * older bundle omits it, the type says it is there, and every reader believes\n * a value nobody sent.\n */\n selection?: \"element\" | \"region\";\n source?: SourceRef;\n /**\n * Every `data-src` from the clicked element up to the document root, innermost\n * first \u2014 an addition to the contract, and the reason is structural.\n *\n * Only host elements carry a ref (A-33), so `source` names the file that\n * *writes the markup*. For a composed design-system component that is\n * `MediaCard.tsx`, not the `GuideDetailCard.tsx` a human would say they\n * clicked. The chain is what lets the PM agent and the coding agent see both,\n * and pick the call site over the leaf when the feedback is about content\n * rather than the component itself.\n */\n sourceChain?: SourceRef[];\n componentName?: string;\n /** CSS selector fallback, for when no ancestor carries a ref at all. */\n domPath: string;\n boundingBox: { x: number; y: number; w: number; h: number };\n /** Truncated to 200 characters. */\n textContent?: string;\n}\n\nexport interface AnnotationPayload {\n schemaVersion: 1;\n capturedAt: string;\n build: BuildInfo;\n page: {\n url: string;\n /** The matched route pattern, not the resolved URL. */\n route: string;\n viewport: { w: number; h: number; dpr: number };\n userAgent: string;\n };\n target: TargetInfo;\n state: {\n consoleErrors: ConsoleEntry[];\n failedRequests: NetworkEntry[];\n featureFlags: Record<string, unknown>;\n };\n screenshot?: { dataUrl: string; region: \"element\" | \"viewport\" };\n /** The human's words, verbatim. The parent frame fills this in; we never rewrite it. */\n comment: string;\n}\n\nexport type ParentToFrame =\n | { type: \"annotate:enable\" }\n | { type: \"annotate:disable\" }\n | { type: \"annotate:ping\" };\n\nexport type FrameToParent =\n | { type: \"annotate:ready\"; sdkVersion: string; build: BuildInfo }\n | { type: \"annotate:hover\"; target: TargetInfo }\n | { type: \"annotate:capture\"; payload: AnnotationPayload }\n | { type: \"annotate:cancel\" };\n\nconst PARENT_TYPES = new Set([\"annotate:enable\", \"annotate:disable\", \"annotate:ping\"]);\n\n/**\n * Is this a message we recognise?\n *\n * Origin is checked separately and first \u2014 a well-formed message from the wrong\n * origin is the case this guard must not be trusted to catch.\n */\nexport function isParentMessage(data: unknown): data is ParentToFrame {\n if (typeof data !== \"object\" || data === null) return false;\n const type = (data as { type?: unknown }).type;\n return typeof type === \"string\" && PARENT_TYPES.has(type);\n}\n\n/**\n * Reject wildcards outright rather than treating them as \"any origin\".\n *\n * `postMessage(msg, \"*\")` broadcasts the payload to whatever happens to be\n * framing us, and an annotation payload carries page content, console errors and\n * failed request URLs. The spec's rule is no wildcards in either direction; this\n * makes it impossible to configure one by accident.\n */\nexport function assertConcreteOrigin(origin: string): string {\n if (!origin || origin === \"*\") {\n throw new Error(\n \"[annotation] a concrete parent origin is required; '*' would broadcast \" +\n \"annotation payloads to any embedder.\",\n );\n }\n return origin;\n}\n\n/**\n * The query parameter that arms the SDK in a managed product, and the cookie\n * that records it.\n *\n * THESE LIVE IN THE PROTOCOL BECAUSE THE TWO HALVES ARE NOW IN DIFFERENT\n * REPOSITORIES. The managed product's middleware looks for the parameter and\n * sets the cookie; the management app appends the parameter and the SDK reads\n * the cookie. While both halves were one monorepo, a test could read the other\n * side's source and assert the strings matched \u2014 that gate does not survive a\n * repository split, and a mismatch produces no error at all: the frame loads,\n * the cookie is never set, the SDK never boots, and the reviewer sees \"waiting\n * for the page to announce itself\" with nothing to go on.\n *\n * So they stop being two spellings that agree and become one exported\n * constant. A managed product that imports this package cannot drift from it.\n */\nexport const ANNOTATION_TOKEN_PARAM = \"ai_annotate\";\nexport const ANNOTATION_ARMED_COOKIE = \"ai_annotate_armed\";\n\n/**\n * The names these used to have (A-98).\n *\n * -------------------------------------------------------------------------\n * WHY THE RENAME, AND WHY IT COULD NOT JUST BE DONE\n * -------------------------------------------------------------------------\n *\n * They were `pits_annotate` and `pits_annotate_armed` \u2014 one managed product's\n * initialism, in a contract every managed product's bundle has to implement.\n * Onboarding a second product meant asking an unrelated codebase to adopt the\n * first product's name in its cookies and its query strings. That is the bug\n * A-98 is about, and it is a bug in a wire protocol rather than in a variable\n * name, which is what makes it expensive.\n *\n * The two halves live in DIFFERENT REPOSITORIES and deploy independently. A\n * clean rename is therefore a breaking change to every product bundle\n * simultaneously, and its failure mode is silence: the frame loads, the cookie\n * is never set, the SDK never boots, and the reviewer sees \"waiting for the\n * page to announce itself\" with nothing to go on. Nothing throws, nothing logs,\n * and the natural reading is that the agent broke the site.\n *\n * -------------------------------------------------------------------------\n * THE MIGRATION: READ BOTH, WRITE BOTH, THEN STOP WRITING THE OLD ONE\n * -------------------------------------------------------------------------\n *\n * 1. **Now.** Every reader accepts either name. Every writer emits BOTH, so a\n * product still running the old bundle arms on the legacy name and one\n * running the new bundle arms on the new one. Deploy order stops mattering,\n * which is the whole point \u2014 there is no window in which a reviewer sees the\n * silent failure above.\n * 2. **After every managed product's bundle is on this version** (A-102). Drop\n * the legacy names from the writers, then from the readers. Two steps, not\n * one: a reader that stops accepting the old name before the writers stop\n * emitting it re-creates the outage in the other direction.\n *\n * The legacy names are exported rather than inlined so the deletion in step 2\n * is `git grep LEGACY_` rather than an archaeology exercise.\n */\nexport const LEGACY_ANNOTATION_TOKEN_PARAM = \"pits_annotate\";\nexport const LEGACY_ANNOTATION_ARMED_COOKIE = \"pits_annotate_armed\";\n\n/**\n * ===========================================================================\n * AND THE NAMES ARE NOT CHANGING AGAIN FOR THE PUBLISHED PACKAGE (A-184)\n * ===========================================================================\n *\n * When this becomes `@weatherboard/annotation` the obvious next thought is that\n * the wire names should match the package \u2014 `weatherboard_annotate`. They\n * should not, and the reason is the migration described above rather than\n * taste.\n *\n * That migration is STILL IN FLIGHT. Readers accept both spellings; writers\n * emit both; step 2 waits on every managed product's bundle being current.\n * A third spelling introduced now does not replace the second, it joins it:\n * every reader accepts three names, every writer emits three, and the window in\n * which a product can be on any of them widens instead of closing.\n *\n * The cost is not the code. It is that each rename buys another instance of the\n * failure this file exists to prevent \u2014 the frame loads, the cookie is never\n * set, the SDK never boots, and the reviewer sees \"waiting for the page to\n * announce itself\" with nothing to go on. Twice is a migration. Three times is\n * a habit, and the third one would be paid by consumers who never asked for\n * either.\n *\n * `ai_annotate` is already product-neutral, which was the entire point of A-98.\n * A published package needs names that name no product; it does not need names\n * that name the package. Nobody reading a query string cares which npm scope\n * the library came from.\n *\n * The one real objection is collision: `ai_` is a common prefix and some other\n * tool could plausibly use it. That is survivable by construction \u2014 arming\n * requires the parameter to carry a token the product compares exactly, so a\n * coincidental `?ai_annotate=` from an unrelated tool arms nothing. A colliding\n * COOKIE cannot false-positive either; `isAnnotationArmed` matches on the\n * delimiter rather than by substring, for exactly this reason.\n *\n * If a rename is ever genuinely warranted, it happens ONCE, after step 2 has\n * landed and the reader accepts one name again \u2014 not layered on top of an\n * unfinished one.\n */\n\n/** Both spellings of the query parameter, current first. */\nexport const ANNOTATION_TOKEN_PARAMS = [\n ANNOTATION_TOKEN_PARAM,\n LEGACY_ANNOTATION_TOKEN_PARAM,\n] as const;\n\n/** Both spellings of the cookie, current first. */\nexport const ANNOTATION_ARMED_COOKIES = [\n ANNOTATION_ARMED_COOKIE,\n LEGACY_ANNOTATION_ARMED_COOKIE,\n] as const;\n\n/**\n * Whether a cookie jar carries the armed cookie, under either name.\n *\n * Delimiter-aware rather than a substring test: a cookie named\n * `not_ai_annotate_armed`, or another cookie whose VALUE contains the name,\n * satisfies `includes` \u2014 and arming an annotation channel on a coincidence is\n * not a failure anyone would think to look for.\n */\nexport function isAnnotationArmed(cookie: string): boolean {\n return cookie\n .split(\";\")\n .some((entry) => {\n const trimmed = entry.trimStart();\n return ANNOTATION_ARMED_COOKIES.some((name) => trimmed.startsWith(`${name}=`));\n });\n}\n\n/**\n * The arming token from a URL's query string, under either name.\n *\n * Current name wins when both are present. They should never disagree \u2014 the\n * management app writes both from one value \u2014 but if they ever do, the one this\n * version of the protocol owns is the one to believe.\n */\nexport function readAnnotationToken(params: URLSearchParams): string | null {\n for (const name of ANNOTATION_TOKEN_PARAMS) {\n const value = params.get(name);\n if (value !== null && value.length > 0) return value;\n }\n return null;\n}\n"],
|
|
5
|
+
"mappings": "yaAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,6BAAAE,EAAA,6BAAAC,EAAA,2BAAAC,EAAA,4BAAAC,EAAA,mCAAAC,EAAA,kCAAAC,EAAA,cAAAC,EAAA,gBAAAC,EAAA,aAAAC,EAAA,yBAAAC,EAAA,sBAAAC,EAAA,oBAAAC,EAAA,wBAAAC,IAAA,eAAAC,EAAAf,GASO,IAAMS,EAAc,IAmBdC,EAAW,WACXF,EAAY,gBAsInBQ,EAAe,IAAI,IAAI,CAAC,kBAAmB,mBAAoB,eAAe,CAAC,EAQ9E,SAASH,EAAgBI,EAAsC,CACpE,GAAI,OAAOA,GAAS,UAAYA,IAAS,KAAM,MAAO,GACtD,IAAMC,EAAQD,EAA4B,KAC1C,OAAO,OAAOC,GAAS,UAAYF,EAAa,IAAIE,CAAI,CAC1D,CAUO,SAASP,EAAqBQ,EAAwB,CAC3D,GAAI,CAACA,GAAUA,IAAW,IACxB,MAAM,IAAI,MACR,6GAEF,EAEF,OAAOA,CACT,CAkBO,IAAMf,EAAyB,cACzBF,EAA0B,oBAwC1BK,EAAgC,gBAChCD,EAAiC,sBA2CjCD,EAA0B,CACrCD,EACAG,CACF,EAGaJ,EAA2B,CACtCD,EACAI,CACF,EAUO,SAASM,EAAkBQ,EAAyB,CACzD,OAAOA,EACJ,MAAM,GAAG,EACT,KAAMC,GAAU,CACf,IAAMC,EAAUD,EAAM,UAAU,EAChC,OAAOlB,EAAyB,KAAMoB,GAASD,EAAQ,WAAW,GAAGC,CAAI,GAAG,CAAC,CAC/E,CAAC,CACL,CASO,SAAST,EAAoBU,EAAwC,CAC1E,QAAWD,KAAQlB,EAAyB,CAC1C,IAAMoB,EAAQD,EAAO,IAAID,CAAI,EAC7B,GAAIE,IAAU,MAAQA,EAAM,OAAS,EAAG,OAAOA,CACjD,CACA,OAAO,IACT",
|
|
6
|
+
"names": ["protocol_exports", "__export", "ANNOTATION_ARMED_COOKIE", "ANNOTATION_ARMED_COOKIES", "ANNOTATION_TOKEN_PARAM", "ANNOTATION_TOKEN_PARAMS", "LEGACY_ANNOTATION_ARMED_COOKIE", "LEGACY_ANNOTATION_TOKEN_PARAM", "NAME_ATTR", "SDK_VERSION", "SRC_ATTR", "assertConcreteOrigin", "isAnnotationArmed", "isParentMessage", "readAnnotationToken", "__toCommonJS", "PARENT_TYPES", "data", "type", "origin", "cookie", "entry", "trimmed", "name", "params", "value"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The annotation postMessage protocol (A-36).
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `docs/contracts/types.ts`. That file is the
|
|
5
|
+
* cross-component contract; this is the runtime copy the product bundle ships,
|
|
6
|
+
* because the product cannot import from a docs directory. Changing a shape
|
|
7
|
+
* here is a change to both — say so in the PR.
|
|
8
|
+
*/
|
|
9
|
+
export declare const SDK_VERSION = "1";
|
|
10
|
+
/**
|
|
11
|
+
* ===========================================================================
|
|
12
|
+
* THE ATTRIBUTES — THE OTHER WIRE (A-186)
|
|
13
|
+
* ===========================================================================
|
|
14
|
+
*
|
|
15
|
+
* These live in `protocol.ts`, not in `target.ts` where they are read, because
|
|
16
|
+
* they are a contract between TWO PACKAGES that a consumer installs separately:
|
|
17
|
+
* `@weatherboard/annotation-next` WRITES them at build time and this package
|
|
18
|
+
* READS them at runtime. Two spellings of `data-src` is not a bug anybody
|
|
19
|
+
* catches — the transform runs, the attributes appear, and every capture comes
|
|
20
|
+
* back with no source ref at all, which is indistinguishable from the transform
|
|
21
|
+
* not running.
|
|
22
|
+
*
|
|
23
|
+
* `annotation-next` imports these from `@weatherboard/annotation/protocol`
|
|
24
|
+
* rather than declaring its own, which is why the subpath has no DOM in it: a
|
|
25
|
+
* build tool importing the page SDK to learn two strings would be absurd.
|
|
26
|
+
*/
|
|
27
|
+
export declare const SRC_ATTR = "data-src";
|
|
28
|
+
export declare const NAME_ATTR = "data-src-name";
|
|
29
|
+
export interface SourceRef {
|
|
30
|
+
file: string;
|
|
31
|
+
line: number;
|
|
32
|
+
column?: number;
|
|
33
|
+
}
|
|
34
|
+
export interface BuildInfo {
|
|
35
|
+
/** Mandatory. Feedback not pinned to a revision is ambiguous the moment someone merges. */
|
|
36
|
+
gitSha: string;
|
|
37
|
+
/**
|
|
38
|
+
* Where this build came from.
|
|
39
|
+
*
|
|
40
|
+
* `"development"` was added in A-37, when the SDK was first mounted in a
|
|
41
|
+
* real app and the two halves turned out not to agree: `resolveBuildInfo` in
|
|
42
|
+
* `resolveBuildInfo` in the product's own Next config package — the module
|
|
43
|
+
* that stamps the build-env variable, and the only producer of this field —
|
|
44
|
+
* emits
|
|
45
|
+
* `production | preview | development`, mirroring `VERCEL_ENV`. This union
|
|
46
|
+
* instead offered `"staging"`, which nothing produces: a staging deploy IS a
|
|
47
|
+
* Vercel preview and reports itself as one. Widening rather than forcing the
|
|
48
|
+
* mount to lie: a locally-annotated build calling itself a preview would put
|
|
49
|
+
* someone's laptop in the corpus as though it were a deployment.
|
|
50
|
+
*/
|
|
51
|
+
env: "production" | "preview" | "staging" | "development";
|
|
52
|
+
branch?: string;
|
|
53
|
+
/** Set when reviewing an agent's preview deploy, so feedback routes to that run's session. */
|
|
54
|
+
previewRunId?: string;
|
|
55
|
+
}
|
|
56
|
+
export interface ConsoleEntry {
|
|
57
|
+
level: "error" | "warn";
|
|
58
|
+
message: string;
|
|
59
|
+
stack?: string;
|
|
60
|
+
at: string;
|
|
61
|
+
}
|
|
62
|
+
export interface NetworkEntry {
|
|
63
|
+
url: string;
|
|
64
|
+
method: string;
|
|
65
|
+
status: number;
|
|
66
|
+
at: string;
|
|
67
|
+
}
|
|
68
|
+
export interface TargetInfo {
|
|
69
|
+
/**
|
|
70
|
+
* What the reviewer selected — one element, or an area (A-179).
|
|
71
|
+
*
|
|
72
|
+
* -------------------------------------------------------------------------
|
|
73
|
+
* WHY THIS EXISTS RATHER THAN BEING INFERRED
|
|
74
|
+
* -------------------------------------------------------------------------
|
|
75
|
+
*
|
|
76
|
+
* A region capture and an element capture are the same SHAPE: a bounding box,
|
|
77
|
+
* a chain of source refs, a DOM path. Nothing in the values distinguishes
|
|
78
|
+
* them, and the two mean opposite things about the chain — for an element it
|
|
79
|
+
* is ANCESTORS, innermost first, so entry two contains entry one; for a
|
|
80
|
+
* region it is SIBLINGS the rectangle happened to cross, which contain
|
|
81
|
+
* nothing of each other.
|
|
82
|
+
*
|
|
83
|
+
* A reader that guesses will describe a region's siblings as a call-site
|
|
84
|
+
* chain, which is a confident false statement about the page. `describeCapture`
|
|
85
|
+
* hands that description to a model that writes acceptance criteria from it.
|
|
86
|
+
*
|
|
87
|
+
* -------------------------------------------------------------------------
|
|
88
|
+
* WHY IT IS OPTIONAL, AND WHY ABSENT MEANS `element`
|
|
89
|
+
* -------------------------------------------------------------------------
|
|
90
|
+
*
|
|
91
|
+
* The two halves of this protocol live in different repositories and deploy
|
|
92
|
+
* independently (see the note on the legacy parameter names below). Every
|
|
93
|
+
* capture in the store today, and every capture from a product bundle that
|
|
94
|
+
* predates region selection, has no such field — and all of them are element
|
|
95
|
+
* captures. Absent therefore has to mean `element`, or this field would
|
|
96
|
+
* retroactively make the entire history ambiguous.
|
|
97
|
+
*
|
|
98
|
+
* Required-with-a-default would have the same effect and a worse failure: an
|
|
99
|
+
* older bundle omits it, the type says it is there, and every reader believes
|
|
100
|
+
* a value nobody sent.
|
|
101
|
+
*/
|
|
102
|
+
selection?: "element" | "region";
|
|
103
|
+
source?: SourceRef;
|
|
104
|
+
/**
|
|
105
|
+
* Every `data-src` from the clicked element up to the document root, innermost
|
|
106
|
+
* first — an addition to the contract, and the reason is structural.
|
|
107
|
+
*
|
|
108
|
+
* Only host elements carry a ref (A-33), so `source` names the file that
|
|
109
|
+
* *writes the markup*. For a composed design-system component that is
|
|
110
|
+
* `MediaCard.tsx`, not the `GuideDetailCard.tsx` a human would say they
|
|
111
|
+
* clicked. The chain is what lets the PM agent and the coding agent see both,
|
|
112
|
+
* and pick the call site over the leaf when the feedback is about content
|
|
113
|
+
* rather than the component itself.
|
|
114
|
+
*/
|
|
115
|
+
sourceChain?: SourceRef[];
|
|
116
|
+
componentName?: string;
|
|
117
|
+
/** CSS selector fallback, for when no ancestor carries a ref at all. */
|
|
118
|
+
domPath: string;
|
|
119
|
+
boundingBox: {
|
|
120
|
+
x: number;
|
|
121
|
+
y: number;
|
|
122
|
+
w: number;
|
|
123
|
+
h: number;
|
|
124
|
+
};
|
|
125
|
+
/** Truncated to 200 characters. */
|
|
126
|
+
textContent?: string;
|
|
127
|
+
}
|
|
128
|
+
export interface AnnotationPayload {
|
|
129
|
+
schemaVersion: 1;
|
|
130
|
+
capturedAt: string;
|
|
131
|
+
build: BuildInfo;
|
|
132
|
+
page: {
|
|
133
|
+
url: string;
|
|
134
|
+
/** The matched route pattern, not the resolved URL. */
|
|
135
|
+
route: string;
|
|
136
|
+
viewport: {
|
|
137
|
+
w: number;
|
|
138
|
+
h: number;
|
|
139
|
+
dpr: number;
|
|
140
|
+
};
|
|
141
|
+
userAgent: string;
|
|
142
|
+
};
|
|
143
|
+
target: TargetInfo;
|
|
144
|
+
state: {
|
|
145
|
+
consoleErrors: ConsoleEntry[];
|
|
146
|
+
failedRequests: NetworkEntry[];
|
|
147
|
+
featureFlags: Record<string, unknown>;
|
|
148
|
+
};
|
|
149
|
+
screenshot?: {
|
|
150
|
+
dataUrl: string;
|
|
151
|
+
region: "element" | "viewport";
|
|
152
|
+
};
|
|
153
|
+
/** The human's words, verbatim. The parent frame fills this in; we never rewrite it. */
|
|
154
|
+
comment: string;
|
|
155
|
+
}
|
|
156
|
+
export type ParentToFrame = {
|
|
157
|
+
type: "annotate:enable";
|
|
158
|
+
} | {
|
|
159
|
+
type: "annotate:disable";
|
|
160
|
+
} | {
|
|
161
|
+
type: "annotate:ping";
|
|
162
|
+
};
|
|
163
|
+
export type FrameToParent = {
|
|
164
|
+
type: "annotate:ready";
|
|
165
|
+
sdkVersion: string;
|
|
166
|
+
build: BuildInfo;
|
|
167
|
+
} | {
|
|
168
|
+
type: "annotate:hover";
|
|
169
|
+
target: TargetInfo;
|
|
170
|
+
} | {
|
|
171
|
+
type: "annotate:capture";
|
|
172
|
+
payload: AnnotationPayload;
|
|
173
|
+
} | {
|
|
174
|
+
type: "annotate:cancel";
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* Is this a message we recognise?
|
|
178
|
+
*
|
|
179
|
+
* Origin is checked separately and first — a well-formed message from the wrong
|
|
180
|
+
* origin is the case this guard must not be trusted to catch.
|
|
181
|
+
*/
|
|
182
|
+
export declare function isParentMessage(data: unknown): data is ParentToFrame;
|
|
183
|
+
/**
|
|
184
|
+
* Reject wildcards outright rather than treating them as "any origin".
|
|
185
|
+
*
|
|
186
|
+
* `postMessage(msg, "*")` broadcasts the payload to whatever happens to be
|
|
187
|
+
* framing us, and an annotation payload carries page content, console errors and
|
|
188
|
+
* failed request URLs. The spec's rule is no wildcards in either direction; this
|
|
189
|
+
* makes it impossible to configure one by accident.
|
|
190
|
+
*/
|
|
191
|
+
export declare function assertConcreteOrigin(origin: string): string;
|
|
192
|
+
/**
|
|
193
|
+
* The query parameter that arms the SDK in a managed product, and the cookie
|
|
194
|
+
* that records it.
|
|
195
|
+
*
|
|
196
|
+
* THESE LIVE IN THE PROTOCOL BECAUSE THE TWO HALVES ARE NOW IN DIFFERENT
|
|
197
|
+
* REPOSITORIES. The managed product's middleware looks for the parameter and
|
|
198
|
+
* sets the cookie; the management app appends the parameter and the SDK reads
|
|
199
|
+
* the cookie. While both halves were one monorepo, a test could read the other
|
|
200
|
+
* side's source and assert the strings matched — that gate does not survive a
|
|
201
|
+
* repository split, and a mismatch produces no error at all: the frame loads,
|
|
202
|
+
* the cookie is never set, the SDK never boots, and the reviewer sees "waiting
|
|
203
|
+
* for the page to announce itself" with nothing to go on.
|
|
204
|
+
*
|
|
205
|
+
* So they stop being two spellings that agree and become one exported
|
|
206
|
+
* constant. A managed product that imports this package cannot drift from it.
|
|
207
|
+
*/
|
|
208
|
+
export declare const ANNOTATION_TOKEN_PARAM = "ai_annotate";
|
|
209
|
+
export declare const ANNOTATION_ARMED_COOKIE = "ai_annotate_armed";
|
|
210
|
+
/**
|
|
211
|
+
* The names these used to have (A-98).
|
|
212
|
+
*
|
|
213
|
+
* -------------------------------------------------------------------------
|
|
214
|
+
* WHY THE RENAME, AND WHY IT COULD NOT JUST BE DONE
|
|
215
|
+
* -------------------------------------------------------------------------
|
|
216
|
+
*
|
|
217
|
+
* They were `pits_annotate` and `pits_annotate_armed` — one managed product's
|
|
218
|
+
* initialism, in a contract every managed product's bundle has to implement.
|
|
219
|
+
* Onboarding a second product meant asking an unrelated codebase to adopt the
|
|
220
|
+
* first product's name in its cookies and its query strings. That is the bug
|
|
221
|
+
* A-98 is about, and it is a bug in a wire protocol rather than in a variable
|
|
222
|
+
* name, which is what makes it expensive.
|
|
223
|
+
*
|
|
224
|
+
* The two halves live in DIFFERENT REPOSITORIES and deploy independently. A
|
|
225
|
+
* clean rename is therefore a breaking change to every product bundle
|
|
226
|
+
* simultaneously, and its failure mode is silence: the frame loads, the cookie
|
|
227
|
+
* is never set, the SDK never boots, and the reviewer sees "waiting for the
|
|
228
|
+
* page to announce itself" with nothing to go on. Nothing throws, nothing logs,
|
|
229
|
+
* and the natural reading is that the agent broke the site.
|
|
230
|
+
*
|
|
231
|
+
* -------------------------------------------------------------------------
|
|
232
|
+
* THE MIGRATION: READ BOTH, WRITE BOTH, THEN STOP WRITING THE OLD ONE
|
|
233
|
+
* -------------------------------------------------------------------------
|
|
234
|
+
*
|
|
235
|
+
* 1. **Now.** Every reader accepts either name. Every writer emits BOTH, so a
|
|
236
|
+
* product still running the old bundle arms on the legacy name and one
|
|
237
|
+
* running the new bundle arms on the new one. Deploy order stops mattering,
|
|
238
|
+
* which is the whole point — there is no window in which a reviewer sees the
|
|
239
|
+
* silent failure above.
|
|
240
|
+
* 2. **After every managed product's bundle is on this version** (A-102). Drop
|
|
241
|
+
* the legacy names from the writers, then from the readers. Two steps, not
|
|
242
|
+
* one: a reader that stops accepting the old name before the writers stop
|
|
243
|
+
* emitting it re-creates the outage in the other direction.
|
|
244
|
+
*
|
|
245
|
+
* The legacy names are exported rather than inlined so the deletion in step 2
|
|
246
|
+
* is `git grep LEGACY_` rather than an archaeology exercise.
|
|
247
|
+
*/
|
|
248
|
+
export declare const LEGACY_ANNOTATION_TOKEN_PARAM = "pits_annotate";
|
|
249
|
+
export declare const LEGACY_ANNOTATION_ARMED_COOKIE = "pits_annotate_armed";
|
|
250
|
+
/**
|
|
251
|
+
* ===========================================================================
|
|
252
|
+
* AND THE NAMES ARE NOT CHANGING AGAIN FOR THE PUBLISHED PACKAGE (A-184)
|
|
253
|
+
* ===========================================================================
|
|
254
|
+
*
|
|
255
|
+
* When this becomes `@weatherboard/annotation` the obvious next thought is that
|
|
256
|
+
* the wire names should match the package — `weatherboard_annotate`. They
|
|
257
|
+
* should not, and the reason is the migration described above rather than
|
|
258
|
+
* taste.
|
|
259
|
+
*
|
|
260
|
+
* That migration is STILL IN FLIGHT. Readers accept both spellings; writers
|
|
261
|
+
* emit both; step 2 waits on every managed product's bundle being current.
|
|
262
|
+
* A third spelling introduced now does not replace the second, it joins it:
|
|
263
|
+
* every reader accepts three names, every writer emits three, and the window in
|
|
264
|
+
* which a product can be on any of them widens instead of closing.
|
|
265
|
+
*
|
|
266
|
+
* The cost is not the code. It is that each rename buys another instance of the
|
|
267
|
+
* failure this file exists to prevent — the frame loads, the cookie is never
|
|
268
|
+
* set, the SDK never boots, and the reviewer sees "waiting for the page to
|
|
269
|
+
* announce itself" with nothing to go on. Twice is a migration. Three times is
|
|
270
|
+
* a habit, and the third one would be paid by consumers who never asked for
|
|
271
|
+
* either.
|
|
272
|
+
*
|
|
273
|
+
* `ai_annotate` is already product-neutral, which was the entire point of A-98.
|
|
274
|
+
* A published package needs names that name no product; it does not need names
|
|
275
|
+
* that name the package. Nobody reading a query string cares which npm scope
|
|
276
|
+
* the library came from.
|
|
277
|
+
*
|
|
278
|
+
* The one real objection is collision: `ai_` is a common prefix and some other
|
|
279
|
+
* tool could plausibly use it. That is survivable by construction — arming
|
|
280
|
+
* requires the parameter to carry a token the product compares exactly, so a
|
|
281
|
+
* coincidental `?ai_annotate=` from an unrelated tool arms nothing. A colliding
|
|
282
|
+
* COOKIE cannot false-positive either; `isAnnotationArmed` matches on the
|
|
283
|
+
* delimiter rather than by substring, for exactly this reason.
|
|
284
|
+
*
|
|
285
|
+
* If a rename is ever genuinely warranted, it happens ONCE, after step 2 has
|
|
286
|
+
* landed and the reader accepts one name again — not layered on top of an
|
|
287
|
+
* unfinished one.
|
|
288
|
+
*/
|
|
289
|
+
/** Both spellings of the query parameter, current first. */
|
|
290
|
+
export declare const ANNOTATION_TOKEN_PARAMS: readonly ["ai_annotate", "pits_annotate"];
|
|
291
|
+
/** Both spellings of the cookie, current first. */
|
|
292
|
+
export declare const ANNOTATION_ARMED_COOKIES: readonly ["ai_annotate_armed", "pits_annotate_armed"];
|
|
293
|
+
/**
|
|
294
|
+
* Whether a cookie jar carries the armed cookie, under either name.
|
|
295
|
+
*
|
|
296
|
+
* Delimiter-aware rather than a substring test: a cookie named
|
|
297
|
+
* `not_ai_annotate_armed`, or another cookie whose VALUE contains the name,
|
|
298
|
+
* satisfies `includes` — and arming an annotation channel on a coincidence is
|
|
299
|
+
* not a failure anyone would think to look for.
|
|
300
|
+
*/
|
|
301
|
+
export declare function isAnnotationArmed(cookie: string): boolean;
|
|
302
|
+
/**
|
|
303
|
+
* The arming token from a URL's query string, under either name.
|
|
304
|
+
*
|
|
305
|
+
* Current name wins when both are present. They should never disagree — the
|
|
306
|
+
* management app writes both from one value — but if they ever do, the one this
|
|
307
|
+
* version of the protocol owns is the one to believe.
|
|
308
|
+
*/
|
|
309
|
+
export declare function readAnnotationToken(params: URLSearchParams): string | null;
|