@jskit-ai/agent-docs 0.1.283 → 0.1.284

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 (151) hide show
  1. package/guide/agent/app-extras/assistant.md +14 -2
  2. package/guide/agent/app-extras/realtime.md +38 -1
  3. package/guide/agent/app-setup/authentication.md +64 -14
  4. package/guide/agent/app-setup/console.md +8 -0
  5. package/guide/agent/app-setup/database-layer.md +23 -103
  6. package/guide/agent/app-setup/existing-application-migration.md +105 -72
  7. package/guide/agent/app-setup/multi-homing.md +94 -32
  8. package/guide/agent/app-setup/quickstart.md +1 -1
  9. package/guide/agent/app-setup/run-an-accounts-hub.md +1 -1
  10. package/guide/agent/app-setup/upgrading-jskit.md +8 -3
  11. package/guide/agent/app-setup/users.md +8 -5
  12. package/guide/agent/framework/application-operations.md +1 -1
  13. package/guide/agent/framework/crud-operations.md +63 -45
  14. package/guide/agent/framework/data-access.md +1031 -0
  15. package/guide/agent/framework/sanity-checks.md +26 -27
  16. package/guide/agent/framework/typed-features.md +114 -247
  17. package/guide/agent/index.md +2 -0
  18. package/package.json +6 -3
  19. package/patterns/feature-package/PATTERN.md +25 -17
  20. package/patterns/feature-package/example/booking-engine/package.json +1 -1
  21. package/patterns/feature-package/example/variations/InvoiceRollupFeature.js +6 -7
  22. package/patterns/feature-package/example/variations/invoiceResource.js +15 -0
  23. package/patterns/feature-package/example/variations/invoiceRollupActions.js +4 -4
  24. package/patterns/feature-package/example/variations/ownedTableRepository.js +27 -0
  25. package/patterns/feature-package/example/variations/types.js +1 -1
  26. package/patterns/minimal-foundation/example/AGENTS.md +4 -0
  27. package/patterns/minimal-foundation/example/package.json +4 -4
  28. package/patterns/minimal-foundation/example/server.js +4 -1
  29. package/patterns/shell-foundation/example/AGENTS.md +4 -0
  30. package/patterns/shell-foundation/example/package.json +5 -5
  31. package/patterns/shell-foundation/example/packages/main/package.json +1 -1
  32. package/patterns/shell-foundation/example/server.js +4 -1
  33. package/reference/autogen/KERNEL_MAP.md +2 -1
  34. package/reference/autogen/PATTERN_INDEX.md +70 -59
  35. package/reference/autogen/README.md +1 -1
  36. package/reference/autogen/packages/agent-docs.md +7 -3
  37. package/reference/autogen/packages/agent-runtime.md +48 -11
  38. package/reference/autogen/packages/architecture-inspector.md +6 -2
  39. package/reference/autogen/packages/assistant-core.md +4 -4
  40. package/reference/autogen/packages/assistant-runtime.md +33 -13
  41. package/reference/autogen/packages/auth-provider-local-core.md +3 -1
  42. package/reference/autogen/packages/auth-provider-local-db-core.md +65 -14
  43. package/reference/autogen/packages/auth-provider-oidc-core.md +5 -1
  44. package/reference/autogen/packages/auth-provider-supabase-core.md +1 -1
  45. package/reference/autogen/packages/connectors-core.md +22 -2
  46. package/reference/autogen/packages/console-core.md +9 -5
  47. package/reference/autogen/packages/crud-core.md +38 -173
  48. package/reference/autogen/packages/database-runtime.md +8 -32
  49. package/reference/autogen/packages/http-runtime.md +3 -3
  50. package/reference/autogen/packages/http-web.md +12 -0
  51. package/reference/autogen/packages/kernel.md +25 -2
  52. package/reference/autogen/packages/oidc-issuer-core.md +4 -0
  53. package/reference/autogen/packages/payments-core.md +29 -6
  54. package/reference/autogen/packages/realtime.md +15 -6
  55. package/reference/autogen/packages/resource-core.md +30 -5
  56. package/reference/autogen/packages/resource-crud-core.md +30 -34
  57. package/reference/autogen/packages/resource-runtime.md +198 -0
  58. package/reference/autogen/packages/rewarded-core.md +16 -10
  59. package/reference/autogen/packages/shell-web.md +1 -0
  60. package/reference/autogen/packages/users-core.md +26 -14
  61. package/reference/autogen/packages/workspaces-core.md +17 -10
  62. package/reference/autogen/tooling/config-eslint.md +69 -0
  63. package/reference/autogen/tooling/jskit-catalog.md +36 -1
  64. package/reference/autogen/tooling/jskit-release.md +3 -0
  65. package/skills/jskit/SKILL.md +4 -1
  66. package/skills/jskit/references/app-operations.md +1 -1
  67. package/skills/jskit/references/crud-operations.md +62 -44
  68. package/skills/jskit/references/data-access.md +1026 -0
  69. package/skills/jskit/references/database-layer.md +411 -0
  70. package/skills/jskit/references/existing-application-migration.md +105 -72
  71. package/skills/jskit/references/heuristics/INDEX.md +6 -4
  72. package/skills/jskit/references/heuristics/crud-authoring.md +22 -14
  73. package/skills/jskit/references/heuristics/data-access.md +19 -0
  74. package/skills/jskit/references/heuristics/filters.md +6 -6
  75. package/skills/jskit/references/heuristics/row-policies.md +40 -16
  76. package/skills/jskit/references/heuristics/server-search.md +30 -42
  77. package/skills/jskit/references/heuristics/table-resource-fields.md +78 -0
  78. package/skills/jskit/references/heuristics/typed-features.md +3 -3
  79. package/skills/jskit/references/multi-homing.md +94 -32
  80. package/skills/jskit/references/pattern-index.md +70 -59
  81. package/skills/jskit/references/patterns/app/minimal-foundation/example/AGENTS.md +4 -0
  82. package/skills/jskit/references/patterns/app/minimal-foundation/example/package.json +4 -4
  83. package/skills/jskit/references/patterns/app/minimal-foundation/example/server.js +4 -1
  84. package/skills/jskit/references/patterns/app/shell-foundation/example/AGENTS.md +4 -0
  85. package/skills/jskit/references/patterns/app/shell-foundation/example/package.json +5 -5
  86. package/skills/jskit/references/patterns/app/shell-foundation/example/packages/main/package.json +1 -1
  87. package/skills/jskit/references/patterns/app/shell-foundation/example/server.js +4 -1
  88. package/skills/jskit/references/patterns/assistant/assistant-surface/PATTERN.md +5 -2
  89. package/skills/jskit/references/patterns/auth/accounts-hub/example/package.json +3 -2
  90. package/skills/jskit/references/patterns/auth/auth-surface/PATTERN.md +15 -0
  91. package/skills/jskit/references/patterns/auth/oidc-sign-in/example/package.json +2 -2
  92. package/skills/jskit/references/patterns/auth/supabase-auth/example/package.json +1 -1
  93. package/skills/jskit/references/patterns/connectors/calendar-cli/example/package.json +4 -4
  94. package/skills/jskit/references/patterns/crud/crud-contract/PATTERN.md +133 -0
  95. package/skills/jskit/references/patterns/crud/crud-contract/example/bookResource.js +74 -0
  96. package/skills/jskit/references/patterns/crud/{json-api-resource-package → crud-feature-package}/PATTERN.md +69 -37
  97. package/skills/jskit/references/patterns/crud/{json-api-resource-package → crud-feature-package}/example/packages/books/package.json +5 -4
  98. package/skills/jskit/references/patterns/crud/crud-feature-package/example/packages/books/src/server/BooksFeature.js +13 -0
  99. package/skills/jskit/references/patterns/crud/crud-feature-package/example/packages/books/src/shared/bookResource.js +74 -0
  100. package/skills/jskit/references/patterns/crud/crud-feature-package/example/packages/books/src/shared/index.js +1 -0
  101. package/skills/jskit/references/patterns/crud/crud-screen-set/PATTERN.md +26 -8
  102. package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookEditPage.vue +2 -2
  103. package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookListPage.vue +2 -2
  104. package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookNewPage.vue +2 -2
  105. package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookViewPage.vue +3 -3
  106. package/skills/jskit/references/patterns/database/mysql-application/PATTERN.md +9 -4
  107. package/skills/jskit/references/patterns/database/mysql-application/example/package.json +2 -1
  108. package/skills/jskit/references/patterns/database/mysql-application/example/scripts/prepare-database.js +8 -0
  109. package/skills/jskit/references/patterns/database/postgres-application/PATTERN.md +8 -4
  110. package/skills/jskit/references/patterns/database/postgres-application/example/package.json +2 -1
  111. package/skills/jskit/references/patterns/database/postgres-application/example/scripts/prepare-database.js +8 -0
  112. package/skills/jskit/references/patterns/database/table-resource/PATTERN.md +117 -0
  113. package/skills/jskit/references/patterns/database/table-resource/example/orderResource.js +25 -0
  114. package/skills/jskit/references/patterns/realtime/realtime-application/PATTERN.md +7 -0
  115. package/skills/jskit/references/patterns/realtime/realtime-application/example/package.json +2 -2
  116. package/skills/jskit/references/patterns/rewards/google-rewarded/PATTERN.md +1 -1
  117. package/skills/jskit/references/patterns/server/feature-package/PATTERN.md +25 -17
  118. package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/package.json +1 -1
  119. package/skills/jskit/references/patterns/server/feature-package/example/variations/InvoiceRollupFeature.js +6 -7
  120. package/skills/jskit/references/patterns/server/feature-package/example/variations/invoiceResource.js +15 -0
  121. package/skills/jskit/references/patterns/server/feature-package/example/variations/invoiceRollupActions.js +4 -4
  122. package/skills/jskit/references/patterns/server/feature-package/example/variations/ownedTableRepository.js +27 -0
  123. package/skills/jskit/references/patterns/server/feature-package/example/variations/types.js +1 -1
  124. package/skills/jskit/references/patterns/users/user-administration-server/PATTERN.md +59 -10
  125. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/package.json +5 -4
  126. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/server/UsersFeature.js +7 -5
  127. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/shared/index.js +1 -1
  128. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/shared/userCrud.js +21 -0
  129. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/package.json +11 -5
  130. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/server/UsersWorkspaceFeature.js +14 -5
  131. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/server/workspaceMembersOnly.js +52 -0
  132. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/shared/index.js +1 -1
  133. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/shared/userCrud.js +21 -0
  134. package/skills/jskit/references/patterns/workspaces/workspace-server/PATTERN.md +9 -9
  135. package/skills/jskit/references/patterns/workspaces/workspace-surfaces/PATTERN.md +16 -0
  136. package/skills/jskit/references/patterns/workspaces/workspace-surfaces/example/src/pages/home/workspaces/index.vue +2 -0
  137. package/skills/jskit/references/sanity-checks.md +27 -28
  138. package/skills/jskit/references/typed-features.md +114 -247
  139. package/patterns/feature-package/example/variations/customKnexRepository.js +0 -40
  140. package/reference/autogen/packages/json-rest-api-core.md +0 -66
  141. package/skills/jskit/references/heuristics/crud-repository-mapping.md +0 -140
  142. package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/src/server/BooksFeature.js +0 -13
  143. package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/src/shared/bookResource.js +0 -83
  144. package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/src/shared/index.js +0 -1
  145. package/skills/jskit/references/patterns/crud/resource-contract/PATTERN.md +0 -155
  146. package/skills/jskit/references/patterns/crud/resource-contract/example/bookResource.js +0 -87
  147. package/skills/jskit/references/patterns/server/feature-package/example/variations/customKnexRepository.js +0 -40
  148. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/shared/userResource.js +0 -69
  149. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/shared/userResource.js +0 -56
  150. /package/skills/jskit/references/patterns/crud/{json-api-resource-package → crud-feature-package}/example/migrations/20260815000000_books.cjs +0 -0
  151. /package/skills/jskit/references/patterns/crud/{json-api-resource-package → crud-feature-package}/example/packages/books/src/server/types.js +0 -0
