@ai-matrx/records 0.57.0 → 0.57.2

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,41 @@
1
1
  # Changelog — @ai-matrx/records
2
2
 
3
+ ## 0.57.2 (unpublished — lane HUB-FIX, 2026-09-23; `custom.table_facts` is live on the main database, ledger 2026-09-23 11:51Z, and `STORE_DOORS` is regenerated to carry it)
4
+
5
+ * `tableList()` folds `custom.table_facts` onto every Table: `visibility` (a record column the
6
+ Table document never carried, so every lane decision fell through to its default) and
7
+ `created_by` — set ONLY to the caller's own id and only on Tables the caller made. The call is
8
+ made only when `STORE_DOORS` carries the door (the door-presence pattern): a build generated
9
+ without it asks nothing and returns the Tables exactly as before. (The code landed inside 0.57.1's commit `7f2a737913` by a sweep; this
10
+ entry is its record.) Test `__tests__/table-list-facts.test.ts`.
11
+ * New `tableArchived({ table_id })`: the archived row for one Table, or `null` — paged over the
12
+ Table kernel's archive, the same door a table's own archive uses.
13
+
14
+ ## 0.57.1 (unpublished — lane RECORDS-UI-FIX, guide re-walk 2026-09-23)
15
+
16
+ * **A refusal is not an app error.** The default error sink printed every refusal through
17
+ `console.error`, and matrx-frontend's error capture counts every `console.error`, so one
18
+ refused value lit a red "2 errors" pill. The codes a person causes (`refused_by_rule`,
19
+ `stale_write`, `invalid_argument`, `already_exists`, `missing_reference`) now go to
20
+ `console.info`; everything else stays `console.error`. New exports (core):
21
+ `isAPersonsRefusal`, `A_PERSONS_REFUSAL`. Test: `src/__tests__/a-refusal-is-never-an-app-error.test.ts`.
22
+ * **Every screen hears the writes made on its page.** `recordUpdate`, `recordDelete`,
23
+ `recordRestore`, `restoreVersion` and `valueRestore` announce the record they wrote on
24
+ success. New exports: core `announceRecordChanged` / `onRecordChanged`, react
25
+ `useRecordChangeRevision(recordId)`; `useRecord` re-reads on it.
26
+
27
+ ## Unreleased — STORE-RULE-GAPS (2026-09-23)
28
+
29
+ * `FieldRule` gains `example` (a pattern's "how to write it") and `min` (a length's shortest);
30
+ `value` is optional for a min-only length rule. `NewFieldDeclaration.allow_other` (sent as
31
+ `config.allow_other`) and `FieldPatch.allow_other`; `allowsOtherValues(field)`.
32
+ `PredictedRefusal.detail`.
33
+ * The mirror predicts the store's new sentences word for word ("… has to be at least 20
34
+ characters long"; a pattern with an example hints "Enter it like …" with `detail.example`),
35
+ and no longer predicts a refusal for a list value that is not a uuid — the store holds option
36
+ KEYS (CHOICE-VALUE) and a list that takes other values adds an unknown word.
37
+ * Needs matrx-frontend `storerulegaps_an_example_a_shortest_length_and_other_choices.sql`.
38
+
3
39
  ## 0.57.0
4
40
 
