@ai-matrx/kit 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/LICENSE +21 -0
  3. package/README.md +72 -0
  4. package/dist/autosave.cjs +92 -0
  5. package/dist/autosave.cjs.map +1 -0
  6. package/dist/autosave.d.cts +17 -0
  7. package/dist/autosave.d.ts +17 -0
  8. package/dist/autosave.js +72 -0
  9. package/dist/autosave.js.map +1 -0
  10. package/dist/clipboard.cjs +152 -0
  11. package/dist/clipboard.cjs.map +1 -0
  12. package/dist/clipboard.d.cts +17 -0
  13. package/dist/clipboard.d.ts +17 -0
  14. package/dist/clipboard.js +132 -0
  15. package/dist/clipboard.js.map +1 -0
  16. package/dist/color-util.cjs +790 -0
  17. package/dist/color-util.cjs.map +1 -0
  18. package/dist/color-util.d.cts +235 -0
  19. package/dist/color-util.d.ts +235 -0
  20. package/dist/color-util.js +767 -0
  21. package/dist/color-util.js.map +1 -0
  22. package/dist/concurrency.cjs +59 -0
  23. package/dist/concurrency.cjs.map +1 -0
  24. package/dist/concurrency.d.cts +19 -0
  25. package/dist/concurrency.d.ts +19 -0
  26. package/dist/concurrency.js +38 -0
  27. package/dist/concurrency.js.map +1 -0
  28. package/dist/confirm.cjs +344 -0
  29. package/dist/confirm.cjs.map +1 -0
  30. package/dist/confirm.d.cts +132 -0
  31. package/dist/confirm.d.ts +132 -0
  32. package/dist/confirm.js +312 -0
  33. package/dist/confirm.js.map +1 -0
  34. package/dist/delimiter-guard.cjs +172 -0
  35. package/dist/delimiter-guard.cjs.map +1 -0
  36. package/dist/delimiter-guard.d.cts +119 -0
  37. package/dist/delimiter-guard.d.ts +119 -0
  38. package/dist/delimiter-guard.js +151 -0
  39. package/dist/delimiter-guard.js.map +1 -0
  40. package/dist/drafts.cjs +223 -0
  41. package/dist/drafts.cjs.map +1 -0
  42. package/dist/drafts.d.cts +62 -0
  43. package/dist/drafts.d.ts +62 -0
  44. package/dist/drafts.js +201 -0
  45. package/dist/drafts.js.map +1 -0
  46. package/dist/idb-store.cjs +178 -0
  47. package/dist/idb-store.cjs.map +1 -0
  48. package/dist/idb-store.d.cts +111 -0
  49. package/dist/idb-store.d.ts +111 -0
  50. package/dist/idb-store.js +155 -0
  51. package/dist/idb-store.js.map +1 -0
  52. package/dist/idle-scheduler.cjs +268 -0
  53. package/dist/idle-scheduler.cjs.map +1 -0
  54. package/dist/idle-scheduler.d.cts +185 -0
  55. package/dist/idle-scheduler.d.ts +185 -0
  56. package/dist/idle-scheduler.js +246 -0
  57. package/dist/idle-scheduler.js.map +1 -0
  58. package/dist/index.cjs +3113 -0
  59. package/dist/index.cjs.map +1 -0
  60. package/dist/index.d.cts +897 -0
  61. package/dist/index.d.ts +897 -0
  62. package/dist/index.js +3088 -0
  63. package/dist/index.js.map +1 -0
  64. package/dist/invalidation.cjs +64 -0
  65. package/dist/invalidation.cjs.map +1 -0
  66. package/dist/invalidation.d.cts +52 -0
  67. package/dist/invalidation.d.ts +52 -0
  68. package/dist/invalidation.js +43 -0
  69. package/dist/invalidation.js.map +1 -0
  70. package/dist/json-format.cjs +319 -0
  71. package/dist/json-format.cjs.map +1 -0
  72. package/dist/json-format.d.cts +148 -0
  73. package/dist/json-format.d.ts +148 -0
  74. package/dist/json-format.js +286 -0
  75. package/dist/json-format.js.map +1 -0
  76. package/dist/latest-request.cjs +36 -0
  77. package/dist/latest-request.cjs.map +1 -0
  78. package/dist/latest-request.d.cts +29 -0
  79. package/dist/latest-request.d.ts +29 -0
  80. package/dist/latest-request.js +16 -0
  81. package/dist/latest-request.js.map +1 -0
  82. package/dist/qr.cjs +109 -0
  83. package/dist/qr.cjs.map +1 -0
  84. package/dist/qr.d.cts +47 -0
  85. package/dist/qr.d.ts +47 -0
  86. package/dist/qr.js +78 -0
  87. package/dist/qr.js.map +1 -0
  88. package/dist/react-tree.cjs +69 -0
  89. package/dist/react-tree.cjs.map +1 -0
  90. package/dist/react-tree.d.cts +38 -0
  91. package/dist/react-tree.d.ts +38 -0
  92. package/dist/react-tree.js +38 -0
  93. package/dist/react-tree.js.map +1 -0
  94. package/dist/search-scoring.cjs +116 -0
  95. package/dist/search-scoring.cjs.map +1 -0
  96. package/dist/search-scoring.d.cts +77 -0
  97. package/dist/search-scoring.d.ts +77 -0
  98. package/dist/search-scoring.js +95 -0
  99. package/dist/search-scoring.js.map +1 -0
  100. package/dist/text-case.cjs +313 -0
  101. package/dist/text-case.cjs.map +1 -0
  102. package/dist/text-case.d.cts +53 -0
  103. package/dist/text-case.d.ts +53 -0
  104. package/dist/text-case.js +292 -0
  105. package/dist/text-case.js.map +1 -0
  106. package/dist/toast.cjs +74 -0
  107. package/dist/toast.cjs.map +1 -0
  108. package/dist/toast.d.cts +92 -0
  109. package/dist/toast.d.ts +92 -0
  110. package/dist/toast.js +54 -0
  111. package/dist/toast.js.map +1 -0
  112. package/dist/url-state.cjs +212 -0
  113. package/dist/url-state.cjs.map +1 -0
  114. package/dist/url-state.d.cts +90 -0
  115. package/dist/url-state.d.ts +90 -0
  116. package/dist/url-state.js +199 -0
  117. package/dist/url-state.js.map +1 -0
  118. package/package.json +286 -0
