@weatherboard/annotation 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ADOPTING.md +52 -0
- package/README.md +9 -1
- package/dist/anchor.cjs +2 -0
- package/dist/anchor.cjs.map +7 -0
- package/dist/anchor.d.cts +295 -0
- package/dist/anchor.d.mts +295 -0
- package/dist/anchor.d.ts +295 -0
- package/dist/anchor.mjs +2 -0
- package/dist/anchor.mjs.map +7 -0
- package/dist/arming.d.cts +30 -2
- package/dist/arming.d.mts +30 -2
- package/dist/arming.d.ts +30 -2
- package/dist/host.cjs +1 -1
- package/dist/host.cjs.map +1 -1
- package/dist/host.mjs.map +1 -1
- package/dist/index.cjs +4 -4
- package/dist/index.cjs.map +3 -3
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +4 -4
- package/dist/index.mjs.map +3 -3
- package/dist/markers.cjs +57 -0
- package/dist/markers.cjs.map +7 -0
- package/dist/markers.d.cts +246 -0
- package/dist/markers.d.mts +246 -0
- package/dist/markers.d.ts +246 -0
- package/dist/markers.mjs +57 -0
- package/dist/markers.mjs.map +7 -0
- package/dist/protocol.cjs +1 -1
- package/dist/protocol.cjs.map +3 -3
- package/dist/protocol.d.cts +29 -0
- package/dist/protocol.d.mts +29 -0
- package/dist/protocol.d.ts +29 -0
- package/dist/protocol.mjs +1 -1
- package/dist/protocol.mjs.map +3 -3
- package/dist/read.cjs +2 -0
- package/dist/read.cjs.map +7 -0
- package/dist/read.d.cts +149 -0
- package/dist/read.d.mts +149 -0
- package/dist/read.d.ts +149 -0
- package/dist/read.mjs +2 -0
- package/dist/read.mjs.map +7 -0
- package/dist/transport.d.cts +7 -1
- package/dist/transport.d.mts +7 -1
- package/dist/transport.d.ts +7 -1
- package/package.json +48 -5
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/protocol.ts", "../src/target.ts", "../src/anchor.ts", "../src/markers.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\n/**\n * What kind of capture this is, with the default applied once (A-189).\n *\n * `selection` is optional and **absent means `element`** \u2014 every capture taken\n * before A-179, and everything the managed product's live bundle emits today,\n * has no such field. Any other default would retroactively reclassify the whole\n * history as regions, silently.\n *\n * A function rather than a documented convention because the default is the\n * kind of thing two readers implement slightly differently and neither notices:\n * one writes `=== \"region\"`, another writes `?? \"element\"`, a third forgets and\n * treats `undefined` as falsy in a branch that meant something else. This is\n * the protocol's answer, and `compatibility.test.ts` replays every recorded\n * payload through it.\n */\nexport function selectionOf(target: Pick<TargetInfo, \"selection\">): \"element\" | \"region\" {\n return target.selection === \"region\" ? \"region\" : \"element\";\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 CAPTURE token's query parameter (A-240).\n *\n * -------------------------------------------------------------------------\n * WHY IT IS HERE AND NOT WHERE IT IS WRITTEN\n * -------------------------------------------------------------------------\n *\n * A second name on the same wire, and a second credential \u2014 see\n * `docs/plans/first-party-annotation.md` \u00A7 *Authentication*. `ai_annotate`\n * answers *should this page offer an annotation UI at all*; this one answers\n * *may this capture become an issue*. Different secrets, different lifetimes,\n * different blast radii, and deliberately not folded into one token.\n *\n * It lives beside `ANNOTATION_TOKEN_PARAM` for that constant's own reason,\n * stated at length above: the console WRITES it into a deep link and the\n * product's bundle READS it, the two halves ship from different repositories,\n * and a mismatch produces no error anywhere \u2014 the reviewer writes a post-it,\n * clicks send, and the ingest rejects an unsigned capture. One exported\n * constant is the only thing that makes those two spellings unable to drift.\n *\n * It has no legacy spelling because it has never had another one: nothing has\n * shipped it yet. It is exported here BEFORE its writer needs it precisely so\n * the writer never has a reason to spell it itself.\n */\nexport const CAPTURE_TOKEN_PARAM = \"ai_capture\";\n\n/**\n * The run under review, when the console knows of one (A-241).\n *\n * Third name on the same wire, here for the reason the two above are: the\n * console writes it and the product's bundle reads it, from two repositories.\n * `apps/manager/src/lib/request-changes.ts` spells it itself today and its own\n * comment asks for exactly this \u2014 the constant to move into the package so the\n * writer has no reason to spell it. **That deletion is not this slice's**\n * (A-243/A-244 are merged and settled), so for now there are two definitions of\n * one string, and `protocol.test.ts` is the only thing standing between them\n * and a silent divergence. Nothing about it is signed: the ingest already\n * treats it as attacker-supplied text and files on it rather than routing on it.\n */\nexport const REVIEW_RUN_PARAM = \"ai_run\";\n\n/**\n * The READ token's query parameter (A-277).\n *\n * ===========================================================================\n * WHY THE MARKER LAYER GETS ITS OWN CREDENTIAL AND NOT `ai_capture`\n * ===========================================================================\n *\n * Fourth name on the same wire, and the reason it is a fourth is A-276's\n * disclosure decision rather than tidiness. The capture token is minted for\n * WRITING and lives four hours so a reviewer can work through a session; reusing\n * it for the read would make one leaked deep link a write capability AND a read\n * capability over every reviewer's comments on that origin for the same window.\n * The scope is a signed field, `verifyCaptureToken` requires an expectation at\n * every call site, and the read endpoint refuses a write claim \u2014 so handing\n * `ai_capture` to the reader does not degrade to a read, it fails.\n *\n * The read claim's TTL is ONE hour rather than four (`READ_TOKEN_TTL_S`), which\n * is the practical consequence a reader of this constant should know: the\n * toolbar outlives the markers, so a reviewer who leaves a tab open all\n * afternoon will find the count line saying it cannot read before they find the\n * Send button broken. That is the intended ordering \u2014 the shorter credential is\n * the one that discloses other people's words.\n *\n * Exported here for the reason the three above are: the console writes it into a\n * deep link and the product's bundle reads it, from two repositories, and a\n * mismatch produces no error anywhere \u2014 just a page with no markers, which is\n * indistinguishable from nine other causes.\n */\nexport const READ_TOKEN_PARAM = \"ai_read\";\n\n/**\n * The arming cookie, as a `Set-Cookie` value (A-241).\n *\n * ===========================================================================\n * IT GOT SIMPLER BY MOVING FIRST-PARTY, AND THAT IS THE WHOLE POINT\n * ===========================================================================\n *\n * Framed, this had to be `SameSite=None; Secure` with no `Partitioned` \u2014 a\n * genuine cross-site cookie, which A-211 records would have broken on its own\n * under Chrome's third-party cookie work, independently of the auth problem\n * that killed the frame. Top-level at the product's own origin it is an\n * ordinary cookie and none of that applies.\n *\n * Each attribute, and why:\n *\n * - **`SameSite=Lax`.** The reviewer arrives by clicking *Request changes* in\n * the console, which is a top-level cross-site GET navigation \u2014 the one case\n * `Lax` still sends on. `Strict` would withhold the cookie on that very first\n * render, so the toolbar would be missing on the page the link opened and\n * appear only after an internal click; that reads as a broken deploy.\n * - **`Secure`.** It is a marker rather than a credential, but a marker that\n * travels in clear is a marker an attacker on the path can set.\n * - **`Path=/`.** Arming survives the reviewer navigating the whole app, which\n * is the point of a cookie rather than a URL parameter.\n * - **No `Max-Age` / `Expires`.** Session-scoped: closing the tab disarms.\n *\n * ===========================================================================\n * WHAT IS *NOT* HERE: `HttpOnly`, AND THE TOKEN\n * ===========================================================================\n *\n * No `HttpOnly`, deliberately. Page script both READS this (`isAnnotationArmed`\n * decides whether a toolbar exists) and CLEARS it (*Done* disarms). An\n * `HttpOnly` arming cookie makes *Done* impossible from the page, which is the\n * one control the reviewer has.\n *\n * That is affordable only because THE VALUE IS A MARKER AND NEVER THE TOKEN.\n * The value is a constant and there is no argument that supplies one: the\n * middleware compares the URL parameter against its secret and then calls this,\n * and what it stores is *yes*, not *what was presented*. The single parameter\n * is the cookie's NAME, and it exists only so a product mid-migration can write\n * the legacy spelling \u2014 it is not a value slot wearing a different label, and\n * `arming.test.ts` pins which side of the `=` it lands on.\n *\n * This is a shape argument, not a guarantee about callers. Nothing stops a\n * determined caller passing a secret as the name; what the signature removes is\n * the ACCIDENT \u2014 the middleware author with a verified token in hand and one\n * parameter available, reaching for the obvious.\n */\nexport const ANNOTATION_ARMED_VALUE = \"1\";\n\nexport function armedCookie(name: string = ANNOTATION_ARMED_COOKIE): string {\n return `${name}=${ANNOTATION_ARMED_VALUE}; Path=/; SameSite=Lax; Secure`;\n}\n\n/**\n * The same cookie, expired \u2014 what *Done* and a disarm write.\n *\n * `Max-Age=0` AND an `Expires` in the past, because they are read by different\n * things and the pair is what every browser honours. The path must match the\n * one set above or the browser stores a second cookie and the first survives \u2014\n * a *Done* button that does nothing and reports success.\n */\nexport function clearedCookie(name: string): string {\n return `${name}=; Path=/; SameSite=Lax; Secure; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT`;\n}\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", "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 * Putting a capture back on the element it came from (A-275).\n *\n * ===========================================================================\n * WHY THIS IS A PACKAGE EXPORT AND NOT A MEASUREMENT HARNESS\n * ===========================================================================\n *\n * The slice that produced this file was originally scoped as \"measure whether\n * anchoring can work\", with rendering explicitly out of scope. That was wrong,\n * and the reason is A-268's shape: resolving an anchor against a live DOM **is**\n * the marker-placement code. Measuring with one implementation and shipping\n * another later gives two implementations of one primitive, and two\n * implementations of one primitive is a way for them to disagree \u2014 which is the\n * principle `capture-token.ts` already names, and the defect A-268 already was.\n *\n * So `resolveAnchor` is the export the marker layer (A-277) imports, and the\n * measurement in A-275 is that same function called on staging pages.\n *\n * **The measurement and the decision it produced are in\n * `docs/decisions/a-275-anchor-placement.md`**, and `anchor-measure.mjs` runs it\n * again. Read the decision before changing anything here.\n *\n * Every figure in this file is quoted from that document with its denominator,\n * or is a property of the code rather than a measurement and says so. That rule\n * exists because it was broken here: two comments claimed `MEASURED: 105 of 200`\n * \u2014 a stale figure, from a SAMPLE the decision document explicitly retires, for a\n * branch it was not even a statistic about. **A number labelled MEASURED that no\n * checked-in artifact can reproduce is worse than no number**, which this\n * package's own size-budget note already says about a different number.\n *\n * The headline, so it is not paraphrased loosely elsewhere: **139 of 546\n * annotated elements on the staging home page resolve to exactly one element,\n * and 0 of 546 resolve to the wrong one.** Every plausible improvement to the\n * first figure is a way of trading the second one away.\n *\n * ===========================================================================\n * WHY `source_file:source_line` IS NOT AN ANCHOR\n * ===========================================================================\n *\n * A source ref identifies a **component**, not an **instance**. Six cards\n * rendered from one `<PubCard/>` all carry the same `data-src`, and the file and\n * line say nothing about which one a reviewer clicked.\n *\n * The first draft of this scheme fixed that with an ordinal \u2014 the element's\n * index among the matches, in document order. A reviewer showed that this is\n * **wrong rather than merely incomplete**, and the argument is worth keeping\n * because it is not obvious:\n *\n * An ordinal is POSITIONAL. Delete the second card, and a capture anchored to\n * ordinal 3 resolves to what used to be card 4. Exactly one match. A marker is\n * drawn, on the wrong element, and NOTHING ANYWHERE HAS A COMPLAINT \u2014 a\n * \"cannot be confidently placed\" rule never fires, because the failure IS a\n * confident single match.\n *\n * A marker on the wrong element does not degrade this feature, it inverts it:\n * the entire value of putting the conversation on the component is that\n * *\"this one, here\"* stops being ambiguous.\n *\n * ===========================================================================\n * SO: A DISCONFIRMING SIGNAL, NOT A UNIQUENESS TEST\n * ===========================================================================\n *\n * Uniqueness is satisfied by every silent failure above, so uniqueness cannot be\n * the test. What the anchor stores instead is the **whole source chain** \u2014\n * `TargetInfo.sourceChain`, every `data-src` from the clicked element up to the\n * root, innermost first, which the capture has carried since A-33 and which the\n * ingest has persisted since A-287 \u2014 and a candidate must match the chain, not\n * just the leaf.\n *\n * A reordered sibling still matches (its ancestors did not change). An element\n * that MOVED does not (its ancestors did). The chain is a claim the DOM can\n * contradict, and that is the only kind of claim worth storing.\n *\n * It does not make repeated siblings distinguishable, and it is not supposed to:\n * two identical cards in one list have identical chains, so an anchor to either\n * resolves `\"ambiguous\"`. That is the SAFE failure \u2014 the marker is withheld and\n * the count says so \u2014 and it is the one an ordinal would have converted into a\n * confident wrong answer.\n *\n * ===========================================================================\n * A TAGGED UNION FROM DAY ONE\n * ===========================================================================\n *\n * Not speculative generality: two capture kinds that cannot carry a usable\n * source ref ALREADY SHIP. `TargetInfo.domPath` is the documented fallback for a\n * page whose elements have no refs, and region captures (A-179) exist, where\n * `sourceChain` is the siblings a rectangle crossed rather than ancestors \u2014\n * a chain that contains nothing of itself and would be silently read as an\n * ancestor chain by anything that assumed one shape.\n *\n * Retrofitting a union onto flat columns after a year of rows is the expensive\n * version of this, and the read response (A-276) is additive-only forever, so\n * `kind` is what lets an older SDK skip a marker it does not understand rather\n * than misplace it.\n */\n\nimport {\n collectSourceChain,\n findAnnotatedAncestor,\n parseSourceRef,\n MAX_CHAIN,\n SRC_ATTR,\n type ElementLike,\n} from \"./target\";\nimport { selectionOf, type SourceRef, type TargetInfo } from \"./protocol\";\n\n/**\n * An element identified by the source location that rendered it, plus the chain\n * of source locations above it.\n *\n * `chain` is innermost-first and INCLUDES the leaf, exactly as\n * `collectSourceChain` produces it \u2014 one array, not a leaf plus a tail, because\n * two representations of one list is a way for them to disagree about whether\n * entry zero is the element or its parent.\n */\nexport interface SourceRefAnchor {\n kind: \"source-ref\";\n file: string;\n line: number;\n column?: number;\n chain: SourceRef[];\n /**\n * The element's index among the leaf-matching candidates at capture time.\n *\n * **Recorded, and deliberately never used to select.** It is here because\n * knowing \"this was the 3rd of 6\" is genuinely useful when a human is looking\n * at an unresolved anchor and asking what happened \u2014 and it is *not* here as a\n * tie-break, for the reason set out at length in this file's header. If you\n * are about to reach for it to turn an `\"ambiguous\"` into an element, that is\n * the bug this whole scheme was rewritten to avoid; `anchor.test.ts` has a\n * test that fails if you do.\n */\n ordinal?: number;\n}\n\n/** A CSS-ish path, for elements no build transform gave a ref. */\nexport interface DomPathAnchor {\n kind: \"dom-path\";\n path: string;\n}\n\n/**\n * \"Something on THIS page.\"\n *\n * First-class rather than a fallback of last resort: it is the honest home for a\n * region capture, for a canvas, for a PDF, and for any product that is not a JSX\n * tree.\n *\n * ===========================================================================\n * `path` IS NOT OPTIONAL DECORATION \u2014 IT IS WHAT MAKES \"THIS PAGE\" MEAN ANYTHING\n * ===========================================================================\n *\n * The first version of this interface had no fields at all and `resolveAnchor`\n * returned `doc.body` unconditionally, with a comment calling it *\"always\n * placeable\"*. A reviewer pointed out that this reintroduces, through the route\n * branch, exactly the failure the source-ref scheme was rewritten to prevent:\n *\n * A capture taken on `/guides/a` resolves CONFIDENTLY on `/guides/b`. One\n * match, marker drawn, wrong page, counted as `placed`. No signal anywhere.\n *\n * That is not a rate to be estimated. `return doc.body` has no input but the\n * document, so under it **every** route anchor placed on **every** page it was\n * shown \u2014 100%, by construction, and there is nothing to measure. The reason it\n * matters is `payload.page.route`: it is the matched PATTERN rather than the\n * URL, so \"the page this came from\" was never pinned in the first place. And\n * `anchorFromTarget` sends every region capture down this branch, so it is the\n * common case rather than the edge.\n *\n * The measured neighbour is the SOURCE-REF branch meeting the same problem:\n * 414 anchors captured on one guide page resolve **identically** after a real\n * navigation to another guide page \u2014 161 placed, 253 ambiguous, 0 refused, the\n * same three numbers on both \u2014 recorded with its method and denominator in\n * `docs/decisions/a-275-anchor-placement.md` \u00A7 *The limit this does not fix*.\n * It is a different branch and a different number, cited here as evidence that\n * the pattern-not-URL problem is real, not as a figure for this one.\n *\n * So a route anchor carries the RESOLVED path it was captured on, and\n * `resolveAnchor` refuses when the document is somewhere else.\n *\n * ===========================================================================\n * AND ABSENT MEANS \"CANNOT PLACE\", NEVER \"PLACE ANYWAY\"\n * ===========================================================================\n *\n * `path` is optional because the field is newer than the wire, and the read-back\n * plan's versioning rule for exactly this case is that a new anchor field is\n * optional and **absent means cannot place** \u2014 the precedent `selection?:` set.\n * A route anchor with no `path`, or a document that cannot say where it is,\n * therefore resolves to `\"none\"`.\n *\n * That is a deliberate choice to under-place rather than mis-place, and it costs\n * nothing today because no route anchor has ever been stored: markers have not\n * shipped. It would be a real cost if this were retrofitted a year from now,\n * which is the argument for doing it here.\n *\n * WHAT IT DOES NOT FIX: a SOURCE-REF anchor still resolves happily on a\n * different `[id]` of the same route pattern \u2014 the markup is identical, so there\n * is nothing for the chain to disagree with, and no amount of anchor design\n * closes it. That one is the read endpoint's (A-276): scope the query on the\n * resolved path, not on `route`. Stated here because an unstated limit gets read\n * as a covered one.\n */\nexport interface RouteAnchor {\n kind: \"route\";\n /** `location.pathname` at capture time. Absent means the anchor cannot be placed. */\n path?: string;\n}\n\nexport type Anchor = SourceRefAnchor | DomPathAnchor | RouteAnchor;\n\n/**\n * Where a marker goes, or why it does not go anywhere.\n *\n * Three outcomes and not two, because `\"none\"` and `\"ambiguous\"` are different\n * facts about the page and a caller counting them together loses the one that\n * says the markup moved.\n */\nexport type AnchorResolution<E extends ElementLike = ElementLike> = E | \"none\" | \"ambiguous\";\n\n/**\n * The narrowest slice of `Document` this module reads. A real `Document`\n * satisfies it, which `anchor.test.ts` asserts at compile time.\n *\n * `location` is here for the route branch above. It is optional because a parsed\n * fixture has no location, and a document that cannot say where it is makes a\n * route anchor unplaceable rather than placeable-by-default.\n */\nexport interface DocumentLike<E extends ElementLike = ElementLike> {\n querySelectorAll(selectors: string): ArrayLike<E>;\n body?: E | null;\n location?: { pathname: string } | null;\n}\n\nfunction sameRef(a: SourceRef, b: SourceRef): boolean {\n if (a.file !== b.file || a.line !== b.line) return false;\n /**\n * Column is compared only when BOTH sides have one.\n *\n * It is optional in `SourceRef` and the ingest drops a malformed one rather\n * than defaulting it (A-287), so an absent column means \"not recorded\" and\n * never \"column zero\". Treating a missing column as a mismatch would make\n * every capture from a build that omits it permanently unplaceable.\n */\n if (a.column === undefined || b.column === undefined) return true;\n return a.column === b.column;\n}\n\n/**\n * Does this candidate's live chain still match the one recorded at capture time?\n *\n * The length rule is the half that does the work, and it is asymmetric on\n * purpose:\n *\n * - A recorded chain SHORTER than `MAX_CHAIN` is complete \u2014 it ran out of\n * ancestors, not out of budget \u2014 so the live chain must be exactly as long.\n * An element that gained an annotated ancestor (a conditional wrapper, a\n * portal boundary, a new layout) is a moved element and must not match.\n * - A recorded chain AT `MAX_CHAIN` was truncated by `collectSourceChain`, so\n * all that can honestly be required is that the recorded frames are a prefix\n * of the live ones.\n *\n * THE SECOND CASE IS NOT WHERE THE REAL WEAKNESS LIVES, and an earlier version\n * of this comment said it was. An ancestor inserted above the eighth frame is\n * invisible because `collectSourceChain` never walked that far \u2014 the frame is\n * missing from the recorded chain AND from the live one, so no comparison here\n * can see it. MEASURED: making this length rule strict for truncated chains too\n * changes no test; raising `MAX_CHAIN` changes four.\n *\n * `anchor.test.ts` pins that ceiling under the name\n * *\"MAX_CHAIN truncation hides an ancestor added above the eighth frame\"*, so it\n * is an executable fact rather than a paragraph.\n */\nfunction chainMatches(recorded: SourceRef[], live: SourceRef[]): boolean {\n if (recorded.length === 0) return false;\n if (recorded.length < MAX_CHAIN) {\n if (live.length !== recorded.length) return false;\n } else if (live.length < recorded.length) {\n return false;\n }\n return recorded.every((ref, i) => { const other = live[i]; return other !== undefined && sameRef(ref, other); });\n}\n\n/**\n * Candidates for a source-ref anchor: every element whose OWN `data-src` is the\n * anchor's leaf, minus the nested repeats.\n *\n * One JSX element frequently produces several nested DOM nodes carrying the same\n * ref. `collectSourceChain` already collapses those when it builds a chain; not\n * collapsing them here would make a perfectly unambiguous element report\n * `\"ambiguous\"` against copies of itself \u2014 a false unplaceable, which is the\n * quiet way a scheme gets abandoned for being useless rather than for being\n * wrong. The outermost node of a repeat group is kept, because that is the one\n * whose box covers the component.\n */\nfunction leafCandidates<E extends ElementLike>(anchor: SourceRefAnchor, doc: DocumentLike<E>): E[] {\n const leaf: SourceRef = { file: anchor.file, line: anchor.line };\n if (anchor.column !== undefined) leaf.column = anchor.column;\n\n const all = doc.querySelectorAll(`[${SRC_ATTR}]`);\n const matched: E[] = [];\n for (let i = 0; i < all.length; i++) {\n const el = all[i] as E;\n const own = parseSourceRef(el.getAttribute(SRC_ATTR));\n if (!own || !sameRef(leaf, own)) continue;\n\n const parentRef = parseSourceRef(\n findAnnotatedAncestor(el.parentElement)?.getAttribute(SRC_ATTR) ?? null,\n );\n if (parentRef && sameRef(parentRef, own)) continue; // a nested repeat of the same ref\n\n matched.push(el);\n }\n return matched;\n}\n\n/**\n * Resolve an anchor against a document.\n *\n * Pure: it reads the document and returns an element or a refusal, and it never\n * mutates, measures layout, or draws anything. That is what lets the same\n * function be a unit test over a checked-in HTML fixture, a number in a\n * measurement run, and the marker layer's placement call.\n *\n * A `kind` this version does not recognise resolves to `\"none\"` \u2014 the read\n * response is additive-only forever (A-276), so an SDK meeting a newer console\n * must SKIP a marker it cannot place rather than guess at one.\n */\nexport function resolveAnchor<E extends ElementLike>(\n anchor: Anchor,\n doc: DocumentLike<E>,\n): AnchorResolution<E> {\n switch (anchor.kind) {\n case \"route\": {\n /**\n * Both sides must say where they are, and they must agree. See\n * `RouteAnchor` for why this is a refusal rather than a default.\n */\n if (!anchor.path) return \"none\";\n const here = doc.location?.pathname;\n if (!here || here !== anchor.path) return \"none\";\n return doc.body ?? \"none\";\n }\n\n case \"dom-path\": {\n if (!anchor.path) return \"none\";\n let found: ArrayLike<E>;\n try {\n found = doc.querySelectorAll(anchor.path);\n } catch {\n /**\n * `domPath` is documented as a hint for a human and explicitly not a\n * selector we promise to re-resolve \u2014 hashed class names can contain\n * characters that make it an invalid selector, and an invalid selector\n * THROWS rather than matching nothing. Refusing is right; letting a\n * `SyntaxError` out of a placement call would take the whole marker\n * layer down over one bad capture.\n */\n return \"none\";\n }\n if (found.length === 0) return \"none\";\n if (found.length > 1) return \"ambiguous\";\n return found[0] as E;\n }\n\n case \"source-ref\": {\n if (anchor.chain.length === 0) return \"none\";\n const matched = leafCandidates(anchor, doc).filter((el) =>\n chainMatches(anchor.chain, collectSourceChain(el)),\n );\n if (matched.length === 0) return \"none\";\n if (matched.length > 1) return \"ambiguous\";\n return matched[0] as E;\n }\n\n default:\n return \"none\";\n }\n}\n\nexport interface AnchorFromTargetOptions {\n /** Document order index among leaf-matching candidates, when the caller has a document. */\n ordinal?: number;\n /**\n * `location.pathname` at capture time \u2014 the RESOLVED path, not the route\n * pattern.\n *\n * Pass it. A route anchor built without it can never be placed, by design; see\n * `RouteAnchor`. It is optional here rather than required because\n * `anchorFromTarget` is also called for element captures, where it is unused.\n */\n path?: string;\n}\n\n/**\n * Build the anchor for a capture.\n *\n * The capture side of the same primitive, here rather than in `target.ts` so the\n * union and the function that produces it cannot drift apart.\n *\n * **A region capture becomes a `route` anchor.** Its `sourceChain` is the\n * siblings a rectangle crossed, which contain nothing of each other; handing\n * that to `chainMatches` would compare a sibling list against an ancestor list\n * and refuse \u2014 the right answer for the wrong reason, and one that would report\n * every region capture as \"the markup moved\". Route-level is what a region\n * capture actually means: something on this page, in that area.\n */\nexport function anchorFromTarget(\n target: Pick<TargetInfo, \"selection\" | \"source\" | \"sourceChain\" | \"domPath\">,\n options: AnchorFromTargetOptions = {},\n): Anchor {\n const routeAnchor = (): RouteAnchor =>\n options.path === undefined ? { kind: \"route\" } : { kind: \"route\", path: options.path };\n\n if (selectionOf(target) === \"region\") return routeAnchor();\n\n const chain = target.sourceChain ?? [];\n const head = chain[0];\n const leaf = target.source ?? head;\n if (leaf && head && sameRef(leaf, head)) {\n const anchor: SourceRefAnchor = {\n kind: \"source-ref\",\n file: leaf.file,\n line: leaf.line,\n chain,\n };\n if (leaf.column !== undefined) anchor.column = leaf.column;\n if (options.ordinal !== undefined) anchor.ordinal = options.ordinal;\n return anchor;\n }\n\n if (target.domPath) return { kind: \"dom-path\", path: target.domPath };\n return routeAnchor();\n}\n\n/**\n * The counts a caller reports, so that \"0 markers\" is never the same string as\n * \"working, nothing here\" (rule 1 of the read-back plan).\n *\n * ===========================================================================\n * `wrong` IS NOT IN HERE AND CANNOT BE, AND THAT IS A GAP RATHER THAN A DESIGN\n * ===========================================================================\n *\n * Whether a resolved element is the RIGHT element is not knowable from the live\n * page alone \u2014 it takes a before/after comparison, which is what A-275's\n * mutation battery does and what no runtime caller can do.\n *\n * So `placed` means \"resolved to exactly one element\", NOT \"resolved\n * correctly\", and there is one measured channel where those differ: a\n * source-ref anchor captured on one `[id]` resolves on another `[id]` of the\n * same route pattern, because the two pages are structurally identical.\n *\n * MEASURED at **161 of 414 anchors** placing on a different `[id]` of the same\n * route pattern, with zero refusals \u2014 indistinguishable, count for count, from\n * resolving against the page they were captured on. The method, the denominator\n * and the pages are in `docs/decisions/a-275-anchor-placement.md` \u00A7 *The limit\n * this does not fix*, and `node anchor-measure.mjs --url \u2026 --compare-url \u2026`\n * re-runs it in one command. Nothing in this module can detect it; the\n * fix is for the read endpoint to scope on the resolved path (A-276), so a\n * caller must not read `placed` as a correctness figure until it does.\n *\n * The route branch closes its own half of that (`RouteAnchor.path`). This\n * comment is the record that the source-ref half is still open.\n */\nexport interface AnchorTally {\n total: number;\n placed: number;\n ambiguous: number;\n unresolved: number;\n}\n\nexport function tallyAnchors<E extends ElementLike>(\n anchors: readonly Anchor[],\n doc: DocumentLike<E>,\n): AnchorTally {\n const tally: AnchorTally = { total: anchors.length, placed: 0, ambiguous: 0, unresolved: 0 };\n for (const anchor of anchors) {\n const result = resolveAnchor(anchor, doc);\n if (result === \"ambiguous\") tally.ambiguous += 1;\n else if (result === \"none\") tally.unresolved += 1;\n else tally.placed += 1;\n }\n return tally;\n}\n", "import { resolveAnchor } from \"./anchor\";\nimport type { AnnotationMarker, AnnotationReader, AnnotationsResponse } from \"./read\";\n\n/**\n * Markers on the components a reviewer annotated, and the panel behind them\n * (A-277).\n *\n * ===========================================================================\n * WHY THIS IS A SIXTH ENTRY POINT AND NOT PART OF `.`\n * ===========================================================================\n *\n * `./anchor` and `./read` both make the same argument and it applies here with\n * the most weight: this runs only in a product that RENDERS markers, and the SDK\n * that only captures does not need a byte of it. Folding it into `index` would\n * put the marker layer, the panel and its stylesheet into the bundle of every\n * page of every managed product, including the ones that will never show one.\n *\n * ===========================================================================\n * THE COUNT LINE IS THE FEATURE. THE ICONS ARE THE GARNISH.\n * ===========================================================================\n *\n * On a read path **zero markers is exactly what \"working, nothing here\" looks\n * like**, and the runbook lists twelve distinct causes that produce it. So an\n * armed page ALWAYS renders the line, before and after the read, whatever the\n * answer \u2014 *\"3 annotations here, 1 shown\"* \u2014 and that is not a status indicator,\n * it is the only thing that makes an anchor which cannot place VISIBLE rather\n * than silent.\n *\n * The sentence is built from the PAGE's own tally, with the envelope's `total`\n * as the denominator. It is deliberately not built from the envelope's\n * `anchored`: the server has no DOM and cannot know whether a marker lands, and\n * two numbers called \"placed\" meaning different things is the shape this\n * repository keeps getting burned by. `anchored` is a diagnostic for the\n * runbook \u2014 it separates *the console has nothing for this page* from *the\n * console has three and your DOM matched none of them* \u2014 and it is exposed on\n * `MarkerRender` rather than rendered as though it were the page's answer.\n *\n * ===========================================================================\n * MOST MARKERS ON A LIST PAGE ARE ROUTE-LEVEL, AND THAT IS A DESIGNED STATE\n * ===========================================================================\n *\n * MEASURED by A-275 against live staging pages: on a listing page only about\n * **25%** of annotated elements resolve uniquely, and about 75% are ambiguous\n * identical siblings. That is not a defect to be worked around \u2014 a source ref\n * names a COMPONENT, and two identical cards are genuinely indistinguishable\n * from one. Wrong-placement was **0** across every mutation tested, and it is\n * zero because the resolver refuses rather than guesses.\n *\n * So this layer is built for the refusing case rather than treating it as\n * degraded:\n *\n * - an `\"ambiguous\"` anchor NEVER gets a marker. A marker on the wrong element\n * does not degrade this feature, it inverts it.\n * - markers that resolve to the SAME element are drawn as one icon carrying a\n * count, and the panel lists all of them. Without that, a route anchor \u2014 the\n * honest home for a region capture and for any page with no source refs \u2014\n * would stack every marker on the page at one point, and three captures would\n * render as one icon with two hidden underneath it.\n * - a route anchor resolves to `document.body`, so its cluster sits in the\n * page's own top-right corner and reads as *\"about this page\"*. That is what\n * the anchor means.\n *\n * ===========================================================================\n * IT NEVER RE-RESOLVES SOMETHING IT DID NOT READ FROM THE CONSOLE\n * ===========================================================================\n *\n * `resolveAnchor` is IMPORTED from `./anchor` and nothing here reimplements any\n * part of it. That is A-275's whole point: the function that measured the scheme\n * and the function that places the markers must be one function, or the\n * measurement is about code nobody runs.\n *\n * ===========================================================================\n * THIS RUNS INSIDE SOMEBODY ELSE'S PRODUCTION SITE\n * ===========================================================================\n *\n * Everything rendered here came off the network. `read.ts` sanitises it \u2014 bounded\n * strings, control characters dropped, a marker that cannot be read skipped \u2014 and\n * this file adds the second half of that guarantee: every value reaches the DOM\n * through `textContent` and never through `innerHTML`, so a console that has been\n * compromised or has merely gone wrong cannot put markup into a customer's page.\n * There is no `href` anywhere in this layer, which is why `safeIssueUrl` has no\n * caller here.\n *\n * The whole layer lives in a shadow root, so the product's stylesheet cannot\n * reach in and this cannot reach out.\n */\n\n/** The layer's host node. Namespaced, and never product-named (A-98). */\nexport const MARKERS_ID = \"ai-annotation-markers\";\n\n/**\n * Above the toolbar's `2147483001` by one, so a marker sitting under the\n * toolbar's corner is still clickable. The toolbar is 16px from the bottom-right\n * and markers hang off the top-right of components, so they collide rarely \u2014 but\n * \"rarely\" on a fixed element is \"always, for one component on one page\", and\n * that component would have an icon nobody could press.\n */\nconst Z_INDEX = \"2147483002\";\n\nexport interface AnnotationMarkerLabels {\n /** The region the whole layer announces itself as. */\n region: string;\n /** Built with the two counts: `(total, shown) => string`. */\n count: (total: number, shown: number) => string;\n /** Shown before the first read completes. */\n loading: string;\n /** The console answered and refused. `reason` is the envelope's, or empty. */\n refused: (reason: string) => string;\n /** The read did not complete at all \u2014 no status, no body. */\n unreachable: string;\n /** No read token, so nothing was even attempted. */\n noCredential: string;\n /** Freshness, from `asOf`. */\n asOf: (age: string) => string;\n freshnessUnknown: string;\n close: string;\n}\n\nexport const DEFAULT_MARKER_LABELS: AnnotationMarkerLabels = {\n region: \"Annotations on this page\",\n /**\n * *\"3 annotations here, 1 shown\"* \u2014 the sentence A-277 specifies, and the\n * shape matters: the denominator is what the CONSOLE holds for this page and\n * the numerator is what this DOM matched, so the two disagreeing is the\n * signal. A page that rendered only the second number would be silent in\n * exactly the case the line exists for.\n */\n count: (total, shown) =>\n total === 0\n ? \"No annotations on this page\"\n : `${total} annotation${total === 1 ? \"\" : \"s\"} here, ${shown} shown`,\n loading: \"Looking for annotations\u2026\",\n refused: (reason) =>\n reason === \"retired-link\"\n ? \"This annotation link is an older format the console no longer accepts. Open Request changes again.\"\n : reason === \"rate-limited\"\n ? \"The console is rate-limiting this page's reads. Wait a moment.\"\n : \"The console refused this page's read.\",\n unreachable: \"Could not reach the console for this page's annotations.\",\n noCredential: \"No read credential, so annotations were not requested.\",\n asOf: (age) => `status as of ${age} ago`,\n freshnessUnknown: \"the console cannot say how fresh these statuses are\",\n close: \"Close\",\n};\n\nexport interface AnnotationMarkersOptions {\n /**\n * The read client, from `createAnnotationReader`. Required: a marker layer\n * with no way to read is a layer that renders an empty page confidently.\n */\n reader: AnnotationReader | null;\n /**\n * May this page render markers at all?\n *\n * `false` renders NOTHING \u2014 no host node, no shadow root, no stylesheet \u2014\n * exactly as `createAnnotationToolbar` does, and for the same reason: an\n * unarmed page that shipped this code must leave no residue in a customer's\n * DOM. Every method still answers, so a caller does not have to branch.\n */\n armed?: boolean;\n doc?: Document;\n win?: Window;\n /**\n * The path to ask about. Defaults to `location.pathname`.\n *\n * The RESOLVED path and never the route pattern. A pattern is one value for\n * every page of a dynamic route, so asking by pattern returns another page's\n * captures \u2014 measured at 161 of 414 anchors placing confidently on the wrong\n * `[slug]`, with zero refusals, in `docs/decisions/a-275-anchor-placement.md`.\n */\n path?: string;\n labels?: Partial<AnnotationMarkerLabels>;\n /** For the age in the freshness line. Defaults to `Date.now`. */\n now?: () => number;\n}\n\n/** What one render actually put on the page. Returned so a caller can assert on it. */\nexport interface MarkerRender {\n /** The envelope's `total` \u2014 what the console holds for this path. */\n total: number;\n /** Markers whose anchor resolved to exactly one element, and were drawn. */\n shown: number;\n /** Anchors that resolved to more than one element. NEVER drawn. */\n ambiguous: number;\n /** Anchors that matched nothing in this document. */\n unresolved: number;\n /**\n * The CONSOLE's count of anchors it could serve as placeable at all.\n *\n * Carried through for the runbook's checklist and deliberately NOT rendered as\n * though it were `shown`. See this file's header.\n */\n anchored: number;\n /** Distinct elements drawn on. Lower than `shown` wherever markers clustered. */\n clusters: number;\n /** The envelope's `asOf` \u2014 the console's Linear cursor, or `\"\"`. */\n asOf: string;\n /** The line a human reads. Exactly the text in the DOM. */\n line: string;\n}\n\nexport interface AnnotationMarkers {\n /** Read, resolve, draw. Never rejects: a failure is a rendered sentence. */\n refresh(): Promise<MarkerRender>;\n /** Is this node ours? For an SDK that must not capture its own furniture. */\n owns(node: unknown): boolean;\n /** The last render, or null before the first. */\n last(): MarkerRender | null;\n destroy(): void;\n}\n\nconst EMPTY: MarkerRender = {\n total: 0,\n shown: 0,\n ambiguous: 0,\n unresolved: 0,\n anchored: 0,\n clusters: 0,\n asOf: \"\",\n line: \"\",\n};\n\n/**\n * The layer's own palette and scale, declared once on `:host`.\n *\n * The SAME custom-property names `toolbar.ts` uses, with the same values, so the\n * two surfaces a reviewer sees on one page are one design rather than two. They\n * are declared again rather than imported because a shadow root does not inherit\n * another shadow root's properties, and this package has no stylesheet to share.\n *\n * Raw hex and raw pixels live HERE and nowhere below: everything after this\n * block is `var(--ai-\u2026)`. That is what the design audit asks for and it is worth\n * having anyway \u2014 the alternative is a colour repeated in six rules, five of\n * which get updated when somebody changes it.\n */\nconst CSS = `\n:host{\n --ai-bg:#fff;\n --ai-fg:#18181b;\n --ai-line:#d4d4d8;\n --ai-line-soft:#e4e4e7;\n --ai-muted:#52525b;\n --ai-accent:#2563eb;\n --ai-accent-line:#1d4ed8;\n --ai-invert-bg:#18181b;\n --ai-invert-fg:#fafafa;\n --ai-space-1:4px;\n --ai-space-2:6px;\n --ai-space-3:8px;\n --ai-space-4:10px;\n --ai-space-5:12px;\n --ai-radius-control:6px;\n --ai-radius-card:8px;\n --ai-radius-pill:999px;\n --ai-pin:22px;\n --ai-edge:16px;\n all:initial;position:fixed;inset:0;z-index:${Z_INDEX};pointer-events:none;\n font:13px/1.45 ui-sans-serif,system-ui,-apple-system,\"Segoe UI\",Roboto,sans-serif;\n color:var(--ai-fg);color-scheme:light}\n*,*::before,*::after{box-sizing:border-box}\n[hidden]{display:none!important}\n.count{\n position:absolute;left:var(--ai-edge);bottom:var(--ai-edge);pointer-events:auto;\n background:var(--ai-invert-bg);color:var(--ai-invert-fg);\n border-radius:var(--ai-radius-pill);\n padding:var(--ai-space-2) var(--ai-space-5);max-width:min(60vw,420px)}\n.pin{\n position:absolute;pointer-events:auto;\n width:var(--ai-pin);height:var(--ai-pin);\n display:flex;align-items:center;justify-content:center;\n border:1px solid var(--ai-accent-line);border-radius:var(--ai-radius-pill);\n background:var(--ai-accent);color:var(--ai-invert-fg);\n font:600 11px/1 ui-sans-serif,system-ui,sans-serif;cursor:pointer;padding:0}\n.pin:focus-visible{outline:2px solid var(--ai-fg);outline-offset:2px}\n.panel{\n position:absolute;pointer-events:auto;width:min(320px,80vw);\n background:var(--ai-bg);border:1px solid var(--ai-line);\n border-radius:var(--ai-radius-card);padding:var(--ai-space-5);\n max-height:50vh;overflow:auto}\n.entry+.entry{margin-top:var(--ai-space-5);border-top:1px solid var(--ai-line-soft);\n padding-top:var(--ai-space-5)}\n.who{color:var(--ai-muted);font-size:12px;margin:0 0 var(--ai-space-1)}\n.what{margin:0 0 var(--ai-space-2);white-space:pre-wrap;overflow-wrap:anywhere}\n.state{margin:0;font-size:12px;color:var(--ai-muted)}\n.fresh{margin:var(--ai-space-2) 0 0;font-size:11px;color:var(--ai-muted)}\n.close{\n pointer-events:auto;margin-top:var(--ai-space-4);font:inherit;\n background:var(--ai-bg);color:var(--ai-fg);border:1px solid var(--ai-line);\n border-radius:var(--ai-radius-control);\n padding:var(--ai-space-1) var(--ai-space-4);cursor:pointer}\n`;\n\n/** The pin's box, and how far it is kept from a viewport edge. */\nconst PIN = 22;\nconst MARGIN = 4;\n\nconst clamp = (value: number, low: number, high: number): number =>\n Math.min(Math.max(value, low), high);\n\n/** One step of the collision walk: a pin's width plus a hairline of daylight. */\nconst STEP = PIN + 2;\n\n/**\n * The most candidate positions the walk below will try before giving up.\n *\n * A CEILING, not a budget. `MAX_ANNOTATIONS` is 100, so a hundred pins on an\n * ordinary viewport need at most a hundred probes to walk past the occupied\n * slots; 128 clears that with room and bounds every case that is not ordinary.\n */\nconst MAX_PROBES = 128;\n\nexport interface Spot {\n left: number;\n top: number;\n}\n\n/**\n * A position for this pin that no already-placed pin occupies \u2014 or, when there\n * is none, the ideal one, OVERLAPPING.\n *\n * ===========================================================================\n * IT GIVES UP. THAT IS THE WHOLE POINT OF THIS FUNCTION EXISTING.\n * ===========================================================================\n *\n * This was a bare `while` loop with no exit but success, and it could not\n * terminate. Once `left` floored at `MARGIN` the wrap computed\n * `Math.max(MARGIN, MARGIN - STEP) === MARGIN` and `top = MARGIN`, so the search\n * returned to a position it had already rejected and tried it again, forever.\n *\n * REPRODUCED, not reasoned about: two clusters on two distinct elements with\n * `innerWidth` and `innerHeight` both 0 \u2014 `refresh()` never returned, killed at\n * 20s. `position()` is called by `draw()` and by every `scroll` and `resize`\n * listener, so that is **a locked main thread inside a customer's production\n * page**, which is strictly worse than the thrown read `refresh()` refuses to\n * allow.\n *\n * A zero-sized viewport is not exotic: a hidden iframe, a prerender, a host that\n * stubs `win`. Two pins are then enough. A short embedded viewport reaches it\n * honestly too \u2014 300\u00D7200 offers about 77 slots against a possible 100 pins.\n *\n * And the irony is the useful part. `linkedom` returns zeros for every rect,\n * which is exactly why the clamp above had to be found in a browser \u2014 and it is\n * exactly why no fixture test saw this. **The zero-rect case is a layout hazard\n * AND a termination hazard, and the first fix addressed only the layout.**\n *\n * So the walk is bounded twice over: `MAX_PROBES`, and a `left` that has run out\n * of columns. When it gives up, the pin goes at its clamped ideal and is allowed\n * to sit on top of another. **An overlapping marker is a visible imperfection; a\n * locked main thread is the product broken.**\n *\n * Exported so `markers.test.ts` can assert termination directly, under\n * `linkedom`, with no browser: only the QUALITY of the placement needs one.\n */\nexport function freeSpot(ideal: Spot, taken: readonly Spot[], maxTop: number): Spot {\n let { left, top } = ideal;\n const occupied = (l: number, t: number): boolean =>\n taken.some((s) => Math.abs(s.left - l) < PIN && Math.abs(s.top - t) < PIN);\n\n for (let probe = 0; probe < MAX_PROBES; probe += 1) {\n if (!occupied(left, top)) return { left, top };\n top += STEP;\n if (top > maxTop) {\n top = MARGIN;\n left -= STEP;\n // Out of columns. There is no free position, so stop looking for one.\n if (left < MARGIN) break;\n }\n }\n return { left: ideal.left, top: ideal.top };\n}\n\nfunction make(doc: Document, tag: string, attrs?: Record<string, string>): HTMLElement {\n const el = doc.createElement(tag);\n if (attrs) for (const name of Object.keys(attrs)) el.setAttribute(name, attrs[name] ?? \"\");\n return el;\n}\n\n/**\n * A duration a human reads. Two units at most, mirroring `fleet/freshness.ts` \u2014\n * the point of the line is \"is this minutes or hours\", and the seconds in\n * \"2h 10m 4s\" are noise that makes the answer take longer to find.\n *\n * Reimplemented rather than imported because that module is server-side and this\n * package must not depend on the console. It is eleven lines of arithmetic with\n * a test on it, which is the honest version of that trade.\n */\nexport function formatMarkerAge(ms: number): string {\n const seconds = Math.max(0, Math.floor(ms / 1000));\n if (seconds < 60) return `${seconds}s`;\n const minutes = Math.floor(seconds / 60);\n if (minutes < 60) return `${minutes}m`;\n const hours = Math.floor(minutes / 60);\n if (hours < 24) {\n const rest = minutes % 60;\n return rest === 0 ? `${hours}h` : `${hours}h ${rest}m`;\n }\n const days = Math.floor(hours / 24);\n const rest = hours % 24;\n return rest === 0 ? `${days}d` : `${days}d ${rest}h`;\n}\n\n/**\n * Group markers by the element each one resolves to, refusing the ambiguous.\n *\n * Exported and pure so the placement rule is testable over a parsed fixture\n * without a browser \u2014 the same split `anchor.test.ts` uses, and the reason this\n * layer's most important behaviour (`ambiguous` is never drawn) is covered by\n * `pnpm verify` rather than only by a browser nobody schedules.\n */\nexport function placeMarkers(\n markers: readonly AnnotationMarker[],\n doc: Parameters<typeof resolveAnchor>[1],\n): {\n clusters: { element: unknown; markers: AnnotationMarker[] }[];\n ambiguous: number;\n unresolved: number;\n} {\n const byElement = new Map<unknown, AnnotationMarker[]>();\n let ambiguous = 0;\n let unresolved = 0;\n\n for (const marker of markers) {\n const found = resolveAnchor(marker.anchor, doc);\n if (found === \"ambiguous\") {\n ambiguous += 1;\n continue;\n }\n if (found === \"none\") {\n unresolved += 1;\n continue;\n }\n const existing = byElement.get(found);\n if (existing) existing.push(marker);\n else byElement.set(found, [marker]);\n }\n\n return {\n clusters: [...byElement].map(([element, list]) => ({ element, markers: list })),\n ambiguous,\n unresolved,\n };\n}\n\n/**\n * Mount the marker layer.\n *\n * Does not read anything until `refresh()` is called, so a caller decides when\n * the page is settled enough to measure. The count line renders `loading`\n * immediately, because an armed page with nothing on it at all is the state this\n * whole layer exists to make impossible.\n */\nexport function mountAnnotationMarkers(options: AnnotationMarkersOptions): AnnotationMarkers {\n const labels: AnnotationMarkerLabels = { ...DEFAULT_MARKER_LABELS, ...options.labels };\n\n /**\n * Unarmed is not a hidden layer, it is no layer \u2014 `createAnnotationToolbar`'s\n * rule, kept here so the two gates behave identically. Nothing is appended,\n * so a page that somehow shipped this code has no node, no shadow root and no\n * style element in its DOM.\n */\n if (options.armed === false) {\n return {\n refresh: () => Promise.resolve(EMPTY),\n owns: () => false,\n last: () => null,\n destroy: () => {},\n };\n }\n\n const doc = options.doc ?? document;\n const win = options.win ?? doc.defaultView ?? window;\n const now = options.now ?? Date.now;\n\n const host = make(doc, \"div\", { id: MARKERS_ID });\n const root = host.attachShadow({ mode: \"open\" });\n const style = make(doc, \"style\");\n style.textContent = CSS;\n root.appendChild(style);\n /** Rewritten per render. A stylesheet, so no element carries a style attribute. */\n const geometry = make(doc, \"style\");\n root.appendChild(geometry);\n\n const line = make(doc, \"div\", { class: \"count\", role: \"status\", \"aria-label\": labels.region });\n line.textContent = labels.loading;\n root.appendChild(line);\n\n const panel = make(doc, \"div\", {\n class: \"panel\",\n role: \"dialog\",\n /**\n * `false`, and stated rather than omitted, for the composer's reason: modal\n * would mean trapping focus and hiding the rest of the page from assistive\n * technology, in a product the reviewer is here to READ.\n */\n \"aria-modal\": \"false\",\n \"aria-label\": labels.region,\n hidden: \"\",\n });\n root.appendChild(panel);\n\n doc.body.appendChild(host);\n\n let rendered: MarkerRender | null = null;\n let pins: { pin: HTMLElement; element: Element }[] = [];\n let destroyed = false;\n\n function position(): void {\n if (pins.length === 0) {\n geometry.textContent = \"\";\n return;\n }\n /**\n * One stylesheet rewritten wholesale rather than N inline styles, so the\n * layer never writes a `style` attribute into a page whose CSP may forbid\n * one. `getBoundingClientRect` is already viewport coordinates, which is the\n * space a `position:fixed` host is in \u2014 no scroll arithmetic, which is where\n * a marker ends up NEXT to the thing rather than on it.\n */\n const width = win.innerWidth || 0;\n const height = win.innerHeight || 0;\n const taken: { left: number; top: number }[] = [];\n\n const rules: string[] = [];\n pins.forEach(({ element }, i) => {\n const rect = element.getBoundingClientRect();\n /**\n * CLAMPED INTO THE VIEWPORT, and this is not defensive tidying \u2014 it is a\n * defect found by rendering.\n *\n * The ideal spot is the component's top-right corner, hanging half outside\n * it: `right - 11, top - 11`. MEASURED in Chromium against a real page:\n * for a ROUTE anchor that is `document.body`, whose `top` is 0, so the pin\n * landed at `y: -11` \u2014 entirely above the viewport, unclickable, and\n * indistinguishable on screen from a marker that was never drawn. Route\n * anchors are the COMMON case on a listing page (about 75% of elements are\n * ambiguous, so most captures are route-level), so this was the majority\n * case rather than an edge.\n *\n * `linkedom` answers zeros for every rect, so no fixture test can see\n * this. That is what the browser is for.\n */\n let left = clamp(Math.round(rect.right - 11), MARGIN, Math.max(MARGIN, width - PIN - MARGIN));\n let top = clamp(Math.round(rect.top - 11), MARGIN, Math.max(MARGIN, height - PIN - MARGIN));\n\n /**\n * And nudged clear of a pin already placed there. Two anchors resolving to\n * DIFFERENT elements can still clamp to the same corner \u2014 measured, at\n * `481..503` against `489..511`, two pins overlapping by 22 of their 22\n * pixels \u2014 and the reviewer can then only press one of them. Clustering\n * merges markers on the SAME element; this is the other collision, between\n * elements, and it needs a different answer because the two pins mean\n * different things and must both stay reachable.\n */\n const spot = freeSpot({ left, top }, taken, height - PIN - MARGIN);\n taken.push(spot);\n rules.push(`.pin[data-i=\"${i}\"]{left:${spot.left}px;top:${spot.top}px}`);\n });\n geometry.textContent = rules.join(\"\");\n }\n\n const onViewportChange = () => {\n if (!destroyed) position();\n };\n win.addEventListener(\"scroll\", onViewportChange, true);\n win.addEventListener(\"resize\", onViewportChange);\n\n function closePanel(): void {\n panel.setAttribute(\"hidden\", \"\");\n while (panel.firstChild) panel.removeChild(panel.firstChild);\n }\n\n function openPanel(markers: readonly AnnotationMarker[], anchorEl: Element): void {\n closePanel();\n for (const marker of markers) {\n const entry = make(doc, \"div\", { class: \"entry\" });\n\n const who = make(doc, \"p\", { class: \"who\" });\n /**\n * `textContent` for every one of these, and there is no `innerHTML` in\n * this file. All four values came off the network from a console this page\n * does not control; `read.ts` bounded and stripped them, and this is the\n * other half of that guarantee at the boundary that touches the DOM.\n */\n /**\n * The date as a DATE. `createdAt` is an ISO instant off the wire and\n * rendering it raw put `2026-09-04T09:00:00.000Z` beside a reviewer's name\n * in the panel \u2014 measured, by looking at one. An unparseable value falls\n * back to the name alone rather than to the raw string, because a string\n * that is not a date is not information a reviewer can use either.\n */\n const at = marker.createdAt ? new Date(marker.createdAt) : null;\n who.textContent =\n at !== null && !Number.isNaN(at.getTime())\n ? `${marker.reviewer} \u00B7 ${at.toISOString().slice(0, 10)}`\n : marker.reviewer;\n entry.appendChild(who);\n\n const what = make(doc, \"p\", { class: \"what\" });\n what.textContent = marker.comment;\n entry.appendChild(what);\n\n const state = make(doc, \"p\", { class: \"state\" });\n /**\n * The console's DISPLAY string, never its enum and never a Linear state\n * name. `marker.status.state` is the closed value a caller switches on;\n * this is the sentence a human reads, and the console composed it so that\n * each product's own workflow stays on the console's side of the wire.\n */\n state.textContent = marker.status.display;\n entry.appendChild(state);\n\n panel.appendChild(entry);\n }\n\n /**\n * The freshness line, ONCE per panel rather than per entry: it is a property\n * of the console's cache, not of any one capture, and repeating it would\n * read as four independent readings that happen to agree.\n */\n const fresh = make(doc, \"p\", { class: \"fresh\" });\n const asOf = rendered?.asOf ?? \"\";\n const at = asOf === \"\" ? NaN : Date.parse(asOf);\n fresh.textContent = Number.isNaN(at)\n ? labels.freshnessUnknown\n : labels.asOf(formatMarkerAge(now() - at));\n panel.appendChild(fresh);\n\n const close = make(doc, \"button\", { type: \"button\", class: \"close\" });\n close.textContent = labels.close;\n close.addEventListener(\"click\", () => closePanel());\n panel.appendChild(close);\n\n const rect = anchorEl.getBoundingClientRect();\n geometry.textContent =\n `${geometry.textContent ?? \"\"}.panel{left:${Math.round(Math.max(8, rect.right - 320))}px;` +\n `top:${Math.round(rect.top + 20)}px}`;\n panel.removeAttribute(\"hidden\");\n }\n\n function draw(response: AnnotationsResponse | null, failure: string | null): MarkerRender {\n for (const { pin } of pins) pin.remove();\n pins = [];\n closePanel();\n\n if (response === null) {\n line.textContent = failure ?? labels.unreachable;\n const render: MarkerRender = { ...EMPTY, line: line.textContent };\n rendered = render;\n position();\n return render;\n }\n\n const placement = placeMarkers(response.annotations, doc);\n let shown = 0;\n placement.clusters.forEach((cluster, i) => {\n const element = cluster.element as Element;\n const pin = make(doc, \"button\", {\n type: \"button\",\n class: \"pin\",\n \"data-i\": String(i),\n /**\n * The label carries the count because the visible glyph is a number and\n * a screen reader saying \"3\" beside a card means nothing. It names no\n * reviewer and no comment: those are in the panel, which is where a\n * reader chooses to go.\n */\n \"aria-label\": `${cluster.markers.length} annotation${\n cluster.markers.length === 1 ? \"\" : \"s\"\n }`,\n });\n pin.textContent = cluster.markers.length === 1 ? \"1\" : String(cluster.markers.length);\n pin.addEventListener(\"click\", (event) => {\n event.preventDefault();\n event.stopPropagation();\n openPanel(cluster.markers, element);\n });\n root.appendChild(pin);\n pins.push({ pin, element });\n shown += cluster.markers.length;\n });\n\n /**\n * The denominator is the ENVELOPE's `total` \u2014 everything the console holds\n * for this path, including what the cap or the scan window left out of\n * `annotations` \u2014 and the numerator is what this DOM matched. Using\n * `annotations.length` would make the line agree with itself whenever the\n * response was truncated, which is precisely when it must not.\n */\n line.textContent =\n response.ok || response.reason === undefined\n ? labels.count(response.total, shown)\n : labels.refused(response.reason);\n\n const render: MarkerRender = {\n total: response.total,\n shown,\n ambiguous: placement.ambiguous,\n unresolved: placement.unresolved,\n anchored: response.anchored,\n clusters: placement.clusters.length,\n asOf: response.asOf,\n line: line.textContent ?? \"\",\n };\n rendered = render;\n position();\n return render;\n }\n\n return {\n async refresh(): Promise<MarkerRender> {\n if (destroyed) return EMPTY;\n if (options.reader === null) {\n /**\n * A page armed for the toolbar but linked before markers shipped has no\n * `ai_read` parameter, so there is no credential and nothing was even\n * attempted. SAID, rather than rendered as \"no annotations\": those are\n * two of the twelve causes of zero and telling them apart is the count\n * line's entire job.\n */\n return draw(null, labels.noCredential);\n }\n const path = options.path ?? doc.location?.pathname ?? \"/\";\n try {\n const response = await options.reader.read(path);\n return draw(response, null);\n } catch {\n /**\n * NEVER RETHROWN. A cross-origin preflight refusal arrives here as\n * `TypeError: Failed to fetch` with no status and no body \u2014 the failure\n * `curl` cannot see and source review cannot see \u2014 and a marker layer\n * that threw would take the product's own page down over it. The\n * sentence is what a reviewer gets instead, and it is why this method\n * is documented as never rejecting.\n */\n return draw(null, labels.unreachable);\n }\n },\n\n owns(node: unknown): boolean {\n /**\n * The HOST, not its shadow contents: `composedPath` and `event.target`\n * report the host for anything inside an open shadow root from the\n * outside, and the SDK's `ignoreElement` is asking \"is this ours\" about a\n * click in the product's own coordinate space.\n */\n if (node === host) return true;\n const el = node as { closest?: (s: string) => unknown } | null;\n return typeof el?.closest === \"function\" ? el.closest(`#${MARKERS_ID}`) === host : false;\n },\n\n last: () => rendered,\n\n destroy(): void {\n destroyed = true;\n win.removeEventListener(\"scroll\", onViewportChange, true);\n win.removeEventListener(\"resize\", onViewportChange);\n for (const { pin } of pins) pin.remove();\n pins = [];\n host.remove();\n },\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["SRC_ATTR", "MAX_CHAIN", "parseSourceRef", "value", "colIdx", "lineIdx", "file", "line", "column", "collectSourceChain", "el", "max", "chain", "node", "ref", "SRC_ATTR", "prev", "findAnnotatedAncestor", "sameRef", "a", "b", "chainMatches", "recorded", "live", "MAX_CHAIN", "ref", "i", "other", "leafCandidates", "anchor", "doc", "leaf", "all", "SRC_ATTR", "matched", "el", "own", "parseSourceRef", "parentRef", "findAnnotatedAncestor", "resolveAnchor", "here", "found", "collectSourceChain", "MARKERS_ID", "Z_INDEX", "DEFAULT_MARKER_LABELS", "total", "shown", "reason", "age", "EMPTY", "CSS", "PIN", "MARGIN", "clamp", "value", "low", "high", "STEP", "MAX_PROBES", "freeSpot", "ideal", "taken", "maxTop", "left", "top", "occupied", "l", "t", "s", "probe", "make", "doc", "tag", "attrs", "el", "name", "formatMarkerAge", "ms", "seconds", "minutes", "hours", "rest", "days", "placeMarkers", "markers", "byElement", "ambiguous", "unresolved", "marker", "found", "resolveAnchor", "existing", "element", "list", "mountAnnotationMarkers", "options", "labels", "win", "now", "host", "root", "style", "geometry", "line", "panel", "rendered", "pins", "destroyed", "position", "width", "height", "rules", "i", "rect", "spot", "onViewportChange", "closePanel", "openPanel", "anchorEl", "entry", "who", "at", "what", "state", "fresh", "asOf", "close", "draw", "response", "failure", "pin", "render", "placement", "cluster", "event", "path", "node"]
|
|
7
|
+
}
|
package/dist/protocol.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var a=Object.defineProperty;var A=Object.getOwnPropertyDescriptor;var m=Object.getOwnPropertyNames;var d=Object.prototype.hasOwnProperty;var
|
|
1
|
+
"use strict";var a=Object.defineProperty;var A=Object.getOwnPropertyDescriptor;var m=Object.getOwnPropertyNames;var d=Object.prototype.hasOwnProperty;var T=(e,t)=>{for(var n in t)a(e,n,{get:t[n],enumerable:!0})},f=(e,t,n,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of m(t))!d.call(e,r)&&r!==n&&a(e,r,{get:()=>t[r],enumerable:!(o=A(t,r))||o.enumerable});return e};var x=e=>f(a({},"__esModule",{value:!0}),e);var k={};T(k,{ANNOTATION_ARMED_COOKIE:()=>i,ANNOTATION_ARMED_COOKIES:()=>l,ANNOTATION_ARMED_VALUE:()=>c,ANNOTATION_TOKEN_PARAM:()=>s,ANNOTATION_TOKEN_PARAMS:()=>g,CAPTURE_TOKEN_PARAM:()=>I,LEGACY_ANNOTATION_ARMED_COOKIE:()=>p,LEGACY_ANNOTATION_TOKEN_PARAM:()=>u,NAME_ATTR:()=>E,READ_TOKEN_PARAM:()=>S,REVIEW_RUN_PARAM:()=>P,SDK_VERSION:()=>_,SRC_ATTR:()=>N,armedCookie:()=>h,assertConcreteOrigin:()=>b,clearedCookie:()=>w,isAnnotationArmed:()=>C,isParentMessage:()=>y,readAnnotationToken:()=>M,selectionOf:()=>O});module.exports=x(k);var _="1",N="data-src",E="data-src-name";function O(e){return e.selection==="region"?"region":"element"}var R=new Set(["annotate:enable","annotate:disable","annotate:ping"]);function y(e){if(typeof e!="object"||e===null)return!1;let t=e.type;return typeof t=="string"&&R.has(t)}function b(e){if(!e||e==="*")throw new Error("[annotation] a concrete parent origin is required; '*' would broadcast annotation payloads to any embedder.");return e}var s="ai_annotate",i="ai_annotate_armed",I="ai_capture",P="ai_run",S="ai_read",c="1";function h(e=i){return`${e}=${c}; Path=/; SameSite=Lax; Secure`}function w(e){return`${e}=; Path=/; SameSite=Lax; Secure; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT`}var u="pits_annotate",p="pits_annotate_armed",g=[s,u],l=[i,p];function C(e){return e.split(";").some(t=>{let n=t.trimStart();return l.some(o=>n.startsWith(`${o}=`))})}function M(e){for(let t of g){let n=e.get(t);if(n!==null&&n.length>0)return n}return null}
|
|
2
2
|
//# sourceMappingURL=protocol.cjs.map
|
package/dist/protocol.cjs.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
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\n/**\n * What kind of capture this is, with the default applied once (A-189).\n *\n * `selection` is optional and **absent means `element`** \u2014 every capture taken\n * before A-179, and everything the managed product's live bundle emits today,\n * has no such field. Any other default would retroactively reclassify the whole\n * history as regions, silently.\n *\n * A function rather than a documented convention because the default is the\n * kind of thing two readers implement slightly differently and neither notices:\n * one writes `=== \"region\"`, another writes `?? \"element\"`, a third forgets and\n * treats `undefined` as falsy in a branch that meant something else. This is\n * the protocol's answer, and `compatibility.test.ts` replays every recorded\n * payload through it.\n */\nexport function selectionOf(target: Pick<TargetInfo, \"selection\">): \"element\" | \"region\" {\n return target.selection === \"region\" ? \"region\" : \"element\";\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 CAPTURE token's query parameter (A-240).\n *\n * -------------------------------------------------------------------------\n * WHY IT IS HERE AND NOT WHERE IT IS WRITTEN\n * -------------------------------------------------------------------------\n *\n * A second name on the same wire, and a second credential \u2014 see\n * `docs/plans/first-party-annotation.md` \u00A7 *Authentication*. `ai_annotate`\n * answers *should this page offer an annotation UI at all*; this one answers\n * *may this capture become an issue*. Different secrets, different lifetimes,\n * different blast radii, and deliberately not folded into one token.\n *\n * It lives beside `ANNOTATION_TOKEN_PARAM` for that constant's own reason,\n * stated at length above: the console WRITES it into a deep link and the\n * product's bundle READS it, the two halves ship from different repositories,\n * and a mismatch produces no error anywhere \u2014 the reviewer writes a post-it,\n * clicks send, and the ingest rejects an unsigned capture. One exported\n * constant is the only thing that makes those two spellings unable to drift.\n *\n * It has no legacy spelling because it has never had another one: nothing has\n * shipped it yet. It is exported here BEFORE its writer needs it precisely so\n * the writer never has a reason to spell it itself.\n */\nexport const CAPTURE_TOKEN_PARAM = \"ai_capture\";\n\n/**\n * The run under review, when the console knows of one (A-241).\n *\n * Third name on the same wire, here for the reason the two above are: the\n * console writes it and the product's bundle reads it, from two repositories.\n * `apps/manager/src/lib/request-changes.ts` spells it itself today and its own\n * comment asks for exactly this \u2014 the constant to move into the package so the\n * writer has no reason to spell it. **That deletion is not this slice's**\n * (A-243/A-244 are merged and settled), so for now there are two definitions of\n * one string, and `protocol.test.ts` is the only thing standing between them\n * and a silent divergence. Nothing about it is signed: the ingest already\n * treats it as attacker-supplied text and files on it rather than routing on it.\n */\nexport const REVIEW_RUN_PARAM = \"ai_run\";\n\n/**\n * The arming cookie, as a `Set-Cookie` value (A-241).\n *\n * ===========================================================================\n * IT GOT SIMPLER BY MOVING FIRST-PARTY, AND THAT IS THE WHOLE POINT\n * ===========================================================================\n *\n * Framed, this had to be `SameSite=None; Secure` with no `Partitioned` \u2014 a\n * genuine cross-site cookie, which A-211 records would have broken on its own\n * under Chrome's third-party cookie work, independently of the auth problem\n * that killed the frame. Top-level at the product's own origin it is an\n * ordinary cookie and none of that applies.\n *\n * Each attribute, and why:\n *\n * - **`SameSite=Lax`.** The reviewer arrives by clicking *Request changes* in\n * the console, which is a top-level cross-site GET navigation \u2014 the one case\n * `Lax` still sends on. `Strict` would withhold the cookie on that very first\n * render, so the toolbar would be missing on the page the link opened and\n * appear only after an internal click; that reads as a broken deploy.\n * - **`Secure`.** It is a marker rather than a credential, but a marker that\n * travels in clear is a marker an attacker on the path can set.\n * - **`Path=/`.** Arming survives the reviewer navigating the whole app, which\n * is the point of a cookie rather than a URL parameter.\n * - **No `Max-Age` / `Expires`.** Session-scoped: closing the tab disarms.\n *\n * ===========================================================================\n * WHAT IS *NOT* HERE: `HttpOnly`, AND THE TOKEN\n * ===========================================================================\n *\n * No `HttpOnly`, deliberately. Page script both READS this (`isAnnotationArmed`\n * decides whether a toolbar exists) and CLEARS it (*Done* disarms). An\n * `HttpOnly` arming cookie makes *Done* impossible from the page, which is the\n * one control the reviewer has.\n *\n * That is affordable only because THE VALUE IS A MARKER AND NEVER THE TOKEN.\n * The value is a constant and there is no argument that supplies one: the\n * middleware compares the URL parameter against its secret and then calls this,\n * and what it stores is *yes*, not *what was presented*. The single parameter\n * is the cookie's NAME, and it exists only so a product mid-migration can write\n * the legacy spelling \u2014 it is not a value slot wearing a different label, and\n * `arming.test.ts` pins which side of the `=` it lands on.\n *\n * This is a shape argument, not a guarantee about callers. Nothing stops a\n * determined caller passing a secret as the name; what the signature removes is\n * the ACCIDENT \u2014 the middleware author with a verified token in hand and one\n * parameter available, reaching for the obvious.\n */\nexport const ANNOTATION_ARMED_VALUE = \"1\";\n\nexport function armedCookie(name: string = ANNOTATION_ARMED_COOKIE): string {\n return `${name}=${ANNOTATION_ARMED_VALUE}; Path=/; SameSite=Lax; Secure`;\n}\n\n/**\n * The same cookie, expired \u2014 what *Done* and a disarm write.\n *\n * `Max-Age=0` AND an `Expires` in the past, because they are read by different\n * things and the pair is what every browser honours. The path must match the\n * one set above or the browser stores a second cookie and the first survives \u2014\n * a *Done* button that does nothing and reports success.\n */\nexport function clearedCookie(name: string): string {\n return `${name}=; Path=/; SameSite=Lax; Secure; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT`;\n}\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,2BAAAC,EAAA,4BAAAC,EAAA,wBAAAC,EAAA,mCAAAC,EAAA,kCAAAC,EAAA,cAAAC,EAAA,qBAAAC,EAAA,gBAAAC,EAAA,aAAAC,EAAA,gBAAAC,EAAA,yBAAAC,EAAA,kBAAAC,EAAA,sBAAAC,EAAA,oBAAAC,EAAA,wBAAAC,EAAA,gBAAAC,IAAA,eAAAC,
|
|
6
|
-
"names": ["protocol_exports", "__export", "ANNOTATION_ARMED_COOKIE", "ANNOTATION_ARMED_COOKIES", "ANNOTATION_ARMED_VALUE", "ANNOTATION_TOKEN_PARAM", "ANNOTATION_TOKEN_PARAMS", "CAPTURE_TOKEN_PARAM", "LEGACY_ANNOTATION_ARMED_COOKIE", "LEGACY_ANNOTATION_TOKEN_PARAM", "NAME_ATTR", "REVIEW_RUN_PARAM", "SDK_VERSION", "SRC_ATTR", "armedCookie", "assertConcreteOrigin", "clearedCookie", "isAnnotationArmed", "isParentMessage", "readAnnotationToken", "selectionOf", "__toCommonJS", "target", "PARENT_TYPES", "data", "type", "origin", "name", "cookie", "entry", "trimmed", "params", "value"]
|
|
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\n/**\n * What kind of capture this is, with the default applied once (A-189).\n *\n * `selection` is optional and **absent means `element`** \u2014 every capture taken\n * before A-179, and everything the managed product's live bundle emits today,\n * has no such field. Any other default would retroactively reclassify the whole\n * history as regions, silently.\n *\n * A function rather than a documented convention because the default is the\n * kind of thing two readers implement slightly differently and neither notices:\n * one writes `=== \"region\"`, another writes `?? \"element\"`, a third forgets and\n * treats `undefined` as falsy in a branch that meant something else. This is\n * the protocol's answer, and `compatibility.test.ts` replays every recorded\n * payload through it.\n */\nexport function selectionOf(target: Pick<TargetInfo, \"selection\">): \"element\" | \"region\" {\n return target.selection === \"region\" ? \"region\" : \"element\";\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 CAPTURE token's query parameter (A-240).\n *\n * -------------------------------------------------------------------------\n * WHY IT IS HERE AND NOT WHERE IT IS WRITTEN\n * -------------------------------------------------------------------------\n *\n * A second name on the same wire, and a second credential \u2014 see\n * `docs/plans/first-party-annotation.md` \u00A7 *Authentication*. `ai_annotate`\n * answers *should this page offer an annotation UI at all*; this one answers\n * *may this capture become an issue*. Different secrets, different lifetimes,\n * different blast radii, and deliberately not folded into one token.\n *\n * It lives beside `ANNOTATION_TOKEN_PARAM` for that constant's own reason,\n * stated at length above: the console WRITES it into a deep link and the\n * product's bundle READS it, the two halves ship from different repositories,\n * and a mismatch produces no error anywhere \u2014 the reviewer writes a post-it,\n * clicks send, and the ingest rejects an unsigned capture. One exported\n * constant is the only thing that makes those two spellings unable to drift.\n *\n * It has no legacy spelling because it has never had another one: nothing has\n * shipped it yet. It is exported here BEFORE its writer needs it precisely so\n * the writer never has a reason to spell it itself.\n */\nexport const CAPTURE_TOKEN_PARAM = \"ai_capture\";\n\n/**\n * The run under review, when the console knows of one (A-241).\n *\n * Third name on the same wire, here for the reason the two above are: the\n * console writes it and the product's bundle reads it, from two repositories.\n * `apps/manager/src/lib/request-changes.ts` spells it itself today and its own\n * comment asks for exactly this \u2014 the constant to move into the package so the\n * writer has no reason to spell it. **That deletion is not this slice's**\n * (A-243/A-244 are merged and settled), so for now there are two definitions of\n * one string, and `protocol.test.ts` is the only thing standing between them\n * and a silent divergence. Nothing about it is signed: the ingest already\n * treats it as attacker-supplied text and files on it rather than routing on it.\n */\nexport const REVIEW_RUN_PARAM = \"ai_run\";\n\n/**\n * The READ token's query parameter (A-277).\n *\n * ===========================================================================\n * WHY THE MARKER LAYER GETS ITS OWN CREDENTIAL AND NOT `ai_capture`\n * ===========================================================================\n *\n * Fourth name on the same wire, and the reason it is a fourth is A-276's\n * disclosure decision rather than tidiness. The capture token is minted for\n * WRITING and lives four hours so a reviewer can work through a session; reusing\n * it for the read would make one leaked deep link a write capability AND a read\n * capability over every reviewer's comments on that origin for the same window.\n * The scope is a signed field, `verifyCaptureToken` requires an expectation at\n * every call site, and the read endpoint refuses a write claim \u2014 so handing\n * `ai_capture` to the reader does not degrade to a read, it fails.\n *\n * The read claim's TTL is ONE hour rather than four (`READ_TOKEN_TTL_S`), which\n * is the practical consequence a reader of this constant should know: the\n * toolbar outlives the markers, so a reviewer who leaves a tab open all\n * afternoon will find the count line saying it cannot read before they find the\n * Send button broken. That is the intended ordering \u2014 the shorter credential is\n * the one that discloses other people's words.\n *\n * Exported here for the reason the three above are: the console writes it into a\n * deep link and the product's bundle reads it, from two repositories, and a\n * mismatch produces no error anywhere \u2014 just a page with no markers, which is\n * indistinguishable from nine other causes.\n */\nexport const READ_TOKEN_PARAM = \"ai_read\";\n\n/**\n * The arming cookie, as a `Set-Cookie` value (A-241).\n *\n * ===========================================================================\n * IT GOT SIMPLER BY MOVING FIRST-PARTY, AND THAT IS THE WHOLE POINT\n * ===========================================================================\n *\n * Framed, this had to be `SameSite=None; Secure` with no `Partitioned` \u2014 a\n * genuine cross-site cookie, which A-211 records would have broken on its own\n * under Chrome's third-party cookie work, independently of the auth problem\n * that killed the frame. Top-level at the product's own origin it is an\n * ordinary cookie and none of that applies.\n *\n * Each attribute, and why:\n *\n * - **`SameSite=Lax`.** The reviewer arrives by clicking *Request changes* in\n * the console, which is a top-level cross-site GET navigation \u2014 the one case\n * `Lax` still sends on. `Strict` would withhold the cookie on that very first\n * render, so the toolbar would be missing on the page the link opened and\n * appear only after an internal click; that reads as a broken deploy.\n * - **`Secure`.** It is a marker rather than a credential, but a marker that\n * travels in clear is a marker an attacker on the path can set.\n * - **`Path=/`.** Arming survives the reviewer navigating the whole app, which\n * is the point of a cookie rather than a URL parameter.\n * - **No `Max-Age` / `Expires`.** Session-scoped: closing the tab disarms.\n *\n * ===========================================================================\n * WHAT IS *NOT* HERE: `HttpOnly`, AND THE TOKEN\n * ===========================================================================\n *\n * No `HttpOnly`, deliberately. Page script both READS this (`isAnnotationArmed`\n * decides whether a toolbar exists) and CLEARS it (*Done* disarms). An\n * `HttpOnly` arming cookie makes *Done* impossible from the page, which is the\n * one control the reviewer has.\n *\n * That is affordable only because THE VALUE IS A MARKER AND NEVER THE TOKEN.\n * The value is a constant and there is no argument that supplies one: the\n * middleware compares the URL parameter against its secret and then calls this,\n * and what it stores is *yes*, not *what was presented*. The single parameter\n * is the cookie's NAME, and it exists only so a product mid-migration can write\n * the legacy spelling \u2014 it is not a value slot wearing a different label, and\n * `arming.test.ts` pins which side of the `=` it lands on.\n *\n * This is a shape argument, not a guarantee about callers. Nothing stops a\n * determined caller passing a secret as the name; what the signature removes is\n * the ACCIDENT \u2014 the middleware author with a verified token in hand and one\n * parameter available, reaching for the obvious.\n */\nexport const ANNOTATION_ARMED_VALUE = \"1\";\n\nexport function armedCookie(name: string = ANNOTATION_ARMED_COOKIE): string {\n return `${name}=${ANNOTATION_ARMED_VALUE}; Path=/; SameSite=Lax; Secure`;\n}\n\n/**\n * The same cookie, expired \u2014 what *Done* and a disarm write.\n *\n * `Max-Age=0` AND an `Expires` in the past, because they are read by different\n * things and the pair is what every browser honours. The path must match the\n * one set above or the browser stores a second cookie and the first survives \u2014\n * a *Done* button that does nothing and reports success.\n */\nexport function clearedCookie(name: string): string {\n return `${name}=; Path=/; SameSite=Lax; Secure; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT`;\n}\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,2BAAAC,EAAA,4BAAAC,EAAA,wBAAAC,EAAA,mCAAAC,EAAA,kCAAAC,EAAA,cAAAC,EAAA,qBAAAC,EAAA,qBAAAC,EAAA,gBAAAC,EAAA,aAAAC,EAAA,gBAAAC,EAAA,yBAAAC,EAAA,kBAAAC,EAAA,sBAAAC,EAAA,oBAAAC,EAAA,wBAAAC,EAAA,gBAAAC,IAAA,eAAAC,EAAAtB,GASO,IAAMa,EAAc,IAmBdC,EAAW,WACXJ,EAAY,gBAoHlB,SAASW,EAAYE,EAA6D,CACvF,OAAOA,EAAO,YAAc,SAAW,SAAW,SACpD,CAmCA,IAAMC,EAAe,IAAI,IAAI,CAAC,kBAAmB,mBAAoB,eAAe,CAAC,EAQ9E,SAASL,EAAgBM,EAAsC,CACpE,GAAI,OAAOA,GAAS,UAAYA,IAAS,KAAM,MAAO,GACtD,IAAMC,EAAQD,EAA4B,KAC1C,OAAO,OAAOC,GAAS,UAAYF,EAAa,IAAIE,CAAI,CAC1D,CAUO,SAASV,EAAqBW,EAAwB,CAC3D,GAAI,CAACA,GAAUA,IAAW,IACxB,MAAM,IAAI,MACR,6GAEF,EAEF,OAAOA,CACT,CAkBO,IAAMtB,EAAyB,cACzBH,EAA0B,oBA0B1BK,EAAsB,aAetBK,EAAmB,SA8BnBD,EAAmB,UAkDnBP,EAAyB,IAE/B,SAASW,EAAYa,EAAe1B,EAAiC,CAC1E,MAAO,GAAG0B,CAAI,IAAIxB,CAAsB,gCAC1C,CAUO,SAASa,EAAcW,EAAsB,CAClD,MAAO,GAAGA,CAAI,mFAChB,CAwCO,IAAMnB,EAAgC,gBAChCD,EAAiC,sBA2CjCF,EAA0B,CACrCD,EACAI,CACF,EAGaN,EAA2B,CACtCD,EACAM,CACF,EAUO,SAASU,EAAkBW,EAAyB,CACzD,OAAOA,EACJ,MAAM,GAAG,EACT,KAAMC,GAAU,CACf,IAAMC,EAAUD,EAAM,UAAU,EAChC,OAAO3B,EAAyB,KAAMyB,GAASG,EAAQ,WAAW,GAAGH,CAAI,GAAG,CAAC,CAC/E,CAAC,CACL,CASO,SAASR,EAAoBY,EAAwC,CAC1E,QAAWJ,KAAQtB,EAAyB,CAC1C,IAAM2B,EAAQD,EAAO,IAAIJ,CAAI,EAC7B,GAAIK,IAAU,MAAQA,EAAM,OAAS,EAAG,OAAOA,CACjD,CACA,OAAO,IACT",
|
|
6
|
+
"names": ["protocol_exports", "__export", "ANNOTATION_ARMED_COOKIE", "ANNOTATION_ARMED_COOKIES", "ANNOTATION_ARMED_VALUE", "ANNOTATION_TOKEN_PARAM", "ANNOTATION_TOKEN_PARAMS", "CAPTURE_TOKEN_PARAM", "LEGACY_ANNOTATION_ARMED_COOKIE", "LEGACY_ANNOTATION_TOKEN_PARAM", "NAME_ATTR", "READ_TOKEN_PARAM", "REVIEW_RUN_PARAM", "SDK_VERSION", "SRC_ATTR", "armedCookie", "assertConcreteOrigin", "clearedCookie", "isAnnotationArmed", "isParentMessage", "readAnnotationToken", "selectionOf", "__toCommonJS", "target", "PARENT_TYPES", "data", "type", "origin", "name", "cookie", "entry", "trimmed", "params", "value"]
|
|
7
7
|
}
|
package/dist/protocol.d.cts
CHANGED
|
@@ -262,6 +262,35 @@ export declare const CAPTURE_TOKEN_PARAM = "ai_capture";
|
|
|
262
262
|
* treats it as attacker-supplied text and files on it rather than routing on it.
|
|
263
263
|
*/
|
|
264
264
|
export declare const REVIEW_RUN_PARAM = "ai_run";
|
|
265
|
+
/**
|
|
266
|
+
* The READ token's query parameter (A-277).
|
|
267
|
+
*
|
|
268
|
+
* ===========================================================================
|
|
269
|
+
* WHY THE MARKER LAYER GETS ITS OWN CREDENTIAL AND NOT `ai_capture`
|
|
270
|
+
* ===========================================================================
|
|
271
|
+
*
|
|
272
|
+
* Fourth name on the same wire, and the reason it is a fourth is A-276's
|
|
273
|
+
* disclosure decision rather than tidiness. The capture token is minted for
|
|
274
|
+
* WRITING and lives four hours so a reviewer can work through a session; reusing
|
|
275
|
+
* it for the read would make one leaked deep link a write capability AND a read
|
|
276
|
+
* capability over every reviewer's comments on that origin for the same window.
|
|
277
|
+
* The scope is a signed field, `verifyCaptureToken` requires an expectation at
|
|
278
|
+
* every call site, and the read endpoint refuses a write claim — so handing
|
|
279
|
+
* `ai_capture` to the reader does not degrade to a read, it fails.
|
|
280
|
+
*
|
|
281
|
+
* The read claim's TTL is ONE hour rather than four (`READ_TOKEN_TTL_S`), which
|
|
282
|
+
* is the practical consequence a reader of this constant should know: the
|
|
283
|
+
* toolbar outlives the markers, so a reviewer who leaves a tab open all
|
|
284
|
+
* afternoon will find the count line saying it cannot read before they find the
|
|
285
|
+
* Send button broken. That is the intended ordering — the shorter credential is
|
|
286
|
+
* the one that discloses other people's words.
|
|
287
|
+
*
|
|
288
|
+
* Exported here for the reason the three above are: the console writes it into a
|
|
289
|
+
* deep link and the product's bundle reads it, from two repositories, and a
|
|
290
|
+
* mismatch produces no error anywhere — just a page with no markers, which is
|
|
291
|
+
* indistinguishable from nine other causes.
|
|
292
|
+
*/
|
|
293
|
+
export declare const READ_TOKEN_PARAM = "ai_read";
|
|
265
294
|
/**
|
|
266
295
|
* The arming cookie, as a `Set-Cookie` value (A-241).
|
|
267
296
|
*
|
package/dist/protocol.d.mts
CHANGED
|
@@ -262,6 +262,35 @@ export declare const CAPTURE_TOKEN_PARAM = "ai_capture";
|
|
|
262
262
|
* treats it as attacker-supplied text and files on it rather than routing on it.
|
|
263
263
|
*/
|
|
264
264
|
export declare const REVIEW_RUN_PARAM = "ai_run";
|
|
265
|
+
/**
|
|
266
|
+
* The READ token's query parameter (A-277).
|
|
267
|
+
*
|
|
268
|
+
* ===========================================================================
|
|
269
|
+
* WHY THE MARKER LAYER GETS ITS OWN CREDENTIAL AND NOT `ai_capture`
|
|
270
|
+
* ===========================================================================
|
|
271
|
+
*
|
|
272
|
+
* Fourth name on the same wire, and the reason it is a fourth is A-276's
|
|
273
|
+
* disclosure decision rather than tidiness. The capture token is minted for
|
|
274
|
+
* WRITING and lives four hours so a reviewer can work through a session; reusing
|
|
275
|
+
* it for the read would make one leaked deep link a write capability AND a read
|
|
276
|
+
* capability over every reviewer's comments on that origin for the same window.
|
|
277
|
+
* The scope is a signed field, `verifyCaptureToken` requires an expectation at
|
|
278
|
+
* every call site, and the read endpoint refuses a write claim — so handing
|
|
279
|
+
* `ai_capture` to the reader does not degrade to a read, it fails.
|
|
280
|
+
*
|
|
281
|
+
* The read claim's TTL is ONE hour rather than four (`READ_TOKEN_TTL_S`), which
|
|
282
|
+
* is the practical consequence a reader of this constant should know: the
|
|
283
|
+
* toolbar outlives the markers, so a reviewer who leaves a tab open all
|
|
284
|
+
* afternoon will find the count line saying it cannot read before they find the
|
|
285
|
+
* Send button broken. That is the intended ordering — the shorter credential is
|
|
286
|
+
* the one that discloses other people's words.
|
|
287
|
+
*
|
|
288
|
+
* Exported here for the reason the three above are: the console writes it into a
|
|
289
|
+
* deep link and the product's bundle reads it, from two repositories, and a
|
|
290
|
+
* mismatch produces no error anywhere — just a page with no markers, which is
|
|
291
|
+
* indistinguishable from nine other causes.
|
|
292
|
+
*/
|
|
293
|
+
export declare const READ_TOKEN_PARAM = "ai_read";
|
|
265
294
|
/**
|
|
266
295
|
* The arming cookie, as a `Set-Cookie` value (A-241).
|
|
267
296
|
*
|
package/dist/protocol.d.ts
CHANGED
|
@@ -262,6 +262,35 @@ export declare const CAPTURE_TOKEN_PARAM = "ai_capture";
|
|
|
262
262
|
* treats it as attacker-supplied text and files on it rather than routing on it.
|
|
263
263
|
*/
|
|
264
264
|
export declare const REVIEW_RUN_PARAM = "ai_run";
|
|
265
|
+
/**
|
|
266
|
+
* The READ token's query parameter (A-277).
|
|
267
|
+
*
|
|
268
|
+
* ===========================================================================
|
|
269
|
+
* WHY THE MARKER LAYER GETS ITS OWN CREDENTIAL AND NOT `ai_capture`
|
|
270
|
+
* ===========================================================================
|
|
271
|
+
*
|
|
272
|
+
* Fourth name on the same wire, and the reason it is a fourth is A-276's
|
|
273
|
+
* disclosure decision rather than tidiness. The capture token is minted for
|
|
274
|
+
* WRITING and lives four hours so a reviewer can work through a session; reusing
|
|
275
|
+
* it for the read would make one leaked deep link a write capability AND a read
|
|
276
|
+
* capability over every reviewer's comments on that origin for the same window.
|
|
277
|
+
* The scope is a signed field, `verifyCaptureToken` requires an expectation at
|
|
278
|
+
* every call site, and the read endpoint refuses a write claim — so handing
|
|
279
|
+
* `ai_capture` to the reader does not degrade to a read, it fails.
|
|
280
|
+
*
|
|
281
|
+
* The read claim's TTL is ONE hour rather than four (`READ_TOKEN_TTL_S`), which
|
|
282
|
+
* is the practical consequence a reader of this constant should know: the
|
|
283
|
+
* toolbar outlives the markers, so a reviewer who leaves a tab open all
|
|
284
|
+
* afternoon will find the count line saying it cannot read before they find the
|
|
285
|
+
* Send button broken. That is the intended ordering — the shorter credential is
|
|
286
|
+
* the one that discloses other people's words.
|
|
287
|
+
*
|
|
288
|
+
* Exported here for the reason the three above are: the console writes it into a
|
|
289
|
+
* deep link and the product's bundle reads it, from two repositories, and a
|
|
290
|
+
* mismatch produces no error anywhere — just a page with no markers, which is
|
|
291
|
+
* indistinguishable from nine other causes.
|
|
292
|
+
*/
|
|
293
|
+
export declare const READ_TOKEN_PARAM = "ai_read";
|
|
265
294
|
/**
|
|
266
295
|
* The arming cookie, as a `Set-Cookie` value (A-241).
|
|
267
296
|
*
|
package/dist/protocol.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var l="1",A="data-src",m="data-src-name";function d(e){return e.selection==="region"?"region":"element"}var a=new Set(["annotate:enable","annotate:disable","annotate:ping"]);function
|
|
1
|
+
var l="1",A="data-src",m="data-src-name";function d(e){return e.selection==="region"?"region":"element"}var a=new Set(["annotate:enable","annotate:disable","annotate:ping"]);function T(e){if(typeof e!="object"||e===null)return!1;let t=e.type;return typeof t=="string"&&a.has(t)}function f(e){if(!e||e==="*")throw new Error("[annotation] a concrete parent origin is required; '*' would broadcast annotation payloads to any embedder.");return e}var i="ai_annotate",r="ai_annotate_armed",x="ai_capture",_="ai_run",N="ai_read",s="1";function E(e=r){return`${e}=${s}; Path=/; SameSite=Lax; Secure`}function O(e){return`${e}=; Path=/; SameSite=Lax; Secure; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT`}var c="pits_annotate",u="pits_annotate_armed",p=[i,c],g=[r,u];function R(e){return e.split(";").some(t=>{let n=t.trimStart();return g.some(o=>n.startsWith(`${o}=`))})}function y(e){for(let t of p){let n=e.get(t);if(n!==null&&n.length>0)return n}return null}export{r as ANNOTATION_ARMED_COOKIE,g as ANNOTATION_ARMED_COOKIES,s as ANNOTATION_ARMED_VALUE,i as ANNOTATION_TOKEN_PARAM,p as ANNOTATION_TOKEN_PARAMS,x as CAPTURE_TOKEN_PARAM,u as LEGACY_ANNOTATION_ARMED_COOKIE,c as LEGACY_ANNOTATION_TOKEN_PARAM,m as NAME_ATTR,N as READ_TOKEN_PARAM,_ as REVIEW_RUN_PARAM,l as SDK_VERSION,A as SRC_ATTR,E as armedCookie,f as assertConcreteOrigin,O as clearedCookie,R as isAnnotationArmed,T as isParentMessage,y as readAnnotationToken,d as selectionOf};
|
|
2
2
|
//# sourceMappingURL=protocol.mjs.map
|