@ai-matrx/associations 0.5.1 → 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,105 @@
1
1
  # Changelog — @ai-matrx/associations
2
2
 
3
+ ## 0.6.0 — 2026-08-30 (C23 — every port ships a working default; the split entry)
4
+
5
+ THE ALL-INCLUSIVE LAW retrofit (register row: associations). The audit found
6
+ seven ports a host had to fill, four UI ports with NO shipped default, and the
7
+ bundle-split concern pushed onto the host as hand-written `next/dynamic` glue.
8
+ All of it now lives in the package.
9
+
10
+ ### Ports: defaults, not degradations
11
+
12
+ - **`windowShell` → `DefaultWindowShell` (new).** The absent-port path was a
13
+ fixed, non-draggable card. It is now a REAL window: drag by the header,
14
+ resize from a labelled grip, maximize/restore, viewport-clamped on drag and
15
+ on viewport resize (a window can never be dragged somewhere it cannot be
16
+ dragged back from), Escape-to-close, portalled to `<body>` (a transformed
17
+ ancestor can no longer clip it), non-modal, and a full-width bottom card
18
+ under 640px. Exported as `DefaultWindowShell` for direct use.
19
+ - **`errorSink` is no longer REQUIRED.** `createAssociationsStore` ships
20
+ `createDefaultErrorSink()` (new, `/core`): a tagged `console.error` per
21
+ event that ANNOUNCES ONCE that no sink is bound and names the remedy — the
22
+ default is louder about itself than the old hard failure was. Only
23
+ `dataSource` and `identity` still throw; they ARE app identity.
24
+ - **`entityDoors.DoorControls` → `DoorControlsSlot` (new).** Every face now
25
+ renders doors through one slot: the host port when bound, otherwise a real,
26
+ keyboard-reachable, `aria-label`led `target="_blank"` anchor built from the
27
+ registry overlay's `hrefFor`. A token with no registered route renders
28
+ nothing — honest absence, never a control that goes nowhere.
29
+ - **`capture` → the whole upload flow over one injected primitive.** Bind
30
+ `capture.uploadFile(file, { folderPath, visibility })` and the package ships
31
+ everything else: the file input, whole-body drag-and-drop, sequential
32
+ uploads, the stall watchdog, per-file outcomes, and the loud
33
+ created-but-unattached report. `requestUpload` remains for a host that owns
34
+ a richer pipeline and still wins when both are bound.
35
+ - **`pickerOverrides` needs no default** — the in-package `AssociationPicker`
36
+ IS the default for every token, and it is complete (candidate list, search,
37
+ attach/detach, the create-then-associate "+ New" footer).
38
+ - **`notifier` degradation unchanged** (warn-once, failures always reach the
39
+ errorSink). The package deliberately does NOT ship a toast stack: a toast
40
+ surface needs a host-mounted root, so a "default" would be the silent
41
+ no-op this package exists to prevent.
42
+
43
+ ### The bundle split is package-owned
44
+
45
+ - **`@ai-matrx/associations/react/lazy` (new subpath, 3.4 KB)** — every heavy
46
+ face behind a real lazy edge: `AssociationWindow`, `AssociationPicker`,
47
+ `UniversalAssociationPicker`, `AttachedItemsSheet`, `AssociationCard`,
48
+ `AssociationCardGrid`, `AssociationList`, `AssociationCaptureToolbar`,
49
+ `AssociationEntitySelect`, `CategorySelect`, `CategoryTagPicker`,
50
+ `CommentThread`. The loader dynamic-imports this package's own `./react`
51
+ subpath by name (external in our build), so the CONSUMER's bundler emits the
52
+ chunk. Framework-agnostic — `React.lazy` + `Suspense`, no `next/dynamic`,
53
+ no `next` dependency.
54
+ - **`lazyWindowShell(loader)` / `lazyPickerOverride(loader)`** (`/react`) —
55
+ the same seam for a HOST component bound to a port, so a host stops
56
+ hand-writing `next/dynamic({ ssr: false })` glue per binding.
57
+ `lazyPickerOverride` additionally gates on `open`, so an override never
58
+ fetches its chunk until its picker is opened.
59
+
60
+ ### Hard parts moved in (C22)
61
+
62
+ - **The demanded-schema boot probe runs in `AssociationsProvider`** — ON in a
63
+ development build, off elsewhere, overridable with `probeSchema`. Every host
64
+ was writing this effect itself (dynamic import, try/catch, hand-rolled
65
+ `demanded_schema_violation` scream). The store now also exposes the bound
66
+ `dataSource` so nothing has to be re-exported to run it.
67
+ - **`useAssociationPickerBridge(props, opts)` (new, `/react`)** — the
68
+ attach-batch / toggle engine a per-token picker override needs: loud partial
69
+ outcomes, created-but-unattached copy naming where the item lives, refusal
70
+ screams on both attach and detach (the inert-click bug class, feedback
71
+ 35d311a9), and errorSink routing. Overrides now write two calls instead of
72
+ re-implementing failure semantics.
73
+
74
+ ### Consumer action
75
+
76
+ - **Nothing breaks.** `errorSink` moving from required to optional is
77
+ source-compatible; every existing binding keeps working unchanged.
78
+ - **Delete host `next/dynamic` glue** around associations port bindings and
79
+ use `lazyWindowShell` / `lazyPickerOverride` from `@ai-matrx/associations/react`.
80
+ - **Delete the host demanded-schema probe effect** (the `assertDemandedSchema`
81
+ boot check) — the provider owns it. Pass `probeSchema={false}` to opt out.
82
+ - **Delete hand-written attach/detach failure handling** inside picker
83
+ overrides and bind `useAssociationPickerBridge` instead.
84
+ - **A host that binds `windowShell` should re-decide whether it needs to**:
85
+ bind it only for a real window MANAGER (tray docking, workspace
86
+ persistence, cross-window z-order). Otherwise drop the binding and take
87
+ `DefaultWindowShell`.
88
+ - **A host with no upload pipeline** can now get the whole capture flow by
89
+ binding only `capture.uploadFile`.
90
+ - New optional peer `react-dom` (`>=18.0.0`) — the default window portals.
91
+ Any React DOM host already has it.
92
+
93
+ ## 0.5.2 — 2026-08-30 (X3 peer retrofit)
94
+
95
+ - `@ai-matrx/design-system` is now a REAL dependency (`^0.3.1`) instead of an
96
+ optional peer (register row X3 / law C23: sibling `@ai-matrx/*` packages are
97
+ never peers — only react/react-dom/react-native stay peers). Installing this
98
+ package brings the faces' chrome automatically; nothing to resolve by hand.
99
+ The tarball canary no longer installs design-system manually — the graph
100
+ resolves itself from the registry. `react` remains the only (optional) peer.
101
+ No code changes.
102
+
3
103
  ## 0.5.1 — 2026-08-30 (vocabulary patch)
