@ai-matrx/records 0.86.2 → 0.87.1
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 +30 -0
- package/dist/core/index.cjs +6 -4
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.js +6 -4
- package/dist/core/index.js.map +1 -1
- package/dist/data/index.cjs +19 -6
- package/dist/data/index.cjs.map +1 -1
- package/dist/data/index.d.cts +13 -0
- package/dist/data/index.d.ts +13 -0
- package/dist/data/index.js +19 -6
- package/dist/data/index.js.map +1 -1
- package/dist/data/react.d.cts +13 -0
- package/dist/data/react.d.ts +13 -0
- package/dist/index.cjs +2 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +0 -34
- package/dist/index.d.ts +0 -34
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/list/index.cjs.map +1 -1
- package/dist/list/index.d.cts +5 -9
- package/dist/list/index.d.ts +5 -9
- package/dist/list/index.js.map +1 -1
- package/dist/react/index.cjs +20 -7
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +2 -36
- package/dist/react/index.d.ts +2 -36
- package/dist/react/index.js +20 -7
- package/dist/react/index.js.map +1 -1
- package/dist/realtime/index.cjs +2 -1
- package/dist/realtime/index.cjs.map +1 -1
- package/dist/realtime/index.js +2 -1
- package/dist/realtime/index.js.map +1 -1
- package/dist/typed-table/index.cjs +7 -5
- package/dist/typed-table/index.cjs.map +1 -1
- package/dist/typed-table/index.d.cts +2 -2
- package/dist/typed-table/index.d.ts +2 -2
- package/dist/typed-table/index.js +7 -5
- package/dist/typed-table/index.js.map +1 -1
- package/package.json +3 -3
package/dist/list/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/list/index.ts","../../src/list/list-scope.ts","../../src/list/entity-list.ts"],"sourcesContent":["/**\n * `@ai-matrx/records/list` — the list vocabulary every records surface shares (P16):\n * the five-lane list scope and the entity-list query, filters, sort, scope counts and\n * facets. Pure types + tiny helpers; moved from matrx-frontend `lib/list-scope` and\n * `lib/entity-list/types`.\n */\nexport * from \"./list-scope\";\nexport * from \"./entity-list\";\n","// lib/list-scope/types.ts\n//\n// THE VIEW LAW (CLAUDE.md + common-docs/systems/architecture/database/FEATURE.md §6):\n// RLS is the ceiling, never the view definition. Every list query MUST\n// declare its own scope explicitly — a bare `.select(\"*\")` relying on RLS\n// alone to \"just filter to mine\" is a defect the moment a user belongs to\n// more than one org (every user does: personal org + N companies).\n//\n// THE VOCABULARY IS FIXED AND LIVES HERE. A surface declares WHICH of these\n// it supports and supplies the predicate. It may not invent one of its own —\n// a scope the user learns on one page has to mean the same thing on every\n// other page. See lib/list-scope/FEATURE.md.\n//\n// all → everything that is mine to see: Mine ∪ My team ∪ My Orgs ∪ Shared\n// (THE DEFAULT LANE; Public and System are discovery lanes, never folded in)\n// mine → what did I make?\n// team → what did my team make? (people I share a team with)\n// orgs → what does my organization have?\n// shared → what did someone hand me? (explicit iam.permissions grant)\n// industry → what does my field publish? (see below)\n// public → what has a tenant published platform-wide?\n// system → what does the PLATFORM ITSELF ship? (see below)\n//\n// SYSTEM (added 2026-08-26) is the platform's own corpus — builtin agents,\n// global shortcuts, platform content blocks: records AI Matrx publishes, not\n// records a tenant published. Which rows a viewer gets is the RPC's decision:\n// on /agents/all every signed-in person reads the PUBLISHED built-ins and a\n// platform admin reads the whole corpus (2026-09-27, agx_list_scoped).\n//\n// It is a SCOPE and not a separate admin page because that separation is what\n// produced the drift it repairs: /administration/agents/system-agents/agents\n// was a second, poorer list UI over the same table, and every feature added to\n// /agents/all (facets, server sort, the one action menu, doors, Orchestras)\n// silently skipped the system corpus. One list, one more question it answers.\n//\n// INDUSTRY is opt-in on BOTH ends, and is \"orgs\" with one more hop rather than\n// a new kind of thing: curators publish into an industry\n// (iam.industry_curators), and an org must ATTACH that industry\n// (iam.org_industries) before its members can read the corpus. Records attach\n// to an industry by GRANT ROW, following rag.data_store_grants.industry_id.\n//\n// A page renders one tab per scope the surface supports; switching scopes\n// changes the declared query, never silently reinterprets RLS output.\n//\n// TWO AXES, NEVER MIXED (Arman 2026-09-30, common-docs\n// /policies/access-ladder.md). The LANE (this union) says HOW\n// I can see a record. WHICH ORGANIZATION it is in is a separate axis — the\n// page's organization filter, `EntityListQuery.orgId` (URL `?org_filter=`, null = All\n// organizations) — and it narrows EVERY lane. A lane carries no organization id.\n// The ACTIVE organization is neither: it never reaches a list read.\n\n// THE ADMIN SEAT (Arman, 2026-09-26): \"No one acts as themselves in admin.\"\n// An admin page never shows mine / orgs / shared — those are PERSONAL-SEAT\n// questions (\"what did I make?\", \"what does MY team have?\").\n//\n// AN ADMIN MANAGEMENT PAGE HAS NO SCOPE LANES AT ALL: it manages the\n// platform's own records (the `system` scope, alone, with no tabs). Looking\n// into a tenant's records is tech support, and lives only on a separate\n// support route (…/support), which declares ADMIN_SUPPORT_LIST_SCOPES:\n//\n// platform_orgs → every organization's records (narrowable to one org)\n// platform_users → every person's own records (narrowable to one person,\n// by their personal org id)\n// platform_all → the whole platform corpus\n//\n// Guarded by `pnpm check:admin-no-personal-seat`.\n\nexport type ListScopeKind =\n | \"all\"\n | \"mine\"\n | \"team\"\n | \"orgs\"\n | \"shared\"\n | \"industry\"\n | \"public\"\n | \"system\"\n | \"platform_orgs\"\n | \"platform_users\"\n | \"platform_all\";\n\nexport type ListScope =\n /** ALL: Mine ∪ My team ∪ My Orgs ∪ Shared, one row per record. The default lane. */\n | { kind: \"all\" }\n | { kind: \"mine\" }\n /**\n * MY TEAM (access ladder: \"my team or department\"). Rows in an organization I\n * belong to, made by someone who shares a live team with me THERE — me\n * included where I am on a team. A person on no team sees NOTHING here (and\n * the list says so) — never a copy of Mine (2026-09-28). A team is a\n * list filter, never an access boundary: every row here is one \"orgs\" would\n * also show. Server reach: `iam.my_team_reach(p_org_id)` (teams FEATURE.md).\n * Narrowing to one organization is the organization filter's job (`orgId`).\n */\n | { kind: \"team\" }\n /** Every organization I belong to; one organization = the organization filter. */\n | { kind: \"orgs\" }\n | { kind: \"shared\" }\n /** `industryId: null` = blended across every industry my orgs have attached. */\n | { kind: \"industry\"; industryId: string | null }\n | { kind: \"public\" }\n /** Platform-published records; the RPC decides what each viewer reads. */\n | { kind: \"system\" }\n /** ADMIN: every organization's records. `organizationId` narrows to one. */\n | { kind: \"platform_orgs\"; organizationId: string | null }\n /** ADMIN: every person's own records. `organizationId` = one person's personal org. */\n | { kind: \"platform_users\"; organizationId: string | null }\n /** ADMIN: the whole platform corpus. */\n | { kind: \"platform_all\" };\n\n/**\n * The scopes an admin SUPPORT route declares (…/support, e.g. Mandate support\n * lookup) — never on a management page, never a personal-seat scope.\n */\nexport const ADMIN_SUPPORT_LIST_SCOPES: ListScopeKind[] = [\n \"platform_orgs\",\n \"platform_users\",\n \"platform_all\",\n];\n\n/** The personal-seat scopes: legal on user pages, banned on admin pages. */\nexport const PERSONAL_SEAT_SCOPES: readonly ListScopeKind[] = [\"all\", \"mine\", \"team\", \"orgs\", \"shared\"];\n\n/** The lanes the All lane unions. */\nexport const ALL_LANE_MEMBERS: readonly ListScopeKind[] = [\"mine\", \"team\", \"orgs\", \"shared\"];\n\n/** Every user list opens on All unless its page or knob says otherwise. */\nexport const DEFAULT_LIST_SCOPE: ListScope = { kind: \"all\" };\n\n/**\n * The vocabulary as a runtime value — for validating a scope string that\n * arrives from outside TypeScript (a counts RPC row, a URL parameter).\n *\n * Every such check reads THIS. A hand-listed subset at a call site is how the\n * `system` scope came back from the server with a real total and was silently\n * dropped on the way to its tab.\n */\nexport const LIST_SCOPE_KINDS: readonly ListScopeKind[] = [\n \"all\",\n \"mine\",\n \"team\",\n \"orgs\",\n \"shared\",\n \"industry\",\n \"public\",\n \"system\",\n \"platform_orgs\",\n \"platform_users\",\n \"platform_all\",\n];\n\n// ── Narrowing helpers ───────────────────────────────────────────────────────\n\nexport function isMineScope(\n scope: ListScope,\n): scope is Extract<ListScope, { kind: \"mine\" }> {\n return scope.kind === \"mine\";\n}\n\nexport function isTeamScope(\n scope: ListScope,\n): scope is Extract<ListScope, { kind: \"team\" }> {\n return scope.kind === \"team\";\n}\n\n/** The lanes a type can structurally be unable to hold (All and Mine always exist). */\nexport type DeclarableLane = \"team\" | \"orgs\" | \"shared\" | \"industry\" | \"public\" | \"system\";\n\n/**\n * THE ONE DECLARED STATEMENT of which lanes a surface's type cannot hold\n * (`EntityListConfig.lanes`, `EntityScopeTabs lanes`): `{ public: false }` = this type has no\n * publish path, so Public would only ever say 0. A lane that is possible but empty for this\n * person is NOT declared: it stays and shows an honest 0. Omitted = every lane the surface's\n * `scopes` names is offered (and the shell adds My team beside My Orgs).\n *\n * The facts come from the type, never from who is looking: team needs a team concept on its\n * records, shared needs a `platform.shareable_resource_registry` row, public needs a publish path\n * (`is_link_shareable`, or a public-card path as agents and workflows have), system needs a\n * platform corpus of that type.\n */\nexport type LaneSupport = Partial<Record<DeclarableLane, false>>;\n\n/** What a surface declares about the standard lanes (`EntityListConfig.lanes`). */\nexport interface StandardLanesOptions {\n lanes?: LaneSupport;\n}\n\n/** The lanes a surface declared it cannot hold. */\nexport function declaredAbsentLanes(lanes: LaneSupport | undefined): ListScopeKind[] {\n return (Object.keys(lanes ?? {}) as DeclarableLane[]).filter((k) => lanes?.[k] === false);\n}\n\n/**\n * THE ONE PLACE \"My team\" joins a list (T-29, 2026-09-27). Every surface that\n * offers \"My Orgs\" offers \"My team\" beside it — never declared per page. Its\n * `*_list_scoped` RPC answers `p_scope = 'team'` through `iam.my_team_reach`,\n * and its counts RPC returns a `team` row; a surface whose RPC cannot is a\n * defect in that RPC, not a reason to hide the tab here.\n *\n * Inserted directly after \"mine\" (the ladder reads narrow → wide: only me, my\n * team, my organization). Idempotent; a list without \"orgs\" is untouched —\n * an admin page, or a Private type, has no team question to answer.\n */\nexport function withTeamScope(\n scopes: readonly ListScopeKind[],\n lanes: StandardLanesOptions = {},\n): ListScopeKind[] {\n // A surface DECLARES the lanes its type cannot hold (`lanes: { public: false }`): a lane that\n // can never hold anything is absent, never an empty tab (a screen never lies).\n const absent = declaredAbsentLanes(lanes.lanes);\n const declared = scopes.filter((k) => !absent.includes(k));\n if (absent.includes(\"team\")) return declared;\n if (!declared.includes(\"orgs\") || declared.includes(\"team\")) return [...declared];\n const out = [...declared];\n const at = out.indexOf(\"mine\");\n out.splice(at >= 0 ? at + 1 : out.indexOf(\"orgs\"), 0, \"team\");\n return out;\n}\n\n/**\n * THE ONE PLACE \"All\" joins a list (2026-09-30). A tab bar offering two or more\n * of the personal lanes (Mine / My team / My Orgs / Shared) gets All as its\n * FIRST tab — never declared per page. A list with one personal lane (a Private\n * type: Mine alone) or none (an admin page) is untouched: All would repeat it.\n * Idempotent; its `*_list_scoped` RPC answers `p_scope = 'all'`.\n */\nexport function withAllScope(scopes: readonly ListScopeKind[]): ListScopeKind[] {\n if (scopes.includes(\"all\")) return [...scopes];\n const personal = scopes.filter((k) => ALL_LANE_MEMBERS.includes(k)).length;\n return personal >= 2 ? [\"all\", ...scopes] : [...scopes];\n}\n\n/** The lanes a tab bar renders: the surface's own, plus All and My team where they belong. */\nexport function withStandardLanes(\n scopes: readonly ListScopeKind[],\n lanes: StandardLanesOptions = {},\n): ListScopeKind[] {\n return withAllScope(withTeamScope(scopes, lanes));\n}\n\nexport function isAllScope(\n scope: ListScope,\n): scope is Extract<ListScope, { kind: \"all\" }> {\n return scope.kind === \"all\";\n}\n\nexport function isOrgsScope(\n scope: ListScope,\n): scope is Extract<ListScope, { kind: \"orgs\" }> {\n return scope.kind === \"orgs\";\n}\n\nexport function isSharedScope(\n scope: ListScope,\n): scope is Extract<ListScope, { kind: \"shared\" }> {\n return scope.kind === \"shared\";\n}\n\nexport function isIndustryScope(\n scope: ListScope,\n): scope is Extract<ListScope, { kind: \"industry\" }> {\n return scope.kind === \"industry\";\n}\n\nexport function isSystemScope(\n scope: ListScope,\n): scope is Extract<ListScope, { kind: \"system\" }> {\n return scope.kind === \"system\";\n}\n\nexport function isPublicScope(\n scope: ListScope,\n): scope is Extract<ListScope, { kind: \"public\" }> {\n return scope.kind === \"public\";\n}\n\n/** The industry this scope narrows to, or null. */\nexport function scopeIndustryId(scope: ListScope): string | null {\n return scope.kind === \"industry\" ? scope.industryId : null;\n}\n\n/**\n * The id a LANE itself is narrowed to — an industry, or an admin support lane's\n * one organization / person. Personal lanes never carry one: which organization\n * a list shows is the organization filter (`EntityListQuery.orgId`).\n */\nexport function scopeNarrowId(scope: ListScope): string | null {\n if (scope.kind === \"platform_orgs\" || scope.kind === \"platform_users\")\n return scope.organizationId;\n if (scope.kind === \"industry\") return scope.industryId;\n return null;\n}\n\n/**\n * THE `p_org_id` EVERY `*_list_scoped` / `*_scope_counts` / facets RPC receives:\n * the page's organization filter (null = All organizations → undefined), or —\n * on an admin support lane — that lane's own one-organization narrowing. Never\n * the active organization.\n */\nexport function listOrgParam(query: {\n scope: ListScope;\n orgId: string | null;\n}): string | undefined {\n if (query.orgId) return query.orgId;\n if (query.scope.kind === \"platform_orgs\" || query.scope.kind === \"platform_users\")\n return query.scope.organizationId ?? undefined;\n return undefined;\n}\n\n/** Stable identity for tab selection / React keys. */\nexport function scopeKey(scope: ListScope): string {\n if (scope.kind === \"industry\")\n return scope.industryId ? `industry:${scope.industryId}` : \"industry\";\n if (scope.kind === \"platform_orgs\" || scope.kind === \"platform_users\")\n return scope.organizationId ? `${scope.kind}:${scope.organizationId}` : scope.kind;\n return scope.kind;\n}\n\n/** Build a scope from a kind + optional narrowing id (tab click handlers). */\nexport function makeScope(\n kind: ListScopeKind,\n narrowToId: string | null = null,\n): ListScope {\n switch (kind) {\n case \"all\":\n return { kind: \"all\" };\n case \"orgs\":\n return { kind: \"orgs\" };\n case \"team\":\n return { kind: \"team\" };\n case \"industry\":\n return { kind: \"industry\", industryId: narrowToId };\n case \"mine\":\n return { kind: \"mine\" };\n case \"shared\":\n return { kind: \"shared\" };\n case \"public\":\n return { kind: \"public\" };\n case \"system\":\n return { kind: \"system\" };\n case \"platform_orgs\":\n return { kind: \"platform_orgs\", organizationId: narrowToId };\n case \"platform_users\":\n return { kind: \"platform_users\", organizationId: narrowToId };\n case \"platform_all\":\n return { kind: \"platform_all\" };\n default: {\n const _exhaustive: never = kind;\n throw new Error(\n `[list-scope] unknown scope kind: ${String(_exhaustive)}`,\n );\n }\n }\n}\n","// lib/entity-list/types.ts\n//\n// The feature-agnostic half of a canonical entity-list surface. Extracted from\n// features/agents/browse (live at /agents/all) — the proving ground. Nothing\n// here knows what an agent is.\n//\n// THE SPLIT THAT RUNS THROUGH THIS WHOLE SYSTEM:\n//\n// QUERY — scope, search, filters, page. Lives here. NEVER persisted:\n// restoring a stale search that renders an empty list is a bug\n// wearing a feature's clothes.\n// STYLE — view mode, density, sort, page size, columns. Lives in\n// lib/list-views (persisted per user, synced across devices).\n//\n// See features/agents/browse/FEATURE.md for the worked implementation and\n// lib/list-scope/FEATURE.md for the scope vocabulary + RPC template rules.\n\nimport type { ArchiveFilterValue } from \"@ai-matrx/design-system\";\nimport type { ListScope, ListScopeKind } from \"./list-scope\";\nimport { DEFAULT_LIST_SCOPE, LIST_SCOPE_KINDS } from \"./list-scope\";\n\n/** Sentinel for \"has no value\" (uncategorized / untagged). Matches the SQL. */\nexport const NONE_VALUE = \"__none__\";\n\n/**\n * ONE filter vocabulary, shared by column headers and any filter panel.\n * Serialized straight into `<feature>_list_scoped(p_filters)`, so a filter set\n * from a column header and the same filter set from a panel are the same query\n * and cannot drift.\n *\n * Mirrors MatrxDataTable's ColumnFilterValue on purpose — the table is the\n * primary producer, and a translation layer with its own vocabulary would be a\n * third place for the two to disagree.\n */\nexport type EntityFilterValue =\n | { kind: \"text\"; value: string }\n | { kind: \"select\"; values: string[] }\n | { kind: \"boolean\"; value: boolean };\n\n/** Keyed by COLUMN ID, so the server predicate and the header agree by name. */\nexport type EntityFilters = Record<string, EntityFilterValue>;\n\n/**\n * THE ARCHIVED-ITEMS LAW's tri-state, from the ONE place it is defined:\n * `@ai-matrx/design-system`'s `ArchiveFilterValue` (`active` | `archived` |\n * `all`). This alias exists only so the ~200 call sites that already say\n * `ArchivedFilter` keep reading naturally — the vocabulary itself is the\n * package's, never this repo's, so the words on screen and the words in the\n * type can never drift apart.\n */\nexport type ArchivedFilter = ArchiveFilterValue;\n\nexport interface EntityListQuery {\n /** The access LANE — how I can see a record (All, Mine, My team, …). */\n scope: ListScope;\n /**\n * THE ORGANIZATION FILTER — which of my organizations to look at. Its own\n * axis, never inside a lane: null = All organizations (the default on every\n * load), else one organization id. URL `?org_filter=`. It narrows EVERY lane and\n * the counts, and is sent to every list RPC as `p_org_id`\n * (`listOrgParam(query)`). It never initializes from, syncs with, or writes\n * to the ACTIVE organization (common-docs\n * /policies/access-ladder.md).\n */\n orgId: string | null;\n search: string;\n /**\n * Reach into full record content (prompt bodies, transcript text...).\n * Opt-in: it is a full scan server-side and must never be the default.\n */\n deep: boolean;\n /**\n * Kept OUT of `filters` because it carries a DEFAULT (\"active only\") rather\n * than being absent-means-unfiltered like every other column.\n */\n archived: ArchivedFilter;\n filters: EntityFilters;\n /** One-based, matching the pagination UI. */\n page: number;\n}\n\nexport const DEFAULT_ENTITY_LIST_QUERY: EntityListQuery = {\n scope: DEFAULT_LIST_SCOPE,\n orgId: null,\n search: \"\",\n deep: false,\n archived: \"active\",\n filters: {},\n page: 1,\n};\n\n/**\n * How many things are narrowing the list right now (the Filters badge).\n * Counts ONLY what the user applied — never the sort or the active scope.\n * A badge that reads \"1\" on an untouched page is a permanent lie that trains\n * people to ignore the number (which is exactly what /agents/all's did).\n *\n * 🚨 THE ARCHIVE AXIS IS COMPARED AGAINST THE SURFACE'S OWN DEFAULT, NOT\n * AGAINST THE LITERAL `\"active\"`. THE ARCHIVED-ITEMS LAW §6 made the starting\n * value a user KNOB (`userPreferences.lists.archivedDefault`), so a user who\n * set theirs to \"Active + archived\" met an untouched page whose Filters badge\n * read \"1\" and whose empty state claimed a search-and-filter narrowing that\n * nobody had applied — the exact permanent lie this function exists to\n * prevent, reintroduced by the knob. The default is passed in because only the\n * hook knows it.\n */\nexport function countActiveFilters(\n query: EntityListQuery,\n defaultArchived: ArchivedFilter = DEFAULT_ENTITY_LIST_QUERY.archived,\n): number {\n return (\n Object.keys(query.filters).length +\n (query.archived !== defaultArchived ? 1 : 0)\n );\n}\n\n/**\n * 🚨 THE ALL-ARCHIVED FACT — what a list must know before it may say \"none\".\n *\n * THE DEFECT THIS CLOSES (row F10 repair, 2026-09-10). Every archive-aware\n * `EntityListConfig` carries a STATIC `emptyState`, and the shell fell back to\n * it whenever the query was untouched and no rows came back. But the archive\n * filter's default HIDES archived rows (THE ARCHIVED-ITEMS LAW §2), so \"the\n * live half is empty\" and \"there is nothing here\" are different facts — and\n * the shell was printing the second when it only knew the first. Measured on\n * `/maps` with all 46 of a user's maps archived: *\"No maps yet — A map is a\n * picture of how something works … Make one\"*, beside a \"New map\" button, one\n * popover away from that page's own Archived filter holding all 46. A screen\n * telling an Expert to rebuild work they already have.\n *\n * The count CANNOT come from the facets or the scope counts: both are fetched\n * with the query's CURRENT `archived` value, so on the defaulted page they\n * describe the live half too. It is its own read, `archived: \"archived\"`, and\n * it is asked ONLY when the live half came back empty — at most one extra,\n * page-size-1 request, on the one screen that would otherwise lie.\n */\nexport type ArchivedProbe =\n /** Not applicable: the surface has no archive axis, or is already showing archived rows, or the live half is not empty. */\n | { state: \"off\" }\n /** The live half is empty and the archived count is in flight. Say nothing yet. */\n | { state: \"loading\" }\n /** Answered. `total` archived rows match this exact scope/search/filters. */\n | { state: \"known\"; total: number; /** An open-ended archive: at least `total`, the store named no count. */ more?: boolean }\n /** The read broke. The page must say it cannot tell — never fall back to \"none yet\". */\n | { state: \"failed\" };\n\n/**\n * One narrowing choice inside a scope — an org under \"My Orgs\", an industry\n * under \"Industry\". The LABEL comes from the same query as the count.\n *\n * That is deliberate and load-bearing: reading names from a Redux slice meant\n * depending on `fetchFullContext`, a thunk that only runs on tasks/org-settings\n * surfaces — so on /agents/all the slice was empty and the My Orgs dropdown\n * silently never rendered. A tab bar must be self-sufficient from its own\n * query, with no hydration ordering to get wrong.\n */\nexport interface ScopeNarrowOption {\n id: string;\n label: string;\n count: number;\n}\n\nexport interface EntityScopeCounts {\n /** True server-side total per scope kind. Absent kinds are unsupported. */\n byKind: Partial<Record<ListScopeKind, number>>;\n /**\n * The service names no lane counts for this question (an open-ended list the store pages\n * without a count — `EntityListPage.hasMore`). The tabs draw no number at all, never `0`.\n */\n uncounted?: boolean;\n /** Narrowing options per scope kind, in server order. */\n narrow: Partial<Record<ListScopeKind, ScopeNarrowOption[]>>;\n /**\n * WHY a scope has no narrowing options, in a sentence a person can act on.\n *\n * 🚨 THE DEFECT THIS CLOSES (one-resolution FIX-R6/F1, 2026-09-08). On\n * production `/mandates`, `narrow.orgs` came back empty and the declared\n * **Organization** section simply was not rendered — an admin belonging to\n * nine organizations saw a Filters panel that never mentioned organizations,\n * with nothing on screen saying why. \"No options, so no section\" is the\n * fourth law's silent failure: a declared narrowing is either offered or it\n * says what happened.\n *\n * A service that CANNOT offer options — the counts were refused, the caller's\n * organizations have not been read yet, the caller genuinely belongs to none\n * — sets the reason here and the panel prints it. Absent + no options means\n * the shell will say so in its own generic words rather than hide.\n */\n narrowUnavailable?: Partial<Record<ListScopeKind, string>>;\n}\n\nexport const EMPTY_SCOPE_COUNTS: EntityScopeCounts = {\n byKind: {},\n narrow: {},\n};\n\n/** One row of any `*_list_scope_counts` RPC. */\nexport interface ScopeCountRow {\n scope: string;\n narrow_id?: string | null;\n label?: string | null;\n total: number | string | null;\n}\n\n/**\n * THE ONE READER of a `*_list_scope_counts` result. Every surface converts\n * through here, so a scope the server learns (System, then My team) reaches its\n * tab everywhere at once. A hand-listed `kind !== \"mine\" && kind !== \"orgs\"…`\n * filter per service is how `system` came back with a real total and was\n * silently dropped on the way to its tab — the vocabulary check reads\n * LIST_SCOPE_KINDS and nothing else. A row with `narrow_id` is one\n * organization (or industry) inside its scope; without one it is the scope's\n * blended total.\n */\nexport function scopeCountsFromRows(\n rows: readonly ScopeCountRow[] | null | undefined,\n unnamed = \"Unnamed\",\n): EntityScopeCounts {\n const counts: EntityScopeCounts = { byKind: {}, narrow: {} };\n for (const row of rows ?? []) {\n const kind = LIST_SCOPE_KINDS.find((k) => k === row.scope);\n if (!kind) continue;\n const total = Number(row.total ?? 0);\n if (row.narrow_id) {\n (counts.narrow[kind] ??= []).push({\n id: row.narrow_id,\n label: row.label ?? unnamed,\n count: total,\n });\n continue;\n }\n counts.byKind[kind] = total;\n }\n return counts;\n}\n\n/**\n * Server-computed filter OPTIONS with counts, keyed by facet name.\n *\n * Server-computed is not a nicety: deriving \"which categories exist\" from\n * loaded rows means loading every row, which is the pattern this system\n * replaced. One live account has 34 categories and 773 distinct tags.\n */\nexport interface EntityFacets {\n byKind: Record<string, { value: string; count: number }[]>;\n}\n\nexport const EMPTY_FACETS: EntityFacets = { byKind: {} };\n\n/** Count for one facet value, 0 when the facet or value is absent. */\nexport function facetCount(\n facets: EntityFacets,\n kind: string,\n value: string,\n): number {\n return facets.byKind[kind]?.find((v) => v.value === value)?.count ?? 0;\n}\n\n/** Options for one facet, most-used first (the RPC already orders them). */\nexport function facetValues(\n facets: EntityFacets,\n kind: string,\n): { value: string; count: number }[] {\n return facets.byKind[kind] ?? [];\n}\n\n/**\n * A page of rows plus the TRUE total — not `rows.length`. Every\n * `*_list_scoped` RPC returns `total_count` as a window function over the\n * filtered set, identical on every row.\n */\nexport interface EntityListPage<TRow> {\n rows: TRow[];\n total: number;\n /**\n * AN OPEN-ENDED PAGE (TABLE-ACTIONS, 2026-10-03): the store pages this list and names no\n * count. Present → `total` is only how many rows reach through THIS page, and `hasMore` says\n * whether a next page exists; the pagers draw no \"of N\" and Next follows `hasMore`. Absent → a\n * counted list, as always. A count is the store's or absent — never a full read to make one.\n */\n hasMore?: boolean;\n}\n\n/** Sort + paging, handed to the RPC. Style-owned, so it arrives from prefs. */\nexport interface EntityListSort {\n /**\n * Column id. Free string, not a closed union: every column a surface\n * declares is sortable (app policy), so the valid set is that surface's\n * column registry. The server whitelists it and falls back rather than\n * erroring, so a stale stored value can never break a page.\n */\n sort: string;\n direction: \"asc\" | \"desc\";\n /** Pin favorites above every other ordering. */\n favoritesFirst: boolean;\n pageSize: number;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/list/index.ts","../../src/list/list-scope.ts","../../src/list/entity-list.ts"],"sourcesContent":["/**\n * `@ai-matrx/records/list` — the list vocabulary every records surface shares (P16):\n * the five-lane list scope and the entity-list query, filters, sort, scope counts and\n * facets. Pure types + tiny helpers; moved from matrx-frontend `lib/list-scope` and\n * `lib/entity-list/types`.\n */\nexport * from \"./list-scope\";\nexport * from \"./entity-list\";\n","// lib/list-scope/types.ts\n//\n// THE VIEW LAW (CLAUDE.md + common-docs/systems/architecture/database/FEATURE.md §6):\n// RLS is the ceiling, never the view definition. Every list query MUST\n// declare its own scope explicitly — a bare `.select(\"*\")` relying on RLS\n// alone to \"just filter to mine\" is a defect the moment a user belongs to\n// more than one org (every user does: personal org + N companies).\n//\n// THE VOCABULARY IS FIXED AND LIVES HERE. A surface declares WHICH of these\n// it supports and supplies the predicate. It may not invent one of its own —\n// a scope the user learns on one page has to mean the same thing on every\n// other page. See lib/list-scope/FEATURE.md.\n//\n// all → everything that is mine to see: Mine ∪ My team ∪ My Orgs ∪ Shared\n// (THE DEFAULT LANE; Public and System are discovery lanes, never folded in)\n// mine → what did I make?\n// team → what did my team make? (people I share a team with)\n// orgs → what does my organization have?\n// shared → what did someone hand me? (explicit iam.permissions grant)\n// industry → what does my field publish? (see below)\n// public → what has a tenant published platform-wide?\n// system → what does the PLATFORM ITSELF ship? (see below)\n//\n// SYSTEM (added 2026-08-26) is the platform's own corpus — builtin agents,\n// global shortcuts, platform content blocks: records AI Matrx publishes, not\n// records a tenant published. Which rows a viewer gets is the RPC's decision:\n// on /agents/all every signed-in person reads the PUBLISHED built-ins and a\n// platform admin reads the whole corpus (2026-09-27, agx_list_scoped).\n//\n// It is a SCOPE and not a separate admin page because that separation is what\n// produced the drift it repairs: /administration/agents/system-agents/agents\n// was a second, poorer list UI over the same table, and every feature added to\n// /agents/all (facets, server sort, the one action menu, doors, Orchestras)\n// silently skipped the system corpus. One list, one more question it answers.\n//\n// INDUSTRY is opt-in on BOTH ends, and is \"orgs\" with one more hop rather than\n// a new kind of thing: curators publish into an industry\n// (iam.industry_curators), and an org must ATTACH that industry\n// (iam.org_industries) before its members can read the corpus. Records attach\n// to an industry by GRANT ROW, following rag.data_store_grants.industry_id.\n//\n// A page renders one tab per scope the surface supports; switching scopes\n// changes the declared query, never silently reinterprets RLS output.\n//\n// TWO AXES, NEVER MIXED (Arman 2026-09-30, common-docs\n// /policies/access-ladder.md). The LANE (this union) says HOW\n// I can see a record. WHICH ORGANIZATION it is in is a separate axis — the\n// page's organization filter, `EntityListQuery.orgId` (URL `?org_filter=`, null = All\n// organizations) — and it narrows EVERY lane. A lane carries no organization id.\n// The ACTIVE organization is neither: it never reaches a list read.\n\n// THE ADMIN SEAT (Arman, 2026-09-26): \"No one acts as themselves in admin.\"\n// An admin page never shows mine / orgs / shared — those are PERSONAL-SEAT\n// questions (\"what did I make?\", \"what does MY team have?\").\n//\n// AN ADMIN MANAGEMENT PAGE HAS NO SCOPE LANES AT ALL: it manages the\n// platform's own records (the `system` scope, alone, with no tabs). Looking\n// into a tenant's records is tech support, and lives only on a separate\n// support route (…/support), which declares ADMIN_SUPPORT_LIST_SCOPES:\n//\n// platform_orgs → every organization's records (narrowable to one org)\n// platform_users → every person's own records (narrowable to one person,\n// by their personal org id)\n// platform_all → the whole platform corpus\n//\n// Guarded by `pnpm check:admin-no-personal-seat`.\n\nexport type ListScopeKind =\n | \"all\"\n | \"mine\"\n | \"team\"\n | \"orgs\"\n | \"shared\"\n | \"industry\"\n | \"public\"\n | \"system\"\n | \"platform_orgs\"\n | \"platform_users\"\n | \"platform_all\";\n\nexport type ListScope =\n /** ALL: Mine ∪ My team ∪ My Orgs ∪ Shared, one row per record. The default lane. */\n | { kind: \"all\" }\n | { kind: \"mine\" }\n /**\n * MY TEAM (access ladder: \"my team or department\"). Rows in an organization I\n * belong to, made by someone who shares a live team with me THERE — me\n * included where I am on a team. A person on no team sees NOTHING here (and\n * the list says so) — never a copy of Mine (2026-09-28). A team is a\n * list filter, never an access boundary: every row here is one \"orgs\" would\n * also show. Server reach: `iam.my_team_reach(p_org_id)` (teams FEATURE.md).\n * Narrowing to one organization is the organization filter's job (`orgId`).\n */\n | { kind: \"team\" }\n /** Every organization I belong to; one organization = the organization filter. */\n | { kind: \"orgs\" }\n | { kind: \"shared\" }\n /** `industryId: null` = blended across every industry my orgs have attached. */\n | { kind: \"industry\"; industryId: string | null }\n | { kind: \"public\" }\n /** Platform-published records; the RPC decides what each viewer reads. */\n | { kind: \"system\" }\n /** ADMIN: every organization's records. `organizationId` narrows to one. */\n | { kind: \"platform_orgs\"; organizationId: string | null }\n /** ADMIN: every person's own records. `organizationId` = one person's personal org. */\n | { kind: \"platform_users\"; organizationId: string | null }\n /** ADMIN: the whole platform corpus. */\n | { kind: \"platform_all\" };\n\n/**\n * The scopes an admin SUPPORT route declares (…/support, e.g. Mandate support\n * lookup) — never on a management page, never a personal-seat scope.\n */\nexport const ADMIN_SUPPORT_LIST_SCOPES: ListScopeKind[] = [\n \"platform_orgs\",\n \"platform_users\",\n \"platform_all\",\n];\n\n/** The personal-seat scopes: legal on user pages, banned on admin pages. */\nexport const PERSONAL_SEAT_SCOPES: readonly ListScopeKind[] = [\"all\", \"mine\", \"team\", \"orgs\", \"shared\"];\n\n/** The lanes the All lane unions. */\nexport const ALL_LANE_MEMBERS: readonly ListScopeKind[] = [\"mine\", \"team\", \"orgs\", \"shared\"];\n\n/** Every user list opens on All unless its page or knob says otherwise. */\nexport const DEFAULT_LIST_SCOPE: ListScope = { kind: \"all\" };\n\n/**\n * The vocabulary as a runtime value — for validating a scope string that\n * arrives from outside TypeScript (a counts RPC row, a URL parameter).\n *\n * Every such check reads THIS. A hand-listed subset at a call site is how the\n * `system` scope came back from the server with a real total and was silently\n * dropped on the way to its tab.\n */\nexport const LIST_SCOPE_KINDS: readonly ListScopeKind[] = [\n \"all\",\n \"mine\",\n \"team\",\n \"orgs\",\n \"shared\",\n \"industry\",\n \"public\",\n \"system\",\n \"platform_orgs\",\n \"platform_users\",\n \"platform_all\",\n];\n\n// ── Narrowing helpers ───────────────────────────────────────────────────────\n\nexport function isMineScope(\n scope: ListScope,\n): scope is Extract<ListScope, { kind: \"mine\" }> {\n return scope.kind === \"mine\";\n}\n\nexport function isTeamScope(\n scope: ListScope,\n): scope is Extract<ListScope, { kind: \"team\" }> {\n return scope.kind === \"team\";\n}\n\n/** The lanes a type can structurally be unable to hold (All and Mine always exist). */\nexport type DeclarableLane = \"team\" | \"orgs\" | \"shared\" | \"industry\" | \"public\" | \"system\";\n\n/**\n * THE ONE DECLARED STATEMENT of which lanes a surface's type cannot hold\n * (`EntityListConfig.lanes`, `EntityScopeTabs lanes`): `{ public: false }` = this type has no\n * publish path, so Public would only ever say 0. A lane that is possible but empty for this\n * person is NOT declared: it stays and shows an honest 0. Omitted = every lane the surface's\n * `scopes` names is offered (and the shell adds My team beside My Orgs).\n *\n * The facts come from the type, never from who is looking: team needs a team concept on its\n * records, shared needs a `platform.shareable_resource_registry` row, public needs a publish path\n * (`is_link_shareable`, or a public-card path as agents and workflows have), system needs a\n * platform corpus of that type.\n */\nexport type LaneSupport = Partial<Record<DeclarableLane, false>>;\n\n/** What a surface declares about the standard lanes (`EntityListConfig.lanes`). */\nexport interface StandardLanesOptions {\n lanes?: LaneSupport;\n}\n\n/** The lanes a surface declared it cannot hold. */\nexport function declaredAbsentLanes(lanes: LaneSupport | undefined): ListScopeKind[] {\n return (Object.keys(lanes ?? {}) as DeclarableLane[]).filter((k) => lanes?.[k] === false);\n}\n\n/**\n * THE ONE PLACE \"My team\" joins a list (T-29, 2026-09-27). Every surface that\n * offers \"My Orgs\" offers \"My team\" beside it — never declared per page. Its\n * `*_list_scoped` RPC answers `p_scope = 'team'` through `iam.my_team_reach`,\n * and its counts RPC returns a `team` row; a surface whose RPC cannot is a\n * defect in that RPC, not a reason to hide the tab here.\n *\n * Inserted directly after \"mine\" (the ladder reads narrow → wide: only me, my\n * team, my organization). Idempotent; a list without \"orgs\" is untouched —\n * an admin page, or a Private type, has no team question to answer.\n */\nexport function withTeamScope(\n scopes: readonly ListScopeKind[],\n lanes: StandardLanesOptions = {},\n): ListScopeKind[] {\n // A surface DECLARES the lanes its type cannot hold (`lanes: { public: false }`): a lane that\n // can never hold anything is absent, never an empty tab (a screen never lies).\n const absent = declaredAbsentLanes(lanes.lanes);\n const declared = scopes.filter((k) => !absent.includes(k));\n if (absent.includes(\"team\")) return declared;\n if (!declared.includes(\"orgs\") || declared.includes(\"team\")) return [...declared];\n const out = [...declared];\n const at = out.indexOf(\"mine\");\n out.splice(at >= 0 ? at + 1 : out.indexOf(\"orgs\"), 0, \"team\");\n return out;\n}\n\n/**\n * THE ONE PLACE \"All\" joins a list (2026-09-30). A tab bar offering two or more\n * of the personal lanes (Mine / My team / My Orgs / Shared) gets All as its\n * FIRST tab — never declared per page. A list with one personal lane (a Private\n * type: Mine alone) or none (an admin page) is untouched: All would repeat it.\n * Idempotent; its `*_list_scoped` RPC answers `p_scope = 'all'`.\n */\nexport function withAllScope(scopes: readonly ListScopeKind[]): ListScopeKind[] {\n if (scopes.includes(\"all\")) return [...scopes];\n const personal = scopes.filter((k) => ALL_LANE_MEMBERS.includes(k)).length;\n return personal >= 2 ? [\"all\", ...scopes] : [...scopes];\n}\n\n/** The lanes a tab bar renders: the surface's own, plus All and My team where they belong. */\nexport function withStandardLanes(\n scopes: readonly ListScopeKind[],\n lanes: StandardLanesOptions = {},\n): ListScopeKind[] {\n return withAllScope(withTeamScope(scopes, lanes));\n}\n\nexport function isAllScope(\n scope: ListScope,\n): scope is Extract<ListScope, { kind: \"all\" }> {\n return scope.kind === \"all\";\n}\n\nexport function isOrgsScope(\n scope: ListScope,\n): scope is Extract<ListScope, { kind: \"orgs\" }> {\n return scope.kind === \"orgs\";\n}\n\nexport function isSharedScope(\n scope: ListScope,\n): scope is Extract<ListScope, { kind: \"shared\" }> {\n return scope.kind === \"shared\";\n}\n\nexport function isIndustryScope(\n scope: ListScope,\n): scope is Extract<ListScope, { kind: \"industry\" }> {\n return scope.kind === \"industry\";\n}\n\nexport function isSystemScope(\n scope: ListScope,\n): scope is Extract<ListScope, { kind: \"system\" }> {\n return scope.kind === \"system\";\n}\n\nexport function isPublicScope(\n scope: ListScope,\n): scope is Extract<ListScope, { kind: \"public\" }> {\n return scope.kind === \"public\";\n}\n\n/** The industry this scope narrows to, or null. */\nexport function scopeIndustryId(scope: ListScope): string | null {\n return scope.kind === \"industry\" ? scope.industryId : null;\n}\n\n/**\n * The id a LANE itself is narrowed to — an industry, or an admin support lane's\n * one organization / person. Personal lanes never carry one: which organization\n * a list shows is the organization filter (`EntityListQuery.orgId`).\n */\nexport function scopeNarrowId(scope: ListScope): string | null {\n if (scope.kind === \"platform_orgs\" || scope.kind === \"platform_users\")\n return scope.organizationId;\n if (scope.kind === \"industry\") return scope.industryId;\n return null;\n}\n\n/**\n * THE `p_org_id` EVERY `*_list_scoped` / `*_scope_counts` / facets RPC receives:\n * the page's organization filter (null = All organizations → undefined), or —\n * on an admin support lane — that lane's own one-organization narrowing. Never\n * the active organization.\n */\nexport function listOrgParam(query: {\n scope: ListScope;\n orgId: string | null;\n}): string | undefined {\n if (query.orgId) return query.orgId;\n if (query.scope.kind === \"platform_orgs\" || query.scope.kind === \"platform_users\")\n return query.scope.organizationId ?? undefined;\n return undefined;\n}\n\n/** Stable identity for tab selection / React keys. */\nexport function scopeKey(scope: ListScope): string {\n if (scope.kind === \"industry\")\n return scope.industryId ? `industry:${scope.industryId}` : \"industry\";\n if (scope.kind === \"platform_orgs\" || scope.kind === \"platform_users\")\n return scope.organizationId ? `${scope.kind}:${scope.organizationId}` : scope.kind;\n return scope.kind;\n}\n\n/** Build a scope from a kind + optional narrowing id (tab click handlers). */\nexport function makeScope(\n kind: ListScopeKind,\n narrowToId: string | null = null,\n): ListScope {\n switch (kind) {\n case \"all\":\n return { kind: \"all\" };\n case \"orgs\":\n return { kind: \"orgs\" };\n case \"team\":\n return { kind: \"team\" };\n case \"industry\":\n return { kind: \"industry\", industryId: narrowToId };\n case \"mine\":\n return { kind: \"mine\" };\n case \"shared\":\n return { kind: \"shared\" };\n case \"public\":\n return { kind: \"public\" };\n case \"system\":\n return { kind: \"system\" };\n case \"platform_orgs\":\n return { kind: \"platform_orgs\", organizationId: narrowToId };\n case \"platform_users\":\n return { kind: \"platform_users\", organizationId: narrowToId };\n case \"platform_all\":\n return { kind: \"platform_all\" };\n default: {\n const _exhaustive: never = kind;\n throw new Error(\n `[list-scope] unknown scope kind: ${String(_exhaustive)}`,\n );\n }\n }\n}\n","// lib/entity-list/types.ts\n//\n// The feature-agnostic half of a canonical entity-list surface. Extracted from\n// features/agents/browse (live at /agents/all) — the proving ground. Nothing\n// here knows what an agent is.\n//\n// THE SPLIT THAT RUNS THROUGH THIS WHOLE SYSTEM:\n//\n// QUERY — scope, search, filters, page. Lives here. NEVER persisted:\n// restoring a stale search that renders an empty list is a bug\n// wearing a feature's clothes.\n// STYLE — view mode, density, sort, page size, columns. Lives in\n// lib/list-views (persisted per user, synced across devices).\n//\n// See features/agents/browse/FEATURE.md for the worked implementation and\n// lib/list-scope/FEATURE.md for the scope vocabulary + RPC template rules.\n\nimport type { ListScope, ListScopeKind } from \"./list-scope\";\nimport { DEFAULT_LIST_SCOPE, LIST_SCOPE_KINDS } from \"./list-scope\";\n\n/** Sentinel for \"has no value\" (uncategorized / untagged). Matches the SQL. */\nexport const NONE_VALUE = \"__none__\";\n\n/**\n * ONE filter vocabulary, shared by column headers and any filter panel.\n * Serialized straight into `<feature>_list_scoped(p_filters)`, so a filter set\n * from a column header and the same filter set from a panel are the same query\n * and cannot drift.\n *\n * Mirrors MatrxDataTable's ColumnFilterValue on purpose — the table is the\n * primary producer, and a translation layer with its own vocabulary would be a\n * third place for the two to disagree.\n */\nexport type EntityFilterValue =\n | { kind: \"text\"; value: string }\n | { kind: \"select\"; values: string[] }\n | { kind: \"boolean\"; value: boolean };\n\n/** Keyed by COLUMN ID, so the server predicate and the header agree by name. */\nexport type EntityFilters = Record<string, EntityFilterValue>;\n\n/**\n * THE ARCHIVED-ITEMS LAW's tri-state (`active` | `archived` | `all`) — the same words as\n * `@ai-matrx/design-system`'s `ArchiveFilterValue`. Spelled here, not imported, so this headless\n * package never installs the UI kit for one type (2026-10-08); `entity-list.types.test.ts` fails\n * typecheck the moment the two vocabularies drift apart.\n */\nexport type ArchivedFilter = \"active\" | \"archived\" | \"all\";\n\nexport interface EntityListQuery {\n /** The access LANE — how I can see a record (All, Mine, My team, …). */\n scope: ListScope;\n /**\n * THE ORGANIZATION FILTER — which of my organizations to look at. Its own\n * axis, never inside a lane: null = All organizations (the default on every\n * load), else one organization id. URL `?org_filter=`. It narrows EVERY lane and\n * the counts, and is sent to every list RPC as `p_org_id`\n * (`listOrgParam(query)`). It never initializes from, syncs with, or writes\n * to the ACTIVE organization (common-docs\n * /policies/access-ladder.md).\n */\n orgId: string | null;\n search: string;\n /**\n * Reach into full record content (prompt bodies, transcript text...).\n * Opt-in: it is a full scan server-side and must never be the default.\n */\n deep: boolean;\n /**\n * Kept OUT of `filters` because it carries a DEFAULT (\"active only\") rather\n * than being absent-means-unfiltered like every other column.\n */\n archived: ArchivedFilter;\n filters: EntityFilters;\n /** One-based, matching the pagination UI. */\n page: number;\n}\n\nexport const DEFAULT_ENTITY_LIST_QUERY: EntityListQuery = {\n scope: DEFAULT_LIST_SCOPE,\n orgId: null,\n search: \"\",\n deep: false,\n archived: \"active\",\n filters: {},\n page: 1,\n};\n\n/**\n * How many things are narrowing the list right now (the Filters badge).\n * Counts ONLY what the user applied — never the sort or the active scope.\n * A badge that reads \"1\" on an untouched page is a permanent lie that trains\n * people to ignore the number (which is exactly what /agents/all's did).\n *\n * 🚨 THE ARCHIVE AXIS IS COMPARED AGAINST THE SURFACE'S OWN DEFAULT, NOT\n * AGAINST THE LITERAL `\"active\"`. THE ARCHIVED-ITEMS LAW §6 made the starting\n * value a user KNOB (`userPreferences.lists.archivedDefault`), so a user who\n * set theirs to \"Active + archived\" met an untouched page whose Filters badge\n * read \"1\" and whose empty state claimed a search-and-filter narrowing that\n * nobody had applied — the exact permanent lie this function exists to\n * prevent, reintroduced by the knob. The default is passed in because only the\n * hook knows it.\n */\nexport function countActiveFilters(\n query: EntityListQuery,\n defaultArchived: ArchivedFilter = DEFAULT_ENTITY_LIST_QUERY.archived,\n): number {\n return (\n Object.keys(query.filters).length +\n (query.archived !== defaultArchived ? 1 : 0)\n );\n}\n\n/**\n * 🚨 THE ALL-ARCHIVED FACT — what a list must know before it may say \"none\".\n *\n * THE DEFECT THIS CLOSES (row F10 repair, 2026-09-10). Every archive-aware\n * `EntityListConfig` carries a STATIC `emptyState`, and the shell fell back to\n * it whenever the query was untouched and no rows came back. But the archive\n * filter's default HIDES archived rows (THE ARCHIVED-ITEMS LAW §2), so \"the\n * live half is empty\" and \"there is nothing here\" are different facts — and\n * the shell was printing the second when it only knew the first. Measured on\n * `/maps` with all 46 of a user's maps archived: *\"No maps yet — A map is a\n * picture of how something works … Make one\"*, beside a \"New map\" button, one\n * popover away from that page's own Archived filter holding all 46. A screen\n * telling an Expert to rebuild work they already have.\n *\n * The count CANNOT come from the facets or the scope counts: both are fetched\n * with the query's CURRENT `archived` value, so on the defaulted page they\n * describe the live half too. It is its own read, `archived: \"archived\"`, and\n * it is asked ONLY when the live half came back empty — at most one extra,\n * page-size-1 request, on the one screen that would otherwise lie.\n */\nexport type ArchivedProbe =\n /** Not applicable: the surface has no archive axis, or is already showing archived rows, or the live half is not empty. */\n | { state: \"off\" }\n /** The live half is empty and the archived count is in flight. Say nothing yet. */\n | { state: \"loading\" }\n /** Answered. `total` archived rows match this exact scope/search/filters. */\n | { state: \"known\"; total: number; /** An open-ended archive: at least `total`, the store named no count. */ more?: boolean }\n /** The read broke. The page must say it cannot tell — never fall back to \"none yet\". */\n | { state: \"failed\" };\n\n/**\n * One narrowing choice inside a scope — an org under \"My Orgs\", an industry\n * under \"Industry\". The LABEL comes from the same query as the count.\n *\n * That is deliberate and load-bearing: reading names from a Redux slice meant\n * depending on `fetchFullContext`, a thunk that only runs on tasks/org-settings\n * surfaces — so on /agents/all the slice was empty and the My Orgs dropdown\n * silently never rendered. A tab bar must be self-sufficient from its own\n * query, with no hydration ordering to get wrong.\n */\nexport interface ScopeNarrowOption {\n id: string;\n label: string;\n count: number;\n}\n\nexport interface EntityScopeCounts {\n /** True server-side total per scope kind. Absent kinds are unsupported. */\n byKind: Partial<Record<ListScopeKind, number>>;\n /**\n * The service names no lane counts for this question (an open-ended list the store pages\n * without a count — `EntityListPage.hasMore`). The tabs draw no number at all, never `0`.\n */\n uncounted?: boolean;\n /** Narrowing options per scope kind, in server order. */\n narrow: Partial<Record<ListScopeKind, ScopeNarrowOption[]>>;\n /**\n * WHY a scope has no narrowing options, in a sentence a person can act on.\n *\n * 🚨 THE DEFECT THIS CLOSES (one-resolution FIX-R6/F1, 2026-09-08). On\n * production `/mandates`, `narrow.orgs` came back empty and the declared\n * **Organization** section simply was not rendered — an admin belonging to\n * nine organizations saw a Filters panel that never mentioned organizations,\n * with nothing on screen saying why. \"No options, so no section\" is the\n * fourth law's silent failure: a declared narrowing is either offered or it\n * says what happened.\n *\n * A service that CANNOT offer options — the counts were refused, the caller's\n * organizations have not been read yet, the caller genuinely belongs to none\n * — sets the reason here and the panel prints it. Absent + no options means\n * the shell will say so in its own generic words rather than hide.\n */\n narrowUnavailable?: Partial<Record<ListScopeKind, string>>;\n}\n\nexport const EMPTY_SCOPE_COUNTS: EntityScopeCounts = {\n byKind: {},\n narrow: {},\n};\n\n/** One row of any `*_list_scope_counts` RPC. */\nexport interface ScopeCountRow {\n scope: string;\n narrow_id?: string | null;\n label?: string | null;\n total: number | string | null;\n}\n\n/**\n * THE ONE READER of a `*_list_scope_counts` result. Every surface converts\n * through here, so a scope the server learns (System, then My team) reaches its\n * tab everywhere at once. A hand-listed `kind !== \"mine\" && kind !== \"orgs\"…`\n * filter per service is how `system` came back with a real total and was\n * silently dropped on the way to its tab — the vocabulary check reads\n * LIST_SCOPE_KINDS and nothing else. A row with `narrow_id` is one\n * organization (or industry) inside its scope; without one it is the scope's\n * blended total.\n */\nexport function scopeCountsFromRows(\n rows: readonly ScopeCountRow[] | null | undefined,\n unnamed = \"Unnamed\",\n): EntityScopeCounts {\n const counts: EntityScopeCounts = { byKind: {}, narrow: {} };\n for (const row of rows ?? []) {\n const kind = LIST_SCOPE_KINDS.find((k) => k === row.scope);\n if (!kind) continue;\n const total = Number(row.total ?? 0);\n if (row.narrow_id) {\n (counts.narrow[kind] ??= []).push({\n id: row.narrow_id,\n label: row.label ?? unnamed,\n count: total,\n });\n continue;\n }\n counts.byKind[kind] = total;\n }\n return counts;\n}\n\n/**\n * Server-computed filter OPTIONS with counts, keyed by facet name.\n *\n * Server-computed is not a nicety: deriving \"which categories exist\" from\n * loaded rows means loading every row, which is the pattern this system\n * replaced. One live account has 34 categories and 773 distinct tags.\n */\nexport interface EntityFacets {\n byKind: Record<string, { value: string; count: number }[]>;\n}\n\nexport const EMPTY_FACETS: EntityFacets = { byKind: {} };\n\n/** Count for one facet value, 0 when the facet or value is absent. */\nexport function facetCount(\n facets: EntityFacets,\n kind: string,\n value: string,\n): number {\n return facets.byKind[kind]?.find((v) => v.value === value)?.count ?? 0;\n}\n\n/** Options for one facet, most-used first (the RPC already orders them). */\nexport function facetValues(\n facets: EntityFacets,\n kind: string,\n): { value: string; count: number }[] {\n return facets.byKind[kind] ?? [];\n}\n\n/**\n * A page of rows plus the TRUE total — not `rows.length`. Every\n * `*_list_scoped` RPC returns `total_count` as a window function over the\n * filtered set, identical on every row.\n */\nexport interface EntityListPage<TRow> {\n rows: TRow[];\n total: number;\n /**\n * AN OPEN-ENDED PAGE (TABLE-ACTIONS, 2026-10-03): the store pages this list and names no\n * count. Present → `total` is only how many rows reach through THIS page, and `hasMore` says\n * whether a next page exists; the pagers draw no \"of N\" and Next follows `hasMore`. Absent → a\n * counted list, as always. A count is the store's or absent — never a full read to make one.\n */\n hasMore?: boolean;\n}\n\n/** Sort + paging, handed to the RPC. Style-owned, so it arrives from prefs. */\nexport interface EntityListSort {\n /**\n * Column id. Free string, not a closed union: every column a surface\n * declares is sortable (app policy), so the valid set is that surface's\n * column registry. The server whitelists it and falls back rather than\n * erroring, so a stale stored value can never break a page.\n */\n sort: string;\n direction: \"asc\" | \"desc\";\n /** Pin favorites above every other ordering. */\n favoritesFirst: boolean;\n pageSize: number;\n}\n"],"mappings":"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","names":[]}
|
package/dist/list/index.d.cts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { ArchiveFilterValue } from '@ai-matrx/design-system';
|
|
2
|
-
|
|
3
1
|
type ListScopeKind = "all" | "mine" | "team" | "orgs" | "shared" | "industry" | "public" | "system" | "platform_orgs" | "platform_users" | "platform_all";
|
|
4
2
|
type ListScope =
|
|
5
3
|
/** ALL: Mine ∪ My team ∪ My Orgs ∪ Shared, one row per record. The default lane. */
|
|
@@ -186,14 +184,12 @@ type EntityFilterValue = {
|
|
|
186
184
|
/** Keyed by COLUMN ID, so the server predicate and the header agree by name. */
|
|
187
185
|
type EntityFilters = Record<string, EntityFilterValue>;
|
|
188
186
|
/**
|
|
189
|
-
* THE ARCHIVED-ITEMS LAW's tri-state
|
|
190
|
-
* `@ai-matrx/design-system`'s `ArchiveFilterValue
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
* package's, never this repo's, so the words on screen and the words in the
|
|
194
|
-
* type can never drift apart.
|
|
187
|
+
* THE ARCHIVED-ITEMS LAW's tri-state (`active` | `archived` | `all`) — the same words as
|
|
188
|
+
* `@ai-matrx/design-system`'s `ArchiveFilterValue`. Spelled here, not imported, so this headless
|
|
189
|
+
* package never installs the UI kit for one type (2026-10-08); `entity-list.types.test.ts` fails
|
|
190
|
+
* typecheck the moment the two vocabularies drift apart.
|
|
195
191
|
*/
|
|
196
|
-
type ArchivedFilter =
|
|
192
|
+
type ArchivedFilter = "active" | "archived" | "all";
|
|
197
193
|
interface EntityListQuery {
|
|
198
194
|
/** The access LANE — how I can see a record (All, Mine, My team, …). */
|
|
199
195
|
scope: ListScope;
|
package/dist/list/index.d.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { ArchiveFilterValue } from '@ai-matrx/design-system';
|
|
2
|
-
|
|
3
1
|
type ListScopeKind = "all" | "mine" | "team" | "orgs" | "shared" | "industry" | "public" | "system" | "platform_orgs" | "platform_users" | "platform_all";
|
|
4
2
|
type ListScope =
|
|
5
3
|
/** ALL: Mine ∪ My team ∪ My Orgs ∪ Shared, one row per record. The default lane. */
|
|
@@ -186,14 +184,12 @@ type EntityFilterValue = {
|
|
|
186
184
|
/** Keyed by COLUMN ID, so the server predicate and the header agree by name. */
|
|
187
185
|
type EntityFilters = Record<string, EntityFilterValue>;
|
|
188
186
|
/**
|
|
189
|
-
* THE ARCHIVED-ITEMS LAW's tri-state
|
|
190
|
-
* `@ai-matrx/design-system`'s `ArchiveFilterValue
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
* package's, never this repo's, so the words on screen and the words in the
|
|
194
|
-
* type can never drift apart.
|
|
187
|
+
* THE ARCHIVED-ITEMS LAW's tri-state (`active` | `archived` | `all`) — the same words as
|
|
188
|
+
* `@ai-matrx/design-system`'s `ArchiveFilterValue`. Spelled here, not imported, so this headless
|
|
189
|
+
* package never installs the UI kit for one type (2026-10-08); `entity-list.types.test.ts` fails
|
|
190
|
+
* typecheck the moment the two vocabularies drift apart.
|
|
195
191
|
*/
|
|
196
|
-
type ArchivedFilter =
|
|
192
|
+
type ArchivedFilter = "active" | "archived" | "all";
|
|
197
193
|
interface EntityListQuery {
|
|
198
194
|
/** The access LANE — how I can see a record (All, Mine, My team, …). */
|
|
199
195
|
scope: ListScope;
|
package/dist/list/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/list/list-scope.ts","../../src/list/entity-list.ts"],"sourcesContent":["// lib/list-scope/types.ts\n//\n// THE VIEW LAW (CLAUDE.md + common-docs/systems/architecture/database/FEATURE.md §6):\n// RLS is the ceiling, never the view definition. Every list query MUST\n// declare its own scope explicitly — a bare `.select(\"*\")` relying on RLS\n// alone to \"just filter to mine\" is a defect the moment a user belongs to\n// more than one org (every user does: personal org + N companies).\n//\n// THE VOCABULARY IS FIXED AND LIVES HERE. A surface declares WHICH of these\n// it supports and supplies the predicate. It may not invent one of its own —\n// a scope the user learns on one page has to mean the same thing on every\n// other page. See lib/list-scope/FEATURE.md.\n//\n// all → everything that is mine to see: Mine ∪ My team ∪ My Orgs ∪ Shared\n// (THE DEFAULT LANE; Public and System are discovery lanes, never folded in)\n// mine → what did I make?\n// team → what did my team make? (people I share a team with)\n// orgs → what does my organization have?\n// shared → what did someone hand me? (explicit iam.permissions grant)\n// industry → what does my field publish? (see below)\n// public → what has a tenant published platform-wide?\n// system → what does the PLATFORM ITSELF ship? (see below)\n//\n// SYSTEM (added 2026-08-26) is the platform's own corpus — builtin agents,\n// global shortcuts, platform content blocks: records AI Matrx publishes, not\n// records a tenant published. Which rows a viewer gets is the RPC's decision:\n// on /agents/all every signed-in person reads the PUBLISHED built-ins and a\n// platform admin reads the whole corpus (2026-09-27, agx_list_scoped).\n//\n// It is a SCOPE and not a separate admin page because that separation is what\n// produced the drift it repairs: /administration/agents/system-agents/agents\n// was a second, poorer list UI over the same table, and every feature added to\n// /agents/all (facets, server sort, the one action menu, doors, Orchestras)\n// silently skipped the system corpus. One list, one more question it answers.\n//\n// INDUSTRY is opt-in on BOTH ends, and is \"orgs\" with one more hop rather than\n// a new kind of thing: curators publish into an industry\n// (iam.industry_curators), and an org must ATTACH that industry\n// (iam.org_industries) before its members can read the corpus. Records attach\n// to an industry by GRANT ROW, following rag.data_store_grants.industry_id.\n//\n// A page renders one tab per scope the surface supports; switching scopes\n// changes the declared query, never silently reinterprets RLS output.\n//\n// TWO AXES, NEVER MIXED (Arman 2026-09-30, common-docs\n// /policies/access-ladder.md). The LANE (this union) says HOW\n// I can see a record. WHICH ORGANIZATION it is in is a separate axis — the\n// page's organization filter, `EntityListQuery.orgId` (URL `?org_filter=`, null = All\n// organizations) — and it narrows EVERY lane. A lane carries no organization id.\n// The ACTIVE organization is neither: it never reaches a list read.\n\n// THE ADMIN SEAT (Arman, 2026-09-26): \"No one acts as themselves in admin.\"\n// An admin page never shows mine / orgs / shared — those are PERSONAL-SEAT\n// questions (\"what did I make?\", \"what does MY team have?\").\n//\n// AN ADMIN MANAGEMENT PAGE HAS NO SCOPE LANES AT ALL: it manages the\n// platform's own records (the `system` scope, alone, with no tabs). Looking\n// into a tenant's records is tech support, and lives only on a separate\n// support route (…/support), which declares ADMIN_SUPPORT_LIST_SCOPES:\n//\n// platform_orgs → every organization's records (narrowable to one org)\n// platform_users → every person's own records (narrowable to one person,\n// by their personal org id)\n// platform_all → the whole platform corpus\n//\n// Guarded by `pnpm check:admin-no-personal-seat`.\n\nexport type ListScopeKind =\n | \"all\"\n | \"mine\"\n | \"team\"\n | \"orgs\"\n | \"shared\"\n | \"industry\"\n | \"public\"\n | \"system\"\n | \"platform_orgs\"\n | \"platform_users\"\n | \"platform_all\";\n\nexport type ListScope =\n /** ALL: Mine ∪ My team ∪ My Orgs ∪ Shared, one row per record. The default lane. */\n | { kind: \"all\" }\n | { kind: \"mine\" }\n /**\n * MY TEAM (access ladder: \"my team or department\"). Rows in an organization I\n * belong to, made by someone who shares a live team with me THERE — me\n * included where I am on a team. A person on no team sees NOTHING here (and\n * the list says so) — never a copy of Mine (2026-09-28). A team is a\n * list filter, never an access boundary: every row here is one \"orgs\" would\n * also show. Server reach: `iam.my_team_reach(p_org_id)` (teams FEATURE.md).\n * Narrowing to one organization is the organization filter's job (`orgId`).\n */\n | { kind: \"team\" }\n /** Every organization I belong to; one organization = the organization filter. */\n | { kind: \"orgs\" }\n | { kind: \"shared\" }\n /** `industryId: null` = blended across every industry my orgs have attached. */\n | { kind: \"industry\"; industryId: string | null }\n | { kind: \"public\" }\n /** Platform-published records; the RPC decides what each viewer reads. */\n | { kind: \"system\" }\n /** ADMIN: every organization's records. `organizationId` narrows to one. */\n | { kind: \"platform_orgs\"; organizationId: string | null }\n /** ADMIN: every person's own records. `organizationId` = one person's personal org. */\n | { kind: \"platform_users\"; organizationId: string | null }\n /** ADMIN: the whole platform corpus. */\n | { kind: \"platform_all\" };\n\n/**\n * The scopes an admin SUPPORT route declares (…/support, e.g. Mandate support\n * lookup) — never on a management page, never a personal-seat scope.\n */\nexport const ADMIN_SUPPORT_LIST_SCOPES: ListScopeKind[] = [\n \"platform_orgs\",\n \"platform_users\",\n \"platform_all\",\n];\n\n/** The personal-seat scopes: legal on user pages, banned on admin pages. */\nexport const PERSONAL_SEAT_SCOPES: readonly ListScopeKind[] = [\"all\", \"mine\", \"team\", \"orgs\", \"shared\"];\n\n/** The lanes the All lane unions. */\nexport const ALL_LANE_MEMBERS: readonly ListScopeKind[] = [\"mine\", \"team\", \"orgs\", \"shared\"];\n\n/** Every user list opens on All unless its page or knob says otherwise. */\nexport const DEFAULT_LIST_SCOPE: ListScope = { kind: \"all\" };\n\n/**\n * The vocabulary as a runtime value — for validating a scope string that\n * arrives from outside TypeScript (a counts RPC row, a URL parameter).\n *\n * Every such check reads THIS. A hand-listed subset at a call site is how the\n * `system` scope came back from the server with a real total and was silently\n * dropped on the way to its tab.\n */\nexport const LIST_SCOPE_KINDS: readonly ListScopeKind[] = [\n \"all\",\n \"mine\",\n \"team\",\n \"orgs\",\n \"shared\",\n \"industry\",\n \"public\",\n \"system\",\n \"platform_orgs\",\n \"platform_users\",\n \"platform_all\",\n];\n\n// ── Narrowing helpers ───────────────────────────────────────────────────────\n\nexport function isMineScope(\n scope: ListScope,\n): scope is Extract<ListScope, { kind: \"mine\" }> {\n return scope.kind === \"mine\";\n}\n\nexport function isTeamScope(\n scope: ListScope,\n): scope is Extract<ListScope, { kind: \"team\" }> {\n return scope.kind === \"team\";\n}\n\n/** The lanes a type can structurally be unable to hold (All and Mine always exist). */\nexport type DeclarableLane = \"team\" | \"orgs\" | \"shared\" | \"industry\" | \"public\" | \"system\";\n\n/**\n * THE ONE DECLARED STATEMENT of which lanes a surface's type cannot hold\n * (`EntityListConfig.lanes`, `EntityScopeTabs lanes`): `{ public: false }` = this type has no\n * publish path, so Public would only ever say 0. A lane that is possible but empty for this\n * person is NOT declared: it stays and shows an honest 0. Omitted = every lane the surface's\n * `scopes` names is offered (and the shell adds My team beside My Orgs).\n *\n * The facts come from the type, never from who is looking: team needs a team concept on its\n * records, shared needs a `platform.shareable_resource_registry` row, public needs a publish path\n * (`is_link_shareable`, or a public-card path as agents and workflows have), system needs a\n * platform corpus of that type.\n */\nexport type LaneSupport = Partial<Record<DeclarableLane, false>>;\n\n/** What a surface declares about the standard lanes (`EntityListConfig.lanes`). */\nexport interface StandardLanesOptions {\n lanes?: LaneSupport;\n}\n\n/** The lanes a surface declared it cannot hold. */\nexport function declaredAbsentLanes(lanes: LaneSupport | undefined): ListScopeKind[] {\n return (Object.keys(lanes ?? {}) as DeclarableLane[]).filter((k) => lanes?.[k] === false);\n}\n\n/**\n * THE ONE PLACE \"My team\" joins a list (T-29, 2026-09-27). Every surface that\n * offers \"My Orgs\" offers \"My team\" beside it — never declared per page. Its\n * `*_list_scoped` RPC answers `p_scope = 'team'` through `iam.my_team_reach`,\n * and its counts RPC returns a `team` row; a surface whose RPC cannot is a\n * defect in that RPC, not a reason to hide the tab here.\n *\n * Inserted directly after \"mine\" (the ladder reads narrow → wide: only me, my\n * team, my organization). Idempotent; a list without \"orgs\" is untouched —\n * an admin page, or a Private type, has no team question to answer.\n */\nexport function withTeamScope(\n scopes: readonly ListScopeKind[],\n lanes: StandardLanesOptions = {},\n): ListScopeKind[] {\n // A surface DECLARES the lanes its type cannot hold (`lanes: { public: false }`): a lane that\n // can never hold anything is absent, never an empty tab (a screen never lies).\n const absent = declaredAbsentLanes(lanes.lanes);\n const declared = scopes.filter((k) => !absent.includes(k));\n if (absent.includes(\"team\")) return declared;\n if (!declared.includes(\"orgs\") || declared.includes(\"team\")) return [...declared];\n const out = [...declared];\n const at = out.indexOf(\"mine\");\n out.splice(at >= 0 ? at + 1 : out.indexOf(\"orgs\"), 0, \"team\");\n return out;\n}\n\n/**\n * THE ONE PLACE \"All\" joins a list (2026-09-30). A tab bar offering two or more\n * of the personal lanes (Mine / My team / My Orgs / Shared) gets All as its\n * FIRST tab — never declared per page. A list with one personal lane (a Private\n * type: Mine alone) or none (an admin page) is untouched: All would repeat it.\n * Idempotent; its `*_list_scoped` RPC answers `p_scope = 'all'`.\n */\nexport function withAllScope(scopes: readonly ListScopeKind[]): ListScopeKind[] {\n if (scopes.includes(\"all\")) return [...scopes];\n const personal = scopes.filter((k) => ALL_LANE_MEMBERS.includes(k)).length;\n return personal >= 2 ? [\"all\", ...scopes] : [...scopes];\n}\n\n/** The lanes a tab bar renders: the surface's own, plus All and My team where they belong. */\nexport function withStandardLanes(\n scopes: readonly ListScopeKind[],\n lanes: StandardLanesOptions = {},\n): ListScopeKind[] {\n return withAllScope(withTeamScope(scopes, lanes));\n}\n\nexport function isAllScope(\n scope: ListScope,\n): scope is Extract<ListScope, { kind: \"all\" }> {\n return scope.kind === \"all\";\n}\n\nexport function isOrgsScope(\n scope: ListScope,\n): scope is Extract<ListScope, { kind: \"orgs\" }> {\n return scope.kind === \"orgs\";\n}\n\nexport function isSharedScope(\n scope: ListScope,\n): scope is Extract<ListScope, { kind: \"shared\" }> {\n return scope.kind === \"shared\";\n}\n\nexport function isIndustryScope(\n scope: ListScope,\n): scope is Extract<ListScope, { kind: \"industry\" }> {\n return scope.kind === \"industry\";\n}\n\nexport function isSystemScope(\n scope: ListScope,\n): scope is Extract<ListScope, { kind: \"system\" }> {\n return scope.kind === \"system\";\n}\n\nexport function isPublicScope(\n scope: ListScope,\n): scope is Extract<ListScope, { kind: \"public\" }> {\n return scope.kind === \"public\";\n}\n\n/** The industry this scope narrows to, or null. */\nexport function scopeIndustryId(scope: ListScope): string | null {\n return scope.kind === \"industry\" ? scope.industryId : null;\n}\n\n/**\n * The id a LANE itself is narrowed to — an industry, or an admin support lane's\n * one organization / person. Personal lanes never carry one: which organization\n * a list shows is the organization filter (`EntityListQuery.orgId`).\n */\nexport function scopeNarrowId(scope: ListScope): string | null {\n if (scope.kind === \"platform_orgs\" || scope.kind === \"platform_users\")\n return scope.organizationId;\n if (scope.kind === \"industry\") return scope.industryId;\n return null;\n}\n\n/**\n * THE `p_org_id` EVERY `*_list_scoped` / `*_scope_counts` / facets RPC receives:\n * the page's organization filter (null = All organizations → undefined), or —\n * on an admin support lane — that lane's own one-organization narrowing. Never\n * the active organization.\n */\nexport function listOrgParam(query: {\n scope: ListScope;\n orgId: string | null;\n}): string | undefined {\n if (query.orgId) return query.orgId;\n if (query.scope.kind === \"platform_orgs\" || query.scope.kind === \"platform_users\")\n return query.scope.organizationId ?? undefined;\n return undefined;\n}\n\n/** Stable identity for tab selection / React keys. */\nexport function scopeKey(scope: ListScope): string {\n if (scope.kind === \"industry\")\n return scope.industryId ? `industry:${scope.industryId}` : \"industry\";\n if (scope.kind === \"platform_orgs\" || scope.kind === \"platform_users\")\n return scope.organizationId ? `${scope.kind}:${scope.organizationId}` : scope.kind;\n return scope.kind;\n}\n\n/** Build a scope from a kind + optional narrowing id (tab click handlers). */\nexport function makeScope(\n kind: ListScopeKind,\n narrowToId: string | null = null,\n): ListScope {\n switch (kind) {\n case \"all\":\n return { kind: \"all\" };\n case \"orgs\":\n return { kind: \"orgs\" };\n case \"team\":\n return { kind: \"team\" };\n case \"industry\":\n return { kind: \"industry\", industryId: narrowToId };\n case \"mine\":\n return { kind: \"mine\" };\n case \"shared\":\n return { kind: \"shared\" };\n case \"public\":\n return { kind: \"public\" };\n case \"system\":\n return { kind: \"system\" };\n case \"platform_orgs\":\n return { kind: \"platform_orgs\", organizationId: narrowToId };\n case \"platform_users\":\n return { kind: \"platform_users\", organizationId: narrowToId };\n case \"platform_all\":\n return { kind: \"platform_all\" };\n default: {\n const _exhaustive: never = kind;\n throw new Error(\n `[list-scope] unknown scope kind: ${String(_exhaustive)}`,\n );\n }\n }\n}\n","// lib/entity-list/types.ts\n//\n// The feature-agnostic half of a canonical entity-list surface. Extracted from\n// features/agents/browse (live at /agents/all) — the proving ground. Nothing\n// here knows what an agent is.\n//\n// THE SPLIT THAT RUNS THROUGH THIS WHOLE SYSTEM:\n//\n// QUERY — scope, search, filters, page. Lives here. NEVER persisted:\n// restoring a stale search that renders an empty list is a bug\n// wearing a feature's clothes.\n// STYLE — view mode, density, sort, page size, columns. Lives in\n// lib/list-views (persisted per user, synced across devices).\n//\n// See features/agents/browse/FEATURE.md for the worked implementation and\n// lib/list-scope/FEATURE.md for the scope vocabulary + RPC template rules.\n\nimport type { ArchiveFilterValue } from \"@ai-matrx/design-system\";\nimport type { ListScope, ListScopeKind } from \"./list-scope\";\nimport { DEFAULT_LIST_SCOPE, LIST_SCOPE_KINDS } from \"./list-scope\";\n\n/** Sentinel for \"has no value\" (uncategorized / untagged). Matches the SQL. */\nexport const NONE_VALUE = \"__none__\";\n\n/**\n * ONE filter vocabulary, shared by column headers and any filter panel.\n * Serialized straight into `<feature>_list_scoped(p_filters)`, so a filter set\n * from a column header and the same filter set from a panel are the same query\n * and cannot drift.\n *\n * Mirrors MatrxDataTable's ColumnFilterValue on purpose — the table is the\n * primary producer, and a translation layer with its own vocabulary would be a\n * third place for the two to disagree.\n */\nexport type EntityFilterValue =\n | { kind: \"text\"; value: string }\n | { kind: \"select\"; values: string[] }\n | { kind: \"boolean\"; value: boolean };\n\n/** Keyed by COLUMN ID, so the server predicate and the header agree by name. */\nexport type EntityFilters = Record<string, EntityFilterValue>;\n\n/**\n * THE ARCHIVED-ITEMS LAW's tri-state, from the ONE place it is defined:\n * `@ai-matrx/design-system`'s `ArchiveFilterValue` (`active` | `archived` |\n * `all`). This alias exists only so the ~200 call sites that already say\n * `ArchivedFilter` keep reading naturally — the vocabulary itself is the\n * package's, never this repo's, so the words on screen and the words in the\n * type can never drift apart.\n */\nexport type ArchivedFilter = ArchiveFilterValue;\n\nexport interface EntityListQuery {\n /** The access LANE — how I can see a record (All, Mine, My team, …). */\n scope: ListScope;\n /**\n * THE ORGANIZATION FILTER — which of my organizations to look at. Its own\n * axis, never inside a lane: null = All organizations (the default on every\n * load), else one organization id. URL `?org_filter=`. It narrows EVERY lane and\n * the counts, and is sent to every list RPC as `p_org_id`\n * (`listOrgParam(query)`). It never initializes from, syncs with, or writes\n * to the ACTIVE organization (common-docs\n * /policies/access-ladder.md).\n */\n orgId: string | null;\n search: string;\n /**\n * Reach into full record content (prompt bodies, transcript text...).\n * Opt-in: it is a full scan server-side and must never be the default.\n */\n deep: boolean;\n /**\n * Kept OUT of `filters` because it carries a DEFAULT (\"active only\") rather\n * than being absent-means-unfiltered like every other column.\n */\n archived: ArchivedFilter;\n filters: EntityFilters;\n /** One-based, matching the pagination UI. */\n page: number;\n}\n\nexport const DEFAULT_ENTITY_LIST_QUERY: EntityListQuery = {\n scope: DEFAULT_LIST_SCOPE,\n orgId: null,\n search: \"\",\n deep: false,\n archived: \"active\",\n filters: {},\n page: 1,\n};\n\n/**\n * How many things are narrowing the list right now (the Filters badge).\n * Counts ONLY what the user applied — never the sort or the active scope.\n * A badge that reads \"1\" on an untouched page is a permanent lie that trains\n * people to ignore the number (which is exactly what /agents/all's did).\n *\n * 🚨 THE ARCHIVE AXIS IS COMPARED AGAINST THE SURFACE'S OWN DEFAULT, NOT\n * AGAINST THE LITERAL `\"active\"`. THE ARCHIVED-ITEMS LAW §6 made the starting\n * value a user KNOB (`userPreferences.lists.archivedDefault`), so a user who\n * set theirs to \"Active + archived\" met an untouched page whose Filters badge\n * read \"1\" and whose empty state claimed a search-and-filter narrowing that\n * nobody had applied — the exact permanent lie this function exists to\n * prevent, reintroduced by the knob. The default is passed in because only the\n * hook knows it.\n */\nexport function countActiveFilters(\n query: EntityListQuery,\n defaultArchived: ArchivedFilter = DEFAULT_ENTITY_LIST_QUERY.archived,\n): number {\n return (\n Object.keys(query.filters).length +\n (query.archived !== defaultArchived ? 1 : 0)\n );\n}\n\n/**\n * 🚨 THE ALL-ARCHIVED FACT — what a list must know before it may say \"none\".\n *\n * THE DEFECT THIS CLOSES (row F10 repair, 2026-09-10). Every archive-aware\n * `EntityListConfig` carries a STATIC `emptyState`, and the shell fell back to\n * it whenever the query was untouched and no rows came back. But the archive\n * filter's default HIDES archived rows (THE ARCHIVED-ITEMS LAW §2), so \"the\n * live half is empty\" and \"there is nothing here\" are different facts — and\n * the shell was printing the second when it only knew the first. Measured on\n * `/maps` with all 46 of a user's maps archived: *\"No maps yet — A map is a\n * picture of how something works … Make one\"*, beside a \"New map\" button, one\n * popover away from that page's own Archived filter holding all 46. A screen\n * telling an Expert to rebuild work they already have.\n *\n * The count CANNOT come from the facets or the scope counts: both are fetched\n * with the query's CURRENT `archived` value, so on the defaulted page they\n * describe the live half too. It is its own read, `archived: \"archived\"`, and\n * it is asked ONLY when the live half came back empty — at most one extra,\n * page-size-1 request, on the one screen that would otherwise lie.\n */\nexport type ArchivedProbe =\n /** Not applicable: the surface has no archive axis, or is already showing archived rows, or the live half is not empty. */\n | { state: \"off\" }\n /** The live half is empty and the archived count is in flight. Say nothing yet. */\n | { state: \"loading\" }\n /** Answered. `total` archived rows match this exact scope/search/filters. */\n | { state: \"known\"; total: number; /** An open-ended archive: at least `total`, the store named no count. */ more?: boolean }\n /** The read broke. The page must say it cannot tell — never fall back to \"none yet\". */\n | { state: \"failed\" };\n\n/**\n * One narrowing choice inside a scope — an org under \"My Orgs\", an industry\n * under \"Industry\". The LABEL comes from the same query as the count.\n *\n * That is deliberate and load-bearing: reading names from a Redux slice meant\n * depending on `fetchFullContext`, a thunk that only runs on tasks/org-settings\n * surfaces — so on /agents/all the slice was empty and the My Orgs dropdown\n * silently never rendered. A tab bar must be self-sufficient from its own\n * query, with no hydration ordering to get wrong.\n */\nexport interface ScopeNarrowOption {\n id: string;\n label: string;\n count: number;\n}\n\nexport interface EntityScopeCounts {\n /** True server-side total per scope kind. Absent kinds are unsupported. */\n byKind: Partial<Record<ListScopeKind, number>>;\n /**\n * The service names no lane counts for this question (an open-ended list the store pages\n * without a count — `EntityListPage.hasMore`). The tabs draw no number at all, never `0`.\n */\n uncounted?: boolean;\n /** Narrowing options per scope kind, in server order. */\n narrow: Partial<Record<ListScopeKind, ScopeNarrowOption[]>>;\n /**\n * WHY a scope has no narrowing options, in a sentence a person can act on.\n *\n * 🚨 THE DEFECT THIS CLOSES (one-resolution FIX-R6/F1, 2026-09-08). On\n * production `/mandates`, `narrow.orgs` came back empty and the declared\n * **Organization** section simply was not rendered — an admin belonging to\n * nine organizations saw a Filters panel that never mentioned organizations,\n * with nothing on screen saying why. \"No options, so no section\" is the\n * fourth law's silent failure: a declared narrowing is either offered or it\n * says what happened.\n *\n * A service that CANNOT offer options — the counts were refused, the caller's\n * organizations have not been read yet, the caller genuinely belongs to none\n * — sets the reason here and the panel prints it. Absent + no options means\n * the shell will say so in its own generic words rather than hide.\n */\n narrowUnavailable?: Partial<Record<ListScopeKind, string>>;\n}\n\nexport const EMPTY_SCOPE_COUNTS: EntityScopeCounts = {\n byKind: {},\n narrow: {},\n};\n\n/** One row of any `*_list_scope_counts` RPC. */\nexport interface ScopeCountRow {\n scope: string;\n narrow_id?: string | null;\n label?: string | null;\n total: number | string | null;\n}\n\n/**\n * THE ONE READER of a `*_list_scope_counts` result. Every surface converts\n * through here, so a scope the server learns (System, then My team) reaches its\n * tab everywhere at once. A hand-listed `kind !== \"mine\" && kind !== \"orgs\"…`\n * filter per service is how `system` came back with a real total and was\n * silently dropped on the way to its tab — the vocabulary check reads\n * LIST_SCOPE_KINDS and nothing else. A row with `narrow_id` is one\n * organization (or industry) inside its scope; without one it is the scope's\n * blended total.\n */\nexport function scopeCountsFromRows(\n rows: readonly ScopeCountRow[] | null | undefined,\n unnamed = \"Unnamed\",\n): EntityScopeCounts {\n const counts: EntityScopeCounts = { byKind: {}, narrow: {} };\n for (const row of rows ?? []) {\n const kind = LIST_SCOPE_KINDS.find((k) => k === row.scope);\n if (!kind) continue;\n const total = Number(row.total ?? 0);\n if (row.narrow_id) {\n (counts.narrow[kind] ??= []).push({\n id: row.narrow_id,\n label: row.label ?? unnamed,\n count: total,\n });\n continue;\n }\n counts.byKind[kind] = total;\n }\n return counts;\n}\n\n/**\n * Server-computed filter OPTIONS with counts, keyed by facet name.\n *\n * Server-computed is not a nicety: deriving \"which categories exist\" from\n * loaded rows means loading every row, which is the pattern this system\n * replaced. One live account has 34 categories and 773 distinct tags.\n */\nexport interface EntityFacets {\n byKind: Record<string, { value: string; count: number }[]>;\n}\n\nexport const EMPTY_FACETS: EntityFacets = { byKind: {} };\n\n/** Count for one facet value, 0 when the facet or value is absent. */\nexport function facetCount(\n facets: EntityFacets,\n kind: string,\n value: string,\n): number {\n return facets.byKind[kind]?.find((v) => v.value === value)?.count ?? 0;\n}\n\n/** Options for one facet, most-used first (the RPC already orders them). */\nexport function facetValues(\n facets: EntityFacets,\n kind: string,\n): { value: string; count: number }[] {\n return facets.byKind[kind] ?? [];\n}\n\n/**\n * A page of rows plus the TRUE total — not `rows.length`. Every\n * `*_list_scoped` RPC returns `total_count` as a window function over the\n * filtered set, identical on every row.\n */\nexport interface EntityListPage<TRow> {\n rows: TRow[];\n total: number;\n /**\n * AN OPEN-ENDED PAGE (TABLE-ACTIONS, 2026-10-03): the store pages this list and names no\n * count. Present → `total` is only how many rows reach through THIS page, and `hasMore` says\n * whether a next page exists; the pagers draw no \"of N\" and Next follows `hasMore`. Absent → a\n * counted list, as always. A count is the store's or absent — never a full read to make one.\n */\n hasMore?: boolean;\n}\n\n/** Sort + paging, handed to the RPC. Style-owned, so it arrives from prefs. */\nexport interface EntityListSort {\n /**\n * Column id. Free string, not a closed union: every column a surface\n * declares is sortable (app policy), so the valid set is that surface's\n * column registry. The server whitelists it and falls back rather than\n * erroring, so a stale stored value can never break a page.\n */\n sort: string;\n direction: \"asc\" | \"desc\";\n /** Pin favorites above every other ordering. */\n favoritesFirst: boolean;\n pageSize: number;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/list/list-scope.ts","../../src/list/entity-list.ts"],"sourcesContent":["// lib/list-scope/types.ts\n//\n// THE VIEW LAW (CLAUDE.md + common-docs/systems/architecture/database/FEATURE.md §6):\n// RLS is the ceiling, never the view definition. Every list query MUST\n// declare its own scope explicitly — a bare `.select(\"*\")` relying on RLS\n// alone to \"just filter to mine\" is a defect the moment a user belongs to\n// more than one org (every user does: personal org + N companies).\n//\n// THE VOCABULARY IS FIXED AND LIVES HERE. A surface declares WHICH of these\n// it supports and supplies the predicate. It may not invent one of its own —\n// a scope the user learns on one page has to mean the same thing on every\n// other page. See lib/list-scope/FEATURE.md.\n//\n// all → everything that is mine to see: Mine ∪ My team ∪ My Orgs ∪ Shared\n// (THE DEFAULT LANE; Public and System are discovery lanes, never folded in)\n// mine → what did I make?\n// team → what did my team make? (people I share a team with)\n// orgs → what does my organization have?\n// shared → what did someone hand me? (explicit iam.permissions grant)\n// industry → what does my field publish? (see below)\n// public → what has a tenant published platform-wide?\n// system → what does the PLATFORM ITSELF ship? (see below)\n//\n// SYSTEM (added 2026-08-26) is the platform's own corpus — builtin agents,\n// global shortcuts, platform content blocks: records AI Matrx publishes, not\n// records a tenant published. Which rows a viewer gets is the RPC's decision:\n// on /agents/all every signed-in person reads the PUBLISHED built-ins and a\n// platform admin reads the whole corpus (2026-09-27, agx_list_scoped).\n//\n// It is a SCOPE and not a separate admin page because that separation is what\n// produced the drift it repairs: /administration/agents/system-agents/agents\n// was a second, poorer list UI over the same table, and every feature added to\n// /agents/all (facets, server sort, the one action menu, doors, Orchestras)\n// silently skipped the system corpus. One list, one more question it answers.\n//\n// INDUSTRY is opt-in on BOTH ends, and is \"orgs\" with one more hop rather than\n// a new kind of thing: curators publish into an industry\n// (iam.industry_curators), and an org must ATTACH that industry\n// (iam.org_industries) before its members can read the corpus. Records attach\n// to an industry by GRANT ROW, following rag.data_store_grants.industry_id.\n//\n// A page renders one tab per scope the surface supports; switching scopes\n// changes the declared query, never silently reinterprets RLS output.\n//\n// TWO AXES, NEVER MIXED (Arman 2026-09-30, common-docs\n// /policies/access-ladder.md). The LANE (this union) says HOW\n// I can see a record. WHICH ORGANIZATION it is in is a separate axis — the\n// page's organization filter, `EntityListQuery.orgId` (URL `?org_filter=`, null = All\n// organizations) — and it narrows EVERY lane. A lane carries no organization id.\n// The ACTIVE organization is neither: it never reaches a list read.\n\n// THE ADMIN SEAT (Arman, 2026-09-26): \"No one acts as themselves in admin.\"\n// An admin page never shows mine / orgs / shared — those are PERSONAL-SEAT\n// questions (\"what did I make?\", \"what does MY team have?\").\n//\n// AN ADMIN MANAGEMENT PAGE HAS NO SCOPE LANES AT ALL: it manages the\n// platform's own records (the `system` scope, alone, with no tabs). Looking\n// into a tenant's records is tech support, and lives only on a separate\n// support route (…/support), which declares ADMIN_SUPPORT_LIST_SCOPES:\n//\n// platform_orgs → every organization's records (narrowable to one org)\n// platform_users → every person's own records (narrowable to one person,\n// by their personal org id)\n// platform_all → the whole platform corpus\n//\n// Guarded by `pnpm check:admin-no-personal-seat`.\n\nexport type ListScopeKind =\n | \"all\"\n | \"mine\"\n | \"team\"\n | \"orgs\"\n | \"shared\"\n | \"industry\"\n | \"public\"\n | \"system\"\n | \"platform_orgs\"\n | \"platform_users\"\n | \"platform_all\";\n\nexport type ListScope =\n /** ALL: Mine ∪ My team ∪ My Orgs ∪ Shared, one row per record. The default lane. */\n | { kind: \"all\" }\n | { kind: \"mine\" }\n /**\n * MY TEAM (access ladder: \"my team or department\"). Rows in an organization I\n * belong to, made by someone who shares a live team with me THERE — me\n * included where I am on a team. A person on no team sees NOTHING here (and\n * the list says so) — never a copy of Mine (2026-09-28). A team is a\n * list filter, never an access boundary: every row here is one \"orgs\" would\n * also show. Server reach: `iam.my_team_reach(p_org_id)` (teams FEATURE.md).\n * Narrowing to one organization is the organization filter's job (`orgId`).\n */\n | { kind: \"team\" }\n /** Every organization I belong to; one organization = the organization filter. */\n | { kind: \"orgs\" }\n | { kind: \"shared\" }\n /** `industryId: null` = blended across every industry my orgs have attached. */\n | { kind: \"industry\"; industryId: string | null }\n | { kind: \"public\" }\n /** Platform-published records; the RPC decides what each viewer reads. */\n | { kind: \"system\" }\n /** ADMIN: every organization's records. `organizationId` narrows to one. */\n | { kind: \"platform_orgs\"; organizationId: string | null }\n /** ADMIN: every person's own records. `organizationId` = one person's personal org. */\n | { kind: \"platform_users\"; organizationId: string | null }\n /** ADMIN: the whole platform corpus. */\n | { kind: \"platform_all\" };\n\n/**\n * The scopes an admin SUPPORT route declares (…/support, e.g. Mandate support\n * lookup) — never on a management page, never a personal-seat scope.\n */\nexport const ADMIN_SUPPORT_LIST_SCOPES: ListScopeKind[] = [\n \"platform_orgs\",\n \"platform_users\",\n \"platform_all\",\n];\n\n/** The personal-seat scopes: legal on user pages, banned on admin pages. */\nexport const PERSONAL_SEAT_SCOPES: readonly ListScopeKind[] = [\"all\", \"mine\", \"team\", \"orgs\", \"shared\"];\n\n/** The lanes the All lane unions. */\nexport const ALL_LANE_MEMBERS: readonly ListScopeKind[] = [\"mine\", \"team\", \"orgs\", \"shared\"];\n\n/** Every user list opens on All unless its page or knob says otherwise. */\nexport const DEFAULT_LIST_SCOPE: ListScope = { kind: \"all\" };\n\n/**\n * The vocabulary as a runtime value — for validating a scope string that\n * arrives from outside TypeScript (a counts RPC row, a URL parameter).\n *\n * Every such check reads THIS. A hand-listed subset at a call site is how the\n * `system` scope came back from the server with a real total and was silently\n * dropped on the way to its tab.\n */\nexport const LIST_SCOPE_KINDS: readonly ListScopeKind[] = [\n \"all\",\n \"mine\",\n \"team\",\n \"orgs\",\n \"shared\",\n \"industry\",\n \"public\",\n \"system\",\n \"platform_orgs\",\n \"platform_users\",\n \"platform_all\",\n];\n\n// ── Narrowing helpers ───────────────────────────────────────────────────────\n\nexport function isMineScope(\n scope: ListScope,\n): scope is Extract<ListScope, { kind: \"mine\" }> {\n return scope.kind === \"mine\";\n}\n\nexport function isTeamScope(\n scope: ListScope,\n): scope is Extract<ListScope, { kind: \"team\" }> {\n return scope.kind === \"team\";\n}\n\n/** The lanes a type can structurally be unable to hold (All and Mine always exist). */\nexport type DeclarableLane = \"team\" | \"orgs\" | \"shared\" | \"industry\" | \"public\" | \"system\";\n\n/**\n * THE ONE DECLARED STATEMENT of which lanes a surface's type cannot hold\n * (`EntityListConfig.lanes`, `EntityScopeTabs lanes`): `{ public: false }` = this type has no\n * publish path, so Public would only ever say 0. A lane that is possible but empty for this\n * person is NOT declared: it stays and shows an honest 0. Omitted = every lane the surface's\n * `scopes` names is offered (and the shell adds My team beside My Orgs).\n *\n * The facts come from the type, never from who is looking: team needs a team concept on its\n * records, shared needs a `platform.shareable_resource_registry` row, public needs a publish path\n * (`is_link_shareable`, or a public-card path as agents and workflows have), system needs a\n * platform corpus of that type.\n */\nexport type LaneSupport = Partial<Record<DeclarableLane, false>>;\n\n/** What a surface declares about the standard lanes (`EntityListConfig.lanes`). */\nexport interface StandardLanesOptions {\n lanes?: LaneSupport;\n}\n\n/** The lanes a surface declared it cannot hold. */\nexport function declaredAbsentLanes(lanes: LaneSupport | undefined): ListScopeKind[] {\n return (Object.keys(lanes ?? {}) as DeclarableLane[]).filter((k) => lanes?.[k] === false);\n}\n\n/**\n * THE ONE PLACE \"My team\" joins a list (T-29, 2026-09-27). Every surface that\n * offers \"My Orgs\" offers \"My team\" beside it — never declared per page. Its\n * `*_list_scoped` RPC answers `p_scope = 'team'` through `iam.my_team_reach`,\n * and its counts RPC returns a `team` row; a surface whose RPC cannot is a\n * defect in that RPC, not a reason to hide the tab here.\n *\n * Inserted directly after \"mine\" (the ladder reads narrow → wide: only me, my\n * team, my organization). Idempotent; a list without \"orgs\" is untouched —\n * an admin page, or a Private type, has no team question to answer.\n */\nexport function withTeamScope(\n scopes: readonly ListScopeKind[],\n lanes: StandardLanesOptions = {},\n): ListScopeKind[] {\n // A surface DECLARES the lanes its type cannot hold (`lanes: { public: false }`): a lane that\n // can never hold anything is absent, never an empty tab (a screen never lies).\n const absent = declaredAbsentLanes(lanes.lanes);\n const declared = scopes.filter((k) => !absent.includes(k));\n if (absent.includes(\"team\")) return declared;\n if (!declared.includes(\"orgs\") || declared.includes(\"team\")) return [...declared];\n const out = [...declared];\n const at = out.indexOf(\"mine\");\n out.splice(at >= 0 ? at + 1 : out.indexOf(\"orgs\"), 0, \"team\");\n return out;\n}\n\n/**\n * THE ONE PLACE \"All\" joins a list (2026-09-30). A tab bar offering two or more\n * of the personal lanes (Mine / My team / My Orgs / Shared) gets All as its\n * FIRST tab — never declared per page. A list with one personal lane (a Private\n * type: Mine alone) or none (an admin page) is untouched: All would repeat it.\n * Idempotent; its `*_list_scoped` RPC answers `p_scope = 'all'`.\n */\nexport function withAllScope(scopes: readonly ListScopeKind[]): ListScopeKind[] {\n if (scopes.includes(\"all\")) return [...scopes];\n const personal = scopes.filter((k) => ALL_LANE_MEMBERS.includes(k)).length;\n return personal >= 2 ? [\"all\", ...scopes] : [...scopes];\n}\n\n/** The lanes a tab bar renders: the surface's own, plus All and My team where they belong. */\nexport function withStandardLanes(\n scopes: readonly ListScopeKind[],\n lanes: StandardLanesOptions = {},\n): ListScopeKind[] {\n return withAllScope(withTeamScope(scopes, lanes));\n}\n\nexport function isAllScope(\n scope: ListScope,\n): scope is Extract<ListScope, { kind: \"all\" }> {\n return scope.kind === \"all\";\n}\n\nexport function isOrgsScope(\n scope: ListScope,\n): scope is Extract<ListScope, { kind: \"orgs\" }> {\n return scope.kind === \"orgs\";\n}\n\nexport function isSharedScope(\n scope: ListScope,\n): scope is Extract<ListScope, { kind: \"shared\" }> {\n return scope.kind === \"shared\";\n}\n\nexport function isIndustryScope(\n scope: ListScope,\n): scope is Extract<ListScope, { kind: \"industry\" }> {\n return scope.kind === \"industry\";\n}\n\nexport function isSystemScope(\n scope: ListScope,\n): scope is Extract<ListScope, { kind: \"system\" }> {\n return scope.kind === \"system\";\n}\n\nexport function isPublicScope(\n scope: ListScope,\n): scope is Extract<ListScope, { kind: \"public\" }> {\n return scope.kind === \"public\";\n}\n\n/** The industry this scope narrows to, or null. */\nexport function scopeIndustryId(scope: ListScope): string | null {\n return scope.kind === \"industry\" ? scope.industryId : null;\n}\n\n/**\n * The id a LANE itself is narrowed to — an industry, or an admin support lane's\n * one organization / person. Personal lanes never carry one: which organization\n * a list shows is the organization filter (`EntityListQuery.orgId`).\n */\nexport function scopeNarrowId(scope: ListScope): string | null {\n if (scope.kind === \"platform_orgs\" || scope.kind === \"platform_users\")\n return scope.organizationId;\n if (scope.kind === \"industry\") return scope.industryId;\n return null;\n}\n\n/**\n * THE `p_org_id` EVERY `*_list_scoped` / `*_scope_counts` / facets RPC receives:\n * the page's organization filter (null = All organizations → undefined), or —\n * on an admin support lane — that lane's own one-organization narrowing. Never\n * the active organization.\n */\nexport function listOrgParam(query: {\n scope: ListScope;\n orgId: string | null;\n}): string | undefined {\n if (query.orgId) return query.orgId;\n if (query.scope.kind === \"platform_orgs\" || query.scope.kind === \"platform_users\")\n return query.scope.organizationId ?? undefined;\n return undefined;\n}\n\n/** Stable identity for tab selection / React keys. */\nexport function scopeKey(scope: ListScope): string {\n if (scope.kind === \"industry\")\n return scope.industryId ? `industry:${scope.industryId}` : \"industry\";\n if (scope.kind === \"platform_orgs\" || scope.kind === \"platform_users\")\n return scope.organizationId ? `${scope.kind}:${scope.organizationId}` : scope.kind;\n return scope.kind;\n}\n\n/** Build a scope from a kind + optional narrowing id (tab click handlers). */\nexport function makeScope(\n kind: ListScopeKind,\n narrowToId: string | null = null,\n): ListScope {\n switch (kind) {\n case \"all\":\n return { kind: \"all\" };\n case \"orgs\":\n return { kind: \"orgs\" };\n case \"team\":\n return { kind: \"team\" };\n case \"industry\":\n return { kind: \"industry\", industryId: narrowToId };\n case \"mine\":\n return { kind: \"mine\" };\n case \"shared\":\n return { kind: \"shared\" };\n case \"public\":\n return { kind: \"public\" };\n case \"system\":\n return { kind: \"system\" };\n case \"platform_orgs\":\n return { kind: \"platform_orgs\", organizationId: narrowToId };\n case \"platform_users\":\n return { kind: \"platform_users\", organizationId: narrowToId };\n case \"platform_all\":\n return { kind: \"platform_all\" };\n default: {\n const _exhaustive: never = kind;\n throw new Error(\n `[list-scope] unknown scope kind: ${String(_exhaustive)}`,\n );\n }\n }\n}\n","// lib/entity-list/types.ts\n//\n// The feature-agnostic half of a canonical entity-list surface. Extracted from\n// features/agents/browse (live at /agents/all) — the proving ground. Nothing\n// here knows what an agent is.\n//\n// THE SPLIT THAT RUNS THROUGH THIS WHOLE SYSTEM:\n//\n// QUERY — scope, search, filters, page. Lives here. NEVER persisted:\n// restoring a stale search that renders an empty list is a bug\n// wearing a feature's clothes.\n// STYLE — view mode, density, sort, page size, columns. Lives in\n// lib/list-views (persisted per user, synced across devices).\n//\n// See features/agents/browse/FEATURE.md for the worked implementation and\n// lib/list-scope/FEATURE.md for the scope vocabulary + RPC template rules.\n\nimport type { ListScope, ListScopeKind } from \"./list-scope\";\nimport { DEFAULT_LIST_SCOPE, LIST_SCOPE_KINDS } from \"./list-scope\";\n\n/** Sentinel for \"has no value\" (uncategorized / untagged). Matches the SQL. */\nexport const NONE_VALUE = \"__none__\";\n\n/**\n * ONE filter vocabulary, shared by column headers and any filter panel.\n * Serialized straight into `<feature>_list_scoped(p_filters)`, so a filter set\n * from a column header and the same filter set from a panel are the same query\n * and cannot drift.\n *\n * Mirrors MatrxDataTable's ColumnFilterValue on purpose — the table is the\n * primary producer, and a translation layer with its own vocabulary would be a\n * third place for the two to disagree.\n */\nexport type EntityFilterValue =\n | { kind: \"text\"; value: string }\n | { kind: \"select\"; values: string[] }\n | { kind: \"boolean\"; value: boolean };\n\n/** Keyed by COLUMN ID, so the server predicate and the header agree by name. */\nexport type EntityFilters = Record<string, EntityFilterValue>;\n\n/**\n * THE ARCHIVED-ITEMS LAW's tri-state (`active` | `archived` | `all`) — the same words as\n * `@ai-matrx/design-system`'s `ArchiveFilterValue`. Spelled here, not imported, so this headless\n * package never installs the UI kit for one type (2026-10-08); `entity-list.types.test.ts` fails\n * typecheck the moment the two vocabularies drift apart.\n */\nexport type ArchivedFilter = \"active\" | \"archived\" | \"all\";\n\nexport interface EntityListQuery {\n /** The access LANE — how I can see a record (All, Mine, My team, …). */\n scope: ListScope;\n /**\n * THE ORGANIZATION FILTER — which of my organizations to look at. Its own\n * axis, never inside a lane: null = All organizations (the default on every\n * load), else one organization id. URL `?org_filter=`. It narrows EVERY lane and\n * the counts, and is sent to every list RPC as `p_org_id`\n * (`listOrgParam(query)`). It never initializes from, syncs with, or writes\n * to the ACTIVE organization (common-docs\n * /policies/access-ladder.md).\n */\n orgId: string | null;\n search: string;\n /**\n * Reach into full record content (prompt bodies, transcript text...).\n * Opt-in: it is a full scan server-side and must never be the default.\n */\n deep: boolean;\n /**\n * Kept OUT of `filters` because it carries a DEFAULT (\"active only\") rather\n * than being absent-means-unfiltered like every other column.\n */\n archived: ArchivedFilter;\n filters: EntityFilters;\n /** One-based, matching the pagination UI. */\n page: number;\n}\n\nexport const DEFAULT_ENTITY_LIST_QUERY: EntityListQuery = {\n scope: DEFAULT_LIST_SCOPE,\n orgId: null,\n search: \"\",\n deep: false,\n archived: \"active\",\n filters: {},\n page: 1,\n};\n\n/**\n * How many things are narrowing the list right now (the Filters badge).\n * Counts ONLY what the user applied — never the sort or the active scope.\n * A badge that reads \"1\" on an untouched page is a permanent lie that trains\n * people to ignore the number (which is exactly what /agents/all's did).\n *\n * 🚨 THE ARCHIVE AXIS IS COMPARED AGAINST THE SURFACE'S OWN DEFAULT, NOT\n * AGAINST THE LITERAL `\"active\"`. THE ARCHIVED-ITEMS LAW §6 made the starting\n * value a user KNOB (`userPreferences.lists.archivedDefault`), so a user who\n * set theirs to \"Active + archived\" met an untouched page whose Filters badge\n * read \"1\" and whose empty state claimed a search-and-filter narrowing that\n * nobody had applied — the exact permanent lie this function exists to\n * prevent, reintroduced by the knob. The default is passed in because only the\n * hook knows it.\n */\nexport function countActiveFilters(\n query: EntityListQuery,\n defaultArchived: ArchivedFilter = DEFAULT_ENTITY_LIST_QUERY.archived,\n): number {\n return (\n Object.keys(query.filters).length +\n (query.archived !== defaultArchived ? 1 : 0)\n );\n}\n\n/**\n * 🚨 THE ALL-ARCHIVED FACT — what a list must know before it may say \"none\".\n *\n * THE DEFECT THIS CLOSES (row F10 repair, 2026-09-10). Every archive-aware\n * `EntityListConfig` carries a STATIC `emptyState`, and the shell fell back to\n * it whenever the query was untouched and no rows came back. But the archive\n * filter's default HIDES archived rows (THE ARCHIVED-ITEMS LAW §2), so \"the\n * live half is empty\" and \"there is nothing here\" are different facts — and\n * the shell was printing the second when it only knew the first. Measured on\n * `/maps` with all 46 of a user's maps archived: *\"No maps yet — A map is a\n * picture of how something works … Make one\"*, beside a \"New map\" button, one\n * popover away from that page's own Archived filter holding all 46. A screen\n * telling an Expert to rebuild work they already have.\n *\n * The count CANNOT come from the facets or the scope counts: both are fetched\n * with the query's CURRENT `archived` value, so on the defaulted page they\n * describe the live half too. It is its own read, `archived: \"archived\"`, and\n * it is asked ONLY when the live half came back empty — at most one extra,\n * page-size-1 request, on the one screen that would otherwise lie.\n */\nexport type ArchivedProbe =\n /** Not applicable: the surface has no archive axis, or is already showing archived rows, or the live half is not empty. */\n | { state: \"off\" }\n /** The live half is empty and the archived count is in flight. Say nothing yet. */\n | { state: \"loading\" }\n /** Answered. `total` archived rows match this exact scope/search/filters. */\n | { state: \"known\"; total: number; /** An open-ended archive: at least `total`, the store named no count. */ more?: boolean }\n /** The read broke. The page must say it cannot tell — never fall back to \"none yet\". */\n | { state: \"failed\" };\n\n/**\n * One narrowing choice inside a scope — an org under \"My Orgs\", an industry\n * under \"Industry\". The LABEL comes from the same query as the count.\n *\n * That is deliberate and load-bearing: reading names from a Redux slice meant\n * depending on `fetchFullContext`, a thunk that only runs on tasks/org-settings\n * surfaces — so on /agents/all the slice was empty and the My Orgs dropdown\n * silently never rendered. A tab bar must be self-sufficient from its own\n * query, with no hydration ordering to get wrong.\n */\nexport interface ScopeNarrowOption {\n id: string;\n label: string;\n count: number;\n}\n\nexport interface EntityScopeCounts {\n /** True server-side total per scope kind. Absent kinds are unsupported. */\n byKind: Partial<Record<ListScopeKind, number>>;\n /**\n * The service names no lane counts for this question (an open-ended list the store pages\n * without a count — `EntityListPage.hasMore`). The tabs draw no number at all, never `0`.\n */\n uncounted?: boolean;\n /** Narrowing options per scope kind, in server order. */\n narrow: Partial<Record<ListScopeKind, ScopeNarrowOption[]>>;\n /**\n * WHY a scope has no narrowing options, in a sentence a person can act on.\n *\n * 🚨 THE DEFECT THIS CLOSES (one-resolution FIX-R6/F1, 2026-09-08). On\n * production `/mandates`, `narrow.orgs` came back empty and the declared\n * **Organization** section simply was not rendered — an admin belonging to\n * nine organizations saw a Filters panel that never mentioned organizations,\n * with nothing on screen saying why. \"No options, so no section\" is the\n * fourth law's silent failure: a declared narrowing is either offered or it\n * says what happened.\n *\n * A service that CANNOT offer options — the counts were refused, the caller's\n * organizations have not been read yet, the caller genuinely belongs to none\n * — sets the reason here and the panel prints it. Absent + no options means\n * the shell will say so in its own generic words rather than hide.\n */\n narrowUnavailable?: Partial<Record<ListScopeKind, string>>;\n}\n\nexport const EMPTY_SCOPE_COUNTS: EntityScopeCounts = {\n byKind: {},\n narrow: {},\n};\n\n/** One row of any `*_list_scope_counts` RPC. */\nexport interface ScopeCountRow {\n scope: string;\n narrow_id?: string | null;\n label?: string | null;\n total: number | string | null;\n}\n\n/**\n * THE ONE READER of a `*_list_scope_counts` result. Every surface converts\n * through here, so a scope the server learns (System, then My team) reaches its\n * tab everywhere at once. A hand-listed `kind !== \"mine\" && kind !== \"orgs\"…`\n * filter per service is how `system` came back with a real total and was\n * silently dropped on the way to its tab — the vocabulary check reads\n * LIST_SCOPE_KINDS and nothing else. A row with `narrow_id` is one\n * organization (or industry) inside its scope; without one it is the scope's\n * blended total.\n */\nexport function scopeCountsFromRows(\n rows: readonly ScopeCountRow[] | null | undefined,\n unnamed = \"Unnamed\",\n): EntityScopeCounts {\n const counts: EntityScopeCounts = { byKind: {}, narrow: {} };\n for (const row of rows ?? []) {\n const kind = LIST_SCOPE_KINDS.find((k) => k === row.scope);\n if (!kind) continue;\n const total = Number(row.total ?? 0);\n if (row.narrow_id) {\n (counts.narrow[kind] ??= []).push({\n id: row.narrow_id,\n label: row.label ?? unnamed,\n count: total,\n });\n continue;\n }\n counts.byKind[kind] = total;\n }\n return counts;\n}\n\n/**\n * Server-computed filter OPTIONS with counts, keyed by facet name.\n *\n * Server-computed is not a nicety: deriving \"which categories exist\" from\n * loaded rows means loading every row, which is the pattern this system\n * replaced. One live account has 34 categories and 773 distinct tags.\n */\nexport interface EntityFacets {\n byKind: Record<string, { value: string; count: number }[]>;\n}\n\nexport const EMPTY_FACETS: EntityFacets = { byKind: {} };\n\n/** Count for one facet value, 0 when the facet or value is absent. */\nexport function facetCount(\n facets: EntityFacets,\n kind: string,\n value: string,\n): number {\n return facets.byKind[kind]?.find((v) => v.value === value)?.count ?? 0;\n}\n\n/** Options for one facet, most-used first (the RPC already orders them). */\nexport function facetValues(\n facets: EntityFacets,\n kind: string,\n): { value: string; count: number }[] {\n return facets.byKind[kind] ?? [];\n}\n\n/**\n * A page of rows plus the TRUE total — not `rows.length`. Every\n * `*_list_scoped` RPC returns `total_count` as a window function over the\n * filtered set, identical on every row.\n */\nexport interface EntityListPage<TRow> {\n rows: TRow[];\n total: number;\n /**\n * AN OPEN-ENDED PAGE (TABLE-ACTIONS, 2026-10-03): the store pages this list and names no\n * count. Present → `total` is only how many rows reach through THIS page, and `hasMore` says\n * whether a next page exists; the pagers draw no \"of N\" and Next follows `hasMore`. Absent → a\n * counted list, as always. A count is the store's or absent — never a full read to make one.\n */\n hasMore?: boolean;\n}\n\n/** Sort + paging, handed to the RPC. Style-owned, so it arrives from prefs. */\nexport interface EntityListSort {\n /**\n * Column id. Free string, not a closed union: every column a surface\n * declares is sortable (app policy), so the valid set is that surface's\n * column registry. The server whitelists it and falls back rather than\n * erroring, so a stale stored value can never break a page.\n */\n sort: string;\n direction: \"asc\" | \"desc\";\n /** Pin favorites above every other ordering. */\n favoritesFirst: boolean;\n pageSize: number;\n}\n"],"mappings":"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","names":[]}
|
package/dist/react/index.cjs
CHANGED
|
@@ -368,7 +368,8 @@ function isWritingDoor(door) {
|
|
|
368
368
|
}
|
|
369
369
|
|
|
370
370
|
// src/core/recordChanges.ts
|
|
371
|
-
var
|
|
371
|
+
var import_global_slot = require("@ai-matrx/kit/global-slot");
|
|
372
|
+
var listeners = (0, import_global_slot.globalSlot)("ai-matrx.records.record-change-listeners", () => /* @__PURE__ */ new Set());
|
|
372
373
|
function announceRecordChanged(recordId) {
|
|
373
374
|
for (const listener of [...listeners]) {
|
|
374
375
|
try {
|
|
@@ -378,7 +379,7 @@ function announceRecordChanged(recordId) {
|
|
|
378
379
|
}
|
|
379
380
|
}
|
|
380
381
|
}
|
|
381
|
-
var removed = /* @__PURE__ */ new Set();
|
|
382
|
+
var removed = (0, import_global_slot.globalSlot)("ai-matrx.records.removed-records", () => /* @__PURE__ */ new Set());
|
|
382
383
|
function markRecordRemoved(recordId) {
|
|
383
384
|
removed.add(recordId);
|
|
384
385
|
}
|
|
@@ -392,7 +393,7 @@ function onRecordChanged(listener) {
|
|
|
392
393
|
listeners.add(listener);
|
|
393
394
|
return () => listeners.delete(listener);
|
|
394
395
|
}
|
|
395
|
-
var tableListeners = /* @__PURE__ */ new Set();
|
|
396
|
+
var tableListeners = (0, import_global_slot.globalSlot)("ai-matrx.records.table-structure-listeners", () => /* @__PURE__ */ new Set());
|
|
396
397
|
function announceTableStructureChanged(tableId) {
|
|
397
398
|
for (const listener of [...tableListeners]) {
|
|
398
399
|
try {
|
|
@@ -794,9 +795,10 @@ function sourceArchivedOf(value) {
|
|
|
794
795
|
}
|
|
795
796
|
|
|
796
797
|
// src/ops.ts
|
|
798
|
+
var import_global_slot2 = require("@ai-matrx/kit/global-slot");
|
|
797
799
|
var REMEMBER_MS = 3e4;
|
|
798
800
|
var REMEMBER_MAX = 500;
|
|
799
|
-
var ours = /* @__PURE__ */ new Map();
|
|
801
|
+
var ours = (0, import_global_slot2.globalSlot)("ai-matrx.records.op-ledger", () => /* @__PURE__ */ new Map());
|
|
800
802
|
function trim(now) {
|
|
801
803
|
for (const [id, at] of ours) {
|
|
802
804
|
if (now - at <= REMEMBER_MS) break;
|
|
@@ -10716,8 +10718,9 @@ function optionsTableOf(field2) {
|
|
|
10716
10718
|
return typeof id === "string" && id ? id : null;
|
|
10717
10719
|
}
|
|
10718
10720
|
async function readTableShape(client, tableId) {
|
|
10719
|
-
const fields = await client.fields({ table_id: tableId });
|
|
10721
|
+
const [fields, table] = await Promise.all([client.fields({ table_id: tableId }), client.tableRead({ table_id: tableId, organization_id: client.config.organizationId })]);
|
|
10720
10722
|
if (!fields.ok) return fields;
|
|
10723
|
+
const titleField = table.ok ? table.data?.title_field ?? null : null;
|
|
10721
10724
|
const choiceFields = fields.data.filter((f2) => optionsTableOf(f2) !== null);
|
|
10722
10725
|
const options = await Promise.all(choiceFields.map((f2) => client.fieldOptions({ field_id: f2.id })));
|
|
10723
10726
|
const choicesById = /* @__PURE__ */ new Map();
|
|
@@ -10741,8 +10744,10 @@ async function readTableShape(client, tableId) {
|
|
|
10741
10744
|
type,
|
|
10742
10745
|
origin: "custom",
|
|
10743
10746
|
required: Boolean(field2.required),
|
|
10747
|
+
multi: field2.multi === true,
|
|
10744
10748
|
readOnly: COMPUTED_TYPES.has(field2.type) || field2.source === "formula" || field2.compute_on !== null && field2.compute_on !== void 0
|
|
10745
10749
|
};
|
|
10750
|
+
if (titleField !== null && (titleField === field2.key || titleField === field2.id)) column.title = true;
|
|
10746
10751
|
const choices = choicesById.get(field2.id);
|
|
10747
10752
|
if (choices) column.choices = choices;
|
|
10748
10753
|
if (field2.type === "relation" && field2.relation_target) column.linksTo = [field2.relation_target];
|
|
@@ -11008,7 +11013,15 @@ function createTableDataPort({ client, aliases }) {
|
|
|
11008
11013
|
const binding = bindingOf(alias);
|
|
11009
11014
|
if (!binding) return noSuchAlias(alias);
|
|
11010
11015
|
const shape = await shapeOf(alias, binding);
|
|
11011
|
-
|
|
11016
|
+
if (!shape.ok) return shape;
|
|
11017
|
+
return {
|
|
11018
|
+
ok: true,
|
|
11019
|
+
data: shape.data.columns.map((c) => {
|
|
11020
|
+
const target = c.type === "relation" && c.linksTo?.length === 1 ? c.linksTo[0] : null;
|
|
11021
|
+
const served = target ? Object.entries(aliases).find(([, b]) => b.tableId === target)?.[0] : void 0;
|
|
11022
|
+
return served ? { ...c, linksToSource: served } : c;
|
|
11023
|
+
})
|
|
11024
|
+
};
|
|
11012
11025
|
},
|
|
11013
11026
|
async list(alias, q = {}, cursor) {
|
|
11014
11027
|
const binding = bindingOf(alias);
|
|
@@ -11848,7 +11861,7 @@ var f = {
|
|
|
11848
11861
|
member: (opts) => field("member", opts),
|
|
11849
11862
|
/** A file. */
|
|
11850
11863
|
attachment: (opts) => field("attachment", opts),
|
|
11851
|
-
/** A platform thing (`note`, `agent` …) of one of `kinds
|
|
11864
|
+
/** A platform thing (`note`, `agent` …) of one of `kinds`: `{ token, id }`, the store's own shape. */
|
|
11852
11865
|
entityReference: (kinds, opts) => field("entity_reference", opts, { allowedTypes: [...kinds] }),
|
|
11853
11866
|
/** Computed by the store from `formulaText` (`{Visit fee} - {Deposit taken}`); read-only. */
|
|
11854
11867
|
formula: (formulaText, opts) => field("formula", opts, { declaration: { formula_text: formulaText, ...opts?.declaration ?? {} } }, true),
|