latticesql 3.4.0 → 3.4.2

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/dist/index.cjs CHANGED
@@ -919,43 +919,57 @@ var init_postgres = __esm({
919
919
  },
920
920
  {
921
921
  warn: "could not register json_extract polyfill:",
922
- sql: `CREATE OR REPLACE FUNCTION json_extract(doc text, path text)
923
- RETURNS text
924
- LANGUAGE sql
925
- IMMUTABLE
926
- AS $fn$
927
- SELECT doc::jsonb #>> string_to_array(regexp_replace(path, '^\\$\\.?', ''), '.')
928
- $fn$;`
922
+ // Create ONLY if absent. `CREATE OR REPLACE` on an existing function requires
923
+ // ownership, but on a cloud the function is owned by whichever single role
924
+ // created it first — so every OTHER member's per-connect replace raised "must
925
+ // be owner of function" and (sharing the render transaction) aborted it,
926
+ // yielding an empty render. The IF-absent guard makes a present function a
927
+ // clean no-op for everyone, regardless of who owns it.
928
+ sql: `DO $do$ BEGIN
929
+ IF to_regprocedure('json_extract(text, text)') IS NULL THEN
930
+ CREATE FUNCTION json_extract(doc text, path text)
931
+ RETURNS text
932
+ LANGUAGE sql
933
+ IMMUTABLE
934
+ AS $fn$
935
+ SELECT doc::jsonb #>> string_to_array(regexp_replace(path, '^\\$\\.?', ''), '.')
936
+ $fn$;
937
+ END IF;
938
+ END $do$;`
929
939
  },
930
940
  {
931
941
  warn: "could not register strftime polyfill:",
932
- sql: `CREATE OR REPLACE FUNCTION strftime(format text, modifier text)
933
- RETURNS text
934
- LANGUAGE plpgsql
935
- IMMUTABLE
936
- AS $fn$
937
- DECLARE ts timestamptz;
938
- BEGIN
939
- IF modifier = 'now' THEN
940
- ts := now();
941
- ELSE
942
- ts := modifier::timestamptz;
943
- END IF;
944
- RETURN to_char(
945
- ts AT TIME ZONE 'UTC',
946
- replace(replace(replace(replace(replace(replace(replace(replace(
947
- format,
948
- '%Y', 'YYYY'),
949
- '%m', 'MM'),
950
- '%d', 'DD'),
951
- '%H', 'HH24'),
952
- '%M', 'MI'),
953
- '%S', 'SS'),
954
- '%f', 'MS'),
955
- 'T', '"T"')
956
- );
957
- END;
958
- $fn$;`
942
+ sql: `DO $do$ BEGIN
943
+ IF to_regprocedure('strftime(text, text)') IS NULL THEN
944
+ CREATE FUNCTION strftime(format text, modifier text)
945
+ RETURNS text
946
+ LANGUAGE plpgsql
947
+ IMMUTABLE
948
+ AS $fn$
949
+ DECLARE ts timestamptz;
950
+ BEGIN
951
+ IF modifier = 'now' THEN
952
+ ts := now();
953
+ ELSE
954
+ ts := modifier::timestamptz;
955
+ END IF;
956
+ RETURN to_char(
957
+ ts AT TIME ZONE 'UTC',
958
+ replace(replace(replace(replace(replace(replace(replace(replace(
959
+ format,
960
+ '%Y', 'YYYY'),
961
+ '%m', 'MM'),
962
+ '%d', 'DD'),
963
+ '%H', 'HH24'),
964
+ '%M', 'MI'),
965
+ '%S', 'SS'),
966
+ '%f', 'MS'),
967
+ 'T', '"T"')
968
+ );
969
+ END;
970
+ $fn$;
971
+ END IF;
972
+ END $do$;`
959
973
  }
960
974
  ];
961
975
  }
