@ai-matrx/messaging 0.3.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,91 @@
1
1
  # Changelog — `@ai-matrx/messaging`
2
2
 
3
+ ## 0.6.0 — 2026-09-07
4
+
5
+ **The two entries did not agree on their own branded ids.** A consumer that minted a
6
+ `ConversationId` with `asConversationId` from `@ai-matrx/messaging` could not pass it to a hook
7
+ from `@ai-matrx/messaging/react` — the two were different types, with an error message that
8
+ blames the consumer. Found by the first real adoption, which does exactly that on its
9
+ conversation route.
10
+
11
+ ### Fixed
12
+
13
+ - **The nominal brand is a named marker, not a `unique symbol`.** The package ships two entries
14
+ built as two bundles, so every shared type is DECLARED TWICE in the shipped `.d.ts` — and two
15
+ `declare const brand: unique symbol` declarations are two different symbols, hence two
16
+ incompatible `ConversationId`s. A structural marker (`__mxBrand`) is identical in both
17
+ declarations, so the brands unify while still being impossible to produce by accident. No
18
+ runtime change: the marker is types-only.
19
+ - **`verify-tarball` now typechecks a real consumer against BOTH entries** — core-minted ids
20
+ assigned to `/react` types and back. It fails against the `unique symbol` build, which is the
21
+ only way this class can be caught: in `src/` there is only ever one declaration, so every
22
+ source gate stays green while the shipped types are broken.
23
+ - The canary still imported `getInitials` / `avatarPaletteIndex`, which moved to
24
+ `@ai-matrx/kit/format` in 0.5.0 — it would have failed that release's publish.
25
+
26
+ ### Consumer action (C28)
27
+
28
+ None. `asConversationId(...)` and friends behave exactly as before; ids from the two entries now
29
+ actually interoperate, which is what the types always claimed.
30
+
31
+ ## 0.5.0 — 2026-09-07
32
+
33
+ **Consumer action (C28).** Two exports MOVED OUT of this package to
34
+ `@ai-matrx/kit/format`. Update the import; the behaviour is identical.
35
+
36
+ | was | now |
37
+ |---|---|
38
+ | `import { getInitials } from "@ai-matrx/messaging"` | `import { getInitials } from "@ai-matrx/kit/format"` |
39
+ | `import { avatarPaletteIndex } from "@ai-matrx/messaging"` | `import { avatarPaletteIndex } from "@ai-matrx/kit/format"` |
40
+
41
+ **Why.** Duplication census finding H1 (2026-09-07): both bodies were
42
+ duplicated in `@ai-matrx/meet`, and while two packages claimed them no host
43
+ twin had a correct owner to point at. They now live in `@ai-matrx/kit`, which
44
+ has zero sibling dependencies.
45
+
46
+ The conversation-shaped formatters — `formatConversationTime`,
47
+ `formatMessageTime`, `formatDateSeparator`, `formatLastSeen`, `formatTypists`,
48
+ `groupMessages`, `isSameDay`, `participantNames` — all stay here. Nothing is
49
+ re-exported: this package imports kit directly.
50
+
51
+ New dependency: `@ai-matrx/kit` (`latest`).
52
+
53
+ ## 0.4.0 — 2026-09-07
54
+
55
+ **A ```matrx fence written in the PLATFORM's dialect was being deleted on render.** Found the
56
+ moment the first consumer's real messages went through this package's bubble. Third and last
57
+ release of the adoption session.
58
+
59
+ ### Fixed
60
+
61
+ - **A fence this package cannot parse is never dropped.** `splitText` advanced past an
62
+ unrecognized fence and emitted nothing for it, so a message that named a note or a task lost
63
+ that paragraph between the sender and the reader — silently, with the surrounding prose intact
64
+ so nothing looked wrong. This package understands exactly one fence dialect (its own array of
65
+ `{entityType, entityId}`); Matrx's real fences are two-key `__kind` directive shells whose
66
+ items are typed per noun and resolvable only by the app's kind registry, i.e. every real
67
+ reference on the platform hit this path.
68
+ - **The regression test that should have caught it was hollow** — it asserted
69
+ `segments.every(s => s.type === "text")` on what was, after the drop, an EMPTY array. Replaced
70
+ with one that asserts the fence survives, and one for the platform's own dialect.
71
+ - **An unreadable fence collapses to the label `Reference` in previews and notifications**, never
72
+ to nothing. Collapsing to nothing is how a reference-only message becomes an inbox row reading
73
+ "No messages yet".
74
+
75
+ ### Added
76
+
77
+ - **`renderFence`** — the host draws ```matrx fences with its own renderer, every fence, parsed
78
+ or not. A platform's fence dialect is the platform's business.
79
+ - `TextSegment` gains `{ type: "fence", body }`. With no host renderer the bubble draws an inert
80
+ card labeled "Reference" whose title says what is missing and how to wire it — drawn, never
81
+ deleted, never a code block of JSON.
82
+
83
+ ### Consumer action (C28)
84
+
85
+ - **A host on the Matrx reference-fence protocol MUST pass `renderFence`** or its references
86
+ render as the generic inert card. matrx-frontend passes `MatrxEnvelopeBlock`.
87
+ - Code that switches on `TextSegment.type` gains a third case. TypeScript will point at it.
88
+
3
89
  ## 0.3.0 — 2026-09-07
