@copilotkit/web-inspector 2.0.0-rc.1 → 2.0.0-rc.11

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown.cjs","names":[],"sources":["../../src/lib/markdown.ts"],"sourcesContent":["import {\n createStreamingMarkdownParserState,\n finalizeStreamingMarkdown,\n parseStreamingMarkdownChunk,\n} from \"@copilotkit/markdown-renderer\";\nimport type { StreamingMarkdownAstNode } from \"@copilotkit/markdown-renderer\";\n\n/** Inspector policy the HTML renderer applies to URLs and code blocks. */\nexport type MarkdownHtmlOptions = Readonly<{\n /** The href to emit for a link or autolink. Return \"#\" to neuter it. */\n linkHref: (url: string) => string;\n /** The src to emit for an image, or null to render its alt text instead. */\n imageSrc: (url: string) => string | null;\n /** The `data-copy` payload for a code block's Copy button. */\n copyPayload: (code: string) => string;\n}>;\n\ntype MarkdownNode = StreamingMarkdownAstNode;\ntype NodeOfType<T extends MarkdownNode[\"type\"]> = Extract<\n MarkdownNode,\n { type: T }\n>;\n\ntype RenderContext = Readonly<{\n nodes: ReadonlyMap<number, MarkdownNode>;\n /** Normalized Markdown source; node ranges index into it. */\n source: string;\n options: MarkdownHtmlOptions;\n}>;\n\nconst ESCAPED: Readonly<Record<string, string>> = {\n \"&\": \"&amp;\",\n \"<\": \"&lt;\",\n \">\": \"&gt;\",\n '\"': \"&quot;\",\n \"'\": \"&#39;\",\n};\n\n/** Escape every markup-significant character (attributes, code). */\nfunction escapeAll(value: string): string {\n return value.replace(/[&<>\"']/g, (char) => ESCAPED[char]!);\n}\n\n/**\n * Escape prose text but leave existing character references such as `&amp;`\n * or `&copy;` alone, as Markdown (and the previous marked renderer) does. A\n * character reference cannot open a tag, so this does not weaken escaping.\n */\nfunction escapeText(value: string): string {\n return value.replace(\n /[<>\"']|&(?!#\\d{1,7};|#[Xx][a-fA-F0-9]{1,6};|\\w+;)/g,\n (char) => ESCAPED[char]!,\n );\n}\n\n/**\n * Render a complete Markdown document to an HTML string for the What's new\n * article. The output is meant for `unsafeHTML`, so every piece of source text\n * is escaped: raw HTML in the Markdown renders as visible text, and URLs only\n * reach attributes through the caller's `linkHref` / `imageSrc` policy.\n */\nexport function renderMarkdownToHtml(\n markdown: string,\n options: MarkdownHtmlOptions,\n): string {\n const state = finalizeStreamingMarkdown(\n parseStreamingMarkdownChunk(\n createStreamingMarkdownParserState({ segmenter: false }),\n markdown,\n ),\n );\n const nodes = new Map(state.nodes.map((node) => [node.id, node]));\n const root = state.rootId === null ? undefined : nodes.get(state.rootId);\n if (!root || root.type !== \"document\") return \"\";\n return renderChildren(root.children, {\n nodes,\n source: state.source,\n options,\n });\n}\n\nfunction childNodes(ids: readonly number[], context: RenderContext) {\n return ids.flatMap((id) => {\n const node = context.nodes.get(id);\n return node ? [node] : [];\n });\n}\n\nfunction renderChildren(\n ids: readonly number[],\n context: RenderContext,\n): string {\n return childNodes(ids, context)\n .map((node) => renderNode(node, context))\n .join(\"\");\n}\n\nfunction renderNode(node: MarkdownNode, context: RenderContext): string {\n switch (node.type) {\n case \"document\":\n case \"table-row\":\n case \"table-cell\":\n return renderChildren(node.children, context);\n case \"paragraph\":\n return `<p>${renderChildren(node.children, context)}</p>\\n`;\n case \"heading\":\n return `<h${node.level}>${renderChildren(node.children, context)}</h${node.level}>\\n`;\n case \"blockquote\":\n return `<blockquote>\\n${renderChildren(node.children, context)}</blockquote>\\n`;\n case \"list\":\n return renderList(node, context);\n case \"list-item\":\n return `<li>${renderChildren(node.children, context)}</li>\\n`;\n case \"code-block\":\n return renderCodeBlock(node, context);\n case \"table\":\n return renderTable(node, context);\n case \"thematic-break\":\n return \"<hr>\\n\";\n case \"text\":\n return escapeText(node.text);\n case \"em\":\n return `<em>${renderChildren(node.children, context)}</em>`;\n case \"strong\":\n return `<strong>${renderChildren(node.children, context)}</strong>`;\n case \"strikethrough\":\n return `<del>${renderChildren(node.children, context)}</del>`;\n case \"inline-code\":\n return `<code>${escapeAll(node.text)}</code>`;\n case \"soft-break\":\n return \"\\n\";\n case \"hard-break\":\n return \"<br>\";\n case \"link\":\n return renderLink(\n node.url,\n node.title,\n renderChildren(node.children, context),\n context,\n );\n case \"autolink\":\n return renderLink(node.url, undefined, escapeText(node.text), context);\n case \"image\":\n return renderImage(node, context);\n case \"citation\":\n // The article has no footnotes; show the reference as it was typed.\n return escapeText(`[^${node.idRef}]`);\n default: {\n const unhandled: never = node;\n return unhandled;\n }\n }\n}\n\n/** A piece of list item content: one authored paragraph, or another block. */\ntype ItemPart =\n | { kind: \"paragraph\"; nodes: MarkdownNode[] }\n | { kind: \"block\"; node: MarkdownNode };\n\n/**\n * The parser keeps each list item as a single paragraph, so a blank line\n * inside an item survives only as two soft breaks in a row. Split there to\n * recover the paragraphs the author wrote.\n */\nfunction itemParts(\n item: NodeOfType<\"list-item\">,\n context: RenderContext,\n): ItemPart[] {\n return childNodes(item.children, context).flatMap((block): ItemPart[] =>\n block.type === \"paragraph\"\n ? splitAtBlankLines(childNodes(block.children, context)).map((nodes) => ({\n kind: \"paragraph\",\n nodes,\n }))\n : [{ kind: \"block\", node: block }],\n );\n}\n\nfunction splitAtBlankLines(nodes: readonly MarkdownNode[]): MarkdownNode[][] {\n const paragraphs: MarkdownNode[][] = [[]];\n let breaks: MarkdownNode[] = [];\n for (const node of nodes) {\n const isBlankText = node.type === \"text\" && node.text.trim() === \"\";\n if (node.type === \"soft-break\" || (breaks.length > 0 && isBlankText)) {\n breaks.push(node);\n continue;\n }\n const current = paragraphs[paragraphs.length - 1]!;\n if (breaks.filter((n) => n.type === \"soft-break\").length >= 2) {\n paragraphs.push([node]);\n } else {\n current.push(...breaks, node);\n }\n breaks = [];\n }\n return paragraphs.filter((paragraph) => paragraph.length > 0);\n}\n\n/**\n * A list is loose when a blank line separates two of its items or two\n * paragraphs inside one item. The blank line that ends the list does not\n * count, so a list followed by more prose still renders tight.\n */\nfunction isLooseList(\n items: readonly NodeOfType<\"list-item\">[],\n parts: readonly ItemPart[][],\n context: RenderContext,\n): boolean {\n const hasSplitItem = parts.some(\n (item) => item.filter((part) => part.kind === \"paragraph\").length > 1,\n );\n return (\n hasSplitItem ||\n items.slice(0, -1).some((item, index) => {\n const between = context.source.slice(\n item.range.start,\n items[index + 1]!.range.start,\n );\n return /\\n[ \\t]*\\n/.test(between);\n })\n );\n}\n\nconst TASK_MARKER = /^\\[([ xX])\\][ \\t]+/;\n\n/** Render inline nodes, turning a leading `[ ]` / `[x]` into a checkbox. */\nfunction renderTaskAwareInline(\n nodes: readonly MarkdownNode[],\n context: RenderContext,\n): string {\n const [first, ...rest] = nodes;\n const task = first?.type === \"text\" ? TASK_MARKER.exec(first.text) : null;\n if (!first || first.type !== \"text\" || !task) {\n return nodes.map((node) => renderNode(node, context)).join(\"\");\n }\n const checked = task[1] === \" \" ? \"\" : 'checked=\"\" ';\n const label = escapeText(first.text.slice(task[0].length));\n const tail = rest.map((node) => renderNode(node, context)).join(\"\");\n return `<input ${checked}disabled=\"\" type=\"checkbox\"> ${label}${tail}`;\n}\n\nfunction renderList(node: NodeOfType<\"list\">, context: RenderContext): string {\n const tag = node.ordered ? \"ol\" : \"ul\";\n const start =\n node.ordered && node.start !== null && node.start !== 1\n ? ` start=\"${node.start}\"`\n : \"\";\n const items = childNodes(node.children, context).filter(\n (item): item is NodeOfType<\"list-item\"> => item.type === \"list-item\",\n );\n const parts = items.map((item) => itemParts(item, context));\n const loose = isLooseList(items, parts, context);\n const body = parts\n .map((itemContent) => {\n const content = itemContent\n .map((part, index) => {\n if (part.kind === \"block\") return renderNode(part.node, context);\n const inline =\n index === 0\n ? renderTaskAwareInline(part.nodes, context)\n : part.nodes.map((n) => renderNode(n, context)).join(\"\");\n return loose ? `<p>${inline}</p>\\n` : inline;\n })\n .join(\"\");\n return `<li>${content}</li>\\n`;\n })\n .join(\"\");\n return `<${tag}${start}>\\n${body}</${tag}>\\n`;\n}\n\nfunction renderCodeBlock(\n node: NodeOfType<\"code-block\">,\n context: RenderContext,\n): string {\n const language = (node.info ?? \"\").replace(/[^a-z0-9-]/gi, \"\");\n const languageClass = language ? ` class=\"language-${language}\"` : \"\";\n const payload = escapeAll(context.options.copyPayload(node.text));\n return `<div class=\"announcement-code\"><pre><code${languageClass}>${escapeAll(node.text)}</code></pre><div class=\"announcement-code__copy-shield\"><button type=\"button\" class=\"announcement-code__copy\" data-copy=\"${payload}\" aria-label=\"Copy code\">Copy</button></div></div>`;\n}\n\nfunction renderTable(\n node: NodeOfType<\"table\">,\n context: RenderContext,\n): string {\n const rows = childNodes(node.children, context).filter(\n (row): row is NodeOfType<\"table-row\"> => row.type === \"table-row\",\n );\n const renderRow = (row: NodeOfType<\"table-row\">) => {\n const tag = row.isHeader ? \"th\" : \"td\";\n const cells = childNodes(row.children, context)\n .map((cell, index) => {\n const align = node.align[index];\n const alignAttr = align && align !== \"none\" ? ` align=\"${align}\"` : \"\";\n return `<${tag}${alignAttr}>${renderNode(cell, context)}</${tag}>\\n`;\n })\n .join(\"\");\n return `<tr>\\n${cells}</tr>\\n`;\n };\n const header = rows.filter((row) => row.isHeader).map(renderRow);\n const body = rows.filter((row) => !row.isHeader).map(renderRow);\n return `<table>\\n<thead>\\n${header.join(\"\")}</thead>\\n${\n body.length > 0 ? `<tbody>${body.join(\"\")}</tbody>` : \"\"\n }</table>\\n`;\n}\n\nfunction renderImage(\n node: NodeOfType<\"image\">,\n context: RenderContext,\n): string {\n const src = context.options.imageSrc(node.url);\n if (src === null) return escapeText(node.alt);\n const title = node.title ? ` title=\"${escapeAll(node.title)}\"` : \"\";\n return `<img src=\"${escapeAll(src)}\" alt=\"${escapeAll(node.alt)}\"${title}>`;\n}\n\nfunction renderLink(\n url: string,\n title: string | undefined,\n content: string,\n context: RenderContext,\n): string {\n const href = escapeAll(context.options.linkHref(url));\n const titleAttr = title ? ` title=\"${escapeAll(title)}\"` : \"\";\n return `<a href=\"${href}\" target=\"_blank\" rel=\"noopener\"${titleAttr}>${content}</a>`;\n}\n"],"mappings":"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"}
@@ -0,0 +1,170 @@
1
+ import { createStreamingMarkdownParserState, finalizeStreamingMarkdown, parseStreamingMarkdownChunk } from "@copilotkit/markdown-renderer";
2
+
3
+ //#region src/lib/markdown.ts
4
+ const ESCAPED = {
5
+ "&": "&amp;",
6
+ "<": "&lt;",
7
+ ">": "&gt;",
8
+ "\"": "&quot;",
9
+ "'": "&#39;"
10
+ };
11
+ /** Escape every markup-significant character (attributes, code). */
12
+ function escapeAll(value) {
13
+ return value.replace(/[&<>"']/g, (char) => ESCAPED[char]);
14
+ }
15
+ /**
16
+ * Escape prose text but leave existing character references such as `&amp;`
17
+ * or `&copy;` alone, as Markdown (and the previous marked renderer) does. A
18
+ * character reference cannot open a tag, so this does not weaken escaping.
19
+ */
20
+ function escapeText(value) {
21
+ return value.replace(/[<>"']|&(?!#\d{1,7};|#[Xx][a-fA-F0-9]{1,6};|\w+;)/g, (char) => ESCAPED[char]);
22
+ }
23
+ /**
24
+ * Render a complete Markdown document to an HTML string for the What's new
25
+ * article. The output is meant for `unsafeHTML`, so every piece of source text
26
+ * is escaped: raw HTML in the Markdown renders as visible text, and URLs only
27
+ * reach attributes through the caller's `linkHref` / `imageSrc` policy.
28
+ */
29
+ function renderMarkdownToHtml(markdown, options) {
30
+ const state = finalizeStreamingMarkdown(parseStreamingMarkdownChunk(createStreamingMarkdownParserState({ segmenter: false }), markdown));
31
+ const nodes = new Map(state.nodes.map((node) => [node.id, node]));
32
+ const root = state.rootId === null ? void 0 : nodes.get(state.rootId);
33
+ if (!root || root.type !== "document") return "";
34
+ return renderChildren(root.children, {
35
+ nodes,
36
+ source: state.source,
37
+ options
38
+ });
39
+ }
40
+ function childNodes(ids, context) {
41
+ return ids.flatMap((id) => {
42
+ const node = context.nodes.get(id);
43
+ return node ? [node] : [];
44
+ });
45
+ }
46
+ function renderChildren(ids, context) {
47
+ return childNodes(ids, context).map((node) => renderNode(node, context)).join("");
48
+ }
49
+ function renderNode(node, context) {
50
+ switch (node.type) {
51
+ case "document":
52
+ case "table-row":
53
+ case "table-cell": return renderChildren(node.children, context);
54
+ case "paragraph": return `<p>${renderChildren(node.children, context)}</p>\n`;
55
+ case "heading": return `<h${node.level}>${renderChildren(node.children, context)}</h${node.level}>\n`;
56
+ case "blockquote": return `<blockquote>\n${renderChildren(node.children, context)}</blockquote>\n`;
57
+ case "list": return renderList(node, context);
58
+ case "list-item": return `<li>${renderChildren(node.children, context)}</li>\n`;
59
+ case "code-block": return renderCodeBlock(node, context);
60
+ case "table": return renderTable(node, context);
61
+ case "thematic-break": return "<hr>\n";
62
+ case "text": return escapeText(node.text);
63
+ case "em": return `<em>${renderChildren(node.children, context)}</em>`;
64
+ case "strong": return `<strong>${renderChildren(node.children, context)}</strong>`;
65
+ case "strikethrough": return `<del>${renderChildren(node.children, context)}</del>`;
66
+ case "inline-code": return `<code>${escapeAll(node.text)}</code>`;
67
+ case "soft-break": return "\n";
68
+ case "hard-break": return "<br>";
69
+ case "link": return renderLink(node.url, node.title, renderChildren(node.children, context), context);
70
+ case "autolink": return renderLink(node.url, void 0, escapeText(node.text), context);
71
+ case "image": return renderImage(node, context);
72
+ case "citation": return escapeText(`[^${node.idRef}]`);
73
+ default: return node;
74
+ }
75
+ }
76
+ /**
77
+ * The parser keeps each list item as a single paragraph, so a blank line
78
+ * inside an item survives only as two soft breaks in a row. Split there to
79
+ * recover the paragraphs the author wrote.
80
+ */
81
+ function itemParts(item, context) {
82
+ return childNodes(item.children, context).flatMap((block) => block.type === "paragraph" ? splitAtBlankLines(childNodes(block.children, context)).map((nodes) => ({
83
+ kind: "paragraph",
84
+ nodes
85
+ })) : [{
86
+ kind: "block",
87
+ node: block
88
+ }]);
89
+ }
90
+ function splitAtBlankLines(nodes) {
91
+ const paragraphs = [[]];
92
+ let breaks = [];
93
+ for (const node of nodes) {
94
+ const isBlankText = node.type === "text" && node.text.trim() === "";
95
+ if (node.type === "soft-break" || breaks.length > 0 && isBlankText) {
96
+ breaks.push(node);
97
+ continue;
98
+ }
99
+ const current = paragraphs[paragraphs.length - 1];
100
+ if (breaks.filter((n) => n.type === "soft-break").length >= 2) paragraphs.push([node]);
101
+ else current.push(...breaks, node);
102
+ breaks = [];
103
+ }
104
+ return paragraphs.filter((paragraph) => paragraph.length > 0);
105
+ }
106
+ /**
107
+ * A list is loose when a blank line separates two of its items or two
108
+ * paragraphs inside one item. The blank line that ends the list does not
109
+ * count, so a list followed by more prose still renders tight.
110
+ */
111
+ function isLooseList(items, parts, context) {
112
+ return parts.some((item) => item.filter((part) => part.kind === "paragraph").length > 1) || items.slice(0, -1).some((item, index) => {
113
+ const between = context.source.slice(item.range.start, items[index + 1].range.start);
114
+ return /\n[ \t]*\n/.test(between);
115
+ });
116
+ }
117
+ const TASK_MARKER = /^\[([ xX])\][ \t]+/;
118
+ /** Render inline nodes, turning a leading `[ ]` / `[x]` into a checkbox. */
119
+ function renderTaskAwareInline(nodes, context) {
120
+ const [first, ...rest] = nodes;
121
+ const task = first?.type === "text" ? TASK_MARKER.exec(first.text) : null;
122
+ if (!first || first.type !== "text" || !task) return nodes.map((node) => renderNode(node, context)).join("");
123
+ return `<input ${task[1] === " " ? "" : "checked=\"\" "}disabled="" type="checkbox"> ${escapeText(first.text.slice(task[0].length))}${rest.map((node) => renderNode(node, context)).join("")}`;
124
+ }
125
+ function renderList(node, context) {
126
+ const tag = node.ordered ? "ol" : "ul";
127
+ const start = node.ordered && node.start !== null && node.start !== 1 ? ` start="${node.start}"` : "";
128
+ const items = childNodes(node.children, context).filter((item) => item.type === "list-item");
129
+ const parts = items.map((item) => itemParts(item, context));
130
+ const loose = isLooseList(items, parts, context);
131
+ return `<${tag}${start}>\n${parts.map((itemContent) => {
132
+ return `<li>${itemContent.map((part, index) => {
133
+ if (part.kind === "block") return renderNode(part.node, context);
134
+ const inline = index === 0 ? renderTaskAwareInline(part.nodes, context) : part.nodes.map((n) => renderNode(n, context)).join("");
135
+ return loose ? `<p>${inline}</p>\n` : inline;
136
+ }).join("")}</li>\n`;
137
+ }).join("")}</${tag}>\n`;
138
+ }
139
+ function renderCodeBlock(node, context) {
140
+ const language = (node.info ?? "").replace(/[^a-z0-9-]/gi, "");
141
+ const languageClass = language ? ` class="language-${language}"` : "";
142
+ const payload = escapeAll(context.options.copyPayload(node.text));
143
+ return `<div class="announcement-code"><pre><code${languageClass}>${escapeAll(node.text)}</code></pre><div class="announcement-code__copy-shield"><button type="button" class="announcement-code__copy" data-copy="${payload}" aria-label="Copy code">Copy</button></div></div>`;
144
+ }
145
+ function renderTable(node, context) {
146
+ const rows = childNodes(node.children, context).filter((row) => row.type === "table-row");
147
+ const renderRow = (row) => {
148
+ const tag = row.isHeader ? "th" : "td";
149
+ return `<tr>\n${childNodes(row.children, context).map((cell, index) => {
150
+ const align = node.align[index];
151
+ return `<${tag}${align && align !== "none" ? ` align="${align}"` : ""}>${renderNode(cell, context)}</${tag}>\n`;
152
+ }).join("")}</tr>\n`;
153
+ };
154
+ const header = rows.filter((row) => row.isHeader).map(renderRow);
155
+ const body = rows.filter((row) => !row.isHeader).map(renderRow);
156
+ return `<table>\n<thead>\n${header.join("")}</thead>\n${body.length > 0 ? `<tbody>${body.join("")}</tbody>` : ""}</table>\n`;
157
+ }
158
+ function renderImage(node, context) {
159
+ const src = context.options.imageSrc(node.url);
160
+ if (src === null) return escapeText(node.alt);
161
+ const title = node.title ? ` title="${escapeAll(node.title)}"` : "";
162
+ return `<img src="${escapeAll(src)}" alt="${escapeAll(node.alt)}"${title}>`;
163
+ }
164
+ function renderLink(url, title, content, context) {
165
+ return `<a href="${escapeAll(context.options.linkHref(url))}" target="_blank" rel="noopener"${title ? ` title="${escapeAll(title)}"` : ""}>${content}</a>`;
166
+ }
167
+
168
+ //#endregion
169
+ export { renderMarkdownToHtml };
170
+ //# sourceMappingURL=markdown.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown.mjs","names":[],"sources":["../../src/lib/markdown.ts"],"sourcesContent":["import {\n createStreamingMarkdownParserState,\n finalizeStreamingMarkdown,\n parseStreamingMarkdownChunk,\n} from \"@copilotkit/markdown-renderer\";\nimport type { StreamingMarkdownAstNode } from \"@copilotkit/markdown-renderer\";\n\n/** Inspector policy the HTML renderer applies to URLs and code blocks. */\nexport type MarkdownHtmlOptions = Readonly<{\n /** The href to emit for a link or autolink. Return \"#\" to neuter it. */\n linkHref: (url: string) => string;\n /** The src to emit for an image, or null to render its alt text instead. */\n imageSrc: (url: string) => string | null;\n /** The `data-copy` payload for a code block's Copy button. */\n copyPayload: (code: string) => string;\n}>;\n\ntype MarkdownNode = StreamingMarkdownAstNode;\ntype NodeOfType<T extends MarkdownNode[\"type\"]> = Extract<\n MarkdownNode,\n { type: T }\n>;\n\ntype RenderContext = Readonly<{\n nodes: ReadonlyMap<number, MarkdownNode>;\n /** Normalized Markdown source; node ranges index into it. */\n source: string;\n options: MarkdownHtmlOptions;\n}>;\n\nconst ESCAPED: Readonly<Record<string, string>> = {\n \"&\": \"&amp;\",\n \"<\": \"&lt;\",\n \">\": \"&gt;\",\n '\"': \"&quot;\",\n \"'\": \"&#39;\",\n};\n\n/** Escape every markup-significant character (attributes, code). */\nfunction escapeAll(value: string): string {\n return value.replace(/[&<>\"']/g, (char) => ESCAPED[char]!);\n}\n\n/**\n * Escape prose text but leave existing character references such as `&amp;`\n * or `&copy;` alone, as Markdown (and the previous marked renderer) does. A\n * character reference cannot open a tag, so this does not weaken escaping.\n */\nfunction escapeText(value: string): string {\n return value.replace(\n /[<>\"']|&(?!#\\d{1,7};|#[Xx][a-fA-F0-9]{1,6};|\\w+;)/g,\n (char) => ESCAPED[char]!,\n );\n}\n\n/**\n * Render a complete Markdown document to an HTML string for the What's new\n * article. The output is meant for `unsafeHTML`, so every piece of source text\n * is escaped: raw HTML in the Markdown renders as visible text, and URLs only\n * reach attributes through the caller's `linkHref` / `imageSrc` policy.\n */\nexport function renderMarkdownToHtml(\n markdown: string,\n options: MarkdownHtmlOptions,\n): string {\n const state = finalizeStreamingMarkdown(\n parseStreamingMarkdownChunk(\n createStreamingMarkdownParserState({ segmenter: false }),\n markdown,\n ),\n );\n const nodes = new Map(state.nodes.map((node) => [node.id, node]));\n const root = state.rootId === null ? undefined : nodes.get(state.rootId);\n if (!root || root.type !== \"document\") return \"\";\n return renderChildren(root.children, {\n nodes,\n source: state.source,\n options,\n });\n}\n\nfunction childNodes(ids: readonly number[], context: RenderContext) {\n return ids.flatMap((id) => {\n const node = context.nodes.get(id);\n return node ? [node] : [];\n });\n}\n\nfunction renderChildren(\n ids: readonly number[],\n context: RenderContext,\n): string {\n return childNodes(ids, context)\n .map((node) => renderNode(node, context))\n .join(\"\");\n}\n\nfunction renderNode(node: MarkdownNode, context: RenderContext): string {\n switch (node.type) {\n case \"document\":\n case \"table-row\":\n case \"table-cell\":\n return renderChildren(node.children, context);\n case \"paragraph\":\n return `<p>${renderChildren(node.children, context)}</p>\\n`;\n case \"heading\":\n return `<h${node.level}>${renderChildren(node.children, context)}</h${node.level}>\\n`;\n case \"blockquote\":\n return `<blockquote>\\n${renderChildren(node.children, context)}</blockquote>\\n`;\n case \"list\":\n return renderList(node, context);\n case \"list-item\":\n return `<li>${renderChildren(node.children, context)}</li>\\n`;\n case \"code-block\":\n return renderCodeBlock(node, context);\n case \"table\":\n return renderTable(node, context);\n case \"thematic-break\":\n return \"<hr>\\n\";\n case \"text\":\n return escapeText(node.text);\n case \"em\":\n return `<em>${renderChildren(node.children, context)}</em>`;\n case \"strong\":\n return `<strong>${renderChildren(node.children, context)}</strong>`;\n case \"strikethrough\":\n return `<del>${renderChildren(node.children, context)}</del>`;\n case \"inline-code\":\n return `<code>${escapeAll(node.text)}</code>`;\n case \"soft-break\":\n return \"\\n\";\n case \"hard-break\":\n return \"<br>\";\n case \"link\":\n return renderLink(\n node.url,\n node.title,\n renderChildren(node.children, context),\n context,\n );\n case \"autolink\":\n return renderLink(node.url, undefined, escapeText(node.text), context);\n case \"image\":\n return renderImage(node, context);\n case \"citation\":\n // The article has no footnotes; show the reference as it was typed.\n return escapeText(`[^${node.idRef}]`);\n default: {\n const unhandled: never = node;\n return unhandled;\n }\n }\n}\n\n/** A piece of list item content: one authored paragraph, or another block. */\ntype ItemPart =\n | { kind: \"paragraph\"; nodes: MarkdownNode[] }\n | { kind: \"block\"; node: MarkdownNode };\n\n/**\n * The parser keeps each list item as a single paragraph, so a blank line\n * inside an item survives only as two soft breaks in a row. Split there to\n * recover the paragraphs the author wrote.\n */\nfunction itemParts(\n item: NodeOfType<\"list-item\">,\n context: RenderContext,\n): ItemPart[] {\n return childNodes(item.children, context).flatMap((block): ItemPart[] =>\n block.type === \"paragraph\"\n ? splitAtBlankLines(childNodes(block.children, context)).map((nodes) => ({\n kind: \"paragraph\",\n nodes,\n }))\n : [{ kind: \"block\", node: block }],\n );\n}\n\nfunction splitAtBlankLines(nodes: readonly MarkdownNode[]): MarkdownNode[][] {\n const paragraphs: MarkdownNode[][] = [[]];\n let breaks: MarkdownNode[] = [];\n for (const node of nodes) {\n const isBlankText = node.type === \"text\" && node.text.trim() === \"\";\n if (node.type === \"soft-break\" || (breaks.length > 0 && isBlankText)) {\n breaks.push(node);\n continue;\n }\n const current = paragraphs[paragraphs.length - 1]!;\n if (breaks.filter((n) => n.type === \"soft-break\").length >= 2) {\n paragraphs.push([node]);\n } else {\n current.push(...breaks, node);\n }\n breaks = [];\n }\n return paragraphs.filter((paragraph) => paragraph.length > 0);\n}\n\n/**\n * A list is loose when a blank line separates two of its items or two\n * paragraphs inside one item. The blank line that ends the list does not\n * count, so a list followed by more prose still renders tight.\n */\nfunction isLooseList(\n items: readonly NodeOfType<\"list-item\">[],\n parts: readonly ItemPart[][],\n context: RenderContext,\n): boolean {\n const hasSplitItem = parts.some(\n (item) => item.filter((part) => part.kind === \"paragraph\").length > 1,\n );\n return (\n hasSplitItem ||\n items.slice(0, -1).some((item, index) => {\n const between = context.source.slice(\n item.range.start,\n items[index + 1]!.range.start,\n );\n return /\\n[ \\t]*\\n/.test(between);\n })\n );\n}\n\nconst TASK_MARKER = /^\\[([ xX])\\][ \\t]+/;\n\n/** Render inline nodes, turning a leading `[ ]` / `[x]` into a checkbox. */\nfunction renderTaskAwareInline(\n nodes: readonly MarkdownNode[],\n context: RenderContext,\n): string {\n const [first, ...rest] = nodes;\n const task = first?.type === \"text\" ? TASK_MARKER.exec(first.text) : null;\n if (!first || first.type !== \"text\" || !task) {\n return nodes.map((node) => renderNode(node, context)).join(\"\");\n }\n const checked = task[1] === \" \" ? \"\" : 'checked=\"\" ';\n const label = escapeText(first.text.slice(task[0].length));\n const tail = rest.map((node) => renderNode(node, context)).join(\"\");\n return `<input ${checked}disabled=\"\" type=\"checkbox\"> ${label}${tail}`;\n}\n\nfunction renderList(node: NodeOfType<\"list\">, context: RenderContext): string {\n const tag = node.ordered ? \"ol\" : \"ul\";\n const start =\n node.ordered && node.start !== null && node.start !== 1\n ? ` start=\"${node.start}\"`\n : \"\";\n const items = childNodes(node.children, context).filter(\n (item): item is NodeOfType<\"list-item\"> => item.type === \"list-item\",\n );\n const parts = items.map((item) => itemParts(item, context));\n const loose = isLooseList(items, parts, context);\n const body = parts\n .map((itemContent) => {\n const content = itemContent\n .map((part, index) => {\n if (part.kind === \"block\") return renderNode(part.node, context);\n const inline =\n index === 0\n ? renderTaskAwareInline(part.nodes, context)\n : part.nodes.map((n) => renderNode(n, context)).join(\"\");\n return loose ? `<p>${inline}</p>\\n` : inline;\n })\n .join(\"\");\n return `<li>${content}</li>\\n`;\n })\n .join(\"\");\n return `<${tag}${start}>\\n${body}</${tag}>\\n`;\n}\n\nfunction renderCodeBlock(\n node: NodeOfType<\"code-block\">,\n context: RenderContext,\n): string {\n const language = (node.info ?? \"\").replace(/[^a-z0-9-]/gi, \"\");\n const languageClass = language ? ` class=\"language-${language}\"` : \"\";\n const payload = escapeAll(context.options.copyPayload(node.text));\n return `<div class=\"announcement-code\"><pre><code${languageClass}>${escapeAll(node.text)}</code></pre><div class=\"announcement-code__copy-shield\"><button type=\"button\" class=\"announcement-code__copy\" data-copy=\"${payload}\" aria-label=\"Copy code\">Copy</button></div></div>`;\n}\n\nfunction renderTable(\n node: NodeOfType<\"table\">,\n context: RenderContext,\n): string {\n const rows = childNodes(node.children, context).filter(\n (row): row is NodeOfType<\"table-row\"> => row.type === \"table-row\",\n );\n const renderRow = (row: NodeOfType<\"table-row\">) => {\n const tag = row.isHeader ? \"th\" : \"td\";\n const cells = childNodes(row.children, context)\n .map((cell, index) => {\n const align = node.align[index];\n const alignAttr = align && align !== \"none\" ? ` align=\"${align}\"` : \"\";\n return `<${tag}${alignAttr}>${renderNode(cell, context)}</${tag}>\\n`;\n })\n .join(\"\");\n return `<tr>\\n${cells}</tr>\\n`;\n };\n const header = rows.filter((row) => row.isHeader).map(renderRow);\n const body = rows.filter((row) => !row.isHeader).map(renderRow);\n return `<table>\\n<thead>\\n${header.join(\"\")}</thead>\\n${\n body.length > 0 ? `<tbody>${body.join(\"\")}</tbody>` : \"\"\n }</table>\\n`;\n}\n\nfunction renderImage(\n node: NodeOfType<\"image\">,\n context: RenderContext,\n): string {\n const src = context.options.imageSrc(node.url);\n if (src === null) return escapeText(node.alt);\n const title = node.title ? ` title=\"${escapeAll(node.title)}\"` : \"\";\n return `<img src=\"${escapeAll(src)}\" alt=\"${escapeAll(node.alt)}\"${title}>`;\n}\n\nfunction renderLink(\n url: string,\n title: string | undefined,\n content: string,\n context: RenderContext,\n): string {\n const href = escapeAll(context.options.linkHref(url));\n const titleAttr = title ? ` title=\"${escapeAll(title)}\"` : \"\";\n return `<a href=\"${href}\" target=\"_blank\" rel=\"noopener\"${titleAttr}>${content}</a>`;\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- let semver = require("semver");
1
+ const require_semver = require('./semver.cjs');
2
2
 
3
3
  //#region src/lib/notifications.ts
4
4
  function emptyNotificationState() {
@@ -46,7 +46,7 @@ function validNotificationConditions(value) {
46
46
  if (value.sdkVersion !== void 0 && value.framework === void 0) return false;
47
47
  return Object.entries(value).every(([key, expected]) => {
48
48
  if (!text(expected)) return false;
49
- if (key === "sdkVersion") return (0, semver.validRange)(expected) !== null;
49
+ if (key === "sdkVersion") return require_semver.isValidRange(expected);
50
50
  if (key === "plan") return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(expected);
51
51
  return Object.prototype.hasOwnProperty.call(conditionValues, key) && (conditionValues[key]?.includes(expected) ?? false);
52
52
  });
@@ -144,7 +144,7 @@ function matchNotification(notice, context) {
144
144
  matches: false,
145
145
  reasons: ["Development only"]
146
146
  };
147
- if (!context.sdkVersion || !(0, semver.valid)(context.sdkVersion) || (0, semver.prerelease)(context.sdkVersion) !== null) return {
147
+ if (!context.sdkVersion || !require_semver.isStableVersion(context.sdkVersion)) return {
148
148
  matches: false,
149
149
  reasons: ["Known stable SDK required"]
150
150
  };
@@ -152,7 +152,7 @@ function matchNotification(notice, context) {
152
152
  const matches = notice.audiences.some((conditions) => {
153
153
  return Object.entries(conditions).every(([field, expected]) => {
154
154
  const actual = context[field];
155
- const matches = field === "sdkVersion" ? actual !== void 0 && (0, semver.valid)(actual) !== null && (0, semver.prerelease)(actual) === null && (0, semver.satisfies)(actual, expected) : actual === expected;
155
+ const matches = field === "sdkVersion" ? actual !== void 0 && require_semver.isStableVersion(actual) && require_semver.satisfies(actual, expected) : actual === expected;
156
156
  if (!matches) reasons.push(`${field} ${actual === void 0 ? "is unknown" : `requires ${expected} (got ${actual})`}`);
157
157
  return matches;
158
158
  });
@@ -1 +1 @@
1
- {"version":3,"file":"notifications.cjs","names":[],"sources":["../../src/lib/notifications.ts"],"sourcesContent":["import { satisfies, valid, validRange, prerelease } from \"semver\";\n\n/** Versioned CDN notification contract. Keep the Intelligence copy and fixtures aligned. */\nexport type NotificationFramework = \"react\" | \"vue\" | \"angular\";\nexport type NotificationPriority = \"Low\" | \"Normal\" | \"High\" | \"Urgent\";\nexport interface NotificationConditions {\n framework?: NotificationFramework;\n sdkVersion?: string;\n intelligence?: \"enabled\" | \"disabled\";\n deployment?: \"managed\" | \"self-hosted\";\n plan?: string;\n}\nexport interface CohortNotification {\n id: string;\n title: string;\n body: string;\n publishedAt: string;\n audiences: NotificationConditions[];\n priority: NotificationPriority;\n priorityOverride?: number;\n}\nexport interface NotificationFeed {\n schemaVersion: 1;\n notifications: CohortNotification[];\n}\nexport interface NotificationContext extends Omit<\n NotificationConditions,\n \"sdkVersion\"\n> {\n development: boolean;\n sdkVersion?: string;\n}\nexport interface NotificationState {\n schemaVersion: 1;\n eligibleIds: string[];\n readIds: string[];\n suppressedIds: string[];\n activeId: string | null;\n}\nexport function emptyNotificationState(): NotificationState {\n return {\n schemaVersion: 1,\n eligibleIds: [],\n readIds: [],\n suppressedIds: [],\n activeId: null,\n };\n}\nconst priorities: Record<NotificationPriority, number> = {\n Low: 10,\n Normal: 20,\n High: 30,\n Urgent: 40,\n};\nconst conditionValues: Record<string, readonly string[]> = {\n framework: [\"react\", \"vue\", \"angular\"],\n intelligence: [\"enabled\", \"disabled\"],\n deployment: [\"managed\", \"self-hosted\"],\n};\nfunction record(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\nfunction onlyKeys(value: Record<string, unknown>, keys: string[]): boolean {\n return Object.keys(value).every((key) => keys.includes(key));\n}\nfunction text(value: unknown, max = 256): value is string {\n return (\n typeof value === \"string\" && value.trim().length > 0 && value.length <= max\n );\n}\nexport function isNotificationId(value: unknown): value is string {\n return (\n typeof value === \"string\" &&\n /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(\n value,\n )\n );\n}\nfunction ids(value: unknown): value is string[] {\n return (\n Array.isArray(value) &&\n value.every(isNotificationId) &&\n new Set(value).size === value.length\n );\n}\n/** Validate authored conditions; unsupported predicates cannot broaden the audience. */\nexport function validNotificationConditions(\n value: unknown,\n): value is NotificationConditions {\n if (!record(value)) return false;\n if (value.sdkVersion !== undefined && value.framework === undefined)\n return false;\n return Object.entries(value).every(([key, expected]) => {\n if (!text(expected)) return false;\n if (key === \"sdkVersion\") return validRange(expected) !== null;\n if (key === \"plan\") return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(expected);\n return (\n Object.prototype.hasOwnProperty.call(conditionValues, key) &&\n (conditionValues[key]?.includes(expected) ?? false)\n );\n });\n}\n/** Parse the public envelope. Any invalid entry rejects the whole feed. */\nexport function parseNotificationFeed(value: unknown): NotificationFeed | null {\n if (\n !record(value) ||\n !onlyKeys(value, [\"schemaVersion\", \"notifications\"]) ||\n value.schemaVersion !== 1\n )\n return null;\n if (!Array.isArray(value.notifications) || value.notifications.length > 100)\n return null;\n const notifications: CohortNotification[] = [];\n for (const notice of value.notifications) {\n if (\n !record(notice) ||\n !onlyKeys(notice, [\n \"id\",\n \"title\",\n \"body\",\n \"publishedAt\",\n \"audiences\",\n \"priority\",\n \"priorityOverride\",\n ])\n )\n return null;\n if (\n !isNotificationId(notice.id) ||\n !text(notice.title) ||\n !text(notice.body, 100_000) ||\n !text(notice.publishedAt)\n )\n return null;\n if (\n !/^\\d{4}-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d\\.\\d{3}Z$/.test(notice.publishedAt) ||\n !Number.isFinite(Date.parse(notice.publishedAt)) ||\n new Date(notice.publishedAt).toISOString() !== notice.publishedAt\n )\n return null;\n if (\n !Array.isArray(notice.audiences) ||\n !notice.audiences.length ||\n notice.audiences.length > 100 ||\n !notice.audiences.every(validNotificationConditions)\n )\n return null;\n if (\n !text(notice.priority) ||\n !Object.prototype.hasOwnProperty.call(priorities, notice.priority)\n )\n return null;\n if (\n notice.priorityOverride !== undefined &&\n (typeof notice.priorityOverride !== \"number\" ||\n !Number.isSafeInteger(notice.priorityOverride) ||\n notice.priorityOverride < 0)\n )\n return null;\n if (notifications.some((n) => n.id === notice.id)) return null;\n notifications.push({\n id: notice.id,\n title: notice.title,\n body: notice.body,\n publishedAt: notice.publishedAt,\n audiences: notice.audiences,\n priority: notice.priority as NotificationPriority,\n ...(notice.priorityOverride === undefined\n ? {}\n : { priorityOverride: notice.priorityOverride }),\n });\n }\n return { schemaVersion: 1, notifications };\n}\n/** Ignore malformed or incompatible browser state. */\nexport function parseNotificationState(\n value: unknown,\n): NotificationState | null {\n if (\n !record(value) ||\n value.schemaVersion !== 1 ||\n !ids(value.eligibleIds) ||\n !ids(value.readIds) ||\n !ids(value.suppressedIds) ||\n (value.activeId !== null && !isNotificationId(value.activeId))\n )\n return null;\n return {\n schemaVersion: 1,\n eligibleIds: value.eligibleIds,\n readIds: value.readIds,\n suppressedIds: value.suppressedIds,\n activeId: value.activeId,\n };\n}\n/** Internal priority never becomes a developer-facing badge. */\nexport function notificationPriority(notice: CohortNotification): number {\n return notice.priorityOverride ?? priorities[notice.priority];\n}\n/** Stable ordering used by authoring reports and first-time selection. */\nexport function compareNotifications(\n a: CohortNotification,\n b: CohortNotification,\n): number {\n return (\n notificationPriority(b) - notificationPriority(a) ||\n Date.parse(b.publishedAt) - Date.parse(a.publishedAt) ||\n (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)\n );\n}\n/** Refresh eligibility without promoting the previously delivered backlog. */\nexport function reconcileNotifications(\n state: NotificationState,\n feed: NotificationFeed,\n context: NotificationContext,\n): NotificationState {\n const eligible = feed.notifications.filter(\n (n) => matchNotification(n, context).matches,\n );\n const quiet = (n: CohortNotification) =>\n state.readIds.includes(n.id) || state.suppressedIds.includes(n.id);\n const active =\n eligible.find((n) => n.id === state.activeId && !quiet(n)) ??\n feed.notifications.find(\n (n) =>\n n.id === state.activeId &&\n !quiet(n) &&\n n.audiences.some((conditions) => {\n const entries = Object.entries(conditions);\n if (\n !entries.some(\n ([key]) =>\n context[key as keyof NotificationConditions] === undefined,\n )\n )\n return false;\n const known = Object.fromEntries(\n entries.filter(\n ([key]) =>\n context[key as keyof NotificationConditions] !== undefined,\n ),\n );\n return matchNotification({ ...n, audiences: [known] }, context)\n .matches;\n }),\n );\n const newcomer = eligible\n .filter((n) => !quiet(n) && !state.eligibleIds.includes(n.id))\n .sort(compareNotifications)[0];\n const winner =\n active &&\n (!newcomer ||\n notificationPriority(active) >= notificationPriority(newcomer))\n ? active\n : newcomer;\n return {\n ...state,\n eligibleIds: eligible.map((n) => n.id),\n activeId: winner?.id ?? null,\n };\n}\n/** Reading or closing the highlight quiets its delivered backlog, not future IDs. */\nexport function acknowledgeNotification(\n state: NotificationState,\n noticeId: string,\n): NotificationState {\n if (!state.eligibleIds.includes(noticeId)) return state;\n const highlighted = state.activeId === noticeId;\n return {\n ...state,\n readIds: [...new Set([...state.readIds, noticeId])],\n suppressedIds: [\n ...new Set([\n ...state.suppressedIds,\n ...(highlighted ? state.eligibleIds : [noticeId]),\n ]),\n ],\n activeId: highlighted ? null : state.activeId,\n };\n}\n/** Explain targeting using the same rules in CI and the Inspector. */\nexport function matchNotification(\n notice: CohortNotification,\n context: NotificationContext,\n): { matches: boolean; reasons: string[] } {\n if (!context.development)\n return { matches: false, reasons: [\"Development only\"] };\n if (\n !context.sdkVersion ||\n !valid(context.sdkVersion) ||\n prerelease(context.sdkVersion) !== null\n )\n return {\n matches: false,\n reasons: [\"Known stable SDK required\"],\n };\n const reasons: string[] = [];\n const matches = notice.audiences.some((conditions) => {\n return Object.entries(conditions).every(([field, expected]) => {\n const actual = context[field as keyof NotificationConditions];\n const matches =\n field === \"sdkVersion\"\n ? actual !== undefined &&\n valid(actual) !== null &&\n prerelease(actual) === null &&\n satisfies(actual, expected)\n : actual === expected;\n if (!matches)\n reasons.push(\n `${field} ${actual === undefined ? \"is unknown\" : `requires ${expected} (got ${actual})`}`,\n );\n return matches;\n });\n });\n return {\n matches,\n reasons: matches ? [] : reasons,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"notifications.cjs","names":["isValidRange","isStableVersion","satisfies"],"sources":["../../src/lib/notifications.ts"],"sourcesContent":["import { isStableVersion, isValidRange, satisfies } from \"./semver.js\";\n\n/** Versioned CDN notification contract. Keep the Intelligence copy and fixtures aligned. */\nexport type NotificationFramework = \"react\" | \"vue\" | \"angular\";\nexport type NotificationPriority = \"Low\" | \"Normal\" | \"High\" | \"Urgent\";\nexport interface NotificationConditions {\n framework?: NotificationFramework;\n sdkVersion?: string;\n intelligence?: \"enabled\" | \"disabled\";\n deployment?: \"managed\" | \"self-hosted\";\n plan?: string;\n}\nexport interface CohortNotification {\n id: string;\n title: string;\n body: string;\n publishedAt: string;\n audiences: NotificationConditions[];\n priority: NotificationPriority;\n priorityOverride?: number;\n}\nexport interface NotificationFeed {\n schemaVersion: 1;\n notifications: CohortNotification[];\n}\nexport interface NotificationContext extends Omit<\n NotificationConditions,\n \"sdkVersion\"\n> {\n development: boolean;\n sdkVersion?: string;\n}\nexport interface NotificationState {\n schemaVersion: 1;\n eligibleIds: string[];\n readIds: string[];\n suppressedIds: string[];\n activeId: string | null;\n}\nexport function emptyNotificationState(): NotificationState {\n return {\n schemaVersion: 1,\n eligibleIds: [],\n readIds: [],\n suppressedIds: [],\n activeId: null,\n };\n}\nconst priorities: Record<NotificationPriority, number> = {\n Low: 10,\n Normal: 20,\n High: 30,\n Urgent: 40,\n};\nconst conditionValues: Record<string, readonly string[]> = {\n framework: [\"react\", \"vue\", \"angular\"],\n intelligence: [\"enabled\", \"disabled\"],\n deployment: [\"managed\", \"self-hosted\"],\n};\nfunction record(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\nfunction onlyKeys(value: Record<string, unknown>, keys: string[]): boolean {\n return Object.keys(value).every((key) => keys.includes(key));\n}\nfunction text(value: unknown, max = 256): value is string {\n return (\n typeof value === \"string\" && value.trim().length > 0 && value.length <= max\n );\n}\nexport function isNotificationId(value: unknown): value is string {\n return (\n typeof value === \"string\" &&\n /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(\n value,\n )\n );\n}\nfunction ids(value: unknown): value is string[] {\n return (\n Array.isArray(value) &&\n value.every(isNotificationId) &&\n new Set(value).size === value.length\n );\n}\n/** Validate authored conditions; unsupported predicates cannot broaden the audience. */\nexport function validNotificationConditions(\n value: unknown,\n): value is NotificationConditions {\n if (!record(value)) return false;\n if (value.sdkVersion !== undefined && value.framework === undefined)\n return false;\n return Object.entries(value).every(([key, expected]) => {\n if (!text(expected)) return false;\n if (key === \"sdkVersion\") return isValidRange(expected);\n if (key === \"plan\") return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(expected);\n return (\n Object.prototype.hasOwnProperty.call(conditionValues, key) &&\n (conditionValues[key]?.includes(expected) ?? false)\n );\n });\n}\n/** Parse the public envelope. Any invalid entry rejects the whole feed. */\nexport function parseNotificationFeed(value: unknown): NotificationFeed | null {\n if (\n !record(value) ||\n !onlyKeys(value, [\"schemaVersion\", \"notifications\"]) ||\n value.schemaVersion !== 1\n )\n return null;\n if (!Array.isArray(value.notifications) || value.notifications.length > 100)\n return null;\n const notifications: CohortNotification[] = [];\n for (const notice of value.notifications) {\n if (\n !record(notice) ||\n !onlyKeys(notice, [\n \"id\",\n \"title\",\n \"body\",\n \"publishedAt\",\n \"audiences\",\n \"priority\",\n \"priorityOverride\",\n ])\n )\n return null;\n if (\n !isNotificationId(notice.id) ||\n !text(notice.title) ||\n !text(notice.body, 100_000) ||\n !text(notice.publishedAt)\n )\n return null;\n if (\n !/^\\d{4}-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d\\.\\d{3}Z$/.test(notice.publishedAt) ||\n !Number.isFinite(Date.parse(notice.publishedAt)) ||\n new Date(notice.publishedAt).toISOString() !== notice.publishedAt\n )\n return null;\n if (\n !Array.isArray(notice.audiences) ||\n !notice.audiences.length ||\n notice.audiences.length > 100 ||\n !notice.audiences.every(validNotificationConditions)\n )\n return null;\n if (\n !text(notice.priority) ||\n !Object.prototype.hasOwnProperty.call(priorities, notice.priority)\n )\n return null;\n if (\n notice.priorityOverride !== undefined &&\n (typeof notice.priorityOverride !== \"number\" ||\n !Number.isSafeInteger(notice.priorityOverride) ||\n notice.priorityOverride < 0)\n )\n return null;\n if (notifications.some((n) => n.id === notice.id)) return null;\n notifications.push({\n id: notice.id,\n title: notice.title,\n body: notice.body,\n publishedAt: notice.publishedAt,\n audiences: notice.audiences,\n priority: notice.priority as NotificationPriority,\n ...(notice.priorityOverride === undefined\n ? {}\n : { priorityOverride: notice.priorityOverride }),\n });\n }\n return { schemaVersion: 1, notifications };\n}\n/** Ignore malformed or incompatible browser state. */\nexport function parseNotificationState(\n value: unknown,\n): NotificationState | null {\n if (\n !record(value) ||\n value.schemaVersion !== 1 ||\n !ids(value.eligibleIds) ||\n !ids(value.readIds) ||\n !ids(value.suppressedIds) ||\n (value.activeId !== null && !isNotificationId(value.activeId))\n )\n return null;\n return {\n schemaVersion: 1,\n eligibleIds: value.eligibleIds,\n readIds: value.readIds,\n suppressedIds: value.suppressedIds,\n activeId: value.activeId,\n };\n}\n/** Internal priority never becomes a developer-facing badge. */\nexport function notificationPriority(notice: CohortNotification): number {\n return notice.priorityOverride ?? priorities[notice.priority];\n}\n/** Stable ordering used by authoring reports and first-time selection. */\nexport function compareNotifications(\n a: CohortNotification,\n b: CohortNotification,\n): number {\n return (\n notificationPriority(b) - notificationPriority(a) ||\n Date.parse(b.publishedAt) - Date.parse(a.publishedAt) ||\n (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)\n );\n}\n/** Refresh eligibility without promoting the previously delivered backlog. */\nexport function reconcileNotifications(\n state: NotificationState,\n feed: NotificationFeed,\n context: NotificationContext,\n): NotificationState {\n const eligible = feed.notifications.filter(\n (n) => matchNotification(n, context).matches,\n );\n const quiet = (n: CohortNotification) =>\n state.readIds.includes(n.id) || state.suppressedIds.includes(n.id);\n const active =\n eligible.find((n) => n.id === state.activeId && !quiet(n)) ??\n feed.notifications.find(\n (n) =>\n n.id === state.activeId &&\n !quiet(n) &&\n n.audiences.some((conditions) => {\n const entries = Object.entries(conditions);\n if (\n !entries.some(\n ([key]) =>\n context[key as keyof NotificationConditions] === undefined,\n )\n )\n return false;\n const known = Object.fromEntries(\n entries.filter(\n ([key]) =>\n context[key as keyof NotificationConditions] !== undefined,\n ),\n );\n return matchNotification({ ...n, audiences: [known] }, context)\n .matches;\n }),\n );\n const newcomer = eligible\n .filter((n) => !quiet(n) && !state.eligibleIds.includes(n.id))\n .sort(compareNotifications)[0];\n const winner =\n active &&\n (!newcomer ||\n notificationPriority(active) >= notificationPriority(newcomer))\n ? active\n : newcomer;\n return {\n ...state,\n eligibleIds: eligible.map((n) => n.id),\n activeId: winner?.id ?? null,\n };\n}\n/** Reading or closing the highlight quiets its delivered backlog, not future IDs. */\nexport function acknowledgeNotification(\n state: NotificationState,\n noticeId: string,\n): NotificationState {\n if (!state.eligibleIds.includes(noticeId)) return state;\n const highlighted = state.activeId === noticeId;\n return {\n ...state,\n readIds: [...new Set([...state.readIds, noticeId])],\n suppressedIds: [\n ...new Set([\n ...state.suppressedIds,\n ...(highlighted ? state.eligibleIds : [noticeId]),\n ]),\n ],\n activeId: highlighted ? null : state.activeId,\n };\n}\n/** Explain targeting using the same rules in CI and the Inspector. */\nexport function matchNotification(\n notice: CohortNotification,\n context: NotificationContext,\n): { matches: boolean; reasons: string[] } {\n if (!context.development)\n return { matches: false, reasons: [\"Development only\"] };\n if (!context.sdkVersion || !isStableVersion(context.sdkVersion))\n return {\n matches: false,\n reasons: [\"Known stable SDK required\"],\n };\n const reasons: string[] = [];\n const matches = notice.audiences.some((conditions) => {\n return Object.entries(conditions).every(([field, expected]) => {\n const actual = context[field as keyof NotificationConditions];\n const matches =\n field === \"sdkVersion\"\n ? actual !== undefined &&\n isStableVersion(actual) &&\n satisfies(actual, expected)\n : actual === expected;\n if (!matches)\n reasons.push(\n `${field} ${actual === undefined ? \"is unknown\" : `requires ${expected} (got ${actual})`}`,\n );\n return matches;\n });\n });\n return {\n matches,\n reasons: matches ? [] : reasons,\n };\n}\n"],"mappings":";;;AAuCA,SAAgB,yBAA4C;AAC1D,QAAO;EACL,eAAe;EACf,aAAa,EAAE;EACf,SAAS,EAAE;EACX,eAAe,EAAE;EACjB,UAAU;EACX;;AAEH,MAAM,aAAmD;CACvD,KAAK;CACL,QAAQ;CACR,MAAM;CACN,QAAQ;CACT;AACD,MAAM,kBAAqD;CACzD,WAAW;EAAC;EAAS;EAAO;EAAU;CACtC,cAAc,CAAC,WAAW,WAAW;CACrC,YAAY,CAAC,WAAW,cAAc;CACvC;AACD,SAAS,OAAO,OAAkD;AAChE,QAAO,UAAU,QAAQ,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,MAAM;;AAE7E,SAAS,SAAS,OAAgC,MAAyB;AACzE,QAAO,OAAO,KAAK,MAAM,CAAC,OAAO,QAAQ,KAAK,SAAS,IAAI,CAAC;;AAE9D,SAAS,KAAK,OAAgB,MAAM,KAAsB;AACxD,QACE,OAAO,UAAU,YAAY,MAAM,MAAM,CAAC,SAAS,KAAK,MAAM,UAAU;;AAG5E,SAAgB,iBAAiB,OAAiC;AAChE,QACE,OAAO,UAAU,YACjB,6EAA6E,KAC3E,MACD;;AAGL,SAAS,IAAI,OAAmC;AAC9C,QACE,MAAM,QAAQ,MAAM,IACpB,MAAM,MAAM,iBAAiB,IAC7B,IAAI,IAAI,MAAM,CAAC,SAAS,MAAM;;;AAIlC,SAAgB,4BACd,OACiC;AACjC,KAAI,CAAC,OAAO,MAAM,CAAE,QAAO;AAC3B,KAAI,MAAM,eAAe,UAAa,MAAM,cAAc,OACxD,QAAO;AACT,QAAO,OAAO,QAAQ,MAAM,CAAC,OAAO,CAAC,KAAK,cAAc;AACtD,MAAI,CAAC,KAAK,SAAS,CAAE,QAAO;AAC5B,MAAI,QAAQ,aAAc,QAAOA,4BAAa,SAAS;AACvD,MAAI,QAAQ,OAAQ,QAAO,6BAA6B,KAAK,SAAS;AACtE,SACE,OAAO,UAAU,eAAe,KAAK,iBAAiB,IAAI,KACzD,gBAAgB,MAAM,SAAS,SAAS,IAAI;GAE/C;;;AAGJ,SAAgB,sBAAsB,OAAyC;AAC7E,KACE,CAAC,OAAO,MAAM,IACd,CAAC,SAAS,OAAO,CAAC,iBAAiB,gBAAgB,CAAC,IACpD,MAAM,kBAAkB,EAExB,QAAO;AACT,KAAI,CAAC,MAAM,QAAQ,MAAM,cAAc,IAAI,MAAM,cAAc,SAAS,IACtE,QAAO;CACT,MAAM,gBAAsC,EAAE;AAC9C,MAAK,MAAM,UAAU,MAAM,eAAe;AACxC,MACE,CAAC,OAAO,OAAO,IACf,CAAC,SAAS,QAAQ;GAChB;GACA;GACA;GACA;GACA;GACA;GACA;GACD,CAAC,CAEF,QAAO;AACT,MACE,CAAC,iBAAiB,OAAO,GAAG,IAC5B,CAAC,KAAK,OAAO,MAAM,IACnB,CAAC,KAAK,OAAO,MAAM,IAAQ,IAC3B,CAAC,KAAK,OAAO,YAAY,CAEzB,QAAO;AACT,MACE,CAAC,2CAA2C,KAAK,OAAO,YAAY,IACpE,CAAC,OAAO,SAAS,KAAK,MAAM,OAAO,YAAY,CAAC,IAChD,IAAI,KAAK,OAAO,YAAY,CAAC,aAAa,KAAK,OAAO,YAEtD,QAAO;AACT,MACE,CAAC,MAAM,QAAQ,OAAO,UAAU,IAChC,CAAC,OAAO,UAAU,UAClB,OAAO,UAAU,SAAS,OAC1B,CAAC,OAAO,UAAU,MAAM,4BAA4B,CAEpD,QAAO;AACT,MACE,CAAC,KAAK,OAAO,SAAS,IACtB,CAAC,OAAO,UAAU,eAAe,KAAK,YAAY,OAAO,SAAS,CAElE,QAAO;AACT,MACE,OAAO,qBAAqB,WAC3B,OAAO,OAAO,qBAAqB,YAClC,CAAC,OAAO,cAAc,OAAO,iBAAiB,IAC9C,OAAO,mBAAmB,GAE5B,QAAO;AACT,MAAI,cAAc,MAAM,MAAM,EAAE,OAAO,OAAO,GAAG,CAAE,QAAO;AAC1D,gBAAc,KAAK;GACjB,IAAI,OAAO;GACX,OAAO,OAAO;GACd,MAAM,OAAO;GACb,aAAa,OAAO;GACpB,WAAW,OAAO;GAClB,UAAU,OAAO;GACjB,GAAI,OAAO,qBAAqB,SAC5B,EAAE,GACF,EAAE,kBAAkB,OAAO,kBAAkB;GAClD,CAAC;;AAEJ,QAAO;EAAE,eAAe;EAAG;EAAe;;;AAG5C,SAAgB,uBACd,OAC0B;AAC1B,KACE,CAAC,OAAO,MAAM,IACd,MAAM,kBAAkB,KACxB,CAAC,IAAI,MAAM,YAAY,IACvB,CAAC,IAAI,MAAM,QAAQ,IACnB,CAAC,IAAI,MAAM,cAAc,IACxB,MAAM,aAAa,QAAQ,CAAC,iBAAiB,MAAM,SAAS,CAE7D,QAAO;AACT,QAAO;EACL,eAAe;EACf,aAAa,MAAM;EACnB,SAAS,MAAM;EACf,eAAe,MAAM;EACrB,UAAU,MAAM;EACjB;;;AAGH,SAAgB,qBAAqB,QAAoC;AACvE,QAAO,OAAO,oBAAoB,WAAW,OAAO;;;AAGtD,SAAgB,qBACd,GACA,GACQ;AACR,QACE,qBAAqB,EAAE,GAAG,qBAAqB,EAAE,IACjD,KAAK,MAAM,EAAE,YAAY,GAAG,KAAK,MAAM,EAAE,YAAY,KACpD,EAAE,KAAK,EAAE,KAAK,KAAK,EAAE,KAAK,EAAE,KAAK,IAAI;;;AAI1C,SAAgB,uBACd,OACA,MACA,SACmB;CACnB,MAAM,WAAW,KAAK,cAAc,QACjC,MAAM,kBAAkB,GAAG,QAAQ,CAAC,QACtC;CACD,MAAM,SAAS,MACb,MAAM,QAAQ,SAAS,EAAE,GAAG,IAAI,MAAM,cAAc,SAAS,EAAE,GAAG;CACpE,MAAM,SACJ,SAAS,MAAM,MAAM,EAAE,OAAO,MAAM,YAAY,CAAC,MAAM,EAAE,CAAC,IAC1D,KAAK,cAAc,MAChB,MACC,EAAE,OAAO,MAAM,YACf,CAAC,MAAM,EAAE,IACT,EAAE,UAAU,MAAM,eAAe;EAC/B,MAAM,UAAU,OAAO,QAAQ,WAAW;AAC1C,MACE,CAAC,QAAQ,MACN,CAAC,SACA,QAAQ,SAAyC,OACpD,CAED,QAAO;EACT,MAAM,QAAQ,OAAO,YACnB,QAAQ,QACL,CAAC,SACA,QAAQ,SAAyC,OACpD,CACF;AACD,SAAO,kBAAkB;GAAE,GAAG;GAAG,WAAW,CAAC,MAAM;GAAE,EAAE,QAAQ,CAC5D;GACH,CACL;CACH,MAAM,WAAW,SACd,QAAQ,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,YAAY,SAAS,EAAE,GAAG,CAAC,CAC7D,KAAK,qBAAqB,CAAC;CAC9B,MAAM,SACJ,WACC,CAAC,YACA,qBAAqB,OAAO,IAAI,qBAAqB,SAAS,IAC5D,SACA;AACN,QAAO;EACL,GAAG;EACH,aAAa,SAAS,KAAK,MAAM,EAAE,GAAG;EACtC,UAAU,QAAQ,MAAM;EACzB;;;AAGH,SAAgB,wBACd,OACA,UACmB;AACnB,KAAI,CAAC,MAAM,YAAY,SAAS,SAAS,CAAE,QAAO;CAClD,MAAM,cAAc,MAAM,aAAa;AACvC,QAAO;EACL,GAAG;EACH,SAAS,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,MAAM,SAAS,SAAS,CAAC,CAAC;EACnD,eAAe,CACb,GAAG,IAAI,IAAI,CACT,GAAG,MAAM,eACT,GAAI,cAAc,MAAM,cAAc,CAAC,SAAS,CACjD,CAAC,CACH;EACD,UAAU,cAAc,OAAO,MAAM;EACtC;;;AAGH,SAAgB,kBACd,QACA,SACyC;AACzC,KAAI,CAAC,QAAQ,YACX,QAAO;EAAE,SAAS;EAAO,SAAS,CAAC,mBAAmB;EAAE;AAC1D,KAAI,CAAC,QAAQ,cAAc,CAACC,+BAAgB,QAAQ,WAAW,CAC7D,QAAO;EACL,SAAS;EACT,SAAS,CAAC,4BAA4B;EACvC;CACH,MAAM,UAAoB,EAAE;CAC5B,MAAM,UAAU,OAAO,UAAU,MAAM,eAAe;AACpD,SAAO,OAAO,QAAQ,WAAW,CAAC,OAAO,CAAC,OAAO,cAAc;GAC7D,MAAM,SAAS,QAAQ;GACvB,MAAM,UACJ,UAAU,eACN,WAAW,UACXA,+BAAgB,OAAO,IACvBC,yBAAU,QAAQ,SAAS,GAC3B,WAAW;AACjB,OAAI,CAAC,QACH,SAAQ,KACN,GAAG,MAAM,GAAG,WAAW,SAAY,eAAe,YAAY,SAAS,QAAQ,OAAO,KACvF;AACH,UAAO;IACP;GACF;AACF,QAAO;EACL;EACA,SAAS,UAAU,EAAE,GAAG;EACzB"}
@@ -1,4 +1,4 @@
1
- import { prerelease, satisfies, valid, validRange } from "semver";
1
+ import { isStableVersion, isValidRange, satisfies } from "./semver.mjs";
2
2
 
3
3
  //#region src/lib/notifications.ts
4
4
  function emptyNotificationState() {
@@ -46,7 +46,7 @@ function validNotificationConditions(value) {
46
46
  if (value.sdkVersion !== void 0 && value.framework === void 0) return false;
47
47
  return Object.entries(value).every(([key, expected]) => {
48
48
  if (!text(expected)) return false;
49
- if (key === "sdkVersion") return validRange(expected) !== null;
49
+ if (key === "sdkVersion") return isValidRange(expected);
50
50
  if (key === "plan") return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(expected);
51
51
  return Object.prototype.hasOwnProperty.call(conditionValues, key) && (conditionValues[key]?.includes(expected) ?? false);
52
52
  });
@@ -144,7 +144,7 @@ function matchNotification(notice, context) {
144
144
  matches: false,
145
145
  reasons: ["Development only"]
146
146
  };
147
- if (!context.sdkVersion || !valid(context.sdkVersion) || prerelease(context.sdkVersion) !== null) return {
147
+ if (!context.sdkVersion || !isStableVersion(context.sdkVersion)) return {
148
148
  matches: false,
149
149
  reasons: ["Known stable SDK required"]
150
150
  };
@@ -152,7 +152,7 @@ function matchNotification(notice, context) {
152
152
  const matches = notice.audiences.some((conditions) => {
153
153
  return Object.entries(conditions).every(([field, expected]) => {
154
154
  const actual = context[field];
155
- const matches = field === "sdkVersion" ? actual !== void 0 && valid(actual) !== null && prerelease(actual) === null && satisfies(actual, expected) : actual === expected;
155
+ const matches = field === "sdkVersion" ? actual !== void 0 && isStableVersion(actual) && satisfies(actual, expected) : actual === expected;
156
156
  if (!matches) reasons.push(`${field} ${actual === void 0 ? "is unknown" : `requires ${expected} (got ${actual})`}`);
157
157
  return matches;
158
158
  });
@@ -1 +1 @@
1
- {"version":3,"file":"notifications.mjs","names":[],"sources":["../../src/lib/notifications.ts"],"sourcesContent":["import { satisfies, valid, validRange, prerelease } from \"semver\";\n\n/** Versioned CDN notification contract. Keep the Intelligence copy and fixtures aligned. */\nexport type NotificationFramework = \"react\" | \"vue\" | \"angular\";\nexport type NotificationPriority = \"Low\" | \"Normal\" | \"High\" | \"Urgent\";\nexport interface NotificationConditions {\n framework?: NotificationFramework;\n sdkVersion?: string;\n intelligence?: \"enabled\" | \"disabled\";\n deployment?: \"managed\" | \"self-hosted\";\n plan?: string;\n}\nexport interface CohortNotification {\n id: string;\n title: string;\n body: string;\n publishedAt: string;\n audiences: NotificationConditions[];\n priority: NotificationPriority;\n priorityOverride?: number;\n}\nexport interface NotificationFeed {\n schemaVersion: 1;\n notifications: CohortNotification[];\n}\nexport interface NotificationContext extends Omit<\n NotificationConditions,\n \"sdkVersion\"\n> {\n development: boolean;\n sdkVersion?: string;\n}\nexport interface NotificationState {\n schemaVersion: 1;\n eligibleIds: string[];\n readIds: string[];\n suppressedIds: string[];\n activeId: string | null;\n}\nexport function emptyNotificationState(): NotificationState {\n return {\n schemaVersion: 1,\n eligibleIds: [],\n readIds: [],\n suppressedIds: [],\n activeId: null,\n };\n}\nconst priorities: Record<NotificationPriority, number> = {\n Low: 10,\n Normal: 20,\n High: 30,\n Urgent: 40,\n};\nconst conditionValues: Record<string, readonly string[]> = {\n framework: [\"react\", \"vue\", \"angular\"],\n intelligence: [\"enabled\", \"disabled\"],\n deployment: [\"managed\", \"self-hosted\"],\n};\nfunction record(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\nfunction onlyKeys(value: Record<string, unknown>, keys: string[]): boolean {\n return Object.keys(value).every((key) => keys.includes(key));\n}\nfunction text(value: unknown, max = 256): value is string {\n return (\n typeof value === \"string\" && value.trim().length > 0 && value.length <= max\n );\n}\nexport function isNotificationId(value: unknown): value is string {\n return (\n typeof value === \"string\" &&\n /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(\n value,\n )\n );\n}\nfunction ids(value: unknown): value is string[] {\n return (\n Array.isArray(value) &&\n value.every(isNotificationId) &&\n new Set(value).size === value.length\n );\n}\n/** Validate authored conditions; unsupported predicates cannot broaden the audience. */\nexport function validNotificationConditions(\n value: unknown,\n): value is NotificationConditions {\n if (!record(value)) return false;\n if (value.sdkVersion !== undefined && value.framework === undefined)\n return false;\n return Object.entries(value).every(([key, expected]) => {\n if (!text(expected)) return false;\n if (key === \"sdkVersion\") return validRange(expected) !== null;\n if (key === \"plan\") return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(expected);\n return (\n Object.prototype.hasOwnProperty.call(conditionValues, key) &&\n (conditionValues[key]?.includes(expected) ?? false)\n );\n });\n}\n/** Parse the public envelope. Any invalid entry rejects the whole feed. */\nexport function parseNotificationFeed(value: unknown): NotificationFeed | null {\n if (\n !record(value) ||\n !onlyKeys(value, [\"schemaVersion\", \"notifications\"]) ||\n value.schemaVersion !== 1\n )\n return null;\n if (!Array.isArray(value.notifications) || value.notifications.length > 100)\n return null;\n const notifications: CohortNotification[] = [];\n for (const notice of value.notifications) {\n if (\n !record(notice) ||\n !onlyKeys(notice, [\n \"id\",\n \"title\",\n \"body\",\n \"publishedAt\",\n \"audiences\",\n \"priority\",\n \"priorityOverride\",\n ])\n )\n return null;\n if (\n !isNotificationId(notice.id) ||\n !text(notice.title) ||\n !text(notice.body, 100_000) ||\n !text(notice.publishedAt)\n )\n return null;\n if (\n !/^\\d{4}-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d\\.\\d{3}Z$/.test(notice.publishedAt) ||\n !Number.isFinite(Date.parse(notice.publishedAt)) ||\n new Date(notice.publishedAt).toISOString() !== notice.publishedAt\n )\n return null;\n if (\n !Array.isArray(notice.audiences) ||\n !notice.audiences.length ||\n notice.audiences.length > 100 ||\n !notice.audiences.every(validNotificationConditions)\n )\n return null;\n if (\n !text(notice.priority) ||\n !Object.prototype.hasOwnProperty.call(priorities, notice.priority)\n )\n return null;\n if (\n notice.priorityOverride !== undefined &&\n (typeof notice.priorityOverride !== \"number\" ||\n !Number.isSafeInteger(notice.priorityOverride) ||\n notice.priorityOverride < 0)\n )\n return null;\n if (notifications.some((n) => n.id === notice.id)) return null;\n notifications.push({\n id: notice.id,\n title: notice.title,\n body: notice.body,\n publishedAt: notice.publishedAt,\n audiences: notice.audiences,\n priority: notice.priority as NotificationPriority,\n ...(notice.priorityOverride === undefined\n ? {}\n : { priorityOverride: notice.priorityOverride }),\n });\n }\n return { schemaVersion: 1, notifications };\n}\n/** Ignore malformed or incompatible browser state. */\nexport function parseNotificationState(\n value: unknown,\n): NotificationState | null {\n if (\n !record(value) ||\n value.schemaVersion !== 1 ||\n !ids(value.eligibleIds) ||\n !ids(value.readIds) ||\n !ids(value.suppressedIds) ||\n (value.activeId !== null && !isNotificationId(value.activeId))\n )\n return null;\n return {\n schemaVersion: 1,\n eligibleIds: value.eligibleIds,\n readIds: value.readIds,\n suppressedIds: value.suppressedIds,\n activeId: value.activeId,\n };\n}\n/** Internal priority never becomes a developer-facing badge. */\nexport function notificationPriority(notice: CohortNotification): number {\n return notice.priorityOverride ?? priorities[notice.priority];\n}\n/** Stable ordering used by authoring reports and first-time selection. */\nexport function compareNotifications(\n a: CohortNotification,\n b: CohortNotification,\n): number {\n return (\n notificationPriority(b) - notificationPriority(a) ||\n Date.parse(b.publishedAt) - Date.parse(a.publishedAt) ||\n (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)\n );\n}\n/** Refresh eligibility without promoting the previously delivered backlog. */\nexport function reconcileNotifications(\n state: NotificationState,\n feed: NotificationFeed,\n context: NotificationContext,\n): NotificationState {\n const eligible = feed.notifications.filter(\n (n) => matchNotification(n, context).matches,\n );\n const quiet = (n: CohortNotification) =>\n state.readIds.includes(n.id) || state.suppressedIds.includes(n.id);\n const active =\n eligible.find((n) => n.id === state.activeId && !quiet(n)) ??\n feed.notifications.find(\n (n) =>\n n.id === state.activeId &&\n !quiet(n) &&\n n.audiences.some((conditions) => {\n const entries = Object.entries(conditions);\n if (\n !entries.some(\n ([key]) =>\n context[key as keyof NotificationConditions] === undefined,\n )\n )\n return false;\n const known = Object.fromEntries(\n entries.filter(\n ([key]) =>\n context[key as keyof NotificationConditions] !== undefined,\n ),\n );\n return matchNotification({ ...n, audiences: [known] }, context)\n .matches;\n }),\n );\n const newcomer = eligible\n .filter((n) => !quiet(n) && !state.eligibleIds.includes(n.id))\n .sort(compareNotifications)[0];\n const winner =\n active &&\n (!newcomer ||\n notificationPriority(active) >= notificationPriority(newcomer))\n ? active\n : newcomer;\n return {\n ...state,\n eligibleIds: eligible.map((n) => n.id),\n activeId: winner?.id ?? null,\n };\n}\n/** Reading or closing the highlight quiets its delivered backlog, not future IDs. */\nexport function acknowledgeNotification(\n state: NotificationState,\n noticeId: string,\n): NotificationState {\n if (!state.eligibleIds.includes(noticeId)) return state;\n const highlighted = state.activeId === noticeId;\n return {\n ...state,\n readIds: [...new Set([...state.readIds, noticeId])],\n suppressedIds: [\n ...new Set([\n ...state.suppressedIds,\n ...(highlighted ? state.eligibleIds : [noticeId]),\n ]),\n ],\n activeId: highlighted ? null : state.activeId,\n };\n}\n/** Explain targeting using the same rules in CI and the Inspector. */\nexport function matchNotification(\n notice: CohortNotification,\n context: NotificationContext,\n): { matches: boolean; reasons: string[] } {\n if (!context.development)\n return { matches: false, reasons: [\"Development only\"] };\n if (\n !context.sdkVersion ||\n !valid(context.sdkVersion) ||\n prerelease(context.sdkVersion) !== null\n )\n return {\n matches: false,\n reasons: [\"Known stable SDK required\"],\n };\n const reasons: string[] = [];\n const matches = notice.audiences.some((conditions) => {\n return Object.entries(conditions).every(([field, expected]) => {\n const actual = context[field as keyof NotificationConditions];\n const matches =\n field === \"sdkVersion\"\n ? actual !== undefined &&\n valid(actual) !== null &&\n prerelease(actual) === null &&\n satisfies(actual, expected)\n : actual === expected;\n if (!matches)\n reasons.push(\n `${field} ${actual === undefined ? \"is unknown\" : `requires ${expected} (got ${actual})`}`,\n );\n return matches;\n });\n });\n return {\n matches,\n reasons: matches ? [] : reasons,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"notifications.mjs","names":[],"sources":["../../src/lib/notifications.ts"],"sourcesContent":["import { isStableVersion, isValidRange, satisfies } from \"./semver.js\";\n\n/** Versioned CDN notification contract. Keep the Intelligence copy and fixtures aligned. */\nexport type NotificationFramework = \"react\" | \"vue\" | \"angular\";\nexport type NotificationPriority = \"Low\" | \"Normal\" | \"High\" | \"Urgent\";\nexport interface NotificationConditions {\n framework?: NotificationFramework;\n sdkVersion?: string;\n intelligence?: \"enabled\" | \"disabled\";\n deployment?: \"managed\" | \"self-hosted\";\n plan?: string;\n}\nexport interface CohortNotification {\n id: string;\n title: string;\n body: string;\n publishedAt: string;\n audiences: NotificationConditions[];\n priority: NotificationPriority;\n priorityOverride?: number;\n}\nexport interface NotificationFeed {\n schemaVersion: 1;\n notifications: CohortNotification[];\n}\nexport interface NotificationContext extends Omit<\n NotificationConditions,\n \"sdkVersion\"\n> {\n development: boolean;\n sdkVersion?: string;\n}\nexport interface NotificationState {\n schemaVersion: 1;\n eligibleIds: string[];\n readIds: string[];\n suppressedIds: string[];\n activeId: string | null;\n}\nexport function emptyNotificationState(): NotificationState {\n return {\n schemaVersion: 1,\n eligibleIds: [],\n readIds: [],\n suppressedIds: [],\n activeId: null,\n };\n}\nconst priorities: Record<NotificationPriority, number> = {\n Low: 10,\n Normal: 20,\n High: 30,\n Urgent: 40,\n};\nconst conditionValues: Record<string, readonly string[]> = {\n framework: [\"react\", \"vue\", \"angular\"],\n intelligence: [\"enabled\", \"disabled\"],\n deployment: [\"managed\", \"self-hosted\"],\n};\nfunction record(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\nfunction onlyKeys(value: Record<string, unknown>, keys: string[]): boolean {\n return Object.keys(value).every((key) => keys.includes(key));\n}\nfunction text(value: unknown, max = 256): value is string {\n return (\n typeof value === \"string\" && value.trim().length > 0 && value.length <= max\n );\n}\nexport function isNotificationId(value: unknown): value is string {\n return (\n typeof value === \"string\" &&\n /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(\n value,\n )\n );\n}\nfunction ids(value: unknown): value is string[] {\n return (\n Array.isArray(value) &&\n value.every(isNotificationId) &&\n new Set(value).size === value.length\n );\n}\n/** Validate authored conditions; unsupported predicates cannot broaden the audience. */\nexport function validNotificationConditions(\n value: unknown,\n): value is NotificationConditions {\n if (!record(value)) return false;\n if (value.sdkVersion !== undefined && value.framework === undefined)\n return false;\n return Object.entries(value).every(([key, expected]) => {\n if (!text(expected)) return false;\n if (key === \"sdkVersion\") return isValidRange(expected);\n if (key === \"plan\") return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(expected);\n return (\n Object.prototype.hasOwnProperty.call(conditionValues, key) &&\n (conditionValues[key]?.includes(expected) ?? false)\n );\n });\n}\n/** Parse the public envelope. Any invalid entry rejects the whole feed. */\nexport function parseNotificationFeed(value: unknown): NotificationFeed | null {\n if (\n !record(value) ||\n !onlyKeys(value, [\"schemaVersion\", \"notifications\"]) ||\n value.schemaVersion !== 1\n )\n return null;\n if (!Array.isArray(value.notifications) || value.notifications.length > 100)\n return null;\n const notifications: CohortNotification[] = [];\n for (const notice of value.notifications) {\n if (\n !record(notice) ||\n !onlyKeys(notice, [\n \"id\",\n \"title\",\n \"body\",\n \"publishedAt\",\n \"audiences\",\n \"priority\",\n \"priorityOverride\",\n ])\n )\n return null;\n if (\n !isNotificationId(notice.id) ||\n !text(notice.title) ||\n !text(notice.body, 100_000) ||\n !text(notice.publishedAt)\n )\n return null;\n if (\n !/^\\d{4}-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d\\.\\d{3}Z$/.test(notice.publishedAt) ||\n !Number.isFinite(Date.parse(notice.publishedAt)) ||\n new Date(notice.publishedAt).toISOString() !== notice.publishedAt\n )\n return null;\n if (\n !Array.isArray(notice.audiences) ||\n !notice.audiences.length ||\n notice.audiences.length > 100 ||\n !notice.audiences.every(validNotificationConditions)\n )\n return null;\n if (\n !text(notice.priority) ||\n !Object.prototype.hasOwnProperty.call(priorities, notice.priority)\n )\n return null;\n if (\n notice.priorityOverride !== undefined &&\n (typeof notice.priorityOverride !== \"number\" ||\n !Number.isSafeInteger(notice.priorityOverride) ||\n notice.priorityOverride < 0)\n )\n return null;\n if (notifications.some((n) => n.id === notice.id)) return null;\n notifications.push({\n id: notice.id,\n title: notice.title,\n body: notice.body,\n publishedAt: notice.publishedAt,\n audiences: notice.audiences,\n priority: notice.priority as NotificationPriority,\n ...(notice.priorityOverride === undefined\n ? {}\n : { priorityOverride: notice.priorityOverride }),\n });\n }\n return { schemaVersion: 1, notifications };\n}\n/** Ignore malformed or incompatible browser state. */\nexport function parseNotificationState(\n value: unknown,\n): NotificationState | null {\n if (\n !record(value) ||\n value.schemaVersion !== 1 ||\n !ids(value.eligibleIds) ||\n !ids(value.readIds) ||\n !ids(value.suppressedIds) ||\n (value.activeId !== null && !isNotificationId(value.activeId))\n )\n return null;\n return {\n schemaVersion: 1,\n eligibleIds: value.eligibleIds,\n readIds: value.readIds,\n suppressedIds: value.suppressedIds,\n activeId: value.activeId,\n };\n}\n/** Internal priority never becomes a developer-facing badge. */\nexport function notificationPriority(notice: CohortNotification): number {\n return notice.priorityOverride ?? priorities[notice.priority];\n}\n/** Stable ordering used by authoring reports and first-time selection. */\nexport function compareNotifications(\n a: CohortNotification,\n b: CohortNotification,\n): number {\n return (\n notificationPriority(b) - notificationPriority(a) ||\n Date.parse(b.publishedAt) - Date.parse(a.publishedAt) ||\n (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)\n );\n}\n/** Refresh eligibility without promoting the previously delivered backlog. */\nexport function reconcileNotifications(\n state: NotificationState,\n feed: NotificationFeed,\n context: NotificationContext,\n): NotificationState {\n const eligible = feed.notifications.filter(\n (n) => matchNotification(n, context).matches,\n );\n const quiet = (n: CohortNotification) =>\n state.readIds.includes(n.id) || state.suppressedIds.includes(n.id);\n const active =\n eligible.find((n) => n.id === state.activeId && !quiet(n)) ??\n feed.notifications.find(\n (n) =>\n n.id === state.activeId &&\n !quiet(n) &&\n n.audiences.some((conditions) => {\n const entries = Object.entries(conditions);\n if (\n !entries.some(\n ([key]) =>\n context[key as keyof NotificationConditions] === undefined,\n )\n )\n return false;\n const known = Object.fromEntries(\n entries.filter(\n ([key]) =>\n context[key as keyof NotificationConditions] !== undefined,\n ),\n );\n return matchNotification({ ...n, audiences: [known] }, context)\n .matches;\n }),\n );\n const newcomer = eligible\n .filter((n) => !quiet(n) && !state.eligibleIds.includes(n.id))\n .sort(compareNotifications)[0];\n const winner =\n active &&\n (!newcomer ||\n notificationPriority(active) >= notificationPriority(newcomer))\n ? active\n : newcomer;\n return {\n ...state,\n eligibleIds: eligible.map((n) => n.id),\n activeId: winner?.id ?? null,\n };\n}\n/** Reading or closing the highlight quiets its delivered backlog, not future IDs. */\nexport function acknowledgeNotification(\n state: NotificationState,\n noticeId: string,\n): NotificationState {\n if (!state.eligibleIds.includes(noticeId)) return state;\n const highlighted = state.activeId === noticeId;\n return {\n ...state,\n readIds: [...new Set([...state.readIds, noticeId])],\n suppressedIds: [\n ...new Set([\n ...state.suppressedIds,\n ...(highlighted ? state.eligibleIds : [noticeId]),\n ]),\n ],\n activeId: highlighted ? null : state.activeId,\n };\n}\n/** Explain targeting using the same rules in CI and the Inspector. */\nexport function matchNotification(\n notice: CohortNotification,\n context: NotificationContext,\n): { matches: boolean; reasons: string[] } {\n if (!context.development)\n return { matches: false, reasons: [\"Development only\"] };\n if (!context.sdkVersion || !isStableVersion(context.sdkVersion))\n return {\n matches: false,\n reasons: [\"Known stable SDK required\"],\n };\n const reasons: string[] = [];\n const matches = notice.audiences.some((conditions) => {\n return Object.entries(conditions).every(([field, expected]) => {\n const actual = context[field as keyof NotificationConditions];\n const matches =\n field === \"sdkVersion\"\n ? actual !== undefined &&\n isStableVersion(actual) &&\n satisfies(actual, expected)\n : actual === expected;\n if (!matches)\n reasons.push(\n `${field} ${actual === undefined ? \"is unknown\" : `requires ${expected} (got ${actual})`}`,\n );\n return matches;\n });\n });\n return {\n matches,\n reasons: matches ? [] : reasons,\n };\n}\n"],"mappings":"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"}