@ai-matrx/content-ir-react 0.17.0 → 0.18.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 (43) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +1 -1
  3. package/dist/{component-resolution.types-DLO2w-0n.d.cts → component-resolution.types-DY4pGymy.d.cts} +1 -1
  4. package/dist/{component-resolution.types-DLO2w-0n.d.ts → component-resolution.types-DY4pGymy.d.ts} +1 -1
  5. package/dist/{component-resolution.types-C1MzCf2s.d.cts → component-resolution.types-DwKSF64x.d.cts} +1 -1
  6. package/dist/{component-resolution.types-C1MzCf2s.d.ts → component-resolution.types-DwKSF64x.d.ts} +1 -1
  7. package/dist/db-component.d.cts +1 -1
  8. package/dist/db-component.d.ts +1 -1
  9. package/dist/{directive-host-BOML4BQ7.d.cts → directive-host-Bw2_PrgL.d.cts} +14 -0
  10. package/dist/{directive-host-BOML4BQ7.d.ts → directive-host-Bw2_PrgL.d.ts} +14 -0
  11. package/dist/directives.cjs +56 -17
  12. package/dist/directives.cjs.map +1 -1
  13. package/dist/directives.d.cts +33 -5
  14. package/dist/directives.d.ts +33 -5
  15. package/dist/directives.js +56 -17
  16. package/dist/directives.js.map +1 -1
  17. package/dist/{host-types-Dxzs0C3f.d.cts → host-types-YP_l_Q_t.d.cts} +2 -2
  18. package/dist/{host-types-uKO1zZo3.d.ts → host-types-eSjFmXyF.d.ts} +2 -2
  19. package/dist/host.cjs.map +1 -1
  20. package/dist/host.d.cts +4 -4
  21. package/dist/host.d.ts +4 -4
  22. package/dist/host.js.map +1 -1
  23. package/dist/index.cjs +56 -17
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +36 -8
  26. package/dist/index.d.ts +36 -8
  27. package/dist/index.js +56 -17
  28. package/dist/index.js.map +1 -1
  29. package/dist/react.cjs +55 -17
  30. package/dist/react.cjs.map +1 -1
  31. package/dist/react.d.cts +4 -4
  32. package/dist/react.d.ts +4 -4
  33. package/dist/react.js +55 -17
  34. package/dist/react.js.map +1 -1
  35. package/dist/resolver.d.cts +2 -2
  36. package/dist/resolver.d.ts +2 -2
  37. package/dist/{route-CfKxIEdj.d.ts → route-CtYw2cTN.d.ts} +16 -2
  38. package/dist/{route-DjGv9skd.d.cts → route-KoRMp3Qn.d.cts} +16 -2
  39. package/dist/route.cjs.map +1 -1
  40. package/dist/route.d.cts +2 -2
  41. package/dist/route.d.ts +2 -2
  42. package/dist/route.js.map +1 -1
  43. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.18.0 — 2026-10-02
4
+
5
+ The G6A review of the action cards (nightly clone). Guard:
6
+ `__tests__/directive-card-g6a.test.tsx` (3 red on 0.17.1).
7
+
8
+ - **An applied update says what it changed, from the record of the change.** After
9
+ an Update applied, a reload read the record live for the old value — which by
10
+ then IS the new value — and the row said "Title → X (no change)". The rows now
11
+ compare against the live record only while the block is open; once applied they
12
+ draw the old value from `before` — new on `DirectiveApplyResult` and
13
+ `DirectiveApplyState`, each item's replaced values by item index, which aidream
14
+ ledgers at apply time (1122747fb5) — and without it say only "Title → new".
15
+ Until the ledger answers, a row compares against nothing. New exports:
16
+ `DirectiveApplyPhase`, `readsApplyLedger`; `ApplyDirectiveButton` takes `onPhase`.
17
+ - **An update that sets no field says so before the click.** "Nothing to change"
18
+ beside a muted Apply (`data-apply-tone="muted"`, `data-apply-nothing`). Apply
19
+ still works — validation offers, never blocks.
20
+
21
+ Consumer action: a host's `confirm` passes `before` from the server's per-item
22
+ receipts, and its `applyState` passes `before` from each item of
23
+ `/directives/apply_state`.
24
+
25
+ ## 0.17.1
26
+
27
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
28
+ `git diff npm/content-ir-react/v0.17.0..npm/content-ir-react/v0.17.1 -- apps/shared/content-ir-react`).
29
+ No source changes intended and no consumer action required.
30
+
3
31
  ## 0.17.0 — 2026-10-02
4
32
 
5
33
  - **A failed apply speaks plainly.** The card no longer prints the server's text as its
