@ai-matrx/content-ir-react 0.18.1 → 0.19.1

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,35 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.19.1 — 2026-10-02
4
+
5
+ - **One record-revision store per page.** Every entry of this package (`.`,
6
+ `./directives`, …) is bundled self-contained, so 0.19.0's module-level counter
7
+ was a separate counter per entry: an apply announced through one entry was
8
+ never seen by a reader imported through the other. The store now lives in a
9
+ `Symbol.for("@ai-matrx/content-ir-react/record-revisions")` slot on
10
+ `globalThis`. Guard: `directive-record-values-g7.test.tsx` ("the store is ONE
11
+ per page…", red on 0.19.0).
12
+
13
+ No consumer action.
14
+
15
+ ## 0.19.0 — 2026-10-02
16
+
17
+ - **A record's old value is the value it holds NOW.** `useRecordValues` read a
18
+ record ONCE per `noun:id`, so after Update A applied, Update B naming the same
19
+ record — its row, and a host confirm whose body stayed mounted between
20
+ questions — still showed A's old value as "old" (G7: "G6A renamed target"
21
+ while the database held "G6A second rename"). Every apply now announces the
22
+ records it touched (`noteRecordChanged`, for update/delete targets and the
23
+ written records, even when the apply fails) and every `useRecordValues`
24
+ naming one reads it again. New exports: `noteRecordChanged`,
25
+ `recordRevision`, `useRecordRevision`. Guard:
26
+ `__tests__/directive-record-values-g7.test.tsx` (2 red on 0.18.1).
27
+
28
+ Consumer action: a host whose own writers change a record calls
29
+ `noteRecordChanged(id)`; a host `readRecord` that shares in-flight reads keys
30
+ them by `recordRevision(id)`, so a read started before a write is never handed
31
+ to a reader asking after it.
32
+
3
33
  ## 0.18.1 — 2026-10-02
4
34
 
5
35
  - **An apply belongs to the block, not to the card that asked.** A note re-renders
@@ -38,13 +38,16 @@ __export(directives_exports, {
38
38
  getDirectiveRenderer: () => getDirectiveRenderer,
39
39
  itemChanges: () => itemChanges,
40
40
  itemRecordId: () => itemRecordId,
41
+ noteRecordChanged: () => noteRecordChanged,
41
42
  provisionalDirectiveSlug: () => provisionalDirectiveSlug,
42
43
  readsApplyLedger: () => readsApplyLedger,
43
44
  recordFallbackName: () => recordFallbackName,
45
+ recordRevision: () => recordRevision,
44
46
  registerDirectiveRenderer: () => registerDirectiveRenderer,
45
47
  resetDirectiveRenderers: () => resetDirectiveRenderers,
46
48
  useDirectiveHost: () => useDirectiveHost,
47
49
  useDirectiveScope: () => useDirectiveScope,
50
+ useRecordRevision: () => useRecordRevision,
48
51
  useRecordValues: () => useRecordValues
49
52
  });
50
53
  module.exports = __toCommonJS(directives_exports);
@@ -91,7 +94,7 @@ function resetDirectiveRenderers() {
91
94
  }
92
95
 
93
96
  // directives/ApplyDirectiveButton.tsx
94
- var import_react2 = require("react");
97
+ var import_react3 = require("react");
95
98
  var import_content_ir2 = require("@ai-matrx/content-ir");
96
99
 
97
100
  // directives/item-changes.ts
@@ -170,6 +173,73 @@ function itemChanges(item, current, options = {}) {
170
173
  return out;
171
174
  }
172
175
 
