@ai-matrx/content-ir-react 0.18.1 → 0.19.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,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.19.0 — 2026-10-02
4
+
5
+ - **A record's old value is the value it holds NOW.** `useRecordValues` read a
6
+ record ONCE per `noun:id`, so after Update A applied, Update B naming the same
7
+ record — its row, and a host confirm whose body stayed mounted between
8
+ questions — still showed A's old value as "old" (G7: "G6A renamed target"
9
+ while the database held "G6A second rename"). Every apply now announces the
10
+ records it touched (`noteRecordChanged`, for update/delete targets and the
11
+ written records, even when the apply fails) and every `useRecordValues`
12
+ naming one reads it again. New exports: `noteRecordChanged`,
13
+ `recordRevision`, `useRecordRevision`. Guard:
14
+ `__tests__/directive-record-values-g7.test.tsx` (2 red on 0.18.1).
15
+
16
+ Consumer action: a host whose own writers change a record calls
17
+ `noteRecordChanged(id)`; a host `readRecord` that shares in-flight reads keys
18
+ them by `recordRevision(id)`, so a read started before a write is never handed
19
+ to a reader asking after it.
20
+
3
21
  ## 0.18.1 — 2026-10-02
4
22
 
5
23
  - **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,64 @@ 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 revisions = /* @__PURE__ */ new Map();
179
+ var listeners = /* @__PURE__ */ new Set();
180
+ function recordRevision(id) {
181
+ return revisions.get(id) ?? 0;
182
+ }
183
+ function noteRecordChanged(id) {
184
+ if (!id) return;
185
+ revisions.set(id, recordRevision(id) + 1);
186
+ for (const listener of listeners) listener();
187
+ }
188
+ function subscribeRevisions(listener) {
189
+ listeners.add(listener);
190
+ return () => {
191
+ listeners.delete(listener);
192
+ };
193
+ }
194
+ function useRecordRevision(id) {
195
+ return (0, import_react2.useSyncExternalStore)(
196
+ subscribeRevisions,
197
+ () => recordRevision(id),
198
+ () => 0
199
+ );
200
+ }
201
+ function useRecordValues(read, ref) {
202
+ const revision = useRecordRevision(ref?.id ?? "");
203
+ const key = ref && read ? `${ref.noun}:${ref.id}#${revision}` : "";
204
+ const [result, setResult] = (0, import_react2.useState)({
205
+ key: "",
206
+ values: null,
207
+ status: "idle"
208
+ });
209
+ (0, import_react2.useEffect)(() => {
210
+ if (!key || !read) return;
211
+ const split = key.indexOf(":");
212
+ const target = {
213
+ noun: key.slice(0, split),
214
+ id: key.slice(split + 1, key.lastIndexOf("#"))
215
+ };
216
+ let live = true;
217
+ setResult({ key, values: null, status: "loading" });
218
+ read(target).then((values) => {
219
+ if (!live) return;
220
+ setResult({ key, values, status: values ? "ready" : "unavailable" });
221
+ }).catch(() => {
222
+ if (!live) return;
223
+ setResult({ key, values: null, status: "unavailable" });
224
+ });
225
+ return () => {
226
+ live = false;
227
+ };
228
+ }, [key, read]);
229
+ if (!key) return { values: null, status: "idle" };
230
+ if (result.key !== key) return { values: null, status: "loading" };
231
+ return { values: result.values, status: result.status };
232
+ }
233
+
173
234
  // directives/failure-words.ts