package/README.md CHANGED
@@ -74,5 +74,5 @@ cannot express what your host needs, add a seam; never fork the route.
74
74
  ## Docs
75
75
 
76
76
  - Package internals and boundary: `FEATURE.md` beside this file.
77
- - Cross-repo system-of-record: `common-docs/systems/content-ir-twin/FEATURE.md`.
77
+ - Cross-repo system-of-record: `common-docs/systems/architecture/content-ir/content-ir-twin-FEATURE.md`.
78
78
  - Shape System semantics: `matrx-frontend/features/content-ir/docs/SHAPE_SYSTEM.md`.
@@ -29,7 +29,7 @@ declare const consoleErrorReporter: ContentIrErrorReporter;
29
29
  * resolver contract (rulings R1 + R6), stated once for every UI.
30
30
  *
31
31
  * Cross-repo system-of-record:
32
- * `common-docs/systems/content-ir-twin/FEATURE.md` (package boundary) and
32
+ * `common-docs/systems/architecture/content-ir/content-ir-twin-FEATURE.md` (package boundary) and
33
33
  * `matrx-frontend/features/content-ir/docs/SHAPE_SYSTEM.md` (the semantics).
34
34
  *
35
35
  * Nothing here knows where a row comes from. A host supplies rows from
@@ -29,7 +29,7 @@ declare const consoleErrorReporter: ContentIrErrorReporter;
29
29
  * resolver contract (rulings R1 + R6), stated once for every UI.
30
30
  *
31
31
  * Cross-repo system-of-record:
32
- * `common-docs/systems/content-ir-twin/FEATURE.md` (package boundary) and
32
+ * `common-docs/systems/architecture/content-ir/content-ir-twin-FEATURE.md` (package boundary) and
33
33
  * `matrx-frontend/features/content-ir/docs/SHAPE_SYSTEM.md` (the semantics).
34
34
  *
35
35
  * Nothing here knows where a row comes from. A host supplies rows from
@@ -29,7 +29,7 @@ declare const consoleErrorReporter: ContentIrErrorReporter;
29
29
  * resolver contract (rulings R1 + R6), stated once for every UI.
30
30
  *
31
31
  * Cross-repo system-of-record:
32
- * `common-docs/systems/content-ir-twin/FEATURE.md` (package boundary) and
32
+ * `common-docs/systems/architecture/content-ir/content-ir-twin-FEATURE.md` (package boundary) and
33
33
  * `matrx-frontend/features/content-ir/docs/SHAPE_SYSTEM.md` (the semantics).
34
34
  *
35
35
  * Nothing here knows where a row comes from. A host supplies rows from
@@ -29,7 +29,7 @@ declare const consoleErrorReporter: ContentIrErrorReporter;
29
29
  * resolver contract (rulings R1 + R6), stated once for every UI.
30
30
  *
31
31
  * Cross-repo system-of-record:
32
- * `common-docs/systems/content-ir-twin/FEATURE.md` (package boundary) and
32
+ * `common-docs/systems/architecture/content-ir/content-ir-twin-FEATURE.md` (package boundary) and
33
33
  * `matrx-frontend/features/content-ir/docs/SHAPE_SYSTEM.md` (the semantics).
34
34
  *
35
35
  * Nothing here knows where a row comes from. A host supplies rows from
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { C as ComponentResolution, a as ContentIrErrorReporter } from './component-resolution.types-C1MzCf2s.cjs';
2
+ import { C as ComponentResolution, a as ContentIrErrorReporter } from './component-resolution.types-DwKSF64x.cjs';
3
3
 
4
4
  /**
5
5
  * The DB-component COMPILE CACHE — session-scoped, keyed per winning row
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { C as ComponentResolution, a as ContentIrErrorReporter } from './component-resolution.types-C1MzCf2s.js';
2
+ import { C as ComponentResolution, a as ContentIrErrorReporter } from './component-resolution.types-DwKSF64x.js';
3
3
 
4
4
  /**
5
5
  * The DB-component COMPILE CACHE — session-scoped, keyed per winning row
@@ -15,6 +15,14 @@ interface DirectiveApplyResult {
15
15
  * (no dead ends). Rendered through `renderRecord`; absent → the sentence alone.
16
16
  */
17
17
  records?: DirectiveRecordRef[];
18
+ /**
19
+ * For an UPDATE: what each item's fields held just BEFORE the write, by item
20
+ * index (`null` for an item the server did not record). The card draws the
21
+ * applied change from this record — never from a live re-read, which after the
22
+ * apply answers the NEW value (G6A review, 2026-10-02: "(no change)" for a
23
+ * change that happened).
24
+ */
25
+ before?: ReadonlyArray<Record<string, unknown> | null>;
18
26
  }
