@x12i/catalox 4.0.2 → 4.0.4

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/README.md CHANGED
@@ -1,897 +1,909 @@
1
- # `@x12i/catalox`
2
-
3
- Catalox is platform infrastructure for catalogs: a reusable, interoperable way to manage catalog definitions once, expose them consistently to apps, and avoid rebuilding catalog plumbing in every product. It can power a catalogs-as-a-service layer without being a hosted service itself.
4
-
5
- It is a **data-tier** package for managing **app-scoped catalogs** in Firebase **Firestore**, including:
6
-
7
- - **Catalog discovery** for an `appId` (what catalogs are available + access)
8
- - **App-agnostic catalog discovery** (list all catalogs, text search, AI match by name/description; respects descriptor visibility)
9
- - **First-class catalog descriptors** (capabilities, query metadata, identity metadata, field metadata)
10
- - **Descriptor creation/modification rules** (optional governance blocks for AI-assisted create/modify)
11
- - **Native catalogs** (items stored in Firestore)
12
- - **Mapped catalogs** (items normalized from MongoDB or APIs)
13
- - **Catalog item search** (within a catalog: text search + AI match)
14
- - **AI-assisted item create/modify** (calls `aifunctions-js` `createItem` / `modifyItem`; optional `autoPersist` for native catalogs)
15
- - **References + validation contracts** (standardized cross-catalog shapes)
16
- - **Seed/import/export + batch upsert** workflows for native catalogs
17
- - **Optional per-record native history** — NDJSON payloads in GCS plus a Firestore `catalogItemHistory` index; list/show/restore/replay APIs and `catalox history …` ([`docs/record-history.md`](docs/record-history.md))
18
- - **Optional catalog lifecycle** — hard delete (with manifest), restore from manifest, and hard rename across collections; APIs + `catalox catalog …` ([`docs/catalog-crud.md`](docs/catalog-crud.md))
19
- - **Optional GCS tools** — NDJSON export/import of Firestore collections to a bucket, **compare** live data vs bucket snapshots, **`backupData` mode `gcs`** (Catalox-shaped NDJSON + manifest under a bucket prefix), and matching CLI commands (`firestore export-gcs`, `import-gcs`, `compare-gcs`, `firestore backup --mode gcs`; see [`docs/firestore-gcs-export.md`](docs/firestore-gcs-export.md), [`docs/backup.md`](docs/backup.md))
20
-
21
- Catalox **does not** own UI, workflow orchestration, remote execution, artifact blobs, or secret storage.
22
-
23
- ## When to use Catalox (and when not to)
24
-
25
- Use Catalox when you need **governed catalogs**: reusable item inventories that apps must **discover**, **render**, **search**, **reference**, and **validate**.
26
-
27
- Examples that fit Catalox well:
28
-
29
- - Product or plan definitions used by checkout/order flows
30
- - Routing rules, policies, feature flags, lookup tables
31
- - Graph templates, execution-matrix templates, prompt templates
32
- - Mappings and “configuration-as-data” that many services read
33
-
34
- Do **not** use Catalox as the transaction itself (OLTP / event/state engine):
35
-
36
- - Orders, payments, live execution runs, audit/event streams
37
- - Queues, locks, balances, high-write mutable operational state
38
-
39
- A good rule: **Catalox stores the definition or selectable item**; your host system stores the **transaction/run/event/state transition** that uses that item.
40
-
41
- Public format notes:
42
-
43
- - See [`docs/catalog-format.md`](docs/catalog-format.md) for the public shapes: catalog meta, catalog lists, and catalog items.
44
- - See [`docs/migration-catalog-model.md`](docs/migration-catalog-model.md) for the one-time backfill to the newer catalog model.
45
-
46
- **Quick onboarding (env → Firestore probe → seed preset → validate):** [`docs/onboarding-happy-path.md`](docs/onboarding-happy-path.md).
47
-
48
- ## Scoped registries: catalog types, domains, agents
49
-
50
- Catalox supports scoped “enum registries” for three common dimensions:
51
-
52
- - **Catalog types**: allowed `catalogType` values per scope
53
- - **Domains**: allowed domain ids per scope
54
- - **Agents**: allowed agent ids per scope
55
-
56
- Scopes are resolved in priority order: **store → app → global**.
57
-
58
- Item association:
59
-
60
- - Catalog items should carry `metadata.domainIds: string[]` and `metadata.agentIds: string[]` to associate the item (for example: a graph/template) to one or more domains/agents in your host runtime.
61
- - Catalox stores and returns these arrays; their semantics are **host-defined**.
62
-
63
- ## Identity & outcomes (vNext docs)
64
-
65
- If you embed Catalox behind a BFF/UI and need clarity around **tenancy/identity axes** (store/app vs account/group/channel/user/visitor vs domain/agent) and **empty vs denied vs misconfigured** semantics, start here:
66
-
67
- - [`docs/identity-model.md`](docs/identity-model.md)
68
- - [`docs/authorization.md`](docs/authorization.md)
69
- - [`docs/outcomes.md`](docs/outcomes.md)
70
-
71
- ## Install
72
-
73
- This repo is currently set up as a workspace package.
74
-
75
- - **Node**: `>=20`
76
- - **TypeScript**: builds to `dist/`
77
-
78
- `@x12i/helpers` is a **required dependency** (installed as `@x12i/helpers@^1.4.0`; GCS backup uses `@x12i/helpers/gcs`).
79
-
80
- **Direct dependency:** `@google-cloud/storage` is declared for GCS export/import/compare; it uses the same **Application Default Credentials** pattern as Firestore Admin (bucket IAM required).
81
-
82
- ### Package exports (v4)
83
-
84
- | Import path | Intended use |
85
- |-------------|----------------|
86
- | **`@x12i/catalox`** | Full surface: embedder API + operator tooling (markdown, diagrams, migrations, Firebase stores, backup/GCS helpers, etc.). |
87
- | **`@x12i/catalox/embedder`** | Catalog runtime only: **`createCatalox`**, **`Catalox`**, **`withContext` / `CataloxBound`**, contracts, errors, adapters, **`validateMappingSpec` / `executeMapping`**. |
88
- | **`@x12i/catalox/operator`** | Markdown/diagrams/JSX, validation, bindings, **`ContextResolver`**, backup/GCS transfer helpers, identity/json-io, etc. |
89
- | **`@x12i/catalox/firebase`** | Firestore-backed store classes for advanced wiring. |
90
- | **`@x12i/catalox/mapping`** | Full mapping module (including **`helper-gap-report`**). |
91
-
92
- ## Configuration (real connections)
93
-
94
- Catalox is a library: you provide initialized clients + runtime env. For a **full list of environment variables**, CLI vs library behavior, and integration-test requirements, see [`docs/environment.md`](docs/environment.md).
95
-
96
- ### Firestore (Firebase Admin SDK)
97
-
98
- This implementation of Catalox expects a **Firebase Admin SDK** Firestore instance (`firebase-admin`).
99
-
100
- Important clarification:
101
-
102
- - **“Admin” here means privileged access to your Firebase/GCP project’s Firestore**, using a service account.
103
- - It is **not** “admin of the host machine / server OS”.
104
- - Admin SDK access typically **bypasses Firestore Security Rules** (rules are for client SDKs).
105
-
106
- Scoping:
107
-
108
- - You can scope the service account to **Firestore-related IAM roles** and to a specific **project**.
109
- - You generally cannot scope an Admin SDK credential to only certain collections/documents via IAM the way client rules work.
110
-
111
- #### Firebase / Firestore credentials (`createCataloxFromEnv`, CLI, GCS helpers)
112
-
113
- Bootstrap helpers in **`@x12i/catalox/firebase`** resolve credentials in this order (same order as JSDoc on **`resolveFirebaseAdminCredentialFromEnv`**):
114
-
115
- 1. **`serviceAccountBase64`** option, else **`GOOGLE_SERVICE_ACCOUNT_BASE64`** in the environment — base64-encoded standard Google **service account JSON** (the same JSON shape as a downloaded key file), used with **`cert(...)`**. This is the **recommended** way to supply credentials in CI and scripts so behavior does not depend on a developer machine’s Application Default Credentials.
116
- 2. **`serviceAccountPath`** option onlyyour code passes a filesystem path string; Catalox reads that file and uses **`cert(...)`**. No environment variable in this package supplies that path.
117
- 3. **Application Default Credentials** (`applicationDefault()`) when neither (1) nor (2) is set (for example **`GOOGLE_APPLICATION_CREDENTIALS`**, workload identity, or GCE metadata).
118
-
119
- Optional: **`FIREBASE_PROJECT_ID`** and **`FIRESTORE_DATABASE_ID`** are read from the environment (or options) when using the bootstrap helpers.
120
-
121
- **v4:** credential bootstrap is base64-first as above; use **`GOOGLE_SERVICE_ACCOUNT_BASE64`** in `.env` or your secret store, or pass **`serviceAccountPath`** from your own code if you load a key file yourself.
122
-
123
- This repo’s CLI + live tests typically load `.env` via **`@x12i/env`**. GCS-related code paths honor **`GOOGLE_SERVICE_ACCOUNT_BASE64`** when set, then fall back to ADC.
124
-
125
- Minimal example (ADC):
126
-
127
- ```ts
128
- import { initializeApp, applicationDefault } from "firebase-admin/app";
129
- import { getFirestore } from "firebase-admin/firestore";
130
-
131
- initializeApp({ credential: applicationDefault() });
132
- const firestore = getFirestore();
133
- ```
134
-
135
- Official helper (library bootstrap; no `dotenv`):
136
-
137
- ```ts
138
- import { createCataloxFromEnv } from "@x12i/catalox/firebase";
139
-
140
- // Precedence: GOOGLE_SERVICE_ACCOUNT_BASE64 (or serviceAccountBase64 option), then serviceAccountPath option, else ADC.
141
- // Optional: FIREBASE_PROJECT_ID, FIRESTORE_DATABASE_ID.
142
- const { catalox } = createCataloxFromEnv();
143
- ```
144
-
145
- Optional connectivity check (disposable Admin app, same resolution rules as **`createCataloxFromEnv`**). The read uses a Firestore-valid probe collection (not a reserved `__…__` id):
146
-
147
- ```ts
148
- import { testFirestoreConnectionFromEnv } from "@x12i/catalox/firebase";
149
-
150
- const probe = await testFirestoreConnectionFromEnv();
151
- if (!probe.ok) throw probe.error;
152
- ```
153
-
154
- From the packaged CLI (after `npm i` / `npx`, with the same Firebase env vars as the library):
155
-
156
- ```bash
157
- catalox firestore probe
158
- ```
159
-
160
- Lifecycle-aware helper (recommended for long-lived hosts that may swap env/credentials at runtime):
161
-
162
- ```ts
163
- import { createCataloxHostFromEnv } from "@x12i/catalox/firebase";
164
-
165
- // Tip: set `appName` so you can safely replace the same Admin app instance.
166
- const host = await createCataloxHostFromEnv({
167
- appName: "catalox-host",
168
- replaceExistingApp: true,
169
- });
170
-
171
- // Use the current runtime:
172
- await host.catalox.listAppCatalogs({ appId: "myAppId" }, { appId: "myAppId" });
173
-
174
- // Later, if credentials/project/database identity changes:
175
- await host.rebuild({
176
- appName: "catalox-host",
177
- replaceExistingApp: true,
178
- env: {
179
- GOOGLE_SERVICE_ACCOUNT_BASE64: "<base64-encoded service account JSON>",
180
- FIREBASE_PROJECT_ID: "other-project",
181
- FIRESTORE_DATABASE_ID: "(default)",
182
- },
183
- });
184
-
185
- // Optional: on shutdown.
186
- await host.dispose();
187
- ```
188
-
189
- Construction helper (avoid implicit “default app” binding; use your own Admin app):
190
-
191
- ```ts
192
- import { initializeApp, cert, type App } from "firebase-admin/app";
193
- import { createCataloxFromFirebaseApp } from "@x12i/catalox/firebase";
194
-
195
- const app: App = initializeApp({ credential: cert(serviceAccountJson) }, "my-admin-app");
196
- const { catalox } = createCataloxFromFirebaseApp({ app, databaseId: "(default)" });
197
- ```
198
-
199
- ### Google Cloud Storage (optional export / import / compare)
200
-
201
- When you use **`catalox firestore export-gcs`**, **`import-gcs`**, **`compare-gcs`**, **`firestore backup --mode gcs`**, or the matching **`Catalox`** methods, the runtime needs a GCS bucket and a credential that can **read/write objects** there (typically the same service account as Firestore, with **Storage** roles on that bucket). GCS **backup** uses **`@x12i/helpers/gcs`** (ADC); export/import/compare still use **`@google-cloud/storage`** directly. See [`docs/firestore-gcs-export.md`](docs/firestore-gcs-export.md), [`docs/backup.md`](docs/backup.md), and [`docs/environment.md`](docs/environment.md).
202
-
203
- ### Mongo (mapped catalogs)
204
-
205
- For Mongo-mapped catalogs, provide:
206
-
207
- - `MONGO_URI` (or whatever you store in adapter config as `mongoUriEnvVar`)
208
-
209
- Example `.env` (do not commit secrets):
210
-
211
- ```bash
212
- MONGO_URI=mongodb://127.0.0.1:27017
213
- # Optional: ADC via GOOGLE_APPLICATION_CREDENTIALS, workload identity, etc.
214
- # GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json
215
- # Recommended for CLI + live tests: base64-encoded service account JSON (same shape as a key file)
216
- # GOOGLE_SERVICE_ACCOUNT_BASE64=...
217
- FIREBASE_PROJECT_ID=your-project-id
218
- FIRESTORE_LIVE_TESTS=1
219
- # Optional: record-history live test + CLI (`createCatalox`); use a dedicated test bucket:
220
- # CATALOX_RECORD_HISTORY_BUCKET=your-test-bucket
221
- # CATALOX_RECORD_HISTORY_PREFIX=catalox-record-history/
222
- # Optional defaults for the packaged `catalox` CLI (Catalox app/store context, not GCP):
223
- # CATALOX_APP_ID=myAppId
224
- # CATALOX_STORE_ID=myStoreId
225
- # CATALOX_USER_ID=user-123
226
- # CATALOX_MONGO_URI=mongodb://127.0.0.1:27017
227
- ```
228
-
229
- ### `catalox` CLI environment
230
-
231
- The `catalox` binary loads `.env` via `dotenv`. Use **`CATALOX_*`** for **Catalox runtime context** (catalog `appId`, optional `storeId`, actor, Mongo URI for the bundled adapter). Use **`FIREBASE_*` / `FIRESTORE_*`** plus **`GOOGLE_SERVICE_ACCOUNT_BASE64`** (when you want explicit service account JSON via env) for **Firebase Admin / Firestore connection** (GCP `projectId`, database id, live-test flags).
232
-
233
- - **`CATALOX_APP_ID`** Default `appId` in CLI context when a command does not pass `--app` (when present, `--app` overrides).
234
- - **`CATALOX_STORE_ID`** — Default `storeId` for **`report`** and **`export`** when `--store` is omitted (`--store` overrides).
235
- - **`CATALOX_USER_ID`** — Optional user id / actor for authz-sensitive CLI paths.
236
- - **`CATALOX_MONGO_URI`** — If set, enables the Mongo catalog adapter in the CLI (wired via **`createCatalox`** in `src/catalox/create-catalox.ts`, called from `src/cli/index.ts`).
237
- - **`CATALOX_RECORD_HISTORY_BUCKET`** — If set, **`createCatalox`** wires **`recordHistory`** (GCS payloads + **`catalogItemHistory`** on native writes; also required for **`catalox history …`** to persist new events).
238
- - **`CATALOX_BUCKET`** — Default GCS bucket for Catalox storage features (used by the CLI and live tests; also used as a fallback for `CATALOX_RECORD_HISTORY_BUCKET` when wiring `recordHistory` via env).
239
- - **`CATALOX_RECORD_HISTORY_PREFIX`** — Optional GCS prefix for record history (default `catalox-record-history/`).
240
- - **`CATALOX_RECORD_HISTORY_FAIL_CLOSED`** — When **`1`**, failed history writes fail the parent Firestore mutation.
241
-
242
- **Running commands from this repository:** the shell command `catalox` is only on your `PATH` if the package is installed globally (`npm i -g @x12i/catalox`) or linked (`npm link` from this repo). Otherwise, after `npm run build`, use:
243
-
244
- ```bash
245
- npm run cli -- firestore report-native-layout --app "narrix"
246
- # same as: node dist/src/cli/index.js firestore report-native-layout --app "narrix"
247
- ```
248
-
249
- #### `firestore` CLI quick reference
250
-
251
- | Command | Purpose |
252
- |---------|---------|
253
- | `firestore backup` | Mirror metadata + native rows to Mongo, Firestore `backup-*`, or GCS NDJSON (`--mode gcs --bucket …`; [`docs/backup.md`](docs/backup.md)). |
254
- | `firestore restore-backup` / `undo-restore-backup` | Restore from `backup-*` with undo sidecars ([`docs/restore-firestore-backup.md`](docs/restore-firestore-backup.md)). |
255
- | `firestore restore-backup-from-gcs` | Restore live Firestore from one `backupData` GCS run (same undo as above). |
256
- | `firestore prune-gcs-backups` / `delete-gcs-backup-run` | Retention and one-off run folder deletion in GCS. |
257
- | `firestore gcs-backup-to-export-manifest` | Emit `import-gcs`–compatible manifest JSON from a GCS backup run. |
258
- | `firestore report-native-layout` | Legacy vs flat native layout diagnostics. |
259
- | `firestore migrate-native-catalog-data` | Legacy `catalogData/{id}/items` flat `catalogData-{id}-items` ([`docs/migration-native-catalog-data.md`](docs/migration-native-catalog-data.md)). |
260
- | `firestore export-gcs` / `import-gcs` | NDJSON per collection + optional manifest ([`docs/firestore-gcs-export.md`](docs/firestore-gcs-export.md)). |
261
- | `firestore compare-gcs` | Diff live Firestore vs bucket NDJSON (single collection or manifest). |
262
-
263
- #### `history` / `catalog` CLI (3.1+)
264
-
265
- | Command | Purpose |
266
- |---------|---------|
267
- | `history list` / `history show` / `history restore` / `history replay` | Per-record native history from **`catalogItemHistory`** + GCS ([`docs/record-history.md`](docs/record-history.md)). |
268
- | `catalog delete` / `catalog restore` / `catalog rename` | Hard delete with manifest, restore from manifest, or hard rename ([`docs/catalog-crud.md`](docs/catalog-crud.md)). |
269
-
270
- ## Firestore data model (logical collections)
271
-
272
- For a **full layout** (subcollections, document id conventions, snapshot runs, and query notes), see [`docs/firestore-data-model.md`](docs/firestore-data-model.md). For **native** catalogs specifically (per-catalog `catalogData-*-items`, layout resolution, `listCatalogItems`, filters, troubleshooting), see [`docs/native-catalog-storage-and-api.md`](docs/native-catalog-storage-and-api.md).
273
-
274
- Metadata:
275
-
276
- - `apps/{appId}`
277
- - `catalogs/{catalogId}`
278
- - `catalogBindings/{bindingId}` (many-to-many app↔catalog)
279
- - `storeAppBindings/{bindingId}` (store↔app, multi-app export/report)
280
- - `catalogDefinitions/{catalogId}` (native vs mapped specifics)
281
- - `catalogAdapters/{adapterId}` (mongo/api adapter definitions)
282
- - `catalogMappings/{mappingId}` (field mapping specs)
283
- - `catalogDescriptors/{catalogId}` (**descriptor metadata** for generic consumption)
284
- - `catalogRendererSnippets/{catalogId}:{role}[:{mode}]` (**stored renderer snippets** for list/grid/item/report/dashboard rendering)
285
- - `catalogReferences/{referenceId}` (standardized reference records)
286
- - `catalogItemHistory/{eventId}` (optional **native write history** index; payloads in GCS — [`docs/record-history.md`](docs/record-history.md))
287
-
288
- Data:
289
-
290
- - `catalogData-{catalogId}-items/{itemId}` (native item rows; top-level collection per catalog)
291
- - `catalogData/{catalogId}` (index and metadata for that catalog’s native storage; not item payloads)
292
- - `catalogSnapshots/{catalogId}/items/{itemId}` (mapped snapshot mode)
293
-
294
- Backups (optional operator feature):
295
-
296
- - `backup-*` and `{timestamp}__backup-*` (Firebase mode, same database)
297
- - Mongo database `catalox-backups` (Mongo mode)
298
- - **`gs://` bucket prefixes** `backupData` **`mode: "gcs"`** (NDJSON + `catalox-backup-manifest.json` per run; default prefix `catalox-firestore-backups/`)
299
-
300
- See [`docs/backup.md`](docs/backup.md) for `backupData` / CLI backup (including **`mode: "gcs"`**), [`docs/restore-firestore-backup.md`](docs/restore-firestore-backup.md) for restoring Firebase mirrors to live data with undo, [`docs/migration-native-catalog-data.md`](docs/migration-native-catalog-data.md) for moving legacy native rows into `catalogData-{catalogId}-items`, and [`docs/firestore-native-layout-vs-backup.md`](docs/firestore-native-layout-vs-backup.md) for how live paths differ from `backup-*` mirrors and what `backupData` reports in `nativeItemSourceLayoutByCatalogId`. Gap analysis for GCS backup vs restore: [`.reports/gcs-backup-gap-analysis.md`](.reports/gcs-backup-gap-analysis.md). For **how native storage, layout resolution, `listCatalogItems`, and filters work together**, read [`docs/native-catalog-storage-and-api.md`](docs/native-catalog-storage-and-api.md). For **NDJSON export/import of Firestore collections to a GCS bucket** (single collection, all roots, optional recursive subcollections, manifest restore) and **compare live data vs bucket NDJSON** (`firestore compare-gcs`), see [`docs/firestore-gcs-export.md`](docs/firestore-gcs-export.md) and CLI `firestore export-gcs` / `firestore import-gcs` / `firestore compare-gcs`. If the console still shows **`catalogData/{catalogId}/items`**, run **`catalox firestore report-native-layout`** then **`catalox firestore migrate-native-catalog-data`** (backup + copy to flat; optional `--delete-legacy` after you trust backups).
301
-
302
- ## Core usage (app-first, generic from `appId`)
303
-
304
- ### Create `Catalox` (recommended)
305
-
306
- Use **`createCatalox`** so you do not wire every Firestore store by hand. Optional **`mongoUri`** enables Mongo mapped catalogs; API mapped catalogs use an internal adapter unless you set `enableApiAdapter: false`.
307
-
308
- ```ts
309
- import { createCatalox } from "@x12i/catalox";
310
- import { getFirestore } from "firebase-admin/firestore";
311
-
312
- const firestore = getFirestore();
313
- const catalox = createCatalox({
314
- firestore,
315
- // Optional: per-record native history to GCS + `catalogItemHistory` (or set CATALOX_RECORD_HISTORY_BUCKET for CLI)
316
- // recordHistory: { gcsBucket: "my-bucket", gcsPrefix: "catalox-record-history/" },
317
- });
318
-
319
- // Optional: bind context once (no globals; same semantics as passing context each call).
320
- // App-first (classic):
321
- const scoped = catalox.withContext({ appId: "myApp" });
322
- const list = await scoped.listCatalogItems("myCatalog", { limit: 50 });
323
- // list.listOutcome is "ok" | "mapping_blocked"; empty items with "ok" means zero rows.
324
- ```
325
-
326
- ### CatalogId-first usage (no `appId` required)
327
-
328
- If you already have a `catalogId` (deep link/bookmark/operator tool), you can omit `appId` **for catalog-specific operations**.
329
- Catalox will infer an `appId` from active `catalogBindings` when there is exactly one viable binding (or allow omission when `superAdmin: true`).
330
-
331
- ```ts
332
- const ctx = {}; // tenant/agent coords optional
333
- const items = await catalox.listCatalogItems(ctx, "signals", { limit: 50 });
334
- const descriptor = await catalox.getCatalogDescriptor(ctx, "signals");
335
- ```
336
-
337
- App-first discovery/bootstrap APIs (e.g. `listAppCatalogs`, `getAppCatalogBootstrap`) still require an explicit `appId`.
338
-
339
- ### Advanced: manual `CataloxDependencies`
340
-
341
- If you need a non-default dependency graph, import stores from `@x12i/catalox/firebase`, construct `AuthorizationService`, and pass **`new Catalox(deps)`** as before (see source under `src/catalox/catalox.ts`).
342
-
343
- ## Descriptor contract (planning-critical)
344
-
345
- Catalox is designed so upstream packages can be “generic” (no hardcoded catalog registrations). The stable contract is the persisted **descriptor**:
346
-
347
- - Stored at `catalogDescriptors/{catalogId}`
348
- - Retrieved via `getCatalogDescriptor(...)` or `getAppCatalogBootstrap(...)`
349
- - Used for identity, query, and client rendering metadata
350
-
351
- Below is the **actual current descriptor shape** (TypeScript), with the most planning-relevant subtypes.
352
-
353
- ```ts
354
- export type CatalogCapabilitiesDescriptor = {
355
- canList: boolean;
356
- canGet: boolean;
357
- canCreate: boolean;
358
- canEdit: boolean;
359
- canDelete: boolean;
360
- canImport?: boolean;
361
- canExport?: boolean;
362
- canSync?: boolean;
363
- canValidate?: boolean;
364
- canViewReferences?: boolean;
365
- };
366
-
367
- export type CatalogFieldDescriptor = {
368
- key: string;
369
- label: string;
370
- type:
371
- | "string"
372
- | "number"
373
- | "boolean"
374
- | "date"
375
- | "datetime"
376
- | "enum"
377
- | "object"
378
- | "array"
379
- | "reference";
380
- // Optional path into the stored item payload. If omitted, `key` is assumed.
381
- path?: string;
382
-
383
- // Query + indexing metadata.
384
- filterable?: boolean;
385
- sortable?: boolean;
386
- indexed?: boolean;
387
- multiValue?: boolean;
388
-
389
- // Presentation + contract metadata.
390
- listVisible?: boolean;
391
- detailVisible?: boolean;
392
- required?: boolean;
393
- enumValues?: Array<string | number>;
394
- reference?: { targetCatalogId?: string; targetField?: string };
395
- metadata?: Record<string, unknown>;
396
- };
397
-
398
- export type CatalogIdentityDescriptor = {
399
- itemIdStrategy: "natural" | "composite" | "generated";
400
- itemIdField?: string;
401
- compositeFields?: string[];
402
-
403
- // Optional display decoration fields.
404
- titleField?: string;
405
- subtitleField?: string;
406
- statusField?: string;
407
- updatedAtField?: string;
408
- };
409
-
410
- export type CatalogDescriptor = {
411
- catalogId: string;
412
- label: string;
413
- description?: string;
414
- itemLabel?: string;
415
- sourceMode: "native" | "mapped";
416
- mappedSourceType?: "mongo" | "api";
417
- status: "active" | "disabled" | "draft";
418
- visibility?: "visible" | "hidden";
419
- defaultSort?: { field: string; direction: "asc" | "desc" };
420
- defaultFilters?: Record<string, unknown>;
421
- capabilities: CatalogCapabilitiesDescriptor;
422
- queryableFields: CatalogFieldDescriptor[];
423
- queryCapabilities?: Record<string, unknown>;
424
- filterSpec?: Record<string, unknown>;
425
- presentationSpec?: Record<string, unknown>;
426
- /**
427
- * Optional UI escape hatch: registry components and/or stored snippet refs per role.
428
- * See `CatalogCustomRenderer` in the published types (`renderers[]` with `role` + optional `mode`).
429
- */
430
- customRenderer?: Record<string, unknown>;
431
- identity: CatalogIdentityDescriptor;
432
- // Optional governance for AI-assisted create/modify:
433
- creationRules?: Record<string, unknown>;
434
- modificationRules?: Record<string, unknown>;
435
- metadata?: Record<string, unknown>;
436
- };
437
- ```
438
-
439
- ### UI metadata & custom renderers (optional)
440
-
441
- **Overview (all custom UI surfaces in one place):** [`docs/catalog-custom-ui.md`](docs/catalog-custom-ui.md)
442
-
443
- Catalog descriptors can include optional UI metadata for generic presentation layers:
444
-
445
- - `filterSpec`: declarative filter configuration (built on `FieldSource`)
446
- - `presentationSpec`: declarative layout + view/edit semantics (grid/list/cards/form)
447
- - `catalogType` (optional): a host-defined tag that allows applying shared presentation and design defaults across many catalogs
448
- - `presentationSpec.templates` (optional): host-defined template assignments per role/mode (no executable code)
449
- - `presentationSpec.designRefs` (optional): references to scoped **design objects** (opaque JSON) for tokens/branding/content/assets
450
- - `customRenderer`: escape hatch with a **`renderers[]`** list. Each entry has a **`role`** (`core|list|grid|item|report|dashboard`) and optional **`mode`** (`readonly|editable`, only for `list|grid|item`). An entry may set either:
451
- - a host-resolved component (**`component`**: registry key), or
452
- - a separately-stored renderer snippet (**`snippetRef`**), plus optional **`syntax`** (`html|jsx|tsx`), **`props`**, and **`receiveMap`** (`full|minimal`).
453
-
454
- Use **`role: "core"`** for defaults shared across roles; merge with **`resolveCustomRendererEntry(...)`** (exported from `@x12i/catalox`).
455
-
456
- Custom renderer contracts:
457
-
458
- - [`docs/catalog-list-render-map.md`](docs/catalog-list-render-map.md)
459
- - [`docs/catalog-item-render-map.md`](docs/catalog-item-render-map.md)
460
- - [`docs/custom-renderer-snippets-io.md`](docs/custom-renderer-snippets-io.md) **snippet default export, `map` I/O, `actions`, roles, and `receiveMap`**
461
- - [`docs/design-objects.md`](docs/design-objects.md) scoped opaque JSON design payloads
462
-
463
- #### Presentation bindings + design objects (recommended integration)
464
-
465
- Catalox does **not** render UI. Instead it can return a **resolved binding** that tells your presentation layer *how* to render a catalog surface, plus optional merged design JSON.
466
-
467
- **Two sides (important):**
468
-
469
- - **Orchestration / BFF side (server)**: builds `map` and resolves `binding`.\n Uses `buildCatalogListPresentationSurface(...)` / `buildCatalogItemPresentationSurface(...)` to return `{ map, binding }`.
470
- - **Presentation layer side (UI)**: reads `binding.strategy` and chooses:\n - `custom-renderer`: mount a registry component or compile a stored snippet\n - `template`: render a host-owned template by `templateId`\n - `native-auto`: use `map.presentation` / descriptor hints for generic UI\n In all cases it may also apply `binding.design.merged` (opaque tokens/content) without Catalox interpreting keys.
471
-
472
- Minimal server-side example:
473
-
474
- ```ts
475
- import { createCatalox } from "@x12i/catalox";
476
- import { getFirestore } from "firebase-admin/firestore";
477
-
478
- const catalox = createCatalox({
479
- firestore: getFirestore(),
480
- includeRendererSnippets: true, // optional (only needed for snippet-backed custom renderers)
481
- includePresentationProfiles: true, // optional (type/catalog-scoped defaults)
482
- includeDesignObjects: true, // optional (design objects API + merge)
483
- });
484
-
485
- const ctx = { appId: "myAppId" };
486
-
487
- // List surface: returns render-map + resolved binding (no rendering).
488
- const { map, binding } = await catalox.buildCatalogListPresentationSurface(ctx, "orders", {
489
- displayContext: "grid",
490
- includeSnippet: true,
491
- includeDesignObjects: true,
492
- designScope: { appId: "myAppId", storeId: "myStoreId" },
493
- });
494
-
495
- // Send `{ map, binding }` to your UI (or render on the server with your own engine).
496
- ```
497
-
498
- Minimal UI-side decision:
499
-
500
- ```ts
501
- switch (binding.strategy) {
502
- case "custom-renderer":
503
- // mount binding.customRenderer.entry.component OR compile binding.customRenderer.snippet
504
- break;
505
- case "template":
506
- // render host template registry[binding.template.templateId](...)
507
- break;
508
- case "native-auto":
509
- // generic UI using map.presentation + map.items + map.actions
510
- break;
511
- }
512
-
513
- // Apply design tokens/content if present (opaque JSON).
514
- const design = binding.design?.merged;
515
- ```
516
-
517
- Design objects can also be fetched directly when `includeDesignObjects` is enabled:
518
-
519
- ```ts
520
- const design = await catalox.getDesignObject(ctx, "brand:default");
521
- const designs = await catalox.listDesignObjects(ctx, { appId: "myAppId", storeId: "myStoreId" });
522
- ```
523
-
524
- Important: host presentation layers are responsible for resolving `FieldSource` lookups (cross-catalog enums/refs) and populating `resolvedSources` for renderers.
525
-
526
- ### Search items inside a catalog (text / AI)
527
-
528
- Text search is local filtering on top of a normal `listCatalogItems` call; AI matching uses `aifunctions-js` `match()` over the candidate pool.
529
-
530
- ```ts
531
- const ctx = { appId: "myAppId" };
532
-
533
- const text = await catalox.searchCatalogItems(ctx, "signals", { text: "error", textFields: ["title", "details.message"] });
534
- const ai = await catalox.findCatalogItemsByAi(ctx, "signals", { query: "the onboarding flow item", maxResults: 3 });
535
- ```
536
-
537
- ### AI-assisted item create / modify (native catalogs)
538
-
539
- These methods call `aifunctions-js` and return a structured envelope (`issues`, `reason`, etc.). When `autoPersist: true`, they persist via `upsertNativeCatalogItem` / `updateNativeCatalogItem` **only for native catalogs**.
540
-
541
- ```ts
542
- const ctx = { appId: "myAppId" };
543
-
544
- const created = await catalox.createCatalogItemByAi(ctx, "signals", {
545
- provided: { title: "New signal", categoryId: "core" },
546
- autoPersist: false,
547
- });
548
-
549
- const updated = await catalox.modifyCatalogItemByAi(ctx, "signals", "core:S1", {
550
- patch: { title: "Renamed" },
551
- autoPersist: false,
552
- });
553
- ```
554
-
555
- #### Stored renderer snippets (optional)
556
-
557
- Descriptors reference snippets via **`customRenderer.renderers[].snippetRef`**.
558
-
559
- Snippet documents (Firestore, default layout) live at:
560
-
561
- - `catalogRendererSnippets/{catalogId}:{role}[:{mode}]` where `role` is `core|list|grid|item|report|dashboard` and `mode` is optional (`readonly|editable`)
562
-
563
- Snippet record fields include **`rendererSource`** + optional **`syntax`** (`html|jsx|tsx`). Fetch via **`catalox.getCatalogRendererSnippet(ctx, catalogId, role, mode?)`** when `rendererSnippets` is wired on `Catalox`.
564
-
565
- This package includes a small set of opt-in helpers for snippet compilation/validation:
566
-
567
- - `transpileRendererSourceToModuleSource(...)`: TSX/JSX ESM module source (string)
568
- - `typecheckRendererSnippetIo(...)` (optional): best-effort TypeScript validation that a snippet’s default export matches the role’s render-map I/O contract
569
- - `unsafeCreateRendererFunction(...)`: **UNSAFE** runtime evaluation of transpiled code
570
- - `renderRendererToHtml(...)`: best-effort HTML rendering if `react` + `react-dom` are installed
571
-
572
- **Persisted renderer metadata** must already match the role/`renderers[]` model. There is no built-in migration CLI; see [`docs/custom-renderer-canonical-contract.md`](docs/custom-renderer-canonical-contract.md). The package still exports **`buildRendererSnippetDocId`** for consistent snippet document ids.
573
-
574
- **Worked example (catalog viewer + Catalox `map`)**: [`docs/new-feature/USAGE.md`](docs/new-feature/USAGE.md)
575
-
576
- ### What you need persisted for “generic consumption”
577
-
578
- To let consumers operate *purely* from `appId` with no hardcoded catalog registrations, Catalox relies on these being present in Firestore:
579
-
580
- - **Bindings**: `catalogBindings` determine which catalogs an app can see/use.
581
- - **Descriptors**: `catalogDescriptors/{catalogId}` provide capabilities, query metadata, identity metadata, and field metadata.
582
-
583
- Minimum viable setup for generic consumption is **catalog + descriptor + binding**.
584
-
585
- For **mapped** catalogs, the minimum viable setup is also **definition + mapping + adapter** (created automatically if you use `createCatalog` with `sourceMode: "mapped"`).
586
-
587
- ### Discover catalogs for an app
588
-
589
- ```ts
590
- const context = { appId: "myAppId" };
591
- const catalogs = await catalox.listAppCatalogs(context, { appId: "myAppId" });
592
- ```
593
-
594
- ### Discover catalogs across all apps (app-agnostic)
595
-
596
- These APIs return the **catalog lists themselves** (metadata), not items. They merge `catalogs/{catalogId}` with `catalogDescriptors/{catalogId}` and apply visibility filtering (hidden catalogs are excluded unless `context.superAdmin` or `includeHidden: true`).
597
-
598
- ```ts
599
- const ctx = { appId: "myAppId", superAdmin: true };
600
-
601
- const all = await catalox.listAllCatalogs(ctx, { includeDisabled: false });
602
- const hits = await catalox.findCatalogs(ctx, { text: "users", fields: ["label", "description"] });
603
- const ai = await catalox.findCatalogsByAi(ctx, { query: "customer lists", maxResults: 5 });
604
- ```
605
-
606
- ### Get a catalog descriptor
607
-
608
- ```ts
609
- const descriptor = await catalox.getCatalogDescriptor(context, "signals");
610
- ```
611
-
612
- ### Bootstrap (all descriptors accessible to an app)
613
-
614
- ```ts
615
- const bootstrap = await catalox.getAppCatalogBootstrap(context, "myAppId");
616
- ```
617
-
618
- ### Common “generic client” flow (no hardcoded catalog logic)
619
-
620
- 1) `listAppCatalogs(appId)` → get catalog list + access
621
- 2) `getAppCatalogBootstrap(appId)` get descriptors (capabilities/query/identity/fields)
622
- 3) `listCatalogItems(catalogId, filter/sort)` → `{ listOutcome, items, issues? }` (`listOutcome` distinguishes OK empty lists vs mapping validation blocking)
623
- 4) `getCatalogItem(catalogId, itemId)` → `{ outcome: "found" | "not_found" | "mapping_blocked", ... }`
624
- 5) Optional: `getCatalogItemReferences(...)`, `validateCatalogItem(...)`
625
-
626
- ## Provisioning (create catalog + bind + descriptor)
627
-
628
- Catalox includes provisioning helpers to reduce “manual Firestore wiring”.
629
-
630
- ### Create a native catalog (also seeds a minimal descriptor)
631
-
632
- ```ts
633
- const ctx = { appId: "myAppId", superAdmin: true }; // set only after host auth; often from AppRecord.superAdminApp
634
-
635
- await catalox.createCatalog(ctx, {
636
- catalogId: "signals",
637
- name: "Signals",
638
- sourceMode: "native",
639
- native: { type: "native" },
640
- });
641
- ```
642
-
643
- ### Bind a catalog to an app (enables discovery + access)
644
-
645
- ```ts
646
- await catalox.bindCatalogToApp(ctx, {
647
- appId: "myAppId",
648
- catalogId: "signals",
649
- access: { canRead: true, canWrite: true, canAdmin: true },
650
- });
651
- ```
652
-
653
- ### Patch/upsert a descriptor (recommended for identity + query metadata)
654
-
655
- There is currently no `Catalox.setCatalogDescriptor(...)` method; consumers can upsert the persisted record via `DescriptorStore`.
656
-
657
- ```ts
658
- await descriptors.upsert({
659
- catalogId: "signals",
660
- descriptorVersion: "2",
661
- descriptor: {
662
- catalogId: "signals",
663
- label: "Signals",
664
- sourceMode: "native",
665
- status: "active",
666
- capabilities: { canList: true, canGet: true, canCreate: true, canEdit: true, canDelete: true },
667
- queryableFields: [
668
- { key: "categoryId", label: "Category", type: "string", indexed: true, filterable: true },
669
- { key: "code", label: "Code", type: "string", indexed: true, filterable: true },
670
- { key: "title", label: "Title", type: "string" },
671
- ],
672
- identity: {
673
- itemIdStrategy: "composite",
674
- compositeFields: ["categoryId", "code"],
675
- titleField: "title",
676
- },
677
- },
678
- createdAt: new Date().toISOString(),
679
- updatedAt: new Date().toISOString(),
680
- });
681
- ```
682
-
683
- ### Ensure helpers (idempotent)
684
-
685
- - `ensureCatalog(...)`: creates a minimal catalog record if missing (requires admin binding access).
686
- - `ensureBinding(...)`: creates a binding if missing (enforces god-mode for cross-app provisioning).
687
-
688
- ## Native catalogs (seed/import/export + batch upsert)
689
-
690
- ### Import/Export JSON (helpers)
691
-
692
- ```ts
693
- const items = catalox.importCatalogItemsFromJson<Array<Record<string, unknown>>>(jsonString);
694
- const jsonOut = catalox.exportCatalogItemsToJson(items);
695
- ```
696
-
697
- ### Upsert one item (descriptor-driven identity)
698
-
699
- ```ts
700
- await catalox.upsertNativeCatalogItem({ appId: "myAppId" }, "signals", {
701
- categoryId: "core",
702
- code: "S1",
703
- title: "Example",
704
- indexed: { categoryId: "core", code: "S1" }
705
- });
706
- ```
707
-
708
- ### Native write contract (exact)
709
-
710
- - **Input shape**: the write API accepts a plain object payload. `indexed` is a reserved top-level field used only for query performance and is **not** part of the domain payload.
711
- - **Stored shape** (native): items are stored as `{ itemId, catalogId, indexed, data }`.
712
- - **Identity**: `itemId` is resolved from `descriptor.identity`.
713
- - `natural`: uses `identity.itemIdField` from the input payload.
714
- - `composite`: joins `identity.compositeFields` with `:`.
715
- - `generated`: **currently requires caller-supplied id** (Catalox will throw if descriptor uses `generated` and no id is provided).
716
-
717
- ### Batch upsert
718
-
719
- ```ts
720
- await catalox.batchUpsertNativeCatalogItems({ appId: "myAppId" }, "signals", items);
721
- ```
722
-
723
- ### List native items with equality filtering
724
-
725
- ```ts
726
- const res = await catalox.listCatalogItems({ appId: "myAppId" }, "signals", {
727
- filter: { categoryId: "core" }
728
- });
729
- // `res.listOutcome`: "ok" (list ran) or "mapping_blocked" (see `res.issues`). Empty `items` with "ok" means zero matches.
730
- ```
731
-
732
- Filtering is performed on `indexed.<field>` in stored native records (payload remains in `data`). **Blank filter values** (`""`, whitespace-only strings, `null`, `undefined`) are **not** sent as Firestore constraints so empty UI fields do not zero out lists; see [`docs/native-catalog-storage-and-api.md`](docs/native-catalog-storage-and-api.md) and **`compactCatalogFilter`** in the published API.
733
-
734
- ### Canonical `indexed` rule (native catalogs)
735
-
736
- - **Caller may provide `indexed`**: `indexed: { ... }` is accepted on writes.
737
- - **Catalox may derive `indexed`**: if omitted, Catalox derives `indexed` from the descriptor’s `queryableFields` (fields marked `indexed: true`, using `field.path ?? field.key`).
738
- - **Data cleanliness**: `indexed` is treated as reserved metadata and is **not duplicated inside `data`**.
739
- - **If `indexed` is missing/wrong**: equality filtering and indexed sorting may return incomplete results or fail due to missing Firestore indexes; the canonical fix is to correct the catalog descriptor and/or the write input and re-upsert.
740
-
741
- ## References
742
-
743
- ```ts
744
- const refs = await catalox.getCatalogItemReferences({ appId: "myAppId" }, "signals", "core:S1");
745
- ```
746
-
747
- ### Reference / relations write model
748
-
749
- Catalox supports **generic item-to-item relations** (graph links) via the persisted `catalogReferences/{referenceId}` collection.
750
-
751
- - **Storage record**: `CatalogItemReference` (`fromCatalogId/fromItemId -> toCatalogId/toItemId + relationType`)\n
752
- - **Descriptor rules**: `CatalogDescriptor.relationRules[]` defines which relation types are **allowed** and which are **required**.
753
- - **Render maps**: item render maps include `relations[]` (and keep `references[]` for backward compatibility).
754
-
755
- #### Create/update/delete relations
756
-
757
- ```ts
758
- // Upsert (idempotent; deterministic referenceId)
759
- await catalox.upsertCatalogItemRelation({ appId: "myAppId" }, {
760
- fromCatalogId: "signals",
761
- fromItemId: "core:S1",
762
- toCatalogId: "categories",
763
- toItemId: "core",
764
- relationType: "belongs_to",
765
- label: "Category",
766
- });
767
-
768
- // Delete by endpoints (or by referenceId)
769
- await catalox.deleteCatalogItemRelation({ appId: "myAppId" }, {
770
- fromCatalogId: "signals",
771
- fromItemId: "core:S1",
772
- toCatalogId: "categories",
773
- toItemId: "core",
774
- relationType: "belongs_to",
775
- });
776
- ```
777
-
778
- #### Provide relations at item creation/update time (native catalogs)
779
-
780
- For native item writes, you may include `relations: [...]` (or legacy `references: [...]`) in the write payload to satisfy required relation rules.
781
-
782
- ```ts
783
- await catalox.upsertNativeCatalogItem({ appId: "myAppId" }, "signals", {
784
- categoryId: "core",
785
- code: "S1",
786
- title: "Example",
787
- relations: [{ toCatalogId: "categories", toItemId: "core", relationType: "belongs_to" }],
788
- });
789
- ```
790
-
791
- ## Validation
792
-
793
- Validation APIs exist with standardized contracts. When a catalog descriptor defines `relationRules`, `validateCatalogItem(...)` returns issues for:\n
794
- - missing required relations\n
795
- - disallowed relation types\n
796
- - disallowed target catalogs / catalogTypes\n
797
- - cardinality violations (`multiple: false`)\n
798
- \n
799
- Native item upserts/updates will throw `CatalogValidationError` when `relationRules` are present and the provided relations would violate the rules.
800
-
801
- ## Publishing
802
-
803
- From a clean tree, **`npm publish`** runs **`prepublishOnly`**, which executes **`npm run build`** then **`npm test`** (unit tests). Ensure `dist/` is build output only; the published tarball includes **`dist`**, **`README.md`**, **`LICENSE`**, and **`firestore.indexes.json`** (composite indexes for `catalogItemHistory` queries) per `package.json` **`files`**.
804
-
805
- ## Tests
806
-
807
- Unit tests (default):
808
-
809
- ```bash
810
- npm test
811
- ```
812
-
813
- Integration tests (live Firestore, no mocks/emulators):
814
-
815
- ```bash
816
- npm run test:integration
817
- ```
818
-
819
- Integration tests are **live-only** (no mocks, no emulators). They require:
820
-
821
- - `FIRESTORE_LIVE_TESTS=1`
822
- - `FIREBASE_PROJECT_ID=...`
823
- - **`GOOGLE_SERVICE_ACCOUNT_BASE64`** set to base64-encoded service account JSON, **or** valid Application Default Credentials for the test project
824
-
825
- AI live integration tests (real LLM calls) are additionally gated behind:
826
-
827
- - `AIFUNCTIONS_LIVE_TESTS=1`
828
- - `OPENROUTER_API_KEY` or `OPEN_ROUTER_KEY`
829
-
830
- The **`Firestore live integration`** test (`test/integration/firestore.emulator.test.ts`) uses **`createCatalox`** and asserts v3 contracts: **`listCatalogItems`** returns **`listOutcome: "ok"`**, and **`getCatalogItem`** returns **`{ outcome: "found", item }`** for the seeded row. The descriptor patch in that test includes **`queryableFields` with `indexed: true`** for filtered fields so **`deriveIndexed`** persists **`indexed.*`** (see **Canonical `indexed` rule** above).
831
-
832
- When **`CATALOX_RECORD_HISTORY_BUCKET`** is set, **`test/integration/record-history.live.test.ts`** runs as well: ephemeral app/catalog, **`upsert` → `update` → `listCatalogItemHistory` → `getCatalogItemHistoryEvent` → `restoreCatalogItemFromHistory`**, then best-effort Firestore + GCS cleanup for the created history objects.
833
-
834
- ### Live test safety (read before running)
835
-
836
- - **Never run against production credentials/projects.** Use a dedicated Firebase project for tests.
837
- - **Touched collections**: `apps`, `catalogs`, `catalogBindings`, `catalogDefinitions`, `catalogDescriptors`, `catalogData/{catalogId}`, `catalogData-{catalogId}-items/...`, and (when record history is enabled) `catalogItemHistory/...`. The **`catalox firestore restore-backup`** / **`undo-restore-backup`** commands additionally write `backup-restoreSessions` and `{restoreSessionId}__preRestore-*` sidecar collections (see [`docs/restore-firestore-backup.md`](docs/restore-firestore-backup.md)).
838
- - **Cleanup**: tests do best-effort deletes of the docs they created, but do not guarantee full teardown.
839
-
840
- ## Changelog
841
-
842
- ### 4.0.0
843
-
844
- - **Breaking Firebase bootstrap:** **`resolveFirebaseAdminCredentialFromEnv`**, **`createCataloxFromEnv`**, and related helpers now resolve credentials in order: **`GOOGLE_SERVICE_ACCOUNT_BASE64`** (or **`serviceAccountBase64`** option), then optional **`serviceAccountPath`** (caller-supplied path only), then Application Default Credentials. GCS credential helpers in **`backup-data`** / **`record-history`** use the same base64-then-ADC pattern.
845
- - **New:** **`testFirestoreConnectionFromEnv`** — minimal Firestore read using the same rules as **`createCataloxFromEnv`**, on a disposable named Admin app (probe uses collection **`cataloxConnectivityProbe`**, not reserved `__…__` ids).
846
- - **CLI:** **`catalox firestore probe`** prints JSON from **`testFirestoreConnectionFromEnv`**.
847
- - **New:** **`applyCataloxSeedPreset`**, **`parseCataloxSeedManifest`**, **`loadCataloxSeedManifestFromPath`** (`@x12i/catalox`) — versioned JSON manifests for idempotent catalog + binding + descriptor + native item provisioning.
848
- - **CLI:** **`catalox seed apply --app --file …`** with optional **`--god`** for descriptor sections.
849
- - **New:** **`Catalox.upsertCatalogDescriptor`** / **`CataloxBound.upsertCatalogDescriptor`** (super-admin) for host-controlled descriptor writes.
850
- - **Docs:** [`docs/onboarding-happy-path.md`](docs/onboarding-happy-path.md), [`docs/native-map-catalog-preset.md`](docs/native-map-catalog-preset.md), example [`presets/native-map-v1.json`](presets/native-map-v1.json).
851
-
852
- ### 3.1.1
853
-
854
- - **Live tests:** Firestore integration test now sets **`queryableFields`** on the patched descriptor so equality filters match stored **`indexed.*`** rows (same rule as production descriptors).
855
- - **Live tests:** Record-history integration test covers **update**, **get event**, **restore**, and **GCS object** cleanup when a bucket is configured.
856
- - **Package:** publish **`firestore.indexes.json`** in the npm tarball for operators deploying **`catalogItemHistory`** queries.
857
-
858
- ### 3.1.0
859
-
860
- - **Per-record history:** optional **`recordHistory`** on **`createCatalox`** (or **`CATALOX_RECORD_HISTORY_*`** env for CLI) writes **GCS NDJSON** + Firestore **`catalogItemHistory`** on native upsert/update/delete/batch/move. APIs: **`listCatalogItemHistory`**, **`getCatalogItemHistoryEvent`**, **`restoreCatalogItemFromHistory`**, **`replayCatalogToPointInTime`**. CLI: **`catalox history …`**. See [`docs/record-history.md`](docs/record-history.md), [`firestore.indexes.json`](firestore.indexes.json).
861
- - **Catalog lifecycle:** **`deleteCatalog`**, **`restoreDeletedCatalog`**, **`renameCatalog`** (hard rename) + CLI **`catalox catalog …`**. See [`docs/catalog-crud.md`](docs/catalog-crud.md).
862
-
863
- ### 3.0.0
864
-
865
- - **`createCatalox(config)`** — single factory for Firestore-backed stores, authz, optional Mongo/API adapters, optional renderer snippet store (`src/catalox/create-catalox.ts`).
866
- - **`catalox.withContext(ctx)`** / **`bindCataloxContext(catalox, ctx)`** **`CataloxBound`**: same APIs without repeating `CataloxContext` on every call (`src/catalox/catalox-bound.ts`).
867
- - **Breaking — lists:** **`CatalogListResult`** includes **`listOutcome: "ok" | "mapping_blocked"`**. Mapping validation failures use **`mapping_blocked`** (see **`issues`**). Empty **`items`** with **`listOutcome === "ok"`** means zero matching rows.
868
- - **Breaking get item:** **`getCatalogItem`** returns **`CatalogGetItemResult`** (`found` | `not_found` | `mapping_blocked`), not **`null`**.
869
- - **Package:** **`main` / `types`** and **`exports["."]`** resolve to **`dist/src/...`**. Subpaths **`@x12i/catalox/embedder`**, **`/operator`**, **`/mapping`**, **`/firebase`**. Root **`@x12i/catalox`** re-exports embedder + operator (preserves most existing root imports).
870
-
871
- ### 2.7.0
872
-
873
- - **GCS restore:** **`restoreFirestoreBackupFromGcs`** + CLI **`firestore restore-backup-from-gcs`** same pre-restore / **`undoFirestoreRestore`** model as mirror restore.
874
- - **GCS retention:** **`pruneGcsBackupRuns`**, **`deleteCataloxGcsBackupRunObjects`**, CLI **`prune-gcs-backups`**, **`delete-gcs-backup-run`**.
875
- - **Manifest bridge:** **`cataloxGcsBackupManifestToFirestoreExportManifest`**, CLI **`gcs-backup-to-export-manifest`**, type **`CataloxGcsBackupManifestV1`**; exported **`restoreFirestoreNdjsonStreamToCollection`**.
876
- - **Reliability:** failed **`backupData` GCS** runs trigger **best-effort** deletion of the partial run folder.
877
- - **Contracts:** **`RestoreFirestoreMirrorSource`** vs **`RestoreFirestoreBackupSource`** (adds `gcs` for session manifests).
878
-
879
- ### 2.6.0
880
-
881
- - **GCS backup:** `backupData` supports **`mode: "gcs"`** writes metadata, native catalogs, optional snapshots as **NDJSON** under `gs://{bucket}/{prefix}{timestamp}/` via **`@x12i/helpers/gcs`**, with **`catalox-backup-manifest.json`** per run. Default prefix when omitted: **`catalox-firestore-backups/`**. CLI: **`firestore backup --mode gcs --bucket …`** (`--gcs-prefix` optional).
882
- - **Export:** `gcsBackupRunFolder` helper (for tests and path clarity).
883
- - **Docs / report:** [`docs/backup.md`](docs/backup.md) updated; gap analysis [`.reports/gcs-backup-gap-analysis.md`](.reports/gcs-backup-gap-analysis.md).
884
-
885
- ### 2.5.0
886
-
887
- - **GCS:** NDJSON export/import for one or many Firestore collections (`exportFirestoreCollectionToGcs`, `exportAllFirestoreCollectionsToGcs`, restore helpers, CLI `firestore export-gcs` / `import-gcs`). Optional **`gcsPrefix`** and **`objectNamePostfix`** on object keys; manifest-driven restore-all.
888
- - **GCS:** Compare live collections to bucket NDJSON — identical / changed / only-in-Firestore / only-in-bucket (`compareFirestoreCollectionWithGcsNdjson`, manifest mode, CLI `firestore compare-gcs`). Helpers **`normalizeForCompare`**, **`dataFingerprint`** exported for tooling.
889
- - **Native:** `listCatalogItems` compacts inert filter entries before Firestore queries; `NativeItemStore.list` can re-resolve flat vs legacy layout after an unconstrained empty first page.
890
- - **Docs:** [`docs/native-catalog-storage-and-api.md`](docs/native-catalog-storage-and-api.md), [`docs/firestore-gcs-export.md`](docs/firestore-gcs-export.md); README and environment docs updated for GCS.
891
- - **Dependency:** `@google-cloud/storage` (^7.19).
892
-
893
- ## Boundaries (important)
894
-
895
- - **Secrets**: do not store secret material (API keys, cloud creds) in Catalox. Store only non-secret refs like `credentialsRef`.
896
- - **Artifacts**: store descriptor metadata and remote keys; artifact blobs live in external object storage.
897
-
1
+ # `@x12i/catalox`
2
+
3
+ Catalox is platform infrastructure for catalogs: a reusable, interoperable way to manage catalog definitions once, expose them consistently to apps, and avoid rebuilding catalog plumbing in every product. It can power a catalogs-as-a-service layer without being a hosted service itself.
4
+
5
+ It is a **data-tier** package for managing **app-scoped catalogs** in Firebase **Firestore**, including:
6
+
7
+ - **Catalog discovery** for an `appId` (what catalogs are available + access)
8
+ - **App-agnostic catalog discovery** (list all catalogs, text search, AI match by name/description; respects descriptor visibility)
9
+ - **First-class catalog descriptors** (capabilities, query metadata, identity metadata, field metadata)
10
+ - **Descriptor creation/modification rules** (optional governance blocks for AI-assisted create/modify)
11
+ - **Native catalogs** (items stored in Firestore)
12
+ - **Mapped catalogs** (items normalized from MongoDB or APIs)
13
+ - **Catalog item search** (within a catalog: text search + AI match)
14
+ - **AI-assisted item create/modify** (calls `aifunctions-js` `createItem` / `modifyItem`; optional `autoPersist` for native catalogs)
15
+ - **References + validation contracts** (standardized cross-catalog shapes)
16
+ - **Seed/import/export + batch upsert** workflows for native catalogs
17
+ - **Optional per-record native history** — NDJSON payloads in GCS plus a Firestore `catalogItemHistory` index; list/show/restore/replay APIs and `catalox history …` ([`docs/record-history.md`](docs/record-history.md))
18
+ - **Optional catalog lifecycle** — hard delete (with manifest), restore from manifest, and hard rename across collections; APIs + `catalox catalog …` ([`docs/catalog-crud.md`](docs/catalog-crud.md))
19
+ - **Optional GCS tools** — NDJSON export/import of Firestore collections to a bucket, **compare** live data vs bucket snapshots, **`backupData` mode `gcs`** (Catalox-shaped NDJSON + manifest under a bucket prefix), and matching CLI commands (`firestore export-gcs`, `import-gcs`, `compare-gcs`, `firestore backup --mode gcs`; see [`docs/firestore-gcs-export.md`](docs/firestore-gcs-export.md), [`docs/backup.md`](docs/backup.md))
20
+ - **Operator `toolbox` CLI** — Diagnose app↔catalog access (Firestore `catalogBindings` + simulated `listCatalogItemsWithOutcome`) and create or repair binding docs; see [`docs/cli-toolbox.md`](docs/cli-toolbox.md)
21
+
22
+ Catalox **does not** own UI, workflow orchestration, remote execution, artifact blobs, or secret storage.
23
+
24
+ ## When to use Catalox (and when not to)
25
+
26
+ Use Catalox when you need **governed catalogs**: reusable item inventories that apps must **discover**, **render**, **search**, **reference**, and **validate**.
27
+
28
+ Examples that fit Catalox well:
29
+
30
+ - Product or plan definitions used by checkout/order flows
31
+ - Routing rules, policies, feature flags, lookup tables
32
+ - Graph templates, execution-matrix templates, prompt templates
33
+ - Mappings and “configuration-as-data” that many services read
34
+
35
+ Do **not** use Catalox as the transaction itself (OLTP / event/state engine):
36
+
37
+ - Orders, payments, live execution runs, audit/event streams
38
+ - Queues, locks, balances, high-write mutable operational state
39
+
40
+ A good rule: **Catalox stores the definition or selectable item**; your host system stores the **transaction/run/event/state transition** that uses that item.
41
+
42
+ Public format notes:
43
+
44
+ - See [`docs/catalog-format.md`](docs/catalog-format.md) for the public shapes: catalog meta, catalog lists, and catalog items.
45
+ - See [`docs/migration-catalog-model.md`](docs/migration-catalog-model.md) for the one-time backfill to the newer catalog model.
46
+
47
+ **Quick onboarding (env → Firestore probe → seed preset → validate):** [`docs/onboarding-happy-path.md`](docs/onboarding-happy-path.md). **App↔catalog access troubleshooting (CLI):** [`docs/cli-toolbox.md`](docs/cli-toolbox.md).
48
+
49
+ ## Scoped registries: catalog types, domains, agents
50
+
51
+ Catalox supports scoped “enum registries” for three common dimensions:
52
+
53
+ - **Catalog types**: allowed `catalogType` values per scope
54
+ - **Domains**: allowed domain ids per scope
55
+ - **Agents**: allowed agent ids per scope
56
+
57
+ Scopes are resolved in priority order: **store → app → global**.
58
+
59
+ Item association:
60
+
61
+ - Catalog items should carry `metadata.domainIds: string[]` and `metadata.agentIds: string[]` to associate the item (for example: a graph/template) to one or more domains/agents in your host runtime.
62
+ - Catalox stores and returns these arrays; their semantics are **host-defined**.
63
+
64
+ ## Identity & outcomes (vNext docs)
65
+
66
+ If you embed Catalox behind a BFF/UI and need clarity around **tenancy/identity axes** (store/app vs account/group/channel/user/visitor vs domain/agent) and **empty vs denied vs misconfigured** semantics, start here:
67
+
68
+ - [`docs/identity-model.md`](docs/identity-model.md)
69
+ - [`docs/authorization.md`](docs/authorization.md)
70
+ - [`docs/outcomes.md`](docs/outcomes.md)
71
+
72
+ ## Install
73
+
74
+ This repo is currently set up as a workspace package.
75
+
76
+ - **Node**: `>=20`
77
+ - **TypeScript**: builds to `dist/`
78
+
79
+ `@x12i/helpers` is a **required dependency** (installed as `@x12i/helpers@^1.4.0`; GCS backup uses `@x12i/helpers/gcs`).
80
+
81
+ **Direct dependency:** `@google-cloud/storage` is declared for GCS export/import/compare; it uses the same **Application Default Credentials** pattern as Firestore Admin (bucket IAM required).
82
+
83
+ ### Package exports (v4)
84
+
85
+ | Import path | Intended use |
86
+ |-------------|----------------|
87
+ | **`@x12i/catalox`** | Full surface: embedder API + operator tooling (markdown, diagrams, migrations, Firebase stores, backup/GCS helpers, etc.). |
88
+ | **`@x12i/catalox/embedder`** | Catalog runtime only: **`createCatalox`**, **`Catalox`**, **`withContext` / `CataloxBound`**, contracts, errors, adapters, **`validateMappingSpec` / `executeMapping`**. |
89
+ | **`@x12i/catalox/operator`** | Markdown/diagrams/JSX, validation, bindings, **`ContextResolver`**, backup/GCS transfer helpers, identity/json-io, etc. |
90
+ | **`@x12i/catalox/firebase`** | Firestore-backed store classes for advanced wiring. |
91
+ | **`@x12i/catalox/mapping`** | Full mapping module (including **`helper-gap-report`**). |
92
+
93
+ ## Configuration (real connections)
94
+
95
+ Catalox is a library: you provide initialized clients + runtime env. For a **full list of environment variables**, CLI vs library behavior, and integration-test requirements, see [`docs/environment.md`](docs/environment.md).
96
+
97
+ ### Firestore (Firebase Admin SDK)
98
+
99
+ This implementation of Catalox expects a **Firebase Admin SDK** Firestore instance (`firebase-admin`).
100
+
101
+ Important clarification:
102
+
103
+ - **“Admin” here means privileged access to your Firebase/GCP project’s Firestore**, using a service account.
104
+ - It is **not** “admin of the host machine / server OS”.
105
+ - Admin SDK access typically **bypasses Firestore Security Rules** (rules are for client SDKs).
106
+
107
+ Scoping:
108
+
109
+ - You can scope the service account to **Firestore-related IAM roles** and to a specific **project**.
110
+ - You generally cannot scope an Admin SDK credential to only certain collections/documents via IAM the way client rules work.
111
+
112
+ #### Firebase / Firestore credentials (`createCataloxFromEnv`, CLI, GCS helpers)
113
+
114
+ Bootstrap helpers in **`@x12i/catalox/firebase`** resolve credentials in this order (same order as JSDoc on **`resolveFirebaseAdminCredentialFromEnv`**):
115
+
116
+ 1. **`serviceAccountBase64`** option, else **`GOOGLE_SERVICE_ACCOUNT_BASE64`** in the environment base64-encoded standard Google **service account JSON** (the same JSON shape as a downloaded key file), used with **`cert(...)`**. This is the **recommended** way to supply credentials in CI and scripts so behavior does not depend on a developer machine’s Application Default Credentials.
117
+ 2. **`serviceAccountPath`** option only your code passes a filesystem path string; Catalox reads that file and uses **`cert(...)`**. No environment variable in this package supplies that path.
118
+ 3. **Application Default Credentials** (`applicationDefault()`) when neither (1) nor (2) is set (for example **`GOOGLE_APPLICATION_CREDENTIALS`**, workload identity, or GCE metadata).
119
+
120
+ Optional: **`FIREBASE_PROJECT_ID`** and **`FIRESTORE_DATABASE_ID`** are read from the environment (or options) when using the bootstrap helpers.
121
+
122
+ **v4:** credential bootstrap is base64-first as above; use **`GOOGLE_SERVICE_ACCOUNT_BASE64`** in `.env` or your secret store, or pass **`serviceAccountPath`** from your own code if you load a key file yourself.
123
+
124
+ This repo’s CLI + live tests typically load `.env` via **`@x12i/env`**. GCS-related code paths honor **`GOOGLE_SERVICE_ACCOUNT_BASE64`** when set, then fall back to ADC.
125
+
126
+ Minimal example (ADC):
127
+
128
+ ```ts
129
+ import { initializeApp, applicationDefault } from "firebase-admin/app";
130
+ import { getFirestore } from "firebase-admin/firestore";
131
+
132
+ initializeApp({ credential: applicationDefault() });
133
+ const firestore = getFirestore();
134
+ ```
135
+
136
+ Official helper (library bootstrap; no `dotenv`):
137
+
138
+ ```ts
139
+ import { createCataloxFromEnv } from "@x12i/catalox/firebase";
140
+
141
+ // Precedence: GOOGLE_SERVICE_ACCOUNT_BASE64 (or serviceAccountBase64 option), then serviceAccountPath option, else ADC.
142
+ // Optional: FIREBASE_PROJECT_ID, FIRESTORE_DATABASE_ID.
143
+ const { catalox } = createCataloxFromEnv();
144
+ ```
145
+
146
+ Optional connectivity check (disposable Admin app, same resolution rules as **`createCataloxFromEnv`**). The read uses a Firestore-valid probe collection (not a reserved `__…__` id). Exported from **`@x12i/catalox/firebase`** and **re-exported from `@x12i/catalox`** for a single import surface.
147
+
148
+ ```ts
149
+ import { testFirestoreConnectionFromEnv } from "@x12i/catalox/firebase";
150
+ // or: import { testFirestoreConnectionFromEnv } from "@x12i/catalox";
151
+
152
+ const probe = await testFirestoreConnectionFromEnv();
153
+ if (!probe.ok) throw probe.error;
154
+ ```
155
+
156
+ From the packaged CLI (after `npm i` / `npx`, with the same Firebase env vars as the library):
157
+
158
+ ```bash
159
+ catalox firestore probe
160
+ ```
161
+
162
+ Lifecycle-aware helper (recommended for long-lived hosts that may swap env/credentials at runtime):
163
+
164
+ ```ts
165
+ import { createCataloxHostFromEnv } from "@x12i/catalox/firebase";
166
+
167
+ // Tip: set `appName` so you can safely replace the same Admin app instance.
168
+ const host = await createCataloxHostFromEnv({
169
+ appName: "catalox-host",
170
+ replaceExistingApp: true,
171
+ });
172
+
173
+ // Use the current runtime:
174
+ await host.catalox.listAppCatalogs({ appId: "myAppId" }, { appId: "myAppId" });
175
+
176
+ // Later, if credentials/project/database identity changes:
177
+ await host.rebuild({
178
+ appName: "catalox-host",
179
+ replaceExistingApp: true,
180
+ env: {
181
+ GOOGLE_SERVICE_ACCOUNT_BASE64: "<base64-encoded service account JSON>",
182
+ FIREBASE_PROJECT_ID: "other-project",
183
+ FIRESTORE_DATABASE_ID: "(default)",
184
+ },
185
+ });
186
+
187
+ // Optional: on shutdown.
188
+ await host.dispose();
189
+ ```
190
+
191
+ Construction helper (avoid implicit “default app” binding; use your own Admin app):
192
+
193
+ ```ts
194
+ import { initializeApp, cert, type App } from "firebase-admin/app";
195
+ import { createCataloxFromFirebaseApp } from "@x12i/catalox/firebase";
196
+
197
+ const app: App = initializeApp({ credential: cert(serviceAccountJson) }, "my-admin-app");
198
+ const { catalox } = createCataloxFromFirebaseApp({ app, databaseId: "(default)" });
199
+ ```
200
+
201
+ ### Google Cloud Storage (optional export / import / compare)
202
+
203
+ When you use **`catalox firestore export-gcs`**, **`import-gcs`**, **`compare-gcs`**, **`firestore backup --mode gcs`**, or the matching **`Catalox`** methods, the runtime needs a GCS bucket and a credential that can **read/write objects** there (typically the same service account as Firestore, with **Storage** roles on that bucket). GCS **backup** uses **`@x12i/helpers/gcs`** (ADC); export/import/compare still use **`@google-cloud/storage`** directly. See [`docs/firestore-gcs-export.md`](docs/firestore-gcs-export.md), [`docs/backup.md`](docs/backup.md), and [`docs/environment.md`](docs/environment.md).
204
+
205
+ ### Mongo (mapped catalogs)
206
+
207
+ For Mongo-mapped catalogs, provide:
208
+
209
+ - `MONGO_URI` (or whatever you store in adapter config as `mongoUriEnvVar`)
210
+
211
+ Example `.env` (do not commit secrets):
212
+
213
+ ```bash
214
+ MONGO_URI=mongodb://127.0.0.1:27017
215
+ # Optional: ADC via GOOGLE_APPLICATION_CREDENTIALS, workload identity, etc.
216
+ # GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json
217
+ # Recommended for CLI + live tests: base64-encoded service account JSON (same shape as a key file)
218
+ # GOOGLE_SERVICE_ACCOUNT_BASE64=...
219
+ FIREBASE_PROJECT_ID=your-project-id
220
+ FIRESTORE_LIVE_TESTS=1
221
+ # Optional: record-history live test + CLI (`createCatalox`); use a dedicated test bucket:
222
+ # CATALOX_RECORD_HISTORY_BUCKET=your-test-bucket
223
+ # CATALOX_RECORD_HISTORY_PREFIX=catalox-record-history/
224
+ # Optional defaults for the packaged `catalox` CLI (Catalox app/store context, not GCP):
225
+ # CATALOX_APP_ID=myAppId
226
+ # CATALOX_STORE_ID=myStoreId
227
+ # CATALOX_USER_ID=user-123
228
+ # CATALOX_MONGO_URI=mongodb://127.0.0.1:27017
229
+ ```
230
+
231
+ ### `catalox` CLI environment
232
+
233
+ The `catalox` binary loads `.env` via `dotenv`. Use **`CATALOX_*`** for **Catalox runtime context** (catalog `appId`, optional `storeId`, actor, Mongo URI for the bundled adapter). Use **`FIREBASE_*` / `FIRESTORE_*`** plus **`GOOGLE_SERVICE_ACCOUNT_BASE64`** (when you want explicit service account JSON via env) for **Firebase Admin / Firestore connection** (GCP `projectId`, database id, live-test flags).
234
+
235
+ - **`CATALOX_APP_ID`** — Default `appId` in CLI context when a command does not pass `--app` (when present, `--app` overrides).
236
+ - **`CATALOX_STORE_ID`** — Default `storeId` for **`report`** and **`export`** when `--store` is omitted (`--store` overrides).
237
+ - **`CATALOX_USER_ID`** — Optional user id / actor for authz-sensitive CLI paths.
238
+ - **`CATALOX_MONGO_URI`** — If set, enables the Mongo catalog adapter in the CLI (wired via **`createCatalox`** in `src/catalox/create-catalox.ts`, called from `src/cli/index.ts`).
239
+ - **`CATALOX_RECORD_HISTORY_BUCKET`** — If set, **`createCatalox`** wires **`recordHistory`** (GCS payloads + **`catalogItemHistory`** on native writes; also required for **`catalox history …`** to persist new events).
240
+ - **`CATALOX_BUCKET`** — Default GCS bucket for Catalox storage features (used by the CLI and live tests; also used as a fallback for `CATALOX_RECORD_HISTORY_BUCKET` when wiring `recordHistory` via env).
241
+ - **`CATALOX_RECORD_HISTORY_PREFIX`** — Optional GCS prefix for record history (default `catalox-record-history/`).
242
+ - **`CATALOX_RECORD_HISTORY_FAIL_CLOSED`** When **`1`**, failed history writes fail the parent Firestore mutation.
243
+
244
+ **Running commands from this repository:** the shell command `catalox` is only on your `PATH` if the package is installed globally (`npm i -g @x12i/catalox`) or linked (`npm link` from this repo). Otherwise, after `npm run build`, use:
245
+
246
+ ```bash
247
+ npm run cli -- firestore report-native-layout --app "narrix"
248
+ # same as: node dist/src/cli/index.js firestore report-native-layout --app "narrix"
249
+ ```
250
+
251
+ #### `firestore` CLI quick reference
252
+
253
+ | Command | Purpose |
254
+ |---------|---------|
255
+ | `firestore backup` | Mirror metadata + native rows to Mongo, Firestore `backup-*`, or GCS NDJSON (`--mode gcs --bucket …`; [`docs/backup.md`](docs/backup.md)). |
256
+ | `firestore restore-backup` / `undo-restore-backup` | Restore from `backup-*` with undo sidecars ([`docs/restore-firestore-backup.md`](docs/restore-firestore-backup.md)). |
257
+ | `firestore restore-backup-from-gcs` | Restore live Firestore from one `backupData` GCS run (same undo as above). |
258
+ | `firestore prune-gcs-backups` / `delete-gcs-backup-run` | Retention and one-off run folder deletion in GCS. |
259
+ | `firestore gcs-backup-to-export-manifest` | Emit `import-gcs`–compatible manifest JSON from a GCS backup run. |
260
+ | `firestore report-native-layout` | Legacy vs flat native layout diagnostics. |
261
+ | `firestore migrate-native-catalog-data` | Legacy `catalogData/{id}/items` flat `catalogData-{id}-items` ([`docs/migration-native-catalog-data.md`](docs/migration-native-catalog-data.md)). |
262
+ | `firestore export-gcs` / `import-gcs` | NDJSON per collection + optional manifest ([`docs/firestore-gcs-export.md`](docs/firestore-gcs-export.md)). |
263
+ | `firestore compare-gcs` | Diff live Firestore vs bucket NDJSON (single collection or manifest). |
264
+
265
+ #### `history` / `catalog` CLI (3.1+)
266
+
267
+ | Command | Purpose |
268
+ |---------|---------|
269
+ | `history list` / `history show` / `history restore` / `history replay` | Per-record native history from **`catalogItemHistory`** + GCS ([`docs/record-history.md`](docs/record-history.md)). |
270
+ | `catalog delete` / `catalog restore` / `catalog rename` | Hard delete with manifest, restore from manifest, or hard rename ([`docs/catalog-crud.md`](docs/catalog-crud.md)). |
271
+
272
+ #### `toolbox` CLI (binding / access diagnostics)
273
+
274
+ | Command | Purpose |
275
+ |---------|---------|
276
+ | `toolbox check-access` | Read `catalogBindings/{appId}:{catalogId}`, `catalogs/{catalogId}`, and run **`listCatalogItemsWithOutcome`** (real auth rules). Optional **`--show-all-bindings`**. |
277
+ | `toolbox ensure-binding` | Create binding doc if missing (**`ensureBinding`**; no-op if row already exists). |
278
+ | `toolbox repair-binding` | Merge-write binding to **`active`** + access flags (**`--god`** required). |
279
+
280
+ Full workflow, field meanings, exit codes, and how this differs from **`report` / `export --god`**: [`docs/cli-toolbox.md`](docs/cli-toolbox.md).
281
+
282
+ ## Firestore data model (logical collections)
283
+
284
+ For a **full layout** (subcollections, document id conventions, snapshot runs, and query notes), see [`docs/firestore-data-model.md`](docs/firestore-data-model.md). For **native** catalogs specifically (per-catalog `catalogData-*-items`, layout resolution, `listCatalogItems`, filters, troubleshooting), see [`docs/native-catalog-storage-and-api.md`](docs/native-catalog-storage-and-api.md).
285
+
286
+ Metadata:
287
+
288
+ - `apps/{appId}`
289
+ - `catalogs/{catalogId}`
290
+ - `catalogBindings/{bindingId}` (many-to-many app↔catalog)
291
+ - `storeAppBindings/{bindingId}` (store↔app, multi-app export/report)
292
+ - `catalogDefinitions/{catalogId}` (native vs mapped specifics)
293
+ - `catalogAdapters/{adapterId}` (mongo/api adapter definitions)
294
+ - `catalogMappings/{mappingId}` (field mapping specs)
295
+ - `catalogDescriptors/{catalogId}` (**descriptor metadata** for generic consumption)
296
+ - `catalogRendererSnippets/{catalogId}:{role}[:{mode}]` (**stored renderer snippets** for list/grid/item/report/dashboard rendering)
297
+ - `catalogReferences/{referenceId}` (standardized reference records)
298
+ - `catalogItemHistory/{eventId}` (optional **native write history** index; payloads in GCS — [`docs/record-history.md`](docs/record-history.md))
299
+
300
+ Data:
301
+
302
+ - `catalogData-{catalogId}-items/{itemId}` (native item rows; top-level collection per catalog)
303
+ - `catalogData/{catalogId}` (index and metadata for that catalog’s native storage; not item payloads)
304
+ - `catalogSnapshots/{catalogId}/items/{itemId}` (mapped snapshot mode)
305
+
306
+ Backups (optional operator feature):
307
+
308
+ - `backup-*` and `{timestamp}__backup-*` (Firebase mode, same database)
309
+ - Mongo database `catalox-backups` (Mongo mode)
310
+ - **`gs://` bucket prefixes** `backupData` **`mode: "gcs"`** (NDJSON + `catalox-backup-manifest.json` per run; default prefix `catalox-firestore-backups/`)
311
+
312
+ See [`docs/backup.md`](docs/backup.md) for `backupData` / CLI backup (including **`mode: "gcs"`**), [`docs/restore-firestore-backup.md`](docs/restore-firestore-backup.md) for restoring Firebase mirrors to live data with undo, [`docs/migration-native-catalog-data.md`](docs/migration-native-catalog-data.md) for moving legacy native rows into `catalogData-{catalogId}-items`, and [`docs/firestore-native-layout-vs-backup.md`](docs/firestore-native-layout-vs-backup.md) for how live paths differ from `backup-*` mirrors and what `backupData` reports in `nativeItemSourceLayoutByCatalogId`. Gap analysis for GCS backup vs restore: [`.reports/gcs-backup-gap-analysis.md`](.reports/gcs-backup-gap-analysis.md). For **how native storage, layout resolution, `listCatalogItems`, and filters work together**, read [`docs/native-catalog-storage-and-api.md`](docs/native-catalog-storage-and-api.md). For **NDJSON export/import of Firestore collections to a GCS bucket** (single collection, all roots, optional recursive subcollections, manifest restore) and **compare live data vs bucket NDJSON** (`firestore compare-gcs`), see [`docs/firestore-gcs-export.md`](docs/firestore-gcs-export.md) and CLI `firestore export-gcs` / `firestore import-gcs` / `firestore compare-gcs`. If the console still shows **`catalogData/{catalogId}/items`**, run **`catalox firestore report-native-layout`** then **`catalox firestore migrate-native-catalog-data`** (backup + copy to flat; optional `--delete-legacy` after you trust backups).
313
+
314
+ ## Core usage (app-first, generic from `appId`)
315
+
316
+ ### Create `Catalox` (recommended)
317
+
318
+ Use **`createCatalox`** so you do not wire every Firestore store by hand. Optional **`mongoUri`** enables Mongo mapped catalogs; API mapped catalogs use an internal adapter unless you set `enableApiAdapter: false`.
319
+
320
+ ```ts
321
+ import { createCatalox } from "@x12i/catalox";
322
+ import { getFirestore } from "firebase-admin/firestore";
323
+
324
+ const firestore = getFirestore();
325
+ const catalox = createCatalox({
326
+ firestore,
327
+ // Optional: per-record native history to GCS + `catalogItemHistory` (or set CATALOX_RECORD_HISTORY_BUCKET for CLI)
328
+ // recordHistory: { gcsBucket: "my-bucket", gcsPrefix: "catalox-record-history/" },
329
+ });
330
+
331
+ // Optional: bind context once (no globals; same semantics as passing context each call).
332
+ // App-first (classic):
333
+ const scoped = catalox.withContext({ appId: "myApp" });
334
+ const list = await scoped.listCatalogItems("myCatalog", { limit: 50 });
335
+ // list.listOutcome is "ok" | "mapping_blocked"; empty items with "ok" means zero rows.
336
+ ```
337
+
338
+ ### CatalogId-first usage (no `appId` required)
339
+
340
+ If you already have a `catalogId` (deep link/bookmark/operator tool), you can omit `appId` **for catalog-specific operations**.
341
+ Catalox will infer an `appId` from active `catalogBindings` when there is exactly one viable binding (or allow omission when `superAdmin: true`).
342
+
343
+ ```ts
344
+ const ctx = {}; // tenant/agent coords optional
345
+ const items = await catalox.listCatalogItems(ctx, "signals", { limit: 50 });
346
+ const descriptor = await catalox.getCatalogDescriptor(ctx, "signals");
347
+ ```
348
+
349
+ App-first discovery/bootstrap APIs (e.g. `listAppCatalogs`, `getAppCatalogBootstrap`) still require an explicit `appId`.
350
+
351
+ ### Advanced: manual `CataloxDependencies`
352
+
353
+ If you need a non-default dependency graph, import stores from `@x12i/catalox/firebase`, construct `AuthorizationService`, and pass **`new Catalox(deps)`** as before (see source under `src/catalox/catalox.ts`).
354
+
355
+ ## Descriptor contract (planning-critical)
356
+
357
+ Catalox is designed so upstream packages can be “generic” (no hardcoded catalog registrations). The stable contract is the persisted **descriptor**:
358
+
359
+ - Stored at `catalogDescriptors/{catalogId}`
360
+ - Retrieved via `getCatalogDescriptor(...)` or `getAppCatalogBootstrap(...)`
361
+ - Used for identity, query, and client rendering metadata
362
+
363
+ Below is the **actual current descriptor shape** (TypeScript), with the most planning-relevant subtypes.
364
+
365
+ ```ts
366
+ export type CatalogCapabilitiesDescriptor = {
367
+ canList: boolean;
368
+ canGet: boolean;
369
+ canCreate: boolean;
370
+ canEdit: boolean;
371
+ canDelete: boolean;
372
+ canImport?: boolean;
373
+ canExport?: boolean;
374
+ canSync?: boolean;
375
+ canValidate?: boolean;
376
+ canViewReferences?: boolean;
377
+ };
378
+
379
+ export type CatalogFieldDescriptor = {
380
+ key: string;
381
+ label: string;
382
+ type:
383
+ | "string"
384
+ | "number"
385
+ | "boolean"
386
+ | "date"
387
+ | "datetime"
388
+ | "enum"
389
+ | "object"
390
+ | "array"
391
+ | "reference";
392
+ // Optional path into the stored item payload. If omitted, `key` is assumed.
393
+ path?: string;
394
+
395
+ // Query + indexing metadata.
396
+ filterable?: boolean;
397
+ sortable?: boolean;
398
+ indexed?: boolean;
399
+ multiValue?: boolean;
400
+
401
+ // Presentation + contract metadata.
402
+ listVisible?: boolean;
403
+ detailVisible?: boolean;
404
+ required?: boolean;
405
+ enumValues?: Array<string | number>;
406
+ reference?: { targetCatalogId?: string; targetField?: string };
407
+ metadata?: Record<string, unknown>;
408
+ };
409
+
410
+ export type CatalogIdentityDescriptor = {
411
+ itemIdStrategy: "natural" | "composite" | "generated";
412
+ itemIdField?: string;
413
+ compositeFields?: string[];
414
+
415
+ // Optional display decoration fields.
416
+ titleField?: string;
417
+ subtitleField?: string;
418
+ statusField?: string;
419
+ updatedAtField?: string;
420
+ };
421
+
422
+ export type CatalogDescriptor = {
423
+ catalogId: string;
424
+ label: string;
425
+ description?: string;
426
+ itemLabel?: string;
427
+ sourceMode: "native" | "mapped";
428
+ mappedSourceType?: "mongo" | "api";
429
+ status: "active" | "disabled" | "draft";
430
+ visibility?: "visible" | "hidden";
431
+ defaultSort?: { field: string; direction: "asc" | "desc" };
432
+ defaultFilters?: Record<string, unknown>;
433
+ capabilities: CatalogCapabilitiesDescriptor;
434
+ queryableFields: CatalogFieldDescriptor[];
435
+ queryCapabilities?: Record<string, unknown>;
436
+ filterSpec?: Record<string, unknown>;
437
+ presentationSpec?: Record<string, unknown>;
438
+ /**
439
+ * Optional UI escape hatch: registry components and/or stored snippet refs per role.
440
+ * See `CatalogCustomRenderer` in the published types (`renderers[]` with `role` + optional `mode`).
441
+ */
442
+ customRenderer?: Record<string, unknown>;
443
+ identity: CatalogIdentityDescriptor;
444
+ // Optional governance for AI-assisted create/modify:
445
+ creationRules?: Record<string, unknown>;
446
+ modificationRules?: Record<string, unknown>;
447
+ metadata?: Record<string, unknown>;
448
+ };
449
+ ```
450
+
451
+ ### UI metadata & custom renderers (optional)
452
+
453
+ **Overview (all custom UI surfaces in one place):** [`docs/catalog-custom-ui.md`](docs/catalog-custom-ui.md)
454
+
455
+ Catalog descriptors can include optional UI metadata for generic presentation layers:
456
+
457
+ - `filterSpec`: declarative filter configuration (built on `FieldSource`)
458
+ - `presentationSpec`: declarative layout + view/edit semantics (grid/list/cards/form)
459
+ - `catalogType` (optional): a host-defined tag that allows applying shared presentation and design defaults across many catalogs
460
+ - `presentationSpec.templates` (optional): host-defined template assignments per role/mode (no executable code)
461
+ - `presentationSpec.designRefs` (optional): references to scoped **design objects** (opaque JSON) for tokens/branding/content/assets
462
+ - `customRenderer`: escape hatch with a **`renderers[]`** list. Each entry has a **`role`** (`core|list|grid|item|report|dashboard`) and optional **`mode`** (`readonly|editable`, only for `list|grid|item`). An entry may set either:
463
+ - a host-resolved component (**`component`**: registry key), or
464
+ - a separately-stored renderer snippet (**`snippetRef`**), plus optional **`syntax`** (`html|jsx|tsx`), **`props`**, and **`receiveMap`** (`full|minimal`).
465
+
466
+ Use **`role: "core"`** for defaults shared across roles; merge with **`resolveCustomRendererEntry(...)`** (exported from `@x12i/catalox`).
467
+
468
+ Custom renderer contracts:
469
+
470
+ - [`docs/catalog-list-render-map.md`](docs/catalog-list-render-map.md)
471
+ - [`docs/catalog-item-render-map.md`](docs/catalog-item-render-map.md)
472
+ - [`docs/custom-renderer-snippets-io.md`](docs/custom-renderer-snippets-io.md) — **snippet default export, `map` I/O, `actions`, roles, and `receiveMap`**
473
+ - [`docs/design-objects.md`](docs/design-objects.md) — scoped opaque JSON design payloads
474
+
475
+ #### Presentation bindings + design objects (recommended integration)
476
+
477
+ Catalox does **not** render UI. Instead it can return a **resolved binding** that tells your presentation layer *how* to render a catalog surface, plus optional merged design JSON.
478
+
479
+ **Two sides (important):**
480
+
481
+ - **Orchestration / BFF side (server)**: builds `map` and resolves `binding`.\n Uses `buildCatalogListPresentationSurface(...)` / `buildCatalogItemPresentationSurface(...)` to return `{ map, binding }`.
482
+ - **Presentation layer side (UI)**: reads `binding.strategy` and chooses:\n - `custom-renderer`: mount a registry component or compile a stored snippet\n - `template`: render a host-owned template by `templateId`\n - `native-auto`: use `map.presentation` / descriptor hints for generic UI\n In all cases it may also apply `binding.design.merged` (opaque tokens/content) without Catalox interpreting keys.
483
+
484
+ Minimal server-side example:
485
+
486
+ ```ts
487
+ import { createCatalox } from "@x12i/catalox";
488
+ import { getFirestore } from "firebase-admin/firestore";
489
+
490
+ const catalox = createCatalox({
491
+ firestore: getFirestore(),
492
+ includeRendererSnippets: true, // optional (only needed for snippet-backed custom renderers)
493
+ includePresentationProfiles: true, // optional (type/catalog-scoped defaults)
494
+ includeDesignObjects: true, // optional (design objects API + merge)
495
+ });
496
+
497
+ const ctx = { appId: "myAppId" };
498
+
499
+ // List surface: returns render-map + resolved binding (no rendering).
500
+ const { map, binding } = await catalox.buildCatalogListPresentationSurface(ctx, "orders", {
501
+ displayContext: "grid",
502
+ includeSnippet: true,
503
+ includeDesignObjects: true,
504
+ designScope: { appId: "myAppId", storeId: "myStoreId" },
505
+ });
506
+
507
+ // Send `{ map, binding }` to your UI (or render on the server with your own engine).
508
+ ```
509
+
510
+ Minimal UI-side decision:
511
+
512
+ ```ts
513
+ switch (binding.strategy) {
514
+ case "custom-renderer":
515
+ // mount binding.customRenderer.entry.component OR compile binding.customRenderer.snippet
516
+ break;
517
+ case "template":
518
+ // render host template registry[binding.template.templateId](...)
519
+ break;
520
+ case "native-auto":
521
+ // generic UI using map.presentation + map.items + map.actions
522
+ break;
523
+ }
524
+
525
+ // Apply design tokens/content if present (opaque JSON).
526
+ const design = binding.design?.merged;
527
+ ```
528
+
529
+ Design objects can also be fetched directly when `includeDesignObjects` is enabled:
530
+
531
+ ```ts
532
+ const design = await catalox.getDesignObject(ctx, "brand:default");
533
+ const designs = await catalox.listDesignObjects(ctx, { appId: "myAppId", storeId: "myStoreId" });
534
+ ```
535
+
536
+ Important: host presentation layers are responsible for resolving `FieldSource` lookups (cross-catalog enums/refs) and populating `resolvedSources` for renderers.
537
+
538
+ ### Search items inside a catalog (text / AI)
539
+
540
+ Text search is local filtering on top of a normal `listCatalogItems` call; AI matching uses `aifunctions-js` `match()` over the candidate pool.
541
+
542
+ ```ts
543
+ const ctx = { appId: "myAppId" };
544
+
545
+ const text = await catalox.searchCatalogItems(ctx, "signals", { text: "error", textFields: ["title", "details.message"] });
546
+ const ai = await catalox.findCatalogItemsByAi(ctx, "signals", { query: "the onboarding flow item", maxResults: 3 });
547
+ ```
548
+
549
+ ### AI-assisted item create / modify (native catalogs)
550
+
551
+ These methods call `aifunctions-js` and return a structured envelope (`issues`, `reason`, etc.). When `autoPersist: true`, they persist via `upsertNativeCatalogItem` / `updateNativeCatalogItem` **only for native catalogs**.
552
+
553
+ ```ts
554
+ const ctx = { appId: "myAppId" };
555
+
556
+ const created = await catalox.createCatalogItemByAi(ctx, "signals", {
557
+ provided: { title: "New signal", categoryId: "core" },
558
+ autoPersist: false,
559
+ });
560
+
561
+ const updated = await catalox.modifyCatalogItemByAi(ctx, "signals", "core:S1", {
562
+ patch: { title: "Renamed" },
563
+ autoPersist: false,
564
+ });
565
+ ```
566
+
567
+ #### Stored renderer snippets (optional)
568
+
569
+ Descriptors reference snippets via **`customRenderer.renderers[].snippetRef`**.
570
+
571
+ Snippet documents (Firestore, default layout) live at:
572
+
573
+ - `catalogRendererSnippets/{catalogId}:{role}[:{mode}]` where `role` is `core|list|grid|item|report|dashboard` and `mode` is optional (`readonly|editable`)
574
+
575
+ Snippet record fields include **`rendererSource`** + optional **`syntax`** (`html|jsx|tsx`). Fetch via **`catalox.getCatalogRendererSnippet(ctx, catalogId, role, mode?)`** when `rendererSnippets` is wired on `Catalox`.
576
+
577
+ This package includes a small set of opt-in helpers for snippet compilation/validation:
578
+
579
+ - `transpileRendererSourceToModuleSource(...)`: TSX/JSX → ESM module source (string)
580
+ - `typecheckRendererSnippetIo(...)` (optional): best-effort TypeScript validation that a snippet’s default export matches the role’s render-map I/O contract
581
+ - `unsafeCreateRendererFunction(...)`: **UNSAFE** runtime evaluation of transpiled code
582
+ - `renderRendererToHtml(...)`: best-effort HTML rendering if `react` + `react-dom` are installed
583
+
584
+ **Persisted renderer metadata** must already match the role/`renderers[]` model. There is no built-in migration CLI; see [`docs/custom-renderer-canonical-contract.md`](docs/custom-renderer-canonical-contract.md). The package still exports **`buildRendererSnippetDocId`** for consistent snippet document ids.
585
+
586
+ **Worked example (catalog viewer + Catalox `map`)**: [`docs/new-feature/USAGE.md`](docs/new-feature/USAGE.md)
587
+
588
+ ### What you need persisted for “generic consumption”
589
+
590
+ To let consumers operate *purely* from `appId` with no hardcoded catalog registrations, Catalox relies on these being present in Firestore:
591
+
592
+ - **Bindings**: `catalogBindings` determine which catalogs an app can see/use.
593
+ - **Descriptors**: `catalogDescriptors/{catalogId}` provide capabilities, query metadata, identity metadata, and field metadata.
594
+
595
+ Minimum viable setup for generic consumption is **catalog + descriptor + binding**.
596
+
597
+ For **mapped** catalogs, the minimum viable setup is also **definition + mapping + adapter** (created automatically if you use `createCatalog` with `sourceMode: "mapped"`).
598
+
599
+ ### Discover catalogs for an app
600
+
601
+ ```ts
602
+ const context = { appId: "myAppId" };
603
+ const catalogs = await catalox.listAppCatalogs(context, { appId: "myAppId" });
604
+ ```
605
+
606
+ ### Discover catalogs across all apps (app-agnostic)
607
+
608
+ These APIs return the **catalog lists themselves** (metadata), not items. They merge `catalogs/{catalogId}` with `catalogDescriptors/{catalogId}` and apply visibility filtering (hidden catalogs are excluded unless `context.superAdmin` or `includeHidden: true`).
609
+
610
+ ```ts
611
+ const ctx = { appId: "myAppId", superAdmin: true };
612
+
613
+ const all = await catalox.listAllCatalogs(ctx, { includeDisabled: false });
614
+ const hits = await catalox.findCatalogs(ctx, { text: "users", fields: ["label", "description"] });
615
+ const ai = await catalox.findCatalogsByAi(ctx, { query: "customer lists", maxResults: 5 });
616
+ ```
617
+
618
+ ### Get a catalog descriptor
619
+
620
+ ```ts
621
+ const descriptor = await catalox.getCatalogDescriptor(context, "signals");
622
+ ```
623
+
624
+ ### Bootstrap (all descriptors accessible to an app)
625
+
626
+ ```ts
627
+ const bootstrap = await catalox.getAppCatalogBootstrap(context, "myAppId");
628
+ ```
629
+
630
+ ### Common “generic client” flow (no hardcoded catalog logic)
631
+
632
+ 1) `listAppCatalogs(appId)` → get catalog list + access
633
+ 2) `getAppCatalogBootstrap(appId)` get descriptors (capabilities/query/identity/fields)
634
+ 3) `listCatalogItems(catalogId, filter/sort)` → `{ listOutcome, items, issues? }` (`listOutcome` distinguishes OK empty lists vs mapping validation blocking)
635
+ 4) `getCatalogItem(catalogId, itemId)` → `{ outcome: "found" | "not_found" | "mapping_blocked", ... }`
636
+ 5) Optional: `getCatalogItemReferences(...)`, `validateCatalogItem(...)`
637
+
638
+ ## Provisioning (create catalog + bind + descriptor)
639
+
640
+ Catalox includes provisioning helpers to reduce “manual Firestore wiring”.
641
+
642
+ ### Create a native catalog (also seeds a minimal descriptor)
643
+
644
+ ```ts
645
+ const ctx = { appId: "myAppId", superAdmin: true }; // set only after host auth; often from AppRecord.superAdminApp
646
+
647
+ await catalox.createCatalog(ctx, {
648
+ catalogId: "signals",
649
+ name: "Signals",
650
+ sourceMode: "native",
651
+ native: { type: "native" },
652
+ });
653
+ ```
654
+
655
+ ### Bind a catalog to an app (enables discovery + access)
656
+
657
+ ```ts
658
+ await catalox.bindCatalogToApp(ctx, {
659
+ appId: "myAppId",
660
+ catalogId: "signals",
661
+ access: { canRead: true, canWrite: true, canAdmin: true },
662
+ });
663
+ ```
664
+
665
+ ### Patch/upsert a descriptor (recommended for identity + query metadata)
666
+
667
+ There is currently no `Catalox.setCatalogDescriptor(...)` method; consumers can upsert the persisted record via `DescriptorStore`.
668
+
669
+ ```ts
670
+ await descriptors.upsert({
671
+ catalogId: "signals",
672
+ descriptorVersion: "2",
673
+ descriptor: {
674
+ catalogId: "signals",
675
+ label: "Signals",
676
+ sourceMode: "native",
677
+ status: "active",
678
+ capabilities: { canList: true, canGet: true, canCreate: true, canEdit: true, canDelete: true },
679
+ queryableFields: [
680
+ { key: "categoryId", label: "Category", type: "string", indexed: true, filterable: true },
681
+ { key: "code", label: "Code", type: "string", indexed: true, filterable: true },
682
+ { key: "title", label: "Title", type: "string" },
683
+ ],
684
+ identity: {
685
+ itemIdStrategy: "composite",
686
+ compositeFields: ["categoryId", "code"],
687
+ titleField: "title",
688
+ },
689
+ },
690
+ createdAt: new Date().toISOString(),
691
+ updatedAt: new Date().toISOString(),
692
+ });
693
+ ```
694
+
695
+ ### Ensure helpers (idempotent)
696
+
697
+ - `ensureCatalog(...)`: creates a minimal catalog record if missing (requires admin binding access).
698
+ - `ensureBinding(...)`: creates a binding if missing (enforces god-mode for cross-app provisioning).
699
+
700
+ ## Native catalogs (seed/import/export + batch upsert)
701
+
702
+ ### Import/Export JSON (helpers)
703
+
704
+ ```ts
705
+ const items = catalox.importCatalogItemsFromJson<Array<Record<string, unknown>>>(jsonString);
706
+ const jsonOut = catalox.exportCatalogItemsToJson(items);
707
+ ```
708
+
709
+ ### Upsert one item (descriptor-driven identity)
710
+
711
+ ```ts
712
+ await catalox.upsertNativeCatalogItem({ appId: "myAppId" }, "signals", {
713
+ categoryId: "core",
714
+ code: "S1",
715
+ title: "Example",
716
+ indexed: { categoryId: "core", code: "S1" }
717
+ });
718
+ ```
719
+
720
+ ### Native write contract (exact)
721
+
722
+ - **Input shape**: the write API accepts a plain object payload. `indexed` is a reserved top-level field used only for query performance and is **not** part of the domain payload.
723
+ - **Stored shape** (native): items are stored as `{ itemId, catalogId, indexed, data }`.
724
+ - **Identity**: `itemId` is resolved from `descriptor.identity`.
725
+ - `natural`: uses `identity.itemIdField` from the input payload.
726
+ - `composite`: joins `identity.compositeFields` with `:`.
727
+ - `generated`: **currently requires caller-supplied id** (Catalox will throw if descriptor uses `generated` and no id is provided).
728
+
729
+ ### Batch upsert
730
+
731
+ ```ts
732
+ await catalox.batchUpsertNativeCatalogItems({ appId: "myAppId" }, "signals", items);
733
+ ```
734
+
735
+ ### List native items with equality filtering
736
+
737
+ ```ts
738
+ const res = await catalox.listCatalogItems({ appId: "myAppId" }, "signals", {
739
+ filter: { categoryId: "core" }
740
+ });
741
+ // `res.listOutcome`: "ok" (list ran) or "mapping_blocked" (see `res.issues`). Empty `items` with "ok" means zero matches.
742
+ ```
743
+
744
+ Filtering is performed on `indexed.<field>` in stored native records (payload remains in `data`). **Blank filter values** (`""`, whitespace-only strings, `null`, `undefined`) are **not** sent as Firestore constraints so empty UI fields do not zero out lists; see [`docs/native-catalog-storage-and-api.md`](docs/native-catalog-storage-and-api.md) and **`compactCatalogFilter`** in the published API.
745
+
746
+ ### Canonical `indexed` rule (native catalogs)
747
+
748
+ - **Caller may provide `indexed`**: `indexed: { ... }` is accepted on writes.
749
+ - **Catalox may derive `indexed`**: if omitted, Catalox derives `indexed` from the descriptor’s `queryableFields` (fields marked `indexed: true`, using `field.path ?? field.key`).
750
+ - **Data cleanliness**: `indexed` is treated as reserved metadata and is **not duplicated inside `data`**.
751
+ - **If `indexed` is missing/wrong**: equality filtering and indexed sorting may return incomplete results or fail due to missing Firestore indexes; the canonical fix is to correct the catalog descriptor and/or the write input and re-upsert.
752
+
753
+ ## References
754
+
755
+ ```ts
756
+ const refs = await catalox.getCatalogItemReferences({ appId: "myAppId" }, "signals", "core:S1");
757
+ ```
758
+
759
+ ### Reference / relations write model
760
+
761
+ Catalox supports **generic item-to-item relations** (graph links) via the persisted `catalogReferences/{referenceId}` collection.
762
+
763
+ - **Storage record**: `CatalogItemReference` (`fromCatalogId/fromItemId -> toCatalogId/toItemId + relationType`)\n
764
+ - **Descriptor rules**: `CatalogDescriptor.relationRules[]` defines which relation types are **allowed** and which are **required**.
765
+ - **Render maps**: item render maps include `relations[]` (and keep `references[]` for backward compatibility).
766
+
767
+ #### Create/update/delete relations
768
+
769
+ ```ts
770
+ // Upsert (idempotent; deterministic referenceId)
771
+ await catalox.upsertCatalogItemRelation({ appId: "myAppId" }, {
772
+ fromCatalogId: "signals",
773
+ fromItemId: "core:S1",
774
+ toCatalogId: "categories",
775
+ toItemId: "core",
776
+ relationType: "belongs_to",
777
+ label: "Category",
778
+ });
779
+
780
+ // Delete by endpoints (or by referenceId)
781
+ await catalox.deleteCatalogItemRelation({ appId: "myAppId" }, {
782
+ fromCatalogId: "signals",
783
+ fromItemId: "core:S1",
784
+ toCatalogId: "categories",
785
+ toItemId: "core",
786
+ relationType: "belongs_to",
787
+ });
788
+ ```
789
+
790
+ #### Provide relations at item creation/update time (native catalogs)
791
+
792
+ For native item writes, you may include `relations: [...]` (or legacy `references: [...]`) in the write payload to satisfy required relation rules.
793
+
794
+ ```ts
795
+ await catalox.upsertNativeCatalogItem({ appId: "myAppId" }, "signals", {
796
+ categoryId: "core",
797
+ code: "S1",
798
+ title: "Example",
799
+ relations: [{ toCatalogId: "categories", toItemId: "core", relationType: "belongs_to" }],
800
+ });
801
+ ```
802
+
803
+ ## Validation
804
+
805
+ Validation APIs exist with standardized contracts. When a catalog descriptor defines `relationRules`, `validateCatalogItem(...)` returns issues for:\n
806
+ - missing required relations\n
807
+ - disallowed relation types\n
808
+ - disallowed target catalogs / catalogTypes\n
809
+ - cardinality violations (`multiple: false`)\n
810
+ \n
811
+ Native item upserts/updates will throw `CatalogValidationError` when `relationRules` are present and the provided relations would violate the rules.
812
+
813
+ ## Publishing
814
+
815
+ From a clean tree, **`npm publish`** runs **`prepublishOnly`**, which executes **`npm run build`** then **`npm test`** (unit tests). Ensure `dist/` is build output only; the published tarball includes **`dist`**, **`README.md`**, **`LICENSE`**, and **`firestore.indexes.json`** (composite indexes for `catalogItemHistory` queries) per `package.json` **`files`**.
816
+
817
+ ## Tests
818
+
819
+ Unit tests (default):
820
+
821
+ ```bash
822
+ npm test
823
+ ```
824
+
825
+ Integration tests (live Firestore, no mocks/emulators):
826
+
827
+ ```bash
828
+ npm run test:integration
829
+ ```
830
+
831
+ Integration tests are **live-only** (no mocks, no emulators). They require:
832
+
833
+ - `FIRESTORE_LIVE_TESTS=1`
834
+ - `FIREBASE_PROJECT_ID=...`
835
+ - **`GOOGLE_SERVICE_ACCOUNT_BASE64`** set to base64-encoded service account JSON, **or** valid Application Default Credentials for the test project
836
+
837
+ AI live integration tests (real LLM calls) are additionally gated behind:
838
+
839
+ - `AIFUNCTIONS_LIVE_TESTS=1`
840
+ - `OPENROUTER_API_KEY` or `OPEN_ROUTER_KEY`
841
+
842
+ The **`Firestore live integration`** test (`test/integration/firestore.emulator.test.ts`) uses **`createCatalox`** and asserts v3 contracts: **`listCatalogItems`** returns **`listOutcome: "ok"`**, and **`getCatalogItem`** returns **`{ outcome: "found", item }`** for the seeded row. The descriptor patch in that test includes **`queryableFields` with `indexed: true`** for filtered fields so **`deriveIndexed`** persists **`indexed.*`** (see **Canonical `indexed` rule** above).
843
+
844
+ When **`CATALOX_RECORD_HISTORY_BUCKET`** is set, **`test/integration/record-history.live.test.ts`** runs as well: ephemeral app/catalog, **`upsert` `update` `listCatalogItemHistory` `getCatalogItemHistoryEvent` `restoreCatalogItemFromHistory`**, then best-effort Firestore + GCS cleanup for the created history objects.
845
+
846
+ ### Live test safety (read before running)
847
+
848
+ - **Never run against production credentials/projects.** Use a dedicated Firebase project for tests.
849
+ - **Touched collections**: `apps`, `catalogs`, `catalogBindings`, `catalogDefinitions`, `catalogDescriptors`, `catalogData/{catalogId}`, `catalogData-{catalogId}-items/...`, and (when record history is enabled) `catalogItemHistory/...`. The **`catalox firestore restore-backup`** / **`undo-restore-backup`** commands additionally write `backup-restoreSessions` and `{restoreSessionId}__preRestore-*` sidecar collections (see [`docs/restore-firestore-backup.md`](docs/restore-firestore-backup.md)).
850
+ - **Cleanup**: tests do best-effort deletes of the docs they created, but do not guarantee full teardown.
851
+
852
+ ## Changelog
853
+
854
+ ### 4.0.0
855
+
856
+ - **Breaking — Firebase bootstrap:** **`resolveFirebaseAdminCredentialFromEnv`**, **`createCataloxFromEnv`**, and related helpers now resolve credentials in order: **`GOOGLE_SERVICE_ACCOUNT_BASE64`** (or **`serviceAccountBase64`** option), then optional **`serviceAccountPath`** (caller-supplied path only), then Application Default Credentials. GCS credential helpers in **`backup-data`** / **`record-history`** use the same base64-then-ADC pattern.
857
+ - **New:** **`testFirestoreConnectionFromEnv`** — minimal Firestore read using the same rules as **`createCataloxFromEnv`**, on a disposable named Admin app (probe uses collection **`cataloxConnectivityProbe`**, not reserved `__…__` ids).
858
+ - **CLI:** **`catalox firestore probe`** prints JSON from **`testFirestoreConnectionFromEnv`**.
859
+ - **New:** **`applyCataloxSeedPreset`**, **`parseCataloxSeedManifest`**, **`loadCataloxSeedManifestFromPath`** (`@x12i/catalox`) — versioned JSON manifests for idempotent catalog + binding + descriptor + native item provisioning.
860
+ - **CLI:** **`catalox seed apply --app --file …`** or **`--preset …`** with optional **`--god`** for descriptor sections; **`resolveCataloxSeedPresetPath`** and npm **`catalox.seedPreset`** ([`docs/onboarding-happy-path.md`](docs/onboarding-happy-path.md)).
861
+ - **New:** **`Catalox.upsertCatalogDescriptor`** / **`CataloxBound.upsertCatalogDescriptor`** (super-admin) for host-controlled descriptor writes.
862
+ - **Docs:** [`docs/onboarding-happy-path.md`](docs/onboarding-happy-path.md), [`docs/native-map-catalog-preset.md`](docs/native-map-catalog-preset.md), example [`presets/native-map-v1.json`](presets/native-map-v1.json).
863
+
864
+ ### 3.1.1
865
+
866
+ - **Live tests:** Firestore integration test now sets **`queryableFields`** on the patched descriptor so equality filters match stored **`indexed.*`** rows (same rule as production descriptors).
867
+ - **Live tests:** Record-history integration test covers **update**, **get event**, **restore**, and **GCS object** cleanup when a bucket is configured.
868
+ - **Package:** publish **`firestore.indexes.json`** in the npm tarball for operators deploying **`catalogItemHistory`** queries.
869
+
870
+ ### 3.1.0
871
+
872
+ - **Per-record history:** optional **`recordHistory`** on **`createCatalox`** (or **`CATALOX_RECORD_HISTORY_*`** env for CLI) writes **GCS NDJSON** + Firestore **`catalogItemHistory`** on native upsert/update/delete/batch/move. APIs: **`listCatalogItemHistory`**, **`getCatalogItemHistoryEvent`**, **`restoreCatalogItemFromHistory`**, **`replayCatalogToPointInTime`**. CLI: **`catalox history …`**. See [`docs/record-history.md`](docs/record-history.md), [`firestore.indexes.json`](firestore.indexes.json).
873
+ - **Catalog lifecycle:** **`deleteCatalog`**, **`restoreDeletedCatalog`**, **`renameCatalog`** (hard rename) + CLI **`catalox catalog …`**. See [`docs/catalog-crud.md`](docs/catalog-crud.md).
874
+
875
+ ### 3.0.0
876
+
877
+ - **`createCatalox(config)`** single factory for Firestore-backed stores, authz, optional Mongo/API adapters, optional renderer snippet store (`src/catalox/create-catalox.ts`).
878
+ - **`catalox.withContext(ctx)`** / **`bindCataloxContext(catalox, ctx)`** — **`CataloxBound`**: same APIs without repeating `CataloxContext` on every call (`src/catalox/catalox-bound.ts`).
879
+ - **Breaking — lists:** **`CatalogListResult`** includes **`listOutcome: "ok" | "mapping_blocked"`**. Mapping validation failures use **`mapping_blocked`** (see **`issues`**). Empty **`items`** with **`listOutcome === "ok"`** means zero matching rows.
880
+ - **Breaking — get item:** **`getCatalogItem`** returns **`CatalogGetItemResult`** (`found` | `not_found` | `mapping_blocked`), not **`null`**.
881
+ - **Package:** **`main` / `types`** and **`exports["."]`** resolve to **`dist/src/...`**. Subpaths **`@x12i/catalox/embedder`**, **`/operator`**, **`/mapping`**, **`/firebase`**. Root **`@x12i/catalox`** re-exports embedder + operator (preserves most existing root imports).
882
+
883
+ ### 2.7.0
884
+
885
+ - **GCS restore:** **`restoreFirestoreBackupFromGcs`** + CLI **`firestore restore-backup-from-gcs`** — same pre-restore / **`undoFirestoreRestore`** model as mirror restore.
886
+ - **GCS retention:** **`pruneGcsBackupRuns`**, **`deleteCataloxGcsBackupRunObjects`**, CLI **`prune-gcs-backups`**, **`delete-gcs-backup-run`**.
887
+ - **Manifest bridge:** **`cataloxGcsBackupManifestToFirestoreExportManifest`**, CLI **`gcs-backup-to-export-manifest`**, type **`CataloxGcsBackupManifestV1`**; exported **`restoreFirestoreNdjsonStreamToCollection`**.
888
+ - **Reliability:** failed **`backupData` GCS** runs trigger **best-effort** deletion of the partial run folder.
889
+ - **Contracts:** **`RestoreFirestoreMirrorSource`** vs **`RestoreFirestoreBackupSource`** (adds `gcs` for session manifests).
890
+
891
+ ### 2.6.0
892
+
893
+ - **GCS backup:** `backupData` supports **`mode: "gcs"`** — writes metadata, native catalogs, optional snapshots as **NDJSON** under `gs://{bucket}/{prefix}{timestamp}/` via **`@x12i/helpers/gcs`**, with **`catalox-backup-manifest.json`** per run. Default prefix when omitted: **`catalox-firestore-backups/`**. CLI: **`firestore backup --mode gcs --bucket …`** (`--gcs-prefix` optional).
894
+ - **Export:** `gcsBackupRunFolder` helper (for tests and path clarity).
895
+ - **Docs / report:** [`docs/backup.md`](docs/backup.md) updated; gap analysis [`.reports/gcs-backup-gap-analysis.md`](.reports/gcs-backup-gap-analysis.md).
896
+
897
+ ### 2.5.0
898
+
899
+ - **GCS:** NDJSON export/import for one or many Firestore collections (`exportFirestoreCollectionToGcs`, `exportAllFirestoreCollectionsToGcs`, restore helpers, CLI `firestore export-gcs` / `import-gcs`). Optional **`gcsPrefix`** and **`objectNamePostfix`** on object keys; manifest-driven restore-all.
900
+ - **GCS:** Compare live collections to bucket NDJSON — identical / changed / only-in-Firestore / only-in-bucket (`compareFirestoreCollectionWithGcsNdjson`, manifest mode, CLI `firestore compare-gcs`). Helpers **`normalizeForCompare`**, **`dataFingerprint`** exported for tooling.
901
+ - **Native:** `listCatalogItems` compacts inert filter entries before Firestore queries; `NativeItemStore.list` can re-resolve flat vs legacy layout after an unconstrained empty first page.
902
+ - **Docs:** [`docs/native-catalog-storage-and-api.md`](docs/native-catalog-storage-and-api.md), [`docs/firestore-gcs-export.md`](docs/firestore-gcs-export.md); README and environment docs updated for GCS.
903
+ - **Dependency:** `@google-cloud/storage` (^7.19).
904
+
905
+ ## Boundaries (important)
906
+
907
+ - **Secrets**: do not store secret material (API keys, cloud creds) in Catalox. Store only non-secret refs like `credentialsRef`.
908
+ - **Artifacts**: store descriptor metadata and remote keys; artifact blobs live in external object storage.
909
+