174
235
  var PAST = {
175
236
  create: "created",
@@ -289,6 +350,13 @@ function classIcon(directiveClass, noun) {
289
350
 
290
351
  // directives/ApplyDirectiveButton.tsx
291
352
  var import_jsx_runtime3 = require("react/jsx-runtime");
353
+ function announceChangedRecords(directive, items) {
354
+ if (!TARGETING_CLASSES.has(directive.directiveClass)) return;
355
+ for (const item of items) {
356
+ const id = itemRecordId(item);
357
+ if (id) noteRecordChanged(id);
358
+ }
359
+ }
292
360
  var applyOutcomes = /* @__PURE__ */ new WeakMap();
293
361
  var EMPTY_AUTHORITY = {};
294
362
  var outcomeListeners = /* @__PURE__ */ new Set();
@@ -311,7 +379,7 @@ function subscribeOutcomes(listener) {
311
379
  };
312
380
  }
313
381
  function useApplyOutcome(host, identity) {
314
- return (0, import_react2.useSyncExternalStore)(
382
+ return (0, import_react3.useSyncExternalStore)(
315
383
  subscribeOutcomes,
316
384
  () => applyOutcomes.get(host)?.get(identity) ?? null,
317
385
  () => null
@@ -336,12 +404,12 @@ function ApplyDirectiveButton({
336
404
  const items = directive.items;
337
405
  const readsLedger = readsApplyLedger(host, directive);
338
406
  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);
407
+ const [state, setState] = (0, import_react3.useState)(initial);
408
+ const [elapsed, setElapsed] = (0, import_react3.useState)(0);
409
+ const [askingAgain, setAskingAgain] = (0, import_react3.useState)(false);
410
+ const startedAt = (0, import_react3.useRef)(null);
343
411
  const identity = JSON.stringify([directive.slug, items, conversationId ?? null]);
344
- const [stateFor, setStateFor] = (0, import_react2.useState)(identity);
412
+ const [stateFor, setStateFor] = (0, import_react3.useState)(identity);
345
413
  if (stateFor !== identity) {
346
414
  setStateFor(identity);
347
415
  setState(initial);
@@ -350,15 +418,15 @@ function ApplyDirectiveButton({
350
418
  }
351
419
  const shared = useApplyOutcome(host.confirm ?? EMPTY_AUTHORITY, identity);
352
420
  const view = state.status === "asking" || !shared ? state : shared;
353
- const currentIdentity = (0, import_react2.useRef)(identity);
354
- (0, import_react2.useEffect)(() => {
421
+ const currentIdentity = (0, import_react3.useRef)(identity);
422
+ (0, import_react3.useEffect)(() => {
355
423
  currentIdentity.current = identity;
356
424
  }, [identity]);
357
425
  const nounText = (0, import_content_ir2.nounLabel)(directive.noun, host.nouns);
358
426
  const shellKey = readsLedger ? JSON.stringify([directive.slug, items, conversationId ?? null]) : "";
359
427
  const read = host.applyState;
360
428
  const report = host.reportError;
361
- (0, import_react2.useEffect)(() => {
429
+ (0, import_react3.useEffect)(() => {
362
430
  if (!shellKey || !read) return;
363
431
  const [slug, shellItems, inConversation] = JSON.parse(shellKey);
364
432
  const shell = { __kind: slug, items: shellItems };
@@ -406,14 +474,14 @@ function ApplyDirectiveButton({
406
474
  const appliedClean = view.status === "applied" && view.failed === 0 ? view : null;
407
475
  const appliedBefore = appliedClean ? appliedClean.before : null;
408
476
  const phaseKey = view.status === "checking" || view.status === "finishing" ? "pending" : appliedClean ? `applied:${JSON.stringify(appliedBefore)}` : "open";
409
- (0, import_react2.useEffect)(() => {
477
+ (0, import_react3.useEffect)(() => {
410
478
  if (!onPhase) return;
411
479
  if (phaseKey === "pending") onPhase({ phase: "pending" });
412
480
  else if (phaseKey === "open") onPhase({ phase: "open" });
413
481
  else onPhase({ phase: "applied", before: appliedBefore });
414
482
  }, [phaseKey, onPhase, appliedBefore]);
415
483
  const applyingSince = view.status === "applying" ? view.startedAt : null;
416
- (0, import_react2.useEffect)(() => {
484
+ (0, import_react3.useEffect)(() => {
417
485
  if (applyingSince === null) return;
418
486
  startedAt.current = applyingSince;
419
487
  const id = setInterval(
@@ -465,7 +533,13 @@ function ApplyDirectiveButton({
465
533
  const shell = { __kind: directive.slug, items };
466
534
  if (conversationId) shell.conversationId = conversationId;
467
535
  if (again) shell.force = true;
468
- const result = await confirm(shell);
536
+ let result;
537
+ try {
538
+ result = await confirm(shell);
539
+ } finally {
540
+ announceChangedRecords(directive, items);
541
+ }
542
+ for (const record of result.records ?? []) noteRecordChanged(record.id);
469
543
  publishOutcome(confirm, askedFor, {
470
544
  status: "applied",
471
545
  applied: result.applied,
@@ -626,7 +700,7 @@ function FailureNotice({
626
700
  words,
627
701
  raw
628
702
  }) {
629
- const [open, setOpen] = (0, import_react2.useState)(false);
703
+ const [open, setOpen] = (0, import_react3.useState)(false);
630
704
  const detail = raw?.trim() ?? "";
631
705
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "flex min-w-0 flex-col gap-0.5", "data-apply-failure": true, children: [
632
706
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "min-w-0", children: [
@@ -659,39 +733,6 @@ function FailureNotice({
659
733
  // directives/SideEffectDirectiveCard.tsx
660
734
  var import_react4 = require("react");
661
735
  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
736
  var import_jsx_runtime4 = require("react/jsx-runtime");
696
737
  var ROWS_BEFORE_FOLD = 3;
697
738
  function ItemRow({