19
27
  /**
20
28
  * What the server's action ledger says about ONE block, read on mount so a
@@ -43,6 +51,12 @@ interface DirectiveApplyState {
43
51
  * two records, and the card says so (reviewer, 2026-10-02).
44
52
  */
45
53
  copies?: number;
54
+ /**
55
+ * For an applied UPDATE: each item's replaced values, by item index, from the
56
+ * ledger row (`null` where the ledger has none — a row from before 2026-10-02).
57
+ * Absent → the card says only what each field was set to.
58
+ */
59
+ before?: ReadonlyArray<Record<string, unknown> | null>;
46
60
  }
47
61
  /** One record a directive names: the noun it is, and its id. */
48
62
  interface DirectiveRecordRef {
@@ -15,6 +15,14 @@ interface DirectiveApplyResult {
15
15
  * (no dead ends). Rendered through `renderRecord`; absent → the sentence alone.
16
16
  */
17
17
  records?: DirectiveRecordRef[];
18
+ /**
19
+ * For an UPDATE: what each item's fields held just BEFORE the write, by item
20
+ * index (`null` for an item the server did not record). The card draws the
21
+ * applied change from this record — never from a live re-read, which after the
22
+ * apply answers the NEW value (G6A review, 2026-10-02: "(no change)" for a
23
+ * change that happened).
24
+ */
25
+ before?: ReadonlyArray<Record<string, unknown> | null>;
18
26
  }
19
27
  /**
20
28
  * What the server's action ledger says about ONE block, read on mount so a
@@ -43,6 +51,12 @@ interface DirectiveApplyState {
43
51
  * two records, and the card says so (reviewer, 2026-10-02).
44
52
  */
45
53
  copies?: number;
54
+ /**
55
+ * For an applied UPDATE: each item's replaced values, by item index, from the
56
+ * ledger row (`null` where the ledger has none — a row from before 2026-10-02).
57
+ * Absent → the card says only what each field was set to.
58
+ */
59
+ before?: ReadonlyArray<Record<string, unknown> | null>;
46
60
  }
47
61
  /** One record a directive names: the noun it is, and its id. */