@@ -0,0 +1,411 @@
1
+ <!-- Generated from `packages/agent-docs/site/guide/app-setup/database-layer.md`. -->
2
+
3
+ # Database layer
4
+
5
+ JSKIT applications use Knex through one deliberately selected database driver.
6
+ Install either `@jskit-ai/database-runtime-mysql` or
7
+ `@jskit-ai/database-runtime-postgres`; do not install both unless the product
8
+ genuinely chooses a driver at runtime.
9
+
10
+ ## Install the selected driver
11
+
12
+ For MySQL or MariaDB:
13
+
14
+ ```bash
15
+ npm install @jskit-ai/database-runtime @jskit-ai/database-runtime-mysql
16
+ ```
17
+
18
+ For PostgreSQL:
19
+
20
+ ```bash
21
+ npm install @jskit-ai/database-runtime @jskit-ai/database-runtime-postgres
22
+ ```
23
+
24
+ The application declares `@jskit-ai/database-runtime` itself: each driver takes
25
+ it as a peer, so the application holds its one copy, and brings its own Knex
26
+ driver through ordinary npm dependencies. There is no JSKIT install wizard.
27
+
28
+ ## Connection environment
29
+
30
+ Keep credentials outside Git. Supply either `DATABASE_URL` or the individual
31
+ values:
32
+
33
+ ```dotenv
34
+ DB_HOST=127.0.0.1
35
+ DB_PORT=3306
36
+ DB_NAME=exampleapp
37
+ DB_USER=exampleapp
38
+ DB_PASSWORD=secret
39
+ ```
40
+
41
+ Use port `5432` for PostgreSQL. A single-driver app fixes its dialect in
42
+ `knexfile.js`, so it does not need `DB_CLIENT`. The running database provider
43
+ also derives its dialect from the installed driver and rejects a conflicting
44
+ `DB_CLIENT` value when one is supplied.
45
+
46
+ The application or hosting environment creates the database and provides its
47
+ credentials. JSKIT never guesses or commits them.
48
+
49
+ The running database provider registers a `database` health check that runs
50
+ `select 1` within two seconds. The foundations' `GET /api/health` answers `503`
51
+ while it fails; see [Health endpoint](/guide/app-setup/initial-scaffolding#health-endpoint).
52
+
53
+ ## Migration configuration
54
+
55
+ Use the fixed-dialect pattern owned by the selected driver. The important
56
+ application file is small:
57
+
58
+ ```js
59
+ import { createKnexMigrationConfigFromApp } from
60
+ "@jskit-ai/database-runtime/server/knexMigrationConfig";
61
+
62
+ export default await createKnexMigrationConfigFromApp({ client: "mysql2", environment: process.env });
63
+ ```
64
+
65
+ Use `client: "pg"` for PostgreSQL. The environment is always passed: JSKIT reads
66
+ the connection values from what the application gives it, never from
67
+ `process.env` on its own. The complete examples are in:
68
+
69
+ - `database/mysql-application`
70
+ - `database/postgres-application`
71
+
72
+ Each example is complete for its own engine. Copy files only from the selected
73
+ driver's pattern and never merge the two; for example, the CI workflow must
74
+ contain only that engine's database service.
75
+
76
+ The config discovers migrations directly from the current installed package
77
+ graph. Application migrations live in the app's `migrations/` directory;
78
+ package-owned migrations live in directories declared by the installed
79
+ package's `package.json#jskit.migrations.directories`.
80
+
81
+ There is no migration sync command and no copied migration projection. Knex
82
+ runs the authoritative files where their owners ship them.
83
+
84
+ ## What runs migrations
85
+
86
+ The application owns normal npm scripts:
87
+
88
+ ```json
89
+ {
90
+ "scripts": {
91
+ "db:migrate": "knex --knexfile ./knexfile.js migrate:latest",
92
+ "db:migrate:rollback": "knex --knexfile ./knexfile.js migrate:rollback",
93
+ "db:migrate:status": "knex --knexfile ./knexfile.js migrate:list"
94
+ }
95
+ }
96
+ ```
97
+
98
+ `npm run db:migrate` runs the Knex CLI. Knex loads `knexfile.js`, discovers the
99
+ application and installed-package migration directories, connects using the
100
+ environment, and applies pending migrations.
101
+
102
+ Deployment or a managed development environment may invoke this app-owned
103
+ script as a release step. JSKIT itself does not maintain a background migration
104
+ service.
105
+
106
+ ## Authoring schema changes
107
+
108
+ Write a new immutable migration in the package that owns the schema. Never edit
109
+ an already-applied migration and never change a live database without recording
110
+ the equivalent source-controlled migration.
111
+
112
+ Package migrations are normal `.cjs` Knex migrations and are declared in that
113
+ package's metadata:
114
+
115
+ ```json
116
+ {
117
+ "jskit": {
118
+ "migrations": {
119
+ "directories": ["migrations"]
120
+ }
121
+ }
122
+ }
123
+ ```
124
+
125
+ A `constraints/` subdirectory of a declared directory, such as
126
+ `migrations/constraints/`, holds the package's constraints migrations; it runs
127
+ later, as described below.
128
+
129
+ A package whose table migrations use another package's tables names that
130
+ package in `after`, and its migrations, and its constraints, then run after
131
+ that package's:
132
+
133
+ ```json
134
+ {
135
+ "jskit": {
136
+ "migrations": {
137
+ "directories": ["migrations"],
138
+ "after": ["@jskit-ai/users-core"]
139
+ }
140
+ }
141
+ }
142
+ ```
143
+
144
+ Each name in `after` must be another installed JSKIT package; a name that is
145
+ not installed, the package's own name, or packages that each name the other
146
+ stop discovery with an error. A named package without migrations orders
147
+ nothing.
148
+
149
+ Names must remain unique across the effective migration directories. Preserve
150
+ the basename when relocating a migration: Knex records that name in its ledger.
151
+ Do not rename it to force execution or edit an already-applied migration's body;
152
+ changed schema behavior requires a new migration.
153
+
154
+ ### Directory order comes before filenames
155
+
156
+ `createKnexMigrationConfigFromApp()` sets `sortDirsSeparately: true` and runs
157
+ pending migrations in this order, using the default `migrationsDirectory`:
158
+
159
+ 1. Discovered package migration directories, sorted by their absolute paths
160
+ using `localeCompare`, except that a package's directories follow those of
161
+ every package its `after` names, which follow theirs in turn, each in the
162
+ order listed. Without `after` the path order is unchanged.
163
+ 2. Application `migrations/`.
164
+ 3. The `constraints/` subdirectory of each discovered package migration
165
+ directory that has one, in the same order.
166
+ 4. Application `migrations/constraints/`, when it exists.
167
+
168
+ Knex sorts filenames within each directory, not across the whole graph; see
169
+ its [`sortDirsSeparately` option](https://knexjs.org/guide/migrations.html#migration-api).
170
+ Package discovery does not read tables or foreign keys; only `after` changes
171
+ the package order. A newer timestamp in an earlier package still runs before
172
+ every file in a later package.
173
+ Inspect the effective `knexfile.js` configuration if the app customizes it.
174
+
175
+ Every package table exists before the application's own migrations run, so a
176
+ root `migrations/` file can reference a package table directly, for example a
177
+ `books.user_id` foreign key to `users.id`. A package's table migrations must
178
+ never depend on an application table.
179
+
180
+ Keep table and column creation with the owning package. Put foreign keys
181
+ between two packages, and other constraints that need the complete schema, in a
182
+ constraints phase: the referencing package's own `constraints/` subdirectory
183
+ when it ships the key, or application `migrations/constraints/`. A package
184
+ constraints migration runs after the application's `migrations/`, so it can also
185
+ reference a table that may belong to the application, such as `users`; for
186
+ example, `@jskit-ai/assistant-runtime` adds its `users` and `workspaces` keys
187
+ from `migrations/constraints/`. Similarly, a `pet_notes.product_id` column can be
188
+ added by the pet-notes package, but its foreign key to the products package's
189
+ `products.id` belongs in a constraints phase. Do not rely on the
190
+ package-directory path order to make a cross-package reference work; when a
191
+ package's table migrations need another package's table, declare `after`.
192
+
193
+ Until `@jskit-ai/database-runtime` 0.1.252, root `migrations/` ran before the
194
+ package directories, so a fresh database failed on any application migration
195
+ that referenced a package table. Applied migrations stay in the Knex ledger;
196
+ only pending ones follow the new order. An application migration that still
197
+ creates a package-owned table belongs in that package.
198
+
199
+ The assistant-runtime, console-core and rewarded-core table migrations used to
200
+ add their keys to `users`, `workspaces` and later tables inline. A fresh MySQL
201
+ or MariaDB database failed when the parent table did not exist yet (errno 150),
202
+ and the assistant tables silently skipped their `workspaces` keys. They now add
203
+ them from `migrations/constraints/`. A database migrated earlier keeps the keys
204
+ it already has, and gains any that are missing.
205
+
206
+ `@jskit-ai/database-runtime/server/foreignKeyMigrations` serves constraints
207
+ migrations that must run on fresh and already-migrated databases alike.
208
+ `addMissingForeignKey(knex, key)` adds a single-column key only when no key
209
+ already links that column to the parent column. It fails, naming the table,
210
+ column and count, when rows have no parent, and it deletes nothing; repair
211
+ those rows and migrate again. `dropForeignKeyIfPresent(knex, key)` serves
212
+ `down()`.
213
+
214
+ Before adding the key, check that both tables and columns exist, the referenced
215
+ key is indexed appropriately, column types match (including integer signedness),
216
+ existing values have valid parents, and `ON DELETE SET NULL` uses a nullable
217
+ column. An incorrectly formed foreign key is not always an ordering failure.
218
+ Do not suppress the failure, skip a missing parent, or disable foreign-key
219
+ checks to make preparation appear successful.
220
+
221
+ ### MySQL and MariaDB failures can leave partial schema
222
+
223
+ MySQL and MariaDB DDL can commit implicitly. If adding a column succeeds and a
224
+ later statement adding its foreign key fails, the column can remain even though
225
+ Knex has not recorded that migration as completed. A Knex transaction does not
226
+ make a multi-statement DDL migration atomic. See the
227
+ [MySQL implicit-commit rules](https://dev.mysql.com/doc/refman/8.4/en/implicit-commit.html)
228
+ and [MariaDB implicit-commit rules](https://mariadb.com/docs/server/reference/sql-statements/transactions/sql-statements-that-cause-an-implicit-commit).
229
+
230
+ Design new multi-step DDL migrations for their concrete partial states. Inspect
231
+ the column and foreign key independently; `hasColumn()` alone cannot prove the
232
+ constraint exists. Verify the definition of an existing object before skipping
233
+ its creation, complete only missing work, and fail clearly on incompatible
234
+ schema. Do not catch arbitrary SQL errors as success or mark incomplete work
235
+ as applied. Keep recovery local to the affected migration or an explicit repair;
236
+ do not add schema repair to request handlers or normal server startup.
237
+
238
+ For an existing failure, inspect the ledger and actual schema read-only first.
239
+ Preserve applied migration bodies and use a new corrective migration for
240
+ databases that already recorded them. If an earlier pending migration prevents
241
+ that correction from running, describe the required backed-up, explicit repair
242
+ before retrying; a later file alone cannot repair a blocked earlier step. Any
243
+ relocation must retain its basename and be checked against both fresh and
244
+ existing ledgers. Never drop valuable data or delete ledger entries to force
245
+ a replay.
246
+
247
+ ## Seed data is not a migration
248
+
249
+ Migrations establish schema and invariant framework data. Product fixtures,
250
+ sample accounts, catalog content, and other environment-specific starting data
251
+ belong in an explicit, idempotent application seed operation that runs after
252
+ migrations. Do not hide product seeding in schema migrations.
253
+
254
+ Managed editors must be able to create one isolated database per development
255
+ session, apply the full migration graph, then invoke that explicit seed
256
+ operation. JSKIT supplies portable migration and seed seams; the editor owns
257
+ database allocation, credentials, lifetime, and environment injection.
258
+
259
+ ## Resource services and custom operations
260
+
261
+ Conventional persisted resources use `defineTableResource()`,
262
+ `defineCrudContract()` and `defineCrudFeature()` so the framework owns
263
+ repeated table ownership, service, action, permission, JSON API, and route
264
+ mechanics. This does not make product CRUD behavior fixed.
265
+
266
+ - `decorateRepository` adds resource-specific queries, locks, or writes.
267
+ - `decorateService` overrides a standard method or adds domain methods such as
268
+ `confirm`, `publish`, `cancel`, or `sendReminder`.
269
+ - `operationLifecycle` surrounds a standard operation with `before`, `execute`,
270
+ `after`, and mutation-only `afterCommit` phases. Create, update, and delete
271
+ phases before commit share one repository transaction, and `execute` receives
272
+ `standard(nextInput)` for retaining the normal framework write. A deadlock
273
+ runs that transaction again, so `before`, `execute` and `after` can run twice
274
+ and touch only the database; an outside effect (an email, an HTTP call) goes
275
+ in `afterCommit`, which runs once after commit, or in an outbox row.
276
+ - Named `actions` expose non-CRUD service methods through normal input,
277
+ permission, event, and optional HTTP route contracts.
278
+
279
+ The feature that owns a table reads and writes it through the handles its
280
+ `owns` entry gives its repository; services, hooks and other features call
281
+ that repository or the owner's API, never the table. External delivery belongs after commit; when it must be durable,
282
+ write an outbox record inside the transaction and deliver it separately.
283
+ A separate Feature is warranted when an operation belongs to another domain,
284
+ not merely because a useful resource has behavior beyond list and save. Write
285
+ it as a [typed feature](typed-features.md).
286
+
287
+ ## Writes, versions, locks and deadlocks
288
+
289
+ Writes that skip their read-back (`returning`), optimistic locking
290
+ (`lockVersionField`), row locks and deadlock retries belong to the table
291
+ resource's handle: see [Data access](data-access.md), Layers 1
292
+ and 4 and [Transactions](data-access.md#transactions).
293
+
294
+ ## Verification
295
+
296
+ - For every schema change, run the normal preparation entrypoint against a
297
+ fresh disposable database on the selected engine with the complete package
298
+ graph. An existing development database can hide ordering failures.
299
+ - In a separate disposable fixture at the previous schema and ledger, apply
300
+ the upgrade and prove representative existing rows survive.
301
+ - For multi-step MySQL or MariaDB DDL, reproduce partial completion (for
302
+ example, column present, foreign key absent, migration still pending), retry
303
+ preparation, and verify the final column and constraint definitions.
304
+ - Run preparation again and `npm run db:migrate:status`; require no pending
305
+ migrations. Prove invalid references are rejected and the declared delete
306
+ behavior works. A stubbed Knex test or successful process exit is insufficient.
307
+ - Exercise a real transaction and one invalid-connection case.
308
+ - When a seed operation exists, run it twice and require the second run to be
309
+ safe.
310
+ - Use the application's selected engine; exercise each supported engine when
311
+ changing a shared pattern. Report unavailable database verification explicitly.
312
+
313
+ Do not add migration receipts, sync ledgers, generator provenance, or dialect
314
+ questionnaires. The installed graph, migration source, environment, and
315
+ database migration table are sufficient.
316
+
317
+ ## Integration test databases
318
+
319
+ Disposable integration and migration tests use `TEST_DB_NAME`, never the
320
+ application's `DB_NAME`. A test run prepares that schema once and restores it
321
+ from a stored snapshot. `jskit test --databases` (the database patterns' `test`
322
+ script) does this with `@jskit-ai/database-runtime/server/testDatabases`, from
323
+ the application's `knexfile.js`:
324
+
325
+ - Each test file running at once gets a database of its own for as long as it
326
+ runs, as both `DB_NAME` and `TEST_DB_NAME`: `TEST_DB_NAME` itself, then
327
+ copies named `<TEST_DB_NAME>_1`, `_2`, ... up to the machine's test budget,
328
+ as many as the database user may create. With no usable `TEST_DB_NAME`
329
+ (unset, the connected database itself, or one the database user may not
330
+ prepare), the files run one at a time on the connected database. A copy
331
+ named like the application's database or `PROD_DB_NAME`, or a server that
332
+ cannot be reached, fails the run instead.
333
+ - The snapshot holds the schema as the engine reports it and the rows the
334
+ migrations wrote, including the migration ledger. On MySQL and MariaDB it is
335
+ each table's `SHOW CREATE TABLE` and rows, in
336
+ `node_modules/.cache/jskit-test-databases/`; on PostgreSQL it is the database
337
+ `<TEST_DB_NAME>_snapshot`, which `CREATE DATABASE <name> TEMPLATE <snapshot>`
338
+ copies while nothing is connected to it (the user needs `CREATEDB`), file by
339
+ file (`STRATEGY FILE_COPY`) from PostgreSQL 15. Its key
340
+ is a digest of the dialect, of the name and contents of every migration file
341
+ that the application's `migrations/` and its installed packages supply, and
342
+ of `package-lock.json`, since an upgraded package can change a helper its
343
+ migrations import. MySQL test databases hold tables only: a schema with
344
+ views, triggers, routines or events is refused.
345
+ - Each test file gets the environment `jskit test` was given, plus its
346
+ database's connection (`DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASSWORD`); the
347
+ rest of the `.env` that `knexfile.js` loads is read for the databases only.
348
+ The run's settings, connection password included, are in a file only the
349
+ user can read; the runner's environment names only its directory.
350
+ - When that digest changes, the harness replays the migrations into the
351
+ designated database once, outside the timed tests, and stores a new
352
+ snapshot. Otherwise it restores the stored one.
353
+ - A schema the migrations alone do not build, such as migrations that start
354
+ from a baseline schema file, or reference rows every test reads, comes from
355
+ the application's own build: `knexfile.js` also exports
356
+ `testDatabaseSchema = { files, build }`, and a browser `prepare` module passes
357
+ the same `schema` to `prepareTestDatabases()`. `build(knex)` runs in the empty
358
+ designated database instead of `knex.migrate.latest()` (the baseline, then
359
+ `knex.migrate.latest()`), and `files` lists every file it reads, its own
360
+ module included, relative to the application root: their contents join the
361
+ digest, so changing one takes a new snapshot.
362
+ - A test that runs migrations up or down, or otherwise changes the tables,
363
+ leaves its database dirty: its tables or migration ledger differ from the
364
+ snapshot's. The harness restores the snapshot before another test file uses
365
+ that database, instead of running `migrate.latest()` over the whole history.
366
+ - A browser run's seed is not part of the snapshot: it runs in each restored
367
+ database, in one transaction ([Browser test preparation](#browser-test-preparation)).
368
+ - Only the fresh-schema check in [Verification](#verification) replays the
369
+ whole history, as its own step.
370
+ - The harness reports each phase (digest, restore or rebuild, seed) with its
371
+ duration, and those durations are read, so slow preparation is visible
372
+ instead of hiding inside the next test that waits for it. A slow phase is
373
+ fixed, never absorbed by a longer time limit.
374
+
375
+ Endpoints and batch operations get their own database tests at realistic row
376
+ counts that compare their query counts, counted with `countQueries()` from
377
+ `@jskit-ai/database-runtime/server/testDatabases`, or `countRuntimeQueries()`
378
+ for a route of the server the test boots; see the typed features guide's
379
+ [data access tests](data-access.md#tests).
380
+
381
+ ## Browser test preparation
382
+
383
+ A browser run gives every Playwright worker its own server and its own copy of
384
+ `BROWSER_TEST_DB_NAME`. The database patterns' `example/tests/e2e/databases.js`
385
+ is the `prepare` module of `browserTestConfig({ prepare: "tests/e2e/databases.js" })`
386
+ from `@jskit-ai/jskit-catalog/browser-tests`: it calls `prepareTestDatabases()`
387
+ with `variable: "BROWSER_TEST_DB_NAME"`, the worker count and the app's fixture
388
+ seed, and returns each worker's `DB_NAME`. The worker's own process gets the
389
+ same environment as its server, so a test that opens the database itself (with
390
+ the app's `knexfile.js`) reaches that worker's copy. The copies (`<name>`, `<name>_1`,
391
+ ...) are restored from the same stored snapshot as the Node test databases
392
+ ([Integration test databases](#integration-test-databases)), then seeded in one
393
+ transaction each, so stale rows never survive a run and the migrations are
394
+ replayed only when one changes. It refuses the application's database,
395
+ `PROD_DB_NAME`, `TEST_DB_NAME` and their copies.
396
+
397
+ Give disposable integration and migration tests their own `TEST_DB_NAME`;
398
+ the two names, and their copies, must differ, and the application account
399
+ needs grants for each (a `<app>_%` grant covers the copies). Each preparation
400
+ reports how many databases it made, whether it replayed the migrations for a
401
+ new snapshot, and its restore and seed durations, so a cold rebuild is visible
402
+ to the caller.
403
+
404
+ Run related checks in one suite invocation and prepare only their required
405
+ fixtures. A local browser run serves the production build, never the
406
+ development launcher; fresh-schema migration proofs are separate. A managed
407
+ suite (`PLAYWRIGHT_BASE_URL`) uses the host's server URL and identity, as the
408
+ host serves it, starts no second server, and restores normal Preview after the
409
+ batch. Database isolation alone does not
410
+ disable email, payments or other external effects: configure those test controls
411
+ and verify the actual server identity before destructive requests.
@@ -96,31 +96,33 @@ application's normal verification script, and Playwright.
96
96
 
97
97
  ## 3. Choose the narrowest server seam that owns each behavior
98
98
 
99
- Conventional resources use `defineCrudResource()` and
100
- `defineCrudJsonApiFeature()` so JSKIT owns the repeated repository, service,
101
- permission, action, JSON API, and route mechanics. Extend that contract instead
99
+ Conventional resources use `defineTableResource()`, `defineCrudContract()`
100
+ and `defineCrudFeature()` so JSKIT owns the repeated repository,
101
+ service, permission, action, JSON API, and route mechanics. Extend that contract instead
102
102
  of copying it.
103
103
 
104
104
  | Product need | Normal owner |
105
105
  | --- | --- |
106
106
  | Behavior before, around, or after one standard CRUD operation | `operationLifecycle` |
107
107
  | A resource-specific domain method or override | `decorateService` |
108
- | A resource-specific query, lock, or persistence operation | `decorateRepository` |
108
+ | A resource-specific query, lock, or many-row write | `decorateRepository`, on the table's handle |
109
109
  | A named non-CRUD command on the resource | a named CRUD `action` backed by the service |
110
110
  | A separate aggregate, workflow, or capability domain | `defineFeature()` with named actions |
111
- | Persistence that the high-level resource APIs cannot express | an explicit custom repository owned by that Feature |
111
+ | A report the handle cannot express | one `scopedQuery()` repository method, under `// Knex: <kind> - <reason>` ([data access](data-access.md#layer-5-reports-the-resource-cannot-express)) |
112
112
 
113
113
  For mutations, lifecycle phases before commit share one repository
114
114
  transaction. An `execute` hook that only normalizes input calls
115
- `standard(nextInput)` rather than reimplementing the write. Repositories own
116
- database access; services and hooks orchestrate repositories. External work
115
+ `standard(nextInput)` rather than reimplementing the write. The feature that
116
+ owns a table reads and writes it through the handles its `owns` entry gives
117
+ its repository; services, hooks and other features call that repository or
118
+ the owner's API, never the table. External work
117
119
  runs after commit, or a durable outbox record is written inside the transaction
118
120
  and delivered separately.
119
121
 
120
122
  Give commands product names such as `confirm`, `publish`, or `cancel`, with
121
123
  explicit input, permissions, routes where needed, audit behavior, events, and
122
124
  tests. Do not hide unrelated workflows behind a generic `execute(anything)`
123
- action, and do not create a custom repository merely because SQL is familiar.
125
+ action, and do not write Knex merely because SQL is familiar.
124
126
 
125
127
  ### Reuse response serializers
126
128
 
@@ -143,9 +145,11 @@ upgrading JSKIT and declaring the directly used
143
145
  ```js
144
146
  import Fastify from "fastify";
145
147
  import SerializerSelector from "@fastify/fast-json-stringify-compiler";
146
- import { createCachedResponseSerializerFactory } from "@jskit-ai/kernel/server/http";
148
+ import { createCachedResponseSerializerFactory, resolveTrustedProxies } from "@jskit-ai/kernel/server/http";
147
149
 
148
150
  const app = Fastify({
151
+ // TRUSTED_PROXIES: the proxies whose X-Forwarded-For is believed (loopback without it).
152
+ trustProxy: resolveTrustedProxies(runtimeEnv),
149
153
  schemaController: {
150
154
  compilersFactory: {
151
155
  buildSerializer: createCachedResponseSerializerFactory(SerializerSelector())
@@ -155,84 +159,113 @@ const app = Fastify({
155
159
  ```
156
160
 
157
161
  Keep the application's other Fastify options, including serializer options and
158
- validator configuration. This setup wraps the native default serializer builder;
162
+ validator configuration, but replace an application's own `trustProxy` setting
163
+ with `resolveTrustedProxies(runtimeEnv)` (`runtimeEnv` is the environment the
164
+ server starts with, `process.env` if it has no other), and its environment
165
+ variable with `TRUSTED_PROXIES`; see
166
+ [sign-in protection](https://mobily-enterprises.github.io/jskit-ai/guide/app-setup/authentication#sign-in-protection)
167
+ for the setting. This setup wraps the native default serializer builder;
159
168
  retain any deliberately chosen custom compiler instead of replacing or wrapping
160
169
  it. Native route-level compiler overrides continue to apply. Update the lockfile
161
170
  through the application's normal installation flow and restart the backend to
162
171
  activate the new setup. Updating packages or starter examples alone does not
163
172
  change an existing application's startup source.
164
173
 
165
- ### JSON REST v2 integration
174
+ ### Data access on table resources (json-rest-api 3)
166
175
 
167
- Applications using `@jskit-ai/json-rest-api-core` require Node.js 24 or newer.
176
+ Applications using `@jskit-ai/resource-runtime` require Node.js 24 or newer.
168
177
  JSKIT's repository and application default is Node.js 26. Upgrade dependencies,
169
- custom resource calls, and hooks together; rebuild native database dependencies
170
- after changing Node.
171
-
172
- The standard `defineCrudJsonApiFeature()` API keeps `queryDocuments`,
173
- `getDocumentById`, `createDocument`, `patchDocumentById`, and
174
- `deleteDocumentById`, with `{ trx, context }` repository/service options.
175
- Read, create, and patch services still return tagged JSON:API documents.
176
- Application-owned hosts use `JsonRestApi` from `json-rest-api`; the JSKIT host
177
- already owns that setup.
178
-
179
- For direct resource calls, replace `inputRecord` with exactly one of `data`
180
- (plain values) or `document` (a JSON:API document). `format` selects output
181
- independently: `"plain"` or `"jsonapi"`. Replace `simplified` and
182
- `returnFullRecord` with `format` and `returning: "full" | "minimal" | "none"`.
183
- The JSKIT host defaults to plain output and full write responses; standard
184
- document repositories explicitly request JSON:API output and full create/patch
185
- responses. A plain collection is still `{ data: [...], meta?, links? }`; use
186
- `extractJsonRestCollectionRows()` when a custom repository needs its rows.
187
- A query without a page returns only the resource's first 20 rows; a
188
- server-side read that needs every row uses `queryAllJsonRestCollectionRows()`
189
- ([every row](typed-features.md#every-row)).
178
+ resource calls and hooks together; rebuild native database dependencies after
179
+ changing Node.
190
180
 
191
- ```js
192
- await jsonRestApi.resources.books.patch({
193
- id: recordId,
194
- data: { title: "Revised title" },
195
- format: "jsonapi",
196
- returning: "full",
197
- transaction: trx
198
- }, createJsonRestContext(context));
199
- ```
181
+ Every table has one table resource and one owner feature, and every read and
182
+ write goes through that table's handle
183
+ ([data access](data-access.md)). Rename as you go:
200
184
 
201
- Keep the target ID outside the patch data. Direct plain writes use the
202
- belongs-to relationship name and ID, such as `author: authorId`; the standard
203
- repository maps a resource field's declared `as` alias. Do not send both names.
204
-
205
- Resource hooks share the mutable context supplied as the second argument
206
- within one operation. The normalized hook field remains
207
- `context.inputRecord.data`; only the public input argument changed.
208
- `createJsonRestContext()` clones the JSKIT execution context and its
209
- `visibilityContext` and `scopeValues`, allowing frozen caller contexts; other
210
- nested objects remain shared references. Permission hooks use
211
- `context.originalContext ?? context` for operation-wide caches; enrichment
212
- hooks use `context.parentContext`. Built-in nested operations get separate
213
- contexts with shallow inheritance. For an application-initiated child call,
214
- choose the application properties to share in a fresh context and pass
215
- `transaction` explicitly; reusing or blindly spreading an active operation
216
- context carries its working state into the child.
217
-
218
- The standard repository's `withTransaction(work)` delegates to
219
- `jsonRestApi.transaction(work)`. Its callback and mutation lifecycle phases
220
- receive the actual callable Knex transaction: custom repository SQL can use
221
- `await trx("audit_entries").insert(...)`, while resource calls receive it as
222
- `transaction`. Await every participant before returning. The helper owns
223
- commit, rollback, and completion hooks. Raw `knex.transaction()` handles cannot
224
- own resource writes, and calling `transaction()` again starts an independent
225
- unit; forward the existing handle to compose work.
185
+ | Before | Now |
186
+ | --- | --- |
187
+ | `defineCrudResource()` | `defineTableResource()` plus `defineCrudContract()` |
188
+ | `defineCrudJsonApiFeature()`, `createCrudResourceRuntime()` | `defineCrudFeature({ crud })` |
189
+ | the CRUD runtime's `listByIds()`, `listByForeignIds()` | `queryAll({ filters: { [key]: ids } })` on a filter declared `{ type: "array", filterOperator: "in" }`; a page's children with `createHooksToCollectChildren()` |
190
+ | the CRUD runtime's lookup hydration | `include` on the CRUD list or view |
191
+ | the CRUD runtime's `virtualFields` | `queryFields` in the `owns` entry, or `decorateService` for a value SQL cannot compute |
192
+ | the CRUD runtime's `createdAt`/`updatedAt` stamping | every handle write stamps them; only a system context sets its own |
193
+ | the CRUD runtime's output validation | the table resource's schema, whose types json-rest-api reads each field as |
194
+ | `defineCrudFeature`'s `ownershipFilter`, `searchSchema` | `visibility`; an `owns` entry |
195
+ | a lifecycle hook's `trx`; a decorator's or CRUD capability's `resource` | `options`; `crud` |
196
+ | `decorateRepository`'s `database`, `http`, `jsonRestApi` | its `repository` and `resources`, each owned table's handle |
197
+ | `@jskit-ai/crud-core/server/fieldAccess`, `/server/lookups` | removed (nothing called them); restrict fields in the CRUD contract and its `decorateService` |
198
+ | `runtime.json-rest-api`, `addResourceIfMissing()`, `createJsonRestResourceScopeOptions()` | `defineFeature({ owns })`; a row policy, search schema or query fields go in the `owns` entry |
199
+ | `queryDocuments()`, `getDocumentById()` | `query({ page: { size } })`, `get(id)` |
200
+ | `queryAllJsonRestCollectionRows()`, `queryEveryDocument()` | `queryAll(params)` |
201
+ | `createDocument()`, `createDocuments()` | `create()`, `createMany()` |
202
+ | `patchDocumentById()`, `patchDocumentIfLockVersion()` | `patch(id, changes, options)`, `patch(id, changes, { ...options, lockVersion })` |
203
+ | `deleteDocumentById()` | `delete(id)` |
204
+ | `getDocumentById(…, { forUpdate: true })`, `applyForUpdate()`, `lockScopedRecordId()` | `lock(id)` inside `withTransaction` |
205
+ | `runInTransaction()`, `createWithTransaction()`, `joinOrOpenTransaction()`, `resolveTransaction()`, `resolveQueryOptions()`, `resolveRepoClient()` | `withTransaction(options, (txOptions) => …)` |
206
+ | `returning: "full" \| "minimal"` | `returning: "record" \| "id"` (`"none"` is unchanged) |
207
+ | a hand-built `visibilityContext` or scope values, `applyVisibility()`, `applyVisibilityOwners()` | the caller's context, or `systemContext(reason).forWorkspace(id)` |
208
+ | a field's `default:` | `defaultTo:` (`default` is refused, since json-rest-api never read it) |
209
+ | a `createdAt` or `updatedAt` default of now | nothing: drop it, since every handle write stamps them |
210
+ | a CRUD contract that showed every column | a contract shows only its `fields`, so list every field the API showed |
211
+ | reading every row | `queryAll`, which refuses above 10,000 rows, so page with `query()` |
212
+ | an app's own wrappers around old calls | the handle method they wrapped, then delete the wrapper |
213
+
214
+ Handles answer records, `{ id, ...fields }`, with a belongs-to field as the
215
+ related id; the CRUD service alone answers JSON:API documents. Every call takes
216
+ `options` last: the caller's context, and inside a transaction the `txOptions`
217
+ `withTransaction` handed its work. A call without a context throws on a table
218
+ that is not public.
219
+
220
+ json-rest-api 3 changes what a failed write means: any failed write inside a
221
+ managed transaction fails the whole transaction. "Insert, catch the duplicate,
222
+ then read" inside a transaction no longer works; use
223
+ `createMany(rows, { ...options, onConflict: { keys, action: "ignore" } })` and then read,
224
+ or read first. A `get`, `patch`, `delete` or `lock` of a missing or hidden
225
+ record throws json-rest-api's not-found error (a 404); with `missing: "skip"`
226
+ it answers `null`, without failing the caller's transaction. Upsert rows carry no
227
+ `id`, never repeat their conflict keys, and name the ownership field among
228
+ those keys; only a table with no owners and no row policy may key the upsert on
229
+ the id alone, `onConflict: { keys: ["id"] }`, with an id on every row. `patchWhere` and `deleteWhere` need filters, or `all: true`.
226
230
 
227
231
  Preserve `error.transactionOutcome` and its cause when translating failures:
228
232
  `none` means no transaction was acquired or accepted, `pending` leaves
229
233
  completion to the owner, `rolledBack` confirms rollback, `committed` confirms
230
234
  commit, and `unknown` means the outcome is unresolved. These describe database
231
- transaction evidence, not external effects. A failed JSKIT mutation
232
- `afterCommit` hook raises `RestApiWriteError` with outcome `committed` and does
233
- not undo the write. Do not turn pending, committed, or unknown write failures
234
- into successful missing-record results, or automatically replay a committed
235
- or unresolved write.
235
+ transaction evidence, not external effects. A failed `afterCommit` hook raises
236
+ `RestApiWriteError` with outcome `committed` and does not undo the write. Do
237
+ not turn pending, committed, or unknown write failures into successful
238
+ missing-record results, or automatically replay a committed or unresolved
239
+ write.
240
+
241
+ ### Moving hand-written SQL behind resources
242
+
243
+ An application with Knex in its repositories moves it behind table resources
244
+ one table at a time:
245
+
246
+ 1. **Inventory.** List every statement that reads or writes a table: the
247
+ repository methods, raw queries, migrations' seed data, jobs and scripts.
248
+ `jskit check` and the data-access lint rules (`jskit/no-direct-knex`,
249
+ `jskit/no-hand-scope`) find most of them.
250
+ 2. **One owner per table.** Pick the feature that owns each table and give the
251
+ table one table resource with its `ownership`. Two features writing one
252
+ table means one of them asks the other.
253
+ 3. **Owner APIs.** Code outside the owner calls the owner's service or
254
+ repository capability. JSKIT's own tables already work this way: users
255
+ (`users.profiles`, `users.settings`), workspaces, local sign-in and console
256
+ settings.
257
+ 4. **Map each statement to a layer**
258
+ ([data access](data-access.md)): one record or page →
259
+ Layer 1; screens over HTTP → Layer 2; a loop of writes → a many-row write;
260
+ a `forUpdate` read → a lock; totals and reports → `aggregate`, a window
261
+ include, or one `scopedQuery()` method; work over every workspace → a job
262
+ with `systemScope`.
263
+ 5. **Two tests per path.** `expectScoped` proves the path never reads or
264
+ changes a second owner's rows; `expectFlatQueries` proves its statements do
265
+ not grow with its rows. Both come from `@jskit-ai/resource-runtime/test`.
266
+
267
+ Keep each move behaviour-preserving
268
+ ([refactors](sanity-checks.md#refactors)).
236
269
 
237
270
  ### Workspace invitation transaction compatibility (workspaces-core 0.1.198)
238
271