@ai-matrx/records 0.36.0 → 0.39.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,102 @@
1
1
  # Changelog — @ai-matrx/records
2
2
 
3
+ ## 0.39.0
4
+
5
+ **Two things a records screen could not know: that a table's shape had changed,
6
+ and how many records it was actually looking at.**
7
+
8
+ **A table's shape now travels.** A table's columns are read by several surfaces
9
+ at once — the grid draws them, the field rail lists them, a form builder offers
10
+ them, a filter picks among them — and each called `useFields(tableId)` and got
11
+ its OWN copy. Nothing joined them, so a column added in one left every other
12
+ one drawing a table that no longer existed. Measured on production 2026-09-21
13
+ at Ridgeline Physical Therapy: "Add field" succeeded, the panel confirmed it
14
+ with a read-back and closed, and the grid beside it did not show the column
15
+ until a full page reload. The panel was not lying — the column WAS made — but
16
+ the screen next to it was showing a table that was already out of date, which
17
+ is the same thing to the person looking at it.
18
+
19
+ `tableShapeChanged(tableId)` is one table-scoped revision. `useFields`
20
+ subscribes to it and registers its interest on read; `useFieldMutation`'s
21
+ `declare`, `update` AND `retire` all announce — all three, because retyping a
22
+ column and retiring one leave exactly the same staleness, and a fix that only
23
+ knew about `declare` would have left two thirds of the class open. A listener
24
+ re-reads THROUGH THE DOOR: the tick is a nudge and never data. It is
25
+ deliberately independent of realtime, so a host that binds no realtime port
26
+ behaves identically here — realtime only makes a shape change travel between
27
+ browsers.
28
+
29
+ **A visible total instead of a shrug.** `client.list` answered `total: null`
30
+ unconditionally, so a table holding ten records told its owner *"Unknown total
31
+ — showing the 10 records loaded so far"* — a screen refusing to say a thing it
32
+ knows. A SHORT PAGE already answers the question: if the door was asked for
33
+ `limit` and handed back fewer, the reader has reached the end of the set it is
34
+ allowed to see, so that set's size is exactly `offset + rows.length`. No count
35
+ is ever asked for, no second query is made, and a full page still answers null,
36
+ because "at least this many" is not a total.
37
+
38
+ 🚨 That rests on one property of `custom.read_records`, and it is not the
39
+ obvious one. The door DOES clamp, through `custom.page_size` — the first draft
40
+ of the comment beside the inference said it did not, and writing the guard is
41
+ what caught that. What makes it safe is what the helper does at the ceiling: it
42
+ REFUSES BY NAME and reads nothing, rather than quietly serving fewer. So a
43
+ successful call that returns fewer rows than it asked for has reached the end
44
+ of the set, and nothing else. `src/__tests__/page-truth.test.ts` asserts the
45
+ refusal against the live store, so the day it softens into a silent shrink the
46
+ suite goes red and names the inference that has to change with it.
47
+
48
+ ## 0.38.0
49
+
50
+ **The parity floor this package types is asserted against the one the store ships.**
51
+
52
+ `checkbox` landed as the fourteenth parity type and `store.generated.ts` picked it
53
+ up — but nothing made that agreement a test. Everything that spelled the floor out
54
+ by hand went stale in the same hour: three of the store's own refusal hints, the
55
+ `FieldTypeWord` doc comment, `UseCaseField.parityType`'s doc comment and the
56
+ use-case README's list, all still saying thirteen. A person who asked for a
57
+ checkbox could be told by the store that checkboxes do not exist.
58
+
59
+ * `src/__tests__/real-doors.test.ts` gains three cases, in the one suite that runs
60
+ against the live store: `PARITY_FIELD_TYPES` is exactly
61
+ `custom.parity_field_types()` name for name (a diff NAMES the missing type, it
62
+ never prints a count); every parity type and every behaviour the store ships is
63
+ a legal `parityType` word in `RESOLVABLE_FIELD_TYPE_WORDS`; and `checkbox` is
64
+ present with behaviour `boolean`, with a compile-time assertion that the
65
+ `ParityFieldType` union still admits it. Shown RED: with the `checkbox` row
66
+ removed from `store.generated.ts` all three fail and the first one says
67
+ "expected 'attachment, currency, …' to be 'attachment, checkbox, …'".
68
+ * The four places that wrote the count or the list out by hand now point at the
69
+ derived set instead. No number is written down anywhere in this package.
70
+ * `store.generated.ts` regenerated against the live store (542 doors, 1023 entity
71
+ tokens); `pnpm check:store-registry` was RED on that drift before this release.
72
+
73
+ **Consumer action:** none.
74
+
75
+ ## 0.37.0
76
+
77
+ **Three doors the store already had and this client could not reach.**
78
+
79
+ Until this release the only way to a booking page or a saved view was to ask an
80
+ agent, because no method on `RecordsClient` called the doors that make them —
81
+ so the agent was not the FASTER path to those things, it was the ONLY one.
82
+
83
+ * `bookingDeclare` — `custom.booking_declare`, the owner's side of a booking
84
+ page. `admin` on the Table, because a booking page decides what a stranger may
85
+ write into it and which hours of the organization's week are on offer. It does
86
+ NOT publish: `anonPublish` is still the one act that opens a public surface,
87
+ for a booking page exactly as for a form. `BookingAvailability`,
88
+ `BookingWindow` and `BookingDeclared` are the answer's shape — a screen shows
89
+ the availability that came BACK, never the draft it sent, because the store
90
+ owns every default.
91
+ * `views` — `custom.views`, the saved views of a Table. Narrowed in SQL as the
92
+ definer to Tables the caller can already open, so a list can never reveal a
93
+ Table.
94
+ * `viewDeclare` — `custom.view_declare`. VIEWER on the Table, not editor:
95
+ writing down a question about a Table is not changing it. A subscription is
96
+ written OVER a saved view, so without this door a person who had never saved
97
+ one had nothing to subscribe to and "email me this every Monday" had nowhere
98
+ to land.
99
+
3
100
  ## 0.36.0
