@happyvertical/smrt-core 0.45.2 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/AGENTS.md +1 -0
  2. package/agents/generators.md +0 -1
  3. package/dist/browser.d.ts +2 -1
  4. package/dist/browser.d.ts.map +1 -1
  5. package/dist/embedded-write-queue.d.ts.map +1 -1
  6. package/dist/embedded-write-queue.js +20 -1
  7. package/dist/embedded-write-queue.js.map +1 -1
  8. package/dist/generators/custom-action.d.ts +71 -39
  9. package/dist/generators/custom-action.d.ts.map +1 -1
  10. package/dist/generators/custom-action.js +71 -39
  11. package/dist/generators/custom-action.js.map +1 -1
  12. package/dist/generators/index.d.ts +0 -2
  13. package/dist/generators/index.d.ts.map +1 -1
  14. package/dist/generators/index.js +2 -3
  15. package/dist/generators/mcp.d.ts.map +1 -1
  16. package/dist/generators/mcp.js +1 -1
  17. package/dist/generators/mcp.js.map +1 -1
  18. package/dist/generators/tenant-gate.d.ts +33 -9
  19. package/dist/generators/tenant-gate.d.ts.map +1 -1
  20. package/dist/generators/tenant-gate.js +2 -1
  21. package/dist/generators/tenant-gate.js.map +1 -1
  22. package/dist/generators.js +2 -3
  23. package/dist/index.d.ts +3 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +3 -3
  26. package/dist/knowledge.js +28 -16
  27. package/dist/knowledge.js.map +1 -1
  28. package/dist/manifest/static-manifest.js +1 -1
  29. package/dist/manifest/static-manifest.js.map +1 -1
  30. package/dist/manifest/store.js +1 -1
  31. package/dist/manifest.json +1 -1
  32. package/dist/migrations/framework-base-tables.d.ts +148 -0
  33. package/dist/migrations/framework-base-tables.d.ts.map +1 -0
  34. package/dist/migrations/framework-base-tables.js +419 -0
  35. package/dist/migrations/framework-base-tables.js.map +1 -0
  36. package/dist/migrations/index.d.ts +1 -0
  37. package/dist/migrations/index.d.ts.map +1 -1
  38. package/dist/migrations/index.js +2 -1
  39. package/dist/migrations.js +2 -1
  40. package/dist/postgres-permissions.d.ts +40 -0
  41. package/dist/postgres-permissions.d.ts.map +1 -0
  42. package/dist/postgres-permissions.js +353 -0
  43. package/dist/postgres-permissions.js.map +1 -0
  44. package/dist/registry/framework-base-classes.d.ts.map +1 -1
  45. package/dist/registry/framework-base-classes.js +4 -1
  46. package/dist/registry/framework-base-classes.js.map +1 -1
  47. package/dist/scripts/create-wrappers.js +0 -5
  48. package/dist/smrt-knowledge.json +7 -8
  49. package/dist/testing/database.d.ts +6 -1
  50. package/dist/testing/database.d.ts.map +1 -1
  51. package/dist/testing/database.js +9 -1
  52. package/dist/testing/database.js.map +1 -1
  53. package/dist/vite-plugin/api-client-entries.d.ts.map +1 -1
  54. package/dist/vite-plugin/api-client-entries.js +16 -14
  55. package/dist/vite-plugin/api-client-entries.js.map +1 -1
  56. package/dist/vite-plugin/index.d.ts.map +1 -1
  57. package/dist/vite-plugin/index.js +2 -166
  58. package/dist/vite-plugin/index.js.map +1 -1
  59. package/dist/vite-plugin/sveltekit-generator.d.ts +3 -2
  60. package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
  61. package/dist/vite-plugin/sveltekit-generator.js +20 -24
  62. package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
  63. package/dist/vite-plugin/templates/default-ui.ts +22 -1
  64. package/dist/vite-plugin/web-collections.d.ts.map +1 -1
  65. package/dist/vite-plugin/web-collections.js.map +1 -1
  66. package/package.json +4 -9
  67. package/dist/generators/cli.d.ts +0 -155
  68. package/dist/generators/cli.d.ts.map +0 -1
  69. package/dist/generators/cli.js +0 -473
  70. package/dist/generators/cli.js.map +0 -1
package/AGENTS.md CHANGED
@@ -14,6 +14,7 @@ and repository rules.
14
14
  | `src/collection.ts` | Limits, sort whitelist, generated list order | [agents/query-bounds.md](agents/query-bounds.md) |
15
15
  | `src/data-query.ts` | Transport-neutral bounded query normalization | [agents/data-query.md](agents/data-query.md) |
16
16
  | `src/schema/`, `src/migrations/`, `src/cascade.ts`, `src/system/` | DDL parity, indexes, migrations, delete integrity, retention | [agents/schema-paths.md](agents/schema-paths.md) |
17
+ | `src/postgres-permissions.ts` | Explicit PostgreSQL ACL plans and atomic reconciliation | [deployment permission contract](../../docs/content/postgres-permissions.md) |
17
18
  | `src/change-feed.ts` | Durable changes, cursors, table versions, retention | [agents/change-feed.md](agents/change-feed.md) |
18
19
  | `src/change-signals.ts` | Signal bus, replica fan-out, SSE | [agents/change-signals.md](agents/change-signals.md) |
19
20
  | `src/generators/`, `src/vite-plugin/web-collections.ts` | REST/CLI/MCP generation, manifest hashes, ETags | [agents/generators.md](agents/generators.md) |
@@ -9,7 +9,6 @@ invariants, and the traps that apply before editing anything live in
9
9
  | Generator | Location | Output |
10
10
  |-----------|----------|--------|
11
11
  | REST API | `src/generators/rest.ts` | OpenAPI-compliant CRUD endpoints |
12
- | CLI | `src/generators/cli.ts` | `objectname:action` admin commands — writable allowlist, exhaustive-include, `--from-file`, fail-closed tenant context |
13
12
  | MCP Server | `src/generators/mcp.ts` | Model Context Protocol tools |
