@ai-matrx/records 0.72.23 → 0.73.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog — @ai-matrx/records
2
2
 
3
+ ## 0.73.0
4
+
5
+ New subpath `./list` (P16): the list vocabulary moved from matrx-frontend — list scopes (`ListScope`, lanes, helpers) and the entity-list query, filters, sort, scope counts and facets.
6
+
7
+ ## 0.72.24
8
+
9
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
10
+ `git diff npm/records/v0.72.23..npm/records/v0.72.24 -- apps/shared/records`).
11
+ No source changes intended and no consumer action required.
12
+
3
13
  ## 0.72.23
4
14
 
5
15
  Automatic changed-only republish (docs/metadata drift since the last tag — see
@@ -0,0 +1,221 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/list/index.ts
21
+ var list_exports = {};
22
+ __export(list_exports, {
23
+ ADMIN_SUPPORT_LIST_SCOPES: () => ADMIN_SUPPORT_LIST_SCOPES,
24
+ ALL_LANE_MEMBERS: () => ALL_LANE_MEMBERS,
25
+ DEFAULT_ENTITY_LIST_QUERY: () => DEFAULT_ENTITY_LIST_QUERY,
26
+ DEFAULT_LIST_SCOPE: () => DEFAULT_LIST_SCOPE,
27
+ EMPTY_FACETS: () => EMPTY_FACETS,
28
+ EMPTY_SCOPE_COUNTS: () => EMPTY_SCOPE_COUNTS,
29
+ LIST_SCOPE_KINDS: () => LIST_SCOPE_KINDS,
30
+ NONE_VALUE: () => NONE_VALUE,
31
+ PERSONAL_SEAT_SCOPES: () => PERSONAL_SEAT_SCOPES,
32
+ countActiveFilters: () => countActiveFilters,
33
+ declaredAbsentLanes: () => declaredAbsentLanes,
34
+ facetCount: () => facetCount,
35
+ facetValues: () => facetValues,
36
+ isAllScope: () => isAllScope,
37
+ isIndustryScope: () => isIndustryScope,
38
+ isMineScope: () => isMineScope,
39
+ isOrgsScope: () => isOrgsScope,
40
+ isPublicScope: () => isPublicScope,
41
+ isSharedScope: () => isSharedScope,
42
+ isSystemScope: () => isSystemScope,
43
+ isTeamScope: () => isTeamScope,
44
+ listOrgParam: () => listOrgParam,
45
+ makeScope: () => makeScope,
46
+ scopeCountsFromRows: () => scopeCountsFromRows,
47
+ scopeIndustryId: () => scopeIndustryId,
48
+ scopeKey: () => scopeKey,
49
+ scopeNarrowId: () => scopeNarrowId,
50
+ withAllScope: () => withAllScope,
51
+ withStandardLanes: () => withStandardLanes,
52
+ withTeamScope: () => withTeamScope
53
+ });
54
+ module.exports = __toCommonJS(list_exports);
55
+
56
+ // src/list/list-scope.ts
57
+ var ADMIN_SUPPORT_LIST_SCOPES = [
58
+ "platform_orgs",
59
+ "platform_users",
60
+ "platform_all"
61
+ ];
62
+ var PERSONAL_SEAT_SCOPES = ["all", "mine", "team", "orgs", "shared"];
63
+ var ALL_LANE_MEMBERS = ["mine", "team", "orgs", "shared"];
64
+ var DEFAULT_LIST_SCOPE = { kind: "all" };
65
+ var LIST_SCOPE_KINDS = [
66
+ "all",
67
+ "mine",
68
+ "team",
69
+ "orgs",
70
+ "shared",
71
+ "industry",
72
+ "public",
73
+ "system",
74
+ "platform_orgs",
75
+ "platform_users",
76
+ "platform_all"
77
+ ];
78
+ function isMineScope(scope) {
79
+ return scope.kind === "mine";
80
+ }
81
+ function isTeamScope(scope) {
82
+ return scope.kind === "team";
83
+ }
84
+ function declaredAbsentLanes(lanes) {
85
+ return Object.keys(lanes ?? {}).filter((k) => lanes?.[k] === false);
86
+ }
87
+ function withTeamScope(scopes, lanes = {}) {
88
+ const absent = declaredAbsentLanes(lanes.lanes);
89
+ const declared = scopes.filter((k) => !absent.includes(k));
90
+ if (absent.includes("team")) return declared;
91
+ if (!declared.includes("orgs") || declared.includes("team")) return [...declared];
92
+ const out = [...declared];
93
+ const at = out.indexOf("mine");
94
+ out.splice(at >= 0 ? at + 1 : out.indexOf("orgs"), 0, "team");
95
+ return out;
96
+ }
97
+ function withAllScope(scopes) {
98
+ if (scopes.includes("all")) return [...scopes];
99
+ const personal = scopes.filter((k) => ALL_LANE_MEMBERS.includes(k)).length;
100
+ return personal >= 2 ? ["all", ...scopes] : [...scopes];
101
+ }
102
+ function withStandardLanes(scopes, lanes = {}) {
103
+ return withAllScope(withTeamScope(scopes, lanes));
104
+ }
105
+ function isAllScope(scope) {
106
+ return scope.kind === "all";
107
+ }
108
+ function isOrgsScope(scope) {
109
+ return scope.kind === "orgs";
110
+ }
111
+ function isSharedScope(scope) {
112
+ return scope.kind === "shared";
113
+ }
114
+ function isIndustryScope(scope) {
115
+ return scope.kind === "industry";
116
+ }
117
+ function isSystemScope(scope) {
118
+ return scope.kind === "system";
119
+ }
120
+ function isPublicScope(scope) {
121
+ return scope.kind === "public";
122
+ }
123
+ function scopeIndustryId(scope) {
124
+ return scope.kind === "industry" ? scope.industryId : null;
125
+ }
126
+ function scopeNarrowId(scope) {
127
+ if (scope.kind === "platform_orgs" || scope.kind === "platform_users")
128
+ return scope.organizationId;
129
+ if (scope.kind === "industry") return scope.industryId;
130
+ return null;
131
+ }
132
+ function listOrgParam(query) {
133
+ if (query.orgId) return query.orgId;
134
+ if (query.scope.kind === "platform_orgs" || query.scope.kind === "platform_users")
135
+ return query.scope.organizationId ?? void 0;
136
+ return void 0;
137
+ }
138
+ function scopeKey(scope) {
139
+ if (scope.kind === "industry")
140
+ return scope.industryId ? `industry:${scope.industryId}` : "industry";
141
+ if (scope.kind === "platform_orgs" || scope.kind === "platform_users")
142
+ return scope.organizationId ? `${scope.kind}:${scope.organizationId}` : scope.kind;
143
+ return scope.kind;
144
+ }
145
+ function makeScope(kind, narrowToId = null) {
146
+ switch (kind) {
147
+ case "all":
148
+ return { kind: "all" };
149
+ case "orgs":
150
+ return { kind: "orgs" };
151
+ case "team":
152
+ return { kind: "team" };
153
+ case "industry":
154
+ return { kind: "industry", industryId: narrowToId };
155
+ case "mine":
156
+ return { kind: "mine" };
157
+ case "shared":
158
+ return { kind: "shared" };
159
+ case "public":
160
+ return { kind: "public" };
161
+ case "system":
162
+ return { kind: "system" };
163
+ case "platform_orgs":
164
+ return { kind: "platform_orgs", organizationId: narrowToId };
165
+ case "platform_users":
166
+ return { kind: "platform_users", organizationId: narrowToId };
167
+ case "platform_all":
168
+ return { kind: "platform_all" };
169
+ default: {
170
+ const _exhaustive = kind;
171
+ throw new Error(
172
+ `[list-scope] unknown scope kind: ${String(_exhaustive)}`
173
+ );
174
+ }
175
+ }
176
+ }
177
+
178
+ // src/list/entity-list.ts
179
+ var NONE_VALUE = "__none__";
180
+ var DEFAULT_ENTITY_LIST_QUERY = {
181
+ scope: DEFAULT_LIST_SCOPE,
182
+ orgId: null,
183
+ search: "",
184
+ deep: false,
185
+ archived: "active",
186
+ filters: {},
187
+ page: 1
188
+ };
189
+ function countActiveFilters(query, defaultArchived = DEFAULT_ENTITY_LIST_QUERY.archived) {
190
+ return Object.keys(query.filters).length + (query.archived !== defaultArchived ? 1 : 0);
191
+ }
192
+ var EMPTY_SCOPE_COUNTS = {
193
+ byKind: {},
194
+ narrow: {}
195
+ };
196
+ function scopeCountsFromRows(rows, unnamed = "Unnamed") {
197
+ const counts = { byKind: {}, narrow: {} };
198
+ for (const row of rows ?? []) {
199
+ const kind = LIST_SCOPE_KINDS.find((k) => k === row.scope);
200
+ if (!kind) continue;
201
+ const total = Number(row.total ?? 0);
202
+ if (row.narrow_id) {
203
+ (counts.narrow[kind] ??= []).push({
204
+ id: row.narrow_id,
205
+ label: row.label ?? unnamed,
206
+ count: total
207
+ });
208
+ continue;
209
+ }
210
+ counts.byKind[kind] = total;
211
+ }
212
+ return counts;
213
+ }
214
+ var EMPTY_FACETS = { byKind: {} };
215
+ function facetCount(facets, kind, value) {
216
+ return facets.byKind[kind]?.find((v) => v.value === value)?.count ?? 0;
217
+ }
218
+ function facetValues(facets, kind) {
219
+ return facets.byKind[kind] ?? [];
220
+ }
221
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +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":[]}