176
+ // directives/record-values.ts
177
+ var import_react2 = require("react");
178
+ var STORE_SLOT = /* @__PURE__ */ Symbol.for("@ai-matrx/content-ir-react/record-revisions");
179
+ function revisionStore() {
180
+ const slots = globalThis;
181
+ let store = slots[STORE_SLOT];
182
+ if (!store) {
183
+ store = { revisions: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() };
184
+ slots[STORE_SLOT] = store;
185
+ }
186
+ return store;
187
+ }
188
+ var { revisions, listeners } = revisionStore();
189
+ function recordRevision(id) {
190
+ return revisions.get(id) ?? 0;
191
+ }
192
+ function noteRecordChanged(id) {
193
+ if (!id) return;
194
+ revisions.set(id, recordRevision(id) + 1);
195
+ for (const listener of listeners) listener();
196
+ }
197
+ function subscribeRevisions(listener) {
198
+ listeners.add(listener);
199
+ return () => {
200
+ listeners.delete(listener);
201
+ };
202
+ }
203
+ function useRecordRevision(id) {
204
+ return (0, import_react2.useSyncExternalStore)(
205
+ subscribeRevisions,
206
+ () => recordRevision(id),
207
+ () => 0
208
+ );
209
+ }
210
+ function useRecordValues(read, ref) {
211
+ const revision = useRecordRevision(ref?.id ?? "");
212
+ const key = ref && read ? `${ref.noun}:${ref.id}#${revision}` : "";
213
+ const [result, setResult] = (0, import_react2.useState)({
214
+ key: "",
215
+ values: null,
216
+ status: "idle"
217
+ });
218
+ (0, import_react2.useEffect)(() => {
219
+ if (!key || !read) return;
220
+ const split = key.indexOf(":");
221
+ const target = {
222
+ noun: key.slice(0, split),
223
+ id: key.slice(split + 1, key.lastIndexOf("#"))
224
+ };
225
+ let live = true;
226
+ setResult({ key, values: null, status: "loading" });
227
+ read(target).then((values) => {
228
+ if (!live) return;
229
+ setResult({ key, values, status: values ? "ready" : "unavailable" });
230
+ }).catch(() => {
231
+ if (!live) return;
232
+ setResult({ key, values: null, status: "unavailable" });
233
+ });
234
+ return () => {
235
+ live = false;
236
+ };
237
+ }, [key, read]);
238
+ if (!key) return { values: null, status: "idle" };
239
+ if (result.key !== key) return { values: null, status: "loading" };
240
+ return { values: result.values, status: result.status };
241
+ }
242
+
173
243
  // directives/failure-words.ts