48
62
  interface DirectiveRecordRef {
@@ -39,6 +39,7 @@ __export(directives_exports, {
39
39
  itemChanges: () => itemChanges,
40
40
  itemRecordId: () => itemRecordId,
41
41
  provisionalDirectiveSlug: () => provisionalDirectiveSlug,
42
+ readsApplyLedger: () => readsApplyLedger,
42
43
  recordFallbackName: () => recordFallbackName,
43
44
  registerDirectiveRenderer: () => registerDirectiveRenderer,
44
45
  resetDirectiveRenderers: () => resetDirectiveRenderers,
@@ -290,17 +291,22 @@ function classIcon(directiveClass, noun) {
290
291
  var import_jsx_runtime3 = require("react/jsx-runtime");
291
292
  var FINISH_POLL_MS = 2e3;
292
293
  var FINISH_POLLS_MAX = 30;
294
+ function readsApplyLedger(host, directive) {
295
+ return !!host.applyState && !!host.confirm && directive.parsed.executes && directive.items.length > 0;
296
+ }
293
297
  function ApplyDirectiveButton({
294
298
  directive,
295
299
  label = "Apply",
296
300
  itemCount,
297
- onApplied
301
+ onApplied,
302
+ onPhase,
303
+ nothingToChange = false
298
304
  }) {
299
305
  const host = useDirectiveHost();
300
306
  const scope = useDirectiveScope();
301
307
  const conversationId = scope.conversationId || void 0;
302
308
  const items = directive.items;
303
- const readsLedger = !!host.applyState && !!host.confirm && directive.parsed.executes && items.length > 0;
309
+ const readsLedger = readsApplyLedger(host, directive);
304
310
  const initial = readsLedger ? { status: "checking" } : { status: "idle" };
305
311
  const [state, setState] = (0, import_react2.useState)(initial);
306
312
  const [elapsed, setElapsed] = (0, import_react2.useState)(0);
@@ -341,7 +347,8 @@ function ApplyDirectiveButton({
341
347
  failed: 0,
342
348
  message: answer.message?.trim() ? answer.message.trim() : null,
343
349
  records: answer.records ?? [],
344
- copies: Math.max(1, answer.copies ?? 1)
350
+ copies: Math.max(1, answer.copies ?? 1),
351
+ before: answer.before ?? null
345
352
  } : prev
346
353
  );
347
354
  } else if (answer.state === "in_flight" && polls < FINISH_POLLS_MAX) {
@@ -366,6 +373,15 @@ function ApplyDirectiveButton({
366
373
  if (timer) clearTimeout(timer);
367
374
  };
368
375
  }, [shellKey, read, report]);
376
+ const appliedClean = state.status === "applied" && state.failed === 0 ? state : null;
377
+ const appliedBefore = appliedClean ? appliedClean.before : null;
378
+ const phaseKey = state.status === "checking" || state.status === "finishing" ? "pending" : appliedClean ? `applied:${JSON.stringify(appliedBefore)}` : "open";
379
+ (0, import_react2.useEffect)(() => {
380
+ if (!onPhase) return;
381
+ if (phaseKey === "pending") onPhase({ phase: "pending" });
382
+ else if (phaseKey === "open") onPhase({ phase: "open" });
383
+ else onPhase({ phase: "applied", before: appliedBefore });
384
+ }, [phaseKey, onPhase, appliedBefore]);
369
385
  (0, import_react2.useEffect)(() => {
370
386
  if (state.status !== "applying") return;
371
387
  startedAt.current = Date.now();
@@ -380,7 +396,7 @@ function ApplyDirectiveButton({
380
396
  if (!confirm) return null;
381
397
  async function handleApply(again = false) {
382
398
  if (!confirm) return;
383
- const before = state;
399
+ const prior = state;
384
400
  const askedFor = identity;
385
401
  const stillHere = () => currentIdentity.current === askedFor;
386
402
  const priorCopies = state.status === "applied" ? state.copies : 0;
@@ -407,7 +423,7 @@ function ApplyDirectiveButton({
407
423
  if (!stillHere()) return;
408
424
  setAskingAgain(false);
409
425
  if (!ok) {
410
- setState(again ? before : { status: "idle" });
426
+ setState(again ? prior : { status: "idle" });
411
427
  return;
412
428
  }
413
429
  }
@@ -426,7 +442,10 @@ function ApplyDirectiveButton({
426
442
  message: result.message?.trim() ? result.message.trim() : null,
427
443
  records: result.records ?? [],
428
444
  // "Run again" made one more; a first apply (or a replay of one) is one.
429
- copies: again ? Math.max(1, priorCopies) + 1 : Math.max(1, priorCopies)
445
+ copies: again ? Math.max(1, priorCopies) + 1 : Math.max(1, priorCopies),
446
+ // "Run again" wrote the same values over themselves; what the FIRST
447
+ // apply replaced is still the change this block made.
448
+ before: again && prior.status === "applied" && prior.before ? prior.before : result.before ?? null
430
449
  });
431
450
  onApplied?.();
432
451
  } catch (error) {
@@ -528,15 +547,18 @@ function ApplyDirectiveButton({
528
547
  );
529
548
  }
530
549
  const destructive = directive.directiveClass === "delete";
550
+ const muted = nothingToChange && state.status === "idle";
531
551
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "inline-flex items-center gap-2", children: [
552
+ muted ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-xs text-muted-foreground", "data-apply-nothing": "", children: "Nothing to change" }) : null,
532
553
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
533
554
  "button",
534
555
  {
535
556
  type: "button",
536
557
  onClick: () => void handleApply(),
537
558
  disabled: state.status === "applying" || state.status === "asking" || items.length === 0,
538
- "data-apply-tone": destructive ? "destructive" : "default",
539
- className: `inline-flex items-center gap-1 rounded-md border px-2 py-1 text-xs font-medium disabled:opacity-50 ${destructive ? "border-destructive/50 bg-background text-destructive hover:bg-destructive/10" : "border-border bg-background hover:bg-accent"}`,
559
+ "data-apply-tone": muted ? "muted" : destructive ? "destructive" : "default",
560
+ title: muted ? "It sets no fields, so applying it changes nothing." : void 0,
561
+ className: `inline-flex items-center gap-1 rounded-md border px-2 py-1 text-xs font-medium disabled:opacity-50 ${muted ? "border-border/60 bg-background text-muted-foreground hover:bg-accent" : destructive ? "border-destructive/50 bg-background text-destructive hover:bg-destructive/10" : "border-border bg-background hover:bg-accent"}`,
540
562
  children: [
541
563
  state.status === "applying" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(LoaderIcon, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PlayIcon, { className: "h-3.5 w-3.5" }),
542
564
  state.status === "applying" ? `Applying${itemCount ? ` ${itemCount} ${itemCount === 1 ? "item" : "items"}` : ""}\u2026${elapsed ? ` ${elapsed}s` : ""}` : label
@@ -646,12 +668,13 @@ function ItemRow({
646
668
  directive,
647
669
  item,
648
670
  index,
649
- total
671
+ total,
672
+ phase
650
673
  }) {
651
674
  const host = useDirectiveHost();
652
675
  const recordId = TARGETING_CLASSES.has(directive.directiveClass) ? itemRecordId(item) : null;
653
676
  if (recordId) {
654
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(TargetRow, { directive, item, recordId });
677
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(TargetRow, { directive, item, recordId, index, phase });
655
678
  }
656
679
  const titleColumn = (0, import_content_ir3.nounTitleColumn)(directive.noun, host.nouns);
657
680
  const title = (0, import_content_ir3.itemTitle)(item, titleColumn, index, total);
@@ -738,16 +761,20 @@ function DirectiveChangeList({
738
761
  function TargetRow({
739
762
  directive,
740
763
  item,
741
- recordId
764
+ recordId,
765
+ index,
766
+ phase
742
767
  }) {
743
768
  const host = useDirectiveHost();
744
769
  const fallback = recordFallbackName((0, import_content_ir3.nounLabel)(directive.noun, host.nouns), recordId);
745
770
  const isUpdate = directive.directiveClass === "update";
746
- const current = useRecordValues(
747
- isUpdate ? host.readRecord : void 0,
748
- isUpdate ? { noun: directive.noun, id: recordId } : null
771
+ const readsLive = isUpdate && phase.phase === "open";
772
+ const live = useRecordValues(
773
+ readsLive ? host.readRecord : void 0,
774
+ readsLive ? { noun: directive.noun, id: recordId } : null
749
775
  );
750
- const changes = isUpdate ? itemChanges(item, current.values, { titleColumn: (0, import_content_ir3.nounTitleColumn)(directive.noun, host.nouns) }) : [];
776
+ const compareTo = phase.phase === "applied" ? phase.before?.[index] ?? null : readsLive ? live.values : null;
777
+ const changes = isUpdate ? itemChanges(item, compareTo, { titleColumn: (0, import_content_ir3.nounTitleColumn)(directive.noun, host.nouns) }) : [];
751
778
  const name = host.renderRecord ? host.renderRecord({ noun: directive.noun, id: recordId, fallback, context: "row" }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "truncate font-medium text-foreground", children: fallback });
752
779
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "py-1 text-xs", "data-directive-target": recordId, children: [
753
780
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex min-w-0 items-center gap-2", children: [
@@ -764,6 +791,10 @@ function SideEffectDirectiveCard({ directive }) {
764
791
  const total = items.length;
765
792
  const display = (0, import_content_ir3.directiveDisplay)(directive.directiveClass, directive.noun, host.nouns);
766
793
  const Icon = classIcon(directive.directiveClass, directive.noun);
794
+ const [phase, setPhase] = (0, import_react4.useState)(
795
+ () => readsApplyLedger(host, directive) ? { phase: "pending" } : { phase: "open" }
796
+ );
797
+ const nothingToChange = directive.directiveClass === "update" && total > 0 && items.every((item) => itemChanges(item).length === 0);
767
798
  const shown = expanded ? items : items.slice(0, ROWS_BEFORE_FOLD);
768
799
  const hidden = total - shown.length;
769
800
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "my-3 rounded-md border border-border bg-card px-2.5 py-1.5", "data-directive": directive.slug, children: [
@@ -778,13 +809,21 @@ function SideEffectDirectiveCard({ directive }) {
778
809
  ] }),
779
810
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "ml-auto flex items-center gap-1", children: [
780
811
  host.renderCopy?.({ label: display.title, value: directive.shell, size: "icon" }),
781
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ApplyDirectiveButton, { directive, itemCount: total })
812
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
813
+ ApplyDirectiveButton,
814
+ {
815
+ directive,
816
+ itemCount: total,
817
+ onPhase: setPhase,
818
+ nothingToChange
819
+ }
820
+ )
782
821
  ] })
783
822
  ] }),
784
823
  total === 0 ? (
785
824
  // Stated, never silent: an empty batch is a real (and suspicious) state.
786
825
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "mt-1 text-xs text-muted-foreground", children: "No items \u2014 nothing would be written." })
787
- ) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "mt-0.5 divide-y divide-border/60", children: shown.map((item, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ItemRow, { directive, item, index: i, total }, i)) }),
826
+ ) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "mt-0.5 divide-y divide-border/60", children: shown.map((item, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ItemRow, { directive, item, index: i, total, phase }, i)) }),
788
827
  hidden > 0 || expanded ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
789
828
  "button",
790
829
  {