@ai-matrx/records 0.87.32 → 0.92.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +92 -0
- package/README.md +115 -0
- package/dist/{client-E0VQ0NPO.d.ts → client-DE9Mq6YX.d.cts} +8 -5
- package/dist/{client-C3ZZNfGH.d.cts → client-vJAb7g9r.d.ts} +8 -5
- package/dist/core/index.cjs +25 -7
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +23 -12
- package/dist/core/index.d.ts +23 -12
- package/dist/core/index.js +25 -7
- package/dist/core/index.js.map +1 -1
- package/dist/data/index.cjs +19 -7
- package/dist/data/index.cjs.map +1 -1
- package/dist/data/index.d.cts +12 -142
- package/dist/data/index.d.ts +12 -142
- package/dist/data/index.js +19 -7
- package/dist/data/index.js.map +1 -1
- package/dist/{declareTable-BK3zufr7.d.cts → declareTable-Btg5bql1.d.cts} +4 -4
- package/dist/{declareTable-B12XybRp.d.ts → declareTable-JKt2q34d.d.ts} +4 -4
- package/dist/entity/index.cjs +984 -0
- package/dist/entity/index.cjs.map +1 -0
- package/dist/entity/index.d.cts +36 -0
- package/dist/entity/index.d.ts +36 -0
- package/dist/entity/index.js +964 -0
- package/dist/entity/index.js.map +1 -0
- package/dist/entity/react.cjs +1024 -0
- package/dist/entity/react.cjs.map +1 -0
- package/dist/entity/react.d.cts +511 -0
- package/dist/entity/react.d.ts +511 -0
- package/dist/entity/react.js +1015 -0
- package/dist/entity/react.js.map +1 -0
- package/dist/{field-BanTVGK2.d.cts → field-BJBnTyJ0.d.cts} +1 -1
- package/dist/{field-BPABOJ2Y.d.ts → field-Ti5iAxFr.d.ts} +1 -1
- package/dist/forms/index.cjs +984 -0
- package/dist/forms/index.cjs.map +1 -0
- package/dist/forms/index.d.cts +333 -0
- package/dist/forms/index.d.ts +333 -0
- package/dist/forms/index.js +961 -0
- package/dist/forms/index.js.map +1 -0
- package/dist/index.cjs +10 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -8
- package/dist/index.d.ts +11 -8
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/dist/memory/index.d.cts +7 -6
- package/dist/memory/index.d.ts +7 -6
- package/dist/port-CPVBfDxS.d.cts +301 -0
- package/dist/port-D2i2o7Lg.d.ts +301 -0
- package/dist/portal-Db4CYyHe.d.ts +331 -0
- package/dist/portal-DfeY2Boy.d.cts +331 -0
- package/dist/{ports-TMWX-t9l.d.ts → ports-BWtxAnXh.d.ts} +2 -2
- package/dist/{ports-D8-iuY1P.d.cts → ports-DSU2RsR6.d.cts} +2 -2
- package/dist/react/index.cjs +55 -523
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +14 -1
- package/dist/react/index.d.ts +14 -1
- package/dist/react/index.js +52 -527
- package/dist/react/index.js.map +1 -1
- package/dist/realtime/index.d.cts +2 -2
- package/dist/realtime/index.d.ts +2 -2
- package/dist/{record-DpB2QgAZ.d.cts → record-BQi92d4-.d.cts} +7 -1
- package/dist/{record-DpB2QgAZ.d.ts → record-BQi92d4-.d.ts} +7 -1
- package/dist/relation-DMwkmzgN.d.cts +230 -0
- package/dist/relation-DlcqV_W5.d.ts +230 -0
- package/dist/rule-B8kR0l01.d.ts +250 -0
- package/dist/rule-BMViCLmW.d.cts +250 -0
- package/dist/search/index.cjs +283 -0
- package/dist/search/index.cjs.map +1 -0
- package/dist/search/index.d.cts +44 -0
- package/dist/search/index.d.ts +44 -0
- package/dist/search/index.js +260 -0
- package/dist/search/index.js.map +1 -0
- package/dist/setup/index.cjs +6854 -0
- package/dist/setup/index.cjs.map +1 -0
- package/dist/setup/index.d.cts +87 -0
- package/dist/setup/index.d.ts +87 -0
- package/dist/setup/index.js +6829 -0
- package/dist/setup/index.js.map +1 -0
- package/dist/{subscription-D7KQBHXN.d.ts → subscription-5CLN3Jug.d.ts} +3 -249
- package/dist/{subscription-BD9DnJxX.d.cts → subscription-D-VEzvyy.d.cts} +3 -249
- package/dist/{table-kind-BZu8mtND.d.cts → table-kind-CVp43g6h.d.ts} +3 -558
- package/dist/{table-kind-xKxTHtrV.d.ts → table-kind-Cm6PXkWx.d.cts} +3 -558
- package/dist/templates/index.d.cts +10 -9
- package/dist/templates/index.d.ts +10 -9
- package/dist/typed-table/index.cjs +19 -7
- package/dist/typed-table/index.cjs.map +1 -1
- package/dist/typed-table/index.d.cts +10 -7
- package/dist/typed-table/index.d.ts +10 -7
- package/dist/typed-table/index.js +19 -7
- package/dist/typed-table/index.js.map +1 -1
- package/dist/{types-CLP5H4Wk.d.cts → types-9Q5SHRDJ.d.cts} +2 -2
- package/dist/{types-DmPkWQic.d.ts → types-CfYy-vdV.d.ts} +2 -2
- package/dist/{types-C2uspRtf.d.cts → types-Cs2MyE3u.d.cts} +2 -2
- package/dist/{types-Dv61jStB.d.ts → types-DF-dxB_l.d.ts} +2 -2
- package/dist/types-DTEGaxso.d.cts +139 -0
- package/dist/types-Dji5SpFS.d.ts +139 -0
- package/dist/use-cases/index.d.cts +5 -5
- package/dist/use-cases/index.d.ts +5 -5
- package/dist/{validate-CWVToe1T.d.ts → validate-BydKd-Le.d.ts} +1 -1
- package/dist/{validate-DEB_mn4Y.d.cts → validate-CqJakTKk.d.cts} +1 -1
- package/dist/versions/index.cjs +375 -0
- package/dist/versions/index.cjs.map +1 -0
- package/dist/versions/index.d.cts +160 -0
- package/dist/versions/index.d.ts +160 -0
- package/dist/versions/index.js +353 -0
- package/dist/versions/index.js.map +1 -0
- package/package.json +62 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/forms/index.ts","../../src/portal.ts","../../src/forms/forms.ts","../../src/forms/publicBinding.ts","../../src/button.ts","../../src/newerKinds.ts","../../src/typedDate.ts","../../src/typedNumber.ts","../../src/coerce.ts","../../src/forms/typedAnswers.ts"],"sourcesContent":["// @ai-matrx/records/forms — forms, portals and public addresses without any screen: the form document\n// (flows, looks, endings, jumps, embeds), the store's form doors' arguments, and which read a public\n// address does. React-free; `@ai-matrx/records-ui` draws them.\nexport * from \"./forms\";\nexport * from \"./publicBinding\";\nexport * from \"./typedAnswers\";\n","// src/portal.ts — @ai-matrx/records\n//\n// THE CLIENT PORTAL, FROM THE OWNER'S SIDE.\n//\n// A portal is the one way somebody with no membership of an organization is\n// let in to see the records that name them — their jobs, their invoices —\n// and nothing else. The store holds it in three tables (`custom.portal`,\n// `custom.portal_table`, `custom.portal_principal`) and answers about it\n// through six doors. These are the shapes those doors answer with, written\n// from the doors' own bodies and never from a screen's convenience.\n//\n// TWO FACTS THIS FILE EXISTS TO KEEP STRAIGHT, because a screen that gets\n// either of them wrong tells somebody a comforting lie:\n//\n// · AN INVITATION CONFERS NOTHING. `custom.portal_invite` writes a row and\n// says so in its own `say` sentence: the person gets access the moment\n// they follow the sign-in link and the platform gives them an identity,\n// and until then the row holds nothing at all. That is why\n// `PortalPrincipal` carries `signed_in` separately from `is_active` —\n// \"invited\" and \"can see their records\" are two different states and one\n// number for both is how an owner comes to believe a client is looking at\n// something they cannot reach.\n//\n// · \"VIEW AS THIS CLIENT\" IS NOT A PORTAL-SHAPED QUERY.\n// `custom.portal_preview` asks `custom.visible_set` for HER user id — the\n// exact call `custom.read_records` makes when she opens the page — so the\n// preview cannot disagree with what she sees. When she has not signed in\n// yet it REFUSES with `02000` and a sentence, rather than answering an\n// empty list that would read as \"she sees nothing\".\n\nimport type { Timestamp, Uuid } from \"./record\";\n\n/** One portal as `custom.portals` reports it to its organization. */\nexport interface PortalSummary {\n portal_id: Uuid;\n title: string;\n slug: string;\n /** The Table whose records ARE the clients. A principal is one of its rows. */\n client_table_id: Uuid;\n /** That Table's name, so a list never has to read a second door to draw a row. */\n client_table: string;\n is_active: boolean;\n /** How many Tables this portal exposes. */\n tables: number;\n /** Everybody invited and not revoked — NOT everybody who can see anything. */\n invited: number;\n /** Of those, how many have followed the link and hold an identity. */\n signed_in: number;\n /** `magic_link` today, and the door refuses every other word by name. */\n sign_in_method: string;\n opened_at: Timestamp | null;\n}\n\n/**\n * ONE (portal, Table) PAIR, as `custom.portal_tables` reports it.\n *\n * 🚨 WHY THIS EXISTS (TAILS-3, 2026-09-21). The Portals rail sits on a Table's\n * page beside Forms, Bookings and Dashboards — all table-scoped — while\n * `custom.portals` takes no table at all. On Rincon Plumbing Co's **Parts**\n * table the rail read \"Portals 10\" and listed ten cards titled \"Your jobs and\n * invoices\", which belong to **Jobs**; the empty state never rendered, so the\n * \"Build it / Ask an agent\" offer was unreachable on EVERY table of any\n * organization that already had one portal.\n *\n * This is the whole of what a table-scoped rail needs: is the table I am\n * standing on in any portal, and if not, which portals could take it.\n */\nexport interface PortalTableRef {\n portal_id: Uuid;\n table_id: Uuid;\n /** That Table's name, so a rail never reads a second door to draw a row. */\n name: string;\n}\n\n/** One Table a portal exposes, as `custom.portal_card` reports it. */\nexport interface PortalTableExposure {\n table_id: Uuid;\n /** The Table's own name. */\n name: string;\n /**\n * The key of the relation Field that says WHOSE a record is. This is the\n * whole of \"only theirs\": the association that Field writes is what carries\n * the record to the client, so a portal cannot exist without one.\n */\n names_via: string;\n /** The field keys a client sees. Empty is impossible — the door fills it. */\n visible_fields: string[];\n /** The subset a client may change. Always a subset of `visible_fields`. */\n editable_fields: string[];\n comments: boolean;\n /**\n * The most a client is ever conveyed on this Table — `viewer`, `commenter`\n * or `editor` — worked out by the door from the two lists above, never by a\n * screen.\n */\n conveys: string;\n}\n\n/** One person a portal was opened to, as `custom.portal_card` reports them. */\nexport interface PortalPrincipal {\n principal_id: Uuid;\n email: string;\n /** The record of the client Table this person IS, for the portal's purposes. */\n client_record_id: Uuid;\n /** That record's title — what an owner calls this client. */\n client: string;\n /** They followed the link and hold an identity. Until then this is false. */\n signed_in: boolean;\n /** Not revoked. An invitation that was never followed is still active. */\n is_active: boolean;\n invited_at: Timestamp;\n revoked_at: Timestamp | null;\n /**\n * THE LINK THE OFFICE COPIES FOR THIS PERSON — `/invitations/portal/accept/<token>`, the\n * path `custom.portal_invite` handed back when they were asked, read off the same\n * `iam.invitations` row and never re-templated.\n *\n * 🚨 `null` IS AN ANSWER, NOT AN ABSENCE, and a screen must say which. It is null when they\n * have already followed the link, when the office took it back (a revoke withdraws the\n * invitation in the same statement), when it has expired, or when they were let in through\n * the server's own magic-link route, which binds without an invitation at all. In every one\n * of those cases there is genuinely no link to copy — so draw a sentence, never a dead\n * control. (TAILS-5, 2026-09-21; the gap PORTAL-BIND left named.)\n */\n accept_path: string | null;\n}\n\n/** Everything about ONE portal: what it shows, and who it was opened to. */\nexport interface PortalCard {\n portal_id: Uuid;\n title: string;\n slug: string;\n is_active: boolean;\n sign_in_method: string;\n client_table_id: Uuid;\n tables: PortalTableExposure[];\n principals: PortalPrincipal[];\n /**\n * Whether this organization's outside lane is open\n * (`custom/external_principal_enabled`). Declaring a portal opens it; while\n * it is false every door refuses an outsider BY NAME, and a screen that drew\n * the people as reachable anyway would be lying about the one thing that\n * decides whether they can sign in at all.\n */\n external_lane_open: boolean;\n /** S6 — the look and forms exactly as the owner stored them (`{}` when none). */\n config: PortalConfig;\n /** S6 — the look as a client sees it: the portal's own values over the organization's brand. */\n style: PortalStyle;\n /** S6 — every form on the portal, in its order, with its title, Table and state. */\n forms: PortalFormRef[];\n /** S6 — the accent names a portal may carry (the design system's `STYLE_COLORS`). */\n accents: PortalAccent[];\n /**\n * S6 — the pictures that could be this portal's logo: the organization's own PUBLIC pictures in\n * Files, newest first. A private file is never offered, because the store would refuse it.\n */\n logo_candidates: PublicPicture[];\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// S6 — A PORTAL CARRIES ITS OWN LOOK AND ITS FORMS (UI-CHAMPIONS-PLAN rows 45, 46, 55)\n//\n// The look and the forms live on the portal's own record (`custom.portal.config`), written only\n// by `custom.portal_declare(… p_config)` and judged there: a logo must be a PUBLIC picture of this\n// organization's Files (a portal's logo is shown on its sign-in page, before anybody has signed\n// in), an accent is one of the design system's colour names, and a form is refused unless its\n// answers land in a Table the portal shows. The organization's brand is the default, so a portal\n// with no look of its own is never blank — `from_organization` says which values came from there.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** The design system's colour names (`STYLE_COLORS`), which is all an accent may be. */\nexport const PORTAL_ACCENTS = [\"slate\", \"green\", \"amber\", \"red\", \"blue\", \"violet\", \"teal\"] as const;\nexport type PortalAccent = (typeof PORTAL_ACCENTS)[number];\n\nexport function isPortalAccent(word: unknown): word is PortalAccent {\n return typeof word === \"string\" && (PORTAL_ACCENTS as readonly string[]).includes(word);\n}\n\n/** One link in a portal's footer. The store takes only `https://`, `mailto:` and `tel:`. */\nexport interface PortalFooterLink {\n label: string;\n url: string;\n}\n\n/** A portal's look as the owner sets it. Every key is optional; absent means \"the organization's\". */\nexport interface PortalStyleInput {\n display_name?: string | null;\n welcome?: string | null;\n logo_file_id?: Uuid | null;\n accent?: PortalAccent | null;\n footer_links?: PortalFooterLink[] | null;\n}\n\n/** One form on a portal, as the owner sets it. `order` is the position; `label` overrides the title. */\nexport interface PortalFormInput {\n form_id: Uuid;\n label?: string | null;\n order?: number | null;\n}\n\n/**\n * What `portalDeclare({ config })` sends. A key given REPLACES that key; a key not given is KEPT;\n * `style: null` puts the organization's brand back and `forms: []` takes every form off.\n */\nexport interface PortalConfigInput {\n style?: PortalStyleInput | null;\n forms?: PortalFormInput[] | null;\n}\n\n/** The look and forms as the store holds them (normalized by the store). */\nexport interface PortalConfig {\n style?: PortalStyleInput;\n forms?: Array<{ form_id: Uuid; label?: string; order: number }>;\n}\n\n/** The look a client is shown — resolved by the store, never computed by a screen. */\nexport interface PortalStyle {\n display_name: string;\n welcome: string | null;\n logo_file_id: Uuid | null;\n /** The logo's PUBLIC address, or null. Readable by anybody the portal is shown to. */\n logo_url: string | null;\n /** Null means the app's own colour. */\n accent: PortalAccent | null;\n footer_links: PortalFooterLink[];\n /** MAKE-HOME W5 — a cover picture's PUBLIC address (a form's look carries one), or null. */\n cover_file_id?: Uuid | null;\n cover_url?: string | null;\n /** Which of the values above came from the organization rather than the portal. */\n from_organization: Array<\"display_name\" | \"logo\">;\n}\n\n/**\n * One of the organization's PUBLIC pictures (`custom._public_pictures`): the only pictures a\n * portal's logo or a form's logo and cover may be, because a stranger sees them before signing in.\n */\nexport interface PublicPicture {\n file_id: Uuid;\n name: string;\n url: string | null;\n}\n\n/**\n * What a form's look editor needs (`custom.form_look_options`, MAKE-HOME W5b): the pictures the\n * store accepts, the colours, and the look a stranger would see for the draft presentation.\n */\nexport interface FormLookOptions {\n pictures: PublicPicture[];\n accents: PortalAccent[];\n look: PortalStyle;\n}\n\n/** One form on a portal, with what the owner needs to see about it. */\nexport interface PortalFormRef {\n form_id: Uuid;\n /** What the portal calls it (the owner's label, else the form's title). */\n label: string;\n order: number;\n title: string;\n table_id: Uuid | null;\n table: string;\n /** `gone` is a form that was removed; the outsider's list only ever carries `open`. */\n state: \"open\" | \"closed\" | \"full\" | \"gone\";\n}\n\n/** A Table's stages as a portal client may be told them (`custom.portal_me` → tables[].stage). */\nexport interface PortalStage {\n /** The stage Field's key on the record. */\n field: string;\n label: string;\n /** In the order they were declared. A record stores the stage's `key`. */\n stages: Array<{ key: string; label: string; retired: boolean }>;\n}\n\n/** What `custom.portal_form` answers: one form on the caller's own portal. */\nexport interface PortalFormSpec {\n form_id: Uuid;\n portal_id: Uuid;\n table_id: Uuid;\n title: string;\n label: string;\n presentation: Record<string, unknown>;\n /** The form's exposed Fields as the store holds them — never the one naming the client. */\n fields: Array<Record<string, unknown> & { id: Uuid; key: string }>;\n required: string[];\n state: \"open\" | \"closed\" | \"full\";\n message: string | null;\n}\n\n/** What `custom.portal_form_submit` answers. `held` waits for the office; `message` says so. */\nexport interface PortalFormAnswer {\n submission_id: Uuid | null;\n record_id: Uuid | null;\n state: \"accepted\" | \"held\" | \"rejected\" | \"closed\" | \"full\" | \"too_many\";\n message: string | null;\n}\n\n/** What `custom.portal_invite` answers. `say` is the door's own sentence. */\nexport interface PortalInvitation {\n invited: boolean;\n principal_id: Uuid;\n portal_id: Uuid;\n slug: string;\n email: string;\n client_record_id: Uuid;\n client: string;\n /** They already hold an identity. From a screen this is false: the door is passed no user. */\n bound: boolean;\n /**\n * The door's own words, and the ONLY correct summary of what just happened —\n * it says outright that an invitation confers nothing until the link is\n * followed. Show it; never paraphrase it.\n */\n say: string;\n}\n\n/** What `custom.portal_revoke` answers. `say` names the consequence, past tense. */\nexport interface PortalRevocation {\n revoked: boolean;\n principal_id: Uuid;\n email: string;\n /** The door's own sentence: they can no longer sign in, and their records stop reaching them. */\n say: string;\n}\n\n/** One row of \"what this client sees\", from `custom.portal_preview`. */\nexport interface PortalPreviewRow {\n record_id: Uuid;\n title: string;\n}\n\n/**\n * THE PUBLIC ADDRESS OF A PORTAL, and the one place its shape is written down.\n *\n * Unlike a form's link, a portal's slug is NOT the capability: the page it\n * serves asks who you are and emails a one-time link, so the slug is a name and\n * nothing more. That is why it is readable and why copying it is safe.\n */\nexport function portalPath(slug: string): string {\n return `/portal/c/${slug}`;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// A PORTAL IS ARCHIVED, NEVER DELETED (ORG-CLEANUP, 2026-09-22)\n//\n// The store grew `custom.portal_archive`, `custom.portal_restore` and the\n// archive-aware `custom.list_portals` because Rincon Plumbing Co carried TEN\n// portals all titled \"Your jobs and invoices\" — nine of them the residue of one\n// walk repeated nine times — and there was no door to put any of them away.\n// `is_active = false` is not it: CLOSED is a state an organization chooses about\n// a portal it still has, and its clients are told so by name. ARCHIVED means it\n// is out of the way, and nothing is deleted: every exposed Table and every\n// invited person is handed back exactly as it was.\n//\n// These are the words a screen says about that, written once here so the rail,\n// the guard and any other client say the same thing.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The platform archive filter's own three values, and no fourth. `custom.list_portals`\n * refuses anything else with `22023` rather than quietly treating it as one of them.\n */\nexport const PORTAL_ARCHIVE_FILTERS = [\"active\", \"archived\", \"all\"] as const;\nexport type PortalArchiveFilter = (typeof PORTAL_ARCHIVE_FILTERS)[number];\n\n/** THE LAW'S DEFAULT IS TO HIDE. A list nobody narrowed is the live things. */\nexport const DEFAULT_PORTAL_ARCHIVE_FILTER: PortalArchiveFilter = \"active\";\n\nexport function isPortalArchiveFilter(word: unknown): word is PortalArchiveFilter {\n return typeof word === \"string\" && (PORTAL_ARCHIVE_FILTERS as readonly string[]).includes(word);\n}\n\n/** What `custom.portal_archive` and `custom.portal_restore` answer with. */\nexport interface PortalArchiveAnswer {\n /** Where the portal stands NOW — true after an archive, false after a restore. */\n archived: boolean;\n /** False when it was already in that state; the sentence says so rather than lying about an act. */\n changed: boolean;\n portal_id: Uuid;\n archived_at?: Timestamp | null;\n /** The store's own sentence, in plain English. Show it verbatim; never a constraint name. */\n sentence: string;\n}\n\n/**\n * THE CONSEQUENCE, SAID BEFORE THE CLICK — never a generic \"are you sure?\".\n *\n * What actually changes is two things: nobody can sign in to it any more, and it\n * leaves the organization's portal list. What does NOT change is everything\n * else, and saying so is the difference between an archive somebody dares to\n * press and one they avoid.\n */\nexport function portalArchiveConsequence(title: string, signedIn: number, invited: number): string {\n const people =\n invited === 0\n ? \"Nobody is invited to it\"\n : signedIn === 0\n ? `The ${invited} ${invited === 1 ? \"person\" : \"people\"} invited to it will not be able to sign in`\n : `The ${signedIn} ${signedIn === 1 ? \"person\" : \"people\"} signed in to it, and the ${invited - signedIn} still invited, will not be able to sign in`;\n return (\n `“${title}” will leave your portal list. ${people}, and their invitation links will say this ` +\n `portal was archived and can be restored. Nothing is deleted — every Table it shows and ` +\n `everyone invited to it is kept exactly as it is, and you can restore it at any time.`\n );\n}\n\n/**\n * WHY THE TITLE HAS TO BE TYPED BACK, in the store's own reasoning. Ten portals\n * can carry one title, so the title is not what PICKS this one — the portal you\n * opened is. Typing it back is how you say you meant this one.\n */\nexport function portalConfirmLine(title: string): string {\n return `Type the portal’s title — ${title} — to confirm you mean this one.`;\n}\n\n/** True when what the person typed is what the door will accept. The door decides; this only spares them a round trip. */\nexport function portalTitleTypedBack(typed: string, title: string): boolean {\n return typed.trim() === (title ?? \"\").trim();\n}\n\n/**\n * WHAT AN EMPTY ARCHIVE LANE SAYS. An empty list is an answer: it says nothing\n * has been archived, so \"none here\" can never be read as \"the archive is broken\"\n * or as \"this organization has no portals at all\".\n */\nexport function emptyPortalArchiveLine(): string {\n return \"No portal of this organization has been archived. A portal you put away waits here until somebody restores it, and nothing is ever destroyed.\";\n}\n","\"use client\";\n\n// src/forms/forms.ts — @ai-matrx/records/forms · SCR-13, SCR-14 (moved from records-ui, 2026-10-08)\n//\n// A FORM IS A VIEW ON A REAL TABLE.\n//\n// It is not a second data model with its own \"responses\" store that somebody\n// later has to reconcile with the records people actually work on. A form names\n// a Table, each of its questions names one of that Table's own Fields, and a\n// submission is an ORDINARY RECORD in that Table — so the moment it is sent it\n// is in the grid, in the views, in the exports, in History and in the agent's\n// context, with no plumbing in between.\n//\n// THE AI-FIRST LAW. We build a Typeform duplicate not because people will click\n// a form together one question at a time, but because an agent writes the WHOLE\n// thing in one call (`form_propose`) and the person only tweaks it. `FormSpec`\n// below is that complete declarative object: name, questions, conditional logic,\n// theme, thank-you screen. `FormBuilder` takes one as `seed`, exactly as\n// `ViewBar` and `ActionInbox` do.\n//\n// CONDITIONAL LOGIC IS A RULE, NOT AN `if`. `showIf` is a Rule expression the\n// STORE evaluates (`custom.rule_eval`), referencing Fields BY ID. So the same\n// form means the same thing to the runner, to the server and to an agent, and\n// nobody re-implements the predicate in JavaScript where it can drift.\n//\n// ONE FORM OBJECT, AND THIS IS WHERE IT IS WRITTEN DOWN (2026-09-20).\n// --------------------------------------------------------------------------\n// Until this file changed there were TWO. `form_propose` — the sentence a person\n// actually says — wrote `custom.anon_form`: the thing that has a public link, an\n// accept Rule, a notify Rule and a published state. `FormBuilder` wrote form\n// RECORDS into a package-owned `records_ui_form` Table that had none of those,\n// so the form a person tweaked and the form a stranger answered were different\n// objects with the same name. Nothing reconciled them and nothing ever could.\n//\n// There is now one: `custom.anon_form`, reached through `custom.form_declare`\n// and `custom.forms`, which is exactly what `form_propose` calls. `FormSpec`\n// stays the screen's shape — it is what an agent writes and what `FormRunner`\n// renders — and the two functions below are the ONLY translation between it and\n// the store, in both directions. A second mapper anywhere is the drift coming\n// back.\n\nimport { isPortalAccent, type PortalAccent } from \"../portal\";\nimport type { FormSummary, RuleExpression } from \"../rule\";\nimport type { Uuid } from \"../record\";\n\n/**\n * THE WORDS FOR EACH STATE, SAID ONCE, FOR EVERY SCREEN THAT SHOWS A FORM.\n *\n * `FormsPanel` and `FormBuilder` both put a form in front of a person and both\n * offer the one act that changes this (`custom.anon_publish`). Two copies of\n * these four sentences is two places for them to drift, and a person who reads\n * \"Not published\" on one screen and something else on the other has been lied\n * to by one of them. The store owns the state; this owns the sentence.\n */\nexport const FORM_STATE_WORDS: Record<FormSummary[\"state\"], string> = {\n draft: \"Not published — the link does not answer yet.\",\n open: \"Open — anyone with the link can answer it.\",\n closed: \"Closed — it keeps its answers and takes no more.\",\n full: \"Full — it has all the answers it was set up to take.\",\n};\n\n/** How a form is put in front of a person. */\nexport const FORM_FLOWS = [\"one-at-a-time\", \"single-page\"] as const;\nexport type FormFlow = (typeof FORM_FLOWS)[number];\n\n/** One question. It ASKS FOR one Field of the subject Table — never its own column. */\nexport interface FormQuestionSpec {\n /** The Field's key on the subject Table. */\n field: string;\n /** Ask it in the form's own words. Left out, the Field's own label is the question. */\n ask?: string | null;\n /** One line under the question. */\n help?: string | null;\n /** Override the Field's own `required`. Left out, the Field decides. */\n required?: boolean | null;\n /**\n * CONDITIONAL LOGIC, AS A RULE. `{\"op\":\"eq\",\"args\":[{\"field\":\"<field id>\"},{\"const\":\"Yes\"}]}`\n * — the store answers it, this package never does. An UNDECIDED answer (the\n * store's `null`, which is what an unanswered question gives) SHOWS the\n * question: a person is never silently skipped past something.\n */\n showIf?: RuleExpression | null;\n /**\n * LOGIC JUMPS (lane TYPEFORM-DUP). After this question, the FIRST jump whose `when` the store\n * answers true is taken — to a later question or to one of the form's endings. A jump with no\n * `when` is \"always\". Undecided falls through to the next question. The store judges the route\n * (`custom._form_route`); this package never does.\n */\n jumps?: FormJump[] | null;\n /** SCORING: what each answer is worth (`{\"Yes\": 5}`); a list answer adds each of its parts. */\n points?: Record<string, number> | null;\n}\n\n/** One logic jump: when the Rule is true, go to a later question or to an ending. */\nexport interface FormJump {\n when?: RuleExpression | null;\n to: { question: string } | { ending: string };\n}\n\n/**\n * ONE ENDING (lane TYPEFORM-DUP). A form may have several; a jump picks one, or the first whose\n * condition holds (`when`, `scoreMin`, `scoreMax`), or else the first with no condition.\n */\nexport interface FormEnding {\n /** A short name, lower-case letters, digits, - and _. Jumps point at it. */\n id: string;\n title?: string | null;\n body?: string | null;\n /** One of the organization's public pictures (the builder's side). */\n pictureFileId?: Uuid | null;\n /** The picture's public address (the store resolves it for the public page). */\n pictureUrl?: string | null;\n buttonLabel?: string | null;\n /** A secure page on the organization's own sites — the thank-you's own rule. */\n redirectUrl?: string | null;\n when?: RuleExpression | null;\n scoreMin?: number | null;\n scoreMax?: number | null;\n}\n\n/** THE WELCOME SCREEN, before question one: a title, a message, a picture and a Start button. */\nexport interface FormWelcome {\n title?: string | null;\n body?: string | null;\n pictureFileId?: Uuid | null;\n pictureUrl?: string | null;\n buttonLabel?: string | null;\n}\n\n/** The look. Tokens, so light and dark both come out right without a second theme. */\nexport interface FormTheme {\n /** A design-system accent class or a CSS color. Left out, the app's own accent. */\n accent?: string | null;\n align?: \"left\" | \"center\" | null;\n /** One of design-system `FORM_FONTS`. */\n font?: string | null;\n /** One of design-system `FORM_BUTTON_STYLES`. */\n button?: string | null;\n /** One of design-system `FORM_BACKGROUNDS`. */\n background?: string | null;\n /** The picture for the `picture` background: a public picture of the organization. */\n backgroundFileId?: Uuid | null;\n /** Its public address, as the store resolved it for the public page. */\n backgroundUrl?: string | null;\n}\n\n/**\n * THE FORM'S OWN LOOK, in the portal's words (`custom._portal_style` resolves both): a colour of\n * the app's own palette and two of the organization's PUBLIC pictures. Each part left out is the\n * organization's look — its newest open portal's, then its own name and logo. `custom.form_declare`\n * refuses a colour the app does not have or a picture a stranger could not see, by name.\n */\nexport interface FormLook {\n accent?: PortalAccent | null;\n logoFileId?: Uuid | null;\n coverFileId?: Uuid | null;\n}\n\n/** The screen after sending: the form's words, and optionally where to go next. */\nexport interface FormThankYou {\n title?: string | null;\n body?: string | null;\n /** A secure page on one of the organization's own sites. The store judges it. */\n redirectUrl?: string | null;\n}\n\n/** THE COMPLETE DECLARATIVE SPEC. One `record_write` and the form exists. */\nexport interface FormSpec {\n name: string;\n /** The Table a submission lands in. */\n subject: Uuid;\n intro?: string | null;\n questions: FormQuestionSpec[];\n flow?: FormFlow | null;\n theme?: FormTheme | null;\n /** Colour, logo and cover a stranger sees. Left out, the organization's look. */\n look?: FormLook | null;\n /**\n * WHAT THE PERSON SEES AFTER SENDING. `title` and `body` are the form's own\n * words for the next step (\"You are all set\" / \"Next, book your first visit\").\n * `redirectUrl` optionally sends them on to a page instead — and only ever to\n * one of the organization's own sites: `custom.form_declare` refuses anything\n * else by name, and `custom.form_public` stops handing it out the moment its\n * site leaves the organization's list (lane S7-PRIME).\n */\n thankYou?: FormThankYou | null;\n submitLabel?: string | null;\n /** A separate first screen with a Start button (lane TYPEFORM-DUP). */\n welcome?: FormWelcome | null;\n /** Several endings, each picked by the answers (lane TYPEFORM-DUP). */\n endings?: FormEnding[] | null;\n /**\n * HIDDEN FIELDS: names read from the link (`?utm_source=…&ref=…`), kept on the submission and\n * never shown. Not questions — a question with the same key is prefilled instead.\n */\n hiddenFields?: string[] | null;\n /**\n * Is this form meant to be answerable by someone who is not signed in? The\n * runner does NOT make that true by itself — it needs the anonymous write\n * door, and until that door exists it says so BY NAME rather than writing as\n * whoever happens to be signed in and calling it public.\n */\n isPublic?: boolean;\n /**\n * THE OWNER'S LOGO AND NAME ABOVE THE FORM (lane TYPEFORM-2). Left out, the organization's knob\n * `forms/show_owner_header` (default on); false hides the agency's brand on a client's form.\n */\n showOwnerHeader?: boolean | null;\n /** A single choice goes on to the next question when picked. Left out, knob `forms/choice_auto_advance`. */\n autoAdvance?: boolean | null;\n /** NOTIFY ON ANSWER, the builder's words (lane TYPEFORM-2): a webhook and the automation that sends it. */\n notify?: FormNotify | null;\n}\n\n/** Where an answer is sent besides the owner's own subscription (lane TYPEFORM-2). */\nexport interface FormNotify {\n webhookUrl?: string | null;\n /** The `custom.automation_declare` row that posts to `webhookUrl` (trigger form_answered). */\n webhookAutomationId?: Uuid | null;\n /**\n * EMAIL THE RESPONDENT (TYPEFORM-2), off unless set: one email to the address the person typed\n * in `respondentField` (an email question), worded from the ending reached — sent by\n * `custom.form_respondent_copy` through the platform's notification sender.\n */\n emailRespondent?: boolean | null;\n respondentField?: string | null;\n}\n\n/**\n * A form as `custom.forms` reports it — the spec a screen renders, plus the\n * things only the STORE knows about it.\n *\n * The four store-side ids are carried on purpose. `custom.form_declare` is a\n * RE-STATEMENT of the whole form, so a save that left `notifyRuleId` out would\n * set it to null and the owner would quietly stop being told when somebody\n * answers — the exact silent failure the notify Rule exists to prevent. They are\n * read here and handed straight back by `formDeclareArgs`.\n */\nexport interface SavedForm extends FormSpec {\n id: Uuid;\n /** draft · open · closed · full — the store's own word, never derived on a screen. */\n state: FormSummary[\"state\"];\n /** The form's readable name in the store. `form_declare` keeps it across saves. */\n slug: string;\n /** Stops taking answers at this many. Null is no ceiling. */\n submissionCap: number | null;\n /** The accept Rule: what counts as a complete answer. Null holds every answer for a person. */\n quarantineRuleId: Uuid | null;\n /** The subscription Rule that tells somebody an answer arrived. */\n notifyRuleId: Uuid | null;\n}\n\n/**\n * The spec as `custom.anon_form.presentation` holds it.\n *\n * `questions` is deliberately NOT here: the door writes it itself from the\n * argument it validated (`presentation || {\"questions\": p_questions}`), so the\n * words a form is asked in and the keys it is allowed to accept can never be two\n * different lists.\n */\nexport function formPresentation(spec: FormSpec): Record<string, unknown> {\n return {\n intro: spec.intro ?? null,\n flow: spec.flow ?? \"one-at-a-time\",\n thank_you: thankYouDocument(spec.thankYou ?? null),\n submit_label: spec.submitLabel ?? null,\n look: formLookDocument(spec.look ?? null),\n welcome: welcomeDocument(spec.welcome ?? null),\n endings: (spec.endings ?? []).map(endingDocument),\n hidden_fields: spec.hiddenFields ?? [],\n theme: themeDocument(spec.theme ?? null),\n ...(typeof spec.showOwnerHeader === \"boolean\" ? { show_owner_header: spec.showOwnerHeader } : {}),\n ...(typeof spec.autoAdvance === \"boolean\" ? { auto_advance: spec.autoAdvance } : {}),\n ...(spec.notify && (spec.notify.webhookUrl || spec.notify.webhookAutomationId || spec.notify.emailRespondent)\n ? {\n notify: strip({\n webhook_url: text(spec.notify.webhookUrl),\n webhook_automation_id: text(spec.notify.webhookAutomationId),\n email_respondent: spec.notify.emailRespondent ? true : null,\n respondent_field: spec.notify.emailRespondent ? text(spec.notify.respondentField) : null,\n }),\n }\n : {}),\n };\n}\n\n/** `presentation.notify` back into the screen's words. */\nexport function notifyFromDocument(raw: unknown): FormNotify | null {\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return null;\n const d = raw as Record<string, unknown>;\n const n: FormNotify = {\n webhookUrl: text(d[\"webhook_url\"]),\n webhookAutomationId: text(d[\"webhook_automation_id\"]) as Uuid | null,\n emailRespondent: d[\"email_respondent\"] === true ? true : null,\n respondentField: text(d[\"respondent_field\"]),\n };\n return n.webhookUrl || n.webhookAutomationId || n.emailRespondent ? n : null;\n}\n\nconst bool = (v: unknown): boolean | null => (typeof v === \"boolean\" ? v : null);\n\n/**\n * WHAT A QUESTION REFUSES BEFORE IT GOES ON (lane TYPEFORM-2) — Typeform's words, one per case.\n * `null` is \"may go on\". A required question with no answer is refused; an answer of the email,\n * phone or link kind is refused when it cannot be one. The store judges again on send.\n */\nexport function answerRefusal(kind: string, required: boolean, value: unknown): string | null {\n const empty = value === undefined || value === null || value === \"\" || (Array.isArray(value) && value.length === 0);\n if (empty) return required ? \"This field is required\" : null;\n if (typeof value !== \"string\") return null;\n const v = value.trim();\n if (kind === \"email\" && !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]{2,}$/.test(v)) return \"Enter a valid email\";\n if (kind === \"phone\" && (v.replace(/\\D/g, \"\").length < 7 || /[^\\d\\s()+.\\-x]/i.test(v))) return \"Enter a valid phone number\";\n if (kind === \"url\" && !/^(https?:\\/\\/)?[^\\s.\\/]+(\\.[^\\s.\\/]+)+(\\/\\S*)?$/i.test(v)) return \"Enter a valid link\";\n return null;\n}\n\n/**\n * ANSWER PIPING — Typeform's recall (lane TYPEFORM-2). `{{key}}` or `{{Question words}}` in a later\n * question, a help line or an ending becomes that question's answer as the person gave it; a\n * choice reads as its label. Unanswered, it becomes nothing.\n */\nexport function pipeAnswers(\n text: string,\n answers: Readonly<Record<string, unknown>>,\n questions: ReadonlyArray<{ key: string; ask?: string | null }>,\n labelOf?: (key: string, value: unknown) => string | null,\n): string {\n if (!text.includes(\"{{\")) return text;\n return text.replace(/\\{\\{\\s*([^{}]+?)\\s*\\}\\}/g, (_all, name: string) => {\n const wanted = name.trim().toLowerCase();\n const q = questions.find((x) => x.key.toLowerCase() === wanted) ?? questions.find((x) => (x.ask ?? \"\").trim().toLowerCase() === wanted);\n if (!q) return \"\";\n const value = answers[q.key];\n if (value === undefined || value === null) return \"\";\n const one = (v: unknown) => labelOf?.(q.key, v) ?? String(v);\n return Array.isArray(value) ? value.map(one).join(\", \") : one(value);\n });\n}\n\nconst text = (v: unknown): string | null => (typeof v === \"string\" && v.trim() !== \"\" ? v.trim() : null);\nconst num = (v: unknown): number | null => (typeof v === \"number\" && Number.isFinite(v) ? v : null);\nconst strip = (doc: Record<string, unknown>): Record<string, unknown> =>\n Object.fromEntries(Object.entries(doc).filter(([, v]) => v !== null && v !== undefined));\n\n/** `presentation.theme` from the screen's words: camelCase out, the store's snake_case in. */\nexport function themeDocument(theme: FormTheme | null): Record<string, unknown> {\n if (!theme) return {};\n return strip({\n accent: theme.accent ?? null,\n align: theme.align ?? null,\n font: text(theme.font),\n button: text(theme.button),\n background: text(theme.background),\n background_file_id: text(theme.backgroundFileId),\n });\n}\n\nexport function themeFromDocument(raw: unknown): FormTheme | null {\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return null;\n const d = raw as Record<string, unknown>;\n const align = d[\"align\"] === \"center\" ? \"center\" : d[\"align\"] === \"left\" ? \"left\" : null;\n return {\n accent: text(d[\"accent\"]),\n align,\n font: text(d[\"font\"]),\n button: text(d[\"button\"]),\n background: text(d[\"background\"]),\n backgroundFileId: text(d[\"background_file_id\"]) as Uuid | null,\n backgroundUrl: text(d[\"background_url\"]),\n };\n}\n\nexport function welcomeDocument(welcome: FormWelcome | null): Record<string, unknown> | null {\n if (!welcome) return null;\n const doc = strip({\n title: text(welcome.title),\n body: text(welcome.body),\n picture_file_id: text(welcome.pictureFileId),\n button_label: text(welcome.buttonLabel),\n });\n return Object.keys(doc).length > 0 ? doc : null;\n}\n\nexport function welcomeFromDocument(raw: unknown): FormWelcome | null {\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return null;\n const d = raw as Record<string, unknown>;\n const w: FormWelcome = {\n title: text(d[\"title\"]),\n body: text(d[\"body\"]),\n pictureFileId: text(d[\"picture_file_id\"]) as Uuid | null,\n pictureUrl: text(d[\"picture_url\"]),\n buttonLabel: text(d[\"button_label\"]),\n };\n return w.title || w.body || w.pictureFileId || w.pictureUrl || w.buttonLabel ? w : null;\n}\n\nexport function endingDocument(ending: FormEnding): Record<string, unknown> {\n return strip({\n id: ending.id,\n title: text(ending.title),\n body: text(ending.body),\n picture_file_id: text(ending.pictureFileId),\n button_label: text(ending.buttonLabel),\n redirect_url: text(ending.redirectUrl),\n when: ending.when ?? null,\n score_min: num(ending.scoreMin),\n score_max: num(ending.scoreMax),\n });\n}\n\nexport function endingsFromDocument(raw: unknown): FormEnding[] {\n if (!Array.isArray(raw)) return [];\n return raw\n .filter((e): e is Record<string, unknown> => !!e && typeof e === \"object\" && typeof (e as Record<string, unknown>)[\"id\"] === \"string\")\n .map((d) => ({\n id: String(d[\"id\"]),\n title: text(d[\"title\"]),\n body: text(d[\"body\"]),\n pictureFileId: text(d[\"picture_file_id\"]) as Uuid | null,\n pictureUrl: text(d[\"picture_url\"]),\n buttonLabel: text(d[\"button_label\"]),\n redirectUrl: text(d[\"redirect_url\"]),\n when: (d[\"when\"] as RuleExpression | null | undefined) ?? null,\n scoreMin: num(d[\"score_min\"]),\n scoreMax: num(d[\"score_max\"]),\n }));\n}\n\n/** The hidden fields' values out of a link's query, narrowed to the names the form declares. */\nexport function hiddenFromLink(\n hiddenFields: readonly string[] | null | undefined,\n params: Record<string, string | string[] | undefined>,\n): Record<string, string> {\n const out: Record<string, string> = {};\n for (const key of hiddenFields ?? []) {\n const raw = params[key];\n const one = Array.isArray(raw) ? raw[raw.length - 1] : raw;\n if (typeof one === \"string\" && one.trim() !== \"\") out[key] = one.trim().slice(0, 500);\n }\n return out;\n}\n\n/** Does this form route at all — conditions, jumps, endings or points the store must judge? */\nexport function formRoutes(form: Pick<FormSpec, \"questions\" | \"endings\">): boolean {\n return (\n (form.questions ?? []).some((q) => q.showIf || (q.jumps && q.jumps.length > 0) || q.points) ||\n (form.endings ?? []).length > 0\n );\n}\n\n/** HOW A FORM GOES INTO SOMEBODY ELSE'S PAGE. */\nexport const FORM_EMBEDS = [\"inline\", \"popup\", \"slider\"] as const;\nexport type FormEmbed = (typeof FORM_EMBEDS)[number];\n\n/**\n * THE SNIPPET THE OWNER COPIES (lane TYPEFORM-DUP). `inline` is a plain iframe of the public link\n * in embed mode; `popup` and `slider` are one button plus the platform's own embed script\n * (`<origin>/embed/form.js`), which opens the same iframe in a centred window or a side panel.\n * Nothing in either needs a cookie: the form keeps its place in its own storage and posts to its\n * own origin, so it keeps working with third-party cookies blocked.\n */\nexport function formEmbedSnippet(kind: FormEmbed, formUrl: string, options?: { label?: string | null; height?: number | null }): string {\n const url = new URL(formUrl);\n url.searchParams.set(\"embed\", \"1\");\n const src = url.toString();\n const label = (options?.label ?? \"\").trim() || \"Open the form\";\n const esc = (v: string) => v.replace(/&/g, \"&\").replace(/\"/g, \""\").replace(/</g, \"<\");\n if (kind === \"inline\") {\n const height = options?.height && options.height > 0 ? Math.round(options.height) : 640;\n return `<iframe src=\"${esc(src)}\" title=\"${esc(label)}\" style=\"width:100%;height:${height}px;border:0\" loading=\"lazy\" allow=\"clipboard-write\"></iframe>`;\n }\n const script = `${url.origin}/embed/form.js`;\n return `<button type=\"button\" data-matrx-form=\"${esc(src)}\" data-matrx-form-mode=\"${kind}\">${esc(label)}</button>\\n<script src=\"${esc(script)}\" async></script>`;\n}\n\n/** `presentation.look` both directions. Nothing set is `null`, one spelling on both sides. */\nexport function formLookDocument(look: FormLook | null): Record<string, unknown> | null {\n if (!look) return null;\n const out: Record<string, unknown> = {};\n if (look.accent) out[\"accent\"] = look.accent;\n if (look.logoFileId) out[\"logo_file_id\"] = look.logoFileId;\n if (look.coverFileId) out[\"cover_file_id\"] = look.coverFileId;\n return Object.keys(out).length > 0 ? out : null;\n}\n\nexport function formLookFromDocument(raw: unknown): FormLook | null {\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return null;\n const doc = raw as Record<string, unknown>;\n const id = (v: unknown) => (typeof v === \"string\" && v.trim() !== \"\" ? (v as Uuid) : null);\n const look: FormLook = {\n accent: isPortalAccent(doc[\"accent\"]) ? doc[\"accent\"] : null,\n logoFileId: id(doc[\"logo_file_id\"]),\n coverFileId: id(doc[\"cover_file_id\"]),\n };\n return look.accent || look.logoFileId || look.coverFileId ? look : null;\n}\n\n/**\n * THE THANK-YOU SCREEN, BOTH DIRECTIONS, IN ONE PLACE. The store holds it as\n * `presentation.thank_you = {title, body, redirect_url}`; the screen reads it as\n * `FormThankYou`. A thank-you with nothing in it is `null`, so \"nothing set\"\n * has one spelling on both sides.\n */\nexport function thankYouDocument(thankYou: FormThankYou | null): Record<string, unknown> | null {\n if (!thankYou) return null;\n const title = thankYou.title?.trim() || null;\n const body = thankYou.body?.trim() || null;\n const redirect = thankYou.redirectUrl?.trim() || null;\n if (!title && !body && !redirect) return null;\n return { title, body, ...(redirect ? { redirect_url: redirect } : {}) };\n}\n\n/** `presentation.thank_you` as the store (or `custom.form_public`) answers it, for a screen. */\nexport function thankYouFromDocument(raw: unknown): FormThankYou | null {\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return null;\n const doc = raw as Record<string, unknown>;\n const text = (v: unknown) => (typeof v === \"string\" && v.trim() !== \"\" ? v : null);\n const thankYou: FormThankYou = {\n title: text(doc[\"title\"]),\n body: text(doc[\"body\"]),\n redirectUrl: text(doc[\"redirect_url\"] ?? doc[\"redirectUrl\"]),\n };\n return thankYou.title || thankYou.body || thankYou.redirectUrl ? thankYou : null;\n}\n\n/** The exact arguments `custom.form_declare` is called with, for a new form or a re-statement. */\nexport function formDeclareArgs(form: FormSpec | SavedForm): {\n table_id: Uuid;\n title: string;\n questions: FormQuestionSpec[];\n presentation: Record<string, unknown>;\n submission_cap: number | null;\n quarantine_rule_id: Uuid | null;\n notify_rule_id: Uuid | null;\n form_id: Uuid | null;\n slug: string | null;\n} {\n const saved = form as Partial<SavedForm>;\n return {\n table_id: form.subject,\n title: form.name,\n questions: form.questions ?? [],\n presentation: formPresentation(form),\n submission_cap: saved.submissionCap ?? null,\n quarantine_rule_id: saved.quarantineRuleId ?? null,\n notify_rule_id: saved.notifyRuleId ?? null,\n form_id: saved.id ?? null,\n slug: saved.slug ?? null,\n };\n}\n\n/** One row of `custom.forms` back into the spec a screen and an agent both read. */\nexport function formFromSummary(summary: FormSummary): SavedForm {\n const presentation = (summary.presentation ?? {}) as Record<string, unknown>;\n const flow = String(presentation[\"flow\"] ?? \"one-at-a-time\");\n return {\n id: summary.form_id as Uuid,\n name: summary.title ?? summary.slug,\n subject: summary.table_id as Uuid,\n intro: (presentation[\"intro\"] as string | null) ?? null,\n questions: readQuestions(presentation[\"questions\"]),\n flow: (FORM_FLOWS as readonly string[]).includes(flow) ? (flow as FormFlow) : \"one-at-a-time\",\n theme: themeFromDocument(presentation[\"theme\"]),\n welcome: welcomeFromDocument(presentation[\"welcome\"]),\n endings: endingsFromDocument(presentation[\"endings\"]),\n hiddenFields: Array.isArray(presentation[\"hidden_fields\"])\n ? (presentation[\"hidden_fields\"] as unknown[]).filter((k): k is string => typeof k === \"string\")\n : [],\n thankYou: thankYouFromDocument(presentation[\"thank_you\"]),\n look: formLookFromDocument(presentation[\"look\"]),\n submitLabel: (presentation[\"submit_label\"] as string | null) ?? null,\n showOwnerHeader: bool(presentation[\"show_owner_header\"]),\n autoAdvance: bool(presentation[\"auto_advance\"]),\n notify: notifyFromDocument(presentation[\"notify\"]),\n // A form is public when the store says it is OPEN. A screen never decides\n // this from a flag of its own: the link answers or it does not.\n isPublic: summary.state === \"open\",\n state: summary.state,\n slug: summary.slug,\n submissionCap: summary.submission_cap,\n quarantineRuleId: (summary.quarantine_rule_id as Uuid | null) ?? null,\n notifyRuleId: (summary.notify_rule_id as Uuid | null) ?? null,\n };\n}\n\n/**\n * The questions out of a stored presentation. `custom.form_declare` takes either\n * `field` or `key` as the address, so both come back out — a form written by an\n * agent through the door reads the same as one saved from the builder.\n */\nfunction readQuestions(raw: unknown): FormQuestionSpec[] {\n if (!Array.isArray(raw)) return [];\n const out: FormQuestionSpec[] = [];\n for (const entry of raw) {\n if (!entry || typeof entry !== \"object\") continue;\n const q = entry as Record<string, unknown>;\n const field = String(q[\"field\"] ?? q[\"key\"] ?? \"\").trim();\n if (field === \"\") continue;\n out.push({\n field,\n ask: (q[\"ask\"] as string | null) ?? null,\n help: (q[\"help\"] as string | null) ?? null,\n required: (q[\"required\"] as boolean | null) ?? null,\n showIf: (q[\"showIf\"] as RuleExpression | null) ?? (q[\"show_if\"] as RuleExpression | null) ?? null,\n jumps: Array.isArray(q[\"jumps\"]) ? (q[\"jumps\"] as FormJump[]) : null,\n points:\n q[\"points\"] && typeof q[\"points\"] === \"object\" && !Array.isArray(q[\"points\"])\n ? (q[\"points\"] as Record<string, number>)\n : null,\n });\n }\n return out;\n}\n\n/**\n * THE SOURCE STAMP every submission carries. A record that arrived through a\n * form is not indistinguishable from one somebody typed into the grid: it says\n * which form, which version of it, when, and who was acting. `_source` is a\n * stamp on the document beside the answers, so it travels with the record\n * through export, History and the agent's context without a parallel table.\n */\nexport function submissionStamp(args: {\n formId: Uuid | null;\n formName: string;\n actor: string;\n onBehalfOf?: string | null;\n}): Record<string, unknown> {\n return {\n _source: {\n via: \"form\",\n form_id: args.formId,\n form_name: args.formName,\n at: new Date().toISOString(),\n actor: args.actor,\n ...(args.onBehalfOf ? { on_behalf_of: args.onBehalfOf } : {}),\n },\n };\n}\n","// src/forms/publicBinding.ts — @ai-matrx/records/forms (moved from records-ui, 2026-10-08)\n//\n// WHICH READ A PUBLIC ADDRESS DOES, decided by WHAT IT IS BOUND TO — never by its render mode.\n//\n// `iam.publish_binding.render_mode` is page | embed | feed | raw: HOW it renders, not what it\n// holds. Until v6 lane 11 (2026-10-02) the page tested `render_mode === \"list\"`, a value the store\n// refuses, so every binding fell through to the record read with whatever id it held — a table's\n// id included.\n//\n// Tables, records and dashboards all carry the entity token `record`, so the token cannot say\n// which. `iam.resolve_publish_binding_kind` (the chair's door; anon may call it) can: it names the\n// kind — table | record | dashboard | page | view — and the table a list reads. A table lists its\n// rows; a view lists its table's rows THROUGH THE VIEW'S OWN FILTER (never the whole table — that\n// would publish rows the view leaves out); a record is read; a dashboard or a page reads nothing\n// until the page renderer exists, and the page says so. A signed-out visitor has no read door yet\n// (a later chair wave): the read is refused by the store, and the page shows that refusal.\n\nimport type { Uuid } from \"../record\";\n\nexport interface PublicBindingShape {\n render_mode: string;\n resource_id: Uuid;\n /** What the door says the address holds. */\n kind: string | null | undefined;\n /** The table a list reads: the table itself, a view's subject, a record's table. */\n table_id: Uuid | null | undefined;\n}\n\nexport type PublicRead =\n | { read: \"table\"; tableId: Uuid }\n | { read: \"view\"; tableId: Uuid; viewId: Uuid }\n | { read: \"record\"; recordId: Uuid }\n | { read: \"unknown\" };\n\nexport function publicReadOf(binding: PublicBindingShape): PublicRead {\n if (binding.kind === \"table\" && binding.table_id) return { read: \"table\", tableId: binding.table_id };\n if (binding.kind === \"view\" && binding.table_id) {\n return { read: \"view\", tableId: binding.table_id, viewId: binding.resource_id };\n }\n if (binding.kind === \"record\") return { read: \"record\", recordId: binding.resource_id };\n return { read: \"unknown\" };\n}\n","// src/button.ts — @ai-matrx/records\n//\n// A BUTTON COLUMN (lanes NOTION-PROPS-2/3; champions: Notion's and Airtable's Button). The store keeps it as\n// behaviour text wearing the format `button`; it holds no value, and `config.button` says what a press does\n// through `custom.button_press`:\n//\n// open_url answers `url` with every {{field id}} filled from the row; the screen opens it\n// run_action one of the table's row actions on this row (which is how a button sets fields)\n// run_automation one of the table's automations on this row, for an editor of the row\n//\n// A button with no `do` (what Notion's importer brings — Notion's API hides a button's steps) is a label\n// only, and a press is refused by name. Its settings change through `custom.field_update`'s `button` key.\n\nimport type { CoercibleField } from \"./coerce\";\nimport type { Uuid } from \"./record\";\n\nexport const BUTTON_DOES = [\"open_url\", \"run_action\", \"run_automation\"] as const;\nexport type ButtonDoes = (typeof BUTTON_DOES)[number];\n\n/** What `config.button` holds. */\nexport interface ButtonSettings {\n label: string;\n /** One of the table's colour names (blue, green, …); absent, the button wears the neutral look. */\n color?: string | undefined;\n /** Absent: nothing is set up yet, and a press says so. */\n do?: ButtonDoes | undefined;\n url?: string | undefined;\n action_id?: Uuid | undefined;\n automation_id?: Uuid | undefined;\n}\n\n/** What `custom.button_press` answers. */\nexport type ButtonPressed =\n | { do: \"open_url\"; label: string; url: string }\n | { do: \"run_action\"; label: string; result: { ran?: number; records?: Array<{ record_id: Uuid; version: number }> } & Record<string, unknown> }\n | { do: \"run_automation\"; label: string; run_id: Uuid; automation: string | null; status: string | null; says: string | null };\n\n/** Whether a Field is a button column — `custom.field_kind_of`'s mirror for this one kind. */\nexport function isButtonField(field: CoercibleField | null | undefined): boolean {\n return field?.type === \"text\" && String(field.format ?? \"\") === \"button\";\n}\n\n/** A button column's settings, read off its declaration; null for any other column. */\nexport function buttonOf(field: (CoercibleField & { label?: string | null }) | null | undefined): ButtonSettings | null {\n if (!field || !isButtonField(field)) return null;\n const raw = ((field.config ?? {}) as Record<string, unknown>)[\"button\"];\n const b = raw && typeof raw === \"object\" ? (raw as Record<string, unknown>) : {};\n const text = (v: unknown) => (typeof v === \"string\" && v.trim() !== \"\" ? v.trim() : undefined);\n const does = text(b[\"do\"]);\n return {\n label: text(b[\"label\"]) ?? text(field.label) ?? \"Button\",\n color: text(b[\"color\"]),\n do: (BUTTON_DOES as readonly string[]).includes(does ?? \"\") ? (does as ButtonDoes) : undefined,\n url: text(b[\"url\"]),\n action_id: text(b[\"action_id\"]) as Uuid | undefined,\n automation_id: text(b[\"automation_id\"]) as Uuid | undefined,\n };\n}\n\n/** The `{{field id}}` placeholders a link names, in order — so a settings screen can show them as names. */\nexport function buttonUrlPlaceholders(url: string): string[] {\n return Array.from(url.matchAll(/\\{\\{([^{}]+)\\}\\}/g), (m) => m[1]!.trim());\n}\n\n/**\n * What a button's settings are missing before a press can work, in words; null when it is set up.\n * The store refuses the same cases (`custom._field_kind_shape_ok`, `custom.button_press`).\n */\nexport function buttonNeeds(b: ButtonSettings): string | null {\n if (!b.do) return `${b.label} has nothing set up to do yet.`;\n if (b.do === \"open_url\" && !/^(https?:\\/\\/|mailto:|tel:|\\{\\{)/i.test(b.url ?? \"\")) return `${b.label} needs a link that starts with https://.`;\n if (b.do === \"run_action\" && !b.action_id) return `${b.label} needs a row action to run.`;\n if (b.do === \"run_automation\" && !b.automation_id) return `${b.label} needs an automation to run.`;\n return null;\n}\n","// src/newerKinds.ts — @ai-matrx/records\n//\n// THE NINE NEWER KINDS OF COLUMN (lane 10 VIEWS-AND-FIELDS, sublane P4; champions: Airtable's and\n// SmartSuite's field types). The store keeps each as an EXISTING behaviour wearing a format —\n// FLD-1's six behaviours stay six — and `custom.field_kind_of` reads each back. This file is the\n// client's mirror of that one reader, plus the few words each kind needs wherever only words fit\n// (a card, an export cell, a typed answer):\n//\n// kind behaviour format value\n// rating range rating whole number 0..max (max Rule 1–10, default 5)\n// duration range duration seconds (unit \"seconds\"), never below zero\n// created_by formula — {id, name}; config.system created_by, worked out on read\n// modified_by formula — {id, name}; config.system modified_by, worked out on read\n// status list status the choice's key; config.status_groups key → todo | in_progress | done\n// address text address {street, city, region, postal_code, country}\n// barcode text barcode one printable line; config.symbology optional\n// rich_text text rich_text Markdown, no raw HTML\n// count formula count how many linked records (a roll-up whose agg is count)\n//\n// `fieldKindFor` (coerce.ts) still answers the BASE kind — a rating is a number, a status a\n// select, a count a roll-up — because every editor, sort and aggregate keyed on those keeps\n// working for them. What is particular to each newer kind asks `newerKindOf` here.\n\nimport { formatDurationSeconds } from \"@ai-matrx/kit/format\";\nimport type { CoercibleField } from \"./coerce\";\n\nexport const NEWER_FIELD_KINDS = [\n \"rating\",\n \"duration\",\n \"created_by\",\n \"modified_by\",\n \"status\",\n \"address\",\n \"barcode\",\n \"rich_text\",\n \"count\",\n] as const;\n\nexport type NewerFieldKind = (typeof NEWER_FIELD_KINDS)[number];\n\ntype WithRules = CoercibleField & { rules?: ReadonlyArray<{ kind?: string; value?: unknown }> | null };\n\n/** Which of the nine newer kinds a Field is, read off its declaration — `custom.field_kind_of`'s mirror. */\nexport function newerKindOf(field: CoercibleField): NewerFieldKind | null {\n const format = String(field.format ?? \"\");\n const config = (field.config ?? {}) as Record<string, unknown>;\n switch (field.type) {\n case \"range\":\n if (format === \"rating\") return \"rating\";\n if (format === \"duration\") return \"duration\";\n return null;\n case \"text\":\n if (format === \"address\") return \"address\";\n if (format === \"barcode\") return \"barcode\";\n if (format === \"rich_text\") return \"rich_text\";\n return null;\n case \"list\":\n return format === \"status\" ? \"status\" : null;\n case \"formula\":\n if (format === \"count\" && config[\"agg\"] === \"count\") return \"count\";\n if (config[\"system\"] === \"created_by\") return \"created_by\";\n if (config[\"system\"] === \"modified_by\") return \"modified_by\";\n return null;\n default:\n return null;\n }\n}\n\n// ── RATING ─────────────────────────────────────────────────────────────────────────────────\n\n/** A rating's top star: its max Rule (1–10), else 5 — the store's own default. */\nexport function ratingMaxOf(field: CoercibleField): number {\n const rules = (field as WithRules).rules ?? [];\n const max = rules.find((r) => r?.kind === \"max\")?.value;\n const n = typeof max === \"number\" ? max : typeof max === \"string\" ? Number(max) : NaN;\n return Number.isInteger(n) && n >= 1 && n <= 10 ? n : 5;\n}\n\n// ── DURATION ───────────────────────────────────────────────────────────────────────────────\n\n/** Seconds as h:mm — and h:mm:ss only when there are seconds to show. 2700 → \"0:45\". */\nexport function durationText(seconds: unknown): string | null {\n const n = typeof seconds === \"number\" ? seconds : typeof seconds === \"string\" && seconds.trim() !== \"\" ? Number(seconds) : NaN;\n if (!Number.isFinite(n)) return null;\n return formatDurationSeconds(Math.round(n), { granularity: \"minute\", signed: true });\n}\n\n/**\n * A length of time as a person types it, in seconds. \"45\" and \"45 min\" are 45 minutes (the column\n * reads in h:mm, so a bare number is minutes, as in Airtable); \"1:30\" is an hour and a half;\n * \"1:30:15\" has seconds; \"1h 30m\", \"2 hours\", \"90s\" read as written. Null: not a length of time.\n */\nexport function readTypedDuration(text: string): number | null {\n const t = text.trim().toLowerCase();\n if (t === \"\") return null;\n const clock = /^(\\d+):([0-5]?\\d)(?::([0-5]?\\d))?$/.exec(t);\n if (clock) return Number(clock[1]) * 3600 + Number(clock[2]) * 60 + Number(clock[3] ?? 0);\n if (/^\\d+(\\.\\d+)?$/.test(t)) return Math.round(Number(t) * 60);\n const unitRe = /(\\d+(?:\\.\\d+)?)\\s*(h|hr|hrs|hour|hours|m|min|mins|minute|minutes|s|sec|secs|second|seconds)\\b/g;\n let total = 0;\n let matched = \"\";\n for (const m of t.matchAll(unitRe)) {\n const n = Number(m[1]);\n const u = m[2]!;\n total += u.startsWith(\"h\") ? n * 3600 : u.startsWith(\"m\") ? n * 60 : n;\n matched += m[0];\n }\n if (matched === \"\" || t.replace(unitRe, \"\").replace(/[\\s,and]+/g, \"\") !== \"\") return null;\n return Math.round(total);\n}\n\n// ── ADDRESS ────────────────────────────────────────────────────────────────────────────────\n\nexport const ADDRESS_PARTS = [\"street\", \"city\", \"region\", \"postal_code\", \"country\"] as const;\nexport type AddressPart = (typeof ADDRESS_PARTS)[number];\nexport type AddressValue = Partial<Record<AddressPart, string | null>>;\n\nexport function isAddressValue(value: unknown): value is AddressValue {\n return value !== null && typeof value === \"object\" && !Array.isArray(value)\n && Object.keys(value as object).every((k) => (ADDRESS_PARTS as readonly string[]).includes(k));\n}\n\n/** An address on one line, the way an envelope reads: \"1420 Cedar Ridge Pkwy, Boulder, CO 80301, US\". */\nexport function addressText(value: unknown): string | null {\n if (typeof value === \"string\") return value.trim() === \"\" ? null : value.trim();\n if (!isAddressValue(value)) return null;\n const part = (k: AddressPart) => (typeof value[k] === \"string\" ? (value[k] as string).trim() : \"\");\n const regionLine = [part(\"region\"), part(\"postal_code\")].filter(Boolean).join(\" \");\n const words = [part(\"street\"), part(\"city\"), regionLine, part(\"country\")].filter(Boolean).join(\", \");\n return words === \"\" ? null : words;\n}\n\n/**\n * One typed line as an address in parts. \"street, city, region postal, country\" is read into its\n * parts when it has that shape; anything else is kept whole as the street, so nothing typed is lost.\n */\nexport function addressFromText(text: string): AddressValue | null {\n const t = text.trim();\n if (t === \"\") return null;\n const bits = t.split(\",\").map((b) => b.trim()).filter(Boolean);\n if (bits.length >= 3) {\n const [street, city, regionPostal, ...rest] = bits;\n const rp = /^(.*?)\\s*([A-Za-z0-9-]*\\d[A-Za-z0-9 -]*)?$/.exec(regionPostal ?? \"\");\n const out: AddressValue = { street: street!, city: city! };\n if (rp?.[1]) out.region = rp[1];\n if (rp?.[2]) out.postal_code = rp[2].trim();\n if (rest.length > 0) out.country = rest.join(\", \");\n return out;\n }\n return { street: t };\n}\n\n/** A maps search for an address, opened in a new tab. */\nexport function addressMapUrl(value: unknown): string | null {\n const words = addressText(value);\n return words ? `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(words)}` : null;\n}\n\n// ── CREATED BY / LAST MODIFIED BY ──────────────────────────────────────────────────────────\n\n/**\n * `{id, name, actor}` as the store works it out (`custom.formula_value`). `actor` is the kind of\n * writer — a person (`user`), an agent (`agent`, with `id`/`name` the person it wrote for) or the\n * platform itself (`system`, with no id and no name). `name` is null for somebody outside the\n * organization. A value written before the store said `actor` is a person's.\n */\nexport type PersonStamp = { id: string | null; name: string | null; actor?: \"user\" | \"agent\" | \"system\" };\n\nexport function isPersonStamp(value: unknown): value is PersonStamp {\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) return false;\n const v = value as { id?: unknown; actor?: unknown };\n return typeof v.id === \"string\" || v.actor === \"system\" || v.actor === \"agent\";\n}\n\n/** The words for who made or changed a record. */\nexport const NOT_A_MEMBER_WORDS = \"Not in this organization\";\nexport const SYSTEM_WRITER_WORDS = \"System\";\nexport const AGENT_WRITER_WORDS = \"Agent\";\nexport function personStampText(value: unknown): string | null {\n if (!isPersonStamp(value)) return null;\n const name = value.name && value.name.trim() !== \"\" ? value.name : null;\n // V11 #6: a system write names the person who set it going, when the store knows who.\n if (value.actor === \"system\") return name ? `${SYSTEM_WRITER_WORDS} for ${name}` : SYSTEM_WRITER_WORDS;\n if (value.actor === \"agent\") return name ? `${AGENT_WRITER_WORDS} for ${name}` : AGENT_WRITER_WORDS;\n return name ?? NOT_A_MEMBER_WORDS;\n}\n\n// ── STATUS ─────────────────────────────────────────────────────────────────────────────────\n\nexport const STATUS_GROUPS = [\"todo\", \"in_progress\", \"done\"] as const;\nexport type StatusGroup = (typeof STATUS_GROUPS)[number];\nexport const STATUS_GROUP_WORDS: Record<StatusGroup, string> = { todo: \"To do\", in_progress: \"In progress\", done: \"Done\" };\n\n/** Which group a status choice sits in; a choice the column names no group for is To do. */\nexport function statusGroupOf(field: CoercibleField, choiceKey: string): StatusGroup {\n const groups = ((field.config ?? {}) as Record<string, unknown>)[\"status_groups\"];\n const g = groups && typeof groups === \"object\" ? (groups as Record<string, unknown>)[choiceKey] : undefined;\n return g === \"in_progress\" || g === \"done\" ? g : \"todo\";\n}\n\n// ── BARCODE ────────────────────────────────────────────────────────────────────────────────\n\nexport const BARCODE_SYMBOLOGIES = [\"code128\", \"code39\", \"ean13\", \"ean8\", \"upc_a\", \"upc_e\", \"itf\", \"qr\", \"data_matrix\", \"pdf417\"] as const;\n\n// ── RICH TEXT ──────────────────────────────────────────────────────────────────────────────\n\n/** Markdown read as plain words on one line — what a card title or a sentence shows. */\nexport function richTextPlain(text: string): string {\n return text\n .replace(/\\[([^\\]]+)\\]\\([^)]*\\)/g, \"$1\")\n .replace(/(\\*\\*|__|`)/g, \"\")\n .replace(/(^|\\s)[*_]([^*_\\s][^*_]*)[*_](?=\\s|$|[.,;:!?])/g, \"$1$2\")\n .replace(/^\\s*(#{1,3}|[-*+]|\\d+[.)])\\s+/gm, \"\")\n .replace(/\\s*\\n+\\s*/g, \" · \")\n .trim();\n}\n\n/**\n * A link destination as a browser would follow it: HTML entities and backslash escapes decoded,\n * spaces and control characters removed (`custom._entity_decoded`'s rules). Then only a web page,\n * an email address or a phone number is a link here; everything else is drawn as words.\n */\nexport function safeLinkHref(raw: string): string | null {\n let t = raw\n .replace(/&#[xX]([0-9a-fA-F]{1,8});?/g, (_, h: string) => codePoint(parseInt(h, 16)))\n .replace(/&#([0-9]{1,8});?/g, (_, d: string) => codePoint(parseInt(d, 10)))\n .replace(/:/g, \":\").replace(/	/g, \"\\t\").replace(/
/g, \"\\n\").replace(///g, \"/\")\n .replace(/(/g, \"(\").replace(/)/g, \")\").replace(/./g, \".\").replace(/ /g, \" \")\n .replace(/</g, \"<\").replace(/>/g, \">\").replace(/"/g, '\"').replace(/'/g, \"'\").replace(/&/g, \"&\")\n .replace(/\\\\([!-/:-@[-`{-~])/g, \"$1\");\n t = t.replace(/[\\s\\u0000-\\u001f\\u007f]/g, \"\");\n return /^(https?:\\/\\/|mailto:|tel:)/i.test(t) ? t : null;\n}\n\nfunction codePoint(n: number): string {\n return Number.isFinite(n) && n >= 1 && n <= 0x10ffff && !(n >= 0xd800 && n <= 0xdfff) ? String.fromCodePoint(n) : \"\\ufffd\";\n}\n\n// ── ONE VALUE IN WORDS ─────────────────────────────────────────────────────────────────────\n\n/**\n * The words one value of a newer kind reads as wherever only words fit — an export cell, a card,\n * a sentence. Null: not a newer kind, or nothing particular to say (the caller's own words apply).\n */\nexport function newerKindWords(field: CoercibleField, value: unknown): string | null {\n if (value === null || value === undefined || value === \"\") return null;\n switch (newerKindOf(field)) {\n case \"duration\":\n return durationText(value);\n case \"address\":\n return addressText(value);\n case \"created_by\":\n case \"modified_by\":\n return personStampText(value);\n case \"rich_text\":\n return typeof value === \"string\" ? richTextPlain(value) : null;\n case \"rating\": {\n const n = typeof value === \"number\" ? value : Number(value);\n return Number.isFinite(n) ? `${n} / ${ratingMaxOf(field)}` : null;\n }\n default:\n return null;\n }\n}\n\n/**\n * The words one value of a newer kind is EXPORTED or COPIED as: an address on one line, a length of\n * time as h:mm, who by name. A rating, a count and a barcode stay what they are, and rich text\n * stays its Markdown. Null: the value is written as it is stored.\n */\nexport function newerKindExportWords(field: CoercibleField, value: unknown): string | null {\n const kind = newerKindOf(field);\n if (kind === \"address\" || kind === \"duration\" || kind === \"created_by\" || kind === \"modified_by\") {\n return newerKindWords(field, value);\n }\n return null;\n}\n","// src/typedDate.ts — @ai-matrx/records\n//\n// A DATE A PERSON TYPED, READ THE WAY THEIR SPREADSHEET READS IT — OR REFUSED.\n//\n// 🚨 WHY THIS EXISTS (merged-grid cold review, 2026-09-26). A person selected a Visit Date cell\n// and typed `10/03/2026`; the store kept **Jan 31, 0026**. `2026-10-03` became **Feb 6, 1003**.\n// The keystrokes after the first landed in the browser's segmented date control one segment at a\n// time and every half-typed segment was a \"valid\" date. The reading that replaces that is this\n// one, and it is the ONE reading every typed-date door uses (`coerceTypedAnswer` — the grid's\n// type-to-edit, a paste, a public form, an import).\n//\n// THE RULES (Google Sheets and Airtable are the bar):\n// · `2026-10-03` (and `2026/10/03`) is year-month-day, everywhere.\n// · `10/03/2026` is read in the reader's own LOCALE order — Oct 3 in en-US, 10 March in en-GB —\n// exactly as their spreadsheet would. The locale is the caller's to pass; the default is the\n// runtime's.\n// · A month may be a word: `Oct 3, 2026`, `3 October 2026`, `Saturday, October 3, 2026`.\n// · A TWO-DIGIT YEAR IS READ THE WAY GOOGLE SHEETS READS IT (grids review 3, 2026-09-30):\n// `10/03/26` is Oct 3, 2026 — 00–29 is 2000–2029 and 30–99 is 1930–1999, Sheets' and Excel's\n// own window, so a birthday typed `5/12/85` is 1985 and a visit typed `10/03/26` is this year.\n// Airtable and Sheets both take it; refusing it was the one place this reader was stricter\n// than every spreadsheet a person has used.\n// · NOTHING ELSE IS GUESSED. A missing year (which year?), a one- or three-digit year, a\n// month 0 or 13, a 31 September, a year before 1000 (that is a half-typed year, not a date\n// anybody means) — each is REFUSED with a sentence saying how to write it.\n// · A time of day may follow (`10/03/2026 2:30 PM`, `2026-10-03T14:30`, an ISO instant with its\n// zone). A day-only column keeps the DAY that was written; a date-and-time column keeps the\n// instant, read in the reader's own zone unless the text names one.\n\nexport type TypedDate =\n | { ok: true; day: string; /** ISO instant, only when a time of day was read or asked for. */ instant: string | null }\n | { ok: false; why: string };\n\nconst MONTHS: Record<string, number> = {\n jan: 1, january: 1, feb: 2, february: 2, mar: 3, march: 3, apr: 4, april: 4, may: 5, jun: 6, june: 6,\n jul: 7, july: 7, aug: 8, august: 8, sep: 9, sept: 9, september: 9, oct: 10, october: 10,\n nov: 11, november: 11, dec: 12, december: 12,\n};\nconst WEEKDAYS = /\\b(mon|tue|tues|wed|thu|thur|thurs|fri|sat|sun)(day|nesday|sday|urday)?\\b\\.?,?/gi;\n\n/** The order a locale writes a numeric date in: \"mdy\", \"dmy\" or \"ymd\". */\nexport function localeDateOrder(locale?: string): \"mdy\" | \"dmy\" | \"ymd\" {\n try {\n const parts = new Intl.DateTimeFormat(locale, { year: \"numeric\", month: \"2-digit\", day: \"2-digit\" }).formatToParts(\n new Date(2026, 10, 22),\n );\n const order = parts\n .filter((p) => p.type === \"year\" || p.type === \"month\" || p.type === \"day\")\n .map((p) => p.type.charAt(0))\n .join(\"\");\n if (order === \"mdy\" || order === \"dmy\" || order === \"ymd\") return order;\n } catch {\n // An unknown locale reads like the runtime's own.\n }\n return \"mdy\";\n}\n\nfunction pad(n: number, width = 2): string {\n return String(n).padStart(width, \"0\");\n}\n\nfunction daysIn(year: number, month: number): number {\n return new Date(Date.UTC(year, month, 0)).getUTCDate();\n}\n\nconst HOW = \"Write it as 2026-10-03, or with the year in full the way your spreadsheet writes dates.\";\n\n/** Sheets' and Excel's two-digit-year window: 00–29 → 2000–2029, 30–99 → 1930–1999. */\nexport function twoDigitYear(yy: number): number {\n return yy < 30 ? 2000 + yy : 1900 + yy;\n}\n\nfunction checkDay(year: number, month: number, day: number, raw: string): string | null {\n if (year < 1000 || year > 9999) return `“${raw}” has no four-digit year. ${HOW}`;\n if (month < 1 || month > 12) return `“${raw}” names no month (month ${month}). ${HOW}`;\n if (day < 1 || day > daysIn(year, month)) return `“${raw}” is not a day on the calendar. ${HOW}`;\n return null;\n}\n\n// ───────────────────────────────────────────────────────────────────────────\n// A TIME OF DAY A PERSON TYPED (Arman, 2026-09-28: \"if I type 1200PM, I get 2:00PM because the\n// one isn't recorded\"). The grid opens a plain text box holding every key typed; this reads it\n// ONCE, on commit — for a time column and for the time after a date-and-time column's day.\n//\n// · 12-hour with a meridiem, any spelling: `1200PM`, `12pm`, `12:00 PM`, `9:30a`, `9:30 a.m.`,\n// `930am`, `2 PM`.\n// · 24-hour with minutes: `14:30`, `14:30:05`, and the four-digit clock `1200`, `0930`.\n// · NOTHING IS GUESSED. A bare hour (`9` — morning or evening?), three bare digits (`200`),\n// `13pm`, `0am`, `25:00`, `12:60`, `2400` — each is refused with the way to write it.\n// ───────────────────────────────────────────────────────────────────────────\n\nexport type TypedTime = { ok: true; /** 24-hour `HH:MM`, or `HH:MM:SS` when seconds were typed. */ value: string } | { ok: false; why: string };\n\nconst TIME_HOW = \"Write it like 2:30 PM or 14:30.\";\nconst MERIDIEM = String.raw`([ap])\\.?\\s*(?:m\\.?)?`;\nconst CLOCK = new RegExp(String.raw`^(\\d{1,2})(?::(\\d{2})(?::(\\d{2}))?)?\\s*(?:${MERIDIEM})?$`, \"i\");\nconst COMPACT = new RegExp(String.raw`^(\\d{3,4})\\s*(?:${MERIDIEM})?$`, \"i\");\n\n/** Read one typed time of day. Never throws; never guesses. */\nexport function readTypedTime(raw: string): TypedTime {\n const text = raw.trim();\n if (text === \"\") return { ok: false, why: \"Nothing was typed.\" };\n let h: number;\n let m: number;\n let s: number | null = null;\n let meridiem: string | undefined;\n const clock = CLOCK.exec(text);\n const compact = clock ? null : COMPACT.exec(text);\n if (clock) {\n h = Number(clock[1]);\n meridiem = clock[4]?.toLowerCase();\n if (clock[2] === undefined && !meridiem) {\n return { ok: false, why: `“${raw}” could be in the morning or the evening. ${TIME_HOW}` };\n }\n m = clock[2] === undefined ? 0 : Number(clock[2]);\n if (clock[3] !== undefined) s = Number(clock[3]);\n } else if (compact) {\n const digits = compact[1]!;\n meridiem = compact[2]?.toLowerCase();\n if (digits.length === 3 && !meridiem) {\n return { ok: false, why: `“${raw}” could be in the morning or the evening. ${TIME_HOW}` };\n }\n h = Number(digits.slice(0, digits.length - 2));\n m = Number(digits.slice(-2));\n } else {\n return { ok: false, why: `“${raw}” is not a time of day. ${TIME_HOW}` };\n }\n if (meridiem) {\n if (h < 1 || h > 12) return { ok: false, why: `“${raw}” is not a time of day. ${TIME_HOW}` };\n if (meridiem === \"p\" && h !== 12) h += 12;\n if (meridiem === \"a\" && h === 12) h = 0;\n }\n if (h > 23 || m > 59 || (s !== null && s > 59)) return { ok: false, why: `“${raw}” is not a time of day. ${TIME_HOW}` };\n return { ok: true, value: [h, m, ...(s === null ? [] : [s])].map((n) => pad(n)).join(\":\") };\n}\n\n/**\n * A time of day at the END of the text: `14:30`, `2:30 pm`, `2 PM`, `1200PM`, `14:30:05`. A bare\n * number (`2026` after `Oct 3`) is never a time — only one with a colon or a meridiem is.\n */\nfunction splitTime(text: string): { date: string; time: { h: number; m: number; s: number } | null; bad: boolean } {\n const match = /(?:^|[\\sT,]+)(?:at\\s+)?(\\d{1,4}(?::\\d{2}){0,2}\\s*(?:[ap]\\.?\\s*(?:m\\.?)?)?)$/i.exec(text);\n if (!match) return { date: text, time: null, bad: false };\n const token = match[1]!;\n if (!/:|[ap]\\.?\\s*(?:m\\.?)?$/i.test(token)) return { date: text, time: null, bad: false };\n const read = readTypedTime(token);\n if (!read.ok) return { date: text, time: null, bad: true };\n const [h, m, s] = read.value.split(\":\").map(Number);\n return { date: text.slice(0, match.index).replace(/[\\s,]+$/, \"\"), time: { h: h!, m: m!, s: s ?? 0 }, bad: false };\n}\n\n/**\n * Read one typed date. `withTime` is true for a date-and-time column. Never throws; never guesses.\n */\nexport function readTypedDate(raw: string, opts: { withTime?: boolean; locale?: string } = {}): TypedDate {\n const text = raw.trim();\n if (text === \"\") return { ok: false, why: \"Nothing was typed.\" };\n\n // AN ISO INSTANT WITH ITS ZONE (`…Z`, `…+02:00`) is a moment, placed exactly.\n const iso = /^(\\d{4})-(\\d{2})-(\\d{2})[T ](\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.\\d+)?)?(Z|[+-]\\d{2}:?\\d{2})$/i.exec(text);\n if (iso) {\n const bad = checkDay(Number(iso[1]), Number(iso[2]), Number(iso[3]), raw);\n if (bad) return { ok: false, why: bad };\n const at = new Date(Date.parse(text));\n if (Number.isNaN(at.getTime())) return { ok: false, why: `“${raw}” is not a moment this can read. ${HOW}` };\n const day = `${at.getFullYear()}-${pad(at.getMonth() + 1)}-${pad(at.getDate())}`;\n return { ok: true, day, instant: opts.withTime ? at.toISOString() : null };\n }\n\n const split = splitTime(text.replace(/(\\d)T(\\d)/, \"$1 $2\"));\n if (split.bad) return { ok: false, why: `“${raw}” has a time of day that is not one. Write it as 14:30 or 2:30 PM.` };\n const dateText = split.date.replace(WEEKDAYS, \" \").replace(/\\s+/g, \" \").trim();\n\n let year: number | null = null;\n let month: number | null = null;\n let day: number | null = null;\n\n const numeric = /^(\\d{1,4})[/.\\-](\\d{1,2})[/.\\-](\\d{1,4})$/.exec(dateText);\n // DIGITS WITH NO SEPARATORS (Arman 2026-10-08: typing `10142026` did nothing): MMDDYYYY / DDMMYYYY by\n // the locale's order, MMDDYY (the two-digit-year window), and YYYYMMDD when that is the only reading.\n const compact = /^(\\d{8}|\\d{6})$/.exec(dateText);\n if (compact) {\n const digits = compact[1]!;\n const order = localeDateOrder(opts.locale);\n const first2 = Number(digits.slice(0, 2));\n const mid2 = Number(digits.slice(2, 4));\n if (digits.length === 6) {\n year = twoDigitYear(Number(digits.slice(4)));\n if (order === \"dmy\") {\n day = first2;\n month = mid2;\n } else {\n month = first2;\n day = mid2;\n }\n } else {\n const yearLast = Number(digits.slice(4));\n const yearFirst = Number(digits.slice(0, 4));\n const localeReading = (): [number, number, number] =>\n order === \"dmy\" ? [yearLast, mid2, first2] : [yearLast, first2, mid2];\n const [ly, lm, ld] = localeReading();\n const isoReading: [number, number, number] = [yearFirst, Number(digits.slice(4, 6)), Number(digits.slice(6))];\n const fits = (y: number, m: number, d: number) => checkDay(y, m, d, raw) === null;\n const useIso = order === \"ymd\" || (!fits(ly, lm, ld) && fits(...isoReading));\n [year, month, day] = useIso ? isoReading : [ly, lm, ld];\n }\n } else if (numeric) {\n const [a, b, c] = [numeric[1]!, numeric[2]!, numeric[3]!];\n if (a.length === 4) {\n year = Number(a);\n month = Number(b);\n day = Number(c);\n } else {\n if (c.length !== 4 && c.length !== 2) {\n return { ok: false, why: `“${raw}” has no four-digit year. ${HOW}` };\n }\n const order = localeDateOrder(opts.locale);\n year = c.length === 2 ? twoDigitYear(Number(c)) : Number(c);\n if (order === \"dmy\") {\n day = Number(a);\n month = Number(b);\n } else {\n month = Number(a);\n day = Number(b);\n }\n }\n } else {\n // A MONTH WRITTEN AS A WORD, in any order with a day and a four-digit year.\n const tokens = dateText.toLowerCase().replace(/,/g, \" \").split(/[\\s/.\\-]+/).filter(Boolean);\n const numbers: string[] = [];\n for (const token of tokens) {\n const word = token.replace(/\\.$/, \"\");\n if (/^\\d+(st|nd|rd|th)?$/.test(word)) numbers.push(word.replace(/\\D/g, \"\"));\n else if (word in MONTHS) {\n if (month !== null) return { ok: false, why: `“${raw}” names two months. ${HOW}` };\n month = MONTHS[word]!;\n } else if (word !== \"of\" && word !== \"the\") {\n return { ok: false, why: `“${raw}” is not a date this can read. ${HOW}` };\n }\n }\n if (month === null) return { ok: false, why: `“${raw}” is not a date this can read. ${HOW}` };\n const years = numbers.filter((n) => n.length === 4);\n const days = numbers.filter((n) => n.length <= 2);\n if (years.length === 0) {\n return {\n ok: false,\n why: numbers.some((n) => n.length === 3)\n ? `“${raw}” has no four-digit year. ${HOW}`\n : `“${raw}” has no year, and which year it means is a guess. Write the year too (${\n days[0] ?? \"3\"\n } ${Object.keys(MONTHS).find((k) => MONTHS[k] === month && k.length > 3) ?? \"October\"} 2026).`,\n };\n }\n if (years.length > 1 || days.length !== 1 || numbers.length !== 2) {\n return { ok: false, why: `“${raw}” is not a date this can read. ${HOW}` };\n }\n year = Number(years[0]);\n day = Number(days[0]);\n }\n\n const bad = checkDay(year!, month!, day!, raw);\n if (bad) return { ok: false, why: bad };\n const dayText = `${pad(year!, 4)}-${pad(month!)}-${pad(day!)}`;\n if (!opts.withTime) return { ok: true, day: dayText, instant: null };\n const t = split.time ?? { h: 0, m: 0, s: 0 };\n const local = new Date(year!, month! - 1, day!, t.h, t.m, t.s);\n return { ok: true, day: dayText, instant: local.toISOString() };\n}\n\n/**\n * THE TEXT AN EDITOR OPENS WITH for a stored day or instant — the inverse of `readTypedDate`, so\n * a cell opened by Enter or a double-click shows words this reader reads back to the SAME value\n * (grids review 3, 2026-09-30: those paths used to open the browser's own date picker, which lost\n * the time and saved \"now\"). The day is numeric in the locale's own order with a four-digit year;\n * a time of day follows as `2:30 PM` (a 12-hour locale) or `14:30` (a 24-hour one). An unreadable\n * stored value opens as the stored characters, untouched.\n */\nexport function typedDateText(value: unknown, opts: { withTime?: boolean; locale?: string } = {}): string {\n if (value === null || value === undefined || value === \"\") return \"\";\n const text = String(value).trim();\n const bare = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(text);\n let y: number;\n let mo: number;\n let d: number;\n let at: Date | null = null;\n if (bare) {\n y = Number(bare[1]);\n mo = Number(bare[2]);\n d = Number(bare[3]);\n } else {\n const parsed = value instanceof Date ? value : new Date(Date.parse(text));\n if (Number.isNaN(parsed.getTime())) return text;\n at = parsed;\n y = parsed.getFullYear();\n mo = parsed.getMonth() + 1;\n d = parsed.getDate();\n }\n const order = localeDateOrder(opts.locale);\n const day =\n order === \"ymd\" ? `${pad(y, 4)}-${pad(mo)}-${pad(d)}` : order === \"dmy\" ? `${pad(d)}/${pad(mo)}/${pad(y, 4)}` : `${pad(mo)}/${pad(d)}/${pad(y, 4)}`;\n if (!opts.withTime || !at) return day;\n return `${day} ${typedTimeText(at.getHours(), at.getMinutes(), opts.locale)}`;\n}\n\n/** A time of day in the words `readTypedTime` reads back: `2:30 PM` or `14:30`, by the locale's clock. */\nexport function typedTimeText(hours: number, minutes: number, locale?: string): string {\n let twelve = true;\n try {\n twelve = new Intl.DateTimeFormat(locale, { hour: \"numeric\" }).resolvedOptions().hour12 !== false;\n } catch {\n // An unknown locale reads like the runtime's own.\n }\n if (!twelve) return `${pad(hours)}:${pad(minutes)}`;\n const h = hours % 12 === 0 ? 12 : hours % 12;\n return `${h}:${pad(minutes)} ${hours < 12 ? \"AM\" : \"PM\"}`;\n}\n","// src/typedNumber.ts — @ai-matrx/records\n//\n// A NUMBER A PERSON TYPED, READ THE WAY THEY WROTE IT — OR REFUSED.\n//\n// PEOPLE WRITE NUMBERS THE WAY PEOPLE WRITE NUMBERS. \"$1,250.50\", \"(300)\" for a negative, \"12%\",\n// \"2019\" — all of them are the number that was meant, and refusing them makes the column people use\n// most the one the product cannot take. A MINUS IS A MINUS WHEREVER THE SYMBOL SITS (merged-grid\n// review 2, A9): \"-$150\", \"$-150\" and \"(150)\" are all −150 — Sheets and Airtable read every one of\n// them. Everything before the first digit is symbols and signs; an odd number of minus signs there\n// is a negative.\n//\n// This is the ONE reading: `coerceTypedAnswer` (every record-store door) and the older `/data` grid's\n// typed cells both call it, so a number typed into any grid is read the same way.\n\nexport type TypedNumber = { ok: true; value: number } | { ok: false; why: string };\n\nexport function readTypedNumber(raw: string): TypedNumber {\n const text = raw.trim();\n if (text === \"\") return { ok: false, why: \"Nothing was typed.\" };\n const bracketed = /^\\(.*\\)$/.test(text);\n const inner = (bracketed ? text.slice(1, -1) : text).replace(/[\\s,]/g, \"\");\n const head = /^[^\\d.]*/.exec(inner)?.[0] ?? \"\";\n const bare = inner.slice(head.length).replace(/[^\\d.eE+-]+$/, \"\");\n const minus = (head.match(/[-−]/g) ?? []).length % 2 === 1;\n const n = Number(bare);\n if (bare === \"\" || Number.isNaN(n) || /[\\d]/.test(head)) {\n return {\n ok: false,\n why: `“${raw}” is not a number. Write digits, like 1,250.50 — a currency symbol, a percent sign, commas and a minus sign or brackets for a negative are all fine.`,\n };\n }\n return { ok: true, value: bracketed !== minus ? -n : n };\n}\n","// src/coerce.ts — @ai-matrx/records\n//\n// WHAT A TYPED-IN ANSWER MEANS TO A FIELD, WORKED OUT IN ONE PLACE.\n//\n// Every door into this store that a person types into hands over STRINGS. A\n// browser input is a string. A clipboard cell is a string. A CSV column is a\n// string. A public form's answer is a string. A Field does not take strings —\n// it takes a number, a tick, a day, the id of an option — and until the string\n// has been turned into one of those, the store refuses it.\n//\n// 🚨 WHY THIS FILE EXISTS (AGENT-BUILDS-2's closing walk, 2026-09-21). A real\n// customer picked a time on Ironclad Mobile Mechanic's public booking page,\n// typed their name, phone, email and vehicle, pressed \"Book it\", and the page\n// answered:\n//\n// \"Vehicle Year takes a number, and it was given a string\"\n//\n// The appointment was not made. `BookingPicker` kept its answers as\n// `Record<string, string>` and the confirm route passed them through\n// uncoerced, so a booking page over ANY table with a number field was\n// unbookable by anybody. The store was right and said so plainly; nothing\n// between the person and the store had done the one job that stood between\n// them.\n//\n// AND IT WAS ALREADY SOLVED, TWICE, IN THE WRONG PLACE. The grid's paste\n// (`@ai-matrx/records-ui` `valueFromPastedText`) and the import wizard each\n// carried their own coercion — good ones — while the two PUBLIC doors, the\n// ones a stranger uses, carried none. That is the shape the platform-primitive\n// rule exists to stop: a capability scoped to the feature that surfaced it.\n// So the coercion lives HERE, headless, in the package both the screens and\n// the servers already depend on, and every one of those five doors calls it.\n//\n// THREE RULES IT KEEPS:\n//\n// 1. A VALUE IS COERCED, NEVER GUESSED. Writing 0 for \"about twelve\" or\n// today's date for \"soon\" is how a table quietly becomes untrue. A string\n// that is not the thing is REFUSED, by name, in words the person can act\n// on — \"Vehicle Year holds a number, and “twenty nineteen” is not one\".\n// 2. THE REFUSAL NAMES THE FIELD IN PLAIN WORDS, never its key, because the\n// person reading it is looking at a label on a screen.\n// 3. IT DECIDES NOTHING ELSE. Required, the Rules, the cap, the rate window\n// and every wall are the store's. This turns a string into a value and\n// stops.\n\nimport { isButtonField } from \"./button\";\nimport { addressFromText, newerKindOf, ratingMaxOf, readTypedDuration } from \"./newerKinds\";\nimport { readTypedDate } from \"./typedDate\";\nimport { readTypedNumber } from \"./typedNumber\";\nimport type { Field } from \"./field\";\nimport { isUuidShape } from \"@ai-matrx/kit/uuid\";\n\n/**\n * EVERYTHING THIS FILE NEEDS TO KNOW ABOUT A FIELD, AND NOTHING ELSE.\n *\n * A full `Field` satisfies it, and so does every narrower shape the store\n * hands to a screen — `CaptureField` on a phone with no network, the Field\n * documents `custom.form_public` and `custom.booking_public` publish to a\n * stranger. Asking for the whole `Field` would have meant the capture sheet\n * keeping its own `Number(raw)`, which is exactly the duplication this file\n * exists to end.\n */\nexport interface CoercibleField {\n /** Named so the doc above is checkable: a full {@link Field} satisfies this. */\n key: string;\n label?: string | null;\n name?: string | null;\n /** FLD-1. One of five behaviours; an unknown word is treated as text. */\n type?: string | null;\n multi?: boolean | null;\n unit?: string | null;\n format?: string | null;\n config?: Record<string, unknown> | null | undefined;\n relation_target?: string | null;\n}\n\n// ───────────────────────────────────────────────────────────────────────────\n// WHICH KIND OF THING A FIELD HOLDS\n// ───────────────────────────────────────────────────────────────────────────\n\n/**\n * The kind of value one Field holds, resolved from its DECLARATION only — the\n * behaviour, the modifiers, the format, the relation target and the config,\n * because those are what the store stores. A screen that guessed from a value\n * would answer differently for an empty record.\n *\n * `plain_text`, `long_text`, `number` and `boolean` are behaviours that carry\n * no parity type of their own; every other word here is a parity type.\n */\nexport const FIELD_KINDS = [\n \"attachment\",\n \"checkbox\",\n \"currency\",\n \"datetime\",\n \"email\",\n \"entity_reference\",\n \"formula\",\n \"json\",\n \"long_text\",\n \"lookup\",\n \"member\",\n \"multi_select\",\n \"number\",\n \"percent\",\n \"phone\",\n \"relation\",\n \"rollup\",\n \"select\",\n \"text\",\n \"url\",\n] as const;\n\n/**\n * 🚨 THE UNION IS DERIVED FROM THE LIST ABOVE, and the list is what the\n * coverage test walks. Add a kind here and the test drives it; give it no arm\n * in `coerceTypedAnswer` and the test says so, instead of the new kind falling\n * quietly through `default:` and a value of the wrong shape reaching the store\n * — which is the whole defect this file closes.\n */\nexport type FieldKind = (typeof FIELD_KINDS)[number];\n\n/** The kernel Table ids REC-31 pins the two special relations to. */\nexport interface KernelTargets {\n /** The kernel `File` Table — a relation pointing here is an attachment. */\n file?: string | undefined;\n /** The kernel `Person` Table — a relation pointing here names a person. */\n person?: string | undefined;\n}\n\n/**\n * Which kind of value this Field holds.\n *\n * This is the ONE classifier. `@ai-matrx/records-ui`'s `editorKindFor` — which\n * decides which editor to DRAW — delegates to it, so the editor a person types\n * into and the coercion applied to what they typed can never disagree about\n * the same column. They did disagree before this moved here; that is the whole\n * class of grid defect September was spent closing.\n */\nexport function fieldKindFor(field: CoercibleField, kernel?: KernelTargets): FieldKind {\n const format = (field.format ?? \"\").toLowerCase();\n const config = field.config ?? {};\n switch (field.type) {\n case \"boolean\":\n return \"checkbox\";\n case \"text\":\n if (format === \"email\") return \"email\";\n if (format === \"phone\") return \"phone\";\n if (format === \"url\") return \"url\";\n if (format === \"json\" || config[\"json\"] === true) return \"json\";\n if (format === \"long\" || config[\"multiline\"] === true) return \"long_text\";\n return \"text\";\n case \"range\": {\n const kind = String(config[\"kind\"] ?? \"number\");\n if (kind === \"date\" || kind === \"datetime\") return \"datetime\";\n // THE FORMAT, AND ONLY THE FORMAT. A unit is not a currency: a quantity\n // in `units` drawn as money is how three of something rendered \"units3\".\n if (format === \"currency\") return \"currency\";\n if (format === \"percent\" || field.unit === \"%\") return \"percent\";\n return \"number\";\n }\n case \"list\":\n return field.multi ? \"multi_select\" : \"select\";\n case \"relation\": {\n // SC-R / P12: a relation whose target mode is `any`, restricted by\n // `config.allowed_types` to the platform kinds it may name (notes, web\n // sites, agents …). Its value is `{token, id}`, never a record id.\n if (isEntityReferenceConfig(config)) return \"entity_reference\";\n const parity = String(config[\"parity\"] ?? \"\");\n if (parity === \"attachment\" || parity === \"member\") return parity;\n if (kernel?.file && field.relation_target === kernel.file) return \"attachment\";\n if (kernel?.person && field.relation_target === kernel.person) return \"member\";\n return \"relation\";\n }\n case \"formula\": {\n const via = config[\"via\"];\n if (via && config[\"agg\"]) return \"rollup\";\n if (via) return \"lookup\";\n return \"formula\";\n }\n default:\n return \"text\";\n }\n}\n\n// ───────────────────────────────────────────────────────────────────────────\n// THE COERCION\n// ───────────────────────────────────────────────────────────────────────────\n\n/** One Field's options, indexed the way a typed word has to be looked up. */\nexport interface ChoiceOptions {\n /**\n * Lower-cased label, option key and option id, each pointing at the OPTION\n * RECORD'S ID — which is what a list Field stores (FLD-5/FLD-6).\n */\n byWord: Map<string, string>;\n /** The labels, in order, so a refusal can say what is on offer. */\n labels: string[];\n}\n\n/** What one answer became, or the sentence saying why it did not. */\nexport type Coerced =\n | { value: unknown; words?: string }\n | { refusal: string; unmatched?: string[]; unmatchedNames?: string[]; ambiguousNames?: string[] };\n\n// ───────────────────────────────────────────────────────────────────────────\n// A RELATION FILLED BY NAME (VISION-REACH G1, 2026-10-02)\n// ───────────────────────────────────────────────────────────────────────────\n//\n// A spreadsheet names the record a row points at — \"Dr. Marisol Gutierrez\" —\n// and Airtable fills a linked-record cell from exactly that: the primary\n// field's text. Until this lane every door here refused it (\"chosen rather than\n// typed\"), and the sitting's clinic file needed 264 hand picks. The store now\n// matches a name through ONE matcher (`custom._relation_names_resolve`); a\n// screen asks it in one call per column (`relationNamesMatch`) and hands the\n// answer to the coercion below as a `RelationNameIndex`, exactly as a choice\n// column hands its `ChoiceOptions`. One match is that record; none, or two\n// records of one name, is REFUSED and named — never guessed.\n\n/** One record a name matched. */\nexport interface RelationNameMatch {\n id: string;\n /** What its name column holds. */\n words: string;\n}\n\n/** A relation's target Table, indexed by the names a column holds. */\nexport interface RelationNameIndex {\n /** `relationNameKey(name)` → the records of that name the reader may see. */\n byName: ReadonlyMap<string, readonly RelationNameMatch[]>;\n /** The target Table's name, for the sentence. */\n tableName: string;\n /** The target Table's name column, so a screen can make a record a name matched nothing. */\n titleField: string | null;\n}\n\n/** The answer of `custom.relation_names_match`. */\nexport interface RelationNamesAnswer {\n table_id: string;\n table_name: string | null;\n title_field?: string | null;\n names: Array<{ name: string; matches: RelationNameMatch[] }>;\n}\n\n/**\n * How two spellings of one name are told apart: case and runs of spacing do not\n * count. The store's `custom.relation_name_key` is the same rule.\n */\nexport function relationNameKey(name: string): string {\n return name.trim().replace(/\\s+/g, \" \").toLowerCase();\n}\n\n/** How an import splits a cell that holds several values: `;`, `,` or `|` — the store's own rule. */\nexport function splitListCell(text: string): string[] {\n return text\n .split(/\\s*[;,|]\\s*/)\n .map((w) => w.trim())\n .filter((w) => w.length > 0);\n}\n\n/** Index the store's answer the way the coercion reads it. */\nexport function relationNameIndexOf(answer: RelationNamesAnswer): RelationNameIndex {\n const byName = new Map<string, RelationNameMatch[]>();\n for (const entry of answer.names ?? []) {\n const key = relationNameKey(entry.name ?? \"\");\n if (key !== \"\" && !byName.has(key)) byName.set(key, entry.matches ?? []);\n }\n return { byName, tableName: answer.table_name ?? \"that table\", titleField: answer.title_field ?? null };\n}\n\n/** Every name a relation cell carries, split the way the column reads it. */\nexport function relationNamesIn(field: Pick<CoercibleField, \"multi\">, text: string): string[] {\n const t = text.trim();\n if (t === \"\") return [];\n const parts = field.multi ? splitListCell(t) : [t];\n return parts.filter((p) => !isUuidShape(p));\n}\n\nexport interface CoerceOptions {\n /** This Field's choices, when the caller has loaded them. */\n options?: ChoiceOptions | undefined;\n /** The kernel File/Person Table ids, for classifying relations. */\n kernel?: KernelTargets | undefined;\n /**\n * The Field's name as this caller writes it. Defaults to the Field's own\n * label. A screen passes the same words it printed above the box.\n */\n label?: string | undefined;\n /**\n * A relation column's target, indexed by name (`relationNameIndexOf`). Without it a name in a\n * relation column is refused as before, and only ids pass.\n */\n relationNames?: RelationNameIndex | undefined;\n /** The reader's locale, for the order of a numeric date (10/03/2026). Defaults to the runtime's. */\n locale?: string | undefined;\n /**\n * The column SHOWS AS a percent (its \"Shows as\" format, which this package does not read), so a\n * typed `45%` is a percent here even on a number column. The caller scales it to the column's\n * own percent scale.\n */\n showsAsPercent?: boolean | undefined;\n}\n\n/** The money symbols people type, and the currency each one names. `¥` names two, so it names none. */\nconst MONEY_SYMBOLS: Record<string, string> = { $: \"USD\", \"€\": \"EUR\", \"£\": \"GBP\", \"₹\": \"INR\", \"₩\": \"KRW\", \"₪\": \"ILS\", \"₺\": \"TRY\", \"₽\": \"RUB\" };\n\n/** The currency a money column keeps (`unit` holds a symbol or an ISO code), or null when it names none. */\nfunction columnCurrency(unit: string | null | undefined): string | null {\n const u = (unit ?? \"\").trim();\n if (u === \"\") return null;\n if (MONEY_SYMBOLS[u]) return MONEY_SYMBOLS[u]!;\n return /^[A-Za-z]{3}$/.test(u) ? u.toUpperCase() : null;\n}\n\n/**\n * A UNIT THAT DOES NOT BELONG TO THE COLUMN IS REFUSED, NEVER DROPPED (grids review 3,\n * 2026-09-30): `45%` typed into a Number column was kept as 45 and into a Money column as $45.00 —\n * the percent sign silently thrown away, a value off by a hundred times. Sheets reads `45%` as the\n * percent it is; a column that does not keep percents says so, with the way to write its own kind.\n */\nfunction foreignUnit(kind: \"number\" | \"currency\" | \"percent\", text: string, label: string, raw: string, unit: string | null | undefined, showsAsPercent: boolean): string | null {\n const hasPercent = /%/.test(text);\n const symbols = [...text].filter((ch) => ch in MONEY_SYMBOLS);\n if (kind === \"percent\" || showsAsPercent) {\n if (symbols.length > 0) return `${label} holds a percent, and “${raw}” is an amount of money. Write it like 45%.`;\n return null;\n }\n if (hasPercent) {\n return kind === \"currency\"\n ? `${label} holds an amount of money, and “${raw}” is a percent. Write the amount, like $1,250.50.`\n : `${label} holds a number, and “${raw}” is a percent, which this column does not show. Write 45 or 0.45 — or make the column show as a percent.`;\n }\n if (kind === \"currency\") {\n const own = columnCurrency(unit);\n const typed = symbols.map((ch) => MONEY_SYMBOLS[ch]!);\n if (own && typed.some((code) => code !== own)) {\n return `${label} holds an amount in ${own}, and “${raw}” is written in ${typed.find((code) => code !== own)}. Write the amount in ${own}.`;\n }\n }\n return null;\n}\n\n/** One id, written the way the store writes one (REC-51's shape test). */\n\n/**\n * SC-R / P12. Whether a relation's config makes it an ENTITY REFERENCE: target\n * mode `any` (REL-8) restricted by a non-empty `allowed_types`.\n */\nexport function isEntityReferenceConfig(config: Record<string, unknown> | null | undefined): boolean {\n const allowed = config?.[\"allowed_types\"];\n return Array.isArray(allowed) && allowed.length > 0;\n}\n\n/** The platform kinds an entity-reference Field may name, lower-cased, in its own order. */\nexport function entityReferenceKindsOf(field: Pick<CoercibleField, \"config\">): string[] {\n const allowed = field.config?.[\"allowed_types\"];\n return Array.isArray(allowed) ? allowed.map((k) => String(k).trim().toLowerCase()).filter(Boolean) : [];\n}\n\nconst TRUE_WORDS = new Set([\"true\", \"yes\", \"y\", \"1\", \"✓\", \"x\", \"checked\", \"on\"]);\nconst FALSE_WORDS = new Set([\"false\", \"no\", \"n\", \"0\", \"\", \"unchecked\", \"off\"]);\n\n/** The Field's name in plain words — never its key on a screen. */\nexport function coercionLabel(field: CoercibleField): string {\n const label = (field.label ?? \"\").trim();\n if (label !== \"\") return label;\n const name = (field.name ?? \"\").trim();\n if (name !== \"\") return name;\n return field.key\n .replace(/[_-]+/g, \" \")\n .replace(/\\b\\w/g, (c) => c.toUpperCase())\n .trim();\n}\n\n/**\n * Turn ONE typed-in string into the value this Field holds — or say, in a\n * sentence a person can act on, why it is not one.\n *\n * An empty answer is `null` for every kind: a blank means \"no answer here\",\n * and clearing a value is a thing a person means to do. Whether the Field may\n * be blank is the store's question, asked with the Field's `required` and its\n * Rules, not this one's.\n */\n/**\n * The choices a public question was handed (`public_choices`: `{key, label}` in order), as the\n * options a typed answer is matched against — by its key or by its label, either case — and each\n * answering with its KEY, which is what the public picker sends. Null when the question carries none.\n */\nfunction optionsFromPublicChoices(field: CoercibleField): ChoiceOptions | null {\n const offered = (field as { public_choices?: unknown }).public_choices;\n if (!Array.isArray(offered) || offered.length === 0) return null;\n const byWord = new Map<string, string>();\n const labels: string[] = [];\n for (const c of offered) {\n const key = typeof (c as { key?: unknown })?.key === \"string\" ? (c as { key: string }).key : null;\n if (!key) continue;\n const words = (c as { label?: unknown }).label;\n const choiceLabel = typeof words === \"string\" && words.trim() !== \"\" ? words.trim() : key;\n byWord.set(key.toLowerCase(), key);\n byWord.set(choiceLabel.toLowerCase(), key);\n labels.push(choiceLabel);\n }\n return labels.length > 0 ? { byWord, labels } : null;\n}\n\nexport function coerceTypedAnswer(\n field: CoercibleField,\n raw: string,\n opts: CoerceOptions = {},\n): Coerced {\n const label = opts.label ?? coercionLabel(field);\n const text = raw.trim();\n const kind = fieldKindFor(field, opts.kernel);\n\n if (text === \"\" && kind !== \"checkbox\") return { value: null };\n\n // LANE 10 P4: the newer kinds a format makes more exact (`newerKinds.ts`) read their own way;\n // everything else about them (a status's choices, a count's refusal) is their base kind's.\n // NOTION-PROPS-3: a button holds no value — it is pressed, never typed or pasted into.\n if (isButtonField(field)) return { refusal: `${label} is a button: it is pressed, and holds nothing to type.` };\n const newer = newerKindOf(field);\n if (newer === \"rating\") {\n const max = ratingMaxOf(field);\n const n = Number(text);\n if (Number.isInteger(n) && n >= 0 && n <= max) return { value: n };\n return { refusal: `${label} is a whole number of stars from 0 to ${max}, and “${raw}” is not one.` };\n }\n if (newer === \"duration\") {\n const seconds = readTypedDuration(text);\n if (seconds === null || seconds < 0) {\n return { refusal: `${label} is a length of time, and “${raw}” is not one. Write it like 45 min, 1:30 or 1h 30m.` };\n }\n // The store's own ceiling (custom._kind_value_ok): ten years of 365.25 days.\n if (seconds > 315_576_000) return { refusal: `${label} is longer than ten years, and a length of time here is at most ten years.` };\n return { value: seconds };\n }\n if (newer === \"address\") return { value: addressFromText(text) };\n if (newer === \"barcode\") return { value: text };\n\n switch (kind) {\n case \"checkbox\": {\n const word = text.toLowerCase();\n if (TRUE_WORDS.has(word)) return { value: true };\n if (FALSE_WORDS.has(word)) return { value: word === \"\" ? null : false };\n return {\n refusal: `${label} is a tick box, so it takes yes or no — “${raw}” is neither. Write TRUE/FALSE, yes/no or 1/0.`,\n };\n }\n case \"number\":\n case \"currency\":\n case \"percent\": {\n // The ONE number reading (`typedNumber.ts`): \"$1,250.50\", \"(300)\", \"-$150\", \"12%\".\n const read = readTypedNumber(text);\n if (!read.ok) {\n return {\n refusal: `${label} holds a number, and “${raw}” is not one. Write digits, like 1,250.50 — a currency symbol, a percent sign, commas and a minus sign or brackets for a negative are all fine.`,\n };\n }\n const foreign = foreignUnit(kind, text, label, raw, field.unit, opts.showsAsPercent === true);\n if (foreign) return { refusal: foreign };\n return { value: read.value };\n }\n case \"datetime\": {\n // A DAY IS NOT AN INSTANT, AND TREATING IT AS ONE MOVES IT. `2026-09-01`\n // parsed as an instant is midnight UTC, and a screen west of Greenwich\n // then draws it as 31 August — measured on fifty pasted invoice dates,\n // all one day early, systematically. A date Field holds a DAY.\n const wantsTime = String(field.config?.[\"kind\"] ?? \"date\") === \"datetime\";\n // 🚨 READ, NEVER GUESSED (merged-grid review 2026-09-26): `Date.parse` read \"10/3\" as\n // 2001 and a half-typed year as a real one. `readTypedDate` reads the person's locale\n // order and refuses a two-digit or missing year, a month 13, a 31 September.\n const read = readTypedDate(text, { withTime: wantsTime, ...(opts.locale ? { locale: opts.locale } : {}) });\n if (!read.ok) return { refusal: `${label} holds a date, and ${read.why}` };\n return { value: wantsTime ? read.instant : read.day };\n }\n case \"json\": {\n try {\n return { value: JSON.parse(text) };\n } catch {\n return {\n refusal: `${label} holds a document, and “${raw}” is not valid JSON. Write the whole object, braces and all.`,\n };\n }\n }\n case \"select\":\n case \"multi_select\": {\n // A LIST IS AN IDENTITY, AND A WORD IS HOW A PERSON NAMES ONE. The store\n // keeps the option's id; the person types its label. Matching the two is\n // the whole job, and it is case-insensitive because \"COMPLETED\" and\n // \"Completed\" are one option.\n // A PUBLIC QUESTION CARRIES ITS OWN CHOICES (lane HANDOVER, 2026-09-29). A stranger's page\n // never reads the column's options, and the form and booking doors hand each choice question\n // its choices as `public_choices` instead. Cedar Ridge's booking page sent the Visit type's\n // key (\"follow_up\") and this answered \"its options have not been read yet\", so every booking\n // that answered a choice was refused, on the page and again at the confirm route.\n const options = opts.options ?? optionsFromPublicChoices(field);\n if (!options) {\n return {\n refusal: `${label} is a list and its options have not been read yet, so nothing can be matched to “${raw}”. Try again once the column's choices have loaded.`,\n };\n }\n const wanted = kind === \"multi_select\"\n ? text.split(/[,;]/).map((w) => w.trim()).filter((w) => w.length > 0)\n : [text];\n if (wanted.length === 0) return { value: kind === \"multi_select\" ? [] : null };\n const matched: string[] = [];\n const unmatched: string[] = [];\n for (const word of wanted) {\n const hit = options.byWord.get(word.toLowerCase());\n if (hit === undefined) unmatched.push(word);\n else matched.push(hit);\n }\n if (unmatched.length > 0) {\n const offered = options.labels.length > 0\n ? options.labels.join(\", \")\n : \"it has none yet\";\n return {\n refusal:\n `${label} does not have ${unmatched.map((w) => `“${w}”`).join(\" or \")} among its options. ` +\n `It offers: ${offered}.`,\n unmatched,\n };\n }\n return { value: kind === \"multi_select\" ? matched : matched[0] };\n }\n // AN IDENTITY IS NOT A WORD. Writing the word would put a string where an\n // id belongs, and every filter, colour and rollup built on that column\n // would stop agreeing with it. So it is refused and the person is told\n // which control does hold it.\n //\n // BUT AN ID IS AN IDENTITY (RELATION-TARGETS, 2026-09-23). A Person or File\n // column holds the kernel Person / File record, and the store itself now\n // turns the id a writer actually holds — a member's user id, an uploaded\n // file's id, or the record's own id — into that record at every write door\n // (`custom.relation_kernel_record`), refusing anything else in its own\n // words (\"… names someone who is not a member of …\"). So an import column\n // of member ids, or an agent handing over the file it just uploaded, is a\n // value and not a refusal; the store, not this mirror, decides whether the\n // id is somebody here.\n case \"member\":\n if (isUuidShape(text)) return { value: text.toLowerCase() };\n return {\n refusal: `${label} names a person in this organization, so it is chosen rather than typed. Open it and pick them.`,\n };\n case \"attachment\":\n if (isUuidShape(text)) return { value: text.toLowerCase() };\n return {\n refusal: `${label} holds a file, which cannot arrive as typed-in text. Open it and attach the file.`,\n };\n case \"relation\": {\n // VISION-REACH G1: a record's id, or its NAME read through the index the caller asked the\n // store for. A several-records column splits the cell the way an import splits any list.\n const parts = field.multi ? splitListCell(text) : [text];\n const ids: string[] = [];\n const words: string[] = [];\n const none: string[] = [];\n const many: string[] = [];\n for (const part of parts) {\n if (isUuidShape(part)) {\n if (!ids.includes(part.toLowerCase())) ids.push(part.toLowerCase());\n continue;\n }\n if (!opts.relationNames) {\n return {\n refusal: `${label} points at another record, so it is chosen rather than typed. Open it and pick the record.`,\n };\n }\n const hits = opts.relationNames.byName.get(relationNameKey(part)) ?? [];\n if (hits.length === 0) none.push(part);\n else if (hits.length > 1) many.push(part);\n else if (!ids.includes(hits[0]!.id)) {\n ids.push(hits[0]!.id);\n words.push(hits[0]!.words);\n }\n }\n if (none.length > 0 || many.length > 0) {\n const table = opts.relationNames?.tableName ?? \"that table\";\n const said = many.length > 0\n ? `${many.map((w) => `“${w}”`).join(\", \")} names more than one record in ${table}.`\n : `${table} has no record called ${none.map((w) => `“${w}”`).join(\" or \")}.`;\n return { refusal: `${label}: ${said}`, unmatchedNames: none, ambiguousNames: many };\n }\n if (ids.length === 0) return { value: field.multi ? [] : null };\n return { value: field.multi ? ids : ids[0], ...(words.length > 0 ? { words: words.join(\", \") } : {}) };\n }\n // SC-R / P12. An entity reference names a platform thing by kind and id.\n // An import or an agent may hand over `note:<id>` — or a bare id when the\n // column allows exactly one kind; the store then decides whether the thing\n // is there and whether the writer may open it. Words are chosen, not typed.\n case \"entity_reference\": {\n const allowed = entityReferenceKindsOf(field);\n const [maybeToken, maybeId] = text.includes(\":\") ? text.split(/:(.*)/s, 2) : [\"\", text];\n const token = (maybeToken ?? \"\").trim().toLowerCase() || (allowed.length === 1 ? allowed[0] : \"\");\n const id = (maybeId ?? \"\").trim();\n if (token && isUuidShape(id)) {\n const ref = { token, id: id.toLowerCase() };\n return { value: field.multi ? [ref] : ref };\n }\n return {\n refusal: `${label} points at something on the platform, so it is chosen rather than typed. Open it and pick it.`,\n };\n }\n case \"formula\":\n case \"lookup\":\n case \"rollup\":\n return {\n refusal: `${label} is worked out by the system, so nothing can be written into it.`,\n };\n // A LINK AND AN EMAIL ARE READ, THE WAY AIRTABLE READS THEM (merged-grid review 2, A9).\n // \"company.com\" is a link a person meant: it is kept as https://company.com. Words that\n // cannot be one are refused NAMING THE KIND, WITH ONE EXAMPLE — never \"not written the way\n // this field expects\". Any further format Rule on the column is still the store's to judge.\n case \"url\": {\n if (/^[a-z][a-z0-9+.-]*:\\/\\//i.test(text) || /^mailto:/i.test(text)) return { value: text };\n if (!/\\s/.test(text) && /^[^./@\\s]+(\\.[^./\\s]+)+(\\/.*)?$/.test(text)) return { value: `https://${text}` };\n return { refusal: `${label} holds a link, and “${raw}” is not one. A link looks like company.com or https://company.com/page.` };\n }\n case \"email\": {\n if (/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(text)) return { value: text };\n return { refusal: `${label} holds an email address, and “${raw}” is not one. An email looks like name@company.com.` };\n }\n default:\n // text, long_text, phone — the string IS the value, and the format Rules on it are the\n // store's to judge.\n return { value: raw };\n }\n}\n\n/** What a whole set of typed-in answers became. */\nexport interface CoercedAnswers {\n /** Ready to send to the store. Only the keys that coerced cleanly. */\n values: Record<string, unknown>;\n /** Field key → the sentence saying why that answer was not written. */\n refusals: Record<string, string>;\n /** The field keys the caller sent that this table has no Field for. */\n unknownKeys: string[];\n}\n\n/**\n * Every typed-in answer on one screen, coerced together — the shape the two\n * public doors and the import wizard actually need.\n *\n * Keys the table has no Field for are reported rather than dropped: the store\n * refuses an unknown key by name and a caller that quietly deleted it would\n * hide a real mismatch between a page and its table.\n */\nexport function coerceTypedAnswers(\n fields: readonly CoercibleField[],\n answers: Readonly<Record<string, unknown>>,\n opts: {\n kernel?: KernelTargets | undefined;\n /** Choices per Field key, for the list columns. */\n options?: ReadonlyMap<string, ChoiceOptions> | undefined;\n /** Field key → the words that screen printed above the box. */\n labels?: Readonly<Record<string, string>> | undefined;\n /** Relation columns' targets, indexed by name, by field key (VISION-REACH G1). */\n relationNames?: ReadonlyMap<string, RelationNameIndex> | undefined;\n } = {},\n): CoercedAnswers {\n const byKey = new Map(fields.map((f) => [f.key, f]));\n const values: Record<string, unknown> = {};\n const refusals: Record<string, string> = {};\n const unknownKeys: string[] = [];\n\n for (const [key, given] of Object.entries(answers)) {\n const field = byKey.get(key);\n if (!field) {\n unknownKeys.push(key);\n // Passed through untouched: it is the STORE's job to say it does not\n // know this key, in its own words, naming it.\n values[key] = given;\n continue;\n }\n // ALREADY THE RIGHT SHAPE IS LEFT ALONE. A screen with real editors hands\n // over a number, a boolean or an array already; only a STRING needs this.\n if (typeof given !== \"string\") {\n values[key] = given;\n continue;\n }\n const opt: CoerceOptions = {};\n const choices = opts.options?.get(key);\n if (choices) opt.options = choices;\n const named = opts.relationNames?.get(key);\n if (named) opt.relationNames = named;\n if (opts.kernel) opt.kernel = opts.kernel;\n const label = opts.labels?.[key];\n if (label) opt.label = label;\n\n const answered = coerceTypedAnswer(field, given, opt);\n if (\"refusal\" in answered) refusals[key] = answered.refusal;\n else values[key] = answered.value;\n }\n\n return { values, refusals, unknownKeys };\n}\n","// src/forms/typedAnswers.ts — @ai-matrx/records/forms (moved whole from the website's\n// features/unified-data/typedAnswers.ts, 2026-10-08). WHAT A STRANGER TYPED, TURNED INTO\n// VALUES, ONCE, FOR EVERY PUBLIC DOOR.\n//\n// 🚨 WHY THIS FILE EXISTS. AGENT-BUILDS-2's closing walk, 2026-09-21: a\n// customer opened Ironclad Mobile Mechanic's public booking page, picked\n// 2:00 PM on Tuesday, typed Marcus Delgado, (415) 555-0178, a 2019 Honda CR-V,\n// pressed \"Book it\", and the page answered\n//\n// \"Vehicle Year takes a number, and it was given a string\"\n//\n// and made no appointment. A browser input hands over a STRING; a Field takes a\n// number. The grid's paste and the CSV import each carried their own coercion —\n// good ones — and the two doors a STRANGER uses carried none, so a booking page\n// over ANY table with a number field was unbookable by anybody.\n//\n// THE COERCION ITSELF IS NOT HERE. It is `coerceTypedAnswers` in\n// `@ai-matrx/records`, the same body the grid's paste and the editors use, so\n// a number typed into a public form, pasted into a cell, imported from a CSV\n// or entered through a portal all mean the same thing. This file is the ONE\n// place the two route handlers agree on how to ASK it and how to answer when\n// it refuses.\n//\n// IT IS THE SERVER'S JOB AND NOT ONLY THE SCREEN'S. The public pages coerce\n// too, so a person is told at the box rather than after their slot is held —\n// but a route that trusted the browser to have done it would be one\n// hand-written POST away from the same 400. The screen is the courtesy; this\n// is the contract.\n//\n// IT DECIDES NOTHING ELSE. Required, the Rules, the cap, the rate window, the\n// quarantine Rule, every exposed key and every wall stay the store's, asked by\n// custom.form_submit and custom.booking_confirm. This turns strings into\n// values and stops.\n\nimport { coerceTypedAnswers } from \"../coerce\";\nimport type { Field } from \"../field\";\n\nexport interface TypedAnswers {\n /** Ready for the door. */\n values: Record<string, unknown>;\n /**\n * The sentences, in the order the page asks its questions, or null when\n * every answer became a value. Each one names its Field in plain words.\n */\n refusal: string | null;\n /** Field key → its sentence, so a screen can point at the question. */\n byKey: Record<string, string>;\n}\n\n/**\n * Coerce what a public page collected against the Fields that page published.\n *\n * `fields` is exactly what `custom.form_public` / `custom.booking_public`\n * already hand back — the Field definitions of the questions being asked — so\n * nothing new is read and no door is opened to work this out.\n */\nexport function typedAnswersFor(\n fields: Array<Record<string, unknown>> | null | undefined,\n values: Record<string, unknown>,\n): TypedAnswers {\n // A page with no resolved Fields coerces nothing rather than guessing. The\n // door still judges the payload and says so in its own words — which is the\n // behaviour that existed before this file, not a new silence.\n if (!Array.isArray(fields) || fields.length === 0) {\n return { values, refusal: null, byKey: {} };\n }\n\n const coerced = coerceTypedAnswers(fields as unknown as Field[], values);\n const keys = Object.keys(coerced.refusals);\n if (keys.length === 0) return { values: coerced.values, refusal: null, byKey: {} };\n\n // ONE SENTENCE, IN THE ORDER THE QUESTIONS WERE ASKED, because a person\n // reading it is looking at a form and scanning down it.\n const order = new Map((fields as Array<{ key?: string }>).map((f, at) => [String(f.key ?? \"\"), at]));\n keys.sort((a, b) => (order.get(a) ?? 0) - (order.get(b) ?? 0));\n return {\n values: coerced.values,\n refusal: keys.map((k) => coerced.refusals[k]).join(\" \"),\n byKey: coerced.refusals,\n };\n}\n"],"mappings":"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","names":["text","text","text","text","bad","day","text","text","text"]}
|