174
244
  var PAST = {
175
245
  create: "created",
@@ -289,6 +359,13 @@ function classIcon(directiveClass, noun) {
289
359
 
290
360
  // directives/ApplyDirectiveButton.tsx
291
361
  var import_jsx_runtime3 = require("react/jsx-runtime");
362
+ function announceChangedRecords(directive, items) {
363
+ if (!TARGETING_CLASSES.has(directive.directiveClass)) return;
364
+ for (const item of items) {
365
+ const id = itemRecordId(item);
366
+ if (id) noteRecordChanged(id);
367
+ }
368
+ }
292
369
  var applyOutcomes = /* @__PURE__ */ new WeakMap();
293
370
  var EMPTY_AUTHORITY = {};
294
371
  var outcomeListeners = /* @__PURE__ */ new Set();
@@ -311,7 +388,7 @@ function subscribeOutcomes(listener) {
311
388
  };
312
389
  }
313
390
  function useApplyOutcome(host, identity) {
314
- return (0, import_react2.useSyncExternalStore)(
391
+ return (0, import_react3.useSyncExternalStore)(
315
392
  subscribeOutcomes,
316
393
  () => applyOutcomes.get(host)?.get(identity) ?? null,
317
394
  () => null
@@ -336,12 +413,12 @@ function ApplyDirectiveButton({
336
413
  const items = directive.items;
337
414
  const readsLedger = readsApplyLedger(host, directive);
338
415
  const initial = readsLedger ? { status: "checking" } : { status: "idle" };
339
- const [state, setState] = (0, import_react2.useState)(initial);
340
- const [elapsed, setElapsed] = (0, import_react2.useState)(0);
341
- const [askingAgain, setAskingAgain] = (0, import_react2.useState)(false);
342
- const startedAt = (0, import_react2.useRef)(null);
416
+ const [state, setState] = (0, import_react3.useState)(initial);
417
+ const [elapsed, setElapsed] = (0, import_react3.useState)(0);
418
+ const [askingAgain, setAskingAgain] = (0, import_react3.useState)(false);
419
+ const startedAt = (0, import_react3.useRef)(null);
343
420
  const identity = JSON.stringify([directive.slug, items, conversationId ?? null]);
344
- const [stateFor, setStateFor] = (0, import_react2.useState)(identity);
421
+ const [stateFor, setStateFor] = (0, import_react3.useState)(identity);
345
422
  if (stateFor !== identity) {
346
423
  setStateFor(identity);
347
424
  setState(initial);
@@ -350,15 +427,15 @@ function ApplyDirectiveButton({
350
427
  }
351
428
  const shared = useApplyOutcome(host.confirm ?? EMPTY_AUTHORITY, identity);
352
429
  const view = state.status === "asking" || !shared ? state : shared;
353
- const currentIdentity = (0, import_react2.useRef)(identity);
354
- (0, import_react2.useEffect)(() => {
430
+ const currentIdentity = (0, import_react3.useRef)(identity);
431
+ (0, import_react3.useEffect)(() => {
355
432
  currentIdentity.current = identity;
356
433
  }, [identity]);
357
434
  const nounText = (0, import_content_ir2.nounLabel)(directive.noun, host.nouns);
358
435
  const shellKey = readsLedger ? JSON.stringify([directive.slug, items, conversationId ?? null]) : "";
359
436
  const read = host.applyState;
360
437
  const report = host.reportError;
361
- (0, import_react2.useEffect)(() => {
438
+ (0, import_react3.useEffect)(() => {
362
439
  if (!shellKey || !read) return;
363
440
  const [slug, shellItems, inConversation] = JSON.parse(shellKey);
364
441
  const shell = { __kind: slug, items: shellItems };
@@ -406,14 +483,14 @@ function ApplyDirectiveButton({
406
483
  const appliedClean = view.status === "applied" && view.failed === 0 ? view : null;
407
484
  const appliedBefore = appliedClean ? appliedClean.before : null;
408
485
  const phaseKey = view.status === "checking" || view.status === "finishing" ? "pending" : appliedClean ? `applied:${JSON.stringify(appliedBefore)}` : "open";
409
- (0, import_react2.useEffect)(() => {
486
+ (0, import_react3.useEffect)(() => {
410
487
  if (!onPhase) return;
411
488
  if (phaseKey === "pending") onPhase({ phase: "pending" });
412
489
  else if (phaseKey === "open") onPhase({ phase: "open" });
413
490
  else onPhase({ phase: "applied", before: appliedBefore });
414
491
  }, [phaseKey, onPhase, appliedBefore]);
415
492
  const applyingSince = view.status === "applying" ? view.startedAt : null;
416
- (0, import_react2.useEffect)(() => {
493
+ (0, import_react3.useEffect)(() => {
417
494
  if (applyingSince === null) return;
418
495
  startedAt.current = applyingSince;
419
496
  const id = setInterval(
@@ -465,7 +542,13 @@ function ApplyDirectiveButton({
465
542
  const shell = { __kind: directive.slug, items };
466
543
  if (conversationId) shell.conversationId = conversationId;
467
544
  if (again) shell.force = true;
468
- const result = await confirm(shell);
545
+ let result;
546
+ try {
547
+ result = await confirm(shell);
548
+ } finally {
549
+ announceChangedRecords(directive, items);
550
+ }
551
+ for (const record of result.records ?? []) noteRecordChanged(record.id);
469
552
  publishOutcome(confirm, askedFor, {
470
553
  status: "applied",
471
554
  applied: result.applied,
@@ -626,7 +709,7 @@ function FailureNotice({
626
709
  words,
627
710
  raw
628
711
  }) {
629
- const [open, setOpen] = (0, import_react2.useState)(false);
712
+ const [open, setOpen] = (0, import_react3.useState)(false);
630
713
  const detail = raw?.trim() ?? "";
631
714
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "flex min-w-0 flex-col gap-0.5", "data-apply-failure": true, children: [
632
715
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "min-w-0", children: [
@@ -659,39 +742,6 @@ function FailureNotice({
659
742
  // directives/SideEffectDirectiveCard.tsx
660
743
  var import_react4 = require("react");
661
744
  var import_content_ir3 = require("@ai-matrx/content-ir");
662
-
663
- // directives/record-values.ts
664
- var import_react3 = require("react");
665
- function useRecordValues(read, ref) {
666
- const key = ref && read ? `${ref.noun}:${ref.id}` : "";
667
- const [result, setResult] = (0, import_react3.useState)({
668
- key: "",
669
- values: null,
670
- status: "idle"
671
- });
672
- (0, import_react3.useEffect)(() => {
673
- if (!key || !read) return;
674
- const split = key.indexOf(":");
675
- const target = { noun: key.slice(0, split), id: key.slice(split + 1) };
676
- let live = true;
677
- setResult({ key, values: null, status: "loading" });
678
- read(target).then((values) => {
679
- if (!live) return;
680
- setResult({ key, values, status: values ? "ready" : "unavailable" });
681
- }).catch(() => {
682
- if (!live) return;
683
- setResult({ key, values: null, status: "unavailable" });
684
- });
685
- return () => {
686
- live = false;
687
- };
688
- }, [key, read]);
689
- if (!key) return { values: null, status: "idle" };
690
- if (result.key !== key) return { values: null, status: "loading" };
691
- return { values: result.values, status: result.status };
692
- }
693
-
694
- // directives/SideEffectDirectiveCard.tsx
695
745
  var import_jsx_runtime4 = require("react/jsx-runtime");
696
746
  var ROWS_BEFORE_FOLD = 3;
697
747
  function ItemRow({