declare const ACTOR_VOCABULARY: readonly ["user", "agent", "system"]; declare const ABSENCE_REASONS: readonly ["never asked", "none", "refused", "conflicting"]; declare const STORAGE_MODES: readonly ["light", "heavy"]; /** FLD-11's parity floor, each with the behavior it is MADE OF (FLD-1). */ declare const PARITY_FIELD_TYPES: readonly [{ readonly parity_type: "attachment"; readonly behavior: "relation"; readonly made_of: "REC-31: a File record reached through a relation, target the kernel `File` Table"; }, { readonly parity_type: "checkbox"; readonly behavior: "boolean"; readonly made_of: "a ticked or unticked answer, stored as a REAL boolean — and a record that never answered holds neither, so filters and group-by see three states, not two"; }, { readonly parity_type: "currency"; readonly behavior: "range"; readonly made_of: "FLD-N-1: a number whose UNIT is the currency and whose format is currency — on the Field, never in presentation"; }, { readonly parity_type: "datetime"; readonly behavior: "range"; readonly made_of: "a range of kind date or datetime — the dated modifier says the VALUES are dated, which is a different question"; }, { readonly parity_type: "email"; readonly behavior: "text"; readonly made_of: "text whose format is email"; }, { readonly parity_type: "formula"; readonly behavior: "formula"; readonly made_of: "FLD-9: a declared computation, on read or on write, evaluated by custom.rule_eval"; }, { readonly parity_type: "lookup"; readonly behavior: "formula"; readonly made_of: "a Value read THROUGH a relation — config.via names the relation field, config.pick the Value on the far side"; }, { readonly parity_type: "member"; readonly behavior: "relation"; readonly made_of: "a person: a relation whose target is the kernel `Person` Table"; }, { readonly parity_type: "multi_select"; readonly behavior: "list"; readonly made_of: "many choices from the same Table — the multi modifier, never a second behaviour"; }, { readonly parity_type: "percent"; readonly behavior: "range"; readonly made_of: "a number whose unit is % and whose format is percent"; }, { readonly parity_type: "phone"; readonly behavior: "text"; readonly made_of: "text whose format is phone"; }, { readonly parity_type: "rollup"; readonly behavior: "formula"; readonly made_of: "an aggregate ALONG a relation — config.via, config.of, config.agg; far-side ids are taken DISTINCT, so nothing is counted twice"; }, { readonly parity_type: "select"; readonly behavior: "list"; readonly made_of: "one choice from a Table shown as a list (FLD-5/FLD-6)"; }, { readonly parity_type: "url"; readonly behavior: "text"; readonly made_of: "text whose format is url, with the pattern Rule that makes the format enforceable"; }]; /** * THE ONE REGISTRY: every kind a column can be DECLARED as, from * `custom.field_kinds()`. `parity` says whether the kind is one of FLD-11's parity * floor; the rest are the behaviors and formats the declaration door answers to and * nothing else enumerated — which is how `signature` came to be a shape * `custom.doc_sign` accepted and no screen could offer. */ declare const DECLARABLE_FIELD_KINDS: readonly [{ readonly kind: "address"; readonly behavior: "text"; readonly parity: false; readonly made_of: "P4: behaviour text, format address; the value is {street, city, region, postal_code, country}, every part optional, at least one filled"; }, { readonly kind: "attachment"; readonly behavior: "relation"; readonly parity: true; readonly made_of: "REC-31: a File record reached through a relation, target the kernel `File` Table"; }, { readonly kind: "barcode"; readonly behavior: "text"; readonly parity: false; readonly made_of: "P4: behaviour text, format barcode; one printable line a scanner reads, config.symbology optional (custom.barcode_symbologies()), EAN/UPC digits and check digit enforced"; }, { readonly kind: "button"; readonly behavior: "text"; readonly parity: false; readonly made_of: "NOTION-PROPS-2: behaviour text, format button; holds no value. config.button says what a press does through custom.button_press: open_url (url, {{field id}} filled from the row), run_automation (one of the table's automations, on this row) or run_action (one of its row actions, which sets fields), or add_row (a new row in this table or table_id's, prefilled from this row by prefill, opened); a button with no do yet is a label only"; }, { readonly kind: "checkbox"; readonly behavior: "boolean"; readonly parity: true; readonly made_of: "a ticked or unticked answer, stored as a REAL boolean — and a record that never answered holds neither, so filters and group-by see three states, not two"; }, { readonly kind: "count"; readonly behavior: "formula"; readonly parity: false; readonly made_of: "P4: a roll-up whose config.agg is count, with format count: how many records a many-relation points at, as a number of its own"; }, { readonly kind: "created_by"; readonly behavior: "formula"; readonly parity: false; readonly made_of: "P4: a formula Field whose config.system is created_by: who made the record, {id, name}, filled by the store from the record's writer on read and never typed"; }, { readonly kind: "currency"; readonly behavior: "range"; readonly parity: true; readonly made_of: "FLD-N-1: a number whose UNIT is the currency and whose format is currency — on the Field, never in presentation"; }, { readonly kind: "datetime"; readonly behavior: "range"; readonly parity: true; readonly made_of: "a range of kind date or datetime — the dated modifier says the VALUES are dated, which is a different question"; }, { readonly kind: "duration"; readonly behavior: "range"; readonly parity: false; readonly made_of: "P4: behaviour range, format duration, unit seconds; a length of time kept as seconds, never below zero"; }, { readonly kind: "email"; readonly behavior: "text"; readonly parity: true; readonly made_of: "text whose format is email"; }, { readonly kind: "entity_reference"; readonly behavior: "relation"; readonly parity: false; readonly made_of: "SC-R / P12: a relation whose target mode is any (REL-8), restricted by config.allowed_types to the platform kinds it may name (custom.entity_reference_kinds() — agents, notes, web sites, workbooks, datasets, conversations …); each value is {token, id}, the edge a platform.associations row record → . File and Person stay relations to their kernel Tables"; }, { readonly kind: "formula"; readonly behavior: "formula"; readonly parity: true; readonly made_of: "FLD-9: a declared computation, on read or on write, evaluated by custom.rule_eval"; }, { readonly kind: "json"; readonly behavior: "text"; readonly parity: false; readonly made_of: "W2-A: behaviour text, format json; any JSON value (object, list, number, words, yes/no) kept as jsonb, shown collapsed"; }, { readonly kind: "long_text"; readonly behavior: "text"; readonly parity: false; readonly made_of: "behaviour text with format long and config.multiline, for several lines rather than one"; }, { readonly kind: "lookup"; readonly behavior: "formula"; readonly parity: true; readonly made_of: "a Value read THROUGH a relation — config.via names the relation field, config.pick the Value on the far side"; }, { readonly kind: "member"; readonly behavior: "relation"; readonly parity: true; readonly made_of: "a person: a relation whose target is the kernel `Person` Table"; }, { readonly kind: "modified_by"; readonly behavior: "formula"; readonly parity: false; readonly made_of: "P4: a formula Field whose config.system is modified_by: who last changed the record, {id, name}, filled by the store on read and never typed"; }, { readonly kind: "multi_select"; readonly behavior: "list"; readonly parity: true; readonly made_of: "many choices from the same Table — the multi modifier, never a second behaviour"; }, { readonly kind: "number"; readonly behavior: "range"; readonly parity: false; readonly made_of: "a plain number: behaviour range with no date kind and neither the currency nor the percent format"; }, { readonly kind: "percent"; readonly behavior: "range"; readonly parity: true; readonly made_of: "a number whose unit is % and whose format is percent"; }, { readonly kind: "phone"; readonly behavior: "text"; readonly parity: true; readonly made_of: "text whose format is phone"; }, { readonly kind: "rating"; readonly behavior: "range"; readonly parity: false; readonly made_of: "P4: behaviour range, format rating; a whole number of stars from 0 to its max Rule (1 to 10, default 5), shown as stars"; }, { readonly kind: "relation"; readonly behavior: "relation"; readonly parity: false; readonly made_of: "REL / FLD-11: a relation a person aims themselves — relation_target names a Table of this organization, and it is a BEHAVIOUR, never a parity type of its own"; }, { readonly kind: "rich_text"; readonly behavior: "text"; readonly parity: false; readonly made_of: "P4: behaviour text, format rich_text; Markdown with no raw HTML and no script links, shown formatted"; }, { readonly kind: "rollup"; readonly behavior: "formula"; readonly parity: true; readonly made_of: "an aggregate ALONG a relation — config.via, config.of, config.agg; far-side ids are taken DISTINCT, so nothing is counted twice"; }, { readonly kind: "select"; readonly behavior: "list"; readonly parity: true; readonly made_of: "one choice from a Table shown as a list (FLD-5/FLD-6)"; }, { readonly kind: "signature"; readonly behavior: "text"; readonly parity: false; readonly made_of: "VAL-10: behaviour text whose format is signature — the one shape custom.doc_sign accepts, sealed by custom.doc_signature_write and checked by custom.doc_signature_intact"; }, { readonly kind: "status"; readonly behavior: "list"; readonly parity: false; readonly made_of: "P4: a single-choice list with format status; config.status_groups puts each choice key in todo, in_progress or done (Notion's Status)"; }, { readonly kind: "text"; readonly behavior: "text"; readonly parity: false; readonly made_of: "plain text: behaviour text with no format — it carries no parity type, which is exactly what custom.parity_type answers for it"; }, { readonly kind: "time"; readonly behavior: "text"; readonly parity: false; readonly made_of: "W2-A: behaviour text, format time; a time of day with no date kept as 24-hour HH:MM or HH:MM:SS (zero-padded, so its text order is its time order); HOUR, MINUTE and SECOND read it"; }, { readonly kind: "url"; readonly behavior: "text"; readonly parity: true; readonly made_of: "text whose format is url, with the pattern Rule that makes the format enforceable"; }]; /** The node kinds `custom.rule_eval` evaluates, and the lane that owns each. */ declare const RULE_NODE_KINDS: readonly [{ readonly node: "actor_at_least"; readonly evaluated_by: "PIPELINES"; }, { readonly node: "add"; readonly evaluated_by: "W1-RULE"; }, { readonly node: "and"; readonly evaluated_by: "W1-RULE"; }, { readonly node: "concat"; readonly evaluated_by: "W1-RULE"; }, { readonly node: "const"; readonly evaluated_by: "W1-RULE"; }, { readonly node: "contains"; readonly evaluated_by: "NOTION-Q"; }, { readonly node: "div"; readonly evaluated_by: "W1-RULE"; }, { readonly node: "eq"; readonly evaluated_by: "W1-RULE"; }, { readonly node: "field"; readonly evaluated_by: "W1-RULE"; }, { readonly node: "fx.abs"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.add"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.and"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.arraycompact"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.arrayflatten"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.arrayjoin"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.arrayslice"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.arrayunique"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.at"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.autonumber"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.average"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.blank"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.ceiling"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.concatenate"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.contains"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.count"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.counta"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.countall"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.created_time"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.current"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.current1"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.current2"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.dateadd"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.datediff"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.datestr"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.datetime_diff"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.datetime_format"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.datetime_parse"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.day"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.div"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.encode_url_component"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.eq"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.error"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.even"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.every"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.exp"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.false"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.filter"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.find"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.findfirst"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.first"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.floor"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.fromnow"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.gt"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.gte"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.hour"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.if"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.includes"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.index"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.int"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.is_after"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.is_before"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.is_same"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.isblank"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.iserror"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.join"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.last"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.last_modified_time"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.left"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.len"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.length"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.log"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.lower"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.lt"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.lte"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.map"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.max"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.median"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.mid"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.min"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.minute"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.mod"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.modified_time"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.month"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.mul"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.ne"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.neg"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.not"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.now"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.odd"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.or"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.power"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.record_id"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.recprop"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.regex_extract"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.regex_match"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.regex_replace"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.regexreplace"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.replace"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.rept"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.reverse"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.right"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.round"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.rounddown"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.roundup"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.search"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.second"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.set_locale"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.set_timezone"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.slice"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.some"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.sort"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.split"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.sqrt"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.sub"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.substitute"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.sum"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.switch"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.t"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.timestr"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.today"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.tonow"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.trim"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.true"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.unique"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.upper"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.value"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.weekday"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.weeknum"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.workday"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.workday_diff"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.xor"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "fx.year"; readonly evaluated_by: "GRID-PRIMITIVES G3 (custom.formula_eval)"; }, { readonly node: "gt"; readonly evaluated_by: "W1-RULE"; }, { readonly node: "gte"; readonly evaluated_by: "W1-RULE"; }, { readonly node: "has_all"; readonly evaluated_by: "NOTION-Q"; }, { readonly node: "has_any"; readonly evaluated_by: "NOTION-Q"; }, { readonly node: "has_none"; readonly evaluated_by: "NOTION-Q"; }, { readonly node: "is_empty"; readonly evaluated_by: "NOTION-Q"; }, { readonly node: "is_exactly"; readonly evaluated_by: "NOTION-Q"; }, { readonly node: "is_not_empty"; readonly evaluated_by: "NOTION-Q"; }, { readonly node: "length"; readonly evaluated_by: "W1-RULE"; }, { readonly node: "linked"; readonly evaluated_by: "NOTION-Q"; }, { readonly node: "lt"; readonly evaluated_by: "W1-RULE"; }, { readonly node: "lte"; readonly evaluated_by: "W1-RULE"; }, { readonly node: "matches"; readonly evaluated_by: "W1-RULE"; }, { readonly node: "merge_field"; readonly evaluated_by: "W5-MERGE-B"; }, { readonly node: "mul"; readonly evaluated_by: "W1-RULE"; }, { readonly node: "ne"; readonly evaluated_by: "W1-RULE"; }, { readonly node: "not"; readonly evaluated_by: "W1-RULE"; }, { readonly node: "not_contains"; readonly evaluated_by: "NOTION-Q"; }, { readonly node: "or"; readonly evaluated_by: "W1-RULE"; }, { readonly node: "parent_field"; readonly evaluated_by: "W1-RULE-APPLY"; }, { readonly node: "present"; readonly evaluated_by: "W1-RULE"; }, { readonly node: "previous"; readonly evaluated_by: "PIPELINES"; }, { readonly node: "sibling_count"; readonly evaluated_by: "STAGE-RULES"; }, { readonly node: "stage_count"; readonly evaluated_by: "PIPELINES"; }, { readonly node: "sub"; readonly evaluated_by: "W1-RULE"; }, { readonly node: "within"; readonly evaluated_by: "CHAIR-FILTER"; }]; /** * VAL-8. Who wrote a value. `user`, `agent`, `system` — enforced by every write * door, and the three retired words (`human`, `ai`, `code`) are translated by * the store's own `custom.actor_word`, never by a caller. */ type Actor = (typeof ACTOR_VOCABULARY)[number]; /** * VAL-2. Why a value is missing. A value is never blank for an unexplained * reason: it either holds something or it says one of these four words. */ type AbsenceReason = (typeof ABSENCE_REASONS)[number]; /** * REC-4. `heavy` is indexed storage with Rules on write; `light` is * whole-document storage with Rules on read. Per Table, one store. */ type StorageMode = (typeof STORAGE_MODES)[number]; /** FLD-11. The parity floor: the field type a person picks in the UI. */ type ParityFieldType = (typeof PARITY_FIELD_TYPES)[number]["parity_type"]; /** * THE ONE REGISTRY, as a type: every kind a column can be DECLARED as. * * `ParityFieldType` above is the parity FLOOR — the answer to "which of the fourteen * is this field". It is not the answer to "what may somebody pick", and reading it as * though it were is exactly how `signature` came to be a field shape the store accepted * (`custom.doc_sign` takes no other) and no screen could offer: VERIFIER-10, F6. * `custom.field_kinds()` is the registry, and this is generated from it. */ type DeclarableFieldKind = (typeof DECLARABLE_FIELD_KINDS)[number]["kind"]; /** FLD-1. The five behaviors a Field can have. One, exactly, and closed. */ type FieldBehavior = (typeof PARITY_FIELD_TYPES)[number]["behavior"]; /** The node kinds `custom.rule_eval` evaluates. */ type RuleNodeKind = (typeof RULE_NODE_KINDS)[number]["node"]; /** * REC-32. The seven table types, closed. The parenthesised names are the * retired ones the doctrine records; never coin an eighth. */ declare const TABLE_TYPES: readonly ["entity", "detail", "reference", "ledger", "restricted", "system", "deprecated"]; type TableType = (typeof TABLE_TYPES)[number]; /** REL-2. What happens to the far side, separate from flavour. */ declare const ON_DELETE: readonly ["cascade", "set_null", "restrict"]; type OnDelete = (typeof ON_DELETE)[number]; /** FLD-7. Where a Field's values come from. */ declare const FIELD_SOURCES: readonly ["manual", "formula", "agent", "synced"]; type FieldSource = (typeof FIELD_SOURCES)[number]; /** FLD-9. A Formula declares whether it computes on read or on write. */ declare const COMPUTE_ON: readonly ["read", "write"]; type ComputeOn = (typeof COMPUTE_ON)[number]; /** * AGT-7 / FLD-12. A Field's sensitivity sets a ceiling a merge field can never * widen — and these are THE STORE'S four words, checked against its own guard. * * This list used to read `standard · sensitive · secret`, which the store has * never accepted: writing a Field with `sensitivity: "standard"` is refused * with "the field has to say how sensitive its values are, and it says * standard". The knob `custom/field_sensitivity_levels` keys off these same * four words to decide which level may read and edit each one, so a package * carrying three different words could never have agreed with it. Corrected * against the live store, 2026-09-18. */ declare const FIELD_SENSITIVITIES: readonly ["public", "internal", "confidential", "restricted"]; type FieldSensitivity = (typeof FIELD_SENSITIVITIES)[number]; /** * FLD-12. WHETHER AN AGENT MAY SEE A FIELD'S VALUES, and these are the STORE'S * four words — not the merge field's delivery tiers. * * `Field.context_policy` used to be typed as `Delivery` here, which is a * different question with a different vocabulary: writing a Field with * `context_policy: "inline"` is refused with "the field has to say * whether an agent may see its values, and it says inline". Delivery is about * HOW MUCH of a value a merge field carries into a turn; this is about WHETHER * the value reaches an agent at all, and conflating them let a screen offer a * word the store has never accepted. Corrected against the live store, * 2026-09-18. */ declare const CONTEXT_POLICIES: readonly ["include", "summarize", "exclude", "on_request"]; type ContextPolicy$1 = (typeof CONTEXT_POLICIES)[number]; /** REC-1. How a Table shows itself. */ declare const TABLE_DISPLAYS: readonly ["list", "grid", "board", "calendar", "timeline"]; type TableDisplay = (typeof TABLE_DISPLAYS)[number]; /** * What KIND of refusal it was. The mapping from SQLSTATE to code lives in * `/core`, and it is checked against `STORE_REFUSAL_CODES` — the list of every * SQLSTATE the store's own bodies raise, generated from the live catalog. A * code the store can raise and this union cannot name is a hole, and the * real-door suite fails on it. */ type RecordsErrorCode = /** `PT409` — somebody else's write won. The record clock moved under you. */ "stale_write" /** * The call needs an organization and none was carried (the platform's `organization_required`; the entity * doors' MX002). Never resolved by guessing one: the person is shown their memberships and picks. */ | "organization_required" /** `42501` — a door. Either the schema is revoked, or the store is switched off, or this principal may not pass. */ | "door" /** `23514` — a Rule, a shape guard, or a validation the store enforces refused it. */ | "refused_by_rule" /** * `22004` / `22023` / `22P02` — the call itself was malformed: a missing id, a * patch that is not an object, or a value that does not fit the type the * store holds for that Field. The store's own message names the value and the * type it could not be read as. */ | "invalid_argument" /** `02000` — the record is not here (deleted, never existed, or another organization's). */ | "not_found" /** `23503` — something it points at is not there. */ | "missing_reference" /** `23505` — that thing is already here. */ | "already_exists" /** `0A000` — the store does not do this yet, and says which part. */ | "not_supported" /** `53400` / `22012` — the store ran out of something, or the arithmetic could not be done. */ | "store_limit" /** * `40001` — the read-access snapshot this connection carries is older than a * write the reader has already observed, so the store refused to answer at * all rather than hand back something they just lost. Retry on a fresh * connection; it is the one refusal in this list that is worth repeating. */ | "stale_read" /** * `57014` — the store cancelled the statement at its own timeout. Nothing was * refused on its merits and nothing was written; the same call can succeed on * a smaller page or a quieter moment. With `stale_read`, one of the only two * refusals in this list worth repeating. */ | "timed_out" /** * THE DOOR THAT DOES NOT EXIST YET. Not an error condition in the database — * this package asked for a door that is not in the live catalogue, so it * refused to guess. `door_absent` always names the door and the lane that * owns it; it is never a silent no-op and never an empty list. */ | "door_absent" /** * `PGRST202` / `PGRST205` — PostgREST could not find the function or the * schema. The database this client is pointed at is not serving the record * store: the schema is not exposed, or the grants are not there. */ | "store_not_served" /** * `PT428` — the move was not refused and it did not happen: a stage gate whose * `on_fail` is `require_approval` filed a request with the people who can * answer it, and left the record exactly where it was. The store's own * sentence is the gate's own sentence, and the DETAIL carries the approval. * * It is the one code in this list that is not a failure. A screen that drew it * as one would tell somebody their work was rejected when it is being * considered; a screen that drew it as success would move the card while * somebody is still deciding. `custom.pipeline_move` answers an object on this * path rather than raising, so a caller only meets PT428 when the move went * through the ORDINARY write door — which is most of them. */ | "needs_approval" /** * THE CALL NEVER REACHED THE STORE. Not a refusal — a transport failure: the * browser lost the network, the gateway answered with an HTTP status and no * body, the request was cancelled. supabase-js hands these back with an EMPTY * `code` (a `FetchError`) or with the HTTP status in it (`"404"`, `"502"`), * neither of which is a SQLSTATE, and until 2026-09-22 they all landed in * `internal` — which is the one classification that tells a screen nothing, * and which made the package print a sentence about ITSELF to the person * (lane TAILS-6). Nothing was written, and the same call can simply be made * again, which is what separates it from `internal`. */ | "unreachable" /** Anything the mapper could not place. It still carries the store's own words. */ | "internal"; /** A refusal, in the store's own words. */ interface RecordsError { code: RecordsErrorCode; /** THE STORE'S message, verbatim. Never paraphrased. */ message: string; /** THE STORE'S hint, verbatim — this is where the remedy is written. */ hint?: string; /** The SQLSTATE, so a caller can be exact when a code is not exact enough. */ sqlstate?: string; /** Whatever the store put in DETAIL, parsed when it was json. */ detail?: unknown; /** * TRUE when this package did not recognize the class it was handed, so a * caller can say so instead of reading the silence as "handled". * * 🚨 IT IS A FLAG AND A SEPARATE SENTENCE, NEVER AN APPENDIX TO `hint` * (lane TAILS-6, 2026-09-22). `mapPgError` used to append "@ai-matrx/records * did not recognize SQLSTATE … from checklistTemplates" ONTO `hint`, and * `hint` is the field every screen treats as THE REMEDY — the sentence a * person is told to act on. So a dropped connection on the Checklists tab * showed a plumbing company "Something went wrong at our end" over a line * about a TypeScript package and a SQLSTATE. A note this package writes about * ITSELF is for whoever debugs it and belongs nowhere a person reads. */ unmapped?: boolean; /** * THIS PACKAGE'S OWN NOTE, for an engineer. Never the store's words and never * a person's: screens render it out of sight (a `title` attribute, a * screen-reader line) or not at all. */ diagnostic?: string; } type RecordsResult = { ok: true; data: T; } | { ok: false; error: RecordsError; }; /** * The structural shape of a PostgREST failure as supabase-js resolves it: a * PLAIN OBJECT parsed from the response body, NOT an `Error`. Reading * `.message` off `instanceof Error` discards the real database message on every * genuine failure. */ interface PgError { code?: string | null; message: string; details?: string | null; hint?: string | null; } /** A `uuid` as the store writes it. */ type Uuid = string; /** An ISO-8601 timestamp, as jsonb carries it. */ type Timestamp = string; /** * VAL-1. WHERE a value came from, addressed by a pointer the STORE interns — * `s1`, `s2`, `s3`. A writer supplies the source itself and the store interns * it; a writer that invents a pointer is refused by name. Interned once per * save: two pointers holding the same description is the defect the word * "interned" exists to prevent, and the store refuses that too. */ type ProvenancePointer = string; /** The description a pointer resolves to. Free-shaped by design: the store checks that it is an object, and the organization decides what it says. */ type ValueSource = Record; /** * VAL-3 / VAL-4. Another candidate for the same value, with the source it came * from and the RANK — the order an organization trusts its sources in, never a * score or a confidence a machine invented. Ranks are whole, start at 1, and * two alternates never claim the same one. They live INSIDE this record's * document; there is never a row per value. */ interface ValueAlternate { value: unknown; /** A pointer this record's `_sources` block resolves. */ src?: ProvenancePointer | null; rank: number; } /** * The value envelope. One per field key, in the record's `_values` block, with * the value itself sitting at the top level under the same key. * * The key set is CLOSED (`custom.value_envelope_keys()`). In particular it can * never carry any of the per-value access words (`PER_VALUE_ACCESS_WORDS`): VAL-5 puts those on the Field and on the Record, and VAL-6 says a * value that must be secret goes in a contained record with its own access. * The store refuses each of those words by name. */ interface ValueEnvelope { /** The per-value version, 1 or more. THE STORE WRITES IT — no caller sets it. */ ver: number; /** A provenance pointer this record's `_sources` block resolves. */ src?: ProvenancePointer | null; /** VAL-8. Who wrote it. */ actor: Actor; /** Only an agent acts on behalf of a person; a person's id. */ on_behalf_of?: Uuid | null; at?: Timestamp | null; /** VAL-2. Why it is missing. A value either holds something or says this. */ absent?: AbsenceReason | null; alternates?: ValueAlternate[] | null; /** * FLD-2's `dated` modifier: the WORLD clock for this value — when it was true * of the world, which is a different question from when it was written. Added * to the store's closed key set after this package's first generation, and * caught by `pnpm check:store-registry` rather than by anybody noticing. */ dated?: string | null; } /** * The one jsonb document. Field keys sit at the top level; the store's own * blocks are underscore-prefixed and are never a writer's value. */ interface RecordDocument { /** The store's interned provenance table: pointer -> source. */ _sources?: Record; /** The envelopes, keyed by field key. */ _values?: Record; /** FLD-10. Which type of record this is, selecting the Fields and Rules that apply. */ _type?: string; /** REC-7. Zero or one parent. Never two. */ _parent?: Uuid | null; [fieldKey: string]: unknown; } /** `custom.record` as the store holds it — the row around the document. */ /** * What `custom.record_write_graph` wrote — a parent, the rows that belong to it, and the * edges it stands on, all in one transaction. * * `edges` counts BOTH writers: the edges written through `public.assoc_add` (whose ids are * in `edge_ids`) and the relation targets written through `platform.relation_set`, which * answers a count rather than ids because a relation is one field's worth of targets and * not a list of independent edges. `relations` is that second number on its own, so a * caller can tell them apart instead of subtracting. */ interface RecordGraphWritten { parent_id: Uuid; table_id: Uuid | null; child_ids: Uuid[]; edge_ids: Uuid[]; children: number; edges: number; relations: number; /** The door's own sentence about what it did. Shown, never re-worded. */ how: string; } interface StoredRecord { id: Uuid; organization_id: Uuid; table_id: Uuid | null; /** `record` for ordinary rows, `kernel` for the nine kernel Tables (REC-27). */ data_class: string; data: RecordDocument; created_by: Uuid | null; updated_by: Uuid | null; created_at: Timestamp; updated_at: Timestamp; /** REC-23: soft, and reversible within `retention`. */ deleted_at: Timestamp | null; /** The RECORD clock. `recordUpdate` writes against it (optimistic concurrency). */ version: number; metadata: Record; custom_fields: Record; visibility: string; } /** * One value read back with everything the store knows about it — the shape * `custom.record_values_versioned` and `custom.value_read` answer in. This is * what a screen needs to explain a value (DYN-24) without a second query. */ interface ReadValue { field_key: string; field_id: Uuid | null; value: unknown; value_version: number | null; source: ValueSource | null; absent_reason: AbsenceReason | null; actor: Actor | null; on_behalf_of: Uuid | null; written_at: Timestamp | null; alternates: ValueAlternate[] | null; } /** * Why one field of a record came back empty — `custom.hidden_field_notice`, as * the read door attaches it under `_hidden`. It is the store's own sentence, so * a screen says "you need X to see this" instead of drawing a blank cell. */ interface HiddenFieldNotice { /** The field's sensitivity word. */ reason: string | null; /** The level this person would need. */ needs: string | null; /** The other way in — "a share of this one field with you". */ or: string | null; } /** * One row as the read door `custom.read_records` answers it: the id, the * document the store decided this reader may see, and the reader's level on the * Table. A field this reader may not see is PRESENT and null, with its reason * in `hidden` — the masking is the store's, never this client's. */ interface ReadRow { id: Uuid; document: RecordDocument; /** `public.permission_level` — the caller's level on the Table. */ level: string; hidden: Record; /** * A kept table's device copy holds a change to this row that has not reached the store yet (made * offline, or not sent so far). Only the offline data source sets it; a read from the store never does. */ waiting?: true; } /** * DOOR-SPEED. One key of a page's order, as `custom.read_records_page` takes it. `as` is the * column's own type — `number` sorts numerically (text that is not a number sorts last), * `date` sorts ISO dates and instants, `text` (the default) sorts by lower-cased text. A choice * column always sorts by the WORD a person sees; the store knows which columns are choices. * NULLS LAST both ways, record id as the tie-breaker. */ interface RecordPageSort { field: string; direction?: "asc" | "desc"; as?: "text" | "number" | "date"; } /** One page the store sorted, searched and counted: `total` is the matching set this reader may see. */ interface RecordPage { /** The rows this person can open. Header-only stubs of rows they cannot open are never in here. */ rows: ReadRow[]; /** * The matching set this reader can OPEN (stubs left out). Absent `locked` and `total` agree with * the old meaning. */ total: number; /** Rows the store listed only as a header (a Confidential row this person is not named on), across the whole table. */ locked?: number; limit: number; offset: number; /** * Sort keys the store SKIPPED because this reader cannot see that column (removed, or masked * from their seat). The page is still read; the screen says the sort was not applied. */ sort_ignored: string[]; /** * Columns of a SAVED VIEW's own filter the store SKIPPED because this reader cannot see them (WITHHELD-HONEST-2). * Only a filter marked `skip_withheld` is skipped; an explicit filter on such a column is refused by name. */ filter_ignored?: string[]; } /** * A row as the keyset door answers it: the read row plus the `version` and `organization_id` the * store read it at, in the same statement as its values (so a document and its version never * disagree). `version` is null only on a Confidential header row, which says nothing but "it exists". */ type KeysetRow = ReadRow & { version: number | null; organization_id: Uuid; }; /** * AP3-KEYSET-DOOR. One page of `custom.read_records_keyset`: the same rows and order as `RecordPage`, * paged by an opaque `cursor` (null on the last page) instead of an offset, so nothing is skipped or * repeated while others write. Re-reading named rows (`ids`) answers `total: null` and no cursor. */ interface RecordKeysetPage { rows: KeysetRow[]; total: number | null; /** Header-only stubs this page carried (rows this person cannot open); never in `rows`. */ locked?: number; limit: number; cursor: string | null; /** As on `RecordPage`: sort keys skipped because this reader cannot see that column. */ sort_ignored: string[]; /** As on `RecordPage`: filter columns of a saved view skipped because this reader cannot see them. */ filter_ignored?: string[]; } /** DOOR-SPEED. One change in a `recordChangeMany` batch. */ type RecordChange = { op: "insert"; data: RecordDocument; } | { op: "update"; record_id: Uuid; patch: RecordDocument; expected_version?: number | null; } | { op: "archive"; record_id: Uuid; }; /** What one change did, in the order the changes were handed in. */ type RecordChangeResult = { op: "insert"; id: Uuid; } | { op: "update"; id: Uuid; version: number; } | { op: "archive"; id: Uuid; archived_at: string; }; /** * One revision of one record, as `custom.io_revisions` answers it, newest * first. `version` is the record's own version at that point — the number a * screen hands back as `expectedVersion` so the store, not the screen, decides * whether someone else got there first. */ interface RecordRevision { version: number; changed_at: string; changed_by: Uuid | null; summary: string | null; } /** A whole record as `recordRead` answers it, through `custom.read_record`. */ interface RecordRead { record_id: Uuid; /** The masked document. A hidden field is present and null. */ document: RecordDocument; /** Why each masked field is masked, in the store's words. */ hidden: Record; /** * FLD-9 / REC-19. Which Rule version produced each computed value. Present * only where the store has computed provenance for this record. */ computed: Array<{ field_key: string; field_id: Uuid; value: unknown; rule_id: Uuid; rule_version: number; computed_at: Timestamp; }>; /** * WHY `computed` IS EMPTY, when it is empty because nobody could read it. * * `custom.computed_provenance` is a footnote beside the record and its door is not * open to a person's seat (no EXECUTE for `authenticated`, measured 2026-09-21). It * used to fail the WHOLE read, so an organization's own owner was told "You do not * have access to this" about a record he could read — see `core/client.ts`. The * record comes back now, and this carries what the footnote's door said, so a screen * that draws rule provenance can say it could not read it rather than draw nothing * and mean two different things by it. `null` means the footnote was read fine (or * was never asked for). */ computedUnavailable?: RecordsError | null; /** * Asked with `withVersion: true`: the version this read saw, so a safe edit (`recordUpdate` with `expected_version`) needs no * second call. Read beside the document in the same round of requests; `null` when the store * could not answer it (a write then refuses as "version unread", never as last-write-wins). */ version?: number | null; } /** * THE TWO LANES THE ARCHIVED-ITEMS LAW NAMES (Arman, 2026-09-09) — the same * split every other list in the platform makes between what is mine and what * is the organization's. * * · `mine` — what I archived. * · `org` — what anybody in this organization archived. * * `mine` is by construction a SUBSET of `org`: the store applies it as a * narrowing conjunct beside the read-access ladder, so a lane can never show a * record the ladder would have refused. */ declare const ARCHIVE_LANES: readonly ["mine", "org"]; type ArchiveLane = (typeof ARCHIVE_LANES)[number]; /** * One archived record, as `custom.read_records_archived` answers it: an * ordinary `ReadRow` — same id, same masked document, same level, same hidden * notices as it had while it was live — plus the two facts that only an archive * needs. * * `archivedBy` is null when the archiver is not a member of this organization * any more (their name is not this screen's to learn) or when the history * partition that held the SOFT_DELETE has rolled away. Null is an ANSWER and * gets its own sentence; it is never printed as an empty space. */ interface ArchivedRow extends ReadRow { archivedAt: Timestamp; archivedBy: Uuid | null; archivedByName: string | null; } /** * A rule expression as `custom.rule_eval` ACTUALLY evaluates it — read off the * door's own body, not invented here. A leaf is the node's name carrying its * payload (`{"const": 3}`, `{"field": ""}`); an operator is * `{"op": "eq", "args": [...]}`. * * `field` / `parent_field` name a Field BY ID (REC-17 — the door refuses * `field_name`, `field_key` and `field_label` by name), and `merge_field` is * the resolver's seam, storable today and refused `0A000` until W5-MERGE-B. * * `p_values` is keyed by the Field's KEY: the door resolves the id to a key * (`custom.rule_field_key`) and then reads `p_values -> key`. */ type RuleExpression = { const: unknown; } | { field: Uuid; } | { parent_field: Uuid; } | { merge_field: string; } | { op: RuleNodeKind; args: RuleExpression[]; }; /** One form as `custom.forms` reports it to its owner. */ interface FormSummary { form_id: string; table_id: string; title: string | null; slug: string; published_at: string | null; closed_at: string | null; submission_cap: number | null; /** Every answer that arrived, rejected ones excluded from `in_table` and `held`. */ responses: number; /** Answers that became records — the only number that means "it is in the table". */ in_table: number; /** Answers waiting for a person, because the form's accept Rule did not clear them. */ held: number; /** Answers the accept Rule or the decoy turned away. */ rejected: number; /** draft · open · closed · full — the store's own word, never derived on a screen. */ state: "draft" | "open" | "closed" | "full"; quarantine_rule_id: string | null; notify_rule_id: string | null; presentation: Record; } /** * THE ROUTE THROUGH A FORM, AS THE STORE WALKED IT (lane TYPEFORM-DUP) — `custom._form_route`, * reached by the owner's preview (`custom.form_preview_route`) and the stranger's page * (`custom.form_public_route`). One row per question in order (`asked` false only when a condition * hid it or a logic jump carried the person past it); the ending reached (null = the thank-you * screen); the score (null when no question carries points). */ interface FormRoute { asks: Array<{ field_key: string; asked: boolean; decided: boolean; said: string | null; }>; ending: string | null; score: number | null; } /** A form's results for its owner — `custom.form_results`. Every number is the store's. */ interface FormResults { form_id: string; /** Visits to the public link (one per page open, counted through the app's own route). */ views: number; /** Visits that answered at least one question. */ starts: number; /** Submissions that arrived (the decoy's excluded). */ completions: number; /** completions / starts, as a percentage with one decimal; null before anybody started. */ completion_rate: number | null; questions: Array<{ field_key: string; /** Visits that were shown this question. */ reached: number; /** Visits whose last question was this one and that never sent: the drop-off. */ left: number; /** Submissions that answered it. */ answered: number; /** The twelve most common answers (a list answer counts each of its parts). */ answers: Array<{ value: string; count: number; }>; }>; endings: Array<{ ending: string | null; count: number; }>; hidden: Array<{ key: string; value: string; count: number; }>; score: { count: number; average: number; min: number; max: number; } | null; } /** * ONE BOOKING PAGE, AS ITS OWNER SEES IT — `custom.bookings` (PRODUCTS row 14). * * A booking page is an `anon_form` whose `presentation -> 'booking'` carries the * availability and the id of a slots Table, so it is the SAME object a form is and * this is the same list `forms()` is, asked for the booking half. The numbers are * the store's own: a screen that counted appointments in the browser would drift * from the index that actually protects a slot. */ interface BookingSummary { form_id: string; /** The Table the appointments land in — a booking is an ordinary record in it. */ table_id: string; title: string | null; slug: string; published_at: string | null; closed_at: string | null; /** How long one appointment is. The offer is the store's, never the browser's. */ slot_minutes: number; /** The organization's own zone, which is what the visitor's times are drawn in. */ timezone: string; /** The ONE slots Table `custom.booking_declare` finds-or-makes for this page. */ slot_table_id: string; booked: number; cancelled: number; /** Booked and still ahead of now — the number that answers "what is coming". */ upcoming: number; /** Slots a visitor is holding right now, before they have given their details. */ held: number; /** The next appointment, or null when there is none. */ next_at: string | null; /** draft · open · closed — the store's own word, never derived on a screen. */ state: string; } /** * ONE WINDOW OF AN ORGANIZATION'S WEEK, in the shape `custom._booking_availability` * normalizes to and `custom._booking_slots` generates from. `weekday` is 0 for Sunday * and 6 for Saturday — the database's own numbering, so a picker and the store cannot * disagree about which day Monday is. A window may name ONE member, which is the "with * whom" of an appointment; naming somebody who is not a member is refused BY NAME. */ interface BookingWindow { weekday: number; /** "09:00" */ from: string; /** "17:00" — later in the day than `from`, or the store refuses it by name. */ to: string; member_user_id?: string | null; } /** * WHAT IS ON OFFER. Every field is the store's to default: a page declared with an * empty availability comes back with the organization's timezone, a 30-minute slot and * Monday-to-Friday nine-to-five, because a booking page that offered nothing would look * broken rather than unconfigured. A screen shows what came BACK, never what it sent. */ interface BookingAvailability { timezone: string; slot_minutes: number; /** Dead time kept after each appointment, so two never touch. */ buffer_minutes: number; /** How far ahead of now the earliest bookable slot is. */ lead_minutes: number; /** Most appointments in one day, across every window. */ max_per_day: number; /** How many days ahead the page offers. */ days: number; windows: BookingWindow[]; /** CD-PAGE-10: places at one time for each host (1 = one person per time; a class or group visit takes more). */ capacity?: number; /** CD-PAGE-10: none, auto (a new meet.jit.si room per booking) or link (the owner's own room in `video_url`). */ video?: "none" | "auto" | "link"; video_url?: string; /** CD-PAGE-10: the owner's private iCal address; its busy times are never offered. Owner-only — never in a public answer. */ busy_feed?: string; /** CD-PAGE-10: when the connected calendar was last read, and why the last read failed (the last good list is kept). */ busy_read_at?: string | null; busy_problem?: string | null; } /** * WHAT `custom.booking_declare` ANSWERS. `availability` is the NORMALIZED offer — * show that, not the draft — and `questions_filled_in` is how many questions the store * took over (`slot`, `status`, `booked_with`), which a visitor is never asked. * * It does NOT publish. `anonPublish` is still the one act that opens the link, exactly * as it is for a form, so there is one act and one word for opening a public surface. */ interface BookingDeclared { form_id: string; slot_table_id: string; availability: BookingAvailability; questions_asked: number; questions_filled_in: number; ms: number; } /** The measures the store offers. Asked of `custom.agg_operations()` at run time too. */ type AggregateOperation = "count" | "sum" | "avg" | "min" | "max" /** GRID-PRIMITIVES G1: the summary bar's other four. Blank = no value, JSON null or "". */ | "median" | "filled" | "empty" | "unique" /** * Notion's calculation row (Spaces N6): the share of the rows asked with no value / a value, as a * percent 0..100 (NULL over no rows); and the RANGE, largest minus smallest (a date column: the days * between its earliest and latest moment). */ | "percent_empty" | "percent_filled" | "range"; /** The periods a date Field can be bucketed into (`custom.agg_buckets()`). */ type AggregateBucket = "day" | "week" | "month" | "quarter" | "year"; /** * A HALF-OPEN MOMENT WINDOW: `from` is inclusive, `to` is exclusive. Half-open * is the only shape that makes "September" and "October" meet exactly once — * an inclusive `to` either double-counts the boundary moment or drops it, * depending on the reader, and both of those are a chart telling a lie. */ interface AggregateWindow { from?: string | null; to?: string | null; } /** * ONE FILTER VALUE, as `custom.record_aggregate` and `custom.dashboard_run` * both read it: a SCALAR is an equality, and an OBJECT is a window. It is not * `unknown` on purpose — a filter value the store cannot read is a refusal a * caller should get from the compiler, not from the database. */ type AggregateFilter = string | number | boolean | null | AggregateWindow; /** * 🚨 THE STORE'S ONE FILTER SHAPE, AND THE ONLY NAME FOR IT. * * An object of Field key -> value. A scalar is an equality, `null` is UNSET * (the third state of a tick box), an object is a half-open time window. * * It is not the aggregate's private shape and never was. `platform.saved_view` * has carried exactly this as `definition -> 'filters'` across five live * surface keys since DOOR-18; `custom.view_declare` writes it; * `custom.agg_view_admits` reads it to decide whether a change belongs to a * saved view; `custom.record_aggregate` takes it as `p_filter` to COMPUTE a * dashboard number; and `custom.read_records_matching` takes it to OPEN the * rows behind that number. One `custom.record_filter_sql` in the database turns * it into SQL for the last two, so the number and the rows cannot disagree. * * Invent a second one and a chart can compute a total whose drill-through * lands somewhere else, which is the only way a drill-through can lie. */ type RecordFilter = Record; /** * S2-PRIME FILTER-GROUPS — A READ DOOR'S QUESTION, IN EITHER OF ITS TWO SHAPES. * * The flat `RecordFilter` above (field key → scalar / `null` / window, AND only) is what * dashboards, drill-throughs and the notifier carry. A `RuleExpression` — `{op, args}`, ALL / ANY / * NOT groups nested to any depth, fields by id — is what the condition builder, a view's own * filter and every membership Rule speak. `custom.record_filter_sql(org, table, filter)` turns * EITHER into the one WHERE clause the grid (`list`), the aggregate and the board all use, so the * three can never disagree about which records a view holds. */ type StoreFilter = RecordFilter | RuleExpression; /** One measure. `count` counts rows and needs no Field; the rest read one. */ interface AggregateMeasure { op: AggregateOperation; /** The Field's key. Required for every operation except `count`. */ key?: string; } /** * One row of an aggregate: which group it is, what each measure came to, and * how many records it was worked out from. The measure keys are the store's * own (`count`, `sum_amount`, `avg_amount`, …), never renamed on the way out. */ /** * One group's key, as the column holds it (CHAIR-SHEET-STORE b): a number column's key is a number, a * yes/no column's true/false, a date its ISO text, a date period its first moment, everything else text. */ type AggregateGroupValue = string | number | boolean | null; interface AggregateRow { groups: Record; measures: Record; /** * AGG-WITHHELD — a measure over a column this reader may not read is `null` in `measures` (the * "no value" every number reader already handles) and its reason rides HERE by measure name: the * same notice a withheld cell carries (`reason`, `needs`, `or`) plus the store's own sentence in * `says`. The store never errors a read for a withheld column; it says so. A question that * GROUPS or FILTERS by such a column answers one row with every measure null and `row_count` 0. * Absent when nothing was withheld. */ withheld?: Record; row_count: number; /** * S3 — present only when the question carried a comparison. The same group * in the PRIOR window: a bucket is matched by its position in its own window * (week 1 of September beside week 1 of August), so `prior_groups` names the * prior bucket's own date. `null` when the prior window had no such group. */ prior_groups?: Record | null; /** The prior window's measures, keyed exactly like `measures`. */ prior_measures?: Record | null; prior_row_count?: number | null; /** Every measure now and before, the change, and the change as a percent of before. */ delta?: Record | null; /** The two windows this row was worked out over, and the calendar that cut them. */ compare?: AggregateCompareWindows | null; /** * S3 — a compared, bucketed row's POSITION in its window (0 = the first * bucket). A row the current window has not reached yet has a position and * no current bucket, so a chart names it "Week 6" rather than inventing a date. */ position?: number | null; } /** * S3 — WHAT A PERIOD IS COMPARED AGAINST (`custom.agg_compare_kinds()`). * `previous_period` is the period before, of the same length (August before * September; an explicit window shifted back by its own length); * `same_period_last_year`; `range` is a fixed `baseline`. */ type AggregateCompareKind = "previous_period" | "same_period_last_year" | "range"; /** * S3 — ONE COMPARISON, as `custom.record_aggregate(p_compare)` and a * dashboard block's `compare` both take it. The current window is either the * calendar `period` holding `at` (default: now, in the organization's time * zone) or an explicit `from`/`to`. A date alone ("2026-09-01") is that local * day in the organization's time zone. */ interface AggregateCompare { against?: AggregateCompareKind; /** The date Field the period runs along. Defaults to the bucket's key. */ key?: string; period?: AggregateBucket; at?: string; from?: string; to?: string; /** Compare the part of the period that has happened ("month to date"). */ to_date?: boolean; /** The fixed period `range` compares against. Half-open: `to` is excluded. */ baseline?: { from: string; to: string; }; } /** One measure now and before. `change_pct` is null when before was nothing or zero. */ interface AggregateDelta { current: number | null; prior: number | null; change: number | null; change_pct: number | null; } /** * S3 — THE TWO WINDOWS the store actually ran, as local ISO moments with the * organization's offset ("2026-09-01T00:00:00-07:00"), and the calendar that * cut them. A screen labels the comparison from THIS, never from its own clock. */ interface AggregateCompareWindows { key: string; against: AggregateCompareKind; period: AggregateBucket | null; to_date: boolean; time_zone: string; week_start: string; /** The bucket grain the windows were counted in, when the question had a bucket. */ grain: AggregateBucket | null; /** How many buckets the current window holds — what a target's pace is shared over. */ bucket_count: number | null; window: { from: string; to: string; }; prior_window: { from: string; to: string; }; /** On a row from `recordAggregate`: that row's bucket position (see `AggregateRow.position`). */ position?: number | null; } /** One question, as the sheet words it. */ interface CaptureQuestion { field: string; ask: string; help?: string | null; required: boolean; } /** One Field, as the phone needs it to draw a control and validate offline. */ interface CaptureField { id: Uuid; key: string; label?: string | null; type?: string | null; unit?: string | null; format?: string | null; multi?: boolean | null; required: boolean; /** `attachment` is what makes the phone open a camera instead of a text box. */ parity_type?: string | null; [k: string]: unknown; } /** * Everything the phone loads ONCE and then runs with no network. * * `state` is one of `open`, `not open yet`, `closed`, `read only`. `may_capture` is the * honest label for the screen and never the access decision — `custom.capture_submit` * asks `custom.has_visibility` for `editor` itself, before it admits the sheet exists. */ interface CaptureSheetFace { sheet_id: Uuid; organization_id: Uuid; table_id: Uuid; title: string; presentation: { intro?: string | null; flow?: string | null; thank_you?: { title?: string; body?: string; } | null; questions?: CaptureQuestion[]; [k: string]: unknown; }; fields: CaptureField[]; state: string; may_capture: boolean; message: string | null; } /** A sheet in the foreman's list, with what has come in through it. */ interface CaptureSheetSummary { sheet_id: Uuid; table_id: Uuid; title: string; slug: string; state: string; opened_at: string | null; captures: number; replays: number; last_capture_at: string | null; presentation: Record; } /** A file reference the app has already put in the platform's one byte store. */ interface CaptureFileRef { field: string; name: string; mime_type?: string | null; size_bytes?: number | null; duration_ms?: number | null; /** The `files.files` id. The File record made from this points at it. */ file_id?: string | null; url?: string | null; } /** What one capture became. `replay: true` means it had already arrived. */ interface CaptureOutcome { record_id: Uuid | null; state: string; message: string | null; replay: boolean; file_ids: Uuid[] | null; } /** The seven shapes a block can take, exactly as `custom.dashboard_kinds()` answers. */ type DashboardBlockKind = "number" | "bar" | "column" | "line" | "donut" | "table" | "stuck"; /** * ONE SAVED BLOCK — the spec, not the answer. `table_id` left out means the * dashboard's own subject Table, and `state_key` / `days` belong to `stuck` * alone (the door refuses a `stuck` block that names no field to watch). */ interface DashboardBlock { /** * The block's own id, kept across every save, resize and move (v6 lane 11): the layout and the * per-block refusal tile key on it. The client mints a uuid for a block that has none. */ id?: string; title?: string; kind: DashboardBlockKind; /** Absent = the dashboard's subject Table. */ table_id?: Uuid | null; /** Field keys whose values become the groups. */ group_by?: string[]; /** `count` needs no key; every other operation reads one Field. */ measures?: AggregateMeasure[]; /** A date Field truncated to a period. */ bucket?: { key: string; by: AggregateBucket; } | null; /** A scalar is an equality; an object is a half-open moment window. */ filter?: Record | null; /** Rows this block reads. The door clamps it to 1…500 and defaults to 50. */ limit?: number; /** Columns of the twelve this block occupies. The door clamps it to 2…12 and defaults to 6. */ span?: number; /** `stuck` only: the field whose value is watched for a change. */ state_key?: string; /** `stuck` only: how long a record may sit on the same value. Defaults to 14. */ days?: number; /** * S3 — the comparison this block makes (this month against last month). * The door fills `key` from the bucket when it is left out, and refuses a * comparison along a Field the Table does not have, by name. */ compare?: AggregateCompare | null; /** S3 — the number this block is aiming for. */ target?: DashboardTarget | null; } /** * S3 — A BLOCK'S TARGET. `measure` is one of the block's own measure keys * (default: the first). `per: "period"` is a target for the whole window (a * monthly target — a bucketed chart draws its even share per bucket as the * pace); `per: "bucket"` is a target for every bar or point. * * A target is a FIXED NUMBER (`value`) or A GOAL COLUMN (`field`, a Field key of * the block's Table): the store adds that column up (`op`, default `sum`) over * exactly the records the block's own number read — same filter, same window, * same reader — so "invoiced against quoted" is about the same jobs. Exactly * one of the two; a goal column is always `per: "period"`. */ type DashboardTarget = DashboardTargetBase & ({ value: number; field?: never; op?: never; } | { field: string; op?: DashboardTargetOp; value?: never; }); /** How a goal column is added up across the block's records. */ type DashboardTargetOp = "sum" | "avg" | "min" | "max"; /** What every target carries, whichever kind it is. */ interface DashboardTargetBase { label?: string; measure?: string; per?: "period" | "bucket"; } /** One dashboard as `custom.dashboards` reports it to its organization. */ interface DashboardSummary { dashboard_id: Uuid; /** The subject Table: the one a block that names no table of its own reads. */ table_id: Uuid | null; name: string; blocks: DashboardBlock[]; presentation: Record; block_count: number; version: number; created_at: Timestamp; updated_at: Timestamp; } /** * AT-INTERFACE — ONE INTERFACE as `custom.interface_read` hands it to this viewer. An Interface is a dashboard * record whose `presentation.kind` is `"interface"`; builders (Editor, Full) also get `presentation.draft` and * `presentation.versions`, everybody else only `presentation.published`. `member` = the viewer is in the * interface's organization (elements read through the viewer's own doors); otherwise every element reads * through `custom.interface_rows`. Null from the door = not there, archived, or not shared with her. */ interface InterfaceRead { id: Uuid; organization_id: Uuid; table_id: Uuid | null; name: string; version: number; updated_at: Timestamp; level: "admin" | "editor" | "edit_content" | "commenter" | "viewer"; member: boolean; presentation: Record; blocks: DashboardBlock[]; } /** One column an interface element exposes (`custom.interface_rows`). */ interface InterfaceRowsField { key: string; label: string; type: string; multi: boolean; system: string | null; relation_target: Uuid | null; format: Record | null; sort: number | null; } /** The rows ONE interface element exposes; a number element answers `count` alone. */ interface InterfaceRows { table?: string | null; title_field?: string | null; fields?: InterfaceRowsField[]; rows?: Array<{ id: Uuid; created_at: Timestamp; updated_at: Timestamp; document: Record; }>; count: number; truncated?: boolean; /** Words of the records a cell points at, by id. */ refs?: Record; /** Choice words by field key, then by normalized value. */ labels?: Record>; } /** * One row of a `stuck` block: a record that has sat on the same value of * `state` for `days_unchanged` days. `measured_from` names WHERE that age was * measured from, because a record whose history does not reach back that far is * aged from what the store actually knows and says so. */ interface StuckRow { record_id: Uuid; title: string | null; state: string | null; last_changed_at: Timestamp | null; days_unchanged: number; measured_from: string; } /** * ONE BLOCK'S ANSWER. The door echoes the block back NORMALIZED — the defaults * it applied, the merged filter it actually ran — and adds `rows` and `ms`; * a block it could not run carries `refused` and `sqlstate` INSTEAD of rows, * and a block it could not even normalize carries only its title and kind. */ interface DashboardBlockResult { /** The saved block's own id, when the store echoes it. */ id?: string; title: string; kind: DashboardBlockKind | (string & {}); table_id?: Uuid; group_by?: string[]; measures?: AggregateMeasure[]; bucket?: { key: string; by: AggregateBucket; } | null; /** The block's own filter with the canvas bar merged under it — the block's own key wins. */ filter?: Record; limit?: number; span?: number; state_key?: string; days?: number; /** An aggregate block's groups and measures, or a `stuck` block's records. Absent when refused. */ rows?: AggregateRow[] | StuckRow[]; /** How long this one block took, in milliseconds. Absent when refused. */ ms?: number; /** The store's own sentence, when this block could not be run. */ refused?: string; /** The SQLSTATE that refusal carried. */ sqlstate?: string; /** * S3 — the windows this block was actually compared over; `null` when it was * not compared. (The comparison it was ASKED to make is the saved block's.) */ compare?: AggregateCompareWindows | null; /** S3 — why a canvas comparison was not applied to this block (it has no date). */ compare_refused?: string; /** * S3 — the block's first (or target) measure added up across its rows, now * and before, when the measure adds up (count, sum, filled, empty) or the * block has one row. Worked out by the store from the rows it answered, so * a tile, a chart and a ring cannot disagree. */ totals?: DashboardTotals; /** S3 — the target, with how far the current total has come and its pace per bucket. */ target?: DashboardTargetResult; } /** S3 — one block's total, now and before. */ interface DashboardTotals { measure: string; current: number | null; prior: number | null; change: number | null; change_pct: number | null; } /** S3 — a target as the run answers it. */ interface DashboardTargetResult { /** * The number aimed at: the fixed value, or the goal column added up over this * block's records. `null` when a goal column had nothing to add up, or was refused. */ value: number | null; /** A goal column's key, when the target is read from one. */ field?: string; /** How that column was added up. */ op?: DashboardTargetOp; /** * The store's own sentence when THIS reader may not read the goal column — the * block still answers its number; only the target is withheld. */ refused?: string; label: string; measure: string; per: "period" | "bucket"; /** The current total the target is measured against. */ current: number | null; /** current ÷ value, 4 decimals. */ progress: number | null; /** The line a bucketed chart draws: the target's even share of one bucket, or the target itself per bucket. */ pace: number | null; } /** What `custom.dashboard_run` answers: the canvas, and every block's own result. */ interface DashboardRunResult { dashboard_id: Uuid; name: string | null; subject_table_id: Uuid | null; presentation: Record; /** The one filter bar this run was asked with. */ filter: Record; /** S3 — the date grain the canvas re-cut every bucketed block to, or null. */ grain?: AggregateBucket | null; /** S3 — the canvas comparison this run was asked with, or null. */ compare?: AggregateCompare | null; /** S3 — the organization's calendar every date on this canvas was cut with. */ calendar?: { time_zone: string; week_start: string; }; blocks: DashboardBlockResult[]; } /** One token found in a body, in the order it appears. */ interface DocToken { ordinal: number; /** Exactly as it is written in the body. */ raw: string; field_id: Uuid | null; } /** A token this Table cannot answer, with the store's own reason. */ interface DocUnresolvedToken { raw: string; field_id: Uuid | null; /** The store's sentence. Shown verbatim — a template is not published over it. */ why: string; } /** `custom.doc_template` — a body with tokens, and the Table it renders. */ interface DocTemplateRow { id: Uuid; renders_table_id: Uuid; name: string; body: string; /** REC-68 / VAL-10: a seal is over a document VERSION, so the version is first-class. */ template_version: number; token_count: number; updated_at: Timestamp; } /** * `custom.doc_render` — a rendered document, FROZEN. The bytes and their * SHA-256 are what a signature seals, which is why a render is never edited: * a further agreement is a further version with its own seal. */ interface DocRenderRow { id: Uuid; template_id: Uuid; record_id: Uuid; table_id: Uuid; template_version: number; body: string; content_hash: string; rendered_at: Timestamp; } /** `custom.doc_signature` — one seal, immutable once made. */ interface DocSignatureRow { id: Uuid; render_id: Uuid; record_id: Uuid; field_key: string; signer_name: string; signer_user_id: Uuid | null; signed_at: Timestamp; document_hash: string; document_version: number; } /** Where one agent-written value came from. Interned once per save (VAL-1). */ interface EnrichSource { kind?: string; field_id?: Uuid; model?: string; model_version?: string | null; instruction?: string; /** What was actually read, with addresses. Never invented by the model. */ read?: Array<{ url?: string; title?: string; at?: string; }>; note?: string; confidence?: number; at?: string; } /** One candidate the model did not commit to, with its rank and its own source. */ interface EnrichAlternate { value: unknown; rank: number; source?: EnrichSource | null; } /** One cell, as `custom.enrich_cells` answers it. The badge is drawn from exactly this. */ interface EnrichCell { record_id: Uuid; field_key: string; /** Whether a model owns this column at all. False means: draw no badge. */ agent_owned: boolean; value: unknown; value_version: number | null; /** `user`, `agent` or `system` — VAL-8's closed vocabulary. */ actor: string | null; /** The person an agent acted for. Always set when `actor` is `agent`. */ on_behalf_of: string | null; written_at: string | null; source: EnrichSource | null; /** One of: "never asked", "none", "refused", "conflicting" (VAL-2). */ absent_reason: string | null; alternates: EnrichAlternate[] | null; /** A person holds this cell against the model until they hand it back. */ pinned: boolean; /** Past its freshness date. The value is still there — stale marks it, never erases it. */ stale: boolean; due_at: string | null; } /** The enrichment itself, as it is stored on the Field. */ interface Enrichment { instruction: string; inputs: string[]; web_search?: boolean; connected_source?: string | null; model: string; confidence_floor: number; write_policy: "auto" | "ask"; cost_cap_cents?: number; review_interval_days?: number | null; /** A person's opt-in: values past their date are filled in again without anybody pressing Fill. */ auto_refill?: boolean; /** Who turned refilling on; the refill runs as them. */ auto_refill_by?: string | null; enabled: boolean; enabled_by?: string | null; enabled_at?: string | null; declared_by?: string | null; declared_at?: string | null; } /** One agent-owned column, with its freshness census and its money. */ interface EnrichmentSummary { field_id: Uuid; table_id: Uuid; field_key: string; label: string; enrichment: Enrichment; enabled: boolean; review_interval_days: number | null; rows_total: number; rows_filled: number; rows_stale: number; rows_pinned: number; rows_absent: number; runs: number; cost_cents: number; cost_per_row_cents: number | null; last_run_at: string | null; last_run: Record | null; } /** One record that needs this column filled in, with what the instruction may read. */ interface EnrichDueRow { record_id: Uuid; title: string; inputs: Record; current_value: unknown; written_at: string | null; /** "never filled in", or "past its freshness date — written N days ago…". */ reason: string; /** Set when the ask was larger than the organization's batch ceiling. */ trimmed_to: number | null; } /** What one run did, and what it cost. */ interface EnrichRun { run_id: Uuid; field_id: Uuid | null; field_key: string | null; model: string | null; trigger_word: string | null; rows_seen: number; rows_written: number; rows_absent: number; rows_below_floor: number; rows_pinned: number; rows_refused: number; cost_cents: number; cost_per_row_cents: number | null; ran_at: string; } /** What `custom.enrich_declare` answers. `says` is a sentence for a person. */ interface EnrichDeclared { field_id: Uuid; table_id: Uuid; field_key: string; label: string; enrichment: Enrichment; says: string; } /** VAL-8's whole vocabulary. There is no fourth word. */ type HistoryActorKind = "user" | "agent" | "system"; /** * WHO WROTE A VERSION. * * `kind` is the store's own word for the author. `name` is a person as a person * reads them — never a uuid; SCR-N-5 forbids printing one at a customer. * `on_behalf_of` is present only for an agent, and for an agent it is ALWAYS * present: an agent carries the exact authority of the person operating it * (AGT-N-4), so a screen that showed the machine without the person would be * hiding who is answerable. */ interface HistoryActor { kind: HistoryActorKind; user_id: Uuid | null; name: string | null; on_behalf_of: { user_id: Uuid; name: string | null; } | null; /** * The person who said yes, when an agent's change waited for approval and was * applied (`custom.work_approval_decide`, VISION-REACH W4 b). Absent on every * other version: a direct write has no approver. */ approved_by?: { user_id: Uuid; name: string | null; } | null; } /** * ONE FIELD THAT MOVED, with what it said before and what it says after. * * `label` is the Field's own label, so a timeline reads "Price" and not * `day_rate`. `rule` is FLD-9's computed provenance AT THAT VERSION — which * Rule, and which VERSION of the Rule, produced a derived value. `source` is * VAL-1's provenance, resolved from the pointer the document interns it under, * so a screen never prints `s3` at anybody. */ interface HistoryChange { key: string; field_id: Uuid | null; label: string; before: unknown; after: unknown; rule: { rule_id: Uuid; rule_version: number; computed_at: Timestamp; } | null; source: Record | null; /** VAL-2: why a value is absent — never asked, none, refused, conflicting. */ absent_reason: string | null; /** VAL-3: the other candidates the store kept, in rank order. */ alternates: unknown[] | null; /** * CLOSE-LINK (CD-LINK-07/12): the value in words when no column can say it — a move's record it sat under * (`before_words`) and now sits under (`after_words`), named by the store with the access question asked first. * Absent or null: say the value through its column. */ before_words?: string | null; after_words?: string | null; } /** One version of one record, as `custom.record_history` answers it, newest first. */ interface RecordHistoryEntry { version: number; occurred_at: Timestamp; /** The machine word: `edit`, `merge`, `undo of merge`, `insert`… */ operation: string; /** The same thing in the word a person used: "edited", "merge", "created". */ operation_label: string; actor: HistoryActor; changes: HistoryChange[]; /** Set when a compound verb wrote this version (HIS-8). */ migration_id: Uuid | null; /** * HIS-8: this version was written by a compound operation whose Migration log * row is still there, so the WHOLE operation can be undone through * `custom.migrate_undo`. An ordinary edit is never "undoable" in that sense — * it is RESTORABLE, which is a different verb and a different button. */ undoable: boolean; } /** * ONE CHANGE TO ONE COLUMN, ANYWHERE IN A TABLE — the half of the question no * revision panel answers. Airtable and Notion both make you open each record * and look. Only records this person may see contribute, decided as a predicate * in the same statement rather than a filter afterwards. */ interface FieldHistoryEntry { record_id: Uuid; /** The Table's own title field, so a row names a record and not a uuid. */ record_title: string; version: number; occurred_at: Timestamp; operation_label: string; actor: HistoryActor; before: unknown; after: unknown; } /** * ONE MOMENT OF A WORKED-OUT COLUMN'S HISTORY (`custom.formula_history`, CD-FORMULA-08; VISION Rule 8: "History knows * which one produced a Value"). A formula, rollup or lookup keeps no value of its own, so its history is worked out: at * each moment its answer could have changed — the record was written, or the formula itself was changed — the answer * from the record's values and the formula AS THEY WERE, and which version of the formula produced it. A formula's * version counts changes to what it works out; renaming the column is not a new version. Newest first. */ interface FormulaHistoryEntry { occurred_at: Timestamp; /** What moved: "record created", "record edited" or "formula changed". */ cause: string; /** The record's own version at that moment. */ record_version: number; /** 1 for the formula as first written, +1 each time what it works out changed. */ formula_version: number; /** The formula's words as that version had them (empty for a rollup or lookup). */ formula_text: string; /** The answer then. */ value: unknown; /** The store's own sentence when that version could not be worked out then; `null` otherwise. */ refused: string | null; /** Who wrote the record (or changed the formula) at that moment. */ actor: HistoryActor; } /** * WHAT A RESTORE WOULD DO, BEFORE IT DOES IT. * * `before` on each change is what the record says NOW and `after` is what it * would say. A key the target version did not have comes back with `after` of * `null` — the screen says "cleared", because that is what the write does to * it. Computed by the same body that computes the write, so the sentence and * the act cannot drift. */ interface RestorePreview { record_id: Uuid; from_version: number; to_version: number; saved_at: Timestamp; /** Present when the preview is for one column rather than the whole record. */ field_key: string | null; changes: HistoryChange[]; count: number; } /** What a restore actually did. A NEW version, always. */ interface RestoreResult { record_id: Uuid; restored_from_version: number; /** The version the restore WROTE. */ version: number; changed: HistoryChange[]; count: number; /** Always `false`, and said out loud because it is what people fear. */ history_rewritten: boolean; /** Present on a single-value restore. */ field_key?: string; } /** One comment, with the people on it named. */ interface RecordComment { id: Uuid; body: string; anchor: Record; /** * The column this comment is about, when it is about one. Notion anchors a * comment to a block; here the block is a Field, so "this price looks wrong" * sits on the price instead of at the bottom of the record. */ field_key: string | null; parent_comment_id: Uuid | null; created_by: Uuid; created_by_name: string; created_at: Timestamp; resolved_at: Timestamp | null; resolved_by: Uuid | null; resolved_by_name: string | null; mentions: Array<{ user_id: Uuid; name: string; }>; /** Mine, so a screen offers Resolve on my own and Reply on everybody's. */ mine: boolean; } /** * A RECORD'S WHOLE THREAD, AND WHAT THIS PERSON MAY DO IN IT. * * The two booleans come from the STORE, in the same call that answers the * thread, because a composer drawn for somebody the write door will refuse is * the defect lane UI-HONEST spent a night removing. `commenter` is the second * rung: a viewer reads the conversation and cannot join it, which is exactly * what the two rungs mean. */ interface CommentThreadRead { record_id: Uuid; comments: RecordComment[]; may_comment: boolean; may_resolve: boolean; } /** * Who follows ONE record (`custom.record_followers`) — the follower list the record shows, and whether THIS person is * on it. A person who cannot open the record gets an empty list, never a refusal that confirms it exists. */ interface RecordFollowers { following: boolean; followers: Array<{ user_id: Uuid; name: string; mine: boolean; }>; } /** What `custom.record_follow_set` answers: the caller's own state and how many follow now. */ interface RecordFollowSet { following: boolean; followers: number; } /** What `custom.comment_write` answers. */ interface CommentWritten { comment_id: Uuid; mentioned: Uuid[]; /** * How many people were actually TOLD. "I mentioned three people" and "three * people were told" are different facts and a screen that conflates them is * lying quietly. */ notified: number; } /** * HOW OFTEN. `instant` fires the moment a record ENTERS the view; the other three * are summaries, assembled by the store at that cadence and sent as ONE message * naming what entered, left and changed since the last one. * * `immediate` and `digest` — the two words this store wrote before 2026-09-20 — * are still accepted everywhere and normalized by `custom.agg_cadence_normalize` * to `instant` and `daily`, so a Rule written then and a Rule written by the * picker now mean the same thing to the same runner. */ type SubscriptionCadence = "instant" | "hourly" | "daily" | "weekly"; /** * THE HOURS NOT TO BE TOLD IN. A send inside them is not dropped — the store moves * it to the moment they end through the notification system's own `next_attempt_at`, * so there is still one sender and one retry policy and nothing disappears because * of the clock. */ interface QuietHours { /** "22:00". Left out (with `end`), the block names no night — only the clock. */ start?: string; /** "07:00" — earlier than `start` means the block crosses midnight, which is the ordinary case. */ end?: string; /** * An IANA zone name, e.g. "America/Chicago". Left out, UTC. THIS IS ALSO THE CLOCK THE * SCHEDULE IS READ IN: `custom.agg_digest_due_at` reads "monday 08:00" in this zone, so a * block carrying only `tz` means "no quiet hours, and 08:00 is my 08:00". */ tz?: string; } /** One subscription, exactly as `custom.agg_subscriptions` reports it. */ interface Subscription { rule_id: Uuid; saved_view_id: Uuid | null; cadence: string; /** The digest's schedule. Null for `immediate`. */ schedule: string | null; channel: string; /** Null means this subscription fires at nobody, and the store says so. */ recipient_user_id: Uuid | null; event_key: string; name: string; } /** * ONE SUBSCRIPTION AS ITS OWNER SEES IT — `custom.subscriptions`. * * `custom.agg_subscriptions` is the NOTIFIER's reader: it answers the whole * organization's firing subscriptions so the delivery path can run, and it is * not callable by a client. This is the other side of the same rows — narrowed * to Tables the caller can already open, so the list never reveals a Table — * and it carries the two things a screen needs and the notifier does not: is it * switched off, and may I switch it off. */ interface MySubscription extends Subscription { table_id: Uuid | null; /** Off. `custom.agg_subscriptions` already excludes a muted one, so this is the truth. */ muted: boolean; /** It is addressed to me. */ mine: boolean; /** I may switch it off: mine, or I hold admin on the Table it watches. */ i_may_mute: boolean; /** The hours it will not tell me in. Null means any time. */ quiet_hours: QuietHours | null; /** When this last told me anything at all. Null means it never has. */ last_sent_at: string | null; /** * When the next summary is due — computed by the SAME store functions the runner * uses, so this and the moment it actually fires cannot drift. Null for `instant`, * which has no due time, and null for a muted one, because saying "next Monday" * beside a switch that is off is a screen that lies. */ next_digest_at: string | null; } /** * THE SUMMARY THIS SUBSCRIPTION WOULD SEND RIGHT NOW — `custom.subscription_preview`. * * It assembles and returns: nothing is delivered, nothing is recorded and the * watermark does not move, so "show me one now" never costs somebody a text and * never robs the real Monday summary of a week of news. */ interface DigestPreview { rule_id: Uuid; name: string; cadence: string; schedule: string | null; quiet_hours: QuietHours | null; channel: string; recipient_user_id: Uuid | null; muted: boolean; table_id: Uuid | null; saved_view_id: Uuid | null; window_start: string; window_end: string; entered: Array<{ record_id: Uuid; name: string; }>; left: Array<{ record_id: Uuid; name: string; }>; changed: Array<{ record_id: Uuid; name: string; }>; counts: { entered: number; left: number; changed: number; in_view: number | null; }; /** The grid, filtered by the very view this subscription is over. */ link: string | null; subject: string; body: string; /** * NOTHING FAILS SILENTLY. A subscription that cannot produce a summary — nobody to * tell, no view, a view somebody deleted — says which piece is missing, in the * sentence a person reads, instead of returning an empty summary that looks calm. */ incomplete: string | null; } /** * ONE SAVED VIEW — `custom.views`. A subscription is written OVER one of these, so * a person with no saved view has nothing to be told about; `viewDeclare` is how a * screen gives them one without leaving the screen they are on. * * `filters` is written in the ONE shape `custom.agg_view_admits` reads, so a view * saved here needs no translation before the notifier can admit records to it. */ /** * A ROW TEMPLATE (ROW-TEMPLATES, 2026-10-04) — Notion's database templates: a named preset of a * Table's values ("New Instagram post": Status=Idea, Platform=[Instagram], Page content = a * Hook/Script/Hashtags skeleton) that "New from template" starts a row from. Kept beside the * Table's views in `platform.saved_view` (surface `custom/records/template`); `values` by Field key. */ interface RowTemplateRow { template_id: Uuid; name: string; table_id: Uuid; values: Record; created_at: string; updated_at: string; } interface SavedViewRow { view_id: Uuid; name: string; table_id: Uuid | null; filters: RecordFilter; /** * THE WHOLE VIEW (S0 ONE-SAVED-VIEW, 2026-09-23): `platform.saved_view` is the ONE saved-view * store, so this is what the view bar, the board, the calendar and the gallery draw from — * `layout` (the kind), `group_field`, `measure`, `date_field`, `start_field`, `end_field`, * `image_field`, `sorts`, `rule_id`, * `is_default`, `presentation`, G7's `grid`, G13's `order`, the mover's `moved_from`. */ definition: SavedViewDefinition; created_at: string; } /** Every kind of view the store keeps (`custom.view_keys()` row `layout`). */ type SavedViewLayout = "grid" | "kanban" | "calendar" | "gallery" | "sheet" | "timeline"; /** `presentation.style.colorBy`: a choice or yes/no Field whose colors paint the cards, the calendar and the timeline. */ interface SavedViewColorBy { field: string; target?: string; } /** * THE KEYS OF ONE SAVED VIEW, as `custom.view_keys()` declares them. Fields are named by key * (the store accepts an id and stores the key). Every key the store judges is listed here; the * index signature keeps the rest (`grid`, `order`, `moved_from`, the presentation details) open. */ interface SavedViewDefinition { layout?: SavedViewLayout; /** The board's columns (kanban) or the timeline's rows: any Field. */ group_field?: string | null; swimlane_field?: string | null; measure?: string | null; /** Calendar: the date Field that places a record. */ date_field?: string | null; /** Timeline: the date Field where a record's bar starts. */ start_field?: string | null; /** Calendar and timeline: the date Field where a record ends. Absent, a record is one day. */ end_field?: string | null; /** Gallery: the Field drawn as the card's cover. */ image_field?: string | null; presentation?: { style?: { colorBy?: SavedViewColorBy | null; [key: string]: unknown; } | null; /** Gallery and grids: the Fields hidden from this view, by key. */ hiddenFields?: string[] | null; [key: string]: unknown; } | null; [key: string]: unknown; } /** * What `custom.view_declare` is asked for. With `view_id` the door MERGES: `name` changes only * when sent, `filters` only when the key is sent, each `definition` key replaces that one key and * a key sent as `null` is removed — so a screen sends only what it changed. */ interface SavedViewSpecIn { name?: string; filters?: RecordFilter | null; definition?: SavedViewDefinition | Record | null; view_id?: Uuid | null; } /** What `custom.subscription_declare` is asked for. The store refuses the rest BY NAME. */ interface SubscriptionSpec { name: string; saved_view_id: Uuid; cadence: SubscriptionCadence; /** "monday 08:00", "09:30". Meaningless for `instant`. */ schedule?: string | null; channel?: "in_app" | "email" | "sms"; quiet_hours?: QuietHours | null; /** Left out, the person setting it up — "tell ME". */ recipient_user_id?: Uuid | null; event_key?: string; /** Re-state an existing subscription rather than making a second one. */ rule_id?: Uuid | null; } /** * One of the five states, as `custom.work_states()` lists them — with what it * may become next and whether it is the end. A screen that hardcoded these * would be a second source of truth for the one question the store answers. */ interface WorkState { sort: number; name: string; terminal: boolean; next: string[]; } /** One node of a template graph: a record-to-be, in a named Table, under a ref. */ interface WorkTemplateNode { /** The name the graph refers to this node by, and the key an override is filed under. */ ref: string; /** The Table the record lands in. */ table: Uuid; /** The document it starts life with. An override merges on top at instantiation. */ data?: Record; } /** One edge. `owned` is what `custom.relation_own` makes; W1-REL owns the rest. */ interface WorkTemplateRelation { from: string; to: string; kind?: string; } /** * THE WHOLE TEMPLATE, DECLARATIVELY. REC-70: the graph is created in ONE act * and judged before any of it is written, so an agent writes a twelve-step * checklist in a single call and a half-made checklist is not a state the store * can be in. */ interface WorkGraph { nodes: WorkTemplateNode[]; relations?: WorkTemplateRelation[]; } /** What `custom.work_template_instantiate` answers: what it made, and how much. */ interface WorkInstantiation { template_id: Uuid; /** The log row. Until W3-HIST lands, this row IS the log of the act, and says so. */ instantiation_id: Uuid; records_created: number; relations_created: number; relation_kind: string; /** ref → the record it became, which is how a step finds its own record. */ refs: Record; records: Uuid[]; relations: Uuid[]; ms?: number; } /** One row of `custom.work_whose_turn`: a record, whose turn it is, and where it is up to. */ interface WorkTurn { record_id: Uuid; title: string | null; assignee_id: Uuid | null; turn: string | null; due_on: Timestamp | null; state: string | null; status: string | null; terminal: boolean | null; } /** A Field assignment is made of, as the store itself describes it. */ interface WorkAssignmentField { key: string; label: string; spec: Record; } /** * One hold on one slot. `expired` is the STORE's answer, not a clock comparison * a screen ran: a hold whose time is up is named as lapsed rather than quietly * disappearing, so a person can see why the slot came back. */ interface WorkSlotHold { hold_id: Uuid; slot_key: string; holder: string; expires_at: Timestamp | null; expired: boolean; } /** * What an embed token is good for, from `custom.anon_token_verify`. A token * carries ONE decision: `read` or `write`, never both. */ interface AnonTokenBinding { token_id: Uuid; organization_id: Uuid; form_id: Uuid | null; saved_view_id: Uuid | null; record_id: Uuid | null; mode: "read" | "write"; } /** The three things that land in the one inbox. Closed, one word each. */ type WorkInboxKind = "assignment" | "approval" | "proposal"; /** Who filed it: a colleague, or an agent acting for one. */ type WorkOrigin = "person" | "agent"; /** Where a due date stands, in the store's own words. */ type WorkDueState = "overdue" | "due_today" | "scheduled" | "undated" | "finished"; /** One row of `custom.work_inbox` — the SAME shape whichever of the three it is. */ interface WorkInboxItem { item_id: Uuid; kind: WorkInboxKind; origin: WorkOrigin; title: string; subject_id: Uuid | null; subject_kind: string; summary: string | null; state: string; due_on: Timestamp | null; due_state: WorkDueState | null; /** False for a decided row a person asked to see. A screen shows it, inert. */ actionable: boolean; requested_by: Uuid | null; requested_by_name: string | null; at: Timestamp; /** * The table it is about (lane S5-PRIME): the table itself for a new column, new records or a * template; the record's table for a change to a record; null for a new table (it waits on a * home). An assignment carries its record's table. What an inbox groups by. */ table_id?: Uuid | null; table_name?: string | null; /** * A CLOSED ITEM SAYS WHO AND WHEN (lane S5-PRIME). Null while it is waiting. For `withdrawn` * — nobody decided it; the thing it would change was archived — `decided_by` is whoever * archived it (null when the store's own backlog sweep closed it). */ decided_by?: Uuid | null; decided_by_name?: string | null; decided_at?: Timestamp | null; /** The store's own sentence: what was applied, what was left alone, or why it was withdrawn. */ outcome?: string | null; /** * THIS PERSON'S OWN STATE (lane S5-PRIME-2). Set only in the view that shows it: `snoozed_until` * in the snoozed view, `cleared_at` in the done view. Nobody else's snooze or clear is ever read. */ snoozed_until?: Timestamp | null; cleared_at?: Timestamp | null; /** How many of this person's items are snoozed / cleared right now — the same on every row. */ snoozed_count?: number; cleared_count?: number; /** * HOW LONG A DECISION ON THIS ROW IS HELD WITH UNDO before it is made (VERIFIER-20), in seconds: * the organization's `custom/decision_undo_seconds` (default 5) for a pending approval or * proposal; 0 when something already waits on the answer; null for work and closed rows. */ undo_seconds?: number | null; /** Why a decision here cannot be held (it is made at once) — the store's sentence, or null. */ undo_refusal?: string | null; } /** * Which of a person's three inbox views `custom.work_inbox` answers (lane S5-PRIME-2): what is * waiting now, what they snoozed (soonest back first), what they cleared (latest first). */ type WorkInboxView = "inbox" | "snoozed" | "done"; /** What `custom.inbox_snooze` / `inbox_unsnooze` / `inbox_clear` / `inbox_unclear` answer. */ interface InboxStateChange { item_id: Uuid; /** Where the item stands for this person now. */ state: "snoozed" | "cleared" | "waiting"; /** False when an un-snooze / un-clear found nothing to undo (it says so in `sentence`). */ changed?: boolean; snoozed_until?: Timestamp; cleared_at?: Timestamp; /** The store's own sentence, for the person. */ sentence: string; } /** * One organization's line of `custom.inbox_counts` — what waits on this person there, counted by * the same predicate the inbox lists with. The shell badge's number. */ interface InboxCounts { organization_id: Uuid; organization_name: string | null; waiting: number; snoozed: number; cleared: number; overdue: number; oldest_waiting_at: Timestamp | null; } /** Which of the three lists `custom.work_list` answers. */ type WorkListFlavour = "mine" | "assigned" | "unassigned"; /** One row of `custom.work_list`: a record, whose it is, and where it is up to. */ interface WorkListItem { record_id: Uuid; table_id: Uuid; table_name: string | null; title: string | null; assignee_id: Uuid | null; assignee_name: string | null; assignee_user_id: Uuid | null; due_on: Timestamp | null; due_state: WorkDueState; status: string | null; terminal: boolean; assigned_by: Uuid | null; updated_at: Timestamp; } /** * One state a record may be moved to, with the model's own answer about whether * it may — and, when it may not, the sentence saying why. A screen offers what * is allowed and explains what is not; it never renders a dead control. */ interface WorkRecordState { state_id: Uuid; name: string; sort: number; terminal: boolean; is_current: boolean; allowed: boolean; refusal: string | null; } /** The change a pending approval carries. Two kinds, because two can be applied. */ type WorkChange = { kind: "record_patch"; patch: Record; } | { kind: "field_add"; field: Record; }; /** Somebody who may decide a particular request, and the reason they may. */ interface WorkApprover { user_id: Uuid; name: string; why: string; } /** What `custom.work_approval_request` answers: it is filed, and who it waits on. */ interface WorkApprovalFiled { approval_id: Uuid; state: "pending"; subject_id: Uuid; subject_kind: string; origin: WorkOrigin; approvers: WorkApprover[]; message: string; } /** What a decision answers — including whether the change was actually made. */ interface WorkApprovalDecision { approval_id: Uuid; state: "approved" | "declined"; subject_id: Uuid; /** The whole point: approving APPLIES the change, in the same transaction. */ applied: boolean; field_id: Uuid | null; version: number | null; message: string; } /** One inbox decision of a batch (`custom.work_decide_many`). */ interface WorkDecideManyItem { /** The inbox item — a `work_approval` record's id (`WorkInboxItem.item_id`). */ item: Uuid; decision: "approve" | "decline"; note?: string | null; } /** * `archived` (lane S5-PRIME): the thing it would change is archived, so it cannot be decided; the * sentence names it. A withdrawn approval answers `already_decided` with "That was withdrawn on …". */ type WorkDecideVerdict = "approved" | "declined" | "already_decided" | "no_right" | "archived" | "refused"; interface WorkDecideManyResult { index: number; item: Uuid; decision: "approve" | "decline"; verdict: WorkDecideVerdict; /** The store's own sentence: what was applied, or why it was not. */ sentence: string; subject_id?: Uuid; version?: number | null; record_ids?: Uuid[]; code?: string; } /** What `custom.work_decide_many` answers: the counts, then one verdict per item in order. */ interface WorkDecideMany { asked: number; approved: number; declined: number; not_decided: number; results: WorkDecideManyResult[]; } /** What `custom.work_assign` answers: the value, the access, and who holds it now. */ interface WorkAssignment { record_id: Uuid; assigned: boolean; assignee: Uuid | null; assignee_user_id: Uuid | null; assignee_name: string | null; was: Uuid | null; due_date: Timestamp | null; version: number | null; /** The share sentence, or why nothing was taken away when un-assigning. */ access: string; message: string; } /** What a step asks for before it counts as done. Every one of these is CHECKED. */ type ChecklistRequirementKind = /** A person says it is done. */ "none" /** A sentence of evidence, under `note`. */ | "note" /** A named value filled in on the step itself. */ | "answer" /** A named Field on the record the run is about must hold a value. */ | "record_field" /** The step IS a form (SCR-19): finishing it needs a response record. */ | "form" /** The step IS a document from a doc template (REC-68): finishing it needs a render. */ | "document"; interface ChecklistRequirement { kind: ChecklistRequirementKind; /** For `answer` and `record_field`: which value. */ key?: string; /** What a person is asked for, in their words. */ label?: string; /** For `form`: the published form this step is. */ form_id?: Uuid; /** For `document`: the doc template this step renders. */ template_id?: Uuid; } /** One step of a checklist, as it is WRITTEN — not as it is run. */ interface ChecklistStepSpec { /** The name the checklist refers to this step by; lower-case, unique. */ ref: string; /** What a person reads in their work. */ title: string; /** Which role owns it. A role the checklist never declares is refused at save. */ role?: string; /** How many days after the run starts it is due. */ due_days?: number; /** Steps it waits for. A step waits only for steps listed above it. */ depends_on?: string[]; requires?: ChecklistRequirement; } /** A role, and the person who stands in it by default. */ interface ChecklistRoleSpec { role: string; label?: string; user_id?: Uuid | null; } /** What starts a run. */ type ChecklistTriggerKind = "manual" | "record_created" | "status_reached"; interface ChecklistTrigger { kind: ChecklistTriggerKind; /** The Table being watched, for the two kinds that watch one. */ table_id?: Uuid; /** The state, for `status_reached`. */ status?: string; } /** THE COMPLETE DECLARATIVE CHECKLIST — one object, one call, a whole process. */ interface ChecklistSpec { name: string; /** The Table a run is ABOUT: People, Hires, Jobs, Matters. */ about_table_id: Uuid; roles?: ChecklistRoleSpec[]; trigger?: ChecklistTrigger; steps: ChecklistStepSpec[]; } /** What `custom.checklist_declare` answers. */ interface ChecklistDeclared { template_id: Uuid; name: string; about_table_id: Uuid; steps_table_id: Uuid; steps: number; roles: number; trigger: ChecklistTrigger; created: boolean; message: string; ms: number; } /** One row of `custom.checklist_templates`. */ interface ChecklistTemplateSummary { template_id: Uuid; name: string; about_table_id: Uuid | null; about_table: string | null; steps: number; roles: number; trigger_kind: ChecklistTriggerKind; trigger_status: string | null; open_runs: number; total_runs: number; updated_at: Timestamp; } /** `custom.checklist_template_shape` — the spec, plus what the editor needs. */ type ChecklistTemplateShape = ChecklistSpec & { template_id: Uuid; steps_table_id: Uuid | null; may_change: boolean; }; /** What `custom.checklist_start` answers. */ interface ChecklistRunStarted { run_id: Uuid; template_id: Uuid; template: string; about_record_id: Uuid | null; about: string | null; steps_table_id: Uuid; steps_created: number; assigned: number; unassigned: number; origin: string; steps: Array<{ ref: string; step_id: Uuid; title: string; role: string | null; }>; /** Roles nobody is named for, so the screen can say so rather than show blanks. */ waiting_on_a_name: string[]; message: string; ms: number; } /** One step of a RUN, as `custom.checklist_run` answers it. */ interface ChecklistRunStep { step_id: Uuid; step_order: number; ref: string; title: string; role: string | null; assignee_name: string | null; assignee_user_id: Uuid | null; due_on: Timestamp | null; due_state: WorkDueState; status: string | null; finished: boolean; requires: ChecklistRequirementKind; /** * The key the answer is filed under, as the checklist named it. A screen files * evidence under THIS, never under a name it invented — the store looks for * exactly this key and nothing else. */ requires_key: string | null; requires_label: string | null; /** The form or the document template this step is, when it is one. */ requires_id: Uuid | null; evidence: Record; /** The titles of the steps it is still waiting for. */ blocked_by: string[]; may_complete: boolean; /** Why it cannot be finished right now, in the words a person would use. */ refusal: string | null; } /** One row of `custom.checklist_runs`. */ interface ChecklistRunSummary { run_id: Uuid; name: string; template_id: Uuid | null; template: string | null; about_record_id: Uuid | null; about: string | null; about_table_id: Uuid | null; started_at: Timestamp | null; started_by: Uuid | null; origin: string; step_count: number; done: number; overdue: number; next_step: string | null; next_due: Timestamp | null; closed_at: Timestamp | null; } /** What `custom.checklist_step_complete` answers. */ interface ChecklistStepCompleted { step_id: Uuid; run_id?: Uuid; completed: boolean; evidence?: Record; steps_left?: number; run_closed?: boolean; message: string; } /** What `tableRowLabel` / `tableRowLabelSet` answer. */ interface TableRowLabel { /** The formula as it was typed, or null when the table names its records by its title column. */ formula: string | null; /** The single-column label the table falls back to (its `title_field` key). */ title_field: string | null; /** Which of the two names records now: the formula, or the title column. */ effective: "formula" | "column"; /** The type the formula answers (text, number, ...), when it still reads. */ result_type: string | null; /** False when the stored formula text no longer parses against the table's columns (a column was renamed or retired). */ valid: boolean; /** The parser's sentence when `valid` is false. */ error: string | null; /** Whether the caller may set or clear it (the table's maker, or someone with full access). */ can_change: boolean; } /** The four values of "Shown to", in the ladder's order. */ declare const SHOWN_TO_VALUES: readonly ["only_me", "my_team", "everyone", "everyone_on_ai_matrx"]; type ShownTo = (typeof SHOWN_TO_VALUES)[number]; /** What `custom.table_row_defaults` answers: the table's own choices, what applies, and what may change. */ interface TableRowDefaults { /** The table's own choice; null = the organization's default. */ shown_to: ShownTo | null; published_to_web: boolean | null; indexed: boolean | null; effective: { shown_to: ShownTo; published_to_web: boolean; indexed: boolean; }; allowed_shown_to: ShownTo[]; available: { shown_to: boolean; published_to_web: boolean; indexed: boolean; }; can_change: boolean; } /** What `custom.table_row_defaults_set` takes. A key left out is left alone; null = back to the default. */ interface TableRowDefaultsPatch { shown_to?: ShownTo | null; published_to_web?: boolean | null; indexed?: boolean | null; } /** One pass of `custom.table_row_defaults_apply`. `done: false` → call again with `next_after`. */ interface RowDefaultsApplied { table_id: Uuid; seen: number; changed: number; kept_by_hand: number; done: boolean; next_after: Uuid | null; /** The store's own sentence. Show it verbatim. */ sentence: string; } /** What `custom.record_row_controls` answers for one row. */ interface RecordRowControls { shown_to: { value: ShownTo | null; effective: ShownTo; type_default: ShownTo | null; table_default: ShownTo | null; allowed: ShownTo[]; is_child: boolean; can_change: boolean; legacy_only_me?: boolean; }; published_to_web: { available: boolean; value: boolean; table_default: boolean | null; can_change: boolean; }; indexed: { available: boolean; value: boolean | null; table_default: boolean | null; effective: boolean; can_change: boolean; }; } /** What `custom.form_archive` and `custom.form_restore` answer with. */ interface FormArchiveAnswer { archived: boolean; changed: boolean; form_id: Uuid; kind: "form" | "booking"; title: string | null; archived_at?: Timestamp | null; /** The store's own sentence. Show it verbatim. */ sentence: string; } /** One row of `custom.forms_archived` — a form or a booking page that was put away. */ interface ArchivedForm { form_id: Uuid; table_id: Uuid; table_name: string | null; kind: "form" | "booking"; title: string | null; slug: string; archived_at: Timestamp; archived_by: Uuid | null; table_archived: boolean; can_restore: boolean; } /** One row of `custom.checklist_templates_archived`. */ interface ArchivedChecklistTemplate { template_id: Uuid; name: string | null; about_table_id: Uuid | null; about_table: string | null; steps: number; archived_at: Timestamp; archived_by: Uuid | null; table_archived: boolean; } /** * VIS-31. A signed-in OUTSIDER, with the reason in plain English. * * "No organization" cannot mean "no row in the membership table": signup mints * every account a PERSONAL organization, so under that reading nobody would * ever be external. It means no membership of a NON-personal organization, and * `iam.is_external_principal` says that once so no surface can invent a second * answer. */ interface ExternalPrincipalCard { user_id: Uuid | null; /** The answer itself. */ external: boolean; signed_in: boolean; /** * WHY, in the platform's own words — the sentence a portal shows instead of * an empty screen. `iam` writes it, not us, so the portal and the API and the * admin screen all say the same thing about the same person. */ explanation: string; /** How many NON-personal organizations they belong to. Zero is what external means. */ organization_memberships: number; [key: string]: unknown; } /** VIS-32. A record or a view bound to a public address, with a declared render mode. */ interface PublishBinding { id: Uuid; slug: string; resource_type: string; resource_id: Uuid; organization_id: Uuid; render_mode: string; is_active: boolean; created_at?: Timestamp; [key: string]: unknown; } /** * What a SIGNED-OUT visitor resolves. An invented address, a revoked one and an * unpublished one all resolve to NOTHING — the same 404 — because an address * that answered differently for a revoked binding would be an existence oracle. * * `notice` is D-15's admission, carried on every read: nothing was scanned * before this was listed, and nothing here pretends otherwise. */ interface ResolvedPublishBinding { slug: string; resource_type: string; resource_id: Uuid; organization_id: Uuid; render_mode: string; namespace: string | null; notice: string; } /** What a public address holds, as `iam.resolve_publish_binding_kind` names it. */ type PublishBindingKind = "table" | "record" | "dashboard" | "page" | "view"; /** * `iam.resolve_publish_binding_kind` — the same resolution, plus WHAT the address holds. Tables, * records and dashboards all carry the entity token `record`, so the token alone cannot say which; * this door can. `table_id` is the table a list or a record belongs to (a view's subject, a * dashboard's subject table); `title` is its name. Anon may call it. */ interface ResolvedPublishBindingKind extends ResolvedPublishBinding { kind: PublishBindingKind | string; table_id: Uuid | null; title: string | null; } /** `public.permission_level`, in the store's own order. */ /** * `edit_content` (Notion's "Can edit content") sits between `commenter` and `editor`: it may change * the rows and page content of a thing, never its structure (columns, views, settings) or its * sharing — those stay with `editor` and `admin`. */ type PermissionLevel = "viewer" | "commenter" | "edit_content" | "editor" | "admin"; /** * What one subject's level is, as `custom.my_levels` answers it. `null` is an * ANSWER — "you hold nothing on this" — and never a missing row: a screen that * could not tell "no access" from "not asked yet" would draw one of them as the * other. */ interface MyLevel { id: string; level: PermissionLevel | null; } /** One record a name matched. */ interface RelationNameMatch { id: string; /** What its name column holds. */ words: string; } /** The answer of `custom.relation_names_match`. */ interface RelationNamesAnswer { table_id: string; table_name: string | null; title_field?: string | null; names: Array<{ name: string; matches: RelationNameMatch[]; }>; } declare const BUTTON_DOES: readonly ["open_url", "run_action", "run_automation", "add_row"]; type ButtonDoes = (typeof BUTTON_DOES)[number]; /** What `config.button` holds. */ interface ButtonSettings { label: string; /** One of the table's color names (blue, green, …); absent, the button wears the neutral look. */ color?: string | undefined; /** Absent: nothing is set up yet, and a press says so. */ do?: ButtonDoes | undefined; url?: string | undefined; action_id?: Uuid | undefined; automation_id?: Uuid | undefined; /** W2-A `add_row`: the table the new row goes in; absent = this table. */ table_id?: Uuid | undefined; /** W2-A `add_row`: the new row's field key → this row's field key, or "@row" for this row's id (a link back). */ prefill?: Record | undefined; /** TEMPLATE-GAPS R3 `add_row`: the target table's row template the new row starts from (its values under prefill). */ template_id?: Uuid | undefined; } /** What `custom.button_press` answers. */ type ButtonPressed = { do: "open_url"; label: string; url: string; } /** W2-A: the row the press added (through custom.record_write), for the screen to open. */ | { do: "add_row"; label: string; table_id: Uuid; record_id: Uuid; template_id?: Uuid; } | { do: "run_action"; label: string; result: { ran?: number; records?: Array<{ record_id: Uuid; version: number; }>; } & Record; } | { do: "run_automation"; label: string; /** null when no automation this button starts met its conditions (status "skipped"). */ run_id: Uuid | null; automation: string | null; status: string | null; says: string | null; /** AUTOMATIONS-AIRTABLE: every automation whose trigger is this button, as each ran on the press. */ runs?: Array<{ run_id: Uuid; automation: string | null; status: string | null; says: string | null; }>; }; declare const NEWER_FIELD_KINDS: readonly ["rating", "duration", "created_by", "modified_by", "status", "address", "barcode", "rich_text", "count"]; type NewerFieldKind = (typeof NEWER_FIELD_KINDS)[number]; /** FLD-3. A constraint attached to a Field. A Rule, never a behavior. */ interface FieldRule { /** * `unique` (FLD-3 / B1): no two live records of the Table hold the same value, compared * case- and outer-space-blind. The store enforces it under an advisory lock * (`custom._unique_rule_holds`); it is also what lets `recordUpsert` find "the" record by a key. * The mirror never predicts it — it needs a query. */ kind: "min" | "max" | "length" | "pattern" | "equals_field" | "differs_from_field" | "unique"; /** * The rule's own number or word: the least / the most for `min` / `max`, the * LONGEST for `length`, the pattern for `pattern`, the other column's key for * `equals_field` / `differs_from_field`. A `length` rule that only says how * SHORT a value may be carries `min` and no `value`. */ value?: string | number; /** * STORE-RULE-GAPS (the older grid's `patternHint`). On a `pattern` rule only: * how a person writes it, e.g. `"949-555-0142"`. The store refuses an example * its own pattern would refuse, and a refused value is told "Enter it like * 949-555-0142." (the hint, and `detail.example`). */ example?: string; /** * STORE-RULE-GAPS (the older grid's `minLength`). On a `length` rule only: the * fewest characters. A blank is `required`'s question, never "too short". */ min?: number; /** FLD-10. Empty means "wherever the Field applies". */ applies_to_types?: string[]; } /** * `custom.field` — the ONE definitions table (FLD-8), organization-scoped, so * two organizations add different fields to the same standard table. */ /** * WHICH OF THE OTHER RECORD'S COLUMNS A RELATION SHOWS (REL-DISP, 2026-09-21). * * A Table has ONE stored default reference column — its `title_field` — and until * this existed every relation pointing at that Table could display that column and * nothing else. A Jobs board could read "Maria Chen" but never "Maria Chen — * Rincon", and two columns could not be joined into one string at all. * * ABSENT MEANS THE TABLE'S OWN COLUMN. A field with no `display` resolves exactly * as it did before, so nothing changes for a column nobody has given a spec to. * * The store judges it once, in `custom._display_spec_for`: every column named must * exist on the target Table, and the refusal names the one that does not. It is * resolved by ONE body, `custom._words_for`, which `custom.record_words`, * `custom._card_words`, `custom.relation_words` and `custom.relation_words_many` * are all one-liners onto — so the thirteen store-side readers inherit it too. * * A display column that is itself a relation resolves ONE hop and never a second. */ interface RelationDisplay { /** The target's columns, in the order they are shown. */ columns: string[]; /** What goes between them. Defaults to a single space. */ separator: string; } interface Field { id: Uuid; organization_id: Uuid; /** Set when this Field extends a standard entity rather than a custom Table. */ entity_definition_id: Uuid | null; /** The token of the Table this Field belongs to. */ table_token: string | null; key: string; label: string; name: string | null; /** * When this Field was taken away (retired / archived), or null while it is live. Filled by the reads that * can answer a retired Field — `entityFields({ include_retired: true })`; absent from reads that never do. */ archived_at?: string | null; /** FLD-1. Exactly one of five. */ type: FieldBehavior; /** FLD-2. The shape of the value, never a second behavior. */ multi: boolean; /** FLD-2. The values carry their own dates — the world clock, not History. */ dated: boolean; /** FLD-3. Any number of attached validation Rules. */ rules: FieldRule[]; config: Record; required: boolean; default: unknown; sort: number | null; /** The Table this relation points at. */ relation_target: Uuid | null; relation_max: number | null; on_target_delete: OnDelete | null; /** REL-9. The reverse is visible from both ends and is NOT a second Field. */ inverse_key: string | null; /** REL-DISP. Which of the target's columns this one shows. Null = the Table's own. */ display: RelationDisplay | null; source: FieldSource; source_config: Record; /** FLD-9. A Formula declares whether it computes on read or on write. */ compute_on: ComputeOn | null; /** The unit, on the Field, because it changes the meaning. Never presentation. */ unit: string | null; format: string | null; /** AGT-7. A ceiling a merge field's delivery can never widen. */ sensitivity: FieldSensitivity | null; /** FLD-12. Whether an agent may see this Field's values: include · summarize · exclude · on_request. */ context_policy: ContextPolicy$1 | null; /** AGT-6. How often a `source = agent` Field is refilled. */ review_interval_days: number | null; /** * §D says this is on the wrong object and moves to the merge field. It is * still carried here because the live column is, and the client reads what * the store holds — never what the plan wishes it held. */ depends_on: Uuid[] | null; /** FLD-10. Which record types this Field applies to. Empty means all of them. */ applies_to_types: string[]; /** FLD-5 / FLD-6. A list field's choices are the records of THIS Table. */ options_table_id: Uuid | null; created_by: Uuid | null; updated_by: Uuid | null; created_at: Timestamp; updated_at: Timestamp; version: number; metadata: Record; visibility: string; } /** AGT-8 / AGT-4. What an agent (or a person) proposes when a Field is missing. */ interface FieldProposal { table_id: Uuid; key: string; label: string; type: FieldBehavior; multi?: boolean; required?: boolean; config?: Record; relation_target?: Uuid; /** Why this Field should exist — what an approver reads before approving. */ because: string; } /** AGT-8. What a table proposal looks like, same shape of question. */ interface TableProposal { name: string; slug: string; title_field: string; fields: Array>; because: string; } /** * WHAT A PERSON SAYS WHEN THEY ADD A COLUMN — the argument of * `custom.field_declare`, and deliberately NOT a `Field`. * * A `Field` is what the store STORES: a behavior from a closed set of five, * modifiers beside it, a format, a unit, a relation target, a relation maximum, * an on-delete word, a compute_on, and the attached Rules that make a format * enforceable. A person adding a column knows none of that. They know they want * a date, or a person, or a list with these three choices in it. * * Every client that tried to bridge that gap itself got it wrong — the field * panel picked "Person" and the store read a Formula — so the bridge is the * STORE's, once, and this is the shape it takes. */ interface NewFieldDeclaration { /** What a person reads on the column. The only thing that is always required. */ label: string; /** One of the thirteen. Absent means one of the three plain behaviors below. */ parity_type?: ParityFieldType; /** Plain text, a long text or a plain number — the three that carry no parity type. */ plain?: "text" | "long_text" | "number"; /** * THE ONE WORD EVERY OTHER CALLER REACHES FOR. An agent, a script, an import * and this package's own table-declaring path all say `type`, and until * 2026-09-20 `custom._field_document_for` threw it away — a `number` became a * text column in silence. It now resolves all three vocabularies, so this is * named here rather than left as a shape only the store knows about. */ type?: FieldTypeWord; /** Derived from the label when absent, which is why the panel never asks for it. */ key?: string; /** T8 / CD-CORE-15. The record types this column applies to (its table's type field's choices). Absent: every record. */ applies_to_types?: string[]; required?: boolean; multi?: boolean; dated?: boolean; sort?: number; sensitivity?: FieldSensitivity; context_policy?: ContextPolicy$1; rules?: FieldRule[]; /** DEFECTS 7: `true` is the plain way to say `rules: [{ kind: "unique" }]`; the store adds the rule once. */ unique?: boolean; /** A choice list's choices, as words. The store keeps them as a Table (FLD-5 / FLD-6). */ options?: string[]; /** …or the Table they already come from, when a person picked an existing list. */ options_table_id?: Uuid; /** The currency symbol or code a currency field's numbers are in (FLD-N-1). */ unit?: string; /** A date field: a day, or a day and a time. */ kind?: "date" | "datetime"; /** A lookup or a rollup: the key of the relation Field on THIS table it travels along. */ via?: string; /** A lookup: the key of the value it reads on the far side. */ pick?: string; /** A rollup: what it works out, and of which far-side field (`count` needs none). */ agg?: "sum" | "count" | "min" | "max" | "avg" | "countall" | "counta" | "and" | "or" | "xor" | "arrayjoin" | "concatenate" | "arrayunique" | "arraycompact"; of?: string; /** * A rollup: WHICH linked records it adds up (CHAIR-MATH b, 2026-10-03). The same shape a saved * view's `filters` carries, written against the far table — a flat map of far-side column keys * (`{ status: "open" }`, choice columns by their words, windows as a view writes them) or a Rule * expression over far-side field IDs (`{ op: "eq", args: [{ field: "" }, { const: false }] }`). * The store validates it against the far table on declare and update; absent or `null` means * every linked record. Kept in `config.filter`. */ filter?: Record | null; /** A formula: the expression, in the same shape a Rule uses (REC-15). */ expr?: Record; /** * A formula IN THE LANGUAGE A PERSON TYPES — `{Visit fee} - {Deposit taken}` (GRID-PRIMITIVES * G3). The store parses it (`custom.formula_parse`), keeps the text in `config.formula_text` * and the expression it parses to in `config.expr`; send this OR `expr`, never both. */ formula_text?: string; /** * LANE SC-R — an `entity_reference` column's kinds: the platform entity tokens it may point at * (`note`, `site`, `agent` …), kept by the store in `config.allowed_types`. */ allowed_types?: string[]; compute_on?: ComputeOn; relation_max?: number; /** * REL / FLD-11. The Table this column points at, when it is neither a Person * (`member`) nor a File (`attachment`). The store has taken it since * 2026-09-20 and no client type named it, so a person with an Invoice table * and a Line table could not declare the link through this package at all. */ relation_target?: Uuid; /** REL-2. What happens to this record when the thing it points at goes. */ on_target_delete?: OnDelete; /** * REL-DISP. Which of the target's columns this one shows, and what joins them. * A bare column name and a plain list are both accepted by the door and * normalized; leaving it out keeps the target Table's own reference column. */ display?: RelationDisplay | string[] | string; /** * Presentation and behavior settings the store carries verbatim — a long * text's `multiline`, a list's existing options table. The door has always * read it (`p_spec -> 'config'`) and this interface never offered it, so a * package-owned table could not declare its own multi-line column. */ config?: Record; /** A text column that holds a signature (`custom.doc_sign` accepts no other). */ format?: string; /** * STORE-RULE-GAPS (the older grid's `allowOther`). A choice list only: a value * that is none of the choices is ADDED to the list as typed instead of * refused. Stored as `config.allow_other`; absent means off. */ allow_other?: boolean; /** LANE 10 P4 — a rating's top star, 1 to 10 (5 when absent); the store keeps it as a max Rule. */ max?: number; /** LANE 10 P4 — a status's groups: choice words → `todo`, `in_progress` or `done`. */ status_groups?: Record; /** LANE 10 P4 — a barcode's symbology, one of `BARCODE_SYMBOLOGIES`; absent takes any code. */ symbology?: string; } /** * Every word `custom._field_document_for` resolves for `type`: any parity type * in `PARITY_FIELD_TYPES`, any behavior those types are made of, or one of the * plain synonyms a person or an agent actually writes. No count is written down * here on purpose — the parity floor is regenerated from * `custom.parity_field_types()` and grew from thirteen to fourteen the day * `checkbox` landed, which is how the store's own refusals came to name a list * that no longer matched it. */ type FieldTypeWord = DeclarableFieldKind | FieldBehavior | "number" | "integer" | "decimal" | "float" | "long_text" | "string" | "autonumber" | "created_time" | "modified_time" | "entity_reference" | NewerFieldKind; /** * What `custom.field_update` accepts. * * THE SETTINGS **AND** THE BEHAVIOR. The door has had a behavior arm since * T12 — say `parity_type`, `plain` or `type` and every value of the column is * converted (or kept in `_retired` with its reason) by * `custom._field_type_converts_values`. This interface named only the settings, * so the one client that needed to move a field back onto the behavior it is * written against had no door and reached for `custom.record_update` instead, * which writes the document under the guards rather than through them. */ /** One existing choice of a list, re-worded in place (`FieldPatch.options`). */ interface FieldOptionEdit { /** The option record's id, as `fieldOptions` answers it. */ id: string; /** What the choice reads as from now on. */ words: string; } interface FieldPatch { label?: string; /** T8 / CD-CORE-15. The record types this column applies to; `[]` makes it apply to every record. */ applies_to_types?: string[]; required?: boolean; dated?: boolean; sort?: number; sensitivity?: FieldSensitivity; context_policy?: ContextPolicy$1; unit?: string; rules?: FieldRule[]; /** * The choices of a choice list, rewritten, IN ORDER (lane CHOICE-COLUMN-EDIT, 2026-09-27). * * Each entry is either `{ id, words }` — `id` is the option record's id as `fieldOptions` * answers it — which RE-WORDS that choice in place (its key never changes, so every cell * holding it now reads the new words), or plain words: the live choice with the same words * (any case), else a retired one brought back, else a new choice. Every choice takes the * position it has in this array. A live choice the array does not name is RETIRED (never * deleted): cells holding it keep it and read as its words. All in one transaction. */ options?: Array; /** * Words ADDED to a choice list's choices without touching the others: a live same-words * choice adds nothing, a retired one is brought back, the rest are appended. Refused by name * beside `type` / `parity_type` / `plain`. */ options_add?: string[]; /** One of the thirteen — a change of what this column HOLDS, not of a setting. */ parity_type?: ParityFieldType; /** …or one of the three plain behaviors, …*/ plain?: "text" | "long_text" | "number"; /** …or the one word every other caller reaches for (see `NewFieldDeclaration`). */ type?: FieldTypeWord; /** * FLD-2. The shape of the value — one, or several. * * This comment used to say that a patch moving `multi` ALONE changed nothing * and that the behavior word had to be sent with it. That was true, and it * was a description of a silent failure rather than a contract: the door * answered "saved" and the column still held one. `custom.field_update` has * had a real settings arm for it since 2026-09-20 (matrx-frontend * `migrations/campaign/fix7b_field_multi_arm.sql`), which also moves a * relation's `relation_max` beside it — `custom.validate_values` counts links * against that number, so moving `multi` without it would tick a box and * still refuse the second record. * * A CHOICE LIST IS THE ONE EXCEPTION, and it is refused by name rather than * ignored: for `select` / `multi_select`, "one answer or several" IS the * parity type, so send the type instead. */ multi?: boolean; /** REL / FLD-11: re-point the column. The old edges are withdrawn by the door. */ relation_target?: Uuid; on_target_delete?: OnDelete; /** * REL-DISP. Which of the target's columns this one shows. `null` REMOVES the * spec and the column goes back to whatever the table it points at is titled by; * the door refuses it by name on a column that points at nothing. */ display?: RelationDisplay | string[] | string | null; /** SEAT-SUITES: whether the store builds an index for this column, and a unique one. */ promoted?: boolean; unique?: boolean; /** * STORE-RULE-GAPS. A choice list's "other values allowed" setting — see * `NewFieldDeclaration.allow_other`. Refused by name on any other column; a * retype that stays a list keeps it. */ allow_other?: boolean; /** LANE 10 P4 — a status's groups, changed in place: choice words or keys → `todo` | `in_progress` | `done`. */ status_groups?: Record; /** LANE 10 P4 — a barcode's symbology; null takes any code again. */ symbology?: string | null; /** LANE 10 P4 round 3 — a rating's new top star, 1 to 10; scores above it are set aside in `_retired`. */ max?: number; /** NOTION-PROPS-3 — a button column's settings, whole (`config.button`); refused on any other column. */ button?: ButtonSettings; /** NOTION-PROPS-3 — a link to its own table says one record waits on another (a timeline draws it). */ dependency?: boolean; /** W2-A 4 — a Last changed time column watches only these field keys (`config.only_fields`); null = the whole record. */ only_fields?: string[] | null; /** NOTION-PROPS-3 — a one-record link to its own table names each record's parent: the table nests sub-items. */ sub_items?: boolean; /** CLOSE-LINK (CD-LINK-03) — a link's picker offers only this saved view's records (`config.limit_to_view`); null takes it off. */ limit_to_view?: string | null; /** CLOSE-LINK (CD-LINK-05) — a link follows the linked record (`live`, the default) or keeps a copy of it from when it was linked (`snapshot`, `config.binding`); turning snapshot on takes a copy for every link already there. */ binding?: "live" | "snapshot"; /** CLOSE-LINK (CD-LINK-08) — a link carries access: whoever can see a linked record can see this one (`config.carries`), at most at `carries_max`. */ carries?: boolean; /** CLOSE-LINK (CD-LINK-08) — the most a carrying link conveys: `viewer` (the default) or `editor`. */ carries_max?: "viewer" | "editor"; /** CLOSE-LINK (CD-LINK-09) — two records may point at each other along this link (`config.loops`). */ loops?: boolean; /** CLOSE-LINK (CD-LINK-04) — the other tables a link also points at (`config.target_tables`, its own table always first); null or [] = only its own table. */ target_tables?: readonly string[] | null; /** §D / T7: which fields a worked-out column reads. */ depends_on?: Uuid[]; /** * A rollup: WHICH linked records it adds up — the same shape as `NewFieldDeclaration.filter` * (a saved view's filter written against the far table). `null` removes it, and the rollup adds * up every linked record again. `custom.field_update` validates it against the far table. */ filter?: Record | null; /** * FLD-9. WHEN a worked-out column works its answer out: when somebody reads the record, * or when somebody saves it. * * This door accepted the word, answered success and threw it away until 2026-09-21 * (matrx-frontend `migrations/campaign/tails2_a_formula_says_when_it_works_itself_out.sql`) * — its spec builder used a fixed key list that did not carry it, so a person declaring a * write-stamped formula through this door silently got a read-time one. It is applied now, * in both arms, and the records already saved are brought with it. * * TWO THINGS IT REFUSES BY NAME rather than forcing quietly: a column that is not worked * out by the store has no moment to be worked out at, and a ROLL-UP adds up OTHER records, * so an answer stamped when this record was last saved would be wrong the moment one of * them changed — a roll-up is always `read`. */ compute_on?: ComputeOn; /** * FLD-11 / REC-15. RE-AIMING A WORKED-OUT COLUMN: the expression, in the same * shape a Rule uses and read by the same evaluator — `{"op":"concat","args": * [{"field":"note"}]}`. Every `{"field": …}` in it names a live Field of this * organization, or `custom._field_document_for` refuses the whole patch by * name and writes nothing. * * `custom.field_update` carries this in its BEHAVIOR arm — the arm a type * word opens — so send it WITH the word for what the column is * (`type: "formula"`, or `parity_type` / `plain`). The door's spec builder * reads `coalesce(p_patch -> 'expr', v_old -> 'config' -> 'expr')`, so a * re-type that does not carry one deliberately keeps the expression the * column already had; the settings arm has no `expr` key at all, so `expr` * alone is not a change this door makes. `STORE-TAILS-2` asked for the field * because the one client that needed to re-aim an existing formula had no * typed way to say it and reached for `custom.record_update`, which writes the * Field document under the guards rather than through them. */ expr?: Record; /** * RE-AIMING A FORMULA IN THE LANGUAGE A PERSON TYPES (GRID-PRIMITIVES G3). Same arm as * `expr` — send it with `type: "formula"`; the store parses it and keeps both the text and * the expression. Measured on the dev clone 2026-09-23: `{"type":"formula","formula_text": * "{Visit fee} * 2"}` re-aims the column and keeps its other settings. */ formula_text?: string; } /** * What `custom.field_duplicate` answers (DUPLICATE PROPERTY, Notion N5): the copy's id, key and name, and * how many records had a value copied into it. `values` is `not_a_value` for a link, formula, lookup or * roll-up column, whose copy holds no document values of its own. */ interface FieldDuplicated { field_id: string; key: string; label: string; values_copied: number; values: "copied" | "not_a_value"; } /** The two levels a custom table can hold. */ type TableLevel = "organization" | "confidential"; /** One readers rule: `field` is one of the table's own fields that names a person or a team. */ interface TableReaderRule { field: string; level?: "viewer" | "commenter" | "editor"; } /** The last approval an organization's admin gave this table (`null` when none ever did). */ interface TableLevelApproval { level: TableLevel; reason: string; approver_role: "owner" | "admin"; approver_user_id: string; approved_on: string; } /** `custom.table_level_facts` verbatim. */ interface TableLevelFacts { table_id: string; organization_id: string; level: TableLevel; readers: TableReaderRule[]; maker_is_reader: boolean; /** Who approves a change of level: the table's organization admins, or the platform (Arman). */ set_by: "org_admin" | "platform"; /** True only for an owner/admin of the table's organization on an ordinary custom table. */ can_set: boolean; last_approval: TableLevelApproval | null; /** * True when THIS person is told the table's name and nothing about its columns (a Confidential * table, knob `access/confidential_hides_structure`, `custom.may_see_table_structure`). Its * column list is then empty by decision, never "no columns yet". Absent from a store that * predates it: read as false. */ structure_hidden?: boolean; } /** What `setTableConfidential` / `setTableOrganization` answer. */ interface TableLevelSet { token: string; table_id: string; level: TableLevel; approval_id: number; approver_role: "owner" | "admin"; from: TableLevel; readers?: TableReaderRule[]; } /** * REC-1. A Table declares its fields, exactly one Home, and the properties * `display`, `ordered`, `heavy` | `light`, `retention` and `detail`. */ interface Table { id: Uuid; organization_id: Uuid; /** * The access ladder's Table "Level": organization · public · confidential · private. Absent is * the organization's own. Confidential and Private records have no "Shown to". */ level?: string | null; /** The token this Table is addressed by. */ slug: string; name: string; label_singular: string | null; label_plural: string | null; icon: string | null; color: string | null; /** REC-32. One of the seven, closed. */ type: TableType; /** REC-11. A detail Table inherits only: no direct shares, never a Home. */ detail: boolean; parent_token: string | null; /** AGT-1 / AGT-2. On by default for a custom table; an organization turns it off. */ agent_writable: boolean; display: TableDisplay | null; ordered: boolean; /** REC-4. `heavy` is indexed with Rules on write; `light` is the whole document with Rules on read. */ weight: StorageMode; /** REC-23. How long a soft delete stays reversible. */ retention_days: number | null; /** REC-2. Every Table has one; without it a record cannot be a chip. */ title_field: string | null; /** * T8 / CD-CORE-15. The Field KEY whose value is each record's type (a Shape's Kind); which columns a record * has follows it (`applies_to_types` on a Field). Absent or null: every record has every column. */ type_field?: string | null; /** The field ids this Table declares, in declaration order. */ fields: Uuid[]; default_sort: string | null; row_order: string | null; /** REC-14. Where this Table lives. Exactly one primary Home. */ home_id: Uuid | null; /** The feature that keeps this Table out of the person's lists (`snapshot`, `copying`, …); absent for a person's own table. */ kept_for?: string | null; /** REC-27. One of the nine, defined in code and not editable by Migration. */ is_kernel: boolean; created_by: Uuid | null; updated_by: Uuid | null; created_at: Timestamp; updated_at: Timestamp; version: number; metadata: Record; /** The old lane word; read `shown_to` and `published_to_web` (access ladder), which the store answers now. */ visibility: string; /** Access ladder "Shown to" (which lists show it; never a lock). Absent on a store that does not answer it. */ shown_to?: string | null; /** Access ladder "Published to the web". Absent on a store that does not answer it. */ published_to_web?: boolean; /** * Lane 10 FD. Part of the business's day-one data (Patients, Therapists, Services) — a mark, not * storage. Absent is false. Only admin on the Table changes it (custom._table_shape_guard). */ foundation?: boolean; /** * Present only on what `tableFind({ …, keptFor })` answers for an ARCHIVED table of that slug and * kept_for (the store's `archived_at`); such an answer carries only `id`, `organization_id` and this. */ archived_at?: Timestamp | null; } /** What `tablePropose` / `tableList` hands back about how full a Table is (REC-N-5). */ interface TableCapacity { records: number; ceiling: number; full: boolean; /** The knob that decides the ceiling, named so a person can act on it. */ knob: string; } /** * WHAT ONE PASS OF `custom.table_archive` ANSWERS WITH — the numbers a screen * needs to draw a bar and write a sentence, instead of a spinner and an * SQLSTATE (VERIFIER-10 F3, VERIFIER-11 F3). * * Archiving a table is a JOB, NOT A STATEMENT. `custom.record_delete` on a * Table recurses through everything the Table contains inside one transaction, * which is unbounded by construction: at 1,430 records it dies on `57014`, * "canceling statement due to statement timeout", and nothing is changed. The * remedy for a thing that cannot finish in one statement is more statements, * never a longer one — so the caller loops, passing its own `chunk`, until * `done`. * * IT IS RESUMABLE BECAUSE IT IS STATELESS. There is no job row to lose: * `remaining` is read off the records themselves on every call, so a browser * closed halfway, a failed pass or a deploy mid-run all leave a table that is * half archived and a next call that picks up exactly where the last stopped. * Which is why a screen driving this may NEVER say "nothing was changed" after * a pass that moved rows — `archived_total` is what really landed. * * NOTHING IS DELETED. Every row goes through `custom.record_delete`, so every * guard, history capture and outbox row still happens and `recordRestore` * still brings any of them back. */ interface TableArchiveProgress { table_id: Uuid; /** The table's own name, so a sentence can use it rather than an id. */ table_name: string; /** What THIS pass archived. */ archived: number; /** Records of this table still live. Zero is the end of the row work. */ remaining: number; /** Records this table has ever held: `remaining` + `archived_total`. */ total: number; /** Records of this table already archived, across every pass ever made. */ archived_total: number; /** Whether the Table record itself has been archived. */ table_archived: boolean; /** Nothing live is left to do. The caller's loop ends here. */ done: boolean; /** The chunk the door actually took on — it clamps what it is handed. */ chunk: number; /** The store's own sentence about this pass, in the product's voice. */ message: string; /** * An earlier run of this archive was started and has not finished (an open archive event). A * screen reading this after a reload offers to carry on, never a fresh button (BREAKER-1 B-F13). * Absent from a store that predates it. */ in_progress?: boolean; /** * THE REVERSIBLE ACTION'S SIZE LINE (knob `custom/archive_confirm_over`, organization-overridable): * a table with at most this many live records is archived at once and announced with Undo; one * with more is asked about first, with the count. Absent from a store that predates it — a screen * then asks first, as it always did. */ confirm_over?: number; } /** * BRINGING A WHOLE TABLE BACK — ONE PASS OF IT (`custom.table_restore`, lane TABLE-ACTIONS). * * The mirror of {@link TableArchiveProgress}: the Table and its structure come back in the first * pass, then its records in the order they went, then what was built on it, all from the one * archive event. The CALLER LOOPS until `done` (`client.tableRestoreWhole` does it). */ interface TableRestoreProgress { table_id: Uuid; table_name: string; /** Records THIS pass brought back. */ restored: number; /** The Table, its Fields, rules and views THIS pass brought back (the first pass). */ structure_restored: number; /** Forms, views, portals, child Tables… built on the Table THIS pass brought back. */ built_on_restored: number; /** Rows the archive took that are still waiting. */ remaining: number; built_on_remaining: number; /** Rows refused on their own now (a value another row took since): left archived and named. */ left: number; left_ids?: Uuid[]; /** Each row left archived, with the store's own reason for it. */ left_reasons?: Record; /** Restoring events of this table still open (a re-archive mid-restore leaves two). */ events_open?: number; /** Whether the Table record itself is live. */ table_restored: boolean; /** Nothing the archive took is still waiting. The caller's loop ends here. */ done: boolean; chunk: number; archive_event: Uuid | null; message: string; } /** * WHAT A STORED COLUMN DIFFERS FROM THE SPEC THAT ASKED FOR IT IN (lane PLATFORM-APP-DATA). * * `custom.table_ensure` never retypes, relabels or drops a column a table already has — a retype is * a conversion a person decides. It used to ignore the difference without a word; now it names * each one, aspect by aspect, and still changes nothing. This shape is the contract a release-time * drift check will read, so it stays this small: * * - `type` the column's kind: its parity type when it has one, else its behavior (`text`, `range`, …) * - `format` how it is shown (`long`, `phone`, `currency`, …); `null` when it has none * - `label` the column's name * - `multi` / `required` booleans (absent = `false`) * - `unique` whether it holds a `unique` rule * - `spec` the spec for that column could not be read at all; `declared` is the store's sentence * * The declared side is the spec as the store WOULD store it, so `long_text` compares as the * `text` + `long` it is stored as — never a false difference. */ /** `extra`: a column the table holds that the spec no longer declares (kept, never removed). */ type FieldDriftAspect = "type" | "format" | "label" | "multi" | "required" | "unique" | "spec" | "extra"; interface FieldDrift { /** The column's key. */ field: string; aspect: FieldDriftAspect; /** What the column holds now. */ stored: string | boolean | null; /** What the spec asked for (for `spec`, the store's sentence about why it could not be read). */ declared: string | boolean | null; } /** The Feature Knob `custom/choice_nudge`: ask (the default), always add, or never add. */ type ChoiceNudge = "ask" | "always_add" | "never_add"; /** How many records hold one choice (`custom.field_choice_usage`), keyed by the option's id. */ interface ChoiceUsage { key: string; words: string; retired: boolean; /** Live records the caller may see whose cell holds the choice. */ records: number; } /** Where the records of a removed choice go. */ type ChoiceRehome = { then: "move"; to: string; } | { then: "keep"; } | { then: "clear"; }; /** One cell as it was before a rehoming save — what its undo writes back. */ interface ChoiceCellBack { record_id: string; value: unknown; } /** What `fieldUpdateRehomingChoices` answers. */ interface ChoicesRehomed { field_id: string; rehomed: Array<{ choice_id: string; words: string; then: ChoiceRehome["then"]; to: string | null; records: number; }>; cells_back: number; /** Send `{ patch: { options: undo.options }, cells_back: undo.cells_back }` to put it all back. */ undo: { options: Array<{ id: string; words: string; }>; cells_back: ChoiceCellBack[]; }; } /** One choice as the detail / selection documents hand it back. */ interface PickListItem { id: string; label: string; /** Never present on the selection read: that read is the one a non-owner may see. */ description?: string | null; help_text?: string | null; group_name?: string | null; icon_name?: string | null; } /** A pick list as `pick_list_get` / `pick_list_for_selection` / `pick_list_update` answer it. */ interface PickListDocument { id?: string; list_id?: string; list_name?: string; description?: string | null; organization_id?: string | null; /** The choices in order (the update door answers this flat shape). */ items?: PickListItem[]; /** The choices by group (the read doors answer this shape). */ items_grouped?: Record; [key: string]: unknown; } /** One choice to write when a list's choices are replaced. Either casing of each key is read by the door. */ interface PickListItemInput { label: string; description?: string; help_text?: string; group_name?: string; icon?: string; } /** One row of the pick-list index (`custom.pick_list_index`): what a picker needs to name a list. */ interface PickListIndexEntry { id: string; list_name?: string | null; description?: string | null; item_count?: number | null; organization_id?: string | null; } /** What `custom.pick_list_index` answers: the lists a person may open, and the archived ids. */ interface PickListIndexDocument { lists?: PickListIndexEntry[]; archived_ids?: string[]; [key: string]: unknown; } /** `custom.migrate_retype`'s answer for a Field. */ interface FieldRetyped { verb: "retype"; field_id: string; was: string | null; now: string; changed: boolean; migration_id?: string; /** Records that hold a value for this column after the change. */ records_still_holding_a_value?: number; /** Values of this column now kept aside in `_retired`, with their reason. */ values_in_retired_for_this_field?: number; at?: string; } /** * `custom.field_retype_preview`'s answer: what a change of a column's kind WILL do, counted by the store * running the change itself in a subtransaction it rolls back (lane F2, D11). It cannot disagree with the * change, because it is the change. */ interface FieldRetypePreview { /** Records holding a value before the change. */ held_before: number; /** Records still holding a value after it (converted, or already fitting). Null when `limited`. */ converts: number | null; /** Values the change keeps aside in `_retired`, with their reason. Null when `limited`. */ kept_aside: number | null; /** * The column holds too many values to run the change ahead of time without holding the table (ADV-1 F8), * or another editor held it. No counts then; `held_before` is still the number of values held. */ limited?: boolean; /** Why it is limited: "large" (too many values) or "busy" (another editor holds the table). */ why?: string; /** Up to five of the values kept aside, as words. */ examples: string[]; /** Choices the change makes from the column's words (a change to Choice or Multi-choice). */ choices_added: number; } /** * `custom.migrate_merge`'s answer (T5 / REC-21): the other record merged into the survivor. Its * values the survivor did not hold moved across; the ones that differed are kept as ranked * alternates with their source (or in `_retired` when the key is not a declared field); every * link and child pointing at it now points at the survivor; it is archived and its id resolves to * the survivor. `migration_id` is what `migrateUndo` takes. */ interface RecordsMerged { verb: "merge"; winner: string; loser: string; migration_id: string; values_taken: number; values_copied: number; alternates_added: number; values_retired: number; relations_retargeted: number; relations_detached_as_duplicates: number; resolves_to?: string | null; at?: string; } /** `custom.migrate_split`'s answer. The original keeps its id; the moved values live on `new_record`. */ interface RecordSplit { verb: "split"; kept_the_id: string; new_record: string; migration_id: string; moved: string[]; old_id_resolves_to?: string | null; at?: string; } /** `custom.migrate_extract_parent`'s answer. The named values were COPIED up into `parent`, and the record now sits under it. */ interface ParentExtracted { verb: "extract_parent"; record_id: string; parent: string; was_under: string | null; migration_id: string; at?: string; } /** * `custom.migrate_retype`'s answer for a RECORD (test 9 / REC-N-18): the record moved to another table of * the same organization and KEPT ITS ID, so every link to it still resolves. The values whose column the * new table lacks are `misfits`: taken off the record and kept in History on `migration_id`, with the * reason — neither coerced nor deleted. `migrateUndo(migration_id)` puts the record back on its table * with every value it held. */ interface RecordMoved { verb: "retype"; record_id: string; kept_the_id: boolean; from_table: string; to_table: string; migration_id: string; /** The values that did not fit, by key, exactly as they were. */ misfits: Record; misfits_are?: string; at?: string; } /** One row of `custom.migrations`: a recorded Migration on one target, and whether it was put back. */ /** `custom.history_retention`'s answer: the organization's floor and (when asked) one table's days. */ interface HistoryRetention { organization_id: string; table_id: string | null; floor_days: number; table_days: number | null; /** The store's own sentence. */ says: string; } interface MigrationEntry { id: string; verb: string; target_kind: string; target_id: string; note: string | null; applied_at: string; applied_by: string | null; undone_at: string | null; } /** `custom.migrate_undo`'s answer: the Migration it put back. */ interface MigrationUndone { undone: string; verb: string; kind: string; record_id: string | null; version_after?: number | null; also_restored?: number; ids_unaliased?: number; /** A split or an extracted parent: the record the Migration made, archived by the undo. */ removed?: string | null; at?: string; } /** The seven table types. Only these two carry custom fields (REC-34, DD-011). */ type CustomFieldTableType = "entity" | "detail"; /** What `custom.entity_table` answers about a registry token. */ interface EntityTableFacts { token: string; schema_name: string; table_name: string; type: CustomFieldTableType; title_column: string | null; /** The name a person reads. Never the type word. */ label: string; /** * 57 of the 643 have no `organization_id`. They carry the column like every * other one, but a VALUE on them would belong to nobody — the guard finds an * organization's Fields through the row's own `organization_id` — so the * value doors refuse them BY NAME rather than writing something unowned. */ has_organization: boolean; has_deleted_at: boolean; } /** One custom field of a standard row, as the read door hands it back. */ interface EntityFieldValue { id: Uuid; key: string; label: string; type: Field["type"]; parity_type?: ParityFieldType; format?: string; unit?: string; multi?: boolean; required?: boolean; sort?: number; sensitivity?: Field["sensitivity"]; options_table_id?: Uuid; relation_target?: Uuid; /** The value itself, or undefined when this row has none. */ value?: unknown; /** VAL-1…VAL-8: who wrote it, when, on whose behalf, in which version. */ written?: { ver?: number; actor?: string; on_behalf_of?: string | null; at?: string; src?: unknown; absent?: unknown; alternates?: unknown; dated?: unknown; }; } /** * REC-53 IN ONE ANSWER: a standard row is its real columns, plus the base * contract, plus its organization's custom fields — three named blocks, never * one flat bag, so nothing can quietly shadow anything. */ interface EntityRecordRead { token: string; label: string; type: CustomFieldTableType; id: Uuid; organization_id: Uuid; /** The registry's `title_column` of this row, when it has one. */ title: string | null; /** The row's own columns, `custom_fields` removed. */ columns: Record; /** The custom values, by key. */ custom: Record; /** Their envelopes, by key. */ custom_written: Record; /** The definitions, in the organization's own sort order, each with its value. */ fields: EntityFieldValue[]; live: boolean; } /** AP-4: one custom row that links to a platform record, and which of its fields does the linking. */ interface EntityBackLinkItem { record: { token: "record"; id: Uuid; label: string | null; }; table_id: Uuid; table_label: string | null; field_id: Uuid; field_key: string; field_label: string | null; organization_id: Uuid; linked_at: string | null; } /** What `custom.entity_back_links` answers: the target, one page of linking rows, and where the next page starts. */ interface EntityBackLinks { target: { token: string; id: Uuid; label: string | null; }; items: EntityBackLinkItem[]; next_cursor: string | null; } /** What `custom.entity_row_write` answers: which row, whose, and whether anything changed. */ interface EntityRowWritten { id: Uuid; token: string; organization_id: Uuid; /** True when every value already matched: nothing was written and the version did not move. */ unchanged?: boolean; /** True when the call had no record id and the door added this row (a token whose create_via is "insert"). */ created?: boolean; } /** One row the filter matched. */ interface EntityRecordMatch { id: Uuid; title: string | null; value: unknown; } interface EntityRecordsFound { token: string; label: string; key: string; value: unknown; rows: EntityRecordMatch[]; count: number; } /** * What a person knows when they add a column. The SAME spec `fieldDeclare` * takes for a custom table — the store turns it into the document its guards * demand, so a standard table gets the whole type vocabulary rather than a * second, poorer one. */ type NewEntityFieldDeclaration = Record & { label: string; type?: string; options?: string[]; }; /** * SCR-12. May this person add a column to this standard table, and if not, the * sentence saying why — so the control is ABSENT with a reason rather than dead * when pressed. Filling in the fields that already exist is not admin work. */ /** * LANE 7 W2. One standard table's Fields across every organization a list spans, each with its * organization and — for a Choice — its options keyed by the option's stable key. An * organization the reader may not reach is NAMED under `unavailable` with the store's sentence. */ interface EntityFieldsAcross { fields: Array | null; }>; unavailable: Array<{ organization_id: Uuid; reason: string; }>; } interface EntityFieldRights { token: string; label: string; may_declare: boolean; reason: string | null; may_fill_in: boolean; } /** One stage of a board: the KEY the store keeps, and the LABEL a person reads. */ interface PipelineStage { /** The option's stable key — what a cell holds, and what a move is written with. */ key: string; /** What the column heading says. Renaming a stage rewrites no record. */ label: string; /** A retired choice can still be READ on a record that holds it; it cannot be picked anew. */ retired: boolean; } /** What each Rule of a pipeline is about. Closed, and the store is the authority. */ declare const PIPELINE_RULE_KINDS: readonly ["moves", "requires", "who", "limit", "entry"]; /** * A GATE is the general form under `requires`, `who` and `limit`, and the store spells it * `gate:1`, `gate:2` … so several gates can sit on one stage without one overwriting the * next. It is matched as a prefix rather than listed, because the number is open. */ type PipelineRuleKind = (typeof PIPELINE_RULE_KINDS)[number] | `gate:${number}`; /** * WHAT A GATE DOES WHEN IT SAYS NO. The closed list `custom.stage_rule_on_fail_kinds()` * answers — a screen offering a third answer would be offering something no Rule can carry. */ declare const STAGE_RULE_ON_FAIL: readonly ["refuse", "require_approval"]; type StageRuleOnFail = (typeof STAGE_RULE_ON_FAIL)[number]; /** * THE ORGANIZATION'S ONE KNOB (`custom/stage_rule_enforcement`). `refuse` is the default and * what a gate is for; `warn` lets the move through and hands the same sentence back. It never * softens `require_approval` — "ask somebody" and "carry on with a note" are different answers. */ declare const STAGE_RULE_ENFORCEMENT: readonly ["refuse", "warn"]; type StageRuleEnforcement = (typeof STAGE_RULE_ENFORCEMENT)[number]; /** * One transition Rule, as `custom.pipeline_read` hands it back — WITH the sentence it says * when it refuses, because a screen that knows a rule exists but not what it refuses in * cannot warn anybody before they drag. */ interface PipelineRule { id: Uuid; name: string; /** The exact words a person is shown when this Rule stops a move. */ message: string; kind: PipelineRuleKind | null; /** The stage this Rule is about; `null` on the one Rule about the moves themselves. */ stage: string | null; uses: string[]; /** What happens when a card ARRIVES in this stage — assign, notify, start a checklist. */ on_entry: Record | null; /** * What this Rule does when it STOPS a move. `refuse` is the default and what every * pipeline Rule did before gates existed; `require_approval` files the change in the * approvals queue with this Rule's sentence as the reason and leaves the record alone. */ on_fail?: StageRuleOnFail; /** * THE GATE AS IT WAS WRITTEN, which is the only thing that makes it EDITABLE. * * A gate is saved as one Rule whose expression is an `or` of four arms — not this * stage, OR not a move, OR the gate is not about this card, OR the demand holds. That * compiled form used to be the only copy, so a settings screen could read a gate's name * and its sentence and nothing else: it would open every existing rule with a blank * condition and overwrite it the first time somebody pressed Save. * * Absent on the four sugar rules (`moves`, `requires`, `who`, `limit`), which nobody * writes in a condition builder, and on any gate declared before the store started * keeping them. Absent means "this one was not written here" — never "it has no * condition", which is a different and dangerous thing to draw. */ when?: Record | null; /** @see {@link PipelineRule.when} — the condition that has to hold, as it was written. */ demands?: Record | null; version: number; } /** * WHAT A GATE WOULD DO TO THIS BOARD TODAY, from `custom.pipeline_gate_preview`. * * The one question somebody writing a rule that will refuse their colleagues' work * actually has, answered while they are still typing it — the gate is judged as an * argument, nothing is saved, and nothing is written. The store compiles it with the * SAME compiler `pipeline_declare` uses, so this count and the refusal at the door are * the same rule. */ interface PipelineGatePreview { is_pipeline: boolean; /** Present, and it says what would make a board exist, when `is_pipeline` is false. */ why?: string; /** The stage key the gate was tried against. */ stage?: string; /** Live cards of this board this person may see that are not already in that stage. */ considered: number; /** How many of them this gate would turn away. */ refused: number; /** * Up to three of them, BY NAME. A count with no names is a number nobody can check, and * checking it is the whole reason to show it before saving. */ examples: { record_id: Uuid; title: string; }[]; /** True when the board is larger than the preview will walk, so the count is a floor. */ truncated?: boolean; on_fail?: StageRuleOnFail; /** The word this board uses for one of its cards — "quote", "deal" — for the sentence. */ noun?: string; } /** The whole board's definition, from `custom.pipeline_read`. */ interface Pipeline { is_pipeline: boolean; /** Present, and it says what would make a board exist, when `is_pipeline` is false. */ why?: string; table_id?: Uuid; /** The Field KEY that holds the stage — the twin of a Table's `type_field`. */ stage_field?: string; field_id?: Uuid; /** What that column is called, e.g. "Stage". */ stage_label?: string; /** In the order they were declared, which is the order the board draws them. */ stages?: PipelineStage[]; rules?: PipelineRule[]; } /** One column of the board, with the store's own numbers over what this person may see. */ interface PipelineColumn { stage_key: string; stage_label: string; /** 1-based, the declared order. */ stage_position: number; cards: number; /** The total of the chosen measure, or null when no measure was asked for. */ total: number | null; /** How many cards this column takes at once, or null when it takes any number. */ wip_limit: number | null; /** True when the column already holds MORE than its limit — a state a board must show. */ over_limit: boolean; } /** One column this stage demands before a card may land in it. */ interface PipelineMissingField { field_id: Uuid; key: string; /** What the column is called, so a screen can ASK for it rather than only refuse. */ label: string; } /** * Whether a card may go to a column, answered BEFORE the finger lets go — by the same * Rules, through the same evaluator, on the document the move WOULD produce. * * A board grays a column, and snaps a card back, from this. It never decides for itself. */ interface PipelineVerdict { allowed: boolean; is_pipeline: boolean; /** * The THREE answers a board has to draw differently, and the reason `allowed` alone is * not enough: a card that is going for approval has not been refused, and a card that * moved under `warn` has not been waved through silently. */ outcome?: "allowed" | "refused" | "needs_approval" | "warned"; /** What this organization's one knob says right now. */ enforcement?: StageRuleEnforcement; /** Set on `needs_approval`: what pressing on will actually do, in plain words. */ what_happens?: string; /** Set on `warned`: the gates this organization asked to be told about, not stopped by. */ warnings?: StageRuleWarning[]; /** Approvals already filed against this card's own stage column that nobody has decided. */ pending?: StagePending[]; /** The stage key the move was asked about. */ to?: string; /** The store's own sentence — shown verbatim, never paraphrased by a screen. */ why: string; rule?: string; rule_id?: Uuid; rule_version?: number; kind?: PipelineRuleKind; /** What this Rule does when it stops a move. */ on_fail?: StageRuleOnFail; /** * When a stage demands columns that are empty, exactly which ones — so the screen can * prompt for them on entry instead of sending the person away to find them. */ missing?: PipelineMissingField[]; } /** What `custom.pipeline_declare` says it made or changed. */ interface PipelineDeclared { table_id: Uuid; stage_field: string; field_id: Uuid; stages: string[]; rules: Record; /** Plain sentences naming each thing this declaration wrote. */ said: string[]; } /** The declaration a person (or an agent) writes. Everything is optional but the stages. */ interface PipelineSpec { stage_field: { key: string; label?: string; options?: string[]; sort?: number; }; transitions?: { from: string; to: string; }[]; /** Stage name → the column keys that must be filled in to land there. */ requires?: Record; /** Stage name → the rung somebody needs on the record to move a card INTO it. */ who?: Record; /** Stage name → how many cards may sit there at once. The mover is not counted. */ limits?: Record; /** Stage name → what happens when a card arrives: assign, notify, start a checklist. */ on_entry?: Record>; /** * Stage name → the gates a card has to get past to land there. The general form under * `requires`, `who` and `limits`, and the only one that can say `require_approval`. */ gates?: Record; moves_message?: string; requires_messages?: Record; } /** * A gate the organization's own knob overruled. NOTHING FAILS SILENTLY: the move happened * and the sentence comes back WITH it, together with what to do about it. */ interface StageRuleWarning { rule_id: Uuid; rule: string; /** The gate's own sentence, shown verbatim. */ why: string; what_to_do: string; } /** An approval filed against this card's stage column that nobody has decided yet. */ interface StagePending { approval_id: Uuid; /** The stage key the card is waiting to go to. */ to: string; why: string | null; requested_at: string; state: string; } /** * The same wait, asked for a WHOLE BOARD in one question (`custom.pipeline_pending`), so a * board can badge every card that is waiting without a round trip per card — and therefore * actually does it, instead of drawing a filed card as if nobody had ever dragged it. */ interface StagePendingOnBoard extends StagePending { record_id: Uuid; } /** What `custom.pipeline_move` answers — an OBJECT on every path, never a bare number. */ interface PipelineMoveResult { applied: boolean; moved: boolean; outcome: "moved" | "moved_with_warnings" | "needs_approval"; to: string; version: number; /** Set on `moved_with_warnings`. */ warnings?: StageRuleWarning[]; /** Set on `needs_approval`: the gate's sentence, the wait that was filed, and who has it. */ why?: string; rule?: string; rule_id?: Uuid; approval_id?: Uuid; approvers?: { user_id: Uuid; name: string | null; why: string; }[]; what_happens?: string; } /** One card of a batch move (`custom.pipeline_move_many`). The version is REQUIRED: it is the * version the board loaded, and a card that changed since answers `changed_since_loaded`. */ interface PipelineMoveManyItem { record_id: Uuid; stage: string; expected_version: number; /** Columns the stage asks for, filled in on the way in. */ also?: Record; } /** What one card of a batch move came to. The sentence is always the store's own. */ type PipelineMoveVerdict = "moved" | "needs_approval" | "needs_fields" | "changed_since_loaded" | "no_right" | "refused"; interface PipelineMoveManyResult { index: number; record_id: Uuid; stage: string; verdict: PipelineMoveVerdict; sentence: string; /** Set on `needs_fields`: the empty columns the stage asks for. */ needs?: PipelineMissingField[]; /** Set on `moved` / `needs_approval`: the card's version now. */ version?: number; /** Set on `changed_since_loaded`: the version that won. */ current_version?: number; expected_version?: number; approval_id?: Uuid; approvers?: { user_id: Uuid; name: string | null; why: string; }[]; warnings?: StageRuleWarning[]; rule?: string; code?: string; } /** What `custom.pipeline_move_many` answers: the counts, then one verdict per card in order. */ interface PipelineMoveMany { asked: number; moved: number; needs_approval: number; not_moved: number; results: PipelineMoveManyResult[]; } /** * ONE GATE ON A STAGE, as the condition builder on the stage settings screen writes it. * `requires`, `who` and `limits` are sugar over this shape; this is the general form, and * it speaks the SAME condition language as filters and formulas — never a second one. */ interface PipelineGate { /** What the gate is called in the settings list. */ name?: string; /** The sentence a person reads when it stops them. Required: a refusal with no words is a dead end. */ message: string; /** WHEN this gate is about this record at all. Absent means always. */ when?: Record; /** What has to hold. One condition node of the one Rule language. */ demands: Record; /** Default `refuse`. */ on_fail?: StageRuleOnFail; } /** What the store thinks one column of a file is. */ interface ImportColumnPlan { /** The heading exactly as the file spells it. */ header: string; /** Set when this table already has the column; then `type` is that Field's. */ field_id: Uuid | null; /** The key this column would become, or the key it matched. */ field_key: string; label: string; /** * A parity type, or one of the three plain words (`text`, `long_text`, * `number`). It is exactly what `custom.field_declare` takes, so a plan can be * handed on with nothing in between to reinterpret it. */ type: ParityFieldType | "text" | "long_text" | "number" | "relation"; matched: boolean; /** One sentence, in words a person reads, saying why the store said that. */ why: string; /** Present on a money column. */ unit?: string; /** Present when the store read the column as a dropdown: its words. */ options?: string[]; /** Present when the column points at another Table. */ relation_target?: Uuid; /** A slashed date whose day/month order cannot be read off the file. */ ambiguous?: boolean; /** Another column of the same file would make the same key. */ collides_with?: string; samples?: unknown[]; } interface ImportPlan { table_id: Uuid; columns: ImportColumnPlan[]; matched: number; unmatched: number; fields: Array<{ field_id: Uuid; key: string; label: string; type: string | null; }>; } /** What a run does when it meets something. Both defaults cannot lose anything. */ interface ImportPolicy { /** `skip` leaves the record that is already there; `update` brings it up to date. */ on_duplicate?: "skip" | "update"; /** `propose` sends a new column to the ONE approvals inbox. `create` is an admin's. */ unmapped?: "propose" | "create" | "ignore"; /** How a slashed date is read. Chosen once for the run, never guessed per row. */ date_order?: "mdy" | "dmy"; } interface ImportOpened { import_id: Uuid; state: "open" | "written" | "finished"; /** * TRUE means these exact bytes already landed on this Table and NOTHING was * written again. The numbers are the FIRST run's. */ already: boolean; rows_seen: number; rows_written: number; rows_duplicate: number; source_name: string | null; opened_at?: Timestamp; /** * IMPORT-2. How many rows to send in the FIRST call. The store owns this * number rather than a screen, because nothing has been measured yet when a * run opens and a batch size written into a wizard is wrong on a busy * afternoon and stale the day the write path gets cheaper. Every call after * the first uses `ImportBatchResult.rows_per_call`, which is measured. */ rows_per_call?: number; message: string; } /** One row of the file, and what became of it. */ type ImportOutcome = { row: number; outcome: "landed"; record_id: Uuid; } | { row: number; outcome: "duplicate"; record_id: Uuid; updated: boolean; reason: string; } | { row: number; outcome: "refused"; reason: string; source: Record; }; interface ImportProposal { column: string; field_key: string; label: string; type: string; why: string; samples: unknown[]; state: "proposed" | "waiting" | "accepted" | "rejected" | "ignored" | "refused"; /** Set once the column is waiting in the approvals inbox. */ approval_id?: Uuid; /** Set once an admin added it outright. */ field_id?: Uuid; reason?: string; options?: string[]; unit?: string; relation_target?: Uuid; } interface ImportBatchResult { import_id: Uuid; rows_seen: number; rows_written: number; rows_duplicate: number; rows_refused: number; outcomes: ImportOutcome[]; proposals: ImportProposal[]; /** * IMPORT-2. The columns this call had to declare because the run never took * the `importDeclareColumns` step. Normally empty: declaring is its own step. */ columns_added?: ImportProposal[]; /** * IMPORT-2. FALSE means the batch could not be written as one statement — one * row of it was refused by a guard that refuses the whole statement — so every * row was written on its own instead and the refusals below name the rows that * could not be. The answers are identical either way; this says what it cost. */ one_statement?: boolean; /** How long THIS call's writing phase took, and what that was a row. */ write_ms?: number; ms_per_row?: number; /** * IMPORT-2. How many rows the NEXT call should carry, measured from this one: * the store aims at two seconds of writing, which leaves a database that gets * 50% busier between two calls still inside the three seconds a person should * wait. Send this many next. Ignoring it costs correctness nothing. */ rows_per_call?: number; } /** IMPORT-2. What `importDeclareColumns` made, as its own step of the wizard. */ interface ImportColumnsDeclared { table_id: Uuid; columns_added: ImportProposal[]; /** Header -> Field key, including the headers that already named a column. */ mapping: Record; message: string; } interface ImportFinished { import_id: Uuid; state: "finished"; rows_seen: number; rows_written: number; rows_duplicate: number; rows_refused: number; columns_offered: number; columns_added: number; columns_ignored: number; proposals: ImportProposal[]; message: string; } /** * The rows a person clicks through to. `capped` and `note` are the store saying * out loud which part of the total it can still show — a number with nothing * behind it is the thing this shape exists to prevent. */ interface ImportReport { import_id: Uuid; table_id: Uuid; state: "refused" | "duplicate"; rows: Array>; kept: number; total: number; capped: boolean; note: string | null; } interface ImportRun { import_id: Uuid; table_id: Uuid; format: "csv" | "xlsx"; source_name: string | null; file_hash: string | null; dedupe_key: string | null; policy: ImportPolicy; state: string; rows_seen: number; rows_written: number; rows_duplicate: number; rows_refused: number; columns_offered: number; opened_at: Timestamp; finished_at: Timestamp | null; created_by: Uuid | null; } /** DOOR-19. An address a Table owns, and what has arrived at it. */ interface InboundAddress { inbound_id: Uuid; table_id: Uuid | null; channel: string; address: string; source: string | null; enabled: boolean; last_received_at: Timestamp | null; created_at: Timestamp; received: number; } interface InboundDeclared { inbound_id: Uuid; table_id: Uuid; address: string; channel: string; enabled: boolean; /** * THE ONE THING THAT COULD FAIL SILENTLY, SAID OUT LOUD. The address is real * and stored the moment this comes back; whether mail REACHES it is a fact * about DNS that no door can make true. Show this sentence wherever the * address is shown. */ mail_route: string; message: string; } /** One portal as `custom.portals` reports it to its organization. */ interface PortalSummary { portal_id: Uuid; title: string; slug: string; /** The Table whose records ARE the clients. A principal is one of its rows. */ client_table_id: Uuid; /** That Table's name, so a list never has to read a second door to draw a row. */ client_table: string; is_active: boolean; /** How many Tables this portal exposes. */ tables: number; /** Everybody invited and not revoked — NOT everybody who can see anything. */ invited: number; /** Of those, how many have followed the link and hold an identity. */ signed_in: number; /** `magic_link` today, and the door refuses every other word by name. */ sign_in_method: string; opened_at: Timestamp | null; } /** * ONE (portal, Table) PAIR, as `custom.portal_tables` reports it. * * 🚨 WHY THIS EXISTS (TAILS-3, 2026-09-21). The Portals rail sits on a Table's * page beside Forms, Bookings and Dashboards — all table-scoped — while * `custom.portals` takes no table at all. On Rincon Plumbing Co's **Parts** * table the rail read "Portals 10" and listed ten cards titled "Your jobs and * invoices", which belong to **Jobs**; the empty state never rendered, so the * "Build it / Ask an agent" offer was unreachable on EVERY table of any * organization that already had one portal. * * This is the whole of what a table-scoped rail needs: is the table I am * standing on in any portal, and if not, which portals could take it. */ interface PortalTableRef { portal_id: Uuid; table_id: Uuid; /** That Table's name, so a rail never reads a second door to draw a row. */ name: string; } /** One Table a portal exposes, as `custom.portal_card` reports it. */ interface PortalTableExposure { table_id: Uuid; /** The Table's own name. */ name: string; /** * The key of the relation Field that says WHOSE a record is. This is the * whole of "only theirs": the association that Field writes is what carries * the record to the client, so a portal cannot exist without one. */ names_via: string; /** The field keys a client sees. Empty is impossible — the door fills it. */ visible_fields: string[]; /** The subset a client may change. Always a subset of `visible_fields`. */ editable_fields: string[]; comments: boolean; /** * The most a client is ever conveyed on this Table — `viewer`, `commenter` * or `editor` — worked out by the door from the two lists above, never by a * screen. */ conveys: string; } /** One person a portal was opened to, as `custom.portal_card` reports them. */ interface PortalPrincipal { principal_id: Uuid; email: string; /** The record of the client Table this person IS, for the portal's purposes. */ client_record_id: Uuid; /** That record's title — what an owner calls this client. */ client: string; /** They followed the link and hold an identity. Until then this is false. */ signed_in: boolean; /** Not revoked. An invitation that was never followed is still active. */ is_active: boolean; invited_at: Timestamp; revoked_at: Timestamp | null; /** * THE LINK THE OFFICE COPIES FOR THIS PERSON — `/invitations/portal/accept/`, the * path `custom.portal_invite` handed back when they were asked, read off the same * `iam.invitations` row and never re-templated. * * 🚨 `null` IS AN ANSWER, NOT AN ABSENCE, and a screen must say which. It is null when they * have already followed the link, when the office took it back (a revoke withdraws the * invitation in the same statement), when it has expired, or when they were let in through * the server's own magic-link route, which binds without an invitation at all. In every one * of those cases there is genuinely no link to copy — so draw a sentence, never a dead * control. (TAILS-5, 2026-09-21; the gap PORTAL-BIND left named.) */ accept_path: string | null; } /** Everything about ONE portal: what it shows, and who it was opened to. */ interface PortalCard { portal_id: Uuid; title: string; slug: string; is_active: boolean; sign_in_method: string; client_table_id: Uuid; tables: PortalTableExposure[]; principals: PortalPrincipal[]; /** * Whether this organization's outside lane is open * (`custom/external_principal_enabled`). Declaring a portal opens it; while * it is false every door refuses an outsider BY NAME, and a screen that drew * the people as reachable anyway would be lying about the one thing that * decides whether they can sign in at all. */ external_lane_open: boolean; /** S6 — the look and forms exactly as the owner stored them (`{}` when none). */ config: PortalConfig; /** S6 — the look as a client sees it: the portal's own values over the organization's brand. */ style: PortalStyle; /** S6 — every form on the portal, in its order, with its title, Table and state. */ forms: PortalFormRef[]; /** S6 — the accent names a portal may carry (the design system's `STYLE_COLORS`). */ accents: PortalAccent[]; /** * S6 — the pictures that could be this portal's logo: the organization's own PUBLIC pictures in * Files, newest first. A private file is never offered, because the store would refuse it. */ logo_candidates: PublicPicture[]; } /** The design system's color names (`STYLE_COLORS`), which is all an accent may be. */ declare const PORTAL_ACCENTS: readonly ["slate", "green", "amber", "red", "blue", "violet", "teal"]; type PortalAccent = (typeof PORTAL_ACCENTS)[number]; /** One link in a portal's footer. The store takes only `https://`, `mailto:` and `tel:`. */ interface PortalFooterLink { label: string; url: string; } /** A portal's look as the owner sets it. Every key is optional; absent means "the organization's". */ interface PortalStyleInput { display_name?: string | null; welcome?: string | null; logo_file_id?: Uuid | null; accent?: PortalAccent | null; footer_links?: PortalFooterLink[] | null; } /** One form on a portal, as the owner sets it. `order` is the position; `label` overrides the title. */ interface PortalFormInput { form_id: Uuid; label?: string | null; order?: number | null; } /** * What `portalDeclare({ config })` sends. A key given REPLACES that key; a key not given is KEPT; * `style: null` puts the organization's brand back and `forms: []` takes every form off. */ interface PortalConfigInput { style?: PortalStyleInput | null; forms?: PortalFormInput[] | null; } /** The look and forms as the store holds them (normalized by the store). */ interface PortalConfig { style?: PortalStyleInput; forms?: Array<{ form_id: Uuid; label?: string; order: number; }>; } /** The look a client is shown — resolved by the store, never computed by a screen. */ interface PortalStyle { display_name: string; welcome: string | null; logo_file_id: Uuid | null; /** The logo's PUBLIC address, or null. Readable by anybody the portal is shown to. */ logo_url: string | null; /** Null means the app's own color. */ accent: PortalAccent | null; footer_links: PortalFooterLink[]; /** MAKE-HOME W5 — a cover picture's PUBLIC address (a form's look carries one), or null. */ cover_file_id?: Uuid | null; cover_url?: string | null; /** Which of the values above came from the organization rather than the portal. */ from_organization: Array<"display_name" | "logo">; } /** * One of the organization's PUBLIC pictures (`custom._public_pictures`): the only pictures a * portal's logo or a form's logo and cover may be, because a stranger sees them before signing in. */ interface PublicPicture { file_id: Uuid; name: string; url: string | null; } /** * What a form's look editor needs (`custom.form_look_options`, MAKE-HOME W5b): the pictures the * store accepts, the colors, and the look a stranger would see for the draft presentation. */ interface FormLookOptions { pictures: PublicPicture[]; accents: PortalAccent[]; look: PortalStyle; } /** One form on a portal, with what the owner needs to see about it. */ interface PortalFormRef { form_id: Uuid; /** What the portal calls it (the owner's label, else the form's title). */ label: string; order: number; title: string; table_id: Uuid | null; table: string; /** `gone` is a form that was removed; the outsider's list only ever carries `open`. */ state: "open" | "closed" | "full" | "gone"; } /** What `custom.portal_form` answers: one form on the caller's own portal. */ interface PortalFormSpec { form_id: Uuid; portal_id: Uuid; table_id: Uuid; title: string; label: string; presentation: Record; /** The form's exposed Fields as the store holds them — never the one naming the client. */ fields: Array & { id: Uuid; key: string; }>; required: string[]; state: "open" | "closed" | "full"; message: string | null; } /** What `custom.portal_form_submit` answers. `held` waits for the office; `message` says so. */ interface PortalFormAnswer { submission_id: Uuid | null; record_id: Uuid | null; state: "accepted" | "held" | "rejected" | "closed" | "full" | "too_many"; message: string | null; } /** What `custom.portal_invite` answers. `say` is the door's own sentence. */ interface PortalInvitation { invited: boolean; principal_id: Uuid; portal_id: Uuid; slug: string; email: string; client_record_id: Uuid; client: string; /** They already hold an identity. From a screen this is false: the door is passed no user. */ bound: boolean; /** * The door's own words, and the ONLY correct summary of what just happened — * it says outright that an invitation confers nothing until the link is * followed. Show it; never paraphrase it. */ say: string; } /** What `custom.portal_revoke` answers. `say` names the consequence, past tense. */ interface PortalRevocation { revoked: boolean; principal_id: Uuid; email: string; /** The door's own sentence: they can no longer sign in, and their records stop reaching them. */ say: string; } /** One row of "what this client sees", from `custom.portal_preview`. */ interface PortalPreviewRow { record_id: Uuid; title: string; } /** * The platform archive filter's own three values, and no fourth. `custom.list_portals` * refuses anything else with `22023` rather than quietly treating it as one of them. */ declare const PORTAL_ARCHIVE_FILTERS: readonly ["active", "archived", "all"]; type PortalArchiveFilter = (typeof PORTAL_ARCHIVE_FILTERS)[number]; /** What `custom.portal_archive` and `custom.portal_restore` answer with. */ interface PortalArchiveAnswer { /** Where the portal stands NOW — true after an archive, false after a restore. */ archived: boolean; /** False when it was already in that state; the sentence says so rather than lying about an act. */ changed: boolean; portal_id: Uuid; archived_at?: Timestamp | null; /** The store's own sentence, in plain English. Show it verbatim; never a constraint name. */ sentence: string; } /** Where a question is asked. */ type DrillSource = { kind: "table"; id: Uuid; } | { kind: "entity"; token: string; }; /** Who a question is asked for (the access lanes, applied to counting). */ type DrillLane = "mine" | "organization" | "platform"; type DrillGrain = "hour" | "day" | "week" | "month" | "quarter" | "year"; type DrillMeasureOp = "count" | "count_distinct" | "sum" | "avg" | "min" | "max" | "median" | "percentile" | "rate" | "filled" | "empty" | "ratio" | "sum_of" | "span"; /** * The operations a question may ask ad hoc (`{op, of}`); a ratio, a percentile and a run rate are * declared, never ad hoc (each needs more than a column: its parts, its p, its per). */ type DrillAdHocOp = Exclude; /** * A color a choice keeps on every chart and share bar (lane DRILL-PARITY-LAST): one of the package's * semantic chart tokens (light and dark), never a raw color — the old usage page's origin colors. */ type DrillChartToken = "--matrx-chart-1" | "--matrx-chart-2" | "--matrx-chart-3" | "--matrx-chart-4" | "--matrx-chart-5" | "--matrx-chart-6" | "--matrx-chart-7" | "--matrx-chart-8" | "--matrx-chart-9" | "--matrx-chart-10" | "--matrx-chart-other"; /** A value a Measure's own `where` narrows by: a value, null ("not set") or a list of them. */ type DrillMeasureWhereValue = string | number | boolean | null | Array; /** An axis a question can cut by. */ interface DrillDimension { key: string; label: string; /** The column (fact) or `join.column` it reads. */ from: string; kind: "choice" | "relation" | "boolean" | "text" | "time"; cardinality?: "low" | "medium" | "high"; /** A code's words; `color` keeps the value one chart token on every chart and share bar. */ choices?: Array<{ value: string; label: string; color?: DrillChartToken; }>; /** A relation's target: a registry token (standard) or a Table id (custom). */ relation?: { token?: string; table_id?: Uuid; person?: boolean; }; /** A time's grains, coarsest first. */ grains?: DrillGrain[]; /** The words the empty group reads ("No embedding (nothing new to embed)"); a screen's own default otherwise. */ empty_label?: string; /** * What the Dimension groups by, at most 140 characters (lane DRILL-FLIP-FIXES, the Spend cut tables' * explanations): a screen shows it as the column header's tooltip, never as text on the page. */ description?: string; /** * WHAT A LEVEL OF IT LOOKS LIKE (lane DRILL-LEVELS) — the same shape as the design-system's * `MatrxDrillLevel`; judged by platform.drill_definition_problems and handed to a screen by * drill_describe. Absent = the screen infers it. */ level?: DrillLevel; } /** * A DIMENSION'S LEVEL PROFILE (lane DRILL-LEVELS): what a drilled value of it offers and says. * breakouts — Dimension references (`at:day` for a time) offered as one-click "By …" chips, in order; * `:` names a SIBLING definition's Dimension (`ai_usage_executions:conversation`): * the screen switches the source there carrying the trail (lane DRILL-PRIMITIVE-2) * show — the Measures shown while grouping by it * attributes — Dimension keys each value holds ONE of (a conversation's agent, a model's provider): * columns while grouping by it, and the drilled value's header * records — the columns "See these records" shows at this level (a subset of `records.columns`) */ interface DrillLevel { breakouts?: string[]; show?: string[]; attributes?: string[]; records?: string[]; } /** A number a question can show. */ interface DrillMeasure { key: string; label: string; op: DrillMeasureOp; /** * The column it reads (absent for `count`, `ratio` and `sum_of`). A `max` / `min` of a time column * is a MOMENT ("Last active"): it declares `unit: "time"`, is never additive, and no threshold reads it. */ of?: string; /** * `ratio` only: (the sum of these Measures) / (the sum of `den`), recomputed for every group, Other * and the total from their parts (never averaged). Each part is a `sum` or `count` Measure of the * same definition (optionally narrowed by its own `where`), or another ratio — e.g. cost per request * = `{op: "ratio", num: ["cost"], den: "requests"}`; cache-hit rate = `{num: ["hits"], den: ["hits", * "calls"]}`; the enrichment multiplier = `{num: ["avg_enriched"], den: "avg_plain"}` (two ratios). */ num?: string[]; /** * `sum_of` only: these Measures added up, recomputed for every group, Other and the total from * their own rows — total tokens = `{op: "sum_of", parts: ["tokens_in", "tokens_cached", "tokens_out"]}`. * Each part is a `sum` or `count` that adds up, or another `sum_of`; at least two. */ parts?: string[]; /** `ratio` only: the Measure (or the list of Measures, added up) it is divided by; null where that is zero. */ den?: string | string[]; /** * `ratio` or `rate`: a multiplier, bound by the door — `1000` makes cost per character cost per * 1,000 characters; `10` makes a monthly projection "at ten times today's load". */ scale?: number; /** `percentile` only: the fraction, above 0 and below 1 (`0.9` = p90). Recomputed for Other and the total from their own rows. */ p?: number; /** * `rate` only: the run rate's period. A run rate is the window's sum of `of` ÷ the window's length * × one `per` ("day", or "month" = 30 days — the old KG projection's rule). A question showing one * must have a window with a start; a window reaching past now counts only the time that has passed. */ per?: "day" | "month"; /** * This Measure only, narrowed to the rows whose Dimensions hold these values (Dimension key → a * value, null or a list; never a time): "cost of successful enriched runs". Bound by the door. */ where?: Record; /** * How a screen reads the number: usd, count, tokens, ms, share, times, …; `time` = a moment (ISO); * `duration` = a length of time in milliseconds — what a `span` (`of` a time column: the latest * minus the earliest moment, never additive, recomputed for Other and the total) declares. */ unit?: string; /** Counted once per this column's value (a measure on the "one" side of a join). */ at_grain?: string; /** May it be summed across groups? */ additive?: boolean; /** * What the number counts, at most 140 characters (lane DRILL-FLIP-FIXES): a screen shows it as the * column header's tooltip — "Requests a person or an API caller made; automated runs count as Calls". */ description?: string; /** * (lane DRILL-ENGINE, 2026-10-18) How a summary-backed distinct count that is NOT exact over groups or hours is * answered exactly, definer definitions only. `per_hour`: registry token of a per-hour total (the time column, * `everyone`, this Measure's `of` column, each lane rule's column) read for an ungrouped question — exact to the * hour. `raw_of`: the records column counted distinct, the whole question read from the records relation when the * window is at most `raw_within_hours` long. Any other question showing it says "not exact". */ exact?: { per_hour?: string; raw_of?: string; raw_within_hours?: number; }; } /** A named drill path: the levels a click walks, e.g. ["created_at:year", "created_at:month"]. */ interface DrillPath { key: string; label: string; levels: string[]; } /** * A threshold on the groups of an answer (`having`). Judged on the current window, and applied * BEFORE the cut into Other: a group that misses any threshold is added into Other with the groups * past the limit, so the answer still adds up to its total; `distinct_groups` counts the groups that * met every threshold and the total row says how many did ("3 of 41 groups meet the rule …"). * Exactly one of `value` / `share_of_total` / `times_median`. */ interface DrillHaving { /** A Measure the question shows (a screen shows the number it filters on). */ measure: string; /** At least / more than; at most / less than (lane DRILL-FLIP-FIXES: "groups where cost ≤ 5"). */ op: ">=" | ">" | "<=" | "<"; /** The group's value against a number. */ value?: number; /** The group's value against this percent (0–100] of the window's total; additive Measures only. */ share_of_total?: number; /** The group's value against this multiple of the median group's value. */ times_median?: number; /** With `times_median`: the median of the groups above zero only. */ median_nonzero?: boolean; /** * The address of the drill setting that holds the line (organizations and people decide): * `drill.finding...` inside a finding. When present the setting's value * is the threshold and the number written beside it is its default. Resolved by the door through * platform.drill_knob; clients read the same value from platform.knob_snapshot. */ knob?: string; } /** A question — what a Saved view stores and the explorer's URL encodes. */ interface DrillQuestion { /** Dimension keys, outermost first; a time as `key:grain` ("created_at:week"). At most four. */ by?: string[]; /** One Dimension drawn as the columns of a pivot. */ across?: string; /** Measure keys, or an ad hoc `{op, of}` over a column the definition reads. */ show?: Array; /** * The store's own filter shape: a scalar is an equality, `null` is "not set", a list is any of * them, `{from, to}` is a half-open range; `key:grain` = one period by its label. */ where?: Record; /** * A custom Table only: the view's Rule expression, which must hold beside `where` * (custom.record_drill ANDs the two). A standard source refuses it. */ rule?: RuleExpression; window?: { key?: string; preset?: string; from?: string; to?: string; }; compare?: "previous_period" | "same_period_last_year" | { against: "previous_period" | "same_period_last_year" | "range"; period?: DrillGrain; from?: string; to?: string; baseline?: { from: string; to: string; }; to_date?: boolean; }; sort?: { key: string; direction?: "asc" | "desc"; }; /** Groups per level before Other; the knob's value when absent, never above the page ceiling. */ limit?: number; lane?: DrillLane; /** The drill path a screen's default click follows. */ path?: string; /** Thresholds on the groups, applied before the cut into Other (see DrillHaving). */ having?: DrillHaving[]; /** Records only: the page's start. */ offset?: number; /** Records only: which columns a record shows (a definer definition's records: of its declared columns). */ columns?: string[]; } /** A definition's built-in Saved view: read-only; "Save a copy" makes a person's own. */ interface DrillView { key: string; label: string; question: DrillQuestion; /** * AT A GLANCE (lane DRILL-FLIP-FIXES, the Spend Explorer's costliest requests): Dimensions each group * of the view's outer grouping has ONE value of — a request's person, agent, model — shown as text * columns beside its numbers, no click required. A screen reads them by asking the same door with * the group and these Dimensions (the group's values listed), so they obey the same lane and filters. * A group holding several values of one says so. */ attributes?: string[]; /** * How the view's chart draws (lane DRILL-FLIP-FIXES, the CX tab's input vs output tokens): `stack` * names two or more Measures that add up, drawn as one stacked bar per period (no split). */ chart?: { stack?: string[]; }; } /** * A finding ("dig here"): a question whose `having` thresholds are the rule. Its lines are settings * at `drill.finding...`, seeded from `knobs` by the drill sync; a * threshold that reads one writes the setting's default beside it. A finding with no group says so. */ interface DrillFinding { key: string; label: string; question: DrillQuestion; knobs: Record; } /** * The records behind a DEFINER definition's numbers: a registry relation carrying the fact's column * names (e.g. `bucket`), read by the door definer with the SAME filter compiler and the SAME lane * rule as the number, for a window (required), and never past the number's `as_of` (rows created * after it are not listed). The number and its records are the SAME counted rows, equal as of the * count (`as_of`); a value that changes afterwards reaches both at the next count. * * An INVOKER definition may declare records too (lane DRILL-GAPS): they are its own rows (`fact` is * the definition's own fact), read as the person through the table's row security, among the columns * the definition already reads — so a screen shows "the recent runs" without a host link. */ interface DrillRecords { /** The registry token of the records relation (e.g. "ai_usage_executions"). */ fact: string; /** The columns a record shows, in order. */ columns: string[]; /** The words a person reads for a column, where its name would mislead ("Request's top model"). */ labels?: Record; /** * What ONE record is, singular ("execution", "log line"): the noun "See these records" and their * count read in. REQUIRED when the records are another relation's rows (`fact` is not the * definition's own fact: an hourly summary's records are its executions, never "hours"); optional * otherwise, where the definition's own grain ("one row per deal") already names it. */ noun?: string; } interface DrillDefinition { key: string; label: string; source: DrillSource; /** One sentence: "one row per agent". */ grain: string; lanes: DrillLane[]; dimensions: DrillDimension[]; measures: DrillMeasure[]; paths: DrillPath[]; detail: { columns: string[]; open?: string; }; default: DrillQuestion; /** Keys that came from inference, so a screen can say so. */ inferred?: string[]; mode?: "invoker" | "definer"; /** Honest notes about what inference could not offer, in words. */ says?: string[]; /** Built-in Saved views, listed first and read-only. */ views?: DrillView[]; /** Findings ("dig here"), answered by the same door. */ findings?: DrillFinding[]; /** The records relation (what "See these records" reads): a definer definition's declared relation, or an invoker definition's own rows. */ records?: DrillRecords; /** * The drill setting (minutes) after which the definition's `as_of` is old enough for a screen to say * "the schedule is behind" — null when the definition is counted live. */ stale_after_knob: string | null; } /** One row of an answer. */ interface DrillAnswerRow { /** `group` a shown group · `other` every group past the cap · `total` the whole answer (or a pivot column's total). */ kind: "group" | "other" | "total"; /** Each asked Dimension -> its value (a period by its label). A pivot cell also carries the `across` key. */ groups: Record | null; measures: Record | null; row_count: number; prior_groups: Record | null; prior_measures: Record | null; prior_row_count: number | null; delta: Record | null; compare: Record | null; /** How many groups there were before the cap (current window). */ distinct_groups: number | null; /** Relation Dimensions' labels, read as the person through the target's own row security. */ labels: Record; /** The answer's honest sentences (the cut, a column past its cap, a name that could not be read). */ says: string | null; /** The moment a summary fact counted through (ISO); null when counted live from the table. */ as_of: string | null; } interface DrillAnswer { rows: DrillAnswerRow[]; /** Every sentence the rows carry, once. */ says: string[]; /** The total row (kind total, no group). */ total: DrillAnswerRow | null; /** The moment the answer counted through (the rows' `as_of`); null when counted live. */ as_of: string | null; } /** The records behind a number ("see these records"): the same filter, read as the person. */ interface DrillRowsPage { total: number; limit: number; offset: number; rows: Array>; next_offset?: number; columns?: string[]; says?: string; /** The moment these records are cut at — the number's `as_of`; null when read live. */ as_of: string | null; /** * A definer definition's records, first page only: every Measure that adds up (sum, count, ratio) * summed over the SAME filter — what these records add up to, which is the number they open. */ measures?: Record; } /** `custom.decoration_colors()` — the palette, in the order an uncolored choice takes. */ type DecorationColor = "slate" | "green" | "amber" | "red" | "blue" | "violet" | "teal" | "orange" | "yellow" | "pink" | "brown" | "gray"; /** `custom.decoration_rule_ops()` — the tests a color rule may ask. */ type DecorationRuleOp = "is" | "is_not" | "contains" | "is_empty" | "not_empty" | "is_true" | "is_false" | "gt" | "gte" | "lt" | "lte"; type DecorationTarget = "row" | "cell"; interface DecorationRule { /** Given by the store when omitted; keep it to edit the rule in place. */ id?: string; /** The Field id the rule reads. */ field: Uuid; op: DecorationRuleOp; /** Compared as text for is / is_not / contains, as a number for gt / gte / lt / lte. */ value?: string | number; color: DecorationColor; target: DecorationTarget; } interface TableDecorations { version: 1; colors: DecorationColor[]; rule_ops: Array<{ op: DecorationRuleOp; takes_value: boolean; label: string; }>; color_by?: { field: Uuid; target: DecorationTarget; }; rules: DecorationRule[]; /** Hand highlights: record id → color. */ rows: Record; /** Hand highlights: Field id → color. */ columns: Record; /** Hand highlights: record id → Field id → color. */ cells: Record>; /** What stopped painting because its column is gone — said, never silently dropped. */ stale: Array<{ part: string; field?: Uuid; rule_id?: string; record_id?: Uuid; says: string; }>; } /** * ONE LEAF of a Table's colors — the older door's own path shape * (`public.udt_set_table_style`), so two people highlighting two rows never overwrite each other. */ type DecorationPath = ["color_by"] | ["rules"] | ["rows", Uuid] | ["columns", Uuid] | ["cells", Uuid, Uuid]; type GridLayoutMode = "auto" | "fit" | "scroll"; /** * Airtable's four heights are short (1 line), medium (2), tall (4), extra_tall (6). `compact` and `normal` * are the older words and still read: both are a one-line row. */ type GridRowHeight = "compact" | "short" | "normal" | "medium" | "tall" | "extra_tall"; /** A saved view's own grid choices, kept at `definition.grid` (custom.view_declare judges them). `definition.layout` is the view's KIND (grid / kanban / calendar / gallery), never these. */ interface ViewLayout { mode?: GridLayoutMode; row_height?: GridRowHeight; freeze_first_column?: boolean; wrap?: boolean; /** Field id → pixels, 60–1200. */ widths?: Record; } interface GridLayout { layout: Required> & { fit_max_columns: number; widths?: Record; }; /** Where each choice came from. */ source: Record; view_id: Uuid | null; /** A stored choice the grid cannot honor, named. */ refused: Array<{ from: "organization" | "view"; choice: string; value: unknown; says: string; }>; } type RowActionStep = { field: Uuid; set: "value"; value: unknown; } | { field: Uuid; set: "clear"; } /** Worked out by the store on each record: the older grid's formula text, or an expression. */ | { field: Uuid; set: "compute"; formula_text?: string; expr?: Record; }; interface RowAction { /** Given by the store when omitted. */ id?: Uuid; name: string; kind?: "update" | "agent"; color?: DecorationColor | null; icon?: string; /** Ask before running. */ confirm?: boolean; steps?: RowActionStep[]; /** `agent` only: what the record chat opens with. */ prompt?: string; /** On read: whether custom.action_run can run it. */ runnable?: boolean; } interface ActionRunResult { action_id: Uuid; action: string; table_id: Uuid; ran: number; records: Array<{ record_id: Uuid; version: number; }>; } type FormulaParse = { ok: true; text: string; /** The store's expression shape — what `config.expr` holds. */ expr: Record; references: Array<{ name: string; field_id: Uuid; }>; result_type: "number" | "text" | "boolean" | "date" | "unknown"; } | { ok: false; text: string; error: string; position: number; }; /** NOTION-PROPS: `formulaPreview` — the parse, and what it works out to on one record. */ type FormulaPreview = FormulaParse & { /** The answer on `record_id`, as the formula column would show it; null when nothing was sampled. */ value?: unknown; record_id?: Uuid | null; /** It parses, and could not be worked out on that record (a division by zero, a bad date …). */ error?: string; }; type RecordEvent = "record.created" | "record.updated" | "record.archived" | "record.restored" | "record.purged"; interface TableWebhookDeclared { webhook_id: Uuid; table_id: Uuid; target_url: string; events: RecordEvent[]; /** Shown this once: X-Matrx-Signature is sha256=HMAC-SHA256(secret, body). */ secret: string; says: string; } interface TableWebhook { webhook_id: Uuid; target_url: string; description: string | null; events: RecordEvent[] | null; is_active: boolean; last_attempt_at: string | null; last_success_at: string | null; consecutive_failures: number; owner_id: Uuid; created_at: string; } /** One page of `custom.io_export`: exactly what the read door serves this person. */ interface IoExportPage { table_id: Uuid; token: string; columns: string[]; rows: Array>; /** A column the store withheld from this reader, with the store's reason. */ withheld: Record; page: { requested: number; returned: number; ceiling: number; next: number | null; }; choices: unknown; } /** * One entry of `custom.table_list_everywhere` — `public.get_user_tables`' own shape plus * `store`, so a picker swaps one call and keeps its code. `store` says which home opens it. */ interface TableListEntry { id: Uuid; table_name: string; description: string | null; version: number | null; user_id: Uuid | null; is_public: boolean | null; visibility: string | null; organization_id: Uuid; created_at: string; updated_at: string | null; last_activity_at: string | null; row_count: number; field_count: number; store: "older" | "records"; } /** * One row of `custom.data_home_tables` — every table the person reaches, with the store's own * answers for the organization's name, who made it, and whether the app keeps it. `system` is a * system organization's table (reached on the admin lane only). */ interface DataHomeTable { table_id: Uuid; table_name: string; organization_id: Uuid; organization_name: string; member: boolean; visibility: string | null; updated_at: string | null; mine: boolean; shared_with_me: boolean; platform_owned: boolean; kind: string | null; team?: boolean; system?: boolean; created_by?: Uuid | null; } /** * One row of `custom.admin_all_tables` — a table in ANY organization, for the admin section only * (the store's walls admit a platform admin to system organizations alone, so `DataHomeTable` cannot * list the rest). Answered only on the admin lane for a platform admin; any other caller is refused. */ interface AdminTable { table_id: Uuid; table_name: string; organization_id: Uuid; organization_name: string | null; updated_at: string | null; platform_owned: boolean; system: boolean; } /** Where a synced table comes from. `external_id` is the source's own id (a spreadsheet id). */ interface TableSyncSource { provider: string; external_id: string; tab_id?: string | null; synced_at?: string | null; } /** What `custom.table_sync` takes: the source, its columns, and every row it has NOW. */ interface TableSyncSpec { source: TableSyncSource; table_name: string; columns: string[]; rows: { ref: string; values: Record; }[]; } interface TableSyncResult { table_id: Uuid; created: boolean; fields_added: number; rows_inserted: number; rows_updated: number; rows_archived: number; source_system: string; } /** * What `custom.table_copy_sync` takes (lane SYNC-AIRTABLE): a table synced from another table's * view — read as the caller, so the copy holds only what that view shows them. Lands through * `custom.table_sync` in the caller's active organization (`home_id` defaults to its data home). */ interface TableCopySpec { source_organization_id: Uuid; source_table_id: Uuid; view_id?: Uuid | null; table_name?: string | null; home_id?: Uuid | null; /** `on_change` (default): the store's time tick refreshes the copy when the source changes. */ auto_refresh?: "on_change" | "off"; } /** Every `custom.table_sync` answer also says when the sync happened and what it could not do. */ interface TableSyncAnswer extends TableSyncResult { rows_restored?: number; rows_kept_archived?: number; rows_total?: number; warnings?: string[]; synced_at?: string; } /** How often a synced table refreshes on its own (`sync_source.auto_refresh`). */ type SyncAutoRefresh = "off" | "hourly" | "daily" | "on_change"; /** What `custom.table_sync_settings_set` changes on one synced table. */ interface TableSyncSettings { auto_refresh?: SyncAutoRefresh; /** Google Sheets through a connected account only: edits go back to the sheet. */ write_back?: boolean; } /** The `sync_source` a synced Table carries (what the store wrote, read back). */ interface TableSyncSourceDocument extends TableSyncSource { label?: string | null; url?: string | null; access?: "link" | null; header_row?: number | null; set_by?: Uuid | null; auto_refresh?: SyncAutoRefresh | null; write_back?: boolean | null; source_organization_id?: Uuid | null; due_at?: string | null; } /** One row of `custom.table_row_counts`: the rows of that Table this reader may see. */ interface TableRowCount { table_id: Uuid; visible_rows: number; } /** * One row of `custom.record_headers`: the row's own columns the document door does not answer. * `created_by` is the caller's own id on rows the caller made and null on every other; `mine` * says which. */ interface RecordHeader { id: Uuid; table_id: Uuid; created_at: string; updated_at: string; version: number; deleted_at: string | null; mine: boolean; created_by: Uuid | null; } /** What `custom.view_record_order_set` answers. */ interface ViewOrderSet { view_id: Uuid; table_id: Uuid; order: "manual"; named: number; positioned: number; } /** * What `custom.table_share_outside_grant` answers. `person` says which path ran: * `outside_account` (granted at once and notified — never made a member), `member` (a plain * share), `no_account` (the invitation door's own answer, with its link and delivery). */ interface OutsideShareAnswer { granted: boolean; invited: boolean; joined: boolean; person: "outside_account" | "member" | "no_account"; email: string; table: string; level: string; level_label: string; say: string; user_id?: Uuid; permission_id?: Uuid; invitation_id?: Uuid | null; accept_path?: string | null; delivery_say?: string | null; } /** A value an action writes: a literal, or a reference the store resolves at run time. */ type AutomationValue = string | number | boolean | null | { from: Uuid; } | { now: true; } | { in_days: number; } | { me: true; } | { clear: true; } | unknown[] | Record; /** `{ [field id]: value }` — every key is a live Field of the table the step writes to. */ type AutomationValues = Record; /** A filter in the store's one rule shape; inside an `edit_rows.where`, `{ trigger: }` means that value on the triggering row. */ type AutomationFilter = Record; /** Days of the week as the store counts them: 1 Monday … 7 Sunday. */ type AutomationWeekday = 1 | 2 | 3 | 4 | 5 | 6 | 7; type AutomationTrigger = { on: "row_added"; } | { on: "property_edited"; field: Uuid; to?: unknown; } | { on: "form_answered"; } /** * AUTOMATIONS-AIRTABLE: Airtable's "When record updated" — any of `fields` edited (an empty list or none = any * property). */ | { on: "record_updated"; fields?: Uuid[]; } /** * AUTOMATIONS-AIRTABLE: Airtable's "When record matches conditions" (the spec's `condition`, any depth) or, with * `view_id`, "When record enters view" (that saved view's own filter, read at run time). Fires when a row COMES TO * match — added matching, or edited from not matching to matching — never again while it stays matching. */ | { on: "record_matches"; view_id?: Uuid; } /** AUTOMATIONS-AIRTABLE: a press of this button column (`custom.button_press`) on a row runs it on that row. */ | { on: "button_pressed"; field: Uuid; } /** * AUTOMATION-TIME: a date or date-time property reaching now, `offset_days` before (negative) or after * (positive), at `at` (HH:MM, default 09:00) in the person's own zone, else the organization's. Fires once per row * per date value; the date changing re-arms it. */ | { on: "date_arrives"; field: Uuid; offset_days?: number; at?: string; } /** * AUTOMATION-TIME: the table acts with no row, every day / week (`weekday`) / month (`day`, 1-31, a short month * runs on its last day) at `at`. `set`, a person property, `{from}` and `{trigger}` have no row to read and are * refused; `add_row` is the repeating row and `edit_rows` the scheduled bulk edit. */ | { on: "schedule"; every: "day" | "week" | "month"; at?: string; weekday?: AutomationWeekday; day?: number; } /** * AT-AUTO2: every N minutes (5, 10, 15, 30) or every N hours (1, 2, 3, 4, 6, 8, 12) at `minute` past. Saved switched * OFF; the person switches it on (no blind paid automations). */ | { on: "schedule"; every: "minutes"; minutes: 5 | 10 | 15 | 30; } | { on: "schedule"; every: "hour"; hours?: number; minute?: number; } /** * AT-AUTO2: an inbound webhook. The store makes the automation's `webhook_secret`; the URL is * `automationWebhookUrl(serverBase, automation)`. The JSON body is `{{trigger.body.…}}` to every step. */ | { on: "webhook"; } /** * W2-E: a new email in the OWNER's own connected Gmail matching `query` (Gmail search syntax; none = every new * email). Like every connected-app trigger it has no row, is saved switched OFF, and once switched on the server * checks the account on the automation's own personal schedule (`automationAppTriggerTerms`); each new item starts one * run with the item as `{{trigger.body.…}}` (`subject`, `from`, `to`, `snippet`, `date`, `link`, `id`). */ | { on: "gmail_received"; query?: string; } /** * W2-E: an event on the owner's primary Google Calendar starts (`minutes_before` its start, 0-1440) or is created. * `{{trigger.body.title}}`, `starts_at`, `ends_at`, `location`, `attendees`, `link`, `id`. */ | { on: "calendar_event"; when?: "starts" | "created"; minutes_before?: number; } /** * W2-E: a message posted in a Slack `channel` (optionally containing `contains`). The platform has no Slack sign-in * yet, so the automation says so (`app_says`) until one exists. */ | { on: "slack_message"; channel: string; contains?: string; } /** * AT-MSFT: a new email in the owner's Outlook inbox, read through their own Microsoft connection (`Mail.ReadBasic`: * envelopes only, never the body). `query` keeps it to `from:` / `subject:` terms and plain words (every term must * match; none = every new email). `{{trigger.body.subject}}`, `from`, `from_name`, `to`, `received_at`, `link`, `id`. */ | { on: "outlook_received"; query?: string; }; /** AT-AUTO2: one comparison of a conditional group's path. `value` / `to` take tokens. */ interface AutomationComparison { value: unknown; is: "equals" | "not_equals" | "contains" | "not_contains" | "gt" | "gte" | "lt" | "lte" | "empty" | "not_empty" | "is_true"; to?: unknown; } type AutomationWhen = { all: AutomationComparison[]; } | { any: AutomationComparison[]; }; type AutomationAction = { do: "set"; values: AutomationValues; } | { do: "add_row"; table_id: Uuid; values: AutomationValues; } | { do: "edit_rows"; table_id: Uuid; where: AutomationFilter; values: AutomationValues; limit?: number; } | { do: "notify"; to: { person: Uuid; } | { field: Uuid; } | { author: true; }; text: string; subject?: string; link?: string; } | { do: "webhook"; url: string; } /** * AUTOMATIONS-AIRTABLE: an email through the platform's notification sender (event `custom.automation.email`) to an * address, a member, the address (or person) in a property of the row, or the automation's author. `subject` and * `body` take field tokens: `{Field name}` or `{{}}`. Nothing queued = the step fails and says why. */ | { do: "email"; to: { address: string; } | { person: Uuid; } | { field: Uuid; } | { author: true; }; subject: string; body: string; } /** AUTOMATIONS-AIRTABLE: a Slack incoming webhook (`https://hooks.slack.com/...`), message with field tokens. */ | { do: "slack"; url: string; text: string; } | { do: "agent"; prompt: string; } /** AT-AUTO2: records of a table matching a filter (tokens allowed in `const`s); later steps read `{{stepN.records}}`. */ | { do: "find"; table_id: Uuid; where?: AutomationFilter; limit?: number; } /** AT-AUTO2: change the named records (one id, a list, `{{stepN.records}}`, `{{item.id}}`). */ | { do: "update"; table_id: Uuid; record: string | string[]; values: AutomationValues; } /** AT-AUTO2: Airtable's "delete record" — we archive (restorable), never hard delete. */ | { do: "archive"; record: string | string[]; table_id?: Uuid; } /** * AT-AUTO2: JavaScript run by the server in the owner's own sandbox (never in the API process): `input` is * `inputs` resolved, `output.set(key, value)` answers `{{stepN.output.key}}`, console lines land in the run. * Metered to points. On a time trigger it is saved switched off. */ | { do: "script"; code: string; inputs?: Record; timeout_ms?: number; memory_mb?: number; } /** AT-AUTO2: if / else if / else — the first path whose `when` holds runs its steps; else `else`. */ | { do: "if"; branches: { label?: string; when: AutomationWhen; actions: AutomationAction[]; }[]; else?: AutomationAction[]; } /** * W2-E: open an issue in `repo` (owner/name) through the owner's own GitHub connection; `labels` comma-separated. * Answers `{{stepN.output.number}}` / `.url`. A retry answers the same issue. Metered per issue. */ | { do: "github_issue"; repo: string; title: string; body?: string; labels?: string; } /** * MS-WRITE: send one Outlook email as the owner (`Mail.Send`, approved once under Settings -> Integrations -> Microsoft). * `outlook_calendar_event` creates / changes / cancels an event on the owner's calendar (`Calendars.ReadWrite`; a retried * create is the same event); `teams_message` posts in the channel named by a Teams "Get link to channel" link * (`ChannelMessage.Send`) or in the chat named by a chat message's "Copy link" (`ChatMessage.Send`, AT-MSFT); work or * school accounts. Each is metered per action at `automation.app_action_points`. */ | { do: "outlook_mail"; to: string; subject: string; body: string; } | { do: "outlook_calendar_event"; action?: "create" | "update" | "cancel"; event_id?: string; title?: string; starts_at?: string; ends_at?: string; duration_minutes?: number; timezone?: string; attendees?: string; description?: string; notify?: boolean; } | { do: "teams_message"; channel_link: string; message: string; } /** * AT-SLACK: post one message in a Slack channel (name like `general` or an ID like `C0123ABC`) AS the owner, through * their own Slack sign-in (Settings -> Integrations -> Slack, user scope `chat:write`). Sent once; answers * `{{stepN.output.ts}}` / `.channel`. Metered per message at `automation.app_action_points`. The `slack` step * (an incoming-webhook URL) is separate and unchanged. */ | { do: "slack_post"; channel: string; message: string; } /** AT-AUTO2: repeat `actions` for each item of a list token (at most 200 items). */ | { do: "for_each"; list: string; actions: AutomationAction[]; }; interface AutomationSpec { name: string; trigger: AutomationTrigger; /** An optional filter on the triggering row. */ condition?: AutomationFilter | null; actions: AutomationAction[]; } /** `custom.automation_declare`: stored, or refused with one sentence per path (`trigger`, `actions.0.values.` …). */ type AutomationDeclared = { ok: true; automation_id: Uuid; table_id: Uuid; enabled: boolean; spec: AutomationSpec; /** * AUTOMATIONS-AIRTABLE: true when the trigger is a time and a step is an agent — AI on a schedule is saved * switched OFF and turned on only by `automationSetEnabled`; `says` is the store's sentence. */ ai_on_schedule?: boolean; says?: string; reused?: boolean; } | { ok: false; _errors: Record; }; type AutomationRunStatus = "ran" | "failed" | "stopped" | "waiting"; interface AutomationLastRun { run_id: Uuid; status: AutomationRunStatus; at: string; says: string | null; } interface AutomationItem { id: Uuid; /** AT-AUTO2: a webhook automation's secret, part of its URL. */ webhook_secret?: string | null; /** AT-AUTO2: the last payload its webhook received (its test run uses it). */ webhook_sample?: { body: unknown; at: string; } | null; /** W2-E: a connected-app trigger's last check, and why it could not read the account (null = it could). */ app_checked_at?: string | null; app_says?: string | null; /** W2-E: the last item a connected-app trigger handed to a run. */ app_sample?: { body: unknown; at: string; } | null; name: string; spec: AutomationSpec; enabled: boolean; archived_at?: string | null; /** AUTOMATIONS-AIRTABLE: why the store switched it off (a watched view was removed); cleared when switched on. */ disabled_reason?: string | null; created_by?: Uuid | null; created_at?: string; updated_at?: string; last_run: AutomationLastRun | null; } interface AutomationList { table_id: Uuid; automations: AutomationItem[]; } /** archive / restore / set_enabled. */ interface AutomationFlipped { ok: true; automation_id: Uuid; table_id: Uuid; enabled: boolean; archived_at: string | null; } interface AutomationStep { n: number; do: AutomationAction["do"]; /** AT-AUTO2: the repeat round (0-based) a step inside a repeating group ran in. */ index?: number; /** AT-AUTO2: a script's console lines. */ logs?: string[]; /** AT-AUTO2: a server step's answer, as text. */ output_text?: string; found?: number; status: "done" | "failed" | "waiting"; /** The store's own words. */ says: string; hint?: string | null; record_id?: Uuid; edited?: number; prompt?: string; } interface AutomationRun { run_id: Uuid; status: AutomationRunStatus; says: string | null; record_id: Uuid | null; trigger: { /** `test` for a test run (`of` is the automation's own trigger), `button_pressed` for a press. */ on: AutomationTrigger["on"] | "test"; /** AT-AUTO2: a webhook's payload. */ body?: unknown; of?: AutomationTrigger["on"]; /** A test run: whether the row met the condition (null = it could not be worked out). */ condition_met?: boolean | null; operation: string; via: string | null; changed_field_ids: Uuid[]; /** A time trigger's own facts: the moment it was due, and (date arrives) the date it read. */ fires_at?: string; date_value?: string; every?: "day" | "week" | "month"; } | null; steps: AutomationStep[]; started_at: string | null; finished_at: string | null; ran_by: Uuid | null; at: string; } interface AutomationRuns { automation_id: Uuid; runs: AutomationRun[]; /** Pass back as `cursor` for the next, older page; null at the end. */ next_cursor: string | null; } /** AUTOMATIONS-AIRTABLE: `custom.automation_test` — one run now on a picked row (none for a schedule), on or off. */ interface AutomationTested { ok: true; run_id: Uuid; automation_id: Uuid; record_id: Uuid | null; status: AutomationRunStatus; says: string | null; steps: AutomationStep[]; condition_met: boolean | null; } /** AT-AUTO2: `custom.automation_revisions` — every save of one automation, newest first. */ interface AutomationRevision { revision_id: Uuid; at: string; by: Uuid | null; note: "created" | "edited"; enabled: boolean; spec: AutomationSpec; } interface AutomationRevisions { automation_id: Uuid; table_id: Uuid; revisions: AutomationRevision[]; } /** One of the person's other organizations, and whether the Table can go there. */ interface TableHomeDestination { id: Uuid; name: string; /** * The organization's web address (UI-FIX-19). A screen draws it beside a name another * destination also carries, so two organizations with one name can be told apart. `null` when * the store's answer predates it. */ slug: string | null; /** The person's role there: owner · admin · member. */ role: string; /** False when the store says the Table cannot go there; `why` says why, in its words. */ ok: boolean; why: string | null; } /** `custom.table_home`'s answer, read. */ interface TableHome { table: { id: Uuid; name: string; version: number; }; /** Where it lives, from the Table itself. */ organization: { id: Uuid; name: string; }; /** The Table's maker, or an owner/admin of its organization. */ mayMove: boolean; /** When `mayMove` is false: the sentence that says who can. */ whyNot: string | null; /** Sentences for what holds it where it is. Empty when nothing does. */ heldBy: string[]; /** Every other organization this person belongs to. */ destinations: TableHomeDestination[]; carries: { fields: number; records: number; inTrash: number; choiceLists: number; withIt: number; /** Tables that ride along because they live inside it or inside its rows. */ tablesInside: number; /** Links between its rows and rows that stay, which will reach back across the wall. */ linksAcross: number; }; } /** What `custom.table_move` answered when the move landed. */ interface TableMoved { to: { id: Uuid; name: string; }; from: { id: Uuid; name: string; }; tableName: string; } /** * What `custom.table_duplicate` answered when the copy landed (lane TABLE-ACTIONS). The copy is a * NEW Table in `organization`; `copied` counts what came with it and `notCopied` names, in the * store's own words, what a copy never carries (history, forms, portals, dashboards, rules, …). */ interface TableDuplicated { table: { id: Uuid; name: string; }; from: { id: Uuid; name: string; organizationId: Uuid; }; /** * Where the copy lives. `null` only for a copy carried on by its id (`tableDuplicateContinue` * after a reload): the carry-on door's answer names the copy, not its organization, and the * first answer that did is gone. `tableDuplicate` always fills it from its first answer. */ organization: { id: Uuid; name: string; } | null; copied: { fields: number; choiceLists: number; choices: number; views: number; records: number; withRecords: boolean; }; /** Archived columns of the source, which stay behind with it. */ archivedFieldsLeft: number; /** Archived (or quarantined) records left behind; `null` when records were not copied. */ archivedRecordsLeft: number | null; /** Records this person is not handed (the ladder, then "Shown to"); `null` when records were not copied. */ recordsNotYoursLeft: number | null; /** Columns this person may not read: copied as columns, values left empty — column name → values left behind. */ hiddenColumns: Record; /** Another person's "only me" views of the source, which stay theirs. */ privateViewsLeft: number; /** Where a reference to a row that was not copied was taken out ("view …", "column …", the table's look). */ rowReferencesDropped: string[]; /** Views whose source was "only me": copied as new views are made, still "Shown to: only me". */ viewsSharedAsNewViews: string[]; /** Views the house door for a new view refused, each with its sentence. */ viewsNotCopied: string[]; /** What moved in the source while it was copied (the copy keeps what was fixed when it started). */ changedDuringCopy: { columnsArchivedSince: string[]; rowsChangedAfterCopied: number; rowsArchivedSince: number | null; rowsAddedSince: number | null; }; notCopied: string[]; path: string; } /** One pass of a paged copy (`custom.table_duplicate` / `custom.table_duplicate_continue`). */ interface TableDuplicateProgress { tableId: Uuid; tableName: string; /** "busy": another pass of this copy is running (wait, then carry on). "failed": the source was * archived while it was copied — not handed over; discard it. "discarded": its half table was archived. */ status: "copying" | "done" | "busy" | "failed" | "discarded"; /** What the pass stopped on: "choices" | "fields" | "views" | "records" | "links" | "done". */ step: string; /** The store's sentence for busy / failed / discarded. */ sentence: string | null; /** This pass: how long before its first unit, how many units, how long in all (ms). */ pass: { setupMs: number; units: number; ms: number; } | null; choiceLists: { done: number; total: number; }; fields: { done: number; total: number; }; views: { done: number; total: number; }; /** `null` when records are not being copied. */ records: { done: number; total: number; } | null; } /** One of the caller's unfinished copies (`custom.table_copies_in_progress`). */ interface TableCopyInProgress { tableId: Uuid; tableName: string; organization: { id: Uuid; name: string; }; from: { id: Uuid; name: string; }; /** "copying" (carry it on) or "failed" (the source was archived meanwhile — discard it). */ status: "copying" | "failed"; sentence: string | null; startedAt: string | null; records: { done: number; total: number; } | null; } /** One value the agent was given, with the triple that makes it citable. */ interface RecordScopeField { key: string; label: string; type: string; value: unknown; /** DYN: a merge field resolves to a TRIPLE, not a string. */ cite: { record_id: Uuid; field_id: Uuid; value_version: number; written_at: string | null; }; } /** A Field the agent was NOT given, and which of the two reasons it is. */ interface RecordScopeWithheld { key: string; label: string; because: "masked" | "policy"; reason: string; needs?: string; /** The whole sentence, already written — a screen prints it rather than composing one. */ says: string; } interface RecordScopeRelation { via: string; target_id: Uuid; foreign: boolean; masked: boolean; your_level: PermissionLevel | null; card: Record | null; why: string | null; } interface RecordScopeHistoryEntry { version: number; at: string; what: string; who: string | null; changes: Array>; } interface RecordScopeComment { id: Uuid; body: string; created_at: string; created_by_name: string; field_key: string | null; } interface RecordScopeContext { record: { id: Uuid; title: string; table_id: Uuid; table_name: string | null; scope_type: string; plural: string; version: number; updated_at: string; your_level: PermissionLevel; }; fields: RecordScopeField[]; withheld: RecordScopeWithheld[]; relations: RecordScopeRelation[]; history: RecordScopeHistoryEntry[]; comments: RecordScopeComment[]; may_comment: boolean; siblings: { shown: Array<{ id: Uuid; title: string; }>; more_than_shown: boolean; says: string; }; /** Questions derived from THIS record's shape — never a fixed list. */ suggested: string[]; /** One sentence naming what is not in the conversation. Printed verbatim. */ says: string; assembled_at: string; } /** What a conversation is about. `readable` false means the person lost access to it. */ interface ConversationScope { bound: boolean; readable?: boolean; record_id?: Uuid; table_id?: Uuid; scope_type?: string; title?: string; bound_at?: string; bound_by?: Uuid; because?: string; } interface ConversationScopeContext { scope: ConversationScope; context: RecordScopeContext | null; } /** `custom.table_kind_facts(p_table_id)` verbatim (N1). */ interface TableKindFacts { organization_id: string; table_id: string; name: string | null; title_field: string | null; agent_writable: boolean; purpose: string | null; version: number | null; type_field: string | null; stamp: string | null; /** Every live Field document (typed ones included), plus `id` and `version`, in `sort` order. */ fields: Array>; /** `{: [{key, label, id, retired, position}]}`, retired included, live first. */ choices: Record>; /** `custom.table_column_source` (`fields` / `free_form` / `code`) once the door carries it; absent, the mapping's default. */ column_source?: string | null; /** `custom.record_platform_keys()` once the door carries it; absent, the mapping's keys. */ platform_keys?: string[] | null; } /** * DOOR-8. As-of on EITHER clock, and they are different questions: * · `record` — the record clock: what this record looked like at version time. * · `world` — the world clock: which values were true of the world then * (the Field's `dated` modifier). */ interface AsOf { clock: "record" | "world"; at: Timestamp; } /** A predicate over a PROMOTED field (REC-5: promotion light -> heavy). */ interface PromotedPredicate { field_key: string; /** The value as text, exactly as `custom.promoted_read` takes it. */ equals: string; } interface RecordQuery { table_id: Uuid; /** DOOR-6. Any combination, any subset. An empty list means "no relation constraint". */ relations?: RelationCoordinate[]; /** Predicates the store can answer from an index. */ promoted?: PromotedPredicate[]; /** DOOR-8. */ asOf?: AsOf; /** DOOR-9. Ask across EVERY Home of this Table in one answer. */ acrossHomes?: boolean; /** REC-23. Soft-deleted rows are out unless you say otherwise. */ includeDeleted?: boolean; /** FLD-10. */ recordType?: string; orderBy?: { column: "created_at" | "updated_at"; ascending?: boolean; }; limit?: number; offset?: number; } /** * One client's memory of its own operations. `mint()` names a write and remembers it; `isOwn(opId)` * answers whether a notice is that write's echo. * * OWN-WRITE DETECTION IS PER CLIENT INSTANCE. Two setups in ONE process (a server holding two * identities, a worker beside a UI) must hear each other: a realm-wide ledger made A's write "ours" to * B as well, and B dropped it as an echo (2026-10-08, the README proof). `createRecords` gives every * setup its own ledger and hands it to its client, its entity port and its live-changes port. */ interface OpLedger { mint(): Uuid; isOwn(opId: string | null | undefined): boolean; /** Test seam: forget everything. */ clear(): void; /** Test seam: how many operations are currently remembered. */ readonly size: number; } /** * The data seam: a structural subset of `SupabaseClient`. The package never * opens a database connection of its own and never holds a key — the HOST * supplies a client that already carries the operating person's session, which * is what makes DOOR-5 and AGT-N-4 true (the store sees the person, not us). */ interface RecordsDataSource { /** * Call a door. The name is always a door the live catalog has; `/core` * refuses `door_absent` before it ever reaches here for one that does not. */ rpc(fn: string, args: Record, /** * THE SCHEMA THE DOOR LIVES IN, always supplied: `custom` for the store's * own doors, `iam` for the trust doors (the external principal and the * publish binding, which are the PLATFORM's). A host binding supabase-js * routes this to `client.schema(options.schema).rpc(fn, args)`; a host that * ignores it calls its own default PostgREST profile — `public` — and is * refused by name, which is the failure we want rather than a silent answer * from the wrong system. (Before 2026-09-18 this was described as "only ever * `iam`" and the store's doors went unqualified; the extension's first real * turn proved that asks `public.store_is_open` and finds nothing.) */ options?: { schema?: string; }): PromiseLike<{ data: unknown; error: PgError | null; }>; /** * OPTIONAL. True for the real supabase adapters: the client may open a table page with ONE * `custom.table_page_bundle` call instead of a dozen shape doors one after another (PAGE-BUNDLE). * Leave it off for a test or replay source that does not model the bundle door. */ opensInOneCall?: boolean; /** Reads of `custom.*` go through the schema-qualified builder, never a bare `from`. */ schema(name: string): { from(table: string): RecordsTableQuery; }; } /** The read surface the list and query paths use. A real supabase-js client satisfies it. */ interface RecordsTableQuery { select(columns: string, options?: { count?: "exact" | "planned" | "estimated"; head?: boolean; }): RecordsFilter; } interface RecordsFilter extends PromiseLike { eq(column: string, value: unknown): RecordsFilter; neq(column: string, value: unknown): RecordsFilter; in(column: string, values: readonly unknown[]): RecordsFilter; is(column: string, value: null | boolean): RecordsFilter; gte(column: string, value: unknown): RecordsFilter; lte(column: string, value: unknown): RecordsFilter; order(column: string, options?: { ascending?: boolean; }): RecordsFilter; range(from: number, to: number): RecordsFilter; limit(count: number): RecordsFilter; } interface RecordsResponse { data: unknown; error: PgError | null; count?: number | null; } /** * WHO IS ACTING. AGT-N-4: an agent carries the exact authority of the person * operating it — there is no agent identity and there are no service roles. So * the actor word and the person are two separate facts, and `on_behalf_of` is * meaningful only when the actor is `agent`. */ interface RecordsActor { actor: Actor; /** The signed-in person. The store reads this from the session, not from us; this is for the client's own mirror. */ user_id?: Uuid; /** Only an agent acts on behalf of a person (the store refuses it otherwise). */ on_behalf_of?: Uuid; } /** * OPTIONAL. A live-updates seam. The default degradation is stated rather than * silent: with no port bound, `useRecords` reports `live: false` and the screen * can say "not live" instead of quietly going stale. * * A host binds this with `@ai-matrx/data/react`'s channel hygiene — the package * deliberately does NOT depend on it, so the headless core stays free of a * realtime client and a host that has one keeps using the one it already has. */ interface RecordsRealtimePort { /** * Watch one organization's records of one table. Returns an unsubscribe. * * THE HOST NUDGES THROUGH THIS SINK AND REACHES AROUND NOTHING. Until 0.43.0 the callback * was `(recordId: Uuid) => void` and a host that also wanted to say "the SHAPE moved" * imported `tableShapeChanged` from `@ai-matrx/records/react` and called it directly — * which worked, and which meant the port contract did not describe half of what a port * actually does. A column appearing is not a row changing, and the package owns the * difference. * * NOTHING THAT ARRIVES HERE IS DATA. A nudge has not passed the read door, so the hook * re-reads — by id set when the nudge names ids, whole-page when it cannot. */ subscribeRecords(args: { organization_id: Uuid; table_id: Uuid; }, sink: RecordsChangeSink): () => void; } /** What a realtime port may tell the package. Two calls, because a column is not a row. */ interface RecordsChangeSink { /** * THESE RECORDS MOVED. `null` means "I cannot tell you which" — a reconnect, a tab wake, or * a change above the store's own id cap — and is the one case that costs a whole-page read. * An EMPTY array is not the same thing and is not a nudge at all. */ records(ids: readonly Uuid[] | null): void; /** * THIS TABLE'S SHAPE MOVED — a column added, retired or changed. Re-reading the rows would * redraw the same table without the new column in it, which is a screen that is out of date * while looking healthy. */ shape(): void; } /** * OPTIONAL. Where a refusal that nobody asked about goes — the loud funnel. * Default degradation: `console.error` with the store's own words, never * silence. */ type RecordsErrorSink = (error: { code: string; message: string; hint?: string; where: string; }) => void; /** * OPTIONAL. The merge-field resolver's server half (`W5-MERGE-B`). While it is * not bound, `useMergeField` answers `pending` WITH THE REASON — never a blank, * never a literal placeholder left in the text (DYN-14). */ interface RecordsResolverPort { resolve(args: { organization_id: Uuid; merge_field_keys: string[]; /** The live selection the `state` source reads (DYNAMIC-VALUES §C). */ state?: Record; }): Promise; } /** The one config a host binds. */ interface RecordsConfig { /** REQUIRED. */ dataSource: RecordsDataSource; /** REQUIRED. */ actor: RecordsActor; /** * REQUIRED. REC-29: organizations are hard walls, and the store is keyed * `(organization_id, id)`. There is no "current organization" this package * discovers for itself — the host says which one, every time. * * `null` is the ALL-ORGANIZATIONS mode (access belongs to the person, not the selected * organization): a host whose page lists across every organization the person belongs to * mounts with none. Doors that answer for the person (`custom.data_home*`) take it; a door that * needs one is sent NULL and refuses in its own words, never a guessed organization. */ organizationId: Uuid | null; /** * EVERY organization the person belongs to (active-org-is-never-a-list-filter.md). * `organizationId` above is where NEW things are written and which org a write * runs in; it never decides what a READ shows. With this set, `tableList` / * `tableFacts` span all of these organizations (or one, when the page passes * its `organization_id` filter), and `tableRead` / `recordRead` / * `recordValuesVersioned` open a record in ITS OWN organization. Absent ⇒ the * client reads only `organizationId`, exactly as before. */ organizationIds?: readonly Uuid[]; realtime?: RecordsRealtimePort; /** * This client's own-write memory (`createOpLedger()`). Hand the SAME ledger to the live-changes port * that listens for this client. Absent ⇒ the realm's default ledger (one client per browser tab). */ opLedger?: OpLedger; onError?: RecordsErrorSink; resolver?: RecordsResolverPort; /** * The schema the record store lives in. Exists so a rehearsal branch and * production can differ without a fork; it is `custom` everywhere today. */ schema?: string; /** * Doors the generated catalog (`STORE_DOORS`) does not list YET but this store is known to have — * the nightly clone after a rehearsal, a test store. A screen reading an owed door asks * `doorReady(config, name)` first, so production (which lacks the door until the chair applies it) * is never asked and logs nothing. Leave it out in a shipped host: the regenerated catalog is * the switch. */ knownDoors?: readonly string[]; } /** * DOOR-6. A query coordinate: "related to THIS, along THAT role, in that * direction". Any combination, and any subset of them. */ interface RelationCoordinate { /** The record at the other end. */ record_id: Uuid; /** The field key the edge carries as its role. Omit to mean any role. */ role?: string; /** Which way to walk. `out` is this record's own relation fields. */ direction?: "out" | "in" | "either"; } /** * The arguments of the ONE relation-words resolver: the FIELD, because that is where its display * spec is declared and judged (`custom._display_of_field` reads it server-side; the caller never * carries it), and the ids of a page of cells. */ /** A linked record's icon, and its table's icon for a record that has none (`relationWordsMany`). */ interface RelationIcons { icon: string | null; tableIcon: string | null; } interface RelationWordsManyArgs { field_id: Uuid; record_ids: Uuid[]; } /** One reverse column of a Table, as `custom.reverse_columns` answers it. */ interface ReverseColumn { /** Stable key: the linking Field's `inverse_key`, else `linked_`. Never a Field key of B. */ key: string; /** What the column is called on B: the linking table's name (plus the column's when ambiguous). */ label: string; /** The relation Field on the linking table. */ source_field_id: Uuid; source_field_key: string; source_field_label: string; /** The linking table. */ source_table_id: Uuid; source_table_name: string; /** Always `many`: any number of records may link to one. */ cardinality: "many"; /** * True for a virtual back-link (written only from the linking table's side). False for a TWO-WAY * LINK (NOTION-PROPS): the reverse end is a real Field on this table — `key` is its key, `label` * its label — and `relationReverseSet` writes its links from here. */ read_only: boolean; } /** One linking record, as a chip shows it. */ interface ReverseLink { id: Uuid; /** The linking record's name, read by the store for this reader. */ words: string; } /** The links of one record through one reverse column: how many the reader may see, and one page. */ interface ReverseLinks { total: number; links: ReverseLink[]; } interface ReverseLinksManyArgs { /** The Table whose records are being shown (B). */ table_id: Uuid; /** `ReverseColumn.source_field_id`. */ source_field_id: Uuid; /** A page of B's record ids — up to 200 a call; more are sent in groups by the client. */ record_ids: readonly Uuid[]; /** Links per record, 1–50 (the store holds it there). Default 10. */ limit?: number; /** Skip this many links of each record — the next page of one hub record. Default 0. */ offset?: number; } /** `reverseLinksFields`: the links of a page through several reverse columns, in one call. */ interface ReverseLinksFieldsArgs { table_id: Uuid; /** Each `ReverseColumn.source_field_id` — at most 20. */ source_field_ids: readonly Uuid[]; record_ids: readonly Uuid[]; limit?: number; offset?: number; } /** The store's typed "source archived" answer for a lookup or roll-up. */ interface SourceArchived { __unavailable: "source_archived"; /** `link`: the relation it reads through is archived; `column`: the far column it reads is. */ reads: "link" | "column"; /** The archived Field. Absent when the reader may not know its Table. */ field_id?: Uuid; /** Its label. Absent when the reader may not know its Table. */ label?: string; table_id?: Uuid; /** Its Table's name. Absent when the reader may not know that Table. */ table?: string; } /** * What a context field holds, as a person thinks of it — derived from the Field's behavior, `multi`, * `format`, display format and `config.kind` (`contextFieldKind`). The value is always `value`; the * kind says how to read and edit it. */ type ContextFieldKind = "string" | "markdown" | "email" | "url" | "phone" | "color" | "number" | "percent" | "currency" | "boolean" | "date" | "datetime" | "time" | "object" | "array" | "reference" | "document"; /** The store's context policy on a Field: whether an agent receives it every turn, on request, or never. */ type ContextPolicy = "include" | "on_request" | "exclude"; /** The store's sensitivity words on a Field. */ type ContextSensitivity = "public" | "internal" | "confidential" | "restricted"; /** A scope type: a Table kept for context. */ interface ScopeType { id: Uuid; organization_id: Uuid; label_singular: string; label_plural: string; icon: string; color: string; /** The Table's slug in the store's grammar (`practice_areas`). */ slug: string | null; description: string; sort_order: number; max_assignments_per_entity: number | null; default_variable_keys: string[]; /** Who made the type: with the organization's owners and admins, they shape it (`canShapeScopeType`). */ created_by: Uuid | null; created_at: string; updated_at: string; } /** A scope type with its live scopes, as the tree doors answer it. */ interface ScopeTypeWithScopes extends ScopeType { scopes: Scope[]; } /** A scope: a Record of its type's Table. */ interface Scope { id: Uuid; scope_type_id: Uuid; organization_id: Uuid; name: string; description: string; slug: string | null; sort_order: number; parent_scope_id: Uuid | null; /** Free settings keys of the scope (each a Field of its Table the scope door derived). */ settings: Record; created_by: Uuid | null; created_at: string; updated_at: string; } /** A scope with its type's presentation (`custom.context_scopes`). */ interface ScopeWithType extends Scope { scope_type: ScopeTypeLabel | null; } /** The presentation of a scope type: what a chip or a breadcrumb shows. */ interface ScopeTypeLabel { id: Uuid; /** The type's slug in the store's grammar (`practice_areas`), when it has one. */ slug: string | null; label_singular: string; label_plural: string; icon: string | null; color: string | null; } /** A scope type that was archived, with how many scopes went with it. */ interface ArchivedScopeType { id: Uuid; organization_id: Uuid; label_singular: string; label_plural: string; icon: string; color: string; archived_at: string; created_by: Uuid | null; archived_scope_count: number; } /** A scope that was archived (a Record in the archive of its type's Table). */ interface ArchivedScope { id: Uuid; name: string | null; slug: string | null; archived_at: string | null; } /** A context field: a Field (column) of a scope type's Table. */ interface ContextField { id: Uuid; scope_type_id: Uuid; organization_id: Uuid; key: string; label: string; description: string; kind: ContextFieldKind; /** The Field's own words, for a caller that needs them (`type` is the store's behavior word). */ type: string; multi: boolean; format: string | null; config: Record; relation_target: Uuid | null; /** Its place among the type's context fields, from 0. */ sort: number; context_policy: ContextPolicy; sensitivity: ContextSensitivity; source: string; status: string; status_note: string | null; category: string | null; tags: string[]; max_items: number; custom_component: unknown; reference_source: unknown; allowed_scope_type_ids: Uuid[] | null; allowed_reference_types: string[] | null; review_interval_days: number | null; depends_on: string[]; /** The store's clock on this Field. */ version: number; created_by: Uuid | null; created_at: string; updated_at: string; } /** One thing a reference cell points at. */ interface ContextReference { /** The record id (for a file: the File record's id). */ id: Uuid; /** What it is: `scope`, `file`, `table`, or another entity token. */ type: string; /** Its name, where the door named it to this caller. */ label?: string; /** For a file: the `files.files` id a viewer opens. */ file_id?: Uuid; } /** The cell holds only the first words of a text kept as a file, and the whole could not be read. */ interface IncompleteValue { /** The cell's own first words. */ head: string; /** The whole text's length in characters, when known. */ chars: number | null; /** The file holding the whole text, when it has one yet. */ file_id: Uuid | null; /** Why the whole text is not here, in one sentence. */ why: string; } /** A context value: one cell of a scope's Record. */ interface ContextValue { scope_id: Uuid; field_id: Uuid; key: string; kind: ContextFieldKind; /** * THE CELL, as the store holds it (a text, a number, a boolean, an ISO date, a list, an object). * A reference or document cell is its `references` (ids here). A text kept as a file is its whole * text when the read could open the file (`completeWholeValues`), else the first words and `incomplete`. */ value: unknown; /** A reference / document cell, decoded: what it points at, with names the caller may see. */ references: ContextReference[]; /** The version a PERSON made (system rewrites never move it). */ version: number; set_at: string | null; source_type: string | null; authored_by: Uuid | null; /** The pointer to the file holding a text too big for one cell, when the cell holds only its start. */ whole_value: StoreWholeValue | null; /** Present only when `value` is not the whole text — never save it back as the value. */ incomplete: IncompleteValue | null; } /** The file fields of a whole-value pointer, as `custom.context_values` answers them. */ interface StoreWholeValue { kind: string; file_id?: Uuid | null; file_record?: Uuid | null; bytes?: number | null; chars?: number | null; sha256?: string | null; shown_chars?: number | null; mime?: string | null; pending?: boolean | null; in_value?: boolean | null; } /** The scope types of some organizations, without their scopes, and how many each holds. */ interface ScopeTypeSkeleton { types: ScopeType[]; /** type id → how many scopes the caller sees in it; null when asked without counts. */ counts: Record | null; } /** One page of one type's scopes. */ interface ScopePage { scopes: Scope[]; total: number; next_offset: number | null; } /** What a System Context item is: a curated fact, an ambient fact of the moment, or a dataset. */ type SystemContextItemClass = "curated" | "ambient" | "dataset" | (string & {}); /** * A System Context item (`custom.context_system_items`), as a `ContextField` of the * `SYSTEM_CONTEXT_TYPE_ID` pseudo-type, plus its class. */ interface SystemContextField extends ContextField { item_class: SystemContextItemClass | null; } /** One column of a table template. */ interface TableTemplateField { field_name: string; display_name: string; data_type: string; field_order: number; is_required: boolean; } /** * A table template (a `custom.template` of kind `table`) a scope's dataset field may lock its * table's columns to: the organization's own or a platform one, as `custom.table_templates` answers it. */ interface TableTemplate { id: Uuid; organization_id: Uuid | null; name: string; description: string; version: number; is_platform: boolean; fields: TableTemplateField[]; } /** One scope template of the gallery, as `custom.context_templates` answers it. */ interface ScopeTemplate { [key: string]: unknown; } /** A new scope type, or the parts of one to change (`undefined` = leave as it is). */ interface ScopeTypeSpec { label_singular?: string; label_plural?: string; icon?: string; color?: string; description?: string; sort_order?: number; max_assignments_per_entity?: number | null; default_variable_keys?: string[]; /** Any spelling; the package writes it in the store's grammar. Absent on create = from the label. */ slug?: string; } /** A new scope, or the parts of one to change. */ interface ScopeSpec { name?: string; description?: string; settings?: Record; parent_scope_id?: Uuid | null; slug?: string; sort_order?: number; } /** A new context field, or the parts of one to change (`null` clears). */ interface ContextFieldSpec { key?: string; label?: string; kind?: ContextFieldKind; description?: string; category?: string | null; context_policy?: ContextPolicy; sensitivity?: ContextSensitivity; tags?: string[]; sort?: number; status?: string; status_note?: string | null; custom_component?: unknown; review_interval_days?: number | null; allowed_reference_types?: string[] | null; max_items?: number | null; allowed_scope_type_ids?: Uuid[] | null; reference_source?: unknown; } /** One cell to write. `value` is the cell as `ContextValue.value` reads it; a reference cell takes `references`. */ interface ContextValueWrite { scope_id: Uuid; field_id: Uuid; kind: ContextFieldKind; value?: unknown; /** A reference / document cell: the things it points at (replaces the cell). */ references?: ContextReference[]; /** Who wrote it: `manual` from a person's own edit (the store's default is `ai_enriched`). */ source_type?: string; change_summary?: string; } /** What a written cell answers. */ interface ContextValueWritten { scope_id: Uuid; field_id: Uuid; version: number; source_type: string; } /** Reads a file's whole text as the person (the host's one file reader). */ type ReadContextFileText = (fileId: Uuid) => Promise; interface ScopeDoors { /** Every live scope type of these organizations, each with its live scopes. */ tree(organizationIds: readonly Uuid[]): Promise>; /** The scope types of these organizations without their scopes (the first paint), with or without counts. */ types(organizationIds: readonly Uuid[], options?: { withCounts?: boolean; }): Promise>; /** One page of one type's scopes, in the tree's order. */ typeScopesPage(scopeTypeId: Uuid, offset?: number, limit?: number): Promise>; /** The scopes whose name holds `query` (case-insensitive), across these organizations. */ search(organizationIds: readonly Uuid[], query: string, limit?: number): Promise>; /** Scopes by id, each with its type's label, in whichever organization each lives. */ scopes(ids: readonly Uuid[]): Promise>; /** * The context fields (columns) of these scope types, each type's fields in their order (`sort`, * then label) as the door answers them. */ fields(scopeTypeIds: readonly Uuid[]): Promise>; /** * One context field by its id, read in whichever of these organizations holds it (a Field is a * record of the field kernel; its scope type is the Table it is defined on). Refused `not_found` * when no organization answers it. */ field(fieldId: Uuid, organizationIds: readonly Uuid[]): Promise>; /** * The cells of these scopes. A text kept as a file is read whole through `readFileText`; without * one (or when the file cannot be read) the cell keeps its first words and says so (`incomplete`). */ values(scopeIds: readonly Uuid[], options?: { readFileText?: ReadContextFileText | null; }): Promise>; /** The archived scope types of one organization. */ archivedTypes(organizationId: Uuid): Promise>; /** Of these scope ids, the ones that are ARCHIVED scopes of this type. */ archivedScopesOfType(organizationId: Uuid, scopeTypeId: Uuid, wanted: ReadonlySet): Promise>; /** The System Context items (not scopes: the platform's own ambient context). */ systemItems(): Promise>; /** The table templates an organization's scope dataset fields may use (its own and the platform's). */ tableTemplates(organizationId: Uuid): Promise>; /** The scope templates of the gallery. */ templates(): Promise>; /** A new scope type in `organizationId`. */ createType(organizationId: Uuid, spec: ScopeTypeSpec): Promise>; /** Change a scope type (only the keys given). */ updateType(typeId: Uuid, spec: ScopeTypeSpec): Promise>; /** Archive a scope type; its scopes and fields come back with a restore. */ archiveType(typeId: Uuid): Promise>; restoreType(typeId: Uuid): Promise>; /** A new scope of `typeId` in `organizationId`. */ createScope(organizationId: Uuid, typeId: Uuid, spec: ScopeSpec): Promise>; updateScope(scopeId: Uuid, spec: ScopeSpec): Promise>; archiveScope(scopeId: Uuid): Promise>; restoreScope(scopeId: Uuid): Promise>; /** A new context field (column) on a scope type. Answers the field as the read door does. */ createField(scopeTypeId: Uuid, spec: ContextFieldSpec): Promise>; /** Change a context field (`null` clears a key). Answers the field as the read door does. */ updateField(fieldId: Uuid, scopeTypeId: Uuid, spec: ContextFieldSpec): Promise>; archiveField(fieldId: Uuid): Promise>; restoreField(fieldId: Uuid): Promise>; /** Write one cell. */ writeValue(write: ContextValueWrite): Promise>; /** * G11. The record-store Table a context scope's template-bound item provisions — one per item and * scope (`custom.scope_table_provision`). A scope provisioned before the move answers with its moved * Table (same id); the Home defaults to the organization's own. `organization_id` null lets the door * read the scope's own organization. */ tableProvision(args: { organization_id: Uuid | null; item_id: Uuid; scope_id: Uuid; home_id?: Uuid | null; }): Promise>; } interface PickListDoors { /** * PICK-LISTS: every pick list a person may open in ONE organization (`custom.pick_list_index`): id, name and * choice count, nothing else. Read only when a picker opens (the load law), never with the screen. */ pickListIndex(args: { organization_id: Uuid; }): Promise>; /** * PICK-LISTS: a pick list with its choices, grouped (`custom.pick_list_get`; viewer access on the list). * The list is read by its own id; its organization is the list's own, never the client's. */ pickListGet(args: { list_id: Uuid; }): Promise>; /** PICK-LISTS: a pick list for a selection control (`custom.pick_list_for_selection`): labels, help, groups, icons, NEVER the secret description. */ pickListForSelection(args: { list_id: Uuid; }): Promise>; /** * PICK-LISTS: rename / describe a pick list and, with `items`, REPLACE its choices * (`custom.pick_list_update`; editor access). The replaced choices are archived, soft and restorable. */ pickListUpdate(args: { list_id: Uuid; list_name?: string | null; description?: string | null; items?: readonly PickListItemInput[] | null; }): Promise>; } /** One organization the client could not use, with the sentence that says why. */ interface OrganizationUnavailable { organization_id: Uuid; reason: string; } /** `organizationsOpen`: the organizations whose store answers, and those that do not (with why). */ interface OrganizationsOpenAnswer { open: Uuid[]; unavailable: OrganizationUnavailable[]; } /** `metadataSearch`: every match, each carrying the organization it came from. */ interface MetadataSearchAnswer { matches: Array & { organization_id: Uuid | null; }>; /** Organizations asked in order (the ones that answered). */ organizations_covered: Uuid[]; unavailable: OrganizationUnavailable[]; } /** AT-SHARE-B. What `custom.field_notify_on_assign` answers: whether the column tells people, and the automations doing it. */ interface FieldNotifyOnAssign { field_id: Uuid; notify_on_assign: boolean; changed: boolean; automation_ids: string[]; says?: string; } /** What `custom.base_move` answered when the whole base landed. */ interface BaseMoved { name: string; toOrganizationId: Uuid; /** How many tables went with the Home. */ tables: number; } interface RecordsClient extends PickListDoors { /** The config this client was built with, so a screen can show who it is acting as. */ readonly config: RecordsConfig; /** * THE SCOPE DOORS (`custom.context_*`): scope types are Tables kept for context, scopes Records, * context fields Fields, values cells. Built from this client's own door path. `@ai-matrx/records/scopes`. */ readonly scopes: ScopeDoors; recordWrite(args: { table_id: Uuid; data: RecordDocument; }): Promise>; /** * WRITE THE RECORD THIS KEY NAMES, OR UPDATE IT WHEN IT IS ALREADY THERE — one call, race-safe * (`custom.record_upsert`). `key` is the column key (or keys) that say which record this is, * and every one must carry a `unique` rule: the store refuses any other column by its name. * Two concurrent calls with the same key leave ONE record. An update goes through * `record_update` (with `expectedVersion`, its stale-write refusal); a new record through * `record_write`. Answers the record's id either way. */ recordUpsert(args: { table_id: Uuid; key: string | string[]; data: RecordDocument; expectedVersion?: number; }): Promise>; /** * Write MANY new records of one Table in one call. * * `custom.record_write_many` asks the switch, the organization and the Table * ONCE for the whole batch and inserts the rows in one statement — which is * why a paste of 120 lines is one round trip per chunk instead of 120. It * CREATES only: there is no bulk update door, because an update carries a * per-row optimistic version and a batch would have to either drop that or * fail the whole batch for one stale row. * * It returns the new ids IN THE ORDER THE ROWS WERE HANDED IN, so a caller * can tie each id back to the line of the spreadsheet it came from. Hand in * `ids` to choose them yourself; hand in none and the door mints them. */ recordWriteMany(args: { table_id: Uuid; rows: RecordDocument[]; ids?: Uuid[]; }): Promise>; /** * DOOR-SPEED. MANY CHANGES TO ONE TABLE IN ONE TRANSACTION, through * `custom.record_change_many`: inserts, updates (each with an optional `expected_version`) * and archives. Every update lands in ONE statement, so the store's change notice, history and * activity fire once for the batch while every row trigger still judges every row. ONE REFUSED * CHANGE REFUSES THE BATCH and nothing is saved; the store's own sentence comes back with the * change's position in the hint. The answer is one result per change, in order. */ recordChangeMany(args: { table_id: Uuid; changes: readonly RecordChange[]; }): Promise>; /** * A PARENT, THE EDGES IT STANDS ON AND THE ROWS THAT BELONG TO IT — ALL OR NOTHING. * * `recordWrite` and `recordWriteMany` are each ONE statement. A screen or a * server path whose unit of work is a GRAPH — an AI artifact and the lines * it produced, an order and its items, a research note and its citations — * could therefore not save it as one thing: a loop is N transactions, and a * failure on row nineteen leaves a parent standing without the lines it * promised. * * `custom.record_write_graph` is one statement, so it is one transaction. It * is NOT a second write path: it calls `custom.record_write_many` for the * rows, `public.assoc_add` for an edge that points OUT of the store (the * message a record was produced by), and `platform.relation_set` for the * parent's own declared relation to its children. Same store switch, same * editor rung on the Table, same value envelope, same undeclared-key guard, * same history rows, same realtime notice. * * ONE REFUSAL REFUSES THE WHOLE GRAPH, BY NAME — the answer says which member * failed and carries the store's own sentence about it, and nothing is * written. * * Every child MUST name a `role`: it is the key of a declared RELATION field * on the parent's Table, and an edge out of a record that names no field is * refused by the store (REL-10 / T7) because without it nothing can read the * relation's delete rule, its cardinality or its organization wall. The order * children are handed in IS the order of the relation. */ recordWriteGraph(args: { table_id: Uuid; parent: RecordDocument; /** Each child: its document, the parent FIELD it fills, and optionally its own Table. */ children?: Array<{ data: RecordDocument; role: string; table_id?: Uuid; position?: number; }>; /** Edges to things OUTSIDE the store. `direction` defaults to `"in"` — that thing produced this record. */ edges?: Array<{ entity: string; id: Uuid; direction?: "in" | "out"; label?: string; role?: string; position?: number; metadata?: Record; }>; }): Promise>; recordUpdate(args: { record_id: Uuid; patch: Record; /** Leave it out and the last writer wins; supply it and a loser is TOLD. */ expectedVersion?: number; }): Promise>; /** * A CELL THAT TAKES WORDS WHICH ARE NOT YET A CHOICE — the words become a choice of that * column everywhere and the cell takes it, in ONE transaction (lane CHOICE-COLUMN-EDIT, * `custom.record_update_adding_choices`). `add` is `{ "": ["words", ...] }`; the * cell's value in `patch` is the words, and the store turns them into the new choice's key. * Adding a choice changes the table's columns, so it needs that right; a refusal names it. * Answers the new version, like `recordUpdate`, and announces the record changed. */ recordUpdateAddingChoices(args: { record_id: Uuid; patch: Record; add: Record; expectedVersion?: number; }): Promise>; /** * The Feature Knob `custom/choice_nudge` for the signed-in person in this client's * organization: what a choice cell does with typed words that are not a choice * (`decideTypedChoice` in `choice.ts` is the one place that acts on it). */ choiceNudge(): Promise>; /** * REC-23. Soft, and reversible within `retention`. * * 🚨 NOT FOR A TABLE. A Table is a record, so this door accepts one — and * then recurses through every record the Table contains inside a single * transaction, which is unbounded by construction and dies on `57014` at a * size the product's own import reaches in half a minute. Use * `tableArchive` below, which does the same work in resumable passes. */ recordDelete(args: { record_id: Uuid; }): Promise>; /** * MOVE A RECORD (a Table included) INTO ANOTHER HOME — `custom.record_reparent`, the one door that changes where a * record is contained. `parent_id` is the Home's record id (null takes it out). Admin on the record, viewer on the Home. */ recordReparent(args: { record_id: Uuid; parent_id: Uuid | null; }): Promise>; recordRestore(args: { record_id: Uuid; }): Promise>; /** * CLOSE-LINK (CD-LINK-10) — WHERE A TABLE LIVES: every Home it lives in (`custom.table_homes_named`), each named for * this reader, the Home it was declared in first (`declared`). A Home this reader may not open is named as withheld. */ tableHomes(args: { table_id: Uuid; }): Promise>>; /** * CLOSE-LINK (CD-LINK-10) — "ALSO LIVES IN…": the Table also lives in `home_record_id` (`custom.home_add`, admin on the * Table). A person shared on that Home alone then knows the Table and sees the records inside that Home, never another's. */ homeAdd(args: { table_id: Uuid; home_record_id: Uuid; }): Promise>; /** * ARCHIVING A WHOLE TABLE — ONE PASS OF IT (VERIFIER-11 F3). * * One call archives at most `chunk` of the table's live records through * `custom.record_delete` (so every guard, history capture and outbox row * still happens; nothing is hard-deleted) and comes back with the numbers a * screen draws progress from. The CALLER LOOPS until `done` — that loop is * the fix, because the one thing a single statement cannot be made to do is * finish. * * `chunk: 0` changes nothing and only reports, so a screen can say "1,430 * records will be archived" BEFORE a person commits to it. `includeTable: * false` empties the table and leaves it standing. * * RESUMABLE, WITHOUT A JOB ROW: what remains is read off the records * themselves every call. A pass that fails, a closed tab or a deploy mid-run * leaves a half-archived table and a next call that continues from exactly * there. A caller must therefore never report "nothing happened" after a * failed pass — `archived_total` says what really landed. */ tableArchive(args: { table_id: Uuid; /** * How many records this pass takes on. The door clamps it; it answers with what it * took. Default 50 — measured: ~35ms a record against an 8s statement timeout, so * 200 dies on `57014` and is the original defect one level up. */ chunk?: number; /** Archive the Table record itself once nothing live is left in it. Default true. */ includeTable?: boolean; }): Promise>; /** * ONE PASS OF ARCHIVING, ON PURPOSE, A TABLE THE APP'S CODE DEPENDS ON * (`custom.table_archive_deliberately`, lane 12 P5). Such a table (`code_depends: true`) refuses * `tableArchive`, `recordDelete`, a slug / kept_for rename, `tableMove` and removal of the mark. * This door takes the table's slug TYPED by the person and a reason, refuses an agent, and then * archives through `custom.table_archive` (same progress answer; the caller loops until `done`). * Use `archiveTypedTableDeliberately` (`@ai-matrx/records/typed-table`), which loops. */ tableArchiveDeliberately(args: { table_id: Uuid; typed_slug: string; reason: string; }): Promise>; /** * BRINGING A WHOLE TABLE BACK — ONE PASS OF IT (`custom.table_restore`, lane TABLE-ACTIONS). * * 🚨 NOT `recordRestore` FOR A TABLE: that door brings the Table, its structure, every record * and everything built on it back in one statement and dies on `57014` (the clone's "Referral * Intake Queue 2026-Q4 (copy 7)"). This pass brings back the Table and its structure first, then * at most `chunk` records, then what was built on it — all from the one archive event — and * answers `done` when nothing the archive took is still waiting. Use {@link tableRestoreWhole}. */ tableRestore(args: { table_id: Uuid; chunk?: number; }): Promise>; /** * THE WHOLE RESTORE OF A TABLE — the loop every caller needs: `tableRestore` until `done`, * halving the pass on a statement timeout (a pass that timed out changed nothing). `onPass` * hears every pass's numbers. Answers the last pass, or the refusal that stopped it (what the * passes before it brought back stays back; the next call carries on from there). */ tableRestoreWhole(args: { table_id: Uuid; onPass?: (progress: TableRestoreProgress) => void; }): Promise>; /** * DUPLICATE A TABLE — `custom.table_duplicate` (lane TABLE-ACTIONS). * * Makes a NEW Table from one the caller may open: its settings and look, every live column (new * ids), every choice list as a new list (same words, colors, keys and order), every saved view * (filters, sorts, layouts, every id inside them pointing at the copy), and with `withRecords` * every live record (a link to another row of the same table follows to the copy's row; a link * to another table stays). `name` defaults to " (copy)", "(copy 2)", …; * `organizationId` defaults to the source's organization and must be one the caller belongs to. * The source is never written. The copy is made in passes of a few seconds (any size; the * store answers "copying" until "done"), and this call loops them. Refused before anything is * made, the refusal carries the store's one sentence (not given / archived / not a member there * / a column linking across organizations). */ tableDuplicate(args: { tableId: Uuid; withRecords?: boolean; name?: string | null; organizationId?: Uuid | null; /** Called after every pass with what is copied so far (a progress bar is made of it). */ onProgress?: (progress: TableDuplicateProgress) => void; }): Promise>; /** * FINISH A BASE COPY — `custom.base_copy_finish` (lane AT-BASE). After every table of a base was copied by `tableDuplicate`, * points the copies at each other: `pairs` is {source table id: its copy's id}. Re-points each link column's target and each * link cell whose row was copied too; links to a table outside the base stay. Only tables this person copied are touched. */ baseCopyFinish(args: { organizationId: Uuid; pairs: Record; }): Promise>; /** * CARRY A COPY'S EXTRAS — `custom.copy_table_extras` (lane COPY-EXTRAS). `pairs` is {source table id: its copy's id}. Every automation of a * source is declared on its copy SWITCHED OFF and every page too, each column and view id pointed at the copy's own. The step's own * refusals (one sentence each) come back in `extras_refused`; the base-move job in aidream calls the same function. */ copyTableExtras(args: { organizationId: Uuid; pairs: Record; }): Promise>; /** * CARRY ON A COPY — `custom.table_duplicate_continue` (lane TABLE-ACTIONS). The copy is made in * passes of a few seconds each; `tableDuplicate` loops them, and this resumes one that a closed * tab or a lost connection left unfinished (the copy is kept out of every table list until it * is done). Only the person who started it; it answers the finished copy, or refuses. */ tableDuplicateContinue(args: { copyId: Uuid; onProgress?: (progress: TableDuplicateProgress) => void; }): Promise>; /** * THE CALLER'S UNFINISHED COPIES — `custom.table_copies_in_progress` (lane TABLE-ACTIONS). Each * is still being made (carry it on with `tableDuplicateContinue`) or stopped because its source * was archived meanwhile (discard it: `tableArchive` on the copy). Nobody else can see them. */ tableCopiesInProgress(): Promise>; recordRead(args: { record_id: Uuid; withComputed?: boolean; /** Also answer `version` (read in the same round, so the read takes as long as the slower of the two). */ withVersion?: boolean; organization_id?: Uuid | null; }): Promise>; /** * DOOR-1. A page of one Table's records THROUGH `custom.read_records`: the * store decides which rows this reader may see and which fields of them, and * a field it masked comes back present-and-null with its reason in `hidden`. * `total` is null because the door counts nothing it has not read — a count * of rows a person may not see is a leak with a number on it. */ list(args: { table_id: Uuid; /** * THE STORE'S ONE FILTER SHAPE (`RecordFilter`). Left out, this is the * whole page. Supplied, the store narrows INSIDE its own query — never the * browser, which can only ever filter the rows it was already allowed to * see and would silently drop the rest of the answer. * * It is the same object a saved view stores as `filters` and the same one * `recordAggregate` counts with, so the rows a drill-through opens are the * rows the number on the dashboard counted. */ filter?: StoreFilter; limit?: number; offset?: number; }): Promise>; /** * DOOR-SPEED. ONE PAGE THE STORE SORTED, SEARCHED, FILTERED AND COUNTED, through * `custom.read_records_page` — the same wall, ladder, field mask and filter as `list`, with the * order (`sort`, or a hand-ordered view's positions via `view_id`), a text `search` over the * columns this reader may see (and the words of choice options), and the `total` of the * matching visible set. It is what a grid pages with: nothing reads the whole table to draw one * page. A sort on a column this reader cannot see (gone, or masked) is SKIPPED and named in * `sort_ignored` — the page still loads; one worked out on every read is refused by name. */ listPage(args: { table_id: Uuid; filter?: StoreFilter; search?: string | null; sort?: readonly RecordPageSort[]; view_id?: Uuid | null; limit?: number; offset?: number; }): Promise>; /** * AP3-KEYSET-DOOR. THE SAME PAGE AS `listPage`, PAGED BY A CURSOR, through * `custom.read_records_keyset`: the same wall, ladder, field mask, filter, search and sort (no * hand-ordered view), the same rows in the same order — but the next page starts after the last * row's sort values and id, so a write landing mid-read never skips or repeats a row. Pass back * `cursor` as `after` (it is opaque; `null` on the last page). A cursor handed to a different * sort, filter or search is refused by name ("start from the first page"), never reinterpreted. * * Every row carries the `version` and `organization_id` it was read at, in the same statement as * its values. With `ids`, it re-reads exactly those rows among the ones the reader may see (in the * page's order; `after` ignored, `cursor` and `total` null) — get-by-id, the re-read after a write, * and the changed ids of a realtime notice. */ listKeyset(args: { table_id: Uuid; filter?: StoreFilter; search?: string | null; sort?: readonly RecordPageSort[]; after?: string | null; ids?: readonly Uuid[]; limit?: number; }): Promise>; /** * DOOR-1 FOR AN ID SET. Exactly these records of this Table, through * `custom.read_records_by_ids` — the same ladder, the same level, the same * field masking and the same page ceiling as `list`, addressed by id instead * of by page. An id the reader may not see simply does not come back, which * is the same answer `list` gives by omission. * * It exists because a realtime notice has always named the exact ids that * moved and no door could take them: three cells changing re-read a fifty-row * page in every watching browser. */ listByIds(args: { table_id: Uuid; ids: readonly Uuid[]; }): Promise>; /** * DOOR-1 FOR THE ROWS THAT LEFT: a page of one Table's ARCHIVED records, * through `custom.read_records_archived` — the same ladder, the same level, * the same field masking and the same page ceiling as `list`, with the sign * of the `deleted_at` test flipped, plus who archived each one and when. * * 🚨 IT EXISTS BECAUSE ARCHIVING WAS A ONE-WAY DOOR (VERIFIER-10 F4). The * store has always had `record_restore`, and it has always done exactly the * right thing; what it did not have was any door that could LIST the records * it would act on, because every read door answers live rows only. So an * archived record was unreachable from every screen in the platform. * * `lane` is the archived-items law's two lanes — "mine" (what I archived) and * "org" (what anybody in this organization archived), which is the default. */ listArchived(args: { table_id: Uuid; lane?: ArchiveLane; limit?: number; offset?: number; }): Promise>; query(query: RecordQuery): Promise>; /** * Every Table the person can see across ALL their organizations * (`config.organizationIds`); `organization_id` is the page's optional filter * (absent = all). Each Table carries its own `organization_id`. */ tableList(args?: { organization_id?: Uuid | null; }): Promise>; /** * ONE Table's declaration by id — `null` when this reader may not see it or it is not a Table. * The same kernel record `tableList` lists, read through `read_records_by_ids` for exactly * that id, so a screen that opens one table never lists every table to find it (a list capped * at 500 answered "no table" past the 500th). */ tableRead(args: { table_id: Uuid; organization_id?: Uuid | null; }): Promise>; /** * ONE Table of THIS organization found by its slug — or, with no slug, by its name — in ONE * request (`custom.table_find`). `null` when there is none or this reader may not see it: the * store reads the candidates through the same read door `tableRead` uses, so a table she may * not see answers exactly like one nobody made. Where several share a slug, the oldest she may * see answers, every time (the same pick `ensureTable` makes). Never list every table to find one. * * `keptFor` (an app's table): the slug AND what the table is kept for must match — a person's own * table of the same slug is a different table and never answers. With `keptFor` and no live * match, an ARCHIVED match is answered as `{ id, organization_id, archived_at }` (no document), so * a caller says "archived — restore it", never "missing". */ tableFind(args: ({ slug: string; name?: never; } | { name: string; slug?: never; }) & { keptFor?: string; }): Promise>; /** * `tableFind` BY SLUG FOR MANY ORGANIZATIONS IN ONE REQUEST (`custom.table_find_many`). Each * organization meets the same walls `tableFind` does and is answered through `custom.table_find` * itself. `tables` holds one live Table per organization that has one this reader may see (an * organization with none is absent); `refused` holds each organization the store refused, with * its own refusal, and never stops the others. Never loop `tableFind` over organizations. */ tableFindMany(args: { organizationIds: Uuid[]; slug: string; keptFor?: string; }): Promise; }>>; /** * IS THIS TABLE IN THE ARCHIVE? (lane HUB-FIX, 2026-09-23.) The archived row for one * Table of this organization, or `null` when the archive does not hold it — which * is an ANSWER, never "it may have been deleted". A table page that cannot find a * Table asks this before saying anything: an archived Table's own address used to * read "…or it may have been deleted" when it was archived and one click from back. */ tableArchived(args: { table_id: Uuid; }): Promise>; /** * WHO CAN SEE EACH TABLE, AND WHETHER THE CALLER MADE IT — `custom.table_facts` * (lane HUB-FIX). `visibility` and `created_by` are record COLUMNS the Table document * never carried, so a screen that sorts Tables into lanes asks this and folds the * answer onto `tableList()`'s Tables with `withTableFacts`. * * 🚨 IT IS ITS OWN CALL, NOT PART OF `tableList()` (0.57.3). 0.57.2 folded it into * every table list, so every screen that merely LISTS tables (the field editor's * relation target, the import wizard, settings) paid a second round trip it never * used — and every test double that answers a table list had to answer it too * (records-ui 0.84.5's gate: 47 tests in 15 files red). Only the lane-sorting * screens ask. * * The door-presence pattern: when the store's catalog does not carry the door the * answer is `null` — nothing asked, nothing invented. */ tableFacts(args?: { organization_id?: Uuid | null; }): Promise>; /** * WHICH STORE CURRENTLY WRITES each table — `custom.where_tables_live`. * * This is a batch cutover decision, not a Table's home address: during COPY * mode an older table and its record-store copy share an id, and only this * door knows which one the organization is still using. It is intentionally * unbound from this client's organization because the door derives the answer * from each supplied table and the caller's authority. */ tablesLiveWhere(args: { table_ids: Uuid[]; }): Promise>; /** * WHICH ORGANIZATION A TABLE LIVES IN, BY NAME, AND WHERE IT MAY GO — `custom.table_home` * (SC-1' / SC-1-TAILS). Read FROM THE TABLE, never from the organization the client is bound * to, so it answers for a table shared from outside too. `null` = not given to this person, * or not there (the store says the same for both, on purpose). A store without the door * refuses `door_absent` — never a guessed organization. */ tableHome(args: { table_id: Uuid; }): Promise>; /** * MOVE A TABLE into another organization the caller belongs to — `custom.table_move`. It * carries every Table inside it or its rows and writes the change events; refused, NOTHING * moved and the refusal carries the store's one sentence. `expected_version` is the Table's * version the caller decided on (a concurrent change refuses with "look again"). */ tableMove(args: { table_id: Uuid; to_organization_id: Uuid; expected_version?: number | null; }): Promise>; /** * MOVE A WHOLE BASE into another workspace (organization) the caller belongs to — `custom.base_move` (AT-NOTIFY-MOVE). * Admin on the base's Home. Every table of the Home goes through the one table door (with its pages and automations), the * Home and its base row follow, and the tables stay under that Home over there. All or nothing: a refusal moved nothing and * carries the store's sentence (which table it stopped at, or that the caller is not in that workspace). */ baseMove(args: { home_id: Uuid; to_organization_id: Uuid; }): Promise>; /** * The Fields that apply. NOTION-PROPS: a two-way link's reverse Field (`isReverseField`) is left out * unless `withReverse` — every screen draws it as the table's reverse column (`reverseColumns`, with * `read_only: false`), which reads and writes its links; listed as a Field too it would draw twice. */ fields(args: { table_id: Uuid; recordType?: string; withReverse?: boolean; }): Promise>; /** * FLD-5 / FLD-6: a list field stores the id of an option RECORD, because * every pick-list is already a Table. These are that Table's records — the * store resolves which Table from the Field's own config, so no caller has to * know it. */ fieldOptions(args: { field_id: Uuid; }): Promise>; /** * How many records hold each choice of one choice column, keyed by the option's id * (`custom.field_choice_usage`; counts only records the caller may see). What a column editor * says before a choice that records still hold is removed: "3 records use “X-ray”." */ fieldChoiceUsage(args: { field_id: Uuid; }): Promise>>; /** * A column's choices saved AND the records of each removed choice moved to another choice, * kept as other values, or cleared — ONE transaction (`custom.field_update_rehoming_choices`). * `rehome` is keyed by the removed option's id. The answer carries `undo`; sending * `{ patch: { options: undo.options }, cells_back: undo.cells_back }` back through this same * call puts the list and every cell back, again in one transaction. */ fieldUpdateRehomingChoices(args: { field_id: Uuid; patch: FieldPatch; rehome?: Record | undefined; cells_back?: readonly ChoiceCellBack[] | undefined; }): Promise>; tableCapacity(args: { table_id: Uuid; }): Promise>; /** * REC-1: a Table has to live somewhere, so a Home comes first — and a Home is * an ordinary record in the person kernel. `homeId` is therefore required and * `personKernelId()` is how a caller gets a Table to write one into. */ tableDeclare(args: { spec: Record; homeId: Uuid; }): Promise>; /** * FIND THIS TABLE BY ITS SLUG, OR MAKE IT — ONE call, ONE transaction (`custom.table_ensure`). * The store holds a lock on (organization, slug) across the find and the make, so two tabs * saving their first record at once get the SAME table; a second call adds any column the spec * names that the table lacks and never retypes or drops one. `spec.fields` are full * `custom.field_declare` specs. Prefer `ensureTable(client, NewTableSpec)`, which builds them. * * `drift` names every column the table already has whose stored shape differs from the spec * (type, format, label, multi, required, unique) — reported, never applied. `[]` when nothing * differs; `null` only from a store too old to compare (see `FieldDrift`). */ tableEnsure(args: { spec: Record; }): Promise>; /** * ADD A FIELD TO A TABLE. One call, and the store does the three things no * client can do for itself: it turns a parity type into the behavior, * modifiers, format, unit, relation target and Rules its own guards demand; * it tells the TABLE about the field (REC-1 / FLD-8 — a definition for a * field the table never declared is refused, which is the sentence every * "Add field" used to end on); and it stores the definition as a FIELD * rather than as a plain record, so the delete rules and the * formula-dependency check can see it. * * A choice list carries its `options` as words. The store keeps them the only * way FLD-5 / FLD-6 allows — as the records of a Table it declares for them — * so nobody is made to build a table before they can have a dropdown. */ fieldDeclare(args: { table_id: Uuid; spec: NewFieldDeclaration; }): Promise>; /** Several columns of one Table in ONE call and one transaction (`custom.field_declare_many`): all or none. */ fieldDeclareMany(args: { table_id: Uuid; specs: NewFieldDeclaration[]; }): Promise>; /** * A NEW TABLE IN ONE CALL (`custom.table_create`): its Home, the Table under it and its columns, in * one transaction, so a refusal anywhere leaves nothing behind. Prefer `declareTable(client, spec)`. */ tableCreate(args: { spec: Record; fields: NewFieldDeclaration[]; }): Promise>; /** One field's own settings. What it HOLDS is a conversion and is refused here by name. */ fieldUpdate(args: { field_id: Uuid; patch: FieldPatch; }): Promise>; /** * WHAT A CHANGE OF KIND WILL DO, before it is made (`custom.field_retype_preview`): the store runs the * same `field_update` patch inside a subtransaction, counts what converts, what is kept aside and the * choices it makes, and rolls every row back. Nothing is written. */ fieldRetypePreview(args: { field_id: Uuid; patch: FieldPatch; }): Promise>; /** * CHANGE WHAT A COLUMN HOLDS, through `custom.migrate_retype` (FLD-4 / T12). `to` is the store's * behavior word — `text`, `range` (numbers and dates), `boolean`, `list`, `relation`. Every value * that converts is converted; every value that does not is kept in that record's `_retired` * with the reason, never coerced and never deleted. The answer counts both. */ migrateRetype(args: { field_id: Uuid; to: string; note?: string; }): Promise>; /** * MERGE ONE RECORD INTO ANOTHER, through `custom.migrate_merge` (T5 / REC-21). The loser's values * the winner did not hold move across, the ones that differ are kept as ranked alternates, every * link to the loser now points at the winner, and the loser is archived with its id resolving to * the winner. Editor on BOTH records is decided first. The answer's `migration_id` is what * `migrateUndo` takes. */ migrateMerge(args: { winner_id: Uuid; loser_id: Uuid; note?: string | null; }): Promise>; /** Put a recorded Migration back (`custom.migrate_undo`): a retype or a merge, by its id. */ migrateUndo(args: { migration_id: Uuid; }): Promise>; /** * SPLIT ONE RECORD IN TWO, through `custom.migrate_split` (Rule 11 / REC-22). The named values * MOVE to a new record of the same Table; the original keeps its id, so every link to it still * resolves. Editor on the record is decided first. `migrateUndo(migration_id)` puts it back. */ migrateSplit(args: { record_id: Uuid; moved_keys: string[]; note?: string | null; }): Promise>; /** * EXTRACT A PARENT, through `custom.migrate_extract_parent` (Rule 11 / test 5). The named values * are COPIED up into a new record of `parent_table_id`, and this record is put under it. Its old * parent, if any, becomes the new parent's parent. `migrateUndo(migration_id)` puts it back. */ migrateExtractParent(args: { record_id: Uuid; parent_table_id: Uuid; moved_keys: string[]; note?: string | null; }): Promise>; /** * MOVE A RECORD TO ANOTHER TABLE, through `custom.migrate_retype`'s record arm (test 9 / REC-N-18). The * record keeps its id — every link to it still resolves — and its values whose column `to_table_id` lacks * are taken off and kept in History with the reason (`misfits`). Full (admin) on the record is decided * first; the table must be in the same organization. `migrateUndo(migration_id)` puts it back whole. */ migrateMoveRecord(args: { record_id: Uuid; to_table_id: Uuid; note?: string | null; }): Promise>; /** The Migration log for one record or Table (`custom.migrations`), newest first. */ migrations(args: { target_id?: Uuid | null; limit?: number; }): Promise>; /** * HOW LONG HISTORY IS KEPT (Rule 10 / test 14), through `custom.history_retention`. `floor_days` is the * organization's floor (knob `extensibility.user_tables.history_retention_floor_days`: 30 by default, an * organization may raise it, never lower it); `table_days` is the table's own, never below the floor. */ historyRetention(args?: { table_id?: Uuid | null; }): Promise>; /** Set a table's value-history retention (`custom.history_retention_set`); below the floor is refused in the store's words. */ historyRetentionSet(args: { table_id: Uuid; days: number; }): Promise>; /** Raise this organization's retention floor (`custom.history_retention_floor_raise`); a lower number is refused. */ historyRetentionFloorRaise(args: { days: number; }): Promise>; /** * ONE RECORD ON EITHER CLOCK, through `custom.query_record_as_of` (Rule 10 / test 6). * `recorded_at` answers what the store SAID at that moment; `world_on` answers what was TRUE of * the world on that date, read from `dated` values. Both may be given. `null` data means the * record is not visible to this person (or did not exist then) — an absence, not an empty record. */ recordAsOf(args: { record_id: Uuid; recorded_at?: string | null; world_on?: string | null; }): Promise | null>>; /** * REL-DISP. THE WORDS ONE RELATION CELL SHOWS, resolved by the store through * that column's own display spec — one column or several, joined by its * separator. Never resolve a relation label any other way: the store's * `custom._words_for` is the one body that decides what a record is called, it * asks the read-access ladder BEFORE it reads the name, and a record this reader * may not open comes back as the withheld sentence rather than its name or id. * A value that is not an id is handed straight back — it is already words. */ relationWords(args: { field_id: Uuid; value: string; }): Promise>; /** * The same answer for a WHOLE PAGE of cells in one round trip. Prefer this * wherever a grid, board, list or picker resolves more than one chip: the * per-id lane is what made a page of relation cells a page of requests. */ relationWordsMany(args: RelationWordsManyArgs): Promise> & { archived?: readonly string[]; icons?: Record; }>; /** * VISION-REACH G1. Which records of `table_id` each name names — its name column, case and * spacing ignored, only records this reader may see. Hand the answer to `relationNameIndexOf` * and pass that to the coercion as `relationNames`. Up to 5,000 names a call; more are sent in * groups by this method. */ relationNamesMatch(args: { table_id: Uuid; names: string[]; }): Promise>; /** * VIEWS-AND-FIELDS BL (REL-9). The REVERSE COLUMNS of a Table: one per relation column of another * table (or this one) that links here, read-only, virtual — never a Field of this Table, so * `applicableFields` does not list them. A linking table this reader may not know is left out. */ reverseColumns(args: { table_id: Uuid; }): Promise>; /** * A page of records' links through ONE reverse column, in one round trip per 200 records: * `{ [record id]: { total, links: [{ id, words }] } }`, only linking records this reader may see. * `limit` (1–50) and `offset` page one record's links — a record with thousands of links costs * one count and one page. A record with no links answers `{ total: 0, links: [] }`. */ reverseLinksMany(args: ReverseLinksManyArgs): Promise>>; /** * The same, through SEVERAL reverse columns in ONE call (`custom.reverse_links_many_fields`): * `{ [source field id]: { [record id]: { total, links } } }`. A store without the door answers * `door_absent` — ask `reverseLinksMany` per column then. */ reverseLinksFields(args: ReverseLinksFieldsArgs): Promise>>>; /** * NOTION-PROPS. A TWO-WAY LINK: makes (or returns) the reverse Field of relation `field_id` on its * target table (`custom.relation_reverse_field`), so the link is a column there that edits the same * links. Its key is the reverse column's key; `label` defaults to the linking table's name. */ relationReverseField(args: { field_id: Uuid; label?: string; }): Promise>; /** * NOTION-PROPS. Adds and removes links from the reverse side of a two-way link * (`custom.relation_reverse_set`): each id becomes a write of THAT record's forward Field. * Answers how many records changed. */ relationReverseSet(args: { record_id: Uuid; field_id: Uuid; add?: readonly Uuid[]; remove?: readonly Uuid[]; }): Promise>; /** Take a field off a table. Refuses the title field, the last field, and one another field reads through. */ fieldRetire(args: { field_id: Uuid; }): Promise>; /** * Bring a removed column back (`custom.field_restore`): the set that was retired with it, the * Table's declaration of those columns, and the links they made. The values never left. */ fieldRestore(args: { field_id: Uuid; }): Promise>; /** * DUPLICATE PROPERTY (Notion N5) — `custom.field_duplicate`: a new column right after this one, named * " (1)", the same kind (choices with their colors, unit, link, formula, rules, sensitivity), and * every record's value copied into it in one statement. A link column copies its definition only (its * values are links); a formula, lookup or roll-up works its answer out itself. Needs the table's * add-a-column rung, the same as `fieldDeclare`. */ fieldDuplicate(args: { field_id: Uuid; }): Promise>; /** * CHAIR-ROLLUP-HONEST. Is a column this lookup or roll-up reads archived — the link it reads * through, a back-link's linking column, or the far column it totals (`custom.field_source_archived`)? * `null` while every source is live. Bring it back with `fieldRestore({ field_id })`. */ fieldSourceArchived(args: { field_id: Uuid; }): Promise>; /** * A TABLE AS A KIND (KINDS-GLUE wave 3, N1): `custom.table_kind_facts` verbatim — the Table's * live Fields, choices and stamp, as the asking person. The door finds the Table's organization * itself, so this works for a Table in any organization the person may open; anything else is * refused (`42501`, the store's sentence). */ tableKindFacts(args: { table_id: Uuid; }): Promise>; /** * A TABLE'S ACCESS LEVEL (CONFIDENTIAL-BY-ORG-ADMIN): `custom.table_level_facts` verbatim - its level, * readers, who approves a change (`set_by`) and whether THIS person may (`can_set`). Draw the control only * when `can_set` is true; anyone who may know the table may ask. */ tableLevelFacts(args: { table_id: Uuid; }): Promise>; /** * MAKE A CUSTOM TABLE CONFIDENTIAL, or change whom it names (`custom.set_table_confidential`). Succeeds * only for an owner/admin of the table's own organization on an ordinary custom table; the approver, seat, * reason and date are recorded. `readers` is the list of `{field, level}` rules (a person field of the * table); `reason` is required. */ setTableConfidential(args: { table_id: Uuid; readers: TableReaderRule[]; reason: string; }): Promise>; /** THE WAY BACK: the same caller rule, the table is Organization again (`custom.set_table_organization`). */ setTableOrganization(args: { table_id: Uuid; reason: string; }): Promise>; /** * THE COLUMNS THIS TABLE HAD AND NO LONGER HAS (grids review 3): its retired Field records, * newest first, through the same archived door the Table archive uses — addressed at the Field * kernel (every Field is a record in it) and paged until the door answers short. */ fieldsRetired(args: { table_id: Uuid; }): Promise>; /** What the registry says about a token, and whether it can hold a value at all. */ entityTable(args: { token: string; }): Promise>; /** * The fields THIS organization added to that standard table. `include_retired: true` also answers the * ones taken away (`entityFieldRetire`), live first, each with `archived_at` set — the archived view a * screen offers, with `fieldRestore({ field_id })` to bring one back. Live fields carry `archived_at: null`. */ entityFields(args: { token: string; include_retired?: boolean; }): Promise>; /** The fields EVERY named organization added to that standard table, with their choices, in one read. */ entityFieldsAcross(args: { token: string; organization_ids: Uuid[]; }): Promise>; /** Add a column to a standard table. An organization admin's right - it changes the table for everybody. */ entityFieldDeclare(args: { token: string; spec: NewEntityFieldDeclaration; }): Promise>; /** One field's own settings. A TYPE change is refused here by name, as it is on a custom table. */ entityFieldUpdate(args: { field_id: Uuid; patch: Record; }): Promise>; /** Take a custom field off a standard table. The values already written stay in each row. */ entityFieldRetire(args: { field_id: Uuid; }): Promise>; /** May this person add a column here - and if not, the store's own sentence saying why. */ entityFieldRights(args: { token: string; }): Promise>; /** REC-53 in one answer: the row's real columns AND its custom values AND the definitions. */ entityRecordRead(args: { token: string; record_id: Uuid; }): Promise>; /** * AP-4: the custom rows that link to one platform record, by which field and in which table. * Keyed by the ROW's organization (`organization_id`), never the client's active one. Paged: * pass the previous answer's `next_cursor` as `cursor`. */ entityBackLinks(args: { token: string; record_id: Uuid; organization_id: Uuid; limit?: number; cursor?: string | null; }): Promise>; /** * THE one write door for a standard row (`custom.entity_row_write`, as the person): custom values * as a patch (a key left out is left alone, a key set to null is cleared with its envelope), * archive / restore, and real columns only where the store lists them. The row's own organization * governs; `expected_version` refuses a stale write; a change that changes nothing writes nothing. */ entityRowWrite(args: { token: string; /** * null ADDS a row (chairblk3_a): only for a token whose API facts say `create_via: "insert"` * (`drill_describe(...).api.create_via` — task and project), as one insert AS THE PERSON in * `organization_id` (default: this client's organization); the answer carries `created: true` * and the new `id`. Every other token refuses in its own words. */ record_id: Uuid | null; /** A change by id: only to say which organization the row must belong to. A new row: where it goes. */ organization_id?: Uuid | null; custom?: Record; columns?: Record; expected_version?: number | null; archive?: boolean | null; }): Promise>; /** Rows of that standard table with this custom value. Refuses a field nobody declared. */ entityRecordsFind(args: { token: string; key: string; value?: unknown; limit?: number; offset?: number; }): Promise>; /** The kernel Tables a caller writes a Home, a Field or a Rule into. */ personKernelId(): Promise>; fieldKernelId(): Promise>; ruleKernelId(): Promise>; /** Every record the Rule says is a member. A saved view's list is this, not a filter a screen ran. */ /** PRODUCTS row 7 — the whole board: its stage column, its stages in the declared * order, and every Rule WITH the sentence it says when it refuses. */ pipelineRead(args: { table_id: Uuid; }): Promise>; /** The store's own count and total per column, over what this person may see. */ pipelineBoard(args: { table_id: Uuid; measure?: string | null; /** * S2-PRIME — the view's question, either shape (`StoreFilter`). Given, the headings count and * total exactly the cards the same filter puts on the board (`custom.pipeline_board`'s * four-argument door); left out, the whole table this person may see, as before. */ filter?: StoreFilter | null; }): Promise>; /** Whether this card may go to that column — asked BEFORE the finger lets go. */ pipelineTransitionRefusal(args: { record_id: Uuid; to: string; }): Promise>; /** * Move a record to a stage. An ORDINARY record write, judged by the same Rules. * * It answers an OBJECT on every path. A gate whose `on_fail` is `require_approval` * comes back `applied: false` with the wait it filed — the record has NOT moved, and a * screen that treated the call's success as the move's success would show the card in * the wrong column while somebody is still deciding. */ pipelineMove(args: { record_id: Uuid; to: string; also?: Record; expected_version?: number | null; }): Promise>; /** * MOVE MANY CARDS IN ONE CALL (lane S4, `custom.pipeline_move_many`). Each card is judged by * `pipeline_move` exactly as one is, in its own savepoint: a refused card writes nothing and the * others still land. `expected_version` is required per card — a card that changed since the * board loaded answers `changed_since_loaded` and is never overwritten. A refusal of the WHOLE * call (a stranger, an empty batch, more than the `custom/batch_items_max` knob) is the error. */ pipelineMoveMany(args: { moves: PipelineMoveManyItem[]; }): Promise>; /** Declare (or re-state) a pipeline: the stage column and its transition Rules. */ pipelineDeclare(args: { table_id: Uuid; spec: PipelineSpec; }): Promise>; /** Which column of this table holds its stage, or null when it is not a board. */ tableStageField(args: { table_id: Uuid; }): Promise>; /** The closed list of what a gate does when it says no — the store is the authority. */ stageRuleOnFailKinds(): Promise>; /** Whether this organization's gates refuse or warn. One opinion, asked once. */ stageRuleEnforcement(): Promise>; /** What is waiting on this card: approvals filed against its own stage column. */ recordStagePending(args: { record_id: Uuid; }): Promise>; /** The same question for a whole board, in ONE call — what a Kanban badges cards from. */ pipelinePending(args: { table_id: Uuid; }): Promise>; /** * WHAT THIS GATE WOULD DO TO THIS BOARD TODAY — for a gate that has not been saved. * * The settings screen calls it on every edit of the condition, which is why it takes * the gate rather than a rule id: no saved-rule door can answer about a rule that does * not exist yet. The store compiles it with the same compiler `pipelineDeclare` uses, * so the number here and the refusal at the door are the same rule — a preview with its * own compiler would be wrong exactly at the edge of the condition, which is the only * place anybody looks. * * A gate that does not say what it demands is REFUSED, not answered zero: "no cards * would be stopped" beside a half-written rule is the sentence that gets it saved. */ pipelineGatePreview(args: { table_id: Uuid; stage: string; gate: PipelineGate | Record; }): Promise>; /** * SERVER LANE ONLY. `custom.rule_members` walks the whole scope table through the evaluator and * returns STORED rows — no read-access ladder, no masking — so no browser role holds EXECUTE on it * and it answers 42501 from a signed-in seat. A screen reads a Rule's members through * `ruleMembersVisible`. */ ruleMembers(args: { rule_id: Uuid; }): Promise>; /** * S2-PRIME — THE MEMBERS OF A MEMBERSHIP RULE THIS PERSON MAY SEE, a page at a time * (`custom.rule_members_visible`): the list door asked the Rule's own test, so the rows are the * read door's — the same ladder, masked documents, the same page ceiling. */ ruleMembersVisible(args: { rule_id: Uuid; limit?: number; offset?: number; }): Promise>; /** Is this one record a member, and WHY — the store's own answer. */ ruleMembership(args: { rule_id: Uuid; record_id: Uuid; }): Promise>; /** Run a stored Rule against a document. */ ruleRun(args: { rule_id: Uuid; values: Record; context?: Record; }): Promise>; /** Evaluate a Rule expression that has not been stored — the conditional logic a form runner asks about. */ ruleEval(args: { expr: unknown; values: Record; context?: Record; }): Promise>; /** * Write ONE Rule as a real Rule. `recordWrite` into the Rule kernel makes a * valid Rule document that carries `data_class = 'record'`, and every reader * that looks for a Rule looks for the class — `custom.agg_subscriptions` * selects `where data_class = 'rule'` — so a subscription written that way * fires at nobody. This is the door that closes it. */ /** This organization's forms, with how many answers arrived and how many are in the table. */ forms(args?: { table_id?: Uuid | null; }): Promise>; /** * THIS ORGANIZATION'S BOOKING PAGES (PRODUCTS row 14), with the numbers that make * the list worth opening: how many are booked, canceled, still ahead, and how many * slots somebody is holding right now. A booking page is an `anon_form`, so it is * opened and closed with `anonPublish` exactly as a form is. */ bookings(args?: { table_id?: Uuid | null; }): Promise>; /** * CREATE OR RE-STATE ONE BOOKING PAGE over one Table. `admin` on the Table, * because a booking page decides what a stranger may write into it and which * hours of the organization's week are on offer. * * `slot`, `status` and `booked_with` are the store's to fill in: a question * naming any of the three is refused BY NAME, so a page cannot be built that * lets a visitor choose their own status. It does NOT publish — `anonPublish` * is the one act that opens a public surface, for a booking page exactly as * for a form. * * Until 2026-09-21 this door had no client method, so the ONLY way to a * booking page was to ask an agent for one. The agent path is the faster one, * never the only one. */ bookingDeclare(args: { table_id: Uuid; title: string; questions: unknown[]; availability?: Partial | null; presentation?: Record | null; submission_cap?: number | null; quarantine_rule_id?: Uuid | null; notify_rule_id?: Uuid | null; form_id?: Uuid | null; slug?: string | null; home_id?: Uuid | null; }): Promise>; /** Create or re-state one form. A question naming no Field of that Table is refused BY NAME. */ formDeclare(args: { table_id: Uuid; title: string; questions: unknown[]; presentation?: Record | null; submission_cap?: number | null; quarantine_rule_id?: Uuid | null; notify_rule_id?: Uuid | null; form_id?: Uuid | null; slug?: string | null; }): Promise>; /** * WHICH OF A FORM'S QUESTIONS ARE ASKED, given the answers so far — for the OWNER's preview of * a draft (`custom.form_preview_asks`). One row per question in order; `asked` is false only * when the store's own Rule said so. Undecided and unworkable conditions are asked, the second * with the store's sentence in `said`. */ formPreviewAsks(args: { table_id: Uuid; questions: unknown[]; values: Record; }): Promise>>; /** * WHAT A FORM'S LOOK MAY BE, AND WHAT IT WOULD LOOK LIKE (`custom.form_look_options`): the * organization's public pictures (the same list a portal's look offers), the colors, and the * look a stranger would see for the draft `presentation` — the builder's preview draws that. */ formLookOptions(args?: { presentation?: Record | null; }): Promise>; /** * THE ROUTE THROUGH A DRAFT FORM (`custom.form_preview_route`, lane TYPEFORM-DUP): which questions * are asked given the answers so far (showIf AND logic jumps), the ending reached and the score — * the store's walk, the one the stranger's page and `custom.form_submit` read too. */ formPreviewRoute(args: { table_id: Uuid; questions: unknown[]; endings?: unknown[] | null; values: Record; }): Promise>; /** A form's results (`custom.form_results`): views, starts, completions, drop-off, answers. */ formResults(args: { form_id: Uuid; }): Promise>; /** TYPEFORM-2: the organization's form knobs (`custom.form_knobs`): owner header, choice auto-advance. */ formKnobs(): Promise>; /** * CD-PAGE-08: a public form's password (`custom.form_set_password`). A string of 4 to 128 characters sets it, * `""` clears it, `null` changes nothing; every call answers whether the form has one now. */ formSetPassword(args: { form_id: Uuid; password: string | null; }): Promise>; /** * What the store makes of a file's columns, before anything is written. * `columns` is `[{header, samples}]` — the first page of the file is plenty. */ importPlan(args: { table_id: Uuid; columns: Array<{ header: string; samples: unknown[]; }>; }): Promise>; /** * Open a run. `file_hash` is the run's IDENTITY: the same bytes into the same * table a second time answer the FIRST run with `already: true` and write * nothing, so a re-import doubles nothing. `force` re-opens deliberately. */ importBegin(args: { table_id: Uuid; format?: "csv" | "xlsx"; source_name?: string | null; source_columns?: unknown[]; file_hash?: string | null; policy?: ImportPolicy; /** A Field key. A row whose value for it is already here is a duplicate. */ dedupe_key?: string | null; file_bytes?: number | null; force?: boolean; }): Promise>; /** * IMPORT-2. ADD THE COLUMNS THIS FILE HAS AND THIS TABLE HAS NOT — a step of * its own, BEFORE the run is opened, so the screen can show "8 columns added" * and the person can then name one of them as the file's duplicate key * (`importBegin` refuses a key naming a column that does not exist yet). * Answers the mapping the file should be sent with. Admin on the Table. */ importDeclareColumns(args: { table_id: Uuid; rows: Array>; /** Header -> Field key the person already mapped by hand; never declared. */ mapping?: Record; }): Promise>; /** * One BATCH of rows. The batch is PLANNED and then written as ONE statement * through the batched write door, so 250 round trips became one; a batch a * guard refuses falls back to one write a row, and the answer says which it * was. Send `ImportOpened.rows_per_call` the first time and * `ImportBatchResult.rows_per_call` — measured — every time after that. */ importRows(args: { import_id: Uuid; rows: Array>; /** Header -> Field key. A header that already spells a key needs no entry. */ mapping?: Record; }): Promise>; /** * Close the run and decide the columns the file had and the table did not: * `propose` (the ONE approvals inbox, and the only choice a non-admin has), * `create` (an admin adds them outright) or `ignore`. */ importFinish(args: { import_id: Uuid; unmapped?: "propose" | "create" | "ignore"; }): Promise>; /** The refused rows, or the ones already here, WITH the source row on each. */ importReport(args: { import_id: Uuid; state?: "refused" | "duplicate"; limit?: number; offset?: number; }): Promise>; /** This organization's import runs, newest first. How you find yesterday's. */ imports(args?: { table_id?: Uuid | null; limit?: number; }): Promise>; /** * Forget the RECEIPT for a file so the same bytes can be imported again * deliberately. Never touches the records that import wrote. */ importForget(args: { import_id: Uuid; }): Promise>; /** * Give a table its own address. The answer carries `mail_route`: the one * sentence saying what has to exist in DNS before anything arrives. Show it * wherever the address is shown — an address that silently swallows mail is * the failure this whole door is against. */ inboundDeclare(args: { table_id: Uuid; label?: string | null; }): Promise>; inboundAddresses(args?: { table_id?: Uuid | null; }): Promise>; inboundSet(args: { inbound_id: Uuid; enabled: boolean; }): Promise>; /** Every portal over this organization the caller may know about, with its numbers. */ portals(): Promise>; /** * The SAME list, archive-aware. `custom.portals` keeps its signature and hides * archived portals, because the archived-items law's default is to hide; * this is the one door that also answers the other lane, so a rail gets the * law's one-click reveal without inventing a second control or a second door. * A fourth word is refused `22023` — there is no fourth answer on any surface. */ listPortals(args?: { archived?: PortalArchiveFilter; }): Promise>; /** * PUT ONE PORTAL AWAY. Nothing is deleted and there is no purge: every Table * it exposes and every person invited to it is kept exactly as it is, and * `portalRestore` hands it all back. `confirm_title` is the portal's own title * typed back — ten portals can carry one title, so the title is not what picks * this one and the door refuses `23514` when it does not match. */ portalArchive(args: { portal_id: Uuid; confirm_title: string; reason?: string | null; }): Promise>; /** The way back. Everyone invited signs in exactly as before, and it shows the same Tables. */ portalRestore(args: { portal_id: Uuid; confirm_title: string; }): Promise>; /** * PUT ONE FORM OR BOOKING PAGE AWAY (`custom.form_archive`, admin on its table). Its address stops * answering; everything it collected is kept. `formRestore` brings it back. */ formArchive(args: { form_id: Uuid; }): Promise>; formRestore(args: { form_id: Uuid; }): Promise>; /** The archived forms and booking pages, newest first; `table_id` narrows to one table. */ formsArchived(args?: { table_id?: Uuid | null; limit?: number; }): Promise>; /** * The archived checklist templates. A template is a record: `recordDelete` archives it and * `recordRestore` brings it back. */ checklistTemplatesArchived(args?: { about_table_id?: Uuid | null; limit?: number; }): Promise>; /** * How a table names its records: the row-label FORMULA it keeps (`{First} & " " & {Last}`), the title * column it falls back to, which is in force, and whether the caller may change it. Writes nothing. */ tableRowLabel(args: { table_id: Uuid; }): Promise>; /** * Set the table's row-label formula, or clear it (null / blank). The store parses it against the table's * own columns and refuses an unknown column in a sentence, writing nothing. Answers the new state. */ tableRowLabelSet(args: { table_id: Uuid; formula: string | null; }): Promise>; /** What a table's NEW rows start with (Shown to, and the web half once it lands). */ tableRowDefaults(args: { table_id: Uuid; }): Promise>; /** Set them (the table's maker or an admin of it). A key left out is left alone; null = the default. */ tableRowDefaultsSet(args: { table_id: Uuid; defaults: TableRowDefaultsPatch; }): Promise>; /** * ONE PASS of "apply to existing rows": rows nobody set by hand follow the table's defaults. * `done: false` → call again with `after: next_after`. `rowDefaultsApplyAll` walks every pass. */ tableRowDefaultsApply(args: { table_id: Uuid; after?: Uuid | null; }): Promise>; /** Every pass of `tableRowDefaultsApply`, summed; the last pass's sentence when it took one pass. */ rowDefaultsApplyAll(args: { table_id: Uuid; maxPasses?: number; }): Promise>; /** One row's Shown to (and web half), with who may change it. */ recordRowControls(args: { record_id: Uuid; }): Promise>; recordRowControlsSet(args: { record_id: Uuid; controls: { shown_to?: string | null; published_to_web?: boolean; indexed?: boolean | null; }; }): Promise>; /** * Which Tables each portal of this organization exposes. * * The one question a TABLE-scoped Portals rail has to ask: "is the table I * am standing on in any portal?" Same wall and same read-access gate as * `portals`, so a pair here is always a portal `portals` also returned. */ portalTables(): Promise>; /** ONE portal in full: what it shows, and who it was opened to. */ portalCard(args: { portal_id: Uuid; }): Promise>; /** * Create or RE-STATE one portal. Re-stating REPLACES what it exposes — a * Table left out of `tables` stops being exposed in the same act — and leaves * the people alone: who is invited is not part of what the portal shows. */ portalDeclare(args: { title: string; client_table_id: Uuid; tables: Array<{ table_id: Uuid; names_via: string; visible_fields?: string[]; editable_fields?: string[]; comments?: boolean; }>; portal_id?: Uuid | null; slug?: string | null; sign_in_method?: string | null; /** * S6 — the portal's own look and its forms. A key given REPLACES that key; a key not given * is KEPT (leave `config` out and both stay exactly as they are). The store judges it and * refuses by name: a logo that is not a public picture of this organization's Files, an * accent that is not a design-system color name, a form whose Table the portal does not show. */ config?: PortalConfigInput | null; }): Promise>; /** * S6 — THE CLIENT'S SIDE: one form of HER portal (`custom.portal_form`). Refused unless the * caller is a live principal of that live portal and the form is on its list; the Field that * names the client is never one of the questions — the portal fills it. */ portalForm(args: { portal_id: Uuid; form_id: Uuid; }): Promise>; /** * S6 — send one form of her portal (`custom.portal_form_submit`). The store fills the Field * that names the client with her own record, and the answer reaches her own list at once * (`accepted`) or waits for the office (`held`, with the store's sentence). `client_key` * makes a retry the same submission. */ portalFormSubmit(args: { portal_id: Uuid; form_id: Uuid; answers: Record; client_key?: string | null; }): Promise>; /** * Invite one person, as one record of the portal's client Table. * * `user_id` is deliberately NOT a parameter here. From a screen it is always * null: an invitation confers NOTHING until the person follows their sign-in * link and the platform gives them an identity, and the door's own `say` * sentence states exactly that. A screen that passed an id would be granting * access on somebody else's behalf. */ portalInvite(args: { portal_id: Uuid; client_record_id: Uuid; email: string; }): Promise>; /** Take one person's way in away. The answer's `say` names the consequence. */ portalRevoke(args: { portal_id: Uuid; principal_id: Uuid; }): Promise>; /** * VIEW AS THIS CLIENT. The door asks `custom.visible_set` for HER user id — * the same call `custom.read_records` makes when she opens the page — so this * cannot disagree with what she sees. It REFUSES with `02000` and a sentence * when she has not signed in yet; show the sentence, never an empty list. */ portalPreview(args: { portal_id: Uuid; principal_id: Uuid; table_id: Uuid; }): Promise>; /** This organization's dashboards, optionally only those whose subject is one Table. */ dashboards(args?: { table_id?: Uuid | null; }): Promise>; /** * AT-INTERFACE — one Interface as this viewer may see it (no organization needed: a share of the * interface record is the way in). `null` = not there, archived, or not shared with her. */ interfaceRead(args: { interface_id: Uuid; }): Promise>; /** The rows one PUBLISHED element of an Interface exposes (a builder may name a draft one). */ interfaceRows(args: { interface_id: Uuid; element_id: string; record_id?: Uuid | null; limit?: number; offset?: number; }): Promise>; /** * AT-FIX-G1b — one PUBLISHED chart of an Interface as its reader may see it, in the shape one block of * `dashboardRun` answers (the same chart draws from it). The groups and measures are worked out by the store * over the rows her interface access shows and only through the columns the interface door exposes; a chart * that reads any other column answers `refused` (naming none). `null` = not there, archived, or not shared with her. */ interfaceChart(args: { interface_id: Uuid; chart_id: string; }): Promise>; /** * Create or re-state one dashboard. Every block is judged on the way IN — * a block naming a field this Table does not have is refused BY NAME, with * the table's own field list in the hint — so a canvas cannot be saved broken. */ dashboardDeclare(args: { table_id: Uuid; name: string; blocks?: DashboardBlock[]; presentation?: Record | null; dashboard_id?: Uuid | null; }): Promise>; /** * Run every block and answer the numbers. `filter` is the ONE filter bar * across the canvas: it is merged UNDER each block's own filter, so a block * deliberately scoped to one stage stays scoped while the bar moves the rest. * A block that refuses carries the store's sentence and the others still answer. */ dashboardRun(args: { dashboard_id: Uuid; filter?: RecordFilter; /** * S3 — compare every block that does not carry its own comparison (the * block's own wins, as its own filter does). A block with no date to * compare along answers alone and says so in `compare_refused`. */ compare?: AggregateCompare | null; /** S3 — re-cut every bucketed block to one date grain (the canvas's picker). */ grain?: AggregateBucket | null; }): Promise>; /** The seventh block kind on its own: records that have sat on the same value too long. */ dashboardStuck(args: { table_id: Uuid; state_key: string; days?: number; filter?: RecordFilter; limit?: number; }): Promise>; /** * Declare or re-state a crew capture sheet over a Table. Admin on the Table. * Closed to the crew until `capturePublish`, like every publishing act here. */ captureSheetDeclare(args: { table_id: Uuid; title: string; questions: Array>; presentation?: Record | null; sheet_id?: Uuid | null; notify_rule_id?: Uuid | null; }): Promise>; /** Open the sheet to the crew, or close it. Admin on the Table. */ capturePublish(args: { sheet_id: Uuid; open?: boolean; }): Promise>; /** The crew sheets of this organization, with what has come in through each. */ captureSheets(args?: { table_id?: Uuid | null; }): Promise>; /** * Everything a phone needs to run one sheet OFFLINE — the questions, the Fields * with their ids and parity types, and an honest `may_capture` with a sentence. * Load it once on a signal; it is the last network call before the walk. */ captureOpen(args: { sheet_id: Uuid; }): Promise>; /** * One capture: the answers, the file references, the device, the moment the phone * took it and the place. Idempotent on `client_key`, which the PHONE mints when * the capture is taken, offline, before its first attempt — send the same key on * every retry and the answer comes back `replay: true` with the first record's id. */ captureSubmit(args: { sheet_id: Uuid; client_key: string; answers: Record; files?: CaptureFileRef[]; device?: string | null; captured_at?: string | null; location?: Record | null; }): Promise>; /** Every agent-owned column, with how many cells are filled, stale or pinned, and what it has cost. */ enrichments(args?: { table_id?: Uuid | null; }): Promise>; /** * Define, change or TURN ON an Enrichment on one Field. Admin on the Table. * `{enabled: true}` on its own turns an existing one on without resetting it — * and turning it on is what authorises every value it writes afterwards. */ enrichDeclare(args: { field_id: Uuid; spec: Record; }): Promise>; /** * The rows that need this column filled in, each with what the instruction may * read. A cell somebody pinned is never returned. This is what a PREVIEW shows * before anybody enables anything. */ enrichDue(args: { field_id: Uuid; limit?: number; include_fresh?: boolean; }): Promise>; /** * THE BADGE DATA FOR A WHOLE PAGE, in one call. Who wrote each value, when, out * of what source, its candidates, whether a person pinned it and whether it is * past its freshness date. Asked per cell this would be one round trip per badge. */ enrichCells(args: { table_id: Uuid; field_keys?: string[] | null; record_ids?: Uuid[] | null; }): Promise>; /** Hold one cell against the model that fills its column, or hand it back. */ enrichPin(args: { record_id: Uuid; field_key: string; pinned?: boolean; }): Promise>; /** What this column has cost, run by run. */ enrichRuns(args?: { field_id?: Uuid | null; limit?: number; }): Promise>; /** Take a dashboard away. The records it counted are untouched — a canvas owns nothing. */ dashboardDelete(args: { dashboard_id: Uuid; }): Promise>; /** * Group, bucket, filter and measure INSIDE `custom.record_aggregate`. The * aggregate node sits above the read-access join, so what a chart draws is * exactly what this person may see — the rest is never fetched, let alone * summed and then hidden. */ recordAggregate(args: { table_id: Uuid; /** * OPTIONAL FILTER (active-org law): with `config.organizationIds` set, the table is looked * up across all the person's organizations and the aggregate runs in the one that OWNS it * (each row then carries that `organization_id`); this narrows the lookup to one. Absent and * no `organizationIds` ⇒ this client's own organization, exactly as before. */ organization_id?: Uuid | null; /** Field keys whose values become the groups. */ groupBy?: string[]; /** `count` needs no key; every other operation reads one Field. */ measures?: AggregateMeasure[]; /** A date Field truncated to a period — a group whose expression is a date_trunc. */ bucket?: { key: string; by: AggregateBucket; } | null; /** * A scalar is an equality and an object is a half-open moment window, and * either way it lands in the SAME WHERE as read access. */ filter?: StoreFilter; limit?: number; /** * S3 — answer the same question over a prior window too (previous period, * same period last year, or a fixed baseline). Each row then carries * `prior_measures`, `delta` and the two `compare` windows. Buckets are cut * in the organization's calendar (knobs `custom/time_zone`, `custom/week_start`). */ compare?: AggregateCompare | null; /** * The grid's search, narrowing the answer to the rows `listPage`'s search would show (the * same predicate, `custom.record_search_sql`). Blank or absent: nothing changes. */ search?: string | null; /** The reader's time zone for the search's date matches (absent = the organization's calendar). */ time_zone?: string | null; /** * A CHART counts each part of a many-value column under its own group (a row tagged A and B counts once under A and once * under B; a multi-link counts per linked record id); a grid group-by (absent / false) keeps the exact combination. */ splitMulti?: boolean; }): Promise>; /** Historical aggregate computed by the store at its recorded-time read door. */ recordAggregateAsOf(args: { table_id: Uuid; recorded_at: string; measure?: string; field_key?: string | null; group_by?: string | null; match?: Record; }): Promise>>; /** The measures the store itself offers. Asked, never hardcoded in a picker. */ aggOperations(): Promise>; /** The periods the store itself buckets by. */ aggBuckets(): Promise>; /** * The organization's calendar (knobs `custom/time_zone`, `custom/week_start`), cut by the SAME door the * relative date filters and the chart buckets use. A calendar or timeline reads its first day of the week * from here, never from a constant of its own (lane NOTION-SMALL-4). */ aggCalendar(): Promise>; /** * A source's drill definition — its Dimensions, Measures, drill paths and first screen. * `{kind: "table", id}` is a custom Table; `{kind: "entity", token}` a standard table by its * registry token or a declared definition by its key. */ drillDescribe(args: { source: DrillSource; }): Promise>; /** * Ask a source a question: grouped, bucketed (day…year in the organization's calendar), * compared, pivoted. A standard table is counted AS THE PERSON (its own row security decides * every row); groups past the cap fold into one Other row and the total row says so. */ drillAsk(args: { source: DrillSource; question?: DrillQuestion; }): Promise>; /** The records behind a number, through the same filter, read as the person. */ drillRows(args: { source: DrillSource; where?: DrillQuestion["where"]; window?: DrillQuestion["window"]; sort?: DrillQuestion["sort"]; lane?: DrillQuestion["lane"]; columns?: string[]; limit?: number; offset?: number; /** The viewer's search term, asked of the door (`platform._drill_compile` reads `question.search`). */ search?: string; }): Promise>; /** * Every subscription in this organization, optionally narrowed to one saved * view or one cadence. There is no subscription table: each one is a Rule * record carrying a `subscription` block, so a subscription is readable, * historied and agent-writable like anything else. */ aggSubscriptions(args?: { saved_view_id?: Uuid | null; cadence?: string | null; }): Promise>; /** The cadences the store itself offers. Asked, never hardcoded in a picker. */ aggSubscriptionCadences(): Promise>; /** * The same cadences, through the door a PERSON may call. * `aggSubscriptionCadences` is the notifier's own and holds no client grant, * so a screen that asked for it was refused and fell back to a list typed * into the picker — which drifts from what the digest runner honors. This * one delegates to the same array, so there is exactly one list. */ subscriptionCadences(): Promise>; /** * WHAT THIS PERSON IS BEING TOLD ABOUT, and what they may switch off. * * Mine, plus anyone's over a Table I hold admin on — narrowed to Tables I can * already open, so the list never reveals one. `aggSubscriptions` above is the * NOTIFIER's reader and no client can call it; this is the owner's. */ subscriptions(args?: { table_id?: Uuid | null; }): Promise>; /** * Switch one on or off. Muting is honored in ONE place — the reader every * consumer goes through — so a screen can never say a subscription is off * while it keeps firing. */ subscriptionMute(args: { rule_id: Uuid; muted: boolean; }): Promise>; /** * WRITE ONE. The store refuses a cadence no runner acts on, a channel this * platform does not send on and quiet hours nobody can parse — by name — then * declares the Rule through `rule_declare`, because a Rule written as a record * carries `data_class = 'record'` and every reader that goes looking for a Rule * steps straight past it (REC-72). */ subscriptionDeclare(args: { table_id: Uuid; spec: SubscriptionSpec; }): Promise>; /** * SHOW ME ONE NOW. The summary this subscription would send at this moment, with * what entered, left and changed NAMED. It assembles only: nothing is delivered, * nothing is recorded and the watermark does not move. */ subscriptionPreview(args: { rule_id: Uuid; }): Promise>; /** * THE SAVED VIEWS OF A TABLE — what a subscription may be written over. Narrowed * in SQL as the definer to Tables this caller can already open, so the list can * never reveal a Table. */ views(args?: { table_id?: Uuid | null; }): Promise>; /** * SAVE ONE VIEW. VIEWER on the Table, not editor: writing down a question about a * Table is not changing it, and requiring editor would mean the people who read a * Table could not subscribe to anything in it. */ viewDeclare(args: { table_id: Uuid; spec: SavedViewSpecIn; }): Promise>; /** A Table's row templates ("New from template"). VIEWER on the Table. */ rowTemplates(args: { table_id: Uuid; }): Promise>; /** * SAVE ONE ROW TEMPLATE — `{name, values}` by Field key; the same name again changes it, and * `{template_id, archived: true}` archives it. EDITOR on the Table: it is how everyone's new rows start. */ rowTemplateDeclare(args: { table_id: Uuid; spec: { name?: string; values?: Record; template_id?: Uuid; archived?: boolean; }; }): Promise>; /** * DESIGNATE HOW A TABLE OPENS — "Make this the default". EDITOR on the Table: the named view * becomes the table's only default and, when `layout` is given, that is the layout everyone opens * it in. A layout press never designates (`viewDeclare` refuses the default view's layout and * is_default by name — VIEW-SWITCH-NOT-DESIGNATION, VERIFIER-21). */ viewDesignate(args: { table_id: Uuid; view_id: Uuid; layout?: string | null; }): Promise>; /** * ARCHIVE ONE SAVED VIEW (lane TABLE-ACTIONS, T3.5). The soft delete of the `platform.saved_view` * row behind it — `platform.saved_view_archive('custom/records', id)`, which asks its own ladder * (the view's creator, or admin on the view) and clears a default it held. The records are * untouched. `null` when the view was already gone. `viewRestore` (or Trash) brings it back. */ viewArchive(args: { view_id: Uuid; }): Promise | null>>; /** PUT AN ARCHIVED VIEW BACK — the platform's one restore door, `platform.entity_undelete('platform_saved_view', id)`. */ viewRestore(args: { view_id: Uuid; }): Promise>; /** * THE CALLER'S OWN LOOK AT EACH SAVED VIEW OF A TABLE — `{ "": { look, updated_at } }`. * A look is the view settings one person chose while looking (a board's grouping, a calendar's * date field, a sort, a filter, hidden columns, widths, the layout), kept for her alone until an * editor saves it onto the view with `viewDeclare` ("Save to view"). Viewer on the Table. */ viewLookRead(args: { table_id: Uuid; }): Promise; updated_at: string; }>>>; /** * KEEP THE CALLER'S OWN LOOK AT ONE VIEW — the whole look, as definition keys (`group_field`, * `date_field`, `sorts`, `where`, `presentation`, `layout`, …; a key set to null hides the view's * own). `look: null` is "Reset to view". The shared view row is never written. Answers the look * as kept (null after a reset). */ viewLookSet(args: { table_id: Uuid; view_id: Uuid; look: Record | null; }): Promise | null>>; docTemplateSave(args: { table_id: Uuid; name: string; body: string; template_id?: Uuid | null; }): Promise>; /** The body with every token resolved against one record. Read-only, no render row. */ docRenderBody(args: { template_id: Uuid; record_id: Uuid; }): Promise>; /** Write the render — frozen bytes a signature can be checked against. */ docRenderDocument(args: { template_id: Uuid; record_id: Uuid; }): Promise>; /** Every token in a body, in order, with the Field id each one points at. */ docTokens(args: { body: string; }): Promise>; /** The tokens this Table cannot answer, each with the store's own reason. */ docUnresolvedTokens(args: { table_id: Uuid; body: string; }): Promise>; docSign(args: { render_id: Uuid; field_key: string; signer_name: string; signer_user_id?: Uuid | null; }): Promise>; /** * One signature: the seal AND whether what was signed is still what is there. * The store answers both, in one call, never the screen. Viewer on the record. */ docSignatureRead(args: { signature_id: Uuid; }): Promise>; /** Every template that renders one Table. Viewer on the Table. */ docTemplates(args: { table_id: Uuid; }): Promise>; /** * Retire one template. EDITOR on the Table it renders, because a template is * that Table's wording. Documents already rendered keep their frozen bytes * and their seals, so this can never invalidate a signature. */ docTemplateDelete(args: { template_id: Uuid; }): Promise>; /** Bring back a removed template (the undo of docTemplateDelete, same rung). */ docTemplateRestore(args: { template_id: Uuid; }): Promise>; /** * Remove one saved document (soft). EDITOR on its record. Its bytes, hash and * any signature stay exactly as they were; docRenderRestore brings it back. */ docRenderDelete(args: { render_id: Uuid; }): Promise>; /** The undo of docRenderDelete, same rung. */ docRenderRestore(args: { render_id: Uuid; }): Promise>; /** Every frozen render of one record, newest first. Viewer on the record. */ docRenders(args: { record_id: Uuid; }): Promise>; /** Every signature on one record. Viewer on the record. */ docSignatures(args: { record_id: Uuid; }): Promise>; /** * DOOR-11. One record's revisions, newest first. The read door answers * documents and deliberately not versions, so a screen that wants to save * optimistically reads the version of the record it LOADED from here — for * the one record being edited, never for a list. */ revisions(args: { record_id: Uuid; }): Promise>; /** * PRODUCTS row 4. One record's version list, newest first: who (a person, or * an agent AND the person it acted for), when, the operation in the word * somebody used, and every field that moved with its before, its after, the * Rule version that produced a derived value and its source pointer. Viewer * on the record — reading how something got to be the way it is is reading. */ recordHistory(args: { record_id: Uuid; limit?: number; offset?: number; }): Promise>; /** * AGT-N-9 — EVERYTHING A CHAT OPENED ON THIS RECORD MAY KNOW, IN ONE ROUND TRIP. * * The Fields with their `(record, field, value version)` triples, the relations one hop * out, the history, the comments and the Table's other records — assembled INSIDE the * database, every piece through the read door, under this person's own field-level * security. A Field the store masks comes back in `withheld` with the store's own * sentence and no value; a Field the organization keeps out of conversations comes back * there too, saying which of the two it is. */ recordScopeContext(args: { record_id: Uuid; relations?: number; history?: number; siblings?: number; }): Promise>; /** What is this conversation about? */ conversationScope(args: { conversation_id: Uuid; }): Promise>; /** Point a conversation at one record. Refused unless this person may open it. */ conversationScopeBind(args: { conversation_id: Uuid; record_id: Uuid; }): Promise>; /** This conversation is no longer about a particular record. */ conversationScopeUnbind(args: { conversation_id: Uuid; }): Promise>; /** The binding AND the context of a bound conversation, in one round trip. */ conversationScopeContext(args: { conversation_id: Uuid; relations?: number; history?: number; siblings?: number; }): Promise>; /** * "Who changed the price on ANY record?" One column's history across a whole * Table, narrowed to the records this person may see by a predicate in the * same statement. A column that names no Field is refused BY NAME, never * answered empty. */ fieldHistory(args: { table_id: Uuid; field_key: string; limit?: number; offset?: number; record_id?: Uuid; }): Promise>; /** * A FORMULA, ROLLUP OR LOOKUP COLUMN'S HISTORY ON ONE RECORD (CD-FORMULA-08). Its answer at every moment it changed, * worked out from the values and the formula as they were, with the formula version that produced it. A column * people type into is refused by name (its history is `fieldHistory`). */ formulaHistory(args: { record_id: Uuid; field_key: string; limit?: number; }): Promise>; /** * WHAT A RESTORE WOULD CHANGE, BEFORE IT DOES. Editor on the record: this is * the first half of an act, and somebody who may not perform it is not shown * its preview. Pass `field_key` for one column. */ restorePreview(args: { record_id: Uuid; version: number; field_key?: string; }): Promise>; /** Put the whole record back to a version, as a NEW version. Editor. */ restoreVersion(args: { record_id: Uuid; version: number; }): Promise>; /** Put ONE column back to what it said at a version, as a NEW version (HIS-N-2). Editor. */ valueRestore(args: { record_id: Uuid; field_key: string; version: number; }): Promise>; /** * SCR-18. The record's comment thread with everybody named, plus what THIS * person may do in it — decided by the store, in the same call, so a screen * never draws a composer the write door will refuse. */ commentThread(args: { record_id: Uuid; include_resolved?: boolean; }): Promise>; /** * How many OPEN comments each named row has (up to 500 rows; rows with none are absent), read * through the same reader as the thread — one call for a whole card of rows. */ commentCounts(args: { record_ids: readonly Uuid[]; }): Promise>>; /** * Write a comment at the `commenter` rung, optionally naming people. A * mention is a reference and not a regex over the body: the ids come from the * picker and the store refuses one who is not in the organization, or who * cannot open the record — because that notification would be a dead end. */ commentWrite(args: { record_id: Uuid; body: string; field_key?: string; parent_comment_id?: Uuid; mentions?: Uuid[]; }): Promise>; /** Resolve (or re-open) a thread. `commenter` — resolution is not a delete. */ commentResolve(args: { comment_id: Uuid; resolved?: boolean; }): Promise>; /** * Follow ONE record (AT-FIX-G2; Airtable's "follow a record"). While followed, a change or a comment by SOMEBODY ELSE * puts a notice in this person's bell, written in the same transaction as the change; the notice opens the record. * `follow: false` stops it. Anybody who may read the record may follow it; the row is always the caller's own. */ recordFollowSet(args: { record_id: Uuid; follow?: boolean; }): Promise>; /** Who follows this record, and whether this person does. Empty for somebody who cannot open the record. */ recordFollowers(args: { record_id: Uuid; }): Promise>; /** * Every value of one record WITH its envelope — its own version, who wrote * it, who they were acting for and when. The timeline IS the record (REC-19); * there is no second history table. */ recordValuesVersioned(args: { record_id: Uuid; organization_id?: Uuid | null; }): Promise>; /** * Metadata search across EVERY organization the person belongs to (`config.organizationIds`; * `organization_id` narrows to one): the Tables and Fields whose name, key, label or description * holds `text` (case-insensitive; structure, never rows). Each match is `{id, kind: "table" | * "field", name, description, organization_id}` (a Field adds `key`, `table_id`, `field_type`). * An organization whose read door refuses is listed in `unavailable` WITH its reason and * never hides the rest. Only when NO organization answers is the whole search a failure (the * first refusal). */ metadataSearch(args: { text: string; organization_id?: Uuid | null; }): Promise>; /** * WHICH of the person's organizations can this client use right now? Asks every organization * in scope (`organization_id` narrows to one) whether its record store is open, so a host can * say "organization X is unavailable: " instead of silently dropping it. Organization * order is the person's own, never completion order. */ organizationsOpen(args?: { organization_id?: Uuid | null; }): Promise>; /** * The organization that OWNS a table or record — the one a write to it must run in. A * `table_id` is looked up across all the person's organizations; a `record_id` is opened in its * own. `null` = found in none of them. Never a permission: the store's doors still decide. */ ownerOrganization(args: { table_id?: Uuid | null; record_id?: Uuid | null; organization_id?: Uuid | null; }): Promise>; /** * Mint an embed token. `mode` is `read` OR `write` and never both — the store * refuses a token carrying two decisions. The secret comes back ONCE, because * only its digest is stored: a database read cannot produce a working token. * `allowedOrigins` is required and matched EXACTLY, so `example.com.evil.test` * fails a check that a suffix match would pass. */ anonTokenIssue(args: { mode: "read" | "write"; allowedOrigins: string[]; formId?: Uuid | null; savedViewId?: Uuid | null; recordId?: Uuid | null; expiresAt?: Timestamp | null; }): Promise>; /** What a secret is good for, from an origin. The store's sentence when it is not. */ anonTokenVerify(args: { secret: string; origin: string; requiredMode?: "read" | "write" | null; }): Promise>; anonTokenRevoke(args: { token_id: Uuid; }): Promise>; /** * A STRANGER'S WRITE. The token supplies the organization, so this call names * none — and what it produces is a QUARANTINED SUBMISSION, not a record: * `custom.record` is never touched here. `clientKey` makes a replay free and * idempotent, which is what lets a flaky phone retry safely. */ anonWrite(args: { secret: string; origin: string; payload: Record; clientKey?: string | null; rawPayload?: Record; }): Promise>; /** * SCR-30, and it is the SIGNED-IN offline path, not the stranger's. The client * mints `clientKey` BEFORE the first attempt, offline, and the store's own * replay ledger dedupes on it — so a reconnect that fires the same capture * thirty times writes one record and returns the same id every time. */ anonCapture(args: { table_id: Uuid; clientKey: string; payload: Record; device?: string | null; capturedAt?: Timestamp | null; }): Promise>; /** Open or close a form to strangers. An admin act on the Table, never an editor one. */ anonPublish(args: { form_id: Uuid; published?: boolean; }): Promise>; /** * What is wrong with a checklist somebody is writing, in the words they would * use. `null` means it is fine. Pure — it reads no row and writes nothing — * so an editor may ask it on every keystroke and the refusal arrives BEFORE * the save rather than after it. */ checklistRefusal(args: { spec: ChecklistSpec; }): Promise>; /** * Store a checklist, whole: ordered steps, who each belongs to by role, how * many days in each is due, what each waits for, what each asks for, and what * starts a run. Pass `template_id` to change the checklist that already * exists rather than making a second one. */ checklistDeclare(args: { spec: ChecklistSpec; template_id?: Uuid | null; }): Promise>; checklistTemplates(args?: { about_table_id?: Uuid | null; limit?: number; }): Promise>; checklistTemplateShape(args: { template_id: Uuid; }): Promise>; /** * Run a checklist for one record: one work item per step in the ONE work * layer, assigned by role to real people, dated from the offsets, * dependencies carried across. `roles` names who stands in each role for THIS * run, over whatever the checklist says by default. */ checklistStart(args: { template_id: Uuid; about_record_id?: Uuid | null; roles?: Record; startingAt?: Timestamp | null; }): Promise>; checklistRun(args: { run_id: Uuid; }): Promise>; checklistRuns(args?: { about_table_id?: Uuid | null; about_record_id?: Uuid | null; includeClosed?: boolean; limit?: number; }): Promise>; /** * Tick a step off with whatever it asked for. Refused by name while a step it * waits for is open, or while what it asks for is missing — and the same * refusal meets `workSetState`, because the rule lives in the store and not * in this door. */ checklistStepComplete(args: { step_id: Uuid; evidence?: Record; }): Promise>; /** Why this step cannot be finished right now. `null` means it can. */ checklistStepRefusal(args: { step_id: Uuid; }): Promise>; /** The five states and what each one may become. Asked, never hardcoded in a picker. */ workStates(): Promise>; /** Why this graph would be refused — asked BEFORE it is written. `null` means it is fine. */ workTemplateRefusal(args: { graph: WorkGraph; }): Promise>; workTemplateDeclare(args: { name: string; graph: WorkGraph; }): Promise>; /** * REC-70: the WHOLE graph is created in one act, judged before any of it is * written, so a half-made checklist is not a state this store can be in. */ workTemplateInstantiate(args: { template_id: Uuid; overrides?: Record>; }): Promise>; workTemplateShape(args: { template_id: Uuid; }): Promise>; workInstantiationShape(args: { instantiation_id: Uuid; }): Promise>; /** Whose turn each record is, with its state and whether that state is terminal. */ workWhoseTurn(args: { table_id: Uuid; includeFinished?: boolean; }): Promise>; /** Does this Table carry assignment at all? A screen asks before it offers one. */ workHasAssignment(args: { table_id: Uuid; }): Promise>; /** The Fields assignment is made of, so nothing here invents a column name. */ workAssignmentFields(args?: { options_table_id?: Uuid | null; }): Promise>; /** Add assignment to a Table. */ workTakeAssignment(args: { table_id: Uuid; }): Promise>; /** Why this move would be refused. `null` means it is allowed. */ workTransitionRefusal(args: { from_state_id: Uuid; to_state_id: Uuid; }): Promise>; /** Declare the Table slots are held in. Idempotent — a second call finds the first. */ workSlotsDeclare(args: { name: string; slug: string; homeId?: Uuid | null; }): Promise>; /** * SCR-29. Take a slot for a while. THE STORE decides, under a unique index — * two people racing for the same slot do not both get it, whatever either * screen believed a moment earlier. */ workSlotHold(args: { table_id: Uuid; slotKey: string; holder: string; ttl?: string; }): Promise>; workSlotRelease(args: { hold_id: Uuid; }): Promise>; /** Every hold on this Table, with whether it has already lapsed. */ workSlotHolds(args: { table_id: Uuid; }): Promise>; workSlotExpire(args: { table_id: Uuid; }): Promise>; /** * THE ONE INBOX. Assignments, approvals and the agent's proposals, in one * ordered list with ONE row shape — PRODUCTS.md row 6. A second queue for the * agent's suggestions is the thing this replaces. */ workInbox(args?: { limit?: number; offset?: number; includeDecided?: boolean; /** Lane S5-PRIME-2: "inbox" (default), "snoozed" or "done" — this person's own views. */ view?: WorkInboxView; }): Promise>; /** * SNOOZE ONE ITEM OF MY INBOX UNTIL A TIME (lane S5-PRIME-2, `custom.inbox_snooze`). Only my * inbox changes; the approval or record is never written. A time already past, a snooze past a * year, and a decided item are refused by name. */ inboxSnooze(args: { item_id: Uuid; until: Timestamp; }): Promise>; /** Take my snooze off: it is back in my inbox now. */ inboxUnsnooze(args: { item_id: Uuid; }): Promise>; /** * CLEAR (mark done) ONE ITEM OF MY INBOX. A decision still owed is refused ("This one needs a * decision"). It comes back if somebody else changes it. */ inboxClear(args: { item_id: Uuid; }): Promise>; /** Put a cleared item back in my inbox. */ inboxUnclear(args: { item_id: Uuid; }): Promise>; /** * WHAT WAITS ON ME, COUNTED (`custom.inbox_counts`) — the same predicate the inbox lists with. * `everyOrganization` counts every organization I belong to (the shell badge: what waits on a * person is theirs, not the selected organization's). */ inboxCounts(args?: { everyOrganization?: boolean; }): Promise>; /** "My work", "work I assigned", "waiting on somebody being chosen". */ workList(args?: { flavour?: WorkListFlavour; includeFinished?: boolean; limit?: number; offset?: number; }): Promise>; /** * Give one record to one person: the Assignee value AND editor access, in one * transaction. Passing no `user_id` un-assigns and says plainly that nobody's * access was taken away. */ workAssign(args: { record_id: Uuid; user_id?: Uuid | null; dueDate?: string | null; clearDue?: boolean; }): Promise>; /** The states this record may be moved to, and the sentence for each it may not. */ workRecordStates(args: { record_id: Uuid; }): Promise>; /** Move it. The store refuses a move the model forbids, in its own words. */ workSetState(args: { record_id: Uuid; state_id: Uuid | null; }): Promise>; /** The person-kernel record standing for a signed-in person here. */ workPerson(args: { user_id: Uuid; create?: boolean; }): Promise>; /** The templates this person may run, so a picker has ids to offer. */ workTemplates(args?: { limit?: number; }): Promise>>; /** Who may decide a change about this subject, and why each of them may. */ workApprovalApprovers(args: { subject_id: Uuid | null; approver_id?: Uuid | null; }): Promise>; /** Ask for a change instead of making it. Viewer on the subject is enough to ASK. */ workApprovalRequest(args: { subject_id: Uuid; change: WorkChange; note?: string | null; approver_id?: Uuid | null; origin?: "person" | "agent"; conversation_id?: Uuid | null; }): Promise>; /** Whether THIS person may decide THIS one — so a screen never draws a dead button. */ workApprovalMayDecide(args: { approval_id: Uuid; }): Promise>; /** Approve or decline. Approving APPLIES the change, as the person approving. */ workApprovalDecide(args: { approval_id: Uuid; approve: boolean; note?: string | null; }): Promise>; /** * DECIDE MANY INBOX ITEMS IN ONE CALL (lane S4, `custom.work_decide_many`). Each is judged by * `work_approval_decide` exactly as one is, in its own savepoint: a refused decision leaves that * item waiting and the others still stand. Verdicts: approved, declined, already_decided, * no_right, refused — each with the store's own sentence. */ workDecideMany(args: { decisions: WorkDecideManyItem[]; }): Promise>; /** One waiting change in full, for the person deciding it or the one who asked. */ workApprovalRead(args: { approval_id: Uuid; }): Promise>>; /** VIS-31. Is this account a signed-in outsider — no NON-personal organization? */ isExternalPrincipal(args?: { user_id?: Uuid | null; }): Promise>; /** The same answer WITH the plain-English reason a screen shows. */ externalPrincipalCard(args?: { user_id?: Uuid | null; }): Promise>; /** Exactly the resource ids Visibility gave that outsider. Never an organization's list. */ externalPrincipalReach(args: { resourceType: string; user_id?: Uuid | null; }): Promise>>; /** VIS-32. Bind a record or a view to a public address with a declared render mode. */ publishBindingCreate(args: { slug: string; resourceType: string; resource_id: Uuid; renderMode?: string; }): Promise>; publishBindingRevoke(args: { slug: string; }): Promise>; /** What a SIGNED-OUT visitor resolves. Empty for an invented, revoked or unpublished address. */ resolvePublishBinding(args: { slug: string; }): Promise>; /** The same resolution plus what the address holds (table | record | dashboard | page | view). */ resolvePublishBindingKind(args: { slug: string; }): Promise>; /** D-15. The ONE sentence admitting nothing was scanned, so four surfaces cannot drift. */ worldPublishGapNotice(): Promise>; /** Put a resource in the world lane. Refuses by name while the lane is closed. */ publishToWorld(args: { resourceType: string; resource_id: Uuid; discoverable?: boolean; }): Promise>; /** * Declare a Rule. It is NOT `recordWrite` into the rule kernel, and that is * the whole point: `custom.record_write` inserts `data_class = 'record'`, so a * document written that way is not a Rule and the store's own rule doors * (`custom.rule_members`, `custom.agg_subscriptions`) never see it. Writing * one through the record door and calling it a Rule would be a silent * failure — a screen that looked like it had subscribed you to something. */ ruleDeclare(args: { spec: Record; rule_id?: Uuid | null; }): Promise>; fieldPropose(proposal: FieldProposal): Promise>; tablePropose(proposal: TableProposal): Promise>; promoteTable(args: { table_id: Uuid; }): Promise>; promoteField(args: { table_id: Uuid; field_id: Uuid; }): Promise>; /** * @deprecated The record store is never off (owner ruling, 2026-10-03: everything is on by * default). This answers `true` whenever the door answers; a refusal means the store could not * be reached, never that it is off. Do not gate a screen on it. */ storeIsOpen(): Promise>; /** * WHAT THIS PERSON MAY DO TO THESE THINGS — asked once, for a whole page. * * There is no principal argument, on purpose: the door resolves the caller * from the session, so a person can ask about their OWN access and about * nobody else's. A subject they hold nothing on answers `level: null`, which * is an answer; the row is never omitted, because a screen that could not * tell "no access" from "not asked yet" would draw one of them as the other. */ myLevels(args: { ids: Uuid[]; subject?: "record"; }): Promise>; /** G1. Write ONE leaf of a Table's colors, or clear it with `null`. Editor on the Table. */ tableDecorate(args: { table_id: Uuid; path: DecorationPath; value: unknown; }): Promise>; /** G1. A Table's colors as a screen paints them, with what went stale named. */ tableDecorations(args: { table_id: Uuid; }): Promise>; /** G1. The layout a grid opens with: platform → organization knob → the view's own. */ gridLayout(args: { table_id: Uuid; view_id?: Uuid | null; }): Promise>; /** G3. The older grid's formula text → the store's expression. Never refuses a typo: answers ok:false. */ formulaParse(args: { table_id: Uuid; text: string; }): Promise>; /** * NOTION-PROPS. The formula being typed, parsed AND worked out for one record of the table * (`custom.formula_preview`): the parse, plus `value` — what a saved formula column would show on * that record — or `error` when it parses and cannot be worked out there. Writes nothing. */ formulaPreview(args: { table_id: Uuid; record_id: Uuid | null; text: string; }): Promise>; /** G2. Declare a Table's row actions — the whole list. Editor on the Table. */ actionDeclare(args: { table_id: Uuid; actions: RowAction[]; }): Promise>; /** G2. A Table's row actions, each saying whether it can run here. */ rowActions(args: { table_id: Uuid; }): Promise>; /** * G2. Run one action on a selection, in ONE transaction. A refusal changed NOTHING; read * every record and field that refused with `actionRefusals(result.error)`. */ actionRun(args: { action_id: Uuid; record_ids: Uuid[]; }): Promise>; /** * AUTOMATION-DOOR. Declare (or, with `automation_id`, replace) one automation on a Table — editor on the Table. * A spec the store refuses comes back as `data.ok === false` with `_errors`, one sentence per path. */ automationDeclare(args: { table_id: Uuid; spec: AutomationSpec; automation_id?: Uuid | null; }): Promise>; /** AUTOMATION-DOOR. A Table's automations with their last run; archived ones only when asked. */ automations(args: { table_id: Uuid; include_archived?: boolean; }): Promise>; /** AUTOMATION-DOOR. Archive one (it switches off and keeps its history). Editor on the Table. */ automationArchive(args: { automation_id: Uuid; }): Promise>; /** AUTOMATION-DOOR. Bring an archived one back (it stays off until `automationSetEnabled`). */ automationRestore(args: { automation_id: Uuid; }): Promise>; /** AUTOMATION-DOOR. Switch one on or off. */ automationSetEnabled(args: { automation_id: Uuid; enabled: boolean; }): Promise>; /** AUTOMATION-DOOR. Run history, newest first: status, the store's own sentence, every step. */ automationRuns(args: { automation_id: Uuid; limit?: number; cursor?: string | null; }): Promise>; /** * AUTOMATIONS-AIRTABLE. Test an automation now (Airtable's "Run test"): its steps run once on `record_id` (none for * a schedule), switched on or not, and the run lands in its history as `trigger.on = "test"`. Editor on the Table. */ automationTest(args: { automation_id: Uuid; record_id?: Uuid | null; }): Promise>; /** AT-AUTO2. Every saved version of an automation, newest first. */ automationRevisions(args: { automation_id: Uuid; }): Promise>; /** AT-AUTO2. Restore one version: one more save through automation_declare (admin on the table). */ automationRevisionRestore(args: { automation_id: Uuid; revision_id: Uuid; }): Promise>; /** G4. Send every change to this Table's records to an https webhook. Admin. The secret is shown once. */ tableWebhookDeclare(args: { table_id: Uuid; target_url: string; events?: RecordEvent[] | null; description?: string | null; }): Promise>; /** G4. The webhooks a Table sends to, with their delivery health — never the secret. */ tableWebhooks(args: { table_id: Uuid; }): Promise>; /** G4. Switch one off. It is never deleted. */ tableWebhookArchive(args: { webhook_id: Uuid; }): Promise>; /** G5. Number the records an autonumber column found already there, in creation order. */ autonumberBackfill(args: { field_id: Uuid; }): Promise>; /** * AT-SHARE-B. A Person column's "Notify people when they're added" switch (`custom.field_notify_on_assign`). * `on: null` only READS whether it is on; `true` declares the column's two notify automations (edited, new * record), `false` archives them. Turning it on or off asks to change the table's structure. */ fieldNotifyOnAssign(args: { field_id: Uuid; on: boolean | null; }): Promise>; /** * NOTION-PROPS-3. PRESS A BUTTON COLUMN ON ONE ROW (`custom.button_press`): a link comes back filled from * the row for the screen to open; a row action or an automation runs in the store and its result comes * back. A button with nothing set up is refused by name (22023). */ buttonPress(args: { record_id: Uuid; field_id: Uuid; }): Promise>; /** * NOTION-PROPS-3. The knob `data/timeline_dependency_shift` for the signed-in person in this organization: * whether moving a record on a timeline moves the records waiting on it (default true). */ timelineDependencyShift(): Promise>; /** * FB5 (ADV-5 F4/F10): the record store's screen limits for the signed-in person in this organization, each a * knob (`custom.records_limits`): the AI column fill's try size, one fill's row cap and the Fill box's start, and * a table copy's stall bound. Asked once per client; a store without the door answers the knobs' defaults. */ recordsLimits(): Promise; /** G5. One page of the export door — exactly the rows and columns the read door serves this person. */ ioExport(args: { table_id: Uuid; columns?: string[] | null; limit?: number; }): Promise>; /** G5. That page as an .xlsx; a withheld column is named on its own sheet. */ /** * G9. Every table a picker may offer here, from BOTH stores, each saying which store opens it. * A table the app keeps out of default lists (`custom.table_kept_out_of_lists`: an agent's * outputs) is left out unless `include_platform_tables` is true — the "Show platform tables" switch. */ tableListEverywhere(args?: { include_platform_tables?: boolean; }): Promise>; /** * Every table the person reaches (`custom.data_home_tables`), as the data home lists them. * `organization_id` names one organization; `null` asks for every organization the person * reaches — never the client's bound organization by default. A table the app keeps out of * default lists (an agent's outputs) is answered only with `include_platform_tables: true`. */ dataHomeTables(args: { organization_id: Uuid | null; include_platform_tables?: boolean; }): Promise>; /** * Every table of EVERY organization (`custom.admin_all_tables`) — the admin section's list. The * store's walls admit a platform admin to system organizations only, so this is a separate door that * answers only on the admin lane for a platform admin; anyone else gets a refusal, not an empty list. */ adminAllTables(): Promise>; /** * G10. Refresh a synced source (a sheet tab) into ONE Table under `home_id`: new rows added, * changed rows updated, rows the source dropped archived — never destroyed; columns the * person added in Matrx survive. Build the spec with `sheetSyncSpec`. */ tableSync(args: { home_id: Uuid; spec: TableSyncSpec; }): Promise>; /** Lane SYNC-AIRTABLE: a table synced from another table's view, in the active organization. */ tableCopySync(spec: TableCopySpec): Promise>; /** Lane SYNC-AIRTABLE: auto-refresh / write-back of one synced table (editor of it). */ tableSyncSettingsSet(args: { table_id: Uuid; settings: TableSyncSettings; }): Promise>; /** * G11. The record-store Table a context scope's template-bound item provisions — one per item * and scope. A scope provisioned before the move answers with its moved Table (same id); the * Home defaults to the organization's own. */ scopeTableProvision(args: { item_id: Uuid; scope_id: Uuid; home_id?: Uuid | null; }): Promise>; /** G12. The rows of each Table this reader may see, in one call (fold with `withRowCounts`). A Table she may not know gets no row. */ tableRowCounts(args: { table_ids: Uuid[]; }): Promise>; /** G12. Each live record's own header (created, updated, version) that the read door does not answer. */ recordHeaders(args: { ids: Uuid[]; }): Promise>; /** * G14. Share a Table with a person by email or account id: an existing account outside the * organization is granted at once and notified (never made a member); an address with no * account is invited. The organization's outside-sharing setting decides first. */ tableShareOutsideGrant(args: { table_id: Uuid; person: string; level?: "viewer" | "commenter" | "editor" | "admin"; }): Promise>; /** G13. Keep a hand-set order on a saved view: the named rows first, in this order (build it with `moveInOrder`). */ viewRecordOrderSet(args: { view_id: Uuid; record_ids: Uuid[]; }): Promise>; /** * G13 + ORDER-FILTER. A page of a hand-ordered view: placed rows by position, then the rest by * created time. `filter` is the list door's question (`StoreFilter`: a flat map or a nested * Rule — the view's own `where`, or a number clicked on a dashboard); given, the store reads * exactly the rows that match, in their places, and `limit`/`offset` page THAT order. Each row * answers the position stored for it; rows the filter hides keep theirs. */ readInViewOrder(args: { view_id: Uuid; filter?: StoreFilter | null | undefined; limit?: number; offset?: number; }): Promise>>; exportXlsx(args: { table_id: Uuid; columns?: string[] | null; limit?: number; labels?: Record; sheetLabel?: string; }): Promise>; } /** One Table's two lane facts, as `custom.table_facts` answers them. */ interface TableFact { table_id: Uuid; /** * The old lane word (personal · internal · public), now worked out by the store from `shown_to` * and `published_to_web` (access ladder). Read those two; this stays for older stores. */ visibility: string; /** Whether the person signed in MADE this Table. Never who did. */ mine: boolean; /** Access ladder "Shown to": only_me · my_team · everyone · everyone_on_ai_matrx; null = the default. */ shown_to?: string | null; /** Access ladder "Published to the web": the only way anyone outside the organization opens it. */ published_to_web?: boolean; } /** * WHERE A TABLE IS CURRENTLY READ AND WRITTEN during the older-table cutover. * `tableHome` is an address read from one Table record; this applies the * organization's migration switch to a batch of ids, including same-id copies. */ interface TableLiveLocation { table_id: Uuid; lives_in: "record" | "older"; why: string | null; } /** The record store's screen limits (`recordsLimits`), each a knob in `platform.feature_knob`. */ interface RecordsLimits { /** A fill of at most this many rows runs at once; a bigger one meets the confirm step (knob custom/ai_fill_try_max). */ aiFillTryMax: number; /** The most rows one fill covers (knob custom/ai_fill_rows_max). */ aiFillRowsMax: number; /** The number the "Fill N rows" box starts at (knob custom/ai_fill_default_rows). */ aiFillDefaultRows: number; /** Passes a table copy may make without moving before it stops (knob custom/copy_stall_passes). */ copyStallPasses: number; /** * The highest sensitivity an AI fill may read (knob custom/enrichment_sensitivity_ceiling): public, internal, * restricted or confidential. custom.enrich_due drops anything above it; screens default an AI column's inputs by it. */ enrichmentSensitivityCeiling: string; } type Doc = Record; /** The five doors the mirror uses, narrowed so a test can stand in for the store exactly. */ interface MirrorDoors { /** One keyset page of the table (`custom.read_records_keyset`), each row with its version. */ page(tableId: string, after: string | null): Promise; cursor: string | null; }>>; /** One record and the version it was read at (`custom.read_record`). */ read(recordId: string): Promise>; /** A new record with the device-minted id (`custom.record_write_many` with `ids`). */ create(tableId: string, id: string, data: Doc): Promise>; /** An edit conditional on the version just read (`custom.record_update`). Answers the new version. */ /** `add`: new choices the patch's values need, added to their columns in the same write (`record_update_adding_choices`). */ update(recordId: string, patch: Doc, expectedVersion: number | null, add?: ChoiceAdds): Promise>; /** Archive (`custom.record_delete`; restorable, never a hard delete). */ remove(recordId: string): Promise>; } type MirrorRowState = "synced" | "pending" | "conflict" | "refused"; interface MirrorRow { table_id: string; id: string; /** What this device shows: the cloud copy with this device's waiting changes on top. */ document: Doc; /** The cloud copy this device last agreed with (null: never reached the cloud). */ base: Doc | null; base_version: number | null; state: MirrorRowState; /** In conflict: the cloud's copy now (null: archived in the cloud while this device edited it). */ theirs: Doc | null; theirs_version: number | null; /** Archived on this device, waiting to go up. The copy stays until the cloud agrees. */ deleted: boolean; updated_at: string; } type MirrorOpKind = "create" | "update" | "delete"; /** New choices a change needs, by column key (a person typed a value that is not a choice yet). */ type ChoiceAdds = Record; /** * Where an update op keeps the new choices it needs, inside its `data` (the store persists `data` as * it is). Never a field: a pass takes it off before the field merge and hands it to the door. */ declare const OP_CHOICE_ADDS = "$add_choices"; type MirrorOpState = "waiting" | "held" | "approved" | "refused" | "conflict"; interface MirrorOp { seq: number; table_id: string; record_id: string; kind: MirrorOpKind; /** create: the whole record; update: the changed fields; delete: null. */ data: Doc | null; state: MirrorOpState; /** The store's own sentence, for `refused`. */ error: string | null; created_at: string; } interface MirrorTable { table_id: string; kept_at: string; last_pull_at: string | null; last_error: string | null; /** Copies the last pull found gone from the cloud but held back by the breaker. */ pull_held: number; } /** Where the copy lives: SQLite on the desktop, memory in a test. Synchronous on purpose (one writer). */ interface MirrorStore { tables(): MirrorTable[]; table(tableId: string): MirrorTable | null; putTable(t: MirrorTable): void; dropTable(tableId: string): void; rows(tableId: string): MirrorRow[]; row(tableId: string, id: string): MirrorRow | null; putRow(r: MirrorRow): void; dropRow(tableId: string, id: string): void; ops(tableId: string): MirrorOp[]; /** Appends; answers the op with its seq. */ addOp(op: Omit): MirrorOp; putOp(op: MirrorOp): void; dropOp(seq: number): void; } /** The mass-delete breaker: the same three knobs, the same formula as file sync's planner. */ interface MirrorBreaker { floor: number; ratio: number; max: number; } declare function breakerLimit(b: MirrorBreaker, baseSize: number): number; type MirrorLog = (level: "info" | "warn" | "error", event: string, fields?: Record) => void; interface RecordsMirrorOptions { doors: MirrorDoors; store: MirrorStore; /** Read at every pass so a live knob change applies to the next one. */ breaker: () => MirrorBreaker; log: MirrorLog; now?: () => string; mintId?: () => string; } interface MirrorPassReport { table_id: string; outcome: "done" | "offline" | "failed"; pulled: number; pushed: number; merged: number; conflicts: number; refused: number; /** Deletes waiting for the person (queue over the breaker). */ held_deletes: number; /** Copies gone from the cloud that the breaker kept on this device. */ held_removals: number; error: string | null; } interface MirrorTableStatus extends MirrorTable { stand_in: true; records: number; waiting: number; conflicts: number; refused: number; held_deletes: number; } interface RecordsMirror { keep(tableId: string): void; /** Stops keeping a copy. Refused while changes are waiting unless `discard` (said in the answer). */ forget(tableId: string, opts?: { discard?: boolean; }): { ok: true; } | { ok: false; message: string; }; list(tableId: string): MirrorRow[]; /** Every copy of a kept table, the ones deleted here and still waiting included. */ view(tableId: string): MirrorRow[]; /** `id`: a caller-chosen id (a door's `p_ids`); minted on the device otherwise. */ create(tableId: string, data: Doc, id?: string): string; update(tableId: string, id: string, patch: Doc, add?: ChoiceAdds): void; remove(tableId: string, id: string): void; /** * Brings back a record deleted on this device whose delete has not reached the store yet (held or * waiting): the delete is dropped, the copy shows again. False when the copy holds no such delete * (the store archived it already: restore it there). */ restore(tableId: string, id: string): boolean; /** * Sends what is waiting for one table WITHOUT a pull: only the changed records are read (each * update reads its own record, so the field merge and keep-both conflicts stand) and written; the * delete breaker judges the queue as usual. Calls made while one runs share the next one, so a * burst of edits goes up as one batch. Never throws. */ flush(tableId: string): Promise; /** Pull, then push, for one table or every kept one. Never throws. */ sync(tableId?: string): Promise; /** The person approves the held deletes of a table; the next pass archives them. */ approveDeletes(tableId: string): number; /** The person discards the held deletes: every copy is shown again, nothing goes up. */ discardDeletes(tableId: string): number; /** The person picks a side of a conflict. `mine` re-queues this device's copy against theirs. */ resolve(tableId: string, id: string, keep: "mine" | "theirs"): void; status(): MirrorTableStatus[]; } declare function createRecordsMirror(o: RecordsMirrorOptions): RecordsMirror; /** A store in memory: tests, and a host that has nowhere durable yet (said by its caller). */ declare function memoryMirrorStore(): MirrorStore; /** `kept: false` is "not this device's copy": the call goes to the cloud as it always did. */ type KeptAnswer = { kept: true; data: T; } | { kept: false; }; type KeptChange = { kind: "create"; data: Doc; id?: string; } | { kind: "update"; id: string; patch: Doc; add?: ChoiceAdds; } | { kind: "delete"; id: string; } /** Undo of a delete this device has not sent yet (the grid's Restore / Undo). */ | { kind: "restore"; id: string; }; interface GridRow { id: string; document: Doc; deleted: boolean; /** Made on this device and not yet in the store. */ local_only: boolean; /** The store's version this copy was last agreed at (null: never reached the store). */ version?: number | null; /** This device holds a change to the row the store does not have yet. */ waiting?: boolean; } /** What the host's device copy answers for ONE kept Table (the desktop: main's records mirror, over IPC). */ interface KeptTablePort { /** This device's view of the table: each record as shown here, deleted-and-waiting included. */ rows(): Promise; /** * Queue a change, run the table's pass (bounded), answer. `id` is the record's; `version` the * store's once the change reached it, else one past the copy's base. `kept: false` for a record the * copy does not hold. */ write(change: KeptChange): Promise>; remember(key: string, answer: unknown): Promise; recall(key: string): Promise; } type Answer = { data: unknown; error: { code?: string; message: string; hint?: string | null; details?: string | null; } | null; }; /** supabase-js resolves a dropped network as an error with no SQLSTATE (a FetchError) or a 5xx HTTP status. */ declare function isUnreachable(error: Answer["error"]): boolean; /** Lay this device's view over a door answer: every `{id, document}` row is patched, deleted ones removed, new ones appended to row lists. */ declare function overlay(answer: unknown, rows: GridRow[]): unknown; /** * The whole table's row count (`record_aggregate` with one `count`, no grouping, filter or search — * the grid's page total) counts this device's new rows and leaves out its waiting deletes, as the * page it totals does. Any other aggregate is the store's answer as it stands. */ declare function countFollowsCopy(answer: unknown, args: Record, rows: GridRow[]): unknown; interface OfflineDataSourceOptions { /** A port that threw: the call went to `base` instead, and this says so. */ onError?: (door: string, error: unknown) => void; /** When a delete happened (tests pin it). */ now?: () => string; } /** The kept Table's data source. Wrap ONLY a kept Table's screens; every other Table keeps `base`. */ declare function offlineDataSource(base: RecordsDataSource, tableId: string, port: KeptTablePort, options?: OfflineDataSourceOptions): RecordsDataSource; /** Where remembered read answers live (the desktop: its State DB). */ interface RememberedAnswers { get(tableId: string, key: string): unknown | undefined; set(tableId: string, key: string, answer: unknown): void; } declare function memoryAnswers(): RememberedAnswers; interface GridPortOptions { answers: RememberedAnswers; /** * Sends what is waiting, run after each write (the host bounds it; the change stays queued either * way). Default: the mirror's own `flush` — no pull, only the changed records, a burst batched. */ flush?: () => Promise; /** * Deletes are never flushed one by one: a pass per delete would let a row-by-row mass delete slip * under the breaker. They wait this long after the last one, then go up as ONE batch the breaker * judges whole. */ deleteQuietMs?: number; } /** The port over a records mirror for one kept Table: what a host serves to `offlineDataSource` (directly, or across IPC). */ declare function gridPortFromMirror(mirror: RecordsMirror, tableId: string, o: GridPortOptions): KeptTablePort; /** * A kept Table's screens re-read when the DEVICE COPY changes — a pass that pushed or pulled, an * approved mass delete, a conflict picked, a reconnect — not only when this page wrote. The page * hook already re-reads on a realtime nudge (`records(null)`: the whole page), and a kept Table's * reads come through `offlineDataSource`, so the answer is the store's with this device's waiting * changes laid over it, or the remembered one offline. `onCopyChanged` is the host's signal (the * desktop: main's records mirror `changed` event, for this table); `base` is the host's own port, if * it has one, and keeps working beside it. */ declare function copyChangesRealtime(tableId: string, onCopyChanged: (listener: () => void) => () => void, base?: RecordsRealtimePort): RecordsRealtimePort; /** The mirror's five doors, from the package's own client (supabase-js under row security). */ declare function mirrorDoorsFrom(client: Pick): MirrorDoors; export { OP_CHOICE_ADDS, breakerLimit, copyChangesRealtime, countFollowsCopy, createRecordsMirror, gridPortFromMirror, isUnreachable, memoryAnswers, memoryMirrorStore, mirrorDoorsFrom, offlineDataSource, overlay }; export type { ChoiceAdds, Doc, GridPortOptions, GridRow, KeptAnswer, KeptChange, KeptTablePort, MirrorBreaker, MirrorDoors, MirrorLog, MirrorOp, MirrorOpKind, MirrorOpState, MirrorPassReport, MirrorRow, MirrorRowState, MirrorStore, MirrorTable, MirrorTableStatus, OfflineDataSourceOptions, RecordsMirror, RecordsMirrorOptions, RememberedAnswers };