4
101
 
5
102
  Republish of 0.35.0's contents. 0.35.0 never reached npm: its gate was red on
@@ -668,6 +668,7 @@ var STORE_DOORS = [
668
668
  { name: "assert_client_may_change", args: "p_organization_id uuid, p_subject_id uuid, p_door text, p_required permission_level DEFAULT 'editor'::permission_level, p_subject_word text DEFAULT 'record'::text", returns: "void", security: "invoker" },
669
669
  { name: "assert_client_may_open", args: "p_organization_id uuid, p_subject_id uuid, p_door text, p_required permission_level DEFAULT 'viewer'::permission_level, p_subject_word text DEFAULT 'record'::text", returns: "void", security: "invoker" },
670
670
  { name: "assert_client_may_reach", args: "p_organization_id uuid, p_door text", returns: "void", security: "invoker" },
671
+ { name: "assert_columns_are_defined", args: "p_organization_id uuid, p_record_id uuid, p_patch jsonb", returns: "void", security: "definer" },
671
672
  { name: "assert_entity_door", args: "p_organization_id uuid, p_door text", returns: "void", security: "definer" },
672
673
  { name: "assert_entity_is_organization_scoped", args: "p_token text, p_label text, p_scoped boolean", returns: "void", security: "invoker" },
673
674
  { name: "assert_may_know_table", args: "p_organization_id uuid, p_table_id uuid, p_door text", returns: "void", security: "invoker" },
@@ -722,6 +723,7 @@ var STORE_DOORS = [
722
723
  { name: "comment_mention_deliver", args: "p_organization_id uuid, p_record_id uuid, p_table_id uuid, p_comment_id uuid, p_recipient uuid, p_author_name text, p_record_title text, p_body text", returns: "uuid", security: "definer" },
723
724
  { name: "comment_thread", args: "p_organization_id uuid, p_record_id uuid, p_include_resolved boolean DEFAULT false", returns: "jsonb", security: "definer" },
724
725
  { name: "comment_write", args: "p_organization_id uuid, p_record_id uuid, p_body text, p_anchor jsonb DEFAULT '{}'::jsonb, p_parent_comment_id uuid DEFAULT NULL::uuid, p_mentions uuid[] DEFAULT NULL::uuid[]", returns: "jsonb", security: "definer" },
726
+ { name: "computed_block", args: "p_block jsonb", returns: "jsonb", security: "invoker" },
725
727
  { name: "computed_provenance", args: "p_organization_id uuid, p_record_id uuid", returns: "TABLE(field_key text, field_id uuid, value jsonb, rule_id uuid, rule_version integer, computed_at timestamp with time zone)", security: "invoker" },
726
728
  { name: "containment_chain", args: "p_organization_id uuid, p_record_id uuid", returns: "TABLE(ancestor_id uuid, depth integer)", security: "invoker" },
727
729
  { name: "containment_depth_ceiling", args: "p_organization_id uuid DEFAULT NULL::uuid", returns: "integer", security: "invoker" },
@@ -753,6 +755,7 @@ var STORE_DOORS = [
753
755
  { name: "derive_visibility", args: "p_container_type text, p_container_id uuid", returns: "TABLE(item_type text, item_id uuid, depth integer, max_level permission_level)", security: "definer" },
754
756
  { name: "derived_value", args: "p_organization_id uuid, p_record_id uuid, p_field_data jsonb, p_values jsonb DEFAULT NULL::jsonb", returns: "jsonb", security: "invoker" },
755
757
  { name: "derived_values", args: "p_organization_id uuid, p_record_id uuid", returns: "jsonb", security: "invoker" },
758
+ { name: "derived_values_of", args: "v_rec custom.record", returns: "jsonb", security: "invoker" },
756
759
  { name: "doc_content_hash", args: "p_body text", returns: "text", security: "invoker" },
757
760
  { name: "doc_format_value", args: "p_field_data jsonb, p_value jsonb", returns: "text", security: "invoker" },
758
761
  { name: "doc_letterhead", args: "p_organization_id uuid", returns: "jsonb", security: "definer" },
@@ -1018,6 +1021,7 @@ var STORE_DOORS = [
1018
1021
  { name: "record_table", args: "p_organization_id uuid, p_record_id uuid", returns: "uuid", security: "definer" },
1019
1022
  { name: "record_update", args: "p_organization_id uuid, p_record_id uuid, p_patch jsonb, p_expected_version integer DEFAULT NULL::integer", returns: "integer", security: "definer" },
1020
1023
  { name: "record_values", args: "p_organization_id uuid, p_record_id uuid", returns: "jsonb", security: "invoker" },
1024
+ { name: "record_values_of", args: "p_row custom.record", returns: "jsonb", security: "invoker" },
1021
1025
  { name: "record_values_versioned", args: "p_organization_id uuid, p_record_id uuid", returns: "TABLE(field_key text, field_id uuid, value jsonb, value_version integer, source jsonb, absent_reason text, actor text, on_behalf_of text, written_at timestamp with time zone, alternates jsonb)", security: "definer" },
1022
1026
  { name: "record_write", args: "p_organization_id uuid, p_table_id uuid, p_data jsonb", returns: "uuid", security: "definer" },
1023
1027
  { name: "record_write_many", args: "p_organization_id uuid, p_table_id uuid, p_rows jsonb[], p_ids uuid[] DEFAULT NULL::uuid[]", returns: "uuid[]", security: "definer" },
@@ -1221,7 +1225,7 @@ var STORE_REFUSAL_CODES = [
1221
1225
  { code: "22023", raised_by: "_booking_availability,_entity_custom_fields_guard,_value_envelope,_work_shape_guard,actor_word,agg_assert_key,agg_sql,anon_token_issue,booking_declare,containment_parent,dashboard_block_normalize,dashboard_window_sql,doc_render_body,doc_render_write,doc_signature_write,enrich_declare,enrich_land,enrich_normalize,enrich_pin,entity_value_write,external_history_event,external_source_declare,external_stub_upsert,external_write_through,field_history,io_export,io_import_begin,io_import_finish,io_import_plan,io_import_report,io_proposal_accept,migrate_merge,migrate_reclass,my_levels,organization_clear,page_size,pin_agent_cells,pipeline_move,portal_declare,provenance_write,query_by_coordinates,query_rollup,record_update,record_write_many,relation_carry,rule_eval,rule_members,share_grant,share_lane_set,sign_request_create,sign_request_sign,subscription_declare,table_rules,visibility_as_of,work_approval_request,work_list,work_slot_hold" },
1222
1226
  { code: "23503", raised_by: "_booking_availability,_checklist_instantiate,_containment_guard,_field_document_for,_pipeline_stage_key,_record_rule_uses,agg_digest_assemble,anon_publish,anon_rate_take,anon_token_issue,anon_write,assert_organization_wall,booking_cancel,booking_confirm,booking_declare,booking_hold,booking_reschedule,capture_publish,checklist_declare,checklist_steps_table,comment_write,dashboard_declare,delete_rule,doc_sign,doc_template_save,enrich_declare,enrich_due,enrich_land,form_declare,form_submit,home_add,inbound_declare,inbound_mail_land,inbound_set,io_comment_write,io_import_begin,io_import_finish,io_import_report,io_import_rows,io_proposal_accept,migrate_demote,migrate_promote,pipeline_board,pipeline_declare,pipeline_move,pipeline_read,pipeline_transition_refusal,promote_field,promote_table,record_applicability,record_reparent,relation_carry,relation_own,resolve_first_match,rule_applies,rule_declare,rule_eval,rule_members,rule_run,sign_request_create,subscription_mute,subscription_preview,view_declare,work_set_state,work_take_assignment,work_template_instantiate" },
1223
1227
  { code: "23505", raised_by: "_unique_rule_holds,doc_sign,doc_signature_write,entity_field_declare,field_declare,home_add,inbound_declare,io_proposal_accept,relation_carry,relation_own,share_grant,sign_request_cancel,sign_request_create,work_approval_decide,work_slot_hold" },
1224
- { code: "23514", raised_by: "_checklist_instantiate,_checklist_step_guard,_claimed_column_guard,_containment_guard,_dated_values_guard,_derived_fields,_entity_custom_fields_guard,_field_class_guard,_field_document_for,_field_shape_guard,_field_type_parity_guard,_merge_field_shape_guard,_merge_field_temporal_guard,_promoted_field_cap_guard,_record_rule_uses,_resolve_choice_words,_rule_shape_guard,_rule_topology_guard,_store_relation_edge_names_its_field,_table_columns_word_guard,_table_shape_guard,_undeclared_key_guard,_value_envelope,_work_shape_guard,_workdoors_approval_guard,assert_entity_is_organization_scoped,capture_publish,capture_sheet_declare,checklist_declare,checklist_step_complete,doc_sign,doc_signature_write,doc_template_save,entity_field_declare,entity_field_retire,entity_field_update,entity_records_find,entity_table,field_declare,field_retire,field_update,home_add,io_import_rows,migrate_merge,relation_carry,relation_own,sign_request_create,sign_request_sign,table_declare,validate_value_envelope,validate_values,work_set_state,work_slots_declare,work_take_assignment,work_template_instantiate" },
1228
+ { code: "23514", raised_by: "_checklist_instantiate,_checklist_step_guard,_containment_guard,_dated_values_guard,_derived_fields,_entity_custom_fields_guard,_field_class_guard,_field_document_for,_field_shape_guard,_field_type_parity_guard,_merge_field_shape_guard,_merge_field_temporal_guard,_promoted_field_cap_guard,_record_rule_uses,_resolve_choice_words,_rule_shape_guard,_rule_topology_guard,_store_relation_edge_names_its_field,_table_columns_word_guard,_table_shape_guard,_undeclared_key_guard,_value_envelope,_work_shape_guard,_workdoors_approval_guard,assert_columns_are_defined,assert_entity_is_organization_scoped,capture_publish,capture_sheet_declare,checklist_declare,checklist_step_complete,doc_sign,doc_signature_write,doc_template_save,entity_field_declare,entity_field_retire,entity_field_update,entity_records_find,entity_table,field_declare,field_retire,field_update,home_add,io_import_rows,migrate_merge,relation_carry,relation_own,sign_request_create,sign_request_sign,table_declare,validate_value_envelope,validate_values,work_set_state,work_slots_declare,work_take_assignment,work_template_instantiate" },
1225
1229
  { code: "40001", raised_by: "has_visibility_at" },
1226
1230
  { code: "42501", raised_by: "_doc_render_immutable,_doc_signature_immutable,_field_definition_write,_field_write_door,_rule_definition_write,_store_door,anon_capture,anon_inbound_land,anon_publish,anon_token_issue,anon_token_verify,anon_write,assert_client_may_change,assert_client_may_open,assert_client_may_reach,assert_may_know_table,assert_organization_admin,assert_store_door,booking_confirm,booking_declare,booking_hold,capture_submit,checklist_run,comment_write,conversation_scope_bind,conversation_scope_unbind,enrich_declare,enrich_due,enrich_land,enrich_normalize,entity_field_declare,entity_field_retire,entity_field_update,entity_value_write,external_write_through,form_declare,form_submit,inbound_mail_land,io_comment_resolve,io_comment_write,io_import_begin,io_import_finish,io_restore,migrate_purge,migrate_reclass,my_levels,organization_clear,portal_declare,portal_invitation,portal_invite,portal_principal_bind,promote_table,query_visibility_parity,read_record,read_records,record_table,share_grant,subscription_mute,subscription_preview,visibility_as_of,work_approval_decide,work_approval_read,work_approval_request,work_person" },
1227
1231
  { code: "42703", raised_by: "entity_table,portal_declare" },
@@ -1793,6 +1797,7 @@ var ENTITY_TOKENS = [
1793
1797
  "mandate_advance_batch_row",
1794
1798
  "mandate_binding",
1795
1799
  "mandate_binding_legacy",
1800
+ "mandate_goal_clauses",
1796
1801
  "mandate_legacy",
1797
1802
  "mandate_observation",
1798
1803
  "mandate_reference",
@@ -2428,6 +2433,10 @@ var DOORS = {
2428
2433
  // VISITOR's three (`booking_public`, `booking_hold`, `booking_confirm`) are
2429
2434
  // deliberately absent: they are server-lane and a browser never calls them.
2430
2435
  bookings: "bookings",
2436
+ // The OWNER's side of a booking page, and the door that had no client method at
2437
+ // all until 2026-09-21: without it the only way to get a booking page was to ask
2438
+ // an agent, which made the agent the ONLY path rather than the faster one.
2439
+ bookingDeclare: "booking_declare",
2431
2440
  // PRODUCTS row 9 — "pull my spreadsheet in". W4-IO built `io_import_open` and
2432
2441
  // `io_import_rows`; lane IMPORT gave a run an IDENTITY (the file's sha-256, so
2433
2442
  // a re-import doubles nothing), a duplicate key, a policy, per-row outcomes
@@ -2560,6 +2569,13 @@ var DOORS = {
2560
2569
  // the summary it would send right now without sending it.
2561
2570
  subscriptionDeclare: "subscription_declare",
2562
2571
  subscriptionPreview: "subscription_preview",
2572
+ // A SUBSCRIPTION IS OVER A SAVED VIEW, so a person who has never saved one has
2573
+ // nothing to subscribe to. These two are the saved-view pair (lane DIGESTS'
2574
+ // `digests_a_saved_view_has_a_door.sql`), unreachable from this client until
2575
+ // 2026-09-21: `views` lists this Table's, `viewDeclare` writes one. A LIST IS A
2576
+ // DOOR, never a grant on `platform.saved_view` behind it.
2577
+ views: "views",
2578
+ viewDeclare: "view_declare",
2563
2579
  // The cadence CATALOGUE a person's screen may ask for. `agg_subscription_cadences`
2564
2580
  // above is the notifier's own and holds no client grant; this door delegates to
2565
2581
  // it, so a picker and the digest runner can never offer different words.
@@ -2950,7 +2966,8 @@ function createRecordsClient(config) {
2950
2966
  const { document, hidden } = unfold(row.document);
2951
2967
  return { id: row.id, document, level: row.level, hidden };
2952
2968
  });
2953
- return ok({ rows, total: null });
2969
+ const total = rows.length < limit ? offset + rows.length : null;
2970
+ return ok({ rows, total });
2954
2971
  },
2955
2972
  async query(input) {
2956
2973
  const plan = planQuery(input);
@@ -3334,6 +3351,30 @@ function createRecordsClient(config) {
3334
3351
  "bookings"
3335
3352
  );
3336
3353
  },
3354
+ async bookingDeclare(args) {
3355
+ return callDoor(
3356
+ DOORS.bookingDeclare,
3357
+ {
3358
+ p_organization_id: org,
3359
+ p_table_id: args.table_id,
3360
+ p_title: args.title,
3361
+ p_questions: args.questions,
3362
+ // AN EMPTY OBJECT IS NOT A MISTAKE. The store owns every default, so a
3363
+ // page declared with no availability comes back Monday-to-Friday,
3364
+ // nine-to-five, in the organization's own zone — and the screen draws
3365
+ // what came BACK rather than what it sent.
3366
+ p_availability: args.availability ?? {},
3367
+ p_presentation: args.presentation ?? {},
3368
+ p_submission_cap: args.submission_cap ?? null,
3369
+ p_quarantine_rule_id: args.quarantine_rule_id ?? null,
3370
+ p_notify_rule_id: args.notify_rule_id ?? null,
3371
+ p_form_id: args.form_id ?? null,
3372
+ p_slug: args.slug ?? null,
3373
+ p_home_id: args.home_id ?? null
3374
+ },
3375
+ "bookingDeclare"
3376
+ );
3377
+ },
3337
3378
  async formDeclare(args) {
3338
3379
  return callDoor(
3339
3380
  DOORS.formDeclare,
@@ -3758,6 +3799,20 @@ function createRecordsClient(config) {
3758
3799
  "subscriptionPreview"
3759
3800
  );
3760
3801
  },
3802
+ async views(args) {
3803
+ return callDoor(
3804
+ DOORS.views,
3805
+ { p_organization_id: org, p_table_id: args?.table_id ?? null },
3806
+ "views"
3807
+ );
3808
+ },
3809
+ async viewDeclare({ table_id, spec }) {
3810
+ return callDoor(
3811
+ DOORS.viewDeclare,
3812
+ { p_organization_id: org, p_table_id: table_id, p_spec: spec },
3813
+ "viewDeclare"
3814
+ );
3815
+ },
3761
3816
  async subscriptionMute({ rule_id, muted }) {
3762
3817
  return callDoor(
3763
3818
  DOORS.subscriptionMute,