@@ -0,0 +1,151 @@
1
+ // src/delimiter-guard.ts
2
+ var ZWSP = "\u200B";
3
+ var ESCAPED_DOLLARS = `${ZWSP}$${ZWSP}$`;
4
+ var ESCAPED_BRACKET = `${ZWSP}[`;
5
+ var MAX_MATH_SPAN = 600;
6
+ var STRUCTURAL_MARKDOWN = /\]\(|https?:\/\/|\*\*|(?:^|\n)[ \t]{0,3}#{1,6}[ \t]|(?:^|\n)[ \t]*[-*+][ \t]+|(?:^|\n)[ \t]*\d+[.)][ \t]/;
7
+ var LATEX_COMMAND = /\\[a-zA-Z]/;
8
+ var PROSE_WORD = /[A-Za-z]{3,}/g;
9
+ var PROSE_WORD_LIMIT = 6;
10
+ function looksLikeMath(inner) {
11
+ const s = inner.trim();
12
+ if (!s) return false;
13
+ if (STRUCTURAL_MARKDOWN.test(s)) return false;
14
+ if (s.length > MAX_MATH_SPAN) return false;
15
+ if (LATEX_COMMAND.test(s)) {
16
+ return (s.match(PROSE_WORD) ?? []).length < PROSE_WORD_LIMIT * 3;
17
+ }
18
+ if (/\n[ \t]*\n/.test(s)) return false;
19
+ return (s.match(PROSE_WORD) ?? []).length < PROSE_WORD_LIMIT;
20
+ }
21
+ function protectedRanges(text) {
22
+ const ranges = [];
23
+ const patterns = [/```[\s\S]*?(?:```|$)/g, /~~~[\s\S]*?(?:~~~|$)/g, /`[^`\n]*`/g];
24
+ for (const re of patterns) {
25
+ let m;
26
+ while ((m = re.exec(text)) !== null) {
27
+ ranges.push([m.index, m.index + m[0].length]);
28
+ }
29
+ }
30
+ return ranges;
31
+ }
32
+ function isProtected(index, ranges) {
33
+ return ranges.some(([start, end]) => index >= start && index < end);
34
+ }
35
+ function preview(text, max = 160) {
36
+ const flat = text.replace(/\s+/g, " ").trim();
37
+ return flat.length > max ? `${flat.slice(0, max)}\u2026` : flat;
38
+ }
39
+ function guardMathDelimiters(text) {
40
+ if (!text.includes("$$")) return { text, violations: [] };
41
+ const ranges = protectedRanges(text);
42
+ const tokens = [];
43
+ for (let i = 0; i < text.length - 1; i++) {
44
+ if (text[i] !== "$" || text[i + 1] !== "$") continue;
45
+ if (!isProtected(i, ranges)) tokens.push(i);
46
+ i++;
47
+ }
48
+ if (tokens.length === 0) return { text, violations: [] };
49
+ const violations = [];
50
+ const escapeAt = [];
51
+ let j = 0;
52
+ while (j < tokens.length) {
53
+ const open = tokens[j];
54
+ const close = tokens[j + 1];
55
+ if (close === void 0) {
56
+ violations.push({
57
+ reason: "unpaired",
58
+ index: open,
59
+ spanLength: 0,
60
+ // An unpaired `$$` is inert to remark-math (nothing closes it), so it
61
+ // is reported but NOT escaped — it already renders as literal text.
62
+ preview: preview(text.slice(open, open + 120))
63
+ });
64
+ break;
65
+ }
66
+ const inner = text.slice(open + 2, close);
67
+ if (looksLikeMath(inner)) {
68
+ j += 2;
69
+ continue;
70
+ }
71
+ escapeAt.push(open);
72
+ violations.push({
73
+ reason: "prose-span",
74
+ index: open,
75
+ spanLength: inner.length,
76
+ preview: preview(inner)
77
+ });
78
+ j += 1;
79
+ }
80
+ let guarded = text;
81
+ for (const index of [...escapeAt].sort((a, b) => b - a)) {
82
+ guarded = `${guarded.slice(0, index)}${ESCAPED_DOLLARS}${guarded.slice(index + 2)}`;
83
+ }
84
+ return { text: guarded, violations };
85
+ }
86
+ var MAX_LINK_LABEL = 200;
87
+ var LABEL_BLOCK_STRUCTURE = /\n[ \t]*\n|(?:^|\n)[ \t]*[-*+][ \t]+|#{2,6}[ \t]/;
88
+ function guardRunawayLinks(text) {
89
+ if (!text.includes("[")) return { text, violations: [] };
90
+ const ranges = protectedRanges(text);
91
+ const violations = [];
92
+ const escapeAt = [];
93
+ const linkRe = /\[((?:[^[\]]|\\.)*)\]\(([^\s)]*)/g;
94
+ let m;
95
+ while ((m = linkRe.exec(text)) !== null) {
96
+ const open = m.index;
97
+ if (isProtected(open, ranges)) continue;
98
+ const label = m[1] ?? "";
99
+ const runaway = label.length > MAX_LINK_LABEL || LABEL_BLOCK_STRUCTURE.test(label);
100
+ if (!runaway) continue;
101
+ escapeAt.push(open);
102
+ violations.push({
103
+ reason: "runaway-link",
104
+ index: open,
105
+ spanLength: label.length,
106
+ preview: preview(label)
107
+ });
108
+ }
109
+ let guarded = text;
110
+ for (const index of [...escapeAt].sort((a, b) => b - a)) {
111
+ guarded = `${guarded.slice(0, index)}${ESCAPED_BRACKET}${guarded.slice(index + 1)}`;
112
+ }
113
+ return { text: guarded, violations };
114
+ }
115
+ function guardMarkdownDelimiters(text) {
116
+ const math = guardMathDelimiters(text);
117
+ const links = guardRunawayLinks(math.text);
118
+ return {
119
+ text: links.text,
120
+ violations: [...math.violations, ...links.violations]
121
+ };
122
+ }
123
+ function reportDelimiterViolations(violations, context) {
124
+ if (violations.length === 0) return;
125
+ try {
126
+ const worst = violations.find((v) => v.reason !== "unpaired") ?? violations[0];
127
+ if (!worst) return;
128
+ const message = worst.reason === "prose-span" ? `Malformed math delimiters: a stray "$$" would have turned ${worst.spanLength} chars of prose into a math span (KaTeX would render it as red error text). Escaped it.` : worst.reason === "runaway-link" ? `Runaway markdown link: an unclosed "[" would have turned ${worst.spanLength} chars into one link label. Escaped it.` : `Malformed math delimiters: an unpaired "$$" reached the renderer.`;
129
+ console.warn(`[markdown-delimiter-guard] ${message}`, {
130
+ renderPath: context.renderPath,
131
+ violations
132
+ });
133
+ context.capture?.({
134
+ source: "markdown-delimiters",
135
+ message,
136
+ relation: `markdown:${context.renderPath}`,
137
+ details: worst.preview,
138
+ conversationId: context.conversationId,
139
+ callSite: "guardMarkdownDelimiters",
140
+ raw: { messageId: context.messageId, violations }
141
+ });
142
+ } catch {
143
+ }
144
+ }
145
+ export {
146
+ guardMarkdownDelimiters,
147
+ guardMathDelimiters,
148
+ guardRunawayLinks,
149
+ reportDelimiterViolations
150
+ };
151
+ //# sourceMappingURL=delimiter-guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/delimiter-guard.ts"],"sourcesContent":["/**\n * @ai-matrx/kit/delimiter-guard — stops ONE stray markdown delimiter from\n * swallowing a whole section of a streamed answer.\n *\n * THE FAILURE CLASS\n * -----------------\n * Markdown delimiters pair greedily and blindly. A single stray opener emitted\n * by a model (the common shape is a mangled citation:\n * `…/a-quicker-way-to-heal-prp-and-prf$$ .`) pairs with the next matching\n * delimiter anywhere later in the message, and everything in between —\n * headings, bold, links, whole sections — collapses into one node.\n *\n * Two delimiters cause this in a remark-math + CommonMark pipeline:\n *\n * 1. `$$` (remark-math). The swallowed prose becomes a math node, KaTeX fails\n * to parse it, and `rehype-katex` falls back to its built-in error\n * rendering: the raw source re-emitted inside `<span class=\"katex-error\"\n * style=\"color:#cc0000\">`. The symptom is a huge block of BRIGHT RED\n * unrendered markdown mid-answer. That red is KaTeX reporting a parse error\n * on text that was never math — not a style of yours.\n * 2. `[` (CommonMark link label). The swallowed prose becomes the label of one\n * enormous hyperlink — the same bug wearing blue instead of red.\n *\n * WHAT THIS DOES\n * --------------\n * Before the markdown pipeline runs, each candidate span is checked for\n * plausibility. A `$$…$$` span carrying markdown structure (links, URLs, bold,\n * headings, list markers) or reading as prose is not math; a link label that is\n * hundreds of characters long or contains block structure is not a label. The\n * offending OPENER is neutralized (`&#36;&#36;`, `&#91;`) and scanning resumes at the next\n * delimiter, so genuine math and genuine links later in the same message still\n * render. Real content is never touched.\n *\n * LOUD RECOVERY: every firing is a real upstream defect (a model emitting\n * malformed delimiters, or a producer mangling a citation). Callers report the\n * returned violations — see `reportDelimiterViolations`.\n *\n * Ported verbatim from matrx-frontend `lib/markdown/delimiter-guard.ts`, with\n * ONE coupling inversion: the app's `captureError` store import became the\n * injected `capture` sink on `reportDelimiterViolations`' context (payload\n * shape preserved exactly). No sink means console-only loud recovery.\n */\n\nexport type DelimiterViolationReason =\n /** A `$$…$$` pair whose contents are prose/markdown, not math. */\n | \"prose-span\"\n /** A `$$` with no closing partner in the content. */\n | \"unpaired\"\n /** A `[…](…)` link whose label swallowed prose/structure. */\n | \"runaway-link\";\n\nexport interface DelimiterViolation {\n reason: DelimiterViolationReason;\n /** Character offset of the offending delimiter in the input string. */\n index: number;\n /** Length of the span between the delimiters (0 for `unpaired`). */\n spanLength: number;\n /** Short excerpt of what would have been swallowed. */\n preview: string;\n}\n\nexport interface DelimiterGuardResult {\n /** Input with runaway openers escaped. */\n text: string;\n violations: DelimiterViolation[];\n}\n\n/**\n * Neutralized delimiters. Two requirements drove this encoding:\n *\n * - It must be INVISIBLE in the rendered output. A backslash escape (`\\$\\$`)\n * is emitted literally when the delimiter abuts constructs remark does not\n * re-parse, so the reader sees stray backslashes.\n * - It must not be swallowed by the GFM autolink extension. These strays sit\n * right after a bare URL (that is how they are produced), and a character\n * reference placed there is absorbed into the link target instead of being\n * decoded.\n *\n * A zero-width space satisfies both: it terminates the autolink, splits the\n * `$$` token so remark-math never sees a delimiter (single `$` is inert —\n * `singleDollarTextMath: false`), and renders as nothing. The bracket keeps a\n * character reference (a lone `[` has no token to split) behind a ZWSP.\n */\nconst ZWSP = \"\\u200B\";\nconst ESCAPED_DOLLARS = `${ZWSP}$${ZWSP}$`;\nconst ESCAPED_BRACKET = `${ZWSP}&#91;`;\n\n/** Longest span we will accept as real math when no LaTeX command is present. */\nconst MAX_MATH_SPAN = 600;\n\n/**\n * Markdown structure that can never appear inside real math:\n * a markdown link, a bare URL, bold markers, an ATX heading, or a\n * line-leading list marker.\n */\nconst STRUCTURAL_MARKDOWN =\n /\\]\\(|https?:\\/\\/|\\*\\*|(?:^|\\n)[ \\t]{0,3}#{1,6}[ \\t]|(?:^|\\n)[ \\t]*[-*+][ \\t]+|(?:^|\\n)[ \\t]*\\d+[.)][ \\t]/;\n\n/** A LaTeX control sequence (`\\frac`, `\\sim`, `\\text`, …). */\nconst LATEX_COMMAND = /\\\\[a-zA-Z]/;\n\n/** Alphabetic words of 3+ letters — the prose signal for command-free spans. */\nconst PROSE_WORD = /[A-Za-z]{3,}/g;\n\n/** Word count at which a LaTeX-command-free span is judged to be prose. */\nconst PROSE_WORD_LIMIT = 6;\n\nfunction looksLikeMath(inner: string): boolean {\n const s = inner.trim();\n if (!s) return false;\n\n // Structural markdown wins over every other signal — a swallowed prose span\n // routinely contains real LaTeX fragments (`$\\sim 200 \\text{ g}$`) picked up\n // from the sentences it ate, so the command check cannot run first.\n if (STRUCTURAL_MARKDOWN.test(s)) return false;\n\n if (s.length > MAX_MATH_SPAN) return false;\n\n // A LaTeX control sequence is strong evidence of math — but not proof: a\n // swallowed span often eats sentences that themselves contained inline math\n // (`$\\sim 400 \\text{ g}$`). Real math is symbol-dense, so a span that is\n // mostly English words is still prose.\n if (LATEX_COMMAND.test(s)) {\n return (s.match(PROSE_WORD) ?? []).length < PROSE_WORD_LIMIT * 3;\n }\n\n if (/\\n[ \\t]*\\n/.test(s)) return false;\n\n return (s.match(PROSE_WORD) ?? []).length < PROSE_WORD_LIMIT;\n}\n\n/** Ranges (fenced blocks, inline code) whose `$$` must be ignored. */\nfunction protectedRanges(text: string): Array<[number, number]> {\n const ranges: Array<[number, number]> = [];\n const patterns = [/```[\\s\\S]*?(?:```|$)/g, /~~~[\\s\\S]*?(?:~~~|$)/g, /`[^`\\n]*`/g];\n for (const re of patterns) {\n let m: RegExpExecArray | null;\n while ((m = re.exec(text)) !== null) {\n ranges.push([m.index, m.index + m[0].length]);\n }\n }\n return ranges;\n}\n\nfunction isProtected(index: number, ranges: Array<[number, number]>): boolean {\n return ranges.some(([start, end]) => index >= start && index < end);\n}\n\nfunction preview(text: string, max = 160): string {\n const flat = text.replace(/\\s+/g, \" \").trim();\n return flat.length > max ? `${flat.slice(0, max)}…` : flat;\n}\n\n/**\n * Escapes `$$` delimiters that would make remark-math swallow non-math text.\n * Pure — safe to call on every render / stream chunk.\n */\nexport function guardMathDelimiters(text: string): DelimiterGuardResult {\n if (!text.includes(\"$$\")) return { text, violations: [] };\n\n const ranges = protectedRanges(text);\n\n // Collect `$$` offsets outside code.\n const tokens: number[] = [];\n for (let i = 0; i < text.length - 1; i++) {\n if (text[i] !== \"$\" || text[i + 1] !== \"$\") continue;\n if (!isProtected(i, ranges)) tokens.push(i);\n i++; // never treat the second `$` of a pair as a new opener\n }\n if (tokens.length === 0) return { text, violations: [] };\n\n const violations: DelimiterViolation[] = [];\n const escapeAt: number[] = [];\n\n let j = 0;\n while (j < tokens.length) {\n // Bounded by the loop condition — `tokens[j]` always exists here.\n const open = tokens[j] as number;\n const close = tokens[j + 1];\n\n if (close === undefined) {\n violations.push({\n reason: \"unpaired\",\n index: open,\n spanLength: 0,\n // An unpaired `$$` is inert to remark-math (nothing closes it), so it\n // is reported but NOT escaped — it already renders as literal text.\n preview: preview(text.slice(open, open + 120)),\n });\n break;\n }\n\n const inner = text.slice(open + 2, close);\n if (looksLikeMath(inner)) {\n j += 2;\n continue;\n }\n\n escapeAt.push(open);\n violations.push({\n reason: \"prose-span\",\n index: open,\n spanLength: inner.length,\n preview: preview(inner),\n });\n // Resume at the closing delimiter: it may legitimately open the NEXT span.\n j += 1;\n }\n\n let guarded = text;\n for (const index of [...escapeAt].sort((a, b) => b - a)) {\n guarded = `${guarded.slice(0, index)}${ESCAPED_DOLLARS}${guarded.slice(index + 2)}`;\n }\n\n return { text: guarded, violations };\n}\n\n/** Longest link label we accept before calling it a runaway. */\nconst MAX_LINK_LABEL = 200;\n\n/**\n * Block structure that can never legitimately sit inside a link label:\n * a blank line, a list item on its own line, or an ATX heading marker\n * (`## `…`#### `) anywhere — a heading inside a label always means the label\n * ran past its intended end.\n */\nconst LABEL_BLOCK_STRUCTURE =\n /\\n[ \\t]*\\n|(?:^|\\n)[ \\t]*[-*+][ \\t]+|#{2,6}[ \\t]/;\n\n/**\n * Escapes the `[` of a markdown link whose label ran away — the link twin of\n * the stray-`$$` bug. An unclosed citation bracket pairs with a `]` hundreds of\n * characters later and turns an entire section into one hyperlink.\n *\n * Pure. Runs after the math guard so both share one escaping pass conceptually,\n * but each is independently usable.\n */\nexport function guardRunawayLinks(text: string): DelimiterGuardResult {\n if (!text.includes(\"[\")) return { text, violations: [] };\n\n const ranges = protectedRanges(text);\n const violations: DelimiterViolation[] = [];\n const escapeAt: number[] = [];\n\n // `[label](target)` — label is non-greedy but may span newlines, which is\n // exactly the runaway shape we are looking for.\n const linkRe = /\\[((?:[^[\\]]|\\\\.)*)\\]\\(([^\\s)]*)/g;\n let m: RegExpExecArray | null;\n while ((m = linkRe.exec(text)) !== null) {\n const open = m.index;\n if (isProtected(open, ranges)) continue;\n\n const label = m[1] ?? \"\";\n const runaway =\n label.length > MAX_LINK_LABEL || LABEL_BLOCK_STRUCTURE.test(label);\n if (!runaway) continue;\n\n escapeAt.push(open);\n violations.push({\n reason: \"runaway-link\",\n index: open,\n spanLength: label.length,\n preview: preview(label),\n });\n }\n\n let guarded = text;\n for (const index of [...escapeAt].sort((a, b) => b - a)) {\n guarded = `${guarded.slice(0, index)}${ESCAPED_BRACKET}${guarded.slice(index + 1)}`;\n }\n\n return { text: guarded, violations };\n}\n\n/**\n * The front door: run every delimiter guard in order. Offsets in the returned\n * violations refer to each guard's own input, so they are for diagnostics only.\n */\nexport function guardMarkdownDelimiters(text: string): DelimiterGuardResult {\n const math = guardMathDelimiters(text);\n const links = guardRunawayLinks(math.text);\n return {\n text: links.text,\n violations: [...math.violations, ...links.violations],\n };\n}\n\n/**\n * The payload handed to the injected capture sink — exactly the shape the\n * original passed to the Matrx `captureError` store. A Matrx host passes\n * `captureError` straight through; any host can log/report it its own way.\n */\nexport interface DelimiterCaptureInput {\n source: \"markdown-delimiters\";\n message: string;\n relation: string;\n details: string;\n conversationId?: string | undefined;\n callSite: \"guardMarkdownDelimiters\";\n raw: { messageId?: string | undefined; violations: DelimiterViolation[] };\n}\n\nexport interface DelimiterReportContext {\n renderPath: string;\n messageId?: string | undefined;\n conversationId?: string | undefined;\n /**\n * Optional error-capture sink (a Matrx host passes its `captureError`).\n * Absent, the loud recovery is console-only. Must never be relied on to\n * throw — failures inside it are swallowed so capture can never break\n * rendering.\n */\n capture?: ((input: DelimiterCaptureInput) => void) | undefined;\n}\n\n/**\n * Loud recovery. A firing means malformed math delimiters reached the renderer\n * — the guard kept the message readable, but the producer is still emitting\n * broken content and must be found.\n */\nexport function reportDelimiterViolations(\n violations: DelimiterViolation[],\n context: DelimiterReportContext,\n): void {\n if (violations.length === 0) return;\n try {\n const worst =\n violations.find((v) => v.reason !== \"unpaired\") ?? violations[0];\n if (!worst) return;\n const message =\n worst.reason === \"prose-span\"\n ? `Malformed math delimiters: a stray \"$$\" would have turned ${worst.spanLength} chars of prose into a math span (KaTeX would render it as red error text). Escaped it.`\n : worst.reason === \"runaway-link\"\n ? `Runaway markdown link: an unclosed \"[\" would have turned ${worst.spanLength} chars into one link label. Escaped it.`\n : `Malformed math delimiters: an unpaired \"$$\" reached the renderer.`;\n\n // Loud recovery: this is a defect being reported, not noise.\n console.warn(`[markdown-delimiter-guard] ${message}`, {\n renderPath: context.renderPath,\n violations,\n });\n\n context.capture?.({\n source: \"markdown-delimiters\",\n message,\n relation: `markdown:${context.renderPath}`,\n details: worst.preview,\n conversationId: context.conversationId,\n callSite: \"guardMarkdownDelimiters\",\n raw: { messageId: context.messageId, violations },\n });\n } catch {\n // Capture must never break rendering.\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,223 @@
1
+ "use client";
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/drafts.ts
22
+ var drafts_exports = {};
23
+ __export(drafts_exports, {
24
+ captureDrafts: () => captureDrafts,
25
+ discardDraft: () => discardDraft,
26
+ getDraft: () => getDraft,
27
+ getDraftsVersion: () => getDraftsVersion,
28
+ listDrafts: () => listDrafts,
29
+ registerDraftSource: () => registerDraftSource,
30
+ subscribeDrafts: () => subscribeDrafts,
31
+ useDurableDraft: () => useDurableDraft
32
+ });
33
+ module.exports = __toCommonJS(drafts_exports);
34
+
35
+ // src/drafts/use-durable-draft.ts
36
+ var import_react = require("react");
37
+ var PREFIX = "matrx:durable-draft:";
38
+ var warnedStorageUnavailable = false;
39
+ function storageWrite(storageKey, value) {
40
+ try {
41
+ if (value) window.localStorage.setItem(storageKey, value);
42
+ else window.localStorage.removeItem(storageKey);
43
+ } catch (err) {
44
+ if (!warnedStorageUnavailable) {
45
+ warnedStorageUnavailable = true;
46
+ console.warn(
47
+ "[useDurableDraft] localStorage unavailable \u2014 drafts survive only in memory this session",
48
+ err
49
+ );
50
+ }
51
+ }
52
+ }
53
+ function useDurableDraft(key) {
54
+ const storageKey = PREFIX + key;
55
+ const [draft, setDraftState] = (0, import_react.useState)("");
56
+ const touchedKeyRef = (0, import_react.useRef)(null);
57
+ (0, import_react.useEffect)(() => {
58
+ let saved = null;
59
+ try {
60
+ saved = window.localStorage.getItem(storageKey);
61
+ } catch {
62
+ }
63
+ setDraftState(
64
+ (current) => touchedKeyRef.current === storageKey && current ? current : saved ?? ""
65
+ );
66
+ }, [storageKey]);
67
+ const setDraft = (value) => {
68
+ touchedKeyRef.current = storageKey;
69
+ setDraftState(value);
70
+ storageWrite(storageKey, value);
71
+ };
72
+ const clearDraft = () => {
73
+ touchedKeyRef.current = storageKey;
74
+ setDraftState("");
75
+ storageWrite(storageKey, "");
76
+ };
77
+ return { draft, setDraft, clearDraft };
78
+ }
79
+
80
+ // src/drafts/local-drafts.ts
81
+ var STORAGE_KEY = "matrx.local-drafts.v1";
82
+ var DRAFT_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
83
+ var MAX_DRAFT_CHARS = 4e5;
84
+ var MAX_TOTAL_CHARS = 15e5;
85
+ var TRUNCATION_MARKER = "\n\n[\u2026 truncated by the local draft store \u2014 the note was too large to snapshot in full]";
86
+ var sources = /* @__PURE__ */ new Map();
87
+ var unloadListenerAttached = false;
88
+ var listeners = /* @__PURE__ */ new Set();
89
+ var version = 0;
90
+ function subscribeDrafts(listener) {
91
+ listeners.add(listener);
92
+ return () => {
93
+ listeners.delete(listener);
94
+ };
95
+ }
96
+ function getDraftsVersion() {
97
+ return version;
98
+ }
99
+ function emitDraftsChanged() {
100
+ version += 1;
101
+ for (const listener of listeners) {
102
+ try {
103
+ listener();
104
+ } catch (err) {
105
+ console.error("[LocalDrafts] subscriber threw:", err);
106
+ }
107
+ }
108
+ }
109
+ function registerDraftSource(id, collect) {
110
+ sources.set(id, collect);
111
+ attachUnloadListener();
112
+ return () => {
113
+ if (sources.get(id) === collect) sources.delete(id);
114
+ };
115
+ }
116
+ function attachUnloadListener() {
117
+ if (unloadListenerAttached || typeof window === "undefined") return;
118
+ unloadListenerAttached = true;
119
+ window.addEventListener("pagehide", () => {
120
+ captureDrafts("unload");
121
+ });
122
+ }
123
+ function captureDrafts(reason) {
124
+ if (typeof window === "undefined") return [];
125
+ const collected = [];
126
+ for (const [id, collect] of sources) {
127
+ try {
128
+ collected.push(...collect());
129
+ } catch (err) {
130
+ console.error("[LocalDrafts] draft source failed:", id, err);
131
+ }
132
+ }
133
+ if (collected.length === 0) return [];
134
+ const now = Date.now();
135
+ const written = collected.map((input) => ({
136
+ ...input,
137
+ content: input.content.length > MAX_DRAFT_CHARS ? input.content.slice(0, MAX_DRAFT_CHARS) + TRUNCATION_MARKER : input.content,
138
+ key: draftKey(input.namespace, input.entityId),
139
+ capturedAt: now,
140
+ reason
141
+ }));
142
+ const existing = readAll().filter(
143
+ (d) => !written.some((w) => w.key === d.key)
144
+ );
145
+ writeAll([...written, ...existing]);
146
+ console.warn(
147
+ `[LocalDrafts] snapshotted ${written.length} unsaved item(s) to this browser (reason: ${reason}).`,
148
+ written.map((d) => `${d.key} (${d.content.length} chars)`)
149
+ );
150
+ return written;
151
+ }
152
+ function listDrafts(namespace, ownerId) {
153
+ if (!ownerId) return [];
154
+ return readAll().filter((d) => d.namespace === namespace && d.ownerId === ownerId).sort((a, b) => b.capturedAt - a.capturedAt);
155
+ }
156
+ function getDraft(namespace, entityId, ownerId) {
157
+ if (!ownerId) return null;
158
+ const key = draftKey(namespace, entityId);
159
+ return readAll().find((d) => d.key === key && d.ownerId === ownerId) ?? null;
160
+ }
161
+ function discardDraft(namespace, entityId) {
162
+ const key = draftKey(namespace, entityId);
163
+ const all = readAll();
164
+ const next = all.filter((d) => d.key !== key);
165
+ if (next.length !== all.length) writeAll(next);
166
+ }
167
+ function draftKey(namespace, entityId) {
168
+ return `${namespace}:${entityId}`;
169
+ }
170
+ function readAll() {
171
+ if (typeof window === "undefined") return [];
172
+ let raw = null;
173
+ try {
174
+ raw = window.localStorage.getItem(STORAGE_KEY);
175
+ } catch {
176
+ return [];
177
+ }
178
+ if (!raw) return [];
179
+ try {
180
+ const parsed = JSON.parse(raw);
181
+ if (!Array.isArray(parsed)) return [];
182
+ const cutoff = Date.now() - DRAFT_TTL_MS;
183
+ return parsed.filter(isLocalDraft).filter((d) => d.capturedAt >= cutoff);
184
+ } catch {
185
+ return [];
186
+ }
187
+ }
188
+ function writeAll(drafts) {
189
+ if (typeof window === "undefined") return;
190
+ const cutoff = Date.now() - DRAFT_TTL_MS;
191
+ const fresh = drafts.filter((d) => d.capturedAt >= cutoff).sort((a, b) => b.capturedAt - a.capturedAt);
192
+ const kept = [];
193
+ let total = 0;
194
+ for (const draft of fresh) {
195
+ if (total + draft.content.length > MAX_TOTAL_CHARS && kept.length > 0) {
196
+ console.warn(
197
+ "[LocalDrafts] draft budget exhausted \u2014 dropping older draft",
198
+ draft.key
199
+ );
200
+ continue;
201
+ }
202
+ kept.push(draft);
203
+ total += draft.content.length;
204
+ }
205
+ try {
206
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(kept));
207
+ } catch (err) {
208
+ console.error("[LocalDrafts] failed to persist drafts:", err);
209
+ if (kept.length > 1) {
210
+ try {
211
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify([kept[0]]));
212
+ } catch {
213
+ }
214
+ }
215
+ }
216
+ emitDraftsChanged();
217
+ }
218
+ function isLocalDraft(value) {
219
+ if (typeof value !== "object" || value === null) return false;
220
+ const d = value;
221
+ return typeof d.key === "string" && typeof d.namespace === "string" && typeof d.entityId === "string" && typeof d.content === "string" && typeof d.capturedAt === "number";
222
+ }
223
+ //# sourceMappingURL=drafts.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/drafts.ts","../src/drafts/use-durable-draft.ts","../src/drafts/local-drafts.ts"],"sourcesContent":["/**\n * @ai-matrx/kit/drafts — user-authored text that MUST survive anything.\n *\n * Two tiers, both localStorage-backed, both write-through:\n * - `useDurableDraft` — the one-string composer draft: restore on mount,\n * key-change adoption, cleared only after the content durably landed.\n * - the local-drafts store — the richer multi-draft register (typed source,\n * timestamps, listing) for surfaces that manage several drafts.\n */\nexport { useDurableDraft } from \"./drafts/use-durable-draft\";\nexport * from \"./drafts/local-drafts\";\nexport * from \"./drafts/types\";\n","\"use client\";\n\n// useDurableDraft — user-authored text that MUST survive anything.\n//\n// A composer draft held only in React state dies with the tab: a mobile\n// Safari reload, a crash, an error storm, a mis-tap on Back — and the user's\n// two-minute dictated rant is gone. Losing it once costs all trust\n// (Arman's ruling, 2026-08-16, after exactly that happened in the Vision\n// Interview room).\n//\n// This hook is write-through: every change lands in localStorage\n// synchronously, restore happens on mount, and the draft is removed ONLY\n// via clearDraft() — which callers may invoke only after the content has\n// durably landed somewhere the user can see (a DB row, a rendered turn).\n// A send that fails keeps the draft by construction.\n//\n// Storage failure (private mode, quota) never breaks typing — state still\n// works; the failure is logged loudly once so the degraded durability is\n// visible, not silent.\n\nimport { useEffect, useRef, useState } from \"react\";\n\nconst PREFIX = \"matrx:durable-draft:\";\n\nlet warnedStorageUnavailable = false;\n\nfunction storageWrite(storageKey: string, value: string): void {\n try {\n if (value) window.localStorage.setItem(storageKey, value);\n else window.localStorage.removeItem(storageKey);\n } catch (err) {\n if (!warnedStorageUnavailable) {\n warnedStorageUnavailable = true;\n console.warn(\n \"[useDurableDraft] localStorage unavailable — drafts survive only in memory this session\",\n err,\n );\n }\n }\n}\n\nexport function useDurableDraft(key: string): {\n draft: string;\n setDraft: (value: string) => void;\n clearDraft: () => void;\n} {\n const storageKey = PREFIX + key;\n const [draft, setDraftState] = useState(\"\");\n // Which key the user's live keystrokes belong to. Their typing beats a\n // stale saved copy ONLY for the same key — a key CHANGE always adopts the\n // new key's saved value (or empty), so a swapped entity id can never show\n // or send the previous entity's text.\n const touchedKeyRef = useRef<string | null>(null);\n\n useEffect(() => {\n // Restore runs on mount AND on every key change (localStorage is\n // unavailable during SSR, hence effect not render).\n let saved: string | null = null;\n try {\n saved = window.localStorage.getItem(storageKey);\n } catch {\n // Restore is best-effort; the write path warns once (above).\n }\n setDraftState((current) =>\n touchedKeyRef.current === storageKey && current ? current : (saved ?? \"\"),\n );\n }, [storageKey]);\n\n const setDraft = (value: string) => {\n touchedKeyRef.current = storageKey;\n setDraftState(value);\n storageWrite(storageKey, value);\n };\n\n const clearDraft = () => {\n touchedKeyRef.current = storageKey;\n setDraftState(\"\");\n storageWrite(storageKey, \"\");\n };\n\n return { draft, setDraft, clearDraft };\n}\n","// lib/local-drafts/localDrafts.ts\n//\n// THE LAST-RESORT COPY of unsaved in-memory work, in this browser.\n//\n// Nothing here is a persistence path — every feature still owns its real save.\n// This exists for the moment the app is about to LOSE in-memory edits and has\n// no way to persist them: the tab is being hard-stopped (auth identity drift),\n// the page is unloading, or a feature's saves have been failing so long that\n// the buffer is the only copy that exists. Snapshot first, block second.\n//\n// Written because of D132 (2026-08-08): a domain-wide auth cookie rotated\n// under an open /notes tab, ~14h of autosaves were RLS-filtered to 0 rows, and\n// the \"Account Changed\" overlay then forced a reload that threw the in-memory\n// buffer away. One note never reached the DB at all and is unrecoverable.\n//\n// Rules:\n// - A draft is offered back ONLY to the same `ownerId` that wrote it.\n// - Storage is best-effort: quota errors, private mode, and disabled storage\n// degrade to \"no draft\", never to a thrown error on a save path.\n// - Drafts expire (7 days) and are capped, oldest-first, so this can never\n// grow into a shadow database.\n\nimport type { DraftSource, LocalDraft, LocalDraftInput } from \"./types\";\n\nconst STORAGE_KEY = \"matrx.local-drafts.v1\";\n\n/** Drafts older than this are dropped on the next read/write. */\nconst DRAFT_TTL_MS = 7 * 24 * 60 * 60 * 1000;\n/** A single draft larger than this is stored truncated (with a marker). */\nconst MAX_DRAFT_CHARS = 400_000;\n/** Total budget across all drafts; the oldest are dropped to fit. */\nconst MAX_TOTAL_CHARS = 1_500_000;\n\nconst TRUNCATION_MARKER =\n \"\\n\\n[… truncated by the local draft store — the note was too large to snapshot in full]\";\n\n// ── Sources ────────────────────────────────────────────────────────────────\n\nconst sources = new Map<string, DraftSource>();\nlet unloadListenerAttached = false;\n\n// ── Subscription ───────────────────────────────────────────────────────────\n//\n// A capture can happen while a recovery UI is already on screen (a save-failure\n// escalation for a note that is not the open tab). Without a notification that\n// strip would sit empty until a remount — i.e. the rescue exists and the user\n// is never told. `version` bumps on every write.\n\nconst listeners = new Set<() => void>();\nlet version = 0;\n\n/** Subscribe to draft-store writes. Pair with `getDraftsVersion` for `useSyncExternalStore`. */\nexport function subscribeDrafts(listener: () => void): () => void {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n}\n\nexport function getDraftsVersion(): number {\n return version;\n}\n\nfunction emitDraftsChanged(): void {\n version += 1;\n for (const listener of listeners) {\n try {\n listener();\n } catch (err) {\n console.error(\"[LocalDrafts] subscriber threw:\", err);\n }\n }\n}\n\n/**\n * Register a collector for one feature's unsaved work. Re-registering the same\n * id replaces the previous collector (remounts are safe); the returned\n * unregister only removes the entry if it is still the one it installed.\n */\nexport function registerDraftSource(id: string, collect: DraftSource): () => void {\n sources.set(id, collect);\n attachUnloadListener();\n return () => {\n if (sources.get(id) === collect) sources.delete(id);\n };\n}\n\nfunction attachUnloadListener(): void {\n if (unloadListenerAttached || typeof window === \"undefined\") return;\n unloadListenerAttached = true;\n // `pagehide` fires in cases `beforeunload` does not (bfcache, mobile Safari).\n window.addEventListener(\"pagehide\", () => {\n captureDrafts(\"unload\");\n });\n}\n\n// ── Capture ────────────────────────────────────────────────────────────────\n\n/**\n * Walk every registered source and persist what they hand back.\n * Returns the drafts written (empty when nothing is unsaved).\n *\n * Call this BEFORE anything that discards in-memory state — a forced reload,\n * a blocking overlay, a hard sign-out.\n */\nexport function captureDrafts(reason: string): LocalDraft[] {\n if (typeof window === \"undefined\") return [];\n\n const collected: LocalDraftInput[] = [];\n for (const [id, collect] of sources) {\n try {\n collected.push(...collect());\n } catch (err) {\n console.error(\"[LocalDrafts] draft source failed:\", id, err);\n }\n }\n if (collected.length === 0) return [];\n\n const now = Date.now();\n const written: LocalDraft[] = collected.map((input) => ({\n ...input,\n content:\n input.content.length > MAX_DRAFT_CHARS\n ? input.content.slice(0, MAX_DRAFT_CHARS) + TRUNCATION_MARKER\n : input.content,\n key: draftKey(input.namespace, input.entityId),\n capturedAt: now,\n reason,\n }));\n\n const existing = readAll().filter(\n (d) => !written.some((w) => w.key === d.key),\n );\n writeAll([...written, ...existing]);\n\n console.warn(\n `[LocalDrafts] snapshotted ${written.length} unsaved item(s) to this browser (reason: ${reason}).`,\n written.map((d) => `${d.key} (${d.content.length} chars)`),\n );\n return written;\n}\n\n// ── Read / discard ─────────────────────────────────────────────────────────\n\n/** Every live draft in a namespace that belongs to `ownerId`, newest first. */\nexport function listDrafts(namespace: string, ownerId: string | null): LocalDraft[] {\n if (!ownerId) return [];\n return readAll()\n .filter((d) => d.namespace === namespace && d.ownerId === ownerId)\n .sort((a, b) => b.capturedAt - a.capturedAt);\n}\n\n/** The draft for one entity, if it belongs to `ownerId`. */\nexport function getDraft(\n namespace: string,\n entityId: string,\n ownerId: string | null,\n): LocalDraft | null {\n if (!ownerId) return null;\n const key = draftKey(namespace, entityId);\n return (\n readAll().find((d) => d.key === key && d.ownerId === ownerId) ?? null\n );\n}\n\n/** Drop one draft (restored, discarded by the user, or its entity saved). */\nexport function discardDraft(namespace: string, entityId: string): void {\n const key = draftKey(namespace, entityId);\n const all = readAll();\n const next = all.filter((d) => d.key !== key);\n if (next.length !== all.length) writeAll(next); // writeAll emits\n}\n\n// ── Storage ────────────────────────────────────────────────────────────────\n\nfunction draftKey(namespace: string, entityId: string): string {\n return `${namespace}:${entityId}`;\n}\n\nfunction readAll(): LocalDraft[] {\n if (typeof window === \"undefined\") return [];\n let raw: string | null = null;\n try {\n raw = window.localStorage.getItem(STORAGE_KEY);\n } catch {\n return []; // storage disabled / private mode — no drafts, never a throw\n }\n if (!raw) return [];\n try {\n const parsed: unknown = JSON.parse(raw);\n if (!Array.isArray(parsed)) return [];\n const cutoff = Date.now() - DRAFT_TTL_MS;\n return parsed.filter(isLocalDraft).filter((d) => d.capturedAt >= cutoff);\n } catch {\n return [];\n }\n}\n\nfunction writeAll(drafts: LocalDraft[]): void {\n if (typeof window === \"undefined\") return;\n const cutoff = Date.now() - DRAFT_TTL_MS;\n const fresh = drafts\n .filter((d) => d.capturedAt >= cutoff)\n .sort((a, b) => b.capturedAt - a.capturedAt);\n\n // Newest-first budget: keep taking drafts until the char budget runs out.\n const kept: LocalDraft[] = [];\n let total = 0;\n for (const draft of fresh) {\n if (total + draft.content.length > MAX_TOTAL_CHARS && kept.length > 0) {\n console.warn(\n \"[LocalDrafts] draft budget exhausted — dropping older draft\",\n draft.key,\n );\n continue;\n }\n kept.push(draft);\n total += draft.content.length;\n }\n\n try {\n window.localStorage.setItem(STORAGE_KEY, JSON.stringify(kept));\n } catch (err) {\n // Quota exceeded: retry with only the newest draft before giving up —\n // one recovered note beats zero.\n console.error(\"[LocalDrafts] failed to persist drafts:\", err);\n if (kept.length > 1) {\n try {\n window.localStorage.setItem(STORAGE_KEY, JSON.stringify([kept[0]]));\n } catch {\n /* storage is unusable — nothing more we can do here */\n }\n }\n }\n\n emitDraftsChanged();\n}\n\nfunction isLocalDraft(value: unknown): value is LocalDraft {\n if (typeof value !== \"object\" || value === null) return false;\n const d = value as Record<string, unknown>;\n return (\n typeof d.key === \"string\" &&\n typeof d.namespace === \"string\" &&\n typeof d.entityId === \"string\" &&\n typeof d.content === \"string\" &&\n typeof d.capturedAt === \"number\"\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,62 @@
1
+ declare function useDurableDraft(key: string): {
2
+ draft: string;
3
+ setDraft: (value: string) => void;
4
+ clearDraft: () => void;
5
+ };
6
+
7
+ /** What a draft source hands over for one unsaved entity. */
8
+ interface LocalDraftInput {
9
+ /** Entity family — "note", "task", … One namespace per feature. */
10
+ namespace: string;
11
+ /** The entity's id (a client-minted id is fine — it may have no server row). */
12
+ entityId: string;
13
+ /**
14
+ * The user id the edits were made AS. A draft is only ever offered back to
15
+ * the same user: on the 2026-08-08 identity-drift incident the tab's edits
16
+ * belonged to a different account than the one that reloaded, and restoring
17
+ * across that boundary would leak one account's text into another's note.
18
+ */
19
+ ownerId: string | null;
20
+ /** Human label for the recovery UI ("Meeting notes"). */
21
+ label?: string | null;
22
+ /** The unsaved text itself. */
23
+ content: string;
24
+ /** True when the entity has NO server row yet (its first insert never landed). */
25
+ isNew?: boolean;
26
+ }
27
+ interface LocalDraft extends LocalDraftInput {
28
+ /** `${namespace}:${entityId}` — the storage key. */
29
+ key: string;
30
+ /** Epoch ms this snapshot was taken. */
31
+ capturedAt: number;
32
+ /** Why it was taken ("auth-identity-drift", "note-save-failures", "unload"). */
33
+ reason: string;
34
+ }
35
+ /** A feature registers one collector; it returns everything currently unsaved. */
36
+ type DraftSource = () => LocalDraftInput[];
37
+
38
+ /** Subscribe to draft-store writes. Pair with `getDraftsVersion` for `useSyncExternalStore`. */
39
+ declare function subscribeDrafts(listener: () => void): () => void;
40
+ declare function getDraftsVersion(): number;
41
+ /**
42
+ * Register a collector for one feature's unsaved work. Re-registering the same
43
+ * id replaces the previous collector (remounts are safe); the returned
44
+ * unregister only removes the entry if it is still the one it installed.
45
+ */
46
+ declare function registerDraftSource(id: string, collect: DraftSource): () => void;
47
+ /**
48
+ * Walk every registered source and persist what they hand back.
49
+ * Returns the drafts written (empty when nothing is unsaved).
50
+ *
51
+ * Call this BEFORE anything that discards in-memory state — a forced reload,
52
+ * a blocking overlay, a hard sign-out.
53
+ */
54
+ declare function captureDrafts(reason: string): LocalDraft[];
55
+ /** Every live draft in a namespace that belongs to `ownerId`, newest first. */
56
+ declare function listDrafts(namespace: string, ownerId: string | null): LocalDraft[];
57
+ /** The draft for one entity, if it belongs to `ownerId`. */
58
+ declare function getDraft(namespace: string, entityId: string, ownerId: string | null): LocalDraft | null;
59
+ /** Drop one draft (restored, discarded by the user, or its entity saved). */
60
+ declare function discardDraft(namespace: string, entityId: string): void;
61
+
62
+ export { type DraftSource, type LocalDraft, type LocalDraftInput, captureDrafts, discardDraft, getDraft, getDraftsVersion, listDrafts, registerDraftSource, subscribeDrafts, useDurableDraft };
@@ -0,0 +1,62 @@
1
+ declare function useDurableDraft(key: string): {
2
+ draft: string;
3
+ setDraft: (value: string) => void;
4
+ clearDraft: () => void;
5
+ };
6
+
7
+ /** What a draft source hands over for one unsaved entity. */
8
+ interface LocalDraftInput {
9
+ /** Entity family — "note", "task", … One namespace per feature. */
10
+ namespace: string;
11
+ /** The entity's id (a client-minted id is fine — it may have no server row). */
12
+ entityId: string;
13
+ /**
14
+ * The user id the edits were made AS. A draft is only ever offered back to
15
+ * the same user: on the 2026-08-08 identity-drift incident the tab's edits
16
+ * belonged to a different account than the one that reloaded, and restoring
17
+ * across that boundary would leak one account's text into another's note.
18
+ */
19
+ ownerId: string | null;
20
+ /** Human label for the recovery UI ("Meeting notes"). */
21
+ label?: string | null;
22
+ /** The unsaved text itself. */
23
+ content: string;
24
+ /** True when the entity has NO server row yet (its first insert never landed). */
25
+ isNew?: boolean;
26
+ }
27
+ interface LocalDraft extends LocalDraftInput {
28
+ /** `${namespace}:${entityId}` — the storage key. */
29
+ key: string;
30
+ /** Epoch ms this snapshot was taken. */
31
+ capturedAt: number;
32
+ /** Why it was taken ("auth-identity-drift", "note-save-failures", "unload"). */
33
+ reason: string;
34
+ }
35
+ /** A feature registers one collector; it returns everything currently unsaved. */
36
+ type DraftSource = () => LocalDraftInput[];
37
+
38
+ /** Subscribe to draft-store writes. Pair with `getDraftsVersion` for `useSyncExternalStore`. */
39
+ declare function subscribeDrafts(listener: () => void): () => void;
40
+ declare function getDraftsVersion(): number;
41
+ /**
42
+ * Register a collector for one feature's unsaved work. Re-registering the same
43
+ * id replaces the previous collector (remounts are safe); the returned
44
+ * unregister only removes the entry if it is still the one it installed.
45
+ */
46
+ declare function registerDraftSource(id: string, collect: DraftSource): () => void;
47
+ /**
48
+ * Walk every registered source and persist what they hand back.
49
+ * Returns the drafts written (empty when nothing is unsaved).
50
+ *
51
+ * Call this BEFORE anything that discards in-memory state — a forced reload,
52
+ * a blocking overlay, a hard sign-out.
53
+ */
54
+ declare function captureDrafts(reason: string): LocalDraft[];
55
+ /** Every live draft in a namespace that belongs to `ownerId`, newest first. */
56
+ declare function listDrafts(namespace: string, ownerId: string | null): LocalDraft[];
57
+ /** The draft for one entity, if it belongs to `ownerId`. */
58
+ declare function getDraft(namespace: string, entityId: string, ownerId: string | null): LocalDraft | null;
59
+ /** Drop one draft (restored, discarded by the user, or its entity saved). */
60
+ declare function discardDraft(namespace: string, entityId: string): void;
61
+
62
+ export { type DraftSource, type LocalDraft, type LocalDraftInput, captureDrafts, discardDraft, getDraft, getDraftsVersion, listDrafts, registerDraftSource, subscribeDrafts, useDurableDraft };