@@ -2364,6 +2378,34 @@ var init_engine = __esm({
2364
2378
  setRenderFold(fn) {
2365
2379
  this._foldRows = fn;
2366
2380
  }
2381
+ /**
2382
+ * Incremental scope: is this entity-context table affected by a change to one
2383
+ * of `changed`? Affected when the table itself changed (its own rows / `self`
2384
+ * source / index) OR any of its files SOURCES from a changed table (a cross-
2385
+ * table dependent — e.g. an AGENT.md that lists the agent's tasks must re-render
2386
+ * when `tasks` changes). A `custom` source runs an arbitrary query, so we can't
2387
+ * prove independence — treat it as always-affected (conservative, never stale).
2388
+ */
2389
+ _entityAffected(table, def, changed) {
2390
+ if (changed.has(table)) return true;
2391
+ for (const spec of Object.values(def.files)) {
2392
+ if (this._sourceTouches(spec.source, changed)) return true;
2393
+ }
2394
+ return false;
2395
+ }
2396
+ _sourceTouches(source, changed) {
2397
+ if (source == null || typeof source !== "object") return false;
2398
+ const s2 = source;
2399
+ if (s2.type === "custom") return true;
2400
+ if (typeof s2.table === "string" && changed.has(s2.table)) return true;
2401
+ if (typeof s2.junctionTable === "string" && changed.has(s2.junctionTable)) return true;
2402
+ if (s2.sources != null && typeof s2.sources === "object") {
2403
+ for (const sub of Object.values(s2.sources)) {
2404
+ if (this._sourceTouches(sub, changed)) return true;
2405
+ }
2406
+ }
2407
+ return false;
2408
+ }
2367
2409
  async render(outputDir, opts = {}) {
2368
2410
  const start = Date.now();
2369
2411
  const filesWritten = [];
@@ -2373,6 +2415,7 @@ var init_engine = __esm({
2373
2415
  for (const [name, def] of this._schema.getTables()) {
2374
2416
  if (signal?.aborted) return this._abortedResult(filesWritten, counters, start);
2375
2417
  if (this._skipEmpty && def.render === NOOP_RENDER) continue;
2418
+ if (opts.changedTables && !opts.changedTables.has(name)) continue;
2376
2419
  let rows = await this._schema.queryTable(this._adapter, name, this._readRel);
2377
2420
  if (def.relevanceFilter) {
2378
2421
  const ctx = this._getTaskContext();
@@ -2425,6 +2468,9 @@ var init_engine = __esm({
2425
2468
  }
2426
2469
  for (const [name, def] of this._schema.getMultis()) {
2427
2470
  if (signal?.aborted) return this._abortedResult(filesWritten, counters, start);
2471
+ if (opts.changedTables && def.tables && !def.tables.some((t8) => opts.changedTables?.has(t8))) {
2472
+ continue;
2473
+ }
2428
2474
  const keys = await def.keys();
2429
2475
  const tables = {};
2430
2476
  if (def.tables) {
@@ -2456,16 +2502,22 @@ var init_engine = __esm({
2456
2502
  filesWritten,
2457
2503
  counters,
2458
2504
  throttle,
2459
- signal
2505
+ signal,
2506
+ opts.changedTables
2460
2507
  );
2461
2508
  if (entityContextManifest === null) {
2462
2509
  return this._abortedResult(filesWritten, counters, start);
2463
2510
  }
2464
2511
  if (this._schema.getEntityContexts().size > 0) {
2512
+ let entityContexts = entityContextManifest;
2513
+ if (opts.changedTables) {
2514
+ const prev = readManifest(outputDir);
2515
+ entityContexts = { ...prev?.entityContexts ?? {}, ...entityContextManifest };
2516
+ }
2465
2517
  writeManifest(outputDir, {
2466
2518
  version: 2,
2467
2519
  generated_at: (/* @__PURE__ */ new Date()).toISOString(),
2468
- entityContexts: entityContextManifest
2520
+ entityContexts
2469
2521
  });
2470
2522
  }
2471
2523
  const result = {
@@ -2531,12 +2583,14 @@ var init_engine = __esm({
2531
2583
  * partial tree). Progress is reported through `throttle`; abort is observed
2532
2584
  * via `signal`.
2533
2585
  */
2534
- async _renderEntityContexts(outputDir, filesWritten, counters, throttle, signal) {
2586
+ async _renderEntityContexts(outputDir, filesWritten, counters, throttle, signal, changedTables) {
2535
2587
  const protectedTables = /* @__PURE__ */ new Set();
2536
2588
  for (const [t8, d6] of this._schema.getEntityContexts()) {
2537
2589
  if (d6.protected) protectedTables.add(t8);
2538
2590
  }
2539
- const entityTables = [...this._schema.getEntityContexts()];
2591
+ const entityTables = [...this._schema.getEntityContexts()].filter(
2592
+ ([table, def]) => !changedTables || this._entityAffected(table, def, changedTables)
2593
+ );
2540
2594
  const tableCount = entityTables.length;
2541
2595
  if (signal?.aborted) return null;
2542
2596
  const renderedEntries = await mapWithConcurrency(
@@ -5236,6 +5290,16 @@ var init_lattice = __esm({
5236
5290
  _autoRenderPending = false;
5237
5291
  _autoRenderInFlight = false;
5238
5292
  _autoRenderDebounceMs = 250;
5293
+ /**
5294
+ * Incremental auto-render scope, accumulated between debounced renders. A write
5295
+ * or a remote (cloud) change records the AFFECTED table here, so the next
5296
+ * auto-render re-renders only that entity (+ its cross-table dependents) instead
5297
+ * of the whole tree. `_pendingRenderAll` forces a full render (the initial
5298
+ * render, or a change with no known table). Captured + reset when a render
5299
+ * starts, so changes during a render re-accumulate and re-trigger.
5300
+ */
5301
+ _pendingRenderTables = /* @__PURE__ */ new Set();
5302
+ _pendingRenderAll = true;
5239
5303
  /** Cache of actual table columns (from PRAGMA), populated after init(). */
5240
5304
  _columnCache = /* @__PURE__ */ new Map();
5241
5305
  /** Derived encryption key (from options.encryptionKey via scrypt). */
@@ -6331,7 +6395,7 @@ var init_lattice = __esm({
6331
6395
  `${verb} INTO "${junctionTable}" (${colNames}) VALUES (${placeholders})`,
6332
6396
  Object.values(filtered)
6333
6397
  );
6334
- this._scheduleAutoRender();
6398
+ this._scheduleAutoRender(junctionTable);
6335
6399
  }
6336
6400
  /**
6337
6401
  * Delete rows from a junction table matching all given conditions.
@@ -6347,7 +6411,7 @@ var init_lattice = __esm({
6347
6411
  `DELETE FROM "${junctionTable}" WHERE ${where}`,
6348
6412
  entries.map(([, v2]) => v2)
6349
6413
  );
6350
- this._scheduleAutoRender();
6414
+ this._scheduleAutoRender(junctionTable);
6351
6415
  }
6352
6416
  // -------------------------------------------------------------------------
6353
6417
  // Seeding DSL (v0.13+)
@@ -6603,6 +6667,11 @@ var init_lattice = __esm({
6603
6667
  async renderInBackground(outputDir, opts = {}) {
6604
6668
  const notInit = this._notInitError();
6605
6669
  if (notInit) return notInit;
6670
+ if (!opts.changedTables) {
6671
+ this._pendingRenderAll = false;
6672
+ this._pendingRenderTables = /* @__PURE__ */ new Set();
6673
+ this._autoRenderPending = false;
6674
+ }
6606
6675
  return this._renderGuarded(outputDir, opts);
6607
6676
  }
6608
6677
  /**
@@ -6632,9 +6701,12 @@ var init_lattice = __esm({
6632
6701
  * tree when a REMOTE change arrives — notably an owner re-sharing or un-sharing
6633
6702
  * a row, after which the member's per-viewer projection must be recompiled. A
6634
6703
  * no-op when auto-render isn't enabled.
6704
+ *
6705
+ * Pass the CHANGED table so only that entity (+ its cross-table dependents) is
6706
+ * re-rendered instead of the whole tree; omit it to force a full render.
6635
6707
  */
6636
- requestRender() {
6637
- this._scheduleAutoRender();
6708
+ requestRender(table) {
6709
+ this._scheduleAutoRender(table);
6638
6710
  }
6639
6711
  /**
6640
6712
  * True while a render is actively writing the context tree + manifest (auto-
@@ -7035,7 +7107,7 @@ var init_lattice = __esm({
7035
7107
  for (const h6 of this._errorHandlers) h6(err instanceof Error ? err : new Error(String(err)));
7036
7108
  }
7037
7109
  }
7038
- this._scheduleAutoRender();
7110
+ this._scheduleAutoRender(table);
7039
7111
  }
7040
7112
  /**
7041
7113
  * Turn on automatic rendering into `outputDir`. After this, every insert /
@@ -7059,10 +7131,18 @@ var init_lattice = __esm({
7059
7131
  this._autoRenderPending = false;
7060
7132
  return this;
7061
7133
  }
7062
- _scheduleAutoRender() {
7134
+ _scheduleAutoRender(table) {
7063
7135
  if (!this._autoRenderDir) return;
7136
+ if (table === void 0) this._pendingRenderAll = true;
7137
+ else this._pendingRenderTables.add(table);
7064
7138
  this._autoRenderPending = true;
7065
- if (this._autoRenderTimer) return;
7139
+ this._armAutoRenderTimer();
7140
+ }
7141
+ /** Arm the debounce timer if not already armed. Does NOT change the render
7142
+ * scope — used both by `_scheduleAutoRender` and the post-render re-arm so a
7143
+ * re-arm never escalates a pending incremental render to a full one. */
7144
+ _armAutoRenderTimer() {
7145
+ if (!this._autoRenderDir || this._autoRenderTimer) return;
7066
7146
  this._autoRenderTimer = setTimeout(() => {
7067
7147
  this._autoRenderTimer = void 0;
7068
7148
  void this._runAutoRender();
@@ -7102,10 +7182,15 @@ var init_lattice = __esm({
7102
7182
  }
7103
7183
  if (!this._autoRenderPending) return;
7104
7184
  this._autoRenderPending = false;
7185
+ const renderAll = this._pendingRenderAll;
7186
+ const changed = this._pendingRenderTables;
7187
+ this._pendingRenderAll = false;
7188
+ this._pendingRenderTables = /* @__PURE__ */ new Set();
7105
7189
  this._autoRenderInFlight = true;
7106
7190
  try {
7107
7191
  const prevManifest = readManifest(dir);
7108
- const result = await this._render.render(dir);
7192
+ const renderOpts = renderAll || changed.size === 0 ? {} : { changedTables: changed };
7193
+ const result = await this._render.render(dir, renderOpts);
7109
7194
  for (const h6 of this._renderHandlers) h6(result);
7110
7195
  const newManifest = readManifest(dir);
7111
7196
  await this._render.cleanup(dir, prevManifest, {}, newManifest);
@@ -7117,7 +7202,7 @@ var init_lattice = __esm({
7117
7202
  }
7118
7203
  }
7119
7204
  _rearmAutoRenderIfPending() {
7120
- if (this._autoRenderPending) this._scheduleAutoRender();
7205
+ if (this._autoRenderPending) this._armAutoRenderTimer();
7121
7206
  }
7122
7207
  /**
7123
7208
  * Update or remove the embedding for a row.
@@ -47282,6 +47367,44 @@ CREATE TRIGGER "${trg}" AFTER INSERT OR UPDATE OR DELETE ON ${q3}
47282
47367
  FOR EACH ROW EXECUTE FUNCTION "${trg}"();
47283
47368
  `;
47284
47369
  }
47370
+ async function ownPolyfillsByGroup(db) {
47371
+ if (!isPg(db)) return;
47372
+ for (const sig of ["json_extract(text, text)", "strftime(text, text)"]) {
47373
+ try {
47374
+ const reg = await getAsyncOrSync(db.adapter, `SELECT to_regprocedure($1) AS reg`, [sig]);
47375
+ if (reg?.reg == null) continue;
47376
+ await runAsyncOrSync(db.adapter, `ALTER FUNCTION ${sig} OWNER TO "${MEMBER_GROUP}"`);
47377
+ } catch {
47378
+ }
47379
+ }
47380
+ }
47381
+ async function enableGuiAuditRls(db) {
47382
+ if (!isPg(db)) return;
47383
+ const reg = await getAsyncOrSync(db.adapter, `SELECT to_regclass($1) AS reg`, [
47384
+ "_lattice_gui_audit"
47385
+ ]);
47386
+ if (reg?.reg == null) return;
47387
+ await runCloudBootstrapSql(
47388
+ db,
47389
+ `
47390
+ ALTER TABLE "_lattice_gui_audit" ENABLE ROW LEVEL SECURITY;
47391
+ ALTER TABLE "_lattice_gui_audit" FORCE ROW LEVEL SECURITY;
47392
+ DROP POLICY IF EXISTS "lattice_gui_audit_owner" ON "_lattice_gui_audit";
47393
+ DROP POLICY IF EXISTS "lattice_gui_audit_sel" ON "_lattice_gui_audit";
47394
+ CREATE POLICY "lattice_gui_audit_sel" ON "_lattice_gui_audit" FOR SELECT
47395
+ USING ("row_id" IS NULL OR lattice_row_visible("table_name", "row_id"));
47396
+ DROP POLICY IF EXISTS "lattice_gui_audit_ins" ON "_lattice_gui_audit";
47397
+ CREATE POLICY "lattice_gui_audit_ins" ON "_lattice_gui_audit" FOR INSERT
47398
+ WITH CHECK ("row_id" IS NULL OR lattice_row_visible("table_name", "row_id"));
47399
+ DROP POLICY IF EXISTS "lattice_gui_audit_upd" ON "_lattice_gui_audit";
47400
+ CREATE POLICY "lattice_gui_audit_upd" ON "_lattice_gui_audit" FOR UPDATE
47401
+ USING ("row_id" IS NULL OR lattice_row_visible("table_name", "row_id"));
47402
+ DROP POLICY IF EXISTS "lattice_gui_audit_del" ON "_lattice_gui_audit";
47403
+ CREATE POLICY "lattice_gui_audit_del" ON "_lattice_gui_audit" FOR DELETE
47404
+ USING ("row_id" IS NULL OR lattice_row_visible("table_name", "row_id"));
47405
+ `
47406
+ );
47407
+ }
47285
47408
  async function installCloudRls(db) {
47286
47409
  if (!isPg(db)) return;
47287
47410
  const schema = await cloudSchema(db);
@@ -47315,6 +47438,24 @@ CREATE POLICY "lattice_changelog_ins" ON "__lattice_changelog" FOR INSERT WITH C
47315
47438
  `
47316
47439
  );
47317
47440
  }
47441
+ async function enableChatPrivacyRls(db) {
47442
+ if (!isPg(db)) return;
47443
+ for (const t8 of ["chat_threads", "chat_messages"]) {
47444
+ const reg = await getAsyncOrSync(db.adapter, `SELECT to_regclass($1) AS reg`, [t8]);
47445
+ if (reg?.reg == null) continue;
47446
+ const q3 = `"${t8}"`;
47447
+ await runCloudBootstrapSql(
47448
+ db,
47449
+ `
47450
+ ALTER TABLE ${q3} ENABLE ROW LEVEL SECURITY;
47451
+ ALTER TABLE ${q3} FORCE ROW LEVEL SECURITY;
47452
+ DROP POLICY IF EXISTS "lattice_chat_owner" ON ${q3};
47453
+ CREATE POLICY "lattice_chat_owner" ON ${q3} AS RESTRICTIVE FOR SELECT
47454
+ USING ("owner_user_id" IS NOT NULL AND "owner_user_id" = session_user);
47455
+ `
47456
+ );
47457
+ }
47458
+ }
47318
47459
  async function enableRlsForTable(db, table, pkCols) {
47319
47460
  if (!isPg(db)) return;
47320
47461
  const schema = await cloudSchema(db);
@@ -47429,6 +47570,18 @@ CREATE TABLE IF NOT EXISTS "__lattice_member_invites" (
47429
47570
  -- bootstrap is now run directly + idempotently, not version-gated).
47430
47571
  ALTER TABLE "__lattice_member_invites" ADD COLUMN IF NOT EXISTS "email" text;
47431
47572
 
47573
+ -- Owner-published entity/render LAYOUT (the entities + entityContexts config
47574
+ -- blocks), so a joined member \u2014 whose generated config has entities: {} \u2014 can
47575
+ -- hydrate the full render layout and produce a complete context tree. This holds
47576
+ -- schema CONFIG, not row data, so it is safe to share with members (granted
47577
+ -- SELECT). A shared singleton, like __lattice_user_identity: no per-row RLS.
47578
+ CREATE TABLE IF NOT EXISTS "__lattice_shared_schema" (
47579
+ "id" TEXT PRIMARY KEY DEFAULT 'singleton',
47580
+ "entities_json" TEXT,
47581
+ "contexts_json" TEXT,
47582
+ "updated_at" TEXT
47583
+ );
47584
+
47432
47585
  -- #3.1 \u2014 one-time-use + revocation enforcement. After a member authenticates to
47433
47586
  -- the cloud with their minted credential, the join path calls this to CLAIM the
47434
47587
  -- invite. The single atomic UPDATE stamps redeemed_at and returns true ONLY when
@@ -50357,7 +50510,7 @@ function findDocsDir() {
50357
50510
  }
50358
50511
  for (let i6 = 0; i6 < 8; i6++) {
50359
50512
  const candidate = (0, import_node_path31.join)(dir, "docs");
50360
- if ((0, import_node_fs28.existsSync)((0, import_node_path31.join)(candidate, "cloud.md"))) {
50513
+ if ((0, import_node_fs29.existsSync)((0, import_node_path31.join)(candidate, "cloud.md"))) {
50361
50514
  _docsDir = candidate;
50362
50515
  return _docsDir;
50363
50516
  }
@@ -50398,13 +50551,13 @@ function allSections() {
50398
50551
  const out = [];
50399
50552
  let files = [];
50400
50553
  try {
50401
- files = (0, import_node_fs28.readdirSync)(dir).filter((f6) => f6.endsWith(".md"));
50554
+ files = (0, import_node_fs29.readdirSync)(dir).filter((f6) => f6.endsWith(".md"));
50402
50555
  } catch {
50403
50556
  files = [];
50404
50557
  }
50405
50558
  for (const f6 of files) {
50406
50559
  try {
50407
- out.push(...sectionsOf(f6, (0, import_node_fs28.readFileSync)((0, import_node_path31.join)(dir, f6), "utf8")));
50560
+ out.push(...sectionsOf(f6, (0, import_node_fs29.readFileSync)((0, import_node_path31.join)(dir, f6), "utf8")));
50408
50561
  } catch {
50409
50562
  }
50410
50563
  }
@@ -50446,11 +50599,11 @@ function searchLatticeDocs(query, limit = 4) {
50446
50599
  }))
50447
50600
  };
50448
50601
  }
50449
- var import_node_fs28, import_node_path31, import_node_url2, import_meta5, _docsDir, MAX_SECTION_CHARS, _cache;
50602
+ var import_node_fs29, import_node_path31, import_node_url2, import_meta5, _docsDir, MAX_SECTION_CHARS, _cache;
50450
50603
  var init_lattice_docs = __esm({
50451
50604
  "src/gui/ai/lattice-docs.ts"() {
50452
50605
  "use strict";
50453
- import_node_fs28 = require("fs");
50606
+ import_node_fs29 = require("fs");
50454
50607
  import_node_path31 = require("path");
50455
50608
  import_node_url2 = require("url");
50456
50609
  import_meta5 = {};
@@ -54518,7 +54671,7 @@ var MEMBER_READABLE_BOOKKEEPING = [
54518
54671
  {
54519
54672
  name: "_lattice_gui_audit",
54520
54673
  privs: "SELECT, INSERT",
54521
- why: "the member's own GUI undo/redo log (session-scoped)"
54674
+ why: "GUI undo/redo + version history; RLS (enableGuiAuditRls) scopes reads to entries whose underlying row the member can see"
54522
54675
  },
54523
54676
  {
54524
54677
  name: "__lattice_user_identity",
@@ -54529,6 +54682,11 @@ var MEMBER_READABLE_BOOKKEEPING = [
54529
54682
  name: "__lattice_changelog",
54530
54683
  privs: "SELECT, INSERT",
54531
54684
  why: "per-viewer-RLS-filtered change history for observe()/history (the policy filters reads, so the base grant is safe)"
54685
+ },
54686
+ {
54687
+ name: "__lattice_shared_schema",
54688
+ privs: "SELECT",
54689
+ why: "owner-published entity/render layout (entities + entityContexts) a joined member hydrates its config from so render produces the full context tree"
54532
54690
  }
54533
54691
  ];
54534
54692
  var MEMBER_EXECUTE_FUNCTIONS = [
@@ -54680,9 +54838,12 @@ async function secureCloud(db) {
54680
54838
  if (db.getDialect() !== "postgres") return;
54681
54839
  await registerPostgresPolyfills((sql) => runAsyncOrSync(db.adapter, sql));
54682
54840
  await installCloudRls(db);
54841
+ await ownPolyfillsByGroup(db);
54683
54842
  await installCloudSettings(db);
54684
54843
  await db.ensureObservationSubstrate();
54685
54844
  await enableChangelogRls(db);
54845
+ await enableChatPrivacyRls(db);
54846
+ await enableGuiAuditRls(db);
54686
54847
  await convergeLegacyColumnAudience(db);
54687
54848
  const registered = db.getRegisteredTableNames();
54688
54849
  for (const table of registered) {
@@ -57728,12 +57889,17 @@ var appJs = `
57728
57889
  fetchJson('/api/gui-meta/columns').catch(function () { return {}; }),
57729
57890
  fetchJson('/api/system-tables').catch(function () { return { tables: [] }; }),
57730
57891
  fetchJson('/api/workspaces').catch(function () { return null; }),
57892
+ fetchJson('/api/dbconfig').catch(function () { return {}; }),
57731
57893
  ]).then(function (results) {
57732
57894
  state.entities = results[0];
57733
57895
  state.iconOverrides = results[1] || {};
57734
57896
  state.columnMeta = results[2] || {};
57735
57897
  state.systemTables = (results[3] && results[3].tables) || [];
57736
57898
  renderWsSwitcher(results[4]);
57899
+ // Re-point the header logo at the NEW workspace's mark \u2014 the switch path
57900
+ // must refresh branding the way boot does (the etag cache-busts the
57901
+ // <img>), else the previous workspace's logo stays until a hard refresh.
57902
+ applyWorkspaceLogo((results[5] || {}).logoEtag);
57737
57903
  renderSidebar();
57738
57904
  // renderWsSwitcher set cloudMode from the new workspace's kind; re-render
57739
57905
  // the composer so the Private-mode toggle reflects local vs cloud (it is
@@ -64631,6 +64797,87 @@ init_postgres();
64631
64797
  init_cloud_connect();
64632
64798
  init_rls();
64633
64799
 
64800
+ // src/cloud/shared-schema.ts
64801
+ init_lattice();
64802
+ init_adapter();
64803
+
64804
+ // src/gui/config-io.ts
64805
+ var import_node_fs28 = require("fs");
64806
+ var import_yaml4 = require("yaml");
64807
+ async function execSql(db, sql) {
64808
+ const adapter = db._adapter;
64809
+ if (!adapter.runAsync) throw new Error("Adapter does not support runAsync");
64810
+ await adapter.runAsync(sql);
64811
+ }
64812
+ function loadConfigDoc(configPath) {
64813
+ return (0, import_yaml4.parseDocument)((0, import_node_fs28.readFileSync)(configPath, "utf8"));
64814
+ }
64815
+ function saveConfigDoc(configPath, doc) {
64816
+ (0, import_node_fs28.writeFileSync)(configPath, doc.toString(), "utf8");
64817
+ }
64818
+
64819
+ // src/cloud/shared-schema.ts
64820
+ async function publishSharedSchema(db, configPath) {
64821
+ if (db.getDialect() !== "postgres") return;
64822
+ const cfg = loadConfigDoc(configPath).toJSON();
64823
+ const entities = cfg.entities ?? {};
64824
+ if (Object.keys(entities).length === 0) return;
64825
+ await runAsyncOrSync(
64826
+ db.adapter,
64827
+ `INSERT INTO "__lattice_shared_schema" ("id","entities_json","contexts_json","updated_at")
64828
+ VALUES ('singleton', $1, $2, $3)
64829
+ ON CONFLICT ("id") DO UPDATE SET
64830
+ "entities_json" = EXCLUDED."entities_json",
64831
+ "contexts_json" = EXCLUDED."contexts_json",
64832
+ "updated_at" = EXCLUDED."updated_at"`,
64833
+ [
64834
+ JSON.stringify(entities),
64835
+ JSON.stringify(cfg.entityContexts ?? null),
64836
+ (/* @__PURE__ */ new Date()).toISOString()
64837
+ ]
64838
+ );
64839
+ }
64840
+ async function hydrateMemberConfigFromCloud(configPath, dbUrl, encryptionKey) {
64841
+ if (!isPostgresUrl(dbUrl)) return false;
64842
+ const existing = loadConfigDoc(configPath).toJSON();
64843
+ if (Object.keys(existing.entities ?? {}).length > 0) return false;
64844
+ try {
64845
+ const peek = new Lattice({ config: configPath }, { encryptionKey });
64846
+ try {
64847
+ await peek.init({ introspectOnly: true });
64848
+ const reg = await getAsyncOrSync(
64849
+ peek.adapter,
64850
+ "SELECT to_regclass('__lattice_shared_schema') AS reg"
64851
+ );
64852
+ if (reg?.reg == null) return false;
64853
+ const row = await getAsyncOrSync(
64854
+ peek.adapter,
64855
+ 'SELECT "entities_json","contexts_json" FROM "__lattice_shared_schema" WHERE "id" = $1',
64856
+ ["singleton"]
64857
+ );
64858
+ if (row?.entities_json == null) return false;
64859
+ const entities = JSON.parse(row.entities_json);
64860
+ if (Object.keys(entities).length === 0) return false;
64861
+ const doc = loadConfigDoc(configPath);
64862
+ doc.setIn(["entities"], entities);
64863
+ if (row.contexts_json != null) {
64864
+ const ctx = JSON.parse(row.contexts_json);
64865
+ if (ctx) doc.setIn(["entityContexts"], ctx);
64866
+ }
64867
+ saveConfigDoc(configPath, doc);
64868
+ return true;
64869
+ } finally {
64870
+ peek.close();
64871
+ }
64872
+ } catch (e6) {
64873
+ console.warn(
64874
+ "[hydrateMemberConfigFromCloud] could not hydrate member schema:",
64875
+ e6.message
64876
+ );
64877
+ return false;
64878
+ }
64879
+ }
64880
+
64634
64881
  // src/gui/meta-gen.ts
64635
64882
  init_assistant_routes();
64636
64883
  init_chat();
@@ -64719,21 +64966,6 @@ var FeedBus = class {
64719
64966
  init_fts();
64720
64967
  init_mutations();
64721
64968
 
64722
- // src/gui/config-io.ts
64723
- var import_node_fs29 = require("fs");
64724
- var import_yaml4 = require("yaml");
64725
- async function execSql(db, sql) {
64726
- const adapter = db._adapter;
64727
- if (!adapter.runAsync) throw new Error("Adapter does not support runAsync");
64728
- await adapter.runAsync(sql);
64729
- }
64730
- function loadConfigDoc(configPath) {
64731
- return (0, import_yaml4.parseDocument)((0, import_node_fs29.readFileSync)(configPath, "utf8"));
64732
- }
64733
- function saveConfigDoc(configPath, doc) {
64734
- (0, import_node_fs29.writeFileSync)(configPath, doc.toString(), "utf8");
64735
- }
64736
-
64737
64969
  // src/gui/schema-ops.ts
64738
64970
  init_parser();
64739
64971
  init_canonical_context();
@@ -65922,6 +66154,7 @@ async function dispatchDbConfigRoute(req, res, ctx) {
65922
66154
  const result = await migrateLatticeData(ctx.db, target);
65923
66155
  await target.rebuildFtsIndexes();
65924
66156
  await secureCloud(target);
66157
+ await publishSharedSchema(target, ctx.configPath);
65925
66158
  target.close();
65926
66159
  const sourceDbPath = parseConfigFile(ctx.configPath).dbPath;
65927
66160
  const backupPath = archiveLocalSqlite(sourceDbPath);
@@ -66384,6 +66617,7 @@ function activeCloudCoords(configPath) {
66384
66617
  init_assistant_routes();
66385
66618
 
66386
66619
  // src/gui/chat-routes.ts
66620
+ init_adapter();
66387
66621
  init_assistant_routes();
66388
66622
  init_chat();
66389
66623
  init_user_config();
@@ -66409,6 +66643,15 @@ function sendJson3(res, body, status = 200) {
66409
66643
  function asStr(v2, fallback = "") {
66410
66644
  return typeof v2 === "string" ? v2 : fallback;
66411
66645
  }
66646
+ function isCloudChat(db) {
66647
+ return db.getDialect() === "postgres";
66648
+ }
66649
+ async function resolveChatOwnerId(db) {
66650
+ if (!isCloudChat(db)) return null;
66651
+ const row = await getAsyncOrSync(db.adapter, "SELECT session_user AS u");
66652
+ const u2 = row?.u;
66653
+ return typeof u2 === "string" && u2.length > 0 ? u2 : null;
66654
+ }
66412
66655
  function readJson3(req) {
66413
66656
  return new Promise((resolve12, reject) => {
66414
66657
  let raw = "";
@@ -66601,12 +66844,20 @@ async function persistMessage(db, threadId, role, text, ownerUserId, turns, star
66601
66844
  });
66602
66845
  }
66603
66846
  async function dispatchChatRoute(req, res, ctx) {
66847
+ const ownerUserId = await resolveChatOwnerId(ctx.db);
66848
+ const cloud = isCloudChat(ctx.db);
66849
+ const ownedByMe = (r6) => !cloud || r6.owner_user_id != null && r6.owner_user_id === ownerUserId;
66604
66850
  if (ctx.method === "GET" && ctx.pathname === "/api/chat/threads") {
66851
+ if (cloud && ownerUserId == null) {
66852
+ sendJson3(res, { threads: [] });
66853
+ return true;
66854
+ }
66605
66855
  const filters = [
66606
66856
  { col: "deleted_at", op: "isNull" }
66607
66857
  ];
66858
+ if (ownerUserId != null) filters.push({ col: "owner_user_id", op: "eq", val: ownerUserId });
66608
66859
  const rows = await ctx.db.query("chat_threads", { filters, limit: 100 });
66609
- const threads = rows.filter((r6) => !r6.deleted_at).map((r6) => ({
66860
+ const threads = rows.filter((r6) => !r6.deleted_at && ownedByMe(r6)).map((r6) => ({
66610
66861
  id: asStr(r6.id),
66611
66862
  title: asStr(r6.title, "Chat"),
66612
66863
  created_at: asStr(r6.created_at)
@@ -66617,12 +66868,19 @@ async function dispatchChatRoute(req, res, ctx) {
66617
66868
  const msgMatch = /^\/api\/chat\/threads\/([^/]+)\/messages$/.exec(ctx.pathname);
66618
66869
  if (ctx.method === "GET" && msgMatch) {
66619
66870
  const threadId2 = decodeURIComponent(msgMatch[1] ?? "");
66620
- const msgFilters = [{ col: "thread_id", op: "eq", val: threadId2 }];
66871
+ if (cloud && ownerUserId == null) {
66872
+ sendJson3(res, { messages: [] });
66873
+ return true;
66874
+ }
66875
+ const msgFilters = [
66876
+ { col: "thread_id", op: "eq", val: threadId2 }
66877
+ ];
66878
+ if (ownerUserId != null) msgFilters.push({ col: "owner_user_id", op: "eq", val: ownerUserId });
66621
66879
  const rows = await ctx.db.query("chat_messages", {
66622
66880
  filters: msgFilters,
66623
66881
  limit: 1e3
66624
66882
  });
66625
- const messages = rows.filter((r6) => r6.thread_id === threadId2 && !r6.deleted_at).map((r6) => {
66883
+ const messages = rows.filter((r6) => r6.thread_id === threadId2 && !r6.deleted_at && ownedByMe(r6)).map((r6) => {
66626
66884
  let text = "";
66627
66885
  let turns2;
66628
66886
  let startedAt;
@@ -66675,12 +66933,21 @@ async function dispatchChatRoute(req, res, ctx) {
66675
66933
  return true;
66676
66934
  }
66677
66935
  const requestedThread = typeof body.threadId === "string" ? body.threadId : null;
66936
+ if (cloud && ownerUserId == null) {
66937
+ sendJson3(res, { error: "Could not resolve your cloud identity; chat is disabled." }, 500);
66938
+ return true;
66939
+ }
66678
66940
  const activeContext = parseActiveContext(body.activeContext, ctx.validTables);
66679
- const history = await rehydrateHistory(ctx.db, requestedThread, mapHistory(body.history), null);
66941
+ const history = await rehydrateHistory(
66942
+ ctx.db,
66943
+ requestedThread,
66944
+ mapHistory(body.history),
66945
+ ownerUserId
66946
+ );
66680
66947
  let threadId = "";
66681
66948
  try {
66682
- threadId = await ensureThread(ctx.db, requestedThread, message, null);
66683
- await persistMessage(ctx.db, threadId, "user", message, null);
66949
+ threadId = await ensureThread(ctx.db, requestedThread, message, ownerUserId);
66950
+ await persistMessage(ctx.db, threadId, "user", message, ownerUserId);
66684
66951
  } catch (e6) {
66685
66952
  console.warn("[chat] persist user message failed:", e6.message);
66686
66953
  }
@@ -66752,7 +67019,7 @@ async function dispatchChatRoute(req, res, ctx) {
66752
67019
  threadId,
66753
67020
  "assistant",
66754
67021
  assistantText,
66755
- null,
67022
+ ownerUserId,
66756
67023
  cleanTurns,
66757
67024
  turnStartedAt,
66758
67025
  assistantMsgId
@@ -67596,10 +67863,13 @@ function resolveOutputDirForConfig(configPath) {
67596
67863
  async function openConfig(configPath, outputDir, autoRender = false, realtimeWatchdogMs) {
67597
67864
  healRawDbUrl(configPath);
67598
67865
  const parsed = parseConfigFile(configPath);
67866
+ const encryptionKey = getOrCreateMasterKey();
67599
67867
  if (!/^postgres(ql)?:\/\//i.test(parsed.dbPath) && !parsed.dbPath.startsWith("file:") && parsed.dbPath !== ":memory:") {
67600
67868
  (0, import_node_fs35.mkdirSync)((0, import_node_path38.dirname)(parsed.dbPath), { recursive: true });
67601
67869
  }
67602
- const encryptionKey = getOrCreateMasterKey();
67870
+ if (/^postgres(ql)?:\/\//i.test(parsed.dbPath)) {
67871
+ await hydrateMemberConfigFromCloud(configPath, parsed.dbPath, encryptionKey);
67872
+ }
67603
67873
  const db = new Lattice({ config: configPath }, { encryptionKey });
67604
67874
  registerNativeEntities(db);
67605
67875
  db.define("_lattice_gui_meta", {
@@ -67751,19 +68021,32 @@ async function openConfig(configPath, outputDir, autoRender = false, realtimeWat
67751
68021
  if (await cloudRlsInstalled(db) && await canManageRoles(db)) {
67752
68022
  await registerPostgresPolyfills((sql) => runAsyncOrSync(db.adapter, sql));
67753
68023
  await installCloudRls(db);
68024
+ await ownPolyfillsByGroup(db);
67754
68025
  await installCloudSettings(db);
67755
68026
  await db.ensureObservationSubstrate();
67756
68027
  await enableChangelogRls(db);
68028
+ await enableChatPrivacyRls(db);
68029
+ await enableGuiAuditRls(db);
67757
68030
  const access = await reconcileCloudMemberAccess(db);
67758
68031
  convergeWarnings = access.skipped;
67759
68032
  for (const s2 of convergeWarnings) {
67760
68033
  console.warn(`[openConfig] cloud converge could not manage "${s2.table}": ${s2.reason}`);
67761
68034
  }
68035
+ await publishSharedSchema(db, configPath);
67762
68036
  }
67763
68037
  } catch (e6) {
67764
68038
  console.error("[openConfig] cloud bootstrap converge failed:", e6.message);
67765
68039
  }
67766
68040
  }
68041
+ if (memberOpen) {
68042
+ const userTables = db.getRegisteredTableNames().filter((t8) => !t8.startsWith("_") && !discoveredJunctions.has(t8));
68043
+ if (userTables.length === 0) {
68044
+ convergeWarnings.push({
68045
+ table: "(schema)",
68046
+ reason: "No entity layout is configured for this cloud workspace yet \u2014 ask the cloud owner to open the workspace once so it publishes the schema, then reopen. Until then, render produces no context files."
68047
+ });
68048
+ }
68049
+ }
67767
68050
  const validTables = new Set(parsed.tables.map((t8) => t8.name));
67768
68051
  for (const name of db.getRegisteredTableNames()) {
67769
68052
  if (name.startsWith("__lattice_") || name.startsWith("_lattice_")) continue;
@@ -67915,11 +68198,22 @@ function startBackgroundRender(active) {
67915
68198
  active.eagerRenderWired = true;
67916
68199
  let lastFire = 0;
67917
68200
  let trailing;
68201
+ const pendingTables = /* @__PURE__ */ new Set();
68202
+ let pendingFull = false;
67918
68203
  const fire = () => {
67919
68204
  lastFire = Date.now();
67920
- active.db.requestRender();
68205
+ if (pendingFull || pendingTables.size === 0) {
68206
+ pendingFull = false;
68207
+ pendingTables.clear();
68208
+ active.db.requestRender();
68209
+ return;
68210
+ }
68211
+ for (const t8 of pendingTables) active.db.requestRender(t8);
68212
+ pendingTables.clear();
67921
68213
  };
67922
- active.realtime.subscribePayload(() => {
68214
+ active.realtime.subscribePayload((payload) => {
68215
+ if (payload.table_name) pendingTables.add(payload.table_name);
68216
+ else pendingFull = true;
67923
68217
  const since = Date.now() - lastFire;
67924
68218
  if (since >= EAGER_RERENDER_MIN_INTERVAL_MS) {
67925
68219
  fire();