14
13
  | Web collections | `src/vite-plugin/web-collections.ts` (selectors) + `generateWebModule` | `@happyvertical/smrt-virt-web` — one typed collection definition per API-exposed REST collection (#1761), consumed by `@happyvertical/smrt-web` |
15
14
 
package/dist/browser.d.ts CHANGED
@@ -6,7 +6,8 @@
6
6
  * builds, federation, and client-side applications.
7
7
  *
8
8
  * Excluded:
9
- * - Code generators (CLIGenerator, APIGenerator, MCPGenerator)
9
+ * - Code generators (APIGenerator, MCPGenerator; the local CLI transport
10
+ * lives outside core in packages/cli/src/cli-generator.ts)
10
11
  * - Runtime manifest utilities (Node.js-only)
11
12
  * - Prebuild utilities (Node.js-only)
12
13
  *
@@ -1 +1 @@
1
- {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,cAAc,kBAAkB,CAAC;AAEjC,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,YAAY,EACV,aAAa,EACb,kBAAkB,EAClB,aAAa,EACb,YAAY,GACb,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAOlC,OAAO,EACL,qBAAqB,EACrB,sBAAsB,EACtB,KAAK,2BAA2B,EAChC,KAAK,iBAAiB,EACtB,yBAAyB,EACzB,4BAA4B,EAC5B,uBAAuB,EACvB,wBAAwB,EACxB,sBAAsB,GACvB,MAAM,aAAa,CAAC;AACrB,cAAc,UAAU,CAAC;AACzB,OAAO,EAAE,KAAK,aAAa,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACtE,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,YAAY,GACb,MAAM,YAAY,CAAC;AACpB,cAAc,UAAU,CAAC;AACzB,OAAO,EACL,0BAA0B,EAC1B,KAAK,iCAAiC,GACvC,MAAM,2BAA2B,CAAC;AACnC,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,IAAI,IAAI,YAAY,EAAE,MAAM,YAAY,CAAC;AAElD,cAAc,iBAAiB,CAAC;AAEhC,cAAc,gBAAgB,CAAC;AAC/B,YAAY,EACV,iBAAiB,EACjB,aAAa,EACb,kBAAkB,EAClB,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,aAAa,EACb,iBAAiB,GAClB,MAAM,gBAAgB,CAAC;AAExB,cAAc,eAAe,CAAC"}
1
+ {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,cAAc,kBAAkB,CAAC;AAEjC,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,YAAY,EACV,aAAa,EACb,kBAAkB,EAClB,aAAa,EACb,YAAY,GACb,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAOlC,OAAO,EACL,qBAAqB,EACrB,sBAAsB,EACtB,KAAK,2BAA2B,EAChC,KAAK,iBAAiB,EACtB,yBAAyB,EACzB,4BAA4B,EAC5B,uBAAuB,EACvB,wBAAwB,EACxB,sBAAsB,GACvB,MAAM,aAAa,CAAC;AACrB,cAAc,UAAU,CAAC;AACzB,OAAO,EAAE,KAAK,aAAa,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACtE,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,YAAY,GACb,MAAM,YAAY,CAAC;AACpB,cAAc,UAAU,CAAC;AACzB,OAAO,EACL,0BAA0B,EAC1B,KAAK,iCAAiC,GACvC,MAAM,2BAA2B,CAAC;AACnC,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,IAAI,IAAI,YAAY,EAAE,MAAM,YAAY,CAAC;AAElD,cAAc,iBAAiB,CAAC;AAEhC,cAAc,gBAAgB,CAAC;AAC/B,YAAY,EACV,iBAAiB,EACjB,aAAa,EACb,kBAAkB,EAClB,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,aAAa,EACb,iBAAiB,GAClB,MAAM,gBAAgB,CAAC;AAExB,cAAc,eAAe,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"embedded-write-queue.d.ts","sourceRoot":"","sources":["../src/embedded-write-queue.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AA0C5D,UAAU,iBAAiB;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,iBAAiB,GAAG,OAAO,CAEjE;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,iBAAiB,GAAG,OAAO,CAEjE;AAED;;;;;;;GAOG;AACH,wBAAgB,4BAA4B,CAAC,EAAE,EAAE,iBAAiB,GAAG,OAAO,CAG3E;AAMD;;;GAGG;AACH,wBAAsB,sBAAsB,CAAC,CAAC,EAC5C,EAAE,EAAE,iBAAiB,EACrB,SAAS,EAAE,OAAO,EAClB,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAC1B,OAAO,CAAC,CAAC,CAAC,CAwBZ;AAED;;;;;GAKG;AACH,wBAAsB,4BAA4B,CAAC,CAAC,EAClD,EAAE,EAAE,iBAAiB,EACrB,SAAS,EAAE,OAAO,EAClB,SAAS,EAAE,CAAC,WAAW,EAAE,iBAAiB,KAAK,OAAO,CAAC,CAAC,CAAC,GACxD,OAAO,CAAC,CAAC,CAAC,CAeZ"}
1
+ {"version":3,"file":"embedded-write-queue.d.ts","sourceRoot":"","sources":["../src/embedded-write-queue.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AA0C5D,UAAU,iBAAiB;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,iBAAiB,GAAG,OAAO,CAEjE;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,iBAAiB,GAAG,OAAO,CAEjE;AAED;;;;;;;GAOG;AACH,wBAAgB,4BAA4B,CAAC,EAAE,EAAE,iBAAiB,GAAG,OAAO,CAG3E;AA0BD;;;GAGG;AACH,wBAAsB,sBAAsB,CAAC,CAAC,EAC5C,EAAE,EAAE,iBAAiB,EACrB,SAAS,EAAE,OAAO,EAClB,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAC1B,OAAO,CAAC,CAAC,CAAC,CAwBZ;AAED;;;;;GAKG;AACH,wBAAsB,4BAA4B,CAAC,CAAC,EAClD,EAAE,EAAE,iBAAiB,EACrB,SAAS,EAAE,OAAO,EAClB,SAAS,EAAE,CAAC,WAAW,EAAE,iBAAiB,KAAK,OAAO,CAAC,CAAC,CAAC,GACxD,OAAO,CAAC,CAAC,CAAC,CAeZ"}
@@ -69,8 +69,27 @@ function usesEmbeddedRevisionFallback(db) {
69
69
  const url = db?.url ?? "";
70
70
  return isEmbeddedDatabase(db) && !/^(?:https?|libsql):\/\//iu.test(url);
71
71
  }
72
+ /**
73
+ * Whether `url` identifies a SQLite/DuckDB in-memory database that is
74
+ * private to its own connection, as opposed to shared state.
75
+ *
76
+ * Bare `:memory:` and `file::memory:` (no query string) each mint a fresh,
77
+ * connection-private database -- `resolveDuckDBUrl(':memory:')` returns it
78
+ * unchanged, with no per-instance suffix -- so two independent handles that
79
+ * both happen to carry this literal string are NOT the same database and
80
+ * must not share a write-queue identity (#2707). A `cache=shared` query
81
+ * parameter (e.g. `file::memory:?cache=shared`) is the opposite case: it is
82
+ * SQLite's own mechanism for making an in-memory database genuinely shared
83
+ * across connections in the process, so a URL carrying it must keep the
84
+ * ordinary string-keyed (shared) behavior.
85
+ */
86
+ function isPerConnectionMemoryUrl(url) {
87
+ if (url.includes("cache=shared")) return false;
88
+ return url === ":memory:" || url === "file::memory:";
89
+ }
72
90
  function queueKey(db) {
73
- return typeof db?.url === "string" && db.url.length > 0 ? db.url : db;
91
+ if (typeof db?.url !== "string" || db.url.length === 0) return db;
92
+ return isPerConnectionMemoryUrl(db.url) ? db : db.url;
74
93
  }
75
94
  /**
76
95
  * Run `operation` after every previously queued write on the same database
@@ -1 +1 @@
1
- {"version":3,"file":"embedded-write-queue.js","names":[],"sources":["../src/embedded-write-queue.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks';\nimport type { DatabaseInterface } from '@happyvertical/sql';\n\n/**\n * In-process write serialization for embedded database engines (#2360).\n *\n * File-backed SQLite (libsql) allows one writer at a time and the SDK opens a\n * NEW connection for every null-aware upsert attempt (`transaction(\"write\")`),\n * with no busy timeout on either connection. Any other write on the root\n * connection while such a transaction is open — most commonly the change-feed\n * append that follows every model save — collides as `SQLITE_BUSY`, and under\n * sustained concurrency (a `Promise.all` of creates) the two sides livelock\n * through every retry tier. Before the tenant-led conflict target (#2360) the\n * null-aware path was rare (custom conflict keys with NULL values, #1246);\n * with `tenant_id` leading every tenant-scoped default key, EVERY NULL-tenant\n * (global) create takes it, so the collision became the steady state.\n *\n * The queue makes the process a single writer per embedded database for:\n *\n * - a `save()` whose upsert conflict values contain NULL (the only shape the\n * SDK routes through a second-connection transaction), and\n * - the change-feed append (a root-connection write that follows every save).\n * - every model save and delete, so the embedded revision-CAS compare/upsert\n * fallback cannot race an ordinary save, natural-key create, or delete.\n * - a complete root `SmrtObject.withTransaction()` callback, with writes on\n * its bound handle re-entering the same hold instead of self-deadlocking.\n *\n * Ordinary save/delete holds contain database calls only — never an\n * interceptor or hook. Explicit transaction callbacks are caller-controlled,\n * so their bound model writes re-enter the existing hold. PostgreSQL is never\n * serialized here:\n * its null-aware upsert already arbitrates through advisory locks\n * server-side, and cross-process writers exist that an in-process queue could\n * not order anyway (which is also why this queue is a mitigation for embedded\n * engines' in-process writers, not a cross-process guarantee — embedded\n * engines have no cross-process writers in supported deployments).\n */\n\n/** Queue tail per database identity (URL string, or the handle itself). */\nconst urlQueues = new Map<string, Promise<unknown>>();\nconst handleQueues = new WeakMap<object, Promise<unknown>>();\nconst activeQueueKeys = new AsyncLocalStorage<ReadonlySet<string | object>>();\n\ninterface QueueableDatabase {\n url?: string;\n}\n\n/**\n * Whether `db` is an embedded engine (SQLite/DuckDB/JSON — anything that is\n * not PostgreSQL). Handles without a URL are treated as embedded: serializing\n * them is at worst a throughput cost, while not serializing an embedded\n * handle reintroduces the livelock.\n */\nexport function isEmbeddedDatabase(db: QueueableDatabase): boolean {\n return !isPostgresDatabase(db);\n}\n\n/**\n * Whether `db` is a PostgreSQL handle.\n *\n * The revision compare-and-swap predicate is dialect-specific (#2620): only\n * PostgreSQL needs the millisecond-truncating, timezone-tolerant guard, and\n * only PostgreSQL can evaluate `date_trunc`.\n */\nexport function isPostgresDatabase(db: QueueableDatabase): boolean {\n return /^postgres(?:ql)?:/iu.test(db?.url ?? '');\n}\n\n/**\n * Whether revision compare-and-swap needs the process-local read/upsert path.\n *\n * DuckDB/JSON cannot express the timestamp predicate through the generic\n * update API, and local SQLite has only the process's writers in supported\n * deployments. Remote LibSQL/Turso is different: other processes may write\n * the same database, so it must use one conditional UPDATE at the server.\n */\nexport function usesEmbeddedRevisionFallback(db: QueueableDatabase): boolean {\n const url = db?.url ?? '';\n return isEmbeddedDatabase(db) && !/^(?:https?|libsql):\\/\\//iu.test(url);\n}\n\nfunction queueKey(db: QueueableDatabase): string | object {\n return typeof db?.url === 'string' && db.url.length > 0 ? db.url : db;\n}\n\n/**\n * Run `operation` after every previously queued write on the same database\n * has settled. When `serialize` is false the operation runs immediately.\n */\nexport async function withEmbeddedWriteQueue<T>(\n db: QueueableDatabase,\n serialize: boolean,\n operation: () => Promise<T>,\n): Promise<T> {\n if (!serialize) return operation();\n\n const key = queueKey(db);\n const active = activeQueueKeys.getStore();\n if (active?.has(key)) return operation();\n const previous =\n (typeof key === 'string' ? urlQueues.get(key) : handleQueues.get(key)) ??\n Promise.resolve();\n const run = () =>\n activeQueueKeys.run(new Set([...(active ?? []), key]), operation);\n const next = previous.then(run, run);\n // The stored tail must never reject, or one failed write would poison the\n // queue for every later writer.\n const tail = next.catch(() => undefined);\n if (typeof key === 'string') {\n urlQueues.set(key, tail);\n void tail.finally(() => {\n if (urlQueues.get(key) === tail) urlQueues.delete(key);\n });\n } else {\n handleQueues.set(key, tail);\n }\n return next;\n}\n\n/**\n * Serialize a complete embedded root transaction with ordinary model writes.\n * The transaction handle is registered as a reentrant alias so saves and\n * change-feed appends inside the callback do not queue behind their own root\n * transaction.\n */\nexport async function withEmbeddedWriteTransaction<T>(\n db: DatabaseInterface,\n serialize: boolean,\n operation: (transaction: DatabaseInterface) => Promise<T>,\n): Promise<T> {\n const transaction = db.transaction?.bind(db);\n if (!transaction) {\n throw new Error('Database transaction support is required');\n }\n return withEmbeddedWriteQueue(db, serialize, () =>\n transaction((bound) => {\n if (!serialize) return operation(bound);\n const active = activeQueueKeys.getStore();\n return activeQueueKeys.run(\n new Set([...(active ?? []), queueKey(bound)]),\n () => operation(bound),\n );\n }),\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,IAAM,4BAAY,IAAI,IAA8B;AACpD,IAAM,+BAAe,IAAI,QAAkC;AAC3D,IAAM,kBAAkB,IAAI,kBAAgD;;;;;;;AAY5E,SAAgB,mBAAmB,IAAgC;CACjE,OAAO,CAAC,mBAAmB,EAAE;AAC/B;;;;;;;;AASA,SAAgB,mBAAmB,IAAgC;CACjE,OAAO,sBAAsB,KAAK,IAAI,OAAO,EAAE;AACjD;;;;;;;;;AAUA,SAAgB,6BAA6B,IAAgC;CAC3E,MAAM,MAAM,IAAI,OAAO;CACvB,OAAO,mBAAmB,EAAE,KAAK,CAAC,4BAA4B,KAAK,GAAG;AACxE;AAEA,SAAS,SAAS,IAAwC;CACxD,OAAO,OAAO,IAAI,QAAQ,YAAY,GAAG,IAAI,SAAS,IAAI,GAAG,MAAM;AACrE;;;;;AAMA,eAAsB,uBACpB,IACA,WACA,WACY;CACZ,IAAI,CAAC,WAAW,OAAO,UAAU;CAEjC,MAAM,MAAM,SAAS,EAAE;CACvB,MAAM,SAAS,gBAAgB,SAAS;CACxC,IAAI,QAAQ,IAAI,GAAG,GAAG,OAAO,UAAU;CACvC,MAAM,YACH,OAAO,QAAQ,WAAW,UAAU,IAAI,GAAG,IAAI,aAAa,IAAI,GAAG,MACpE,QAAQ,QAAQ;CAClB,MAAM,YACJ,gBAAgB,oBAAI,IAAI,IAAI,CAAC,GAAI,UAAU,CAAC,GAAI,GAAG,CAAC,GAAG,SAAS;CAClE,MAAM,OAAO,SAAS,KAAK,KAAK,GAAG;CAGnC,MAAM,OAAO,KAAK,YAAY,KAAA,CAAS;CACvC,IAAI,OAAO,QAAQ,UAAU;EAC3B,UAAU,IAAI,KAAK,IAAI;EACvB,KAAU,cAAc;GACtB,IAAI,UAAU,IAAI,GAAG,MAAM,MAAM,UAAU,OAAO,GAAG;EACvD,CAAC;CACH,OACE,aAAa,IAAI,KAAK,IAAI;CAE5B,OAAO;AACT;;;;;;;AAQA,eAAsB,6BACpB,IACA,WACA,WACY;CACZ,MAAM,cAAc,GAAG,aAAa,KAAK,EAAE;CAC3C,IAAI,CAAC,aACH,MAAM,IAAI,MAAM,0CAA0C;CAE5D,OAAO,uBAAuB,IAAI,iBAChC,aAAa,UAAU;EACrB,IAAI,CAAC,WAAW,OAAO,UAAU,KAAK;EACtC,MAAM,SAAS,gBAAgB,SAAS;EACxC,OAAO,gBAAgB,oBACrB,IAAI,IAAI,CAAC,GAAI,UAAU,CAAC,GAAI,SAAS,KAAK,CAAC,CAAC,SACtC,UAAU,KAAK,CACvB;CACF,CAAC,CACH;AACF"}
1
+ {"version":3,"file":"embedded-write-queue.js","names":[],"sources":["../src/embedded-write-queue.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks';\nimport type { DatabaseInterface } from '@happyvertical/sql';\n\n/**\n * In-process write serialization for embedded database engines (#2360).\n *\n * File-backed SQLite (libsql) allows one writer at a time and the SDK opens a\n * NEW connection for every null-aware upsert attempt (`transaction(\"write\")`),\n * with no busy timeout on either connection. Any other write on the root\n * connection while such a transaction is open — most commonly the change-feed\n * append that follows every model save — collides as `SQLITE_BUSY`, and under\n * sustained concurrency (a `Promise.all` of creates) the two sides livelock\n * through every retry tier. Before the tenant-led conflict target (#2360) the\n * null-aware path was rare (custom conflict keys with NULL values, #1246);\n * with `tenant_id` leading every tenant-scoped default key, EVERY NULL-tenant\n * (global) create takes it, so the collision became the steady state.\n *\n * The queue makes the process a single writer per embedded database for:\n *\n * - a `save()` whose upsert conflict values contain NULL (the only shape the\n * SDK routes through a second-connection transaction), and\n * - the change-feed append (a root-connection write that follows every save).\n * - every model save and delete, so the embedded revision-CAS compare/upsert\n * fallback cannot race an ordinary save, natural-key create, or delete.\n * - a complete root `SmrtObject.withTransaction()` callback, with writes on\n * its bound handle re-entering the same hold instead of self-deadlocking.\n *\n * Ordinary save/delete holds contain database calls only — never an\n * interceptor or hook. Explicit transaction callbacks are caller-controlled,\n * so their bound model writes re-enter the existing hold. PostgreSQL is never\n * serialized here:\n * its null-aware upsert already arbitrates through advisory locks\n * server-side, and cross-process writers exist that an in-process queue could\n * not order anyway (which is also why this queue is a mitigation for embedded\n * engines' in-process writers, not a cross-process guarantee — embedded\n * engines have no cross-process writers in supported deployments).\n */\n\n/** Queue tail per database identity (URL string, or the handle itself). */\nconst urlQueues = new Map<string, Promise<unknown>>();\nconst handleQueues = new WeakMap<object, Promise<unknown>>();\nconst activeQueueKeys = new AsyncLocalStorage<ReadonlySet<string | object>>();\n\ninterface QueueableDatabase {\n url?: string;\n}\n\n/**\n * Whether `db` is an embedded engine (SQLite/DuckDB/JSON — anything that is\n * not PostgreSQL). Handles without a URL are treated as embedded: serializing\n * them is at worst a throughput cost, while not serializing an embedded\n * handle reintroduces the livelock.\n */\nexport function isEmbeddedDatabase(db: QueueableDatabase): boolean {\n return !isPostgresDatabase(db);\n}\n\n/**\n * Whether `db` is a PostgreSQL handle.\n *\n * The revision compare-and-swap predicate is dialect-specific (#2620): only\n * PostgreSQL needs the millisecond-truncating, timezone-tolerant guard, and\n * only PostgreSQL can evaluate `date_trunc`.\n */\nexport function isPostgresDatabase(db: QueueableDatabase): boolean {\n return /^postgres(?:ql)?:/iu.test(db?.url ?? '');\n}\n\n/**\n * Whether revision compare-and-swap needs the process-local read/upsert path.\n *\n * DuckDB/JSON cannot express the timestamp predicate through the generic\n * update API, and local SQLite has only the process's writers in supported\n * deployments. Remote LibSQL/Turso is different: other processes may write\n * the same database, so it must use one conditional UPDATE at the server.\n */\nexport function usesEmbeddedRevisionFallback(db: QueueableDatabase): boolean {\n const url = db?.url ?? '';\n return isEmbeddedDatabase(db) && !/^(?:https?|libsql):\\/\\//iu.test(url);\n}\n\n/**\n * Whether `url` identifies a SQLite/DuckDB in-memory database that is\n * private to its own connection, as opposed to shared state.\n *\n * Bare `:memory:` and `file::memory:` (no query string) each mint a fresh,\n * connection-private database -- `resolveDuckDBUrl(':memory:')` returns it\n * unchanged, with no per-instance suffix -- so two independent handles that\n * both happen to carry this literal string are NOT the same database and\n * must not share a write-queue identity (#2707). A `cache=shared` query\n * parameter (e.g. `file::memory:?cache=shared`) is the opposite case: it is\n * SQLite's own mechanism for making an in-memory database genuinely shared\n * across connections in the process, so a URL carrying it must keep the\n * ordinary string-keyed (shared) behavior.\n */\nfunction isPerConnectionMemoryUrl(url: string): boolean {\n if (url.includes('cache=shared')) return false;\n return url === ':memory:' || url === 'file::memory:';\n}\n\nfunction queueKey(db: QueueableDatabase): string | object {\n if (typeof db?.url !== 'string' || db.url.length === 0) return db;\n return isPerConnectionMemoryUrl(db.url) ? db : db.url;\n}\n\n/**\n * Run `operation` after every previously queued write on the same database\n * has settled. When `serialize` is false the operation runs immediately.\n */\nexport async function withEmbeddedWriteQueue<T>(\n db: QueueableDatabase,\n serialize: boolean,\n operation: () => Promise<T>,\n): Promise<T> {\n if (!serialize) return operation();\n\n const key = queueKey(db);\n const active = activeQueueKeys.getStore();\n if (active?.has(key)) return operation();\n const previous =\n (typeof key === 'string' ? urlQueues.get(key) : handleQueues.get(key)) ??\n Promise.resolve();\n const run = () =>\n activeQueueKeys.run(new Set([...(active ?? []), key]), operation);\n const next = previous.then(run, run);\n // The stored tail must never reject, or one failed write would poison the\n // queue for every later writer.\n const tail = next.catch(() => undefined);\n if (typeof key === 'string') {\n urlQueues.set(key, tail);\n void tail.finally(() => {\n if (urlQueues.get(key) === tail) urlQueues.delete(key);\n });\n } else {\n handleQueues.set(key, tail);\n }\n return next;\n}\n\n/**\n * Serialize a complete embedded root transaction with ordinary model writes.\n * The transaction handle is registered as a reentrant alias so saves and\n * change-feed appends inside the callback do not queue behind their own root\n * transaction.\n */\nexport async function withEmbeddedWriteTransaction<T>(\n db: DatabaseInterface,\n serialize: boolean,\n operation: (transaction: DatabaseInterface) => Promise<T>,\n): Promise<T> {\n const transaction = db.transaction?.bind(db);\n if (!transaction) {\n throw new Error('Database transaction support is required');\n }\n return withEmbeddedWriteQueue(db, serialize, () =>\n transaction((bound) => {\n if (!serialize) return operation(bound);\n const active = activeQueueKeys.getStore();\n return activeQueueKeys.run(\n new Set([...(active ?? []), queueKey(bound)]),\n () => operation(bound),\n );\n }),\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,IAAM,4BAAY,IAAI,IAA8B;AACpD,IAAM,+BAAe,IAAI,QAAkC;AAC3D,IAAM,kBAAkB,IAAI,kBAAgD;;;;;;;AAY5E,SAAgB,mBAAmB,IAAgC;CACjE,OAAO,CAAC,mBAAmB,EAAE;AAC/B;;;;;;;;AASA,SAAgB,mBAAmB,IAAgC;CACjE,OAAO,sBAAsB,KAAK,IAAI,OAAO,EAAE;AACjD;;;;;;;;;AAUA,SAAgB,6BAA6B,IAAgC;CAC3E,MAAM,MAAM,IAAI,OAAO;CACvB,OAAO,mBAAmB,EAAE,KAAK,CAAC,4BAA4B,KAAK,GAAG;AACxE;;;;;;;;;;;;;;;AAgBA,SAAS,yBAAyB,KAAsB;CACtD,IAAI,IAAI,SAAS,cAAc,GAAG,OAAO;CACzC,OAAO,QAAQ,cAAc,QAAQ;AACvC;AAEA,SAAS,SAAS,IAAwC;CACxD,IAAI,OAAO,IAAI,QAAQ,YAAY,GAAG,IAAI,WAAW,GAAG,OAAO;CAC/D,OAAO,yBAAyB,GAAG,GAAG,IAAI,KAAK,GAAG;AACpD;;;;;AAMA,eAAsB,uBACpB,IACA,WACA,WACY;CACZ,IAAI,CAAC,WAAW,OAAO,UAAU;CAEjC,MAAM,MAAM,SAAS,EAAE;CACvB,MAAM,SAAS,gBAAgB,SAAS;CACxC,IAAI,QAAQ,IAAI,GAAG,GAAG,OAAO,UAAU;CACvC,MAAM,YACH,OAAO,QAAQ,WAAW,UAAU,IAAI,GAAG,IAAI,aAAa,IAAI,GAAG,MACpE,QAAQ,QAAQ;CAClB,MAAM,YACJ,gBAAgB,oBAAI,IAAI,IAAI,CAAC,GAAI,UAAU,CAAC,GAAI,GAAG,CAAC,GAAG,SAAS;CAClE,MAAM,OAAO,SAAS,KAAK,KAAK,GAAG;CAGnC,MAAM,OAAO,KAAK,YAAY,KAAA,CAAS;CACvC,IAAI,OAAO,QAAQ,UAAU;EAC3B,UAAU,IAAI,KAAK,IAAI;EACvB,KAAU,cAAc;GACtB,IAAI,UAAU,IAAI,GAAG,MAAM,MAAM,UAAU,OAAO,GAAG;EACvD,CAAC;CACH,OACE,aAAa,IAAI,KAAK,IAAI;CAE5B,OAAO;AACT;;;;;;;AAQA,eAAsB,6BACpB,IACA,WACA,WACY;CACZ,MAAM,cAAc,GAAG,aAAa,KAAK,EAAE;CAC3C,IAAI,CAAC,aACH,MAAM,IAAI,MAAM,0CAA0C;CAE5D,OAAO,uBAAuB,IAAI,iBAChC,aAAa,UAAU;EACrB,IAAI,CAAC,WAAW,OAAO,UAAU,KAAK;EACtC,MAAM,SAAS,gBAAgB,SAAS;EACxC,OAAO,gBAAgB,oBACrB,IAAI,IAAI,CAAC,GAAI,UAAU,CAAC,GAAI,SAAS,KAAK,CAAC,CAAC,SACtC,UAAU,KAAK,CACvB;CACF,CAAC,CACH;AACF"}
@@ -15,8 +15,8 @@ export type { ToolEffect } from '../registry/types.js';
15
15
  * even when a subclass declares its own override (e.g. `User.save()` at
16
16
  * `packages/users/src/models/User.ts`). An override is still the same
17
17
  * lifecycle operation, not a new one (#2638). `delete` itself is a CRUD verb
18
- * `CLIGenerator`/`MCPGenerator` already special-case, so it is not repeated
19
- * here.
18
+ * `packages/cli/src/cli-generator.ts`'s `CLIGenerator`/`MCPGenerator` already
19
+ * special-case, so it is not repeated here.
20
20
  *
21
21
  * Scope is deliberately narrower than "every public method on
22
22
  * SmrtObject/SmrtClass/SmrtCollection":
@@ -24,11 +24,13 @@ export type { ToolEffect } from '../registry/types.js';
24
24
  * - AI operations `is()`/`do()`/`describe()` are declared on `SmrtObject`
25
25
  * but are explicitly designed to be overridden with domain-specific
26
26
  * behavior and exposed as a distinct action -- confirmed by existing,
27
- * intentional coverage (`generators/cli-commands.spec.ts`'s
28
- * `describe()` custom-action fixture,
29
- * `vite-plugin/generated-client-integration.test.ts`'s `ArtCollection.
27
+ * intentional coverage
28
+ * (`vite-plugin/generated-client-integration.test.ts`'s `ArtCollection.
30
29
  * describe(tone)` with its own declared API route). Excluding them here
31
- * would regress real, working behavior.
30
+ * would regress real, working behavior. (The sibling
31
+ * `generators/cli-commands.spec.ts` fixture that used to cover the same
32
+ * `describe()` custom action was retired with core's `CLIGenerator`,
33
+ * #2664; this remaining fixture still exercises the behavior.)
32
34
  * - Relationship loading (`loadRelated`/`loadRelatedMany`/`getRelated`/
33
35
  * `isRelatedLoaded`), memory (`remember`/`recall`/`recallAll`/`forget`/
34
36
  * `forgetScope`), embeddings (`generateEmbeddings`/`getEmbedding`/
@@ -89,29 +91,28 @@ export interface ResolvableMethod {
89
91
  * for an object, given its transport config's `include`/`exclude` and its
90
92
  * method map: every public method, minus CRUD verbs, minus framework
91
93
  * lifecycle methods, restricted to `include` when present and always minus
92
- * `exclude`. Reused by three callers, so they agree with each other and
93
- * with what `listCommands()` advertises:
94
+ * `exclude`. Its sole caller as of #2664 (`CLIGenerator` and the
95
+ * `generateCLIModule()` virtual module were retired):
94
96
  *
95
- * - `CLIGenerator.listCommands()` (over the live `ObjectRegistry`).
96
97
  * - `findCliApiCoherenceViolations`'s bare-`cli: true`/`cli: {}` branch
97
98
  * (over the static manifest, no explicit `include`) -- see
98
99
  * `resolveCliActionSet` in `vite-plugin/sveltekit-generator.ts`.
99
- * - `generateCLIModule()` in `vite-plugin/index.ts`, which generates the
100
- * `smrt:cli` virtual module's static command metadata. It layers one
101
- * additional filter on top of this function's result: a leading `_` on
102
- * the method name, because the manifest's `isPublic` is unreliable there
103
- * (see that call site's own comment) and this function has no other
104
- * signal to exclude an internal-by-convention method.
105
100
  *
106
101
  * NOT the one universal resolution, and deliberately not reused by every
107
102
  * caller that resolves a CLI command set:
108
103
  *
109
- * - `CLIGenerator.assertCommandExposed()` does not call this for its custom-
110
- * method branch — it checks `isFrameworkLifecycleMethod()` directly plus
111
- * its own inline public/include/exclude logic, so it can give a distinct
112
- * error message per failure reason (unknown vs. not public vs. not
113
- * enabled vs. lifecycle method) rather than a single boolean membership
114
- * test.
104
+ * - Core's now-retired `CLIGenerator.assertCommandExposed()` (#2664) did not
105
+ * call this for its custom-method branch — it checked
106
+ * `isFrameworkLifecycleMethod()` directly plus its own inline
107
+ * public/include/exclude logic, so it could give a distinct error message
108
+ * per failure reason (unknown vs. not public vs. not enabled vs. lifecycle
109
+ * method) rather than a single boolean membership test. The shipped local
110
+ * CLI's `generateObjectCommands()` (`packages/cli/src/cli-generator.ts`)
111
+ * has no equivalent of that gate at all today — it filters on reserved-CRUD
112
+ * name collision, `isPublic`, and `exclude`/`include`, but never
113
+ * `isFrameworkLifecycleMethod()`, so a locally overridden lifecycle method
114
+ * IS a reachable command there (see `knowledge.ts`'s `configuredOperations()`
115
+ * docblock for the same caveat).
115
116
  * - `findCliApiCoherenceViolations`'s EXPLICIT-`cli.include` branch
116
117
  * deliberately bypasses this function too: an `include` entry naming a
117
118
  * typo, a getter, or a private/protected method must still surface as
@@ -121,12 +122,15 @@ export interface ResolvableMethod {
121
122
  * `resolveCliActionSet` in `vite-plugin/sveltekit-generator.ts` for the
122
123
  * full rationale; do not "simplify" that branch onto this function.
123
124
  *
124
- * `crudActionNames` stays a parameter because the callers do not agree on one
125
- * list. `CLIGenerator` now passes {@link CRUD_OPERATIONS}, which #2646 moved
126
- * into this module — so for that caller the argument is a round trip and could
127
- * be defaulted. The SvelteKit generator still passes its own
128
- * `STANDARD_API_ACTIONS` and `vite-plugin/index.ts` an inline literal, so the
129
- * parameter cannot be removed until those copies are retired (#2665).
125
+ * `crudActionNames` stays a parameter even though every caller now passes
126
+ * {@link CRUD_OPERATIONS} (#2665 retired the last of the inline copies at
127
+ * `vite-plugin/sveltekit-generator.ts`, following `CLIGenerator`'s #2646
128
+ * switch; `vite-plugin/index.ts`'s copy backed the `generateCLIModule()`
129
+ * emitter #2664 later retired, so `index.ts` no longer calls this function
130
+ * or imports `CRUD_OPERATIONS` at all). Removing the parameter is a separate,
131
+ * unevidenced call this fix does not make -- it would foreclose a caller
132
+ * that legitimately needs a different verb set, and no such need has been
133
+ * demonstrated either way.
130
134
  */
131
135
  export declare function resolveCustomActionNames(methods: Iterable<[string, ResolvableMethod]>, config: {
132
136
  include?: string[];
@@ -144,11 +148,25 @@ export declare function resolveCustomActionNames(methods: Iterable<[string, Reso
144
148
  * What a collision means differs by EMITTER, so consult the one you are
145
149
  * changing rather than assuming a single rule. The reservation lives at each
146
150
  * emission site, not here, and several emitters still keep their own inline
147
- * verb array — find them with:
151
+ * verb array — find them with a multi-line-tolerant search (#2665 turned the
152
+ * single-line form of this grep blind to a wrapped literal like
153
+ * `templates/default-ui.ts`'s `CRUD_OPERATIONS_FOR_BROWSER_TEMPLATE`):
148
154
  *
149
- * grep -rn "'update', 'delete'" packages --include='*.ts' | grep -v include:
155
+ * rg -U "'list',\s*'get',\s*'create',\s*'update',\s*'delete'" packages --type ts | grep -v include:
150
156
  *
151
- * (the `include:` filter drops decorator config, which names the same verbs)
157
+ * This is a STARTING POINT, not a closed inventory: the `include:` filter
158
+ * only drops a single-line `include: [...]` block, so a wrapped one (e.g.
159
+ * `packages/content/src/content.ts`) still surfaces, and the pattern also
160
+ * matches non-decorator uses of the same five-word literal that have nothing
161
+ * to do with this collision rule (e.g. `packages/smrt-workbench/src/
162
+ * discovery.ts`'s `CRUD_ACTIONS`, `packages/smrt-dev-mcp/.../
163
+ * introspect-project.ts`'s `DEFAULT_MCP_OPERATIONS`). Triage each hit rather
164
+ * than trusting the raw list. Within `packages/core/src/vite-plugin/`
165
+ * specifically, the emitter-relevant survivors as of #2665 are
166
+ * `generated-client.ts` and `templates/default-ui.ts` (the latter
167
+ * deliberate, value-pinned by `issue-2665-crud-verb-consolidation.spec.ts`);
168
+ * `scanner/manifest-generator.ts` and `packages/users/src/sveltekit/
169
+ * resource-list-handler.ts` are outside this PR's scope.
152
170
  *
153
171
  * The sites this rule was audited against (#2646), NOT an exhaustive
154
172
  * inventory:
@@ -158,10 +176,6 @@ export declare function resolveCustomActionNames(methods: Iterable<[string, Reso
158
176
  * built-in list whichever branch emitted it: the class's method could never
159
177
  * run, and emitting one would hand the caller an operation `include` never
160
178
  * named.
161
- * - `generators/cli.ts` (`CLIGenerator`) — unconditional, exact.
162
- * `assertCommandExposed` returns inside its `isCrud` branch, so a CRUD-named
163
- * action never reaches custom-method resolution, and `listCommands` skips
164
- * those names outright.
165
179
  * - `packages/cli/src/cli-generator.ts`, behind the shipped `smrt` object
166
180
  * commands — reserves only where the CRUD command is actually emitted, and
167
181
  * reserves the command NAMES AND THEIR ALIASES (`ls`, `show`, `new`, `edit`,
@@ -170,8 +184,9 @@ export declare function resolveCustomActionNames(methods: Iterable<[string, Reso
170
184
  * command carries its own handler invoking the class's method, so with
171
185
  * `cli: { include: ['list', 'get'] }` neither a public `create()` nor an
172
186
  * `edit()` collides with anything and both stay reachable.
173
- * - `vite-plugin/sveltekit-generator.ts` — unconditional, exact, from its own
174
- * `STANDARD_API_ACTIONS` copy.
187
+ * - `vite-plugin/sveltekit-generator.ts` — unconditional, exact, via
188
+ * {@link isCrudOperation} (#2665; previously its own `STANDARD_API_ACTIONS`
189
+ * copy).
175
190
  * - `vite-plugin/web-collections.ts` + `tool-schema.ts` — case-folded (ids are
176
191
  * lowercased whole like MCP's), CONDITIONAL, and scoped PER COLLECTION.
177
192
  * Applied in both of `selectWebMcpToolEntries`' loops and at the shared
@@ -194,9 +209,26 @@ export declare function resolveCustomActionNames(methods: Iterable<[string, Reso
194
209
  * `resolveApiActionSet` stays exact-match: REST routes keep declared casing,
195
210
  * so `/products/List` is genuinely distinct from `/products`.
196
211
  *
197
- * Still carrying their own verb copies, tracked on #2665:
198
- * `vite-plugin/api-client-entries.ts`, `vite-plugin/index.ts` and
199
- * `vite-plugin/templates/default-ui.ts`.
212
+ * `vite-plugin/api-client-entries.ts` and `vite-plugin/sveltekit-generator.ts`
213
+ * now import {@link CRUD_OPERATIONS} (or {@link isCrudOperation} where a bare
214
+ * `.includes()` on the readonly tuple failed the stricter
215
+ * `tsconfig.typecheck.json`) rather than keeping their own verb copies
216
+ * (#2665). `vite-plugin/index.ts`'s copy backed `generateCLIModule()`, which
217
+ * #2664 retired along with the rest of the unused `smrt-virt-cli` module, so
218
+ * `index.ts` no longer imports anything from this module at all.
219
+ * `vite-plugin/templates/default-ui.ts` keeps its
220
+ * verb list as a local literal deliberately: `src/vite-plugin/templates/**`
221
+ * is excluded from both tsconfigs and the vite-dts build graph, and the
222
+ * package build copies that directory to `dist/` verbatim rather than
223
+ * compiling it -- see the module's own comment. Its `.ts` source is never
224
+ * resolved or bundled by anything in this package, so a static import of the
225
+ * Node-side `isCrudOperation`/`CRUD_OPERATIONS` (which pull in
226
+ * `tools/tool-generator.js`) would never be satisfied there. The
227
+ * consolidation test instead asserts the literal's *value* matches
228
+ * {@link CRUD_OPERATIONS} rather than assuming it imports it. Consolidating
229
+ * the lists did not change any of the
230
+ * per-emitter RULES documented above -- each site still decides case-folding
231
+ * and conditionality for itself.
200
232
  *
201
233
  * Read the list through {@link isCrudOperation} or {@link isCrudToolAction}
202
234
  * rather than re-declaring it; the two differ only in case folding, because the
@@ -1 +1 @@
1
- {"version":3,"file":"custom-action.d.ts","sourceRoot":"","sources":["../../src/generators/custom-action.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"custom-action.d.ts","sourceRoot":"","sources":["../../src/generators/custom-action.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAG5D,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,YAAY,CAAC;AACtD,YAAY,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuEG;AACH,eAAO,MAAM,gCAAgC,EAAE,WAAW,CAAC,MAAM,CAuB/D,CAAC;AAEH;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAEtE;AAED,0EAA0E;AAC1E,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,QAAQ,CAAC,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,EAC7C,MAAM,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,SAAS,EAC9D,eAAe,EAAE,SAAS,MAAM,EAAE,GACjC,GAAG,CAAC,MAAM,CAAC,CAab;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiGG;AACH,eAAO,MAAM,eAAe,wDAMlB,CAAC;AAEX;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAErD;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,iBAAiB,CAAC;IACzB,8DAA8D;IAC9D,UAAU,EAAE,OAAO,CAAC;IACpB,gEAAgE;IAChE,UAAU,CAAC,EAAE,gBAAgB,CAAC,YAAY,CAAC,CAAC;IAC5C,oEAAoE;IACpE,QAAQ,EAAE,OAAO,CAAC;IAClB,sEAAsE;IACtE,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,oEAAoE;IACpE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,+EAA+E;AAC/E,MAAM,MAAM,4BAA4B,GAAG,oBAAoB,GAC7D,QAAQ,CAAC,IAAI,CAAC,oBAAoB,EAAE,QAAQ,GAAG,YAAY,GAAG,WAAW,CAAC,CAAC,CAAC;AAE9E;;;;;GAKG;AACH,wBAAgB,8BAA8B,CAC5C,SAAS,EAAE,IAAI,CAAC,oBAAoB,EAAE,YAAY,CAAC,EACnD,aAAa,EAAE,MAAM,GACpB,MAAM,CAER;AAED,MAAM,WAAW,kCAAkC;IACjD,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE;QACP,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,UAAU,CAAC,EAAE,gBAAgB,CAAC,YAAY,CAAC,CAAC;KAC7C,CAAC;IACF,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,gFAAgF;IAChF,YAAY,CAAC,EAAE,iBAAiB,CAAC;CAClC;AAED,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAC1C,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAExC;;;;GAIG;AACH,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,kCAAkC,GAC1C,4BAA4B,CAmC9B;AAED,oEAAoE;AACpE,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,oBAAoB,GAC7B,UAAU,CAuDZ;AAED;;;;GAIG;AACH,wBAAgB,+BAA+B,CAC7C,QAAQ,EAAE,oBAAoB,EAC9B,IAAI,EAAE,QAAQ,GACb,OAAO,EAAE,CAwBX;AAED;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,KAAK,CAAC;IACV,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,gFAAgF;AAChF,eAAO,MAAM,qCAAqC,0BAA0B,CAAC;AAE7E;;;;GAIG;AACH,wBAAgB,4BAA4B,CAC1C,KAAK,EAAE,OAAO,GACb,mBAAmB,GAAG,SAAS,CA6BjC"}
@@ -13,8 +13,8 @@ import { convertTypeToJsonSchema } from "../tools/tool-generator.js";
13
13
  * even when a subclass declares its own override (e.g. `User.save()` at
14
14
  * `packages/users/src/models/User.ts`). An override is still the same
15
15
  * lifecycle operation, not a new one (#2638). `delete` itself is a CRUD verb
16
- * `CLIGenerator`/`MCPGenerator` already special-case, so it is not repeated
17
- * here.
16
+ * `packages/cli/src/cli-generator.ts`'s `CLIGenerator`/`MCPGenerator` already
17
+ * special-case, so it is not repeated here.
18
18
  *
19
19
  * Scope is deliberately narrower than "every public method on
20
20
  * SmrtObject/SmrtClass/SmrtCollection":
@@ -22,11 +22,13 @@ import { convertTypeToJsonSchema } from "../tools/tool-generator.js";
22
22
  * - AI operations `is()`/`do()`/`describe()` are declared on `SmrtObject`
23
23
  * but are explicitly designed to be overridden with domain-specific
24
24
  * behavior and exposed as a distinct action -- confirmed by existing,
25
- * intentional coverage (`generators/cli-commands.spec.ts`'s
26
- * `describe()` custom-action fixture,
27
- * `vite-plugin/generated-client-integration.test.ts`'s `ArtCollection.
25
+ * intentional coverage
26
+ * (`vite-plugin/generated-client-integration.test.ts`'s `ArtCollection.
28
27
  * describe(tone)` with its own declared API route). Excluding them here
29
- * would regress real, working behavior.
28
+ * would regress real, working behavior. (The sibling
29
+ * `generators/cli-commands.spec.ts` fixture that used to cover the same
30
+ * `describe()` custom action was retired with core's `CLIGenerator`,
31
+ * #2664; this remaining fixture still exercises the behavior.)
30
32
  * - Relationship loading (`loadRelated`/`loadRelatedMany`/`getRelated`/
31
33
  * `isRelatedLoaded`), memory (`remember`/`recall`/`recallAll`/`forget`/
32
34
  * `forgetScope`), embeddings (`generateEmbeddings`/`getEmbedding`/
@@ -106,29 +108,28 @@ function isFrameworkLifecycleMethod(methodName) {
106
108
  * for an object, given its transport config's `include`/`exclude` and its
107
109
  * method map: every public method, minus CRUD verbs, minus framework
108
110
  * lifecycle methods, restricted to `include` when present and always minus
109
- * `exclude`. Reused by three callers, so they agree with each other and
110
- * with what `listCommands()` advertises:
111
+ * `exclude`. Its sole caller as of #2664 (`CLIGenerator` and the
112
+ * `generateCLIModule()` virtual module were retired):
111
113
  *
112
- * - `CLIGenerator.listCommands()` (over the live `ObjectRegistry`).
113
114
  * - `findCliApiCoherenceViolations`'s bare-`cli: true`/`cli: {}` branch
114
115
  * (over the static manifest, no explicit `include`) -- see
115
116
  * `resolveCliActionSet` in `vite-plugin/sveltekit-generator.ts`.
116
- * - `generateCLIModule()` in `vite-plugin/index.ts`, which generates the
117
- * `smrt:cli` virtual module's static command metadata. It layers one
118
- * additional filter on top of this function's result: a leading `_` on
119
- * the method name, because the manifest's `isPublic` is unreliable there
120
- * (see that call site's own comment) and this function has no other
121
- * signal to exclude an internal-by-convention method.
122
117
  *
123
118
  * NOT the one universal resolution, and deliberately not reused by every
124
119
  * caller that resolves a CLI command set:
125
120
  *
126
- * - `CLIGenerator.assertCommandExposed()` does not call this for its custom-
127
- * method branch — it checks `isFrameworkLifecycleMethod()` directly plus
128
- * its own inline public/include/exclude logic, so it can give a distinct
129
- * error message per failure reason (unknown vs. not public vs. not
130
- * enabled vs. lifecycle method) rather than a single boolean membership
131
- * test.
121
+ * - Core's now-retired `CLIGenerator.assertCommandExposed()` (#2664) did not
122
+ * call this for its custom-method branch — it checked
123
+ * `isFrameworkLifecycleMethod()` directly plus its own inline
124
+ * public/include/exclude logic, so it could give a distinct error message
125
+ * per failure reason (unknown vs. not public vs. not enabled vs. lifecycle
126
+ * method) rather than a single boolean membership test. The shipped local
127
+ * CLI's `generateObjectCommands()` (`packages/cli/src/cli-generator.ts`)
128
+ * has no equivalent of that gate at all today — it filters on reserved-CRUD
129
+ * name collision, `isPublic`, and `exclude`/`include`, but never
130
+ * `isFrameworkLifecycleMethod()`, so a locally overridden lifecycle method
131
+ * IS a reachable command there (see `knowledge.ts`'s `configuredOperations()`
132
+ * docblock for the same caveat).
132
133
  * - `findCliApiCoherenceViolations`'s EXPLICIT-`cli.include` branch
133
134
  * deliberately bypasses this function too: an `include` entry naming a
134
135
  * typo, a getter, or a private/protected method must still surface as
@@ -138,12 +139,15 @@ function isFrameworkLifecycleMethod(methodName) {
138
139
  * `resolveCliActionSet` in `vite-plugin/sveltekit-generator.ts` for the
139
140
  * full rationale; do not "simplify" that branch onto this function.
140
141
  *
141
- * `crudActionNames` stays a parameter because the callers do not agree on one
142
- * list. `CLIGenerator` now passes {@link CRUD_OPERATIONS}, which #2646 moved
143
- * into this module — so for that caller the argument is a round trip and could
144
- * be defaulted. The SvelteKit generator still passes its own
145
- * `STANDARD_API_ACTIONS` and `vite-plugin/index.ts` an inline literal, so the
146
- * parameter cannot be removed until those copies are retired (#2665).
142
+ * `crudActionNames` stays a parameter even though every caller now passes
143
+ * {@link CRUD_OPERATIONS} (#2665 retired the last of the inline copies at
144
+ * `vite-plugin/sveltekit-generator.ts`, following `CLIGenerator`'s #2646
145
+ * switch; `vite-plugin/index.ts`'s copy backed the `generateCLIModule()`
146
+ * emitter #2664 later retired, so `index.ts` no longer calls this function
147
+ * or imports `CRUD_OPERATIONS` at all). Removing the parameter is a separate,
148
+ * unevidenced call this fix does not make -- it would foreclose a caller
149
+ * that legitimately needs a different verb set, and no such need has been
150
+ * demonstrated either way.
147
151
  */
148
152
  function resolveCustomActionNames(methods, config, crudActionNames) {
149
153
  const included = config?.include;
@@ -171,11 +175,25 @@ function resolveCustomActionNames(methods, config, crudActionNames) {
171
175
  * What a collision means differs by EMITTER, so consult the one you are
172
176
  * changing rather than assuming a single rule. The reservation lives at each
173
177
  * emission site, not here, and several emitters still keep their own inline
174
- * verb array — find them with:
178
+ * verb array — find them with a multi-line-tolerant search (#2665 turned the
179
+ * single-line form of this grep blind to a wrapped literal like
180
+ * `templates/default-ui.ts`'s `CRUD_OPERATIONS_FOR_BROWSER_TEMPLATE`):
175
181
  *
176
- * grep -rn "'update', 'delete'" packages --include='*.ts' | grep -v include:
182
+ * rg -U "'list',\s*'get',\s*'create',\s*'update',\s*'delete'" packages --type ts | grep -v include:
177
183
  *
178
- * (the `include:` filter drops decorator config, which names the same verbs)
184
+ * This is a STARTING POINT, not a closed inventory: the `include:` filter
185
+ * only drops a single-line `include: [...]` block, so a wrapped one (e.g.
186
+ * `packages/content/src/content.ts`) still surfaces, and the pattern also
187
+ * matches non-decorator uses of the same five-word literal that have nothing
188
+ * to do with this collision rule (e.g. `packages/smrt-workbench/src/
189
+ * discovery.ts`'s `CRUD_ACTIONS`, `packages/smrt-dev-mcp/.../
190
+ * introspect-project.ts`'s `DEFAULT_MCP_OPERATIONS`). Triage each hit rather
191
+ * than trusting the raw list. Within `packages/core/src/vite-plugin/`
192
+ * specifically, the emitter-relevant survivors as of #2665 are
193
+ * `generated-client.ts` and `templates/default-ui.ts` (the latter
194
+ * deliberate, value-pinned by `issue-2665-crud-verb-consolidation.spec.ts`);
195
+ * `scanner/manifest-generator.ts` and `packages/users/src/sveltekit/
196
+ * resource-list-handler.ts` are outside this PR's scope.
179
197
  *
180
198
  * The sites this rule was audited against (#2646), NOT an exhaustive
181
199
  * inventory:
@@ -185,10 +203,6 @@ function resolveCustomActionNames(methods, config, crudActionNames) {
185
203
  * built-in list whichever branch emitted it: the class's method could never
186
204
  * run, and emitting one would hand the caller an operation `include` never
187
205
  * named.
188
- * - `generators/cli.ts` (`CLIGenerator`) — unconditional, exact.
189
- * `assertCommandExposed` returns inside its `isCrud` branch, so a CRUD-named
190
- * action never reaches custom-method resolution, and `listCommands` skips
191
- * those names outright.
192
206
  * - `packages/cli/src/cli-generator.ts`, behind the shipped `smrt` object
193
207
  * commands — reserves only where the CRUD command is actually emitted, and
194
208
  * reserves the command NAMES AND THEIR ALIASES (`ls`, `show`, `new`, `edit`,
@@ -197,8 +211,9 @@ function resolveCustomActionNames(methods, config, crudActionNames) {
197
211
  * command carries its own handler invoking the class's method, so with
198
212
  * `cli: { include: ['list', 'get'] }` neither a public `create()` nor an
199
213
  * `edit()` collides with anything and both stay reachable.
200
- * - `vite-plugin/sveltekit-generator.ts` — unconditional, exact, from its own
201
- * `STANDARD_API_ACTIONS` copy.
214
+ * - `vite-plugin/sveltekit-generator.ts` — unconditional, exact, via
215
+ * {@link isCrudOperation} (#2665; previously its own `STANDARD_API_ACTIONS`
216
+ * copy).
202
217
  * - `vite-plugin/web-collections.ts` + `tool-schema.ts` — case-folded (ids are
203
218
  * lowercased whole like MCP's), CONDITIONAL, and scoped PER COLLECTION.
204
219
  * Applied in both of `selectWebMcpToolEntries`' loops and at the shared
@@ -221,9 +236,26 @@ function resolveCustomActionNames(methods, config, crudActionNames) {
221
236
  * `resolveApiActionSet` stays exact-match: REST routes keep declared casing,
222
237
  * so `/products/List` is genuinely distinct from `/products`.
223
238
  *
224
- * Still carrying their own verb copies, tracked on #2665:
225
- * `vite-plugin/api-client-entries.ts`, `vite-plugin/index.ts` and
226
- * `vite-plugin/templates/default-ui.ts`.
239
+ * `vite-plugin/api-client-entries.ts` and `vite-plugin/sveltekit-generator.ts`
240
+ * now import {@link CRUD_OPERATIONS} (or {@link isCrudOperation} where a bare
241
+ * `.includes()` on the readonly tuple failed the stricter
242
+ * `tsconfig.typecheck.json`) rather than keeping their own verb copies
243
+ * (#2665). `vite-plugin/index.ts`'s copy backed `generateCLIModule()`, which
244
+ * #2664 retired along with the rest of the unused `smrt-virt-cli` module, so
245
+ * `index.ts` no longer imports anything from this module at all.
246
+ * `vite-plugin/templates/default-ui.ts` keeps its
247
+ * verb list as a local literal deliberately: `src/vite-plugin/templates/**`
248
+ * is excluded from both tsconfigs and the vite-dts build graph, and the
249
+ * package build copies that directory to `dist/` verbatim rather than
250
+ * compiling it -- see the module's own comment. Its `.ts` source is never
251
+ * resolved or bundled by anything in this package, so a static import of the
252
+ * Node-side `isCrudOperation`/`CRUD_OPERATIONS` (which pull in
253
+ * `tools/tool-generator.js`) would never be satisfied there. The
254
+ * consolidation test instead asserts the literal's *value* matches
255
+ * {@link CRUD_OPERATIONS} rather than assuming it imports it. Consolidating
256
+ * the lists did not change any of the
257
+ * per-emitter RULES documented above -- each site still decides case-folding
258
+ * and conditionality for itself.
227
259
  *
228
260
  * Read the list through {@link isCrudOperation} or {@link isCrudToolAction}
229
261
  * rather than re-declaring it; the two differ only in case folding, because the