5
41
  **The grid primitives are live, so their doors are in the catalogue.** (published by lane
@@ -2199,6 +2199,7 @@ __export(core_exports, {
2199
2199
  ARCHIVE_LANES: () => ARCHIVE_LANES,
2200
2200
  ARCHIVE_LANE_LABEL: () => ARCHIVE_LANE_LABEL,
2201
2201
  ARCHIVE_LANE_MEANS: () => ARCHIVE_LANE_MEANS,
2202
+ A_PERSONS_REFUSAL: () => A_PERSONS_REFUSAL,
2202
2203
  COMPUTE_ON: () => COMPUTE_ON,
2203
2204
  CONTEXT_POLICIES: () => CONTEXT_POLICIES,
2204
2205
  DECLARABLE_FIELD_KINDS: () => DECLARABLE_FIELD_KINDS,
@@ -2245,6 +2246,8 @@ __export(core_exports, {
2245
2246
  VALUE_ALTERNATE_KEYS: () => VALUE_ALTERNATE_KEYS,
2246
2247
  VALUE_ENVELOPE_KEYS: () => VALUE_ENVELOPE_KEYS,
2247
2248
  actionRefusals: () => actionRefusals,
2249
+ allowsOtherValues: () => allowsOtherValues,
2250
+ announceRecordChanged: () => announceRecordChanged,
2248
2251
  archivedByLine: () => archivedByLine,
2249
2252
  archivedWhen: () => archivedWhen,
2250
2253
  asPermissionLevel: () => asPermissionLevel,
@@ -2277,6 +2280,7 @@ __export(core_exports, {
2277
2280
  importXlsx: () => importXlsx,
2278
2281
  indexedDbStore: () => indexedDbStore,
2279
2282
  intersectIds: () => intersectIds,
2283
+ isAPersonsRefusal: () => isAPersonsRefusal,
2280
2284
  isArchiveLane: () => isArchiveLane,
2281
2285
  isDecided: () => isDecided,
2282
2286
  isOwnOp: () => isOwnOp,
@@ -2291,6 +2295,7 @@ __export(core_exports, {
2291
2295
  mintClientKey: () => mintClientKey,
2292
2296
  mintOpId: () => mintOpId,
2293
2297
  ok: () => ok,
2298
+ onRecordChanged: () => onRecordChanged,
2294
2299
  openCaptureQueue: () => openCaptureQueue,
2295
2300
  optionKey: () => optionKey,
2296
2301
  optionLabel: () => optionLabel,
@@ -2439,6 +2444,11 @@ function highestLevel(left, right) {
2439
2444
  return ORDINAL[left] >= ORDINAL[right] ? left : right;
2440
2445
  }
2441
2446
 
2447
+ // src/field.ts
2448
+ function allowsOtherValues(field) {
2449
+ return field.type === "list" && field.config?.["allow_other"] === true;
2450
+ }
2451
+
2442
2452
  // src/choice.ts
2443
2453
  var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2444
2454
  function optionKey(option) {
@@ -3813,6 +3823,7 @@ var STORE_DOORS = [
3813
3823
  { name: "table_declare", args: "p_organization_id uuid, p_spec jsonb", returns: "uuid", security: "definer" },
3814
3824
  { name: "table_decorate", args: "p_organization_id uuid, p_table_id uuid, p_path text[], p_value jsonb DEFAULT NULL::jsonb", returns: "jsonb", security: "definer" },
3815
3825
  { name: "table_decorations", args: "p_organization_id uuid, p_table_id uuid", returns: "jsonb", security: "definer" },
3826
+ { name: "table_facts", args: "p_organization_id uuid", returns: "TABLE(table_id uuid, visibility text, mine boolean)", security: "definer" },
3816
3827
  { name: "table_from_example", args: "p_organization_id uuid, p_home_id uuid, p_example jsonb", returns: "jsonb", security: "definer" },
3817
3828
  { name: "table_has_a_visible_record", args: "p_user uuid, p_organization_id uuid, p_table_id uuid", returns: "boolean", security: "definer" },
3818
3829
  { name: "table_is_live", args: "p_organization_id uuid, p_table_id uuid", returns: "boolean", security: "invoker" },
@@ -5313,6 +5324,7 @@ var DOORS = {
5313
5324
  // everywhere in that organization. This is the one question the rail has to
5314
5325
  // ask, under the same wall and visibility gate as `portals` itself.
5315
5326
  portalTables: "portal_tables",
5327
+ tableFacts: "table_facts",
5316
5328
  portalCard: "portal_card",
5317
5329
  portalDeclare: "portal_declare",
5318
5330
  portalInvite: "portal_invite",
@@ -5697,6 +5709,22 @@ function staleWriteDetail(error) {
5697
5709
  };
5698
5710
  }
5699
5711
 
5712
+ // src/core/recordChanges.ts
5713
+ var listeners = /* @__PURE__ */ new Set();
5714
+ function announceRecordChanged(recordId) {
5715
+ for (const listener of [...listeners]) {
5716
+ try {
5717
+ listener(recordId);
5718
+ } catch (thrown) {
5719
+ console.error("[@ai-matrx/records] a record-change listener threw", thrown);
5720
+ }
5721
+ }
5722
+ }
5723
+ function onRecordChanged(listener) {
5724
+ listeners.add(listener);
5725
+ return () => listeners.delete(listener);
5726
+ }
5727
+
5700
5728
  // src/core/query.ts
5701
5729
  var DEFAULT_PAGE_SIZE = 50;
5702
5730
  function planQuery(query) {
@@ -5738,12 +5766,22 @@ function intersectIds(sets) {
5738
5766
 
5739
5767
  // src/core/client.ts
5740
5768
  var DEFAULT_SCHEMA = "custom";
5769
+ var A_PERSONS_REFUSAL = /* @__PURE__ */ new Set([
5770
+ "refused_by_rule",
5771
+ "stale_write",
5772
+ "invalid_argument",
5773
+ "already_exists",
5774
+ "missing_reference"
5775
+ ]);
5776
+ function isAPersonsRefusal(code) {
5777
+ return A_PERSONS_REFUSAL.has(code);
5778
+ }
5741
5779
  function sink(config) {
5742
- return config.onError ?? ((e) => (
5743
- // The defined degradation, stated: loud, with the store's own words, and
5744
- // naming where it happened. Never silence.
5745
- console.error(`[@ai-matrx/records] ${e.where}: ${e.code} \u2014 ${e.message}${e.hint ? ` (${e.hint})` : ""}`)
5746
- ));
5780
+ return config.onError ?? ((e) => {
5781
+ const line = `[@ai-matrx/records] ${e.where}: ${e.code} \u2014 ${e.message}${e.hint ? ` (${e.hint})` : ""}`;
5782
+ if (isAPersonsRefusal(e.code)) console.info(line);
5783
+ else console.error(line);
5784
+ });
5747
5785
  }
5748
5786
  function createRecordsClient(config) {
5749
5787
  const schema = config.schema ?? DEFAULT_SCHEMA;
@@ -5850,21 +5888,26 @@ function createRecordsClient(config) {
5850
5888
  },
5851
5889
  "recordUpdate"
5852
5890
  );
5891
+ if (result.ok) announceRecordChanged(record_id);
5853
5892
  return result;
5854
5893
  },
5855
5894
  async recordDelete({ record_id }) {
5856
- return callDoor(
5895
+ const result = await callDoor(
5857
5896
  DOORS.recordDelete,
5858
5897
  { p_organization_id: org, p_record_id: record_id },
5859
5898
  "recordDelete"
5860
5899
  );
5900
+ if (result.ok) announceRecordChanged(record_id);
5901
+ return result;
5861
5902
  },
5862
5903
  async recordRestore({ record_id }) {
5863
- return callDoor(
5904
+ const result = await callDoor(
5864
5905
  DOORS.recordRestore,
5865
5906
  { p_organization_id: org, p_record_id: record_id },
5866
5907
  "recordRestore"
5867
5908
  );
5909
+ if (result.ok) announceRecordChanged(record_id);
5910
+ return result;
5868
5911
  },
5869
5912
  async tableArchive({ table_id, chunk, includeTable }) {
5870
5913
  return callDoor(
@@ -5914,6 +5957,24 @@ function createRecordsClient(config) {
5914
5957
  })
5915
5958
  );
5916
5959
  },
5960
+ async tableArchived({ table_id }) {
5961
+ const kernel = await callDoor(DOORS.tableKernelId, {}, "tableArchived.kernel");
5962
+ if (!kernel.ok) return err(kernel.error);
5963
+ const PAGE = 200;
5964
+ for (let page = 0; page < 25; page += 1) {
5965
+ const answered = await this.listArchived({
5966
+ table_id: kernel.data,
5967
+ lane: "org",
5968
+ limit: PAGE,
5969
+ offset: page * PAGE
5970
+ });
5971
+ if (!answered.ok) return err(answered.error);
5972
+ const hit = answered.data.rows.find((row) => row.id === table_id);
5973
+ if (hit) return ok(hit);
5974
+ if (answered.data.total !== null) return ok(null);
5975
+ }
5976
+ return ok(null);
5977
+ },
5917
5978
  async listArchived({ table_id, lane = DEFAULT_ARCHIVE_LANE, limit = 50, offset = 0 }) {
5918
5979
  const door = await callDoor(
5919
5980
  DOORS.readRecordsArchived,
@@ -6079,6 +6140,23 @@ function createRecordsClient(config) {
6079
6140
  const { document } = unfold(row.document);
6080
6141
  return { ...document, id: row.id, organization_id: org };
6081
6142
  });
6143
+ if (doorExists(DOORS.tableFacts)) {
6144
+ const facts = await callDoor(
6145
+ DOORS.tableFacts,
6146
+ { p_organization_id: org },
6147
+ "tableList.facts"
6148
+ );
6149
+ if (facts.ok) {
6150
+ const byId = new Map((facts.data ?? []).map((f) => [f.table_id, f]));
6151
+ const me = config.actor.user_id ?? null;
6152
+ for (const table of tables) {
6153
+ const fact = byId.get(table.id);
6154
+ if (!fact) continue;
6155
+ table.visibility = fact.visibility;
6156
+ if (fact.mine && me) table.created_by = me;
6157
+ }
6158
+ }
6159
+ }
6082
6160
  tables.sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""));
6083
6161
  return ok(tables);
6084
6162
  },
@@ -6115,9 +6193,11 @@ function createRecordsClient(config) {
6115
6193
  );
6116
6194
  },
6117
6195
  async fieldDeclare({ table_id, spec }) {
6196
+ const { allow_other, ...rest } = spec;
6197
+ const p_spec = allow_other === void 0 ? rest : { ...rest, config: { ...rest.config ?? {}, allow_other } };
6118
6198
  return callDoor(
6119
6199
  DOORS.fieldDeclare,
6120
- { p_organization_id: org, p_table_id: table_id, p_spec: spec },
6200
+ { p_organization_id: org, p_table_id: table_id, p_spec },
6121
6201
  "fieldDeclare"
6122
6202
  );
6123
6203
  },
@@ -7085,14 +7165,16 @@ function createRecordsClient(config) {
7085
7165
  );
7086
7166
  },
7087
7167
  async restoreVersion({ record_id, version }) {
7088
- return callDoor(
7168
+ const result = await callDoor(
7089
7169
  DOORS.restoreVersion,
7090
7170
  { p_organization_id: org, p_record_id: record_id, p_version: version },
7091
7171
  "restoreVersion"
7092
7172
  );
7173
+ if (result.ok) announceRecordChanged(record_id);
7174
+ return result;
7093
7175
  },
7094
7176
  async valueRestore({ record_id, field_key, version }) {
7095
- return callDoor(
7177
+ const answered = await callDoor(
7096
7178
  DOORS.valueRestore,
7097
7179
  {
7098
7180
  p_organization_id: org,
@@ -7102,6 +7184,8 @@ function createRecordsClient(config) {
7102
7184
  },
7103
7185
  "valueRestore"
7104
7186
  );
7187
+ if (answered.ok) announceRecordChanged(record_id);
7188
+ return answered;
7105
7189
  },
7106
7190
  async commentThread({ record_id, include_resolved }) {
7107
7191
  return callDoor(
@@ -7883,12 +7967,30 @@ function ruleRefusal(field, rule, item, values) {
7883
7967
  }
7884
7968
  return null;
7885
7969
  case "length":
7886
- if (typeof item === "string" && item.length > Number(rule.value)) {
7970
+ if (typeof item !== "string") return null;
7971
+ if (rule.value !== void 0 && rule.value !== null && rule.value !== "" && item.length > Number(rule.value)) {
7887
7972
  return { ...base, message: `${label} is longer than ${rule.value} characters`, hint: "FLD-3." };
7888
7973
  }
7974
+ if (typeof rule.min === "number" && item !== "" && item.length < rule.min) {
7975
+ return {
7976
+ ...base,
7977
+ message: `${label} has to be at least ${rule.min} characters long`,
7978
+ hint: "FLD-3.",
7979
+ detail: { field_key: field.key, rule: "length", min: rule.min }
7980
+ };
7981
+ }
7889
7982
  return null;
7890
7983
  case "pattern":
7891
7984
  if (typeof item === "string" && !new RegExp(String(rule.value)).test(item)) {
7985
+ const example = typeof rule.example === "string" ? rule.example.trim() : "";
7986
+ if (example !== "") {
7987
+ return {
7988
+ ...base,
7989
+ message: `${label} is not written the way this field expects`,
7990
+ hint: `Enter it like ${example}.`,
7991
+ detail: { field_key: field.key, rule: "pattern", example }
7992
+ };
7993
+ }
7892
7994
  return { ...base, message: `${label} is not written the way this field expects`, hint: "FLD-3." };
7893
7995
  }
7894
7996
  return null;
@@ -7973,12 +8075,6 @@ function predictValueRefusals(fields, values, recordType) {
7973
8075
  message: `${label} takes one of its choices, and it was given a ${jsonType(item)}`,
7974
8076
  hint: "FLD-5 / FLD-6: a list field stores the id of the option RECORD, because every pick-list is already a Table."
7975
8077
  });
7976
- } else if (!UUID2.test(item)) {
7977
- found.push({
7978
- ...base,
7979
- message: `${label} was given a choice that is not one of its choices`,
7980
- hint: `REC-51: option membership. ${label} stores the id of an option record, and what it was given is not an id at all.`
7981
- });
7982
8078
  }
7983
8079
  } else if (field.type === "relation") {
7984
8080
  if (typeof item !== "string") {