4
104
 
5
105
  - Entity-token vocabulary regenerated from the live registry: **655 tokens**
package/README.md CHANGED
@@ -32,7 +32,7 @@ import {
32
32
  const store = createAssociationsStore({
33
33
  dataSource: supabase, // structural subset of SupabaseClient
34
34
  identity: { requireUserId }, // throws when unauthenticated
35
- errorSink: (e) => report(e), // recovery layers scream here
35
+ errorSink: (e) => report(e), // OPTIONAL default screams to console
36
36
  entityOverlay: { file: { hrefFor: (id) => `/files/f/${id}` } }, // optional
37
37
  });
38
38
 
@@ -43,13 +43,15 @@ store.titles.prime("task", taskId, "Renamed");
43
43
  await store.favorites.setFavorite("note", noteId, true);
44
44
 
45
45
  // Dev/CI boot check for any new environment (zero side effects; selfTest
46
- // proves the probe can fail before you trust its green):
46
+ // proves the probe can fail before you trust its green). You normally never
47
+ // call this: <AssociationsProvider> runs it for you in a development build.
47
48
  await assertDemandedSchema(supabase, { selfTest: true });
48
49
  ```
49
50
 
50
51
  ```tsx
51
- // /react — the banner'd React binding (peer: react >=18 and
52
- // @ai-matrx/design-system >=0.2.0 for the faces' chrome). Bind ONCE:
52
+ // /react — the banner'd React binding (peer: react >=18;
53
+ // @ai-matrx/design-system is a real dependency — the faces' chrome
54
+ // installs automatically). Bind ONCE:
53
55
  import {
54
56
  AssociationsProvider,
55
57
  useAssociations, // the seven hooks keep the app originals' exact signatures
@@ -60,10 +62,10 @@ import {
60
62
  <AssociationsProvider
61
63
  store={store} // the /core store from createAssociationsStore
62
64
  notifier={toastAdapter} // optional — absent warns once, failures still hit errorSink
63
- windowShell={windowAdapter} // optional — absent falls back to a fixed overlay
64
- capture={captureHandlers} // optional, per-handler chips render only when bound
65
- pickerOverrides={{ file: FilePickerWindowAdapter }} // optional per-token pickers
66
- entityDoors={{ EntityRef, DoorControls, UnresolvedRef }} // optional doors
65
+ windowShell={windowAdapter} // optional — absent uses the package's OWN draggable window
66
+ capture={{ uploadFile }} // optional — bind ONE primitive, get the whole upload flow
67
+ pickerOverrides={{ file: FilePickerWindowAdapter }} // optional — the in-package picker is the default
68
+ entityDoors={{ EntityRef, DoorControls, UnresolvedRef }} // optional — hrefFor anchors by default
67
69
  >
68
70
  <PrimaryEntityProvider value={{ type: "organization", id, orgId, label }}>
69
71
  <AssociationCardGrid />
@@ -125,14 +127,51 @@ regenerated by `pnpm gen:entity-types` inside this package. Skew policy:
125
127
  know renders through the fallback (default icon, raw label, no door) instead
126
128
  of crashing.
127
129
 
128
- ## The seven ports
130
+ ## The ports — two required, the rest DEFAULTED
129
131
 
130
- One `AssociationsConfig`, bound once per host. `dataSource` (a structural subset
131
- of `SupabaseClient` answering the demanded RPCs), `identity`, and `errorSink`
132
- are **required** — construction throws without them. `entityOverlay` (icons /
133
- routes / candidate overrides per token), `notifier`, `windowShell`, `capture`,
134
- `pickerOverrides`, and `entityDoors` are optional, each with a defined
135
- degradation never a dead button, never a silent failure.
132
+ One `AssociationsConfig`, bound once per host. Only **`dataSource`** (a
133
+ structural subset of `SupabaseClient` answering the demanded RPCs) and
134
+ **`identity`** are required — construction throws without them, because they
135
+ ARE your app's identity and no package can invent them.
136
+
137
+ Everything else ships a working default (C23 you override ports, you never
138
+ have to fill them):
139
+
140
+ | Port | Default when unbound |
141
+ |---|---|
142
+ | `errorSink` | `createDefaultErrorSink()` — tagged `console.error` per event; announces ONCE that no sink is bound and names the remedy. |
143
+ | `windowShell` | `DefaultWindowShell` — a real draggable / resizable / maximizable / viewport-clamped / portalled / non-modal window; full-width bottom card under 640px. Bind this port only for a real window MANAGER (tray docking, workspace persistence). |
144
+ | `capture` | Bind `uploadFile(file, { folderPath, visibility })` alone and the package ships the file input, drag-and-drop, batching, the stall watchdog and every loud outcome. Bind `requestUpload` instead when you own a richer pipeline. Chips with no path behind them are never rendered. |
145
+ | `pickerOverrides` | The in-package `AssociationPicker` serves every token (candidate list, search, attach/detach, create-then-associate footer). |
146
+ | `entityDoors` | `DoorControlsSlot` renders a real labelled `target="_blank"` anchor from the overlay's `hrefFor`; a token with no route renders nothing rather than a door to nowhere. |
147
+ | `entityOverlay` | Convention columns, default icon, `${label}s`, no door. |
148
+ | `notifier` | Warn-once through the errorSink, and every user-facing failure still reaches the errorSink. (No toast stack ships: a toast surface needs a host-mounted root, so a "default" would be the silent no-op this package exists to prevent.) |
149
+ | `authorDisplay` | `cmt_list`'s denormalized author fields, then "Unknown user" — never blank. |
150
+
151
+ ## Code splitting — the package owns it
152
+
153
+ Import the heavy faces from **`@ai-matrx/associations/react/lazy`** and each one
154
+ arrives in its own chunk on first render. No `next/dynamic`, no `ssr: false`,
155
+ no host glue:
156
+
157
+ ```tsx
158
+ import { AssociationCardGrid } from "@ai-matrx/associations/react/lazy";
159
+ ```
160
+
161
+ For a HOST component you bind to a port, use the same seam:
162
+
163
+ ```tsx
164
+ windowShell: {
165
+ Window: lazyWindowShell(() => import("./hostWindow").then((m) => m.HostWindow)),
166
+ },
167
+ pickerOverrides: {
168
+ file: lazyPickerOverride(() => import("./hostPicker").then((m) => m.HostPicker)),
169
+ },
170
+ ```
171
+
172
+ A per-token picker override should bind `useAssociationPickerBridge(props)` for
173
+ its attach/detach writes — it carries the loud partial-outcome, refusal and
174
+ created-but-unattached reporting so an override never re-implements them.
136
175
 
137
176
  ## Versioning
138
177