4
90
 
5
91
  **Three more seams the first adoption needed** — each one a case where the host
package/README.md CHANGED
@@ -262,6 +262,11 @@ its own registry, hands those in rather than rebuilding the thread:
262
262
  package's card — deliberately, because an app that draws references everywhere else must not
263
263
  draw them two ways.
264
264
 
265
+ **If your platform has its own ```matrx fence dialect, pass `renderFence` too.** This package
266
+ parses one shape (an array of `{entityType, entityId}`); anything else reaches your renderer
267
+ verbatim as the fence body. Without it those fences draw an honest inert "Reference" card — they
268
+ are never deleted, and never shown to a reader as JSON.
269
+
265
270
  ### Theming
266
271
 
267
272
  Structural CSS ships in the package; the token **contract** is enforced; token **values** are
package/dist/index.cjs CHANGED
@@ -30,7 +30,6 @@ __export(src_exports, {
30
30
  asMessageId: () => asMessageId,
31
31
  asOrganizationId: () => asOrganizationId,
32
32
  asUserId: () => asUserId,
33
- avatarPaletteIndex: () => avatarPaletteIndex,
34
33
  composeFence: () => composeFence,
35
34
  conversationTopic: () => conversationTopic,
36
35
  createActionRegistry: () => createActionRegistry,
@@ -48,7 +47,6 @@ __export(src_exports, {
48
47
  formatLastSeen: () => formatLastSeen,
49
48
  formatMessageTime: () => formatMessageTime,
50
49
  formatTypists: () => formatTypists,
51
- getInitials: () => getInitials,
52
50
  groupMessages: () => groupMessages,
53
51
  inboxTopic: () => inboxTopic,
54
52
  invalidResponse: () => invalidResponse,
@@ -319,21 +317,32 @@ function splitText(content) {
319
317
  if (start > cursor) {
320
318
  segments.push({ type: "text", value: content.slice(cursor, start) });
321
319
  }
322
- parseFenceBody(match[1] ?? "").forEach((reference) => {
323
- segments.push({ type: "reference", reference });
324
- });
320
+ const body = match[1] ?? "";
321
+ const references = parseFenceBody(body);
322
+ if (references.length === 0) {
323
+ segments.push({ type: "fence", body });
324
+ } else {
325
+ references.forEach((reference) => {
326
+ segments.push({ type: "reference", reference });
327
+ });
328
+ }
325
329
  cursor = start + match[0].length;
326
330
  }
327
331
  if (cursor < content.length) {
328
332
  segments.push({ type: "text", value: content.slice(cursor) });
329
333
  }
330
334
  return segments.filter(
331
- (segment) => segment.type === "reference" || segment.value.trim().length > 0
335
+ (segment) => segment.type !== "text" || segment.value.trim().length > 0
332
336
  );
333
337
  }
334
338
  function summarizeText(content, maxLength = 140) {
335
339
  const parts = splitText(content).map(
336
- (segment) => segment.type === "text" ? segment.value : segment.reference.label
340
+ (segment) => segment.type === "text" ? segment.value : segment.type === "reference" ? segment.reference.label : (
341
+ // A fence we cannot read still SAYS something. Collapsing it to
342
+ // nothing is how a reference-only message becomes an inbox row that
343
+ // reads "No messages yet" — a screen telling a lie.
344
+ "Reference"
345
+ )
337
346
  );
338
347
  const flattened = parts.join(" ").replace(/\s+/g, " ").trim();
339
348
  if (flattened.length <= maxLength) return flattened;
@@ -1485,20 +1494,6 @@ function formatDateSeparator(isoString, now = Date.now(), locale) {
1485
1494
  day: "numeric"
1486
1495
  });
1487
1496
  }
1488
- function getInitials(name) {
1489
- const parts = name.trim().split(/\s+/).filter(Boolean);
1490
- if (parts.length === 0) return "?";
1491
- const first = parts[0]?.[0] ?? "";
1492
- const last = parts.length > 1 ? parts.at(-1)?.[0] ?? "" : "";
1493
- return `${first}${last}`.toUpperCase() || "?";
1494
- }
1495
- function avatarPaletteIndex(seed, buckets = 8) {
1496
- let hash = 0;
1497
- for (let index = 0; index < seed.length; index += 1) {
1498
- hash = hash * 31 + seed.charCodeAt(index) | 0;
1499
- }
1500
- return Math.abs(hash) % buckets;
1501
- }
1502
1497
  function groupMessages(messages, args = {}) {
1503
1498
  const windowMs = args.windowMs ?? 5 * MINUTE;
1504
1499
  const now = args.now ?? Date.now();