type ListScopeKind = "all" | "mine" | "team" | "orgs" | "shared" | "industry" | "public" | "system" | "platform_orgs" | "platform_users" | "platform_all"; type ListScope = /** ALL: Mine ∪ My team ∪ My Orgs ∪ Shared, one row per record. The default lane. */ { kind: "all"; } | { kind: "mine"; } /** * MY TEAM (access ladder: "my team or department"). Rows in an organization I * belong to, made by someone who shares a live team with me THERE — me * included where I am on a team. A person on no team sees NOTHING here (and * the list says so) — never a copy of Mine (2026-09-28). A team is a * list filter, never an access boundary: every row here is one "orgs" would * also show. Server reach: `iam.my_team_reach(p_org_id)` (teams FEATURE.md). * Narrowing to one organization is the organization filter's job (`orgId`). */ | { kind: "team"; } /** Every organization I belong to; one organization = the organization filter. */ | { kind: "orgs"; } | { kind: "shared"; } /** `industryId: null` = blended across every industry my orgs have attached. */ | { kind: "industry"; industryId: string | null; } | { kind: "public"; } /** Platform-published records; the RPC decides what each viewer reads. */ | { kind: "system"; } /** ADMIN: every organization's records. `organizationId` narrows to one. */ | { kind: "platform_orgs"; organizationId: string | null; } /** ADMIN: every person's own records. `organizationId` = one person's personal org. */ | { kind: "platform_users"; organizationId: string | null; } /** ADMIN: the whole platform corpus. */ | { kind: "platform_all"; }; /** * The scopes an admin SUPPORT route declares (…/support, e.g. Mandate support * lookup) — never on a management page, never a personal-seat scope. */ declare const ADMIN_SUPPORT_LIST_SCOPES: ListScopeKind[]; /** The personal-seat scopes: legal on user pages, banned on admin pages. */ declare const PERSONAL_SEAT_SCOPES: readonly ListScopeKind[]; /** The lanes the All lane unions. */ declare const ALL_LANE_MEMBERS: readonly ListScopeKind[]; /** The only lanes that may contribute to a personal-seat All. */ declare const VISIBLE_COMPOSABLE_LANES: readonly ListScopeKind[]; /** Reserved transport filter consumed by the scoped-list SQL boundary. */ declare const VISIBLE_LANES_FILTER: "__visible_lanes"; /** Canonical precedence for complete identity-backed list readers. */ declare const EXCLUSIVE_LANE_PRECEDENCE: readonly ListScopeKind[]; type ExclusiveLane = Exclude<(typeof EXCLUSIVE_LANE_PRECEDENCE)[number], "all">; interface LaneFacts { mine: boolean; inMyOrgs: boolean; shared?: boolean; isPublic: boolean; team?: boolean; industry?: boolean; system?: boolean; } /** Classify one complete record exactly once using the canonical precedence. */ declare function exclusiveLaneOf(facts: LaneFacts): ExclusiveLane | null; declare function inExclusiveLane(facts: LaneFacts, lane: ListScopeKind): boolean; /** * Add the display-visible lane vector to an RPC filter bag. The marker is * transport metadata, never an ordinary user filter and never an auth grant. * Unknown/absent vectors are intentionally left absent: callers must not * invent hidden lanes merely because the vocabulary contains them. */ declare function listSourceFilters(query: { filters?: Record | null; visibleScopes?: readonly ListScopeKind[]; }): Record; /** Every user list opens on All unless its page or knob says otherwise. */ declare const DEFAULT_LIST_SCOPE: ListScope; /** * The vocabulary as a runtime value — for validating a scope string that * arrives from outside TypeScript (a counts RPC row, a URL parameter). * * Every such check reads THIS. A hand-listed subset at a call site is how the * `system` scope came back from the server with a real total and was silently * dropped on the way to its tab. */ declare const LIST_SCOPE_KINDS: readonly ListScopeKind[]; declare function isMineScope(scope: ListScope): scope is Extract; declare function isTeamScope(scope: ListScope): scope is Extract; /** The lanes a type can structurally be unable to hold (All and Mine always exist). */ type DeclarableLane = "team" | "orgs" | "shared" | "industry" | "public" | "system"; /** * THE ONE DECLARED STATEMENT of which lanes a surface's type cannot hold * (`EntityListConfig.lanes`, `EntityScopeTabs lanes`): `{ public: false }` = this type has no * publish path, so Public would only ever say 0. A lane that is possible but empty for this * person is NOT declared: it stays and shows an honest 0. Omitted = every lane the surface's * `scopes` names is offered (and the shell adds My team beside My Orgs). * * The facts come from the type, never from who is looking: team needs a team concept on its * records, shared needs a `platform.shareable_resource_registry` row, public needs a publish path * (`is_link_shareable`, or a public-card path as agents and workflows have), system needs a * platform corpus of that type. */ type LaneSupport = Partial>; /** What a surface declares about the standard lanes (`EntityListConfig.lanes`). */ interface StandardLanesOptions { lanes?: LaneSupport; } /** The lanes a surface declared it cannot hold. */ declare function declaredAbsentLanes(lanes: LaneSupport | undefined): ListScopeKind[]; /** * THE ONE PLACE "My team" joins a list (T-29, 2026-09-27). Every surface that * offers "My Orgs" offers "My team" beside it — never declared per page. Its * `*_list_scoped` RPC answers `p_scope = 'team'` through `iam.my_team_reach`, * and its counts RPC returns a `team` row; a surface whose RPC cannot is a * defect in that RPC, not a reason to hide the tab here. * * Inserted directly after "mine" (the ladder reads narrow → wide: only me, my * team, my organization). Idempotent; a list without "orgs" is untouched — * an admin page, or a Private type, has no team question to answer. */ declare function withTeamScope(scopes: readonly ListScopeKind[], lanes?: StandardLanesOptions): ListScopeKind[]; /** * THE ONE PLACE "All" joins a list (2026-09-30). A tab bar offering two or more * of the personal lanes (Mine / My team / My Orgs / Shared) gets All as its * FIRST tab — never declared per page. A list with one personal lane (a Private * type: Mine alone) or none (an admin page) is untouched: All would repeat it. * Idempotent; its `*_list_scoped` RPC answers `p_scope = 'all'`. */ declare function withAllScope(scopes: readonly ListScopeKind[]): ListScopeKind[]; /** The lanes a tab bar renders: the surface's own, plus All and My team where they belong. */ declare function withStandardLanes(scopes: readonly ListScopeKind[], lanes?: StandardLanesOptions): ListScopeKind[]; declare function isAllScope(scope: ListScope): scope is Extract; declare function isOrgsScope(scope: ListScope): scope is Extract; declare function isSharedScope(scope: ListScope): scope is Extract; declare function isIndustryScope(scope: ListScope): scope is Extract; declare function isSystemScope(scope: ListScope): scope is Extract; declare function isPublicScope(scope: ListScope): scope is Extract; /** The industry this scope narrows to, or null. */ declare function scopeIndustryId(scope: ListScope): string | null; /** * The id a LANE itself is narrowed to — an industry, or an admin support lane's * one organization / person. Personal lanes never carry one: which organization * a list shows is the organization filter (`EntityListQuery.orgId`). */ declare function scopeNarrowId(scope: ListScope): string | null; /** * THE `p_org_id` EVERY `*_list_scoped` / `*_scope_counts` / facets RPC receives: * the page's organization filter (null = All organizations → undefined), or — * on an admin support lane — that lane's own one-organization narrowing. Never * the active organization. */ declare function listOrgParam(query: { scope: ListScope; orgId: string | null; }): string | undefined; /** Stable identity for tab selection / React keys. */ declare function scopeKey(scope: ListScope): string; /** Build a scope from a kind + optional narrowing id (tab click handlers). */ declare function makeScope(kind: ListScopeKind, narrowToId?: string | null): ListScope; /** Sentinel for "has no value" (uncategorized / untagged). Matches the SQL. */ declare const NONE_VALUE = "__none__"; /** * ONE filter vocabulary, shared by column headers and any filter panel. * Serialized straight into `_list_scoped(p_filters)`, so a filter set * from a column header and the same filter set from a panel are the same query * and cannot drift. * * Mirrors MatrxDataTable's ColumnFilterValue on purpose — the table is the * primary producer, and a translation layer with its own vocabulary would be a * third place for the two to disagree. */ type EntityFilterValue = { kind: "text"; value: string; } | { kind: "select"; values: string[]; } | { kind: "boolean"; value: boolean; }; /** Keyed by COLUMN ID, so the server predicate and the header agree by name. */ type EntityFilters = Record; /** * THE ARCHIVED-ITEMS LAW's tri-state (`active` | `archived` | `all`) — the same words as * `@ai-matrx/design-system`'s `ArchiveFilterValue`. Spelled here, not imported, so this headless * package never installs the UI kit for one type (2026-10-08); `entity-list.types.test.ts` fails * typecheck the moment the two vocabularies drift apart. */ type ArchivedFilter = "active" | "archived" | "all"; interface EntityListQuery { /** The access LANE — how I can see a record (All, Mine, My team, …). */ scope: ListScope; /** * THE ORGANIZATION FILTER — which of my organizations to look at. Its own * axis, never inside a lane: null = All organizations (the default on every * load), else one organization id. URL `?org_filter=`. It narrows EVERY lane and * the counts, and is sent to every list RPC as `p_org_id` * (`listOrgParam(query)`). It never initializes from, syncs with, or writes * to the ACTIVE organization (common-docs * /policies/access-ladder.md). */ orgId: string | null; search: string; /** * Reach into full record content (prompt bodies, transcript text...). * Opt-in: it is a full scan server-side and must never be the default. */ deep: boolean; /** * Kept OUT of `filters` because it carries a DEFAULT ("active only") rather * than being absent-means-unfiltered like every other column. */ archived: ArchivedFilter; filters: EntityFilters; /** Display-visible lanes, kept out of URLs but carried to every source read. */ visibleScopes?: readonly ListScopeKind[]; /** One-based, matching the pagination UI. */ page: number; } declare const DEFAULT_ENTITY_LIST_QUERY: EntityListQuery; /** * How many things are narrowing the list right now (the Filters badge). * Counts ONLY what the user applied — never the sort or the active scope. * A badge that reads "1" on an untouched page is a permanent lie that trains * people to ignore the number (which is exactly what /agents/all's did). * * 🚨 THE ARCHIVE AXIS IS COMPARED AGAINST THE SURFACE'S OWN DEFAULT, NOT * AGAINST THE LITERAL `"active"`. THE ARCHIVED-ITEMS LAW §6 made the starting * value a user KNOB (`userPreferences.lists.archivedDefault`), so a user who * set theirs to "Active + archived" met an untouched page whose Filters badge * read "1" and whose empty state claimed a search-and-filter narrowing that * nobody had applied — the exact permanent lie this function exists to * prevent, reintroduced by the knob. The default is passed in because only the * hook knows it. */ declare function countActiveFilters(query: EntityListQuery, defaultArchived?: ArchivedFilter): number; /** * 🚨 THE ALL-ARCHIVED FACT — what a list must know before it may say "none". * * THE DEFECT THIS CLOSES (row F10 repair, 2026-09-10). Every archive-aware * `EntityListConfig` carries a STATIC `emptyState`, and the shell fell back to * it whenever the query was untouched and no rows came back. But the archive * filter's default HIDES archived rows (THE ARCHIVED-ITEMS LAW §2), so "the * live half is empty" and "there is nothing here" are different facts — and * the shell was printing the second when it only knew the first. Measured on * `/maps` with all 46 of a user's maps archived: *"No maps yet — A map is a * picture of how something works … Make one"*, beside a "New map" button, one * popover away from that page's own Archived filter holding all 46. A screen * telling an Expert to rebuild work they already have. * * The count CANNOT come from the facets or the scope counts: both are fetched * with the query's CURRENT `archived` value, so on the defaulted page they * describe the live half too. It is its own read, `archived: "archived"`, and * it is asked ONLY when the live half came back empty — at most one extra, * page-size-1 request, on the one screen that would otherwise lie. */ type ArchivedProbe = /** Not applicable: the surface has no archive axis, or is already showing archived rows, or the live half is not empty. */ { state: "off"; } /** The live half is empty and the archived count is in flight. Say nothing yet. */ | { state: "loading"; } /** Answered. `total` archived rows match this exact scope/search/filters. */ | { state: "known"; total: number; /** An open-ended archive: at least `total`, the store named no count. */ more?: boolean; } /** The read broke. The page must say it cannot tell — never fall back to "none yet". */ | { state: "failed"; }; /** * One narrowing choice inside a scope — an org under "My Orgs", an industry * under "Industry". The LABEL comes from the same query as the count. * * That is deliberate and load-bearing: reading names from a Redux slice meant * depending on `fetchFullContext`, a thunk that only runs on tasks/org-settings * surfaces — so on /agents/all the slice was empty and the My Orgs dropdown * silently never rendered. A tab bar must be self-sufficient from its own * query, with no hydration ordering to get wrong. */ interface ScopeNarrowOption { id: string; label: string; count: number; } interface EntityScopeCounts { /** True server-side total per scope kind. Absent kinds are unsupported. */ byKind: Partial>; /** * The service names no lane counts for this question (an open-ended list the store pages * without a count — `EntityListPage.hasMore`). The tabs draw no number at all, never `0`. */ uncounted?: boolean; /** Narrowing options per scope kind, in server order. */ narrow: Partial>; /** * WHY a scope has no narrowing options, in a sentence a person can act on. * * 🚨 THE DEFECT THIS CLOSES (one-resolution FIX-R6/F1, 2026-09-08). On * production `/mandates`, `narrow.orgs` came back empty and the declared * **Organization** section simply was not rendered — an admin belonging to * nine organizations saw a Filters panel that never mentioned organizations, * with nothing on screen saying why. "No options, so no section" is the * fourth law's silent failure: a declared narrowing is either offered or it * says what happened. * * A service that CANNOT offer options — the counts were refused, the caller's * organizations have not been read yet, the caller genuinely belongs to none * — sets the reason here and the panel prints it. Absent + no options means * the shell will say so in its own generic words rather than hide. */ narrowUnavailable?: Partial>; } declare const EMPTY_SCOPE_COUNTS: EntityScopeCounts; /** One row of any `*_list_scope_counts` RPC. */ interface ScopeCountRow { scope: string; narrow_id?: string | null; label?: string | null; total: number | string | null; } /** * THE ONE READER of a `*_list_scope_counts` result. Every surface converts * through here, so a scope the server learns (System, then My team) reaches its * tab everywhere at once. A hand-listed `kind !== "mine" && kind !== "orgs"…` * filter per service is how `system` came back with a real total and was * silently dropped on the way to its tab — the vocabulary check reads * LIST_SCOPE_KINDS and nothing else. A row with `narrow_id` is one * organization (or industry) inside its scope; without one it is the scope's * blended total. */ declare function scopeCountsFromRows(rows: readonly ScopeCountRow[] | null | undefined, unnamed?: string, visibleScopes?: readonly ListScopeKind[]): EntityScopeCounts; /** Exact All count from the visible, composable lane counts; unknown is never zero. */ declare function deriveVisibleAllCount(byKind: Partial>, visibleScopes: readonly ListScopeKind[] | undefined): number | undefined; /** * Server-computed filter OPTIONS with counts, keyed by facet name. * * Server-computed is not a nicety: deriving "which categories exist" from * loaded rows means loading every row, which is the pattern this system * replaced. One live account has 34 categories and 773 distinct tags. */ interface EntityFacets { byKind: Record; } declare const EMPTY_FACETS: EntityFacets; /** Count for one facet value, 0 when the facet or value is absent. */ declare function facetCount(facets: EntityFacets, kind: string, value: string): number; /** Options for one facet, most-used first (the RPC already orders them). */ declare function facetValues(facets: EntityFacets, kind: string): { value: string; count: number; }[]; /** * A page of rows plus the TRUE total — not `rows.length`. Every * `*_list_scoped` RPC returns `total_count` as a window function over the * filtered set, identical on every row. */ interface EntityListPage { rows: TRow[]; total: number; /** * AN OPEN-ENDED PAGE (TABLE-ACTIONS, 2026-10-03): the store pages this list and names no * count. Present → `total` is only how many rows reach through THIS page, and `hasMore` says * whether a next page exists; the pagers draw no "of N" and Next follows `hasMore`. Absent → a * counted list, as always. A count is the store's or absent — never a full read to make one. */ hasMore?: boolean; } /** Sort + paging, handed to the RPC. Style-owned, so it arrives from prefs. */ interface EntityListSort { /** * Column id. Free string, not a closed union: every column a surface * declares is sortable (app policy), so the valid set is that surface's * column registry. The server whitelists it and falls back rather than * erroring, so a stale stored value can never break a page. */ sort: string; direction: "asc" | "desc"; /** Pin favorites above every other ordering. */ favoritesFirst: boolean; pageSize: number; } /** The reserved filter-bag key shared by every list source. */ declare const DIMENSION_FILTER_KEY = "__dimension"; type DimensionSelection = { kind: "select"; values: string[]; }; declare function dimensionValueOf(filters: Readonly>): string | null; /** Preserve a host's extended filter vocabulary (including date windows). */ declare function withDimensionValue(filters: Record, valueId: string | null): Record; export { ADMIN_SUPPORT_LIST_SCOPES, ALL_LANE_MEMBERS, DEFAULT_ENTITY_LIST_QUERY, DEFAULT_LIST_SCOPE, DIMENSION_FILTER_KEY, EMPTY_FACETS, EMPTY_SCOPE_COUNTS, EXCLUSIVE_LANE_PRECEDENCE, LIST_SCOPE_KINDS, NONE_VALUE, PERSONAL_SEAT_SCOPES, VISIBLE_COMPOSABLE_LANES, VISIBLE_LANES_FILTER, countActiveFilters, declaredAbsentLanes, deriveVisibleAllCount, dimensionValueOf, exclusiveLaneOf, facetCount, facetValues, inExclusiveLane, isAllScope, isIndustryScope, isMineScope, isOrgsScope, isPublicScope, isSharedScope, isSystemScope, isTeamScope, listOrgParam, listSourceFilters, makeScope, scopeCountsFromRows, scopeIndustryId, scopeKey, scopeNarrowId, withAllScope, withDimensionValue, withStandardLanes, withTeamScope }; export type { ArchivedFilter, ArchivedProbe, DeclarableLane, EntityFacets, EntityFilterValue, EntityFilters, EntityListPage, EntityListQuery, EntityListSort, EntityScopeCounts, ExclusiveLane, LaneFacts, LaneSupport, ListScope, ListScopeKind, ScopeCountRow, ScopeNarrowOption, StandardLanesOptions };