@jskit-ai/agent-docs 0.1.279 → 0.1.281

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 (138) hide show
  1. package/guide/agent/app-extras/assistant-conversation.md +5 -1
  2. package/guide/agent/app-extras/assistant.md +123 -0
  3. package/guide/agent/app-extras/realtime.md +20 -10
  4. package/guide/agent/app-setup/authentication.md +54 -4
  5. package/guide/agent/app-setup/database-layer.md +108 -35
  6. package/guide/agent/app-setup/existing-application-migration.md +7 -1
  7. package/guide/agent/app-setup/material-3-migration.md +38 -9
  8. package/guide/agent/app-setup/multi-homing.md +14 -4
  9. package/guide/agent/app-setup/upgrading-jskit.md +105 -0
  10. package/guide/agent/app-setup/users.md +9 -1
  11. package/guide/agent/framework/client-features.md +35 -1
  12. package/guide/agent/framework/material-3.md +111 -71
  13. package/guide/agent/framework/sanity-checks.md +181 -0
  14. package/guide/agent/framework/translation.md +4 -0
  15. package/guide/agent/framework/typed-features.md +222 -120
  16. package/guide/agent/framework/ui-operations.md +3 -1
  17. package/guide/agent/index.md +3 -0
  18. package/package.json +2 -2
  19. package/patterns/feature-package/example/booking-engine/package.json +1 -1
  20. package/patterns/minimal-foundation/PATTERN.md +6 -2
  21. package/patterns/minimal-foundation/example/AGENTS.md +22 -5
  22. package/patterns/minimal-foundation/example/package.json +6 -6
  23. package/patterns/minimal-foundation/example/playwright.config.mjs +3 -30
  24. package/patterns/minimal-foundation/example/server.js +7 -1
  25. package/patterns/minimal-foundation/example/src/main.js +3 -2
  26. package/patterns/minimal-foundation/example/tests/e2e/startup.spec.ts +13 -0
  27. package/patterns/minimal-foundation/example/vite.config.mjs +3 -1
  28. package/patterns/shell-foundation/PATTERN.md +4 -1
  29. package/patterns/shell-foundation/example/AGENTS.md +22 -5
  30. package/patterns/shell-foundation/example/package.json +7 -7
  31. package/patterns/shell-foundation/example/packages/main/package.json +1 -1
  32. package/patterns/shell-foundation/example/packages/main/src/client/providers/MainClientProvider.js +3 -0
  33. package/patterns/shell-foundation/example/playwright.config.mjs +3 -30
  34. package/patterns/shell-foundation/example/server.js +7 -1
  35. package/patterns/shell-foundation/example/src/components/menus/SectionTabLinkItem.vue +42 -0
  36. package/patterns/shell-foundation/example/src/main.js +3 -2
  37. package/patterns/shell-foundation/example/src/pages/home.vue +1 -1
  38. package/patterns/shell-foundation/example/src/views/NotFound.vue +1 -1
  39. package/patterns/shell-foundation/example/tests/e2e/startup.spec.ts +13 -0
  40. package/patterns/shell-foundation/example/vite.config.mjs +3 -1
  41. package/reference/autogen/PATTERN_INDEX.md +37 -37
  42. package/reference/autogen/packages/agent-docs.md +4 -0
  43. package/reference/autogen/packages/architecture-inspector.md +1 -0
  44. package/reference/autogen/packages/assistant-runtime.md +28 -3
  45. package/reference/autogen/packages/auth-provider-local-core.md +2 -1
  46. package/reference/autogen/packages/auth-web.md +10 -0
  47. package/reference/autogen/packages/connectors-catalog.md +8 -0
  48. package/reference/autogen/packages/crud-core.md +3 -0
  49. package/reference/autogen/packages/database-runtime.md +33 -0
  50. package/reference/autogen/packages/http-web.md +0 -4
  51. package/reference/autogen/packages/json-rest-api-core.md +17 -4
  52. package/reference/autogen/packages/kernel.md +37 -2
  53. package/reference/autogen/packages/material-web.md +2 -1
  54. package/reference/autogen/packages/oidc-issuer-core.md +0 -1
  55. package/reference/autogen/packages/realtime.md +8 -5
  56. package/reference/autogen/packages/shell-web.md +88 -3
  57. package/reference/autogen/packages/users-web.md +5 -2
  58. package/reference/autogen/packages/workspaces-core.md +3 -1
  59. package/reference/autogen/tooling/jskit-catalog.md +122 -5
  60. package/reference/autogen/tooling/testUtils.md +15 -1
  61. package/skills/jskit/SKILL.md +17 -5
  62. package/skills/jskit/references/client-features.md +35 -1
  63. package/skills/jskit/references/existing-application-migration.md +7 -1
  64. package/skills/jskit/references/heuristics/INDEX.md +3 -1
  65. package/skills/jskit/references/heuristics/row-policies.md +3 -3
  66. package/skills/jskit/references/heuristics/typed-features.md +7 -7
  67. package/skills/jskit/references/heuristics/ui-testing.md +27 -21
  68. package/skills/jskit/references/material-3.md +111 -71
  69. package/skills/jskit/references/multi-homing.md +14 -4
  70. package/skills/jskit/references/pattern-index.md +37 -37
  71. package/skills/jskit/references/patterns/app/minimal-foundation/PATTERN.md +6 -2
  72. package/skills/jskit/references/patterns/app/minimal-foundation/example/AGENTS.md +22 -5
  73. package/skills/jskit/references/patterns/app/minimal-foundation/example/package.json +6 -6
  74. package/skills/jskit/references/patterns/app/minimal-foundation/example/playwright.config.mjs +3 -30
  75. package/skills/jskit/references/patterns/app/minimal-foundation/example/server.js +7 -1
  76. package/skills/jskit/references/patterns/app/minimal-foundation/example/src/main.js +3 -2
  77. package/skills/jskit/references/patterns/app/minimal-foundation/example/tests/e2e/startup.spec.ts +13 -0
  78. package/skills/jskit/references/patterns/app/minimal-foundation/example/vite.config.mjs +3 -1
  79. package/skills/jskit/references/patterns/app/shell-foundation/PATTERN.md +4 -1
  80. package/skills/jskit/references/patterns/app/shell-foundation/example/AGENTS.md +22 -5
  81. package/skills/jskit/references/patterns/app/shell-foundation/example/package.json +7 -7
  82. package/skills/jskit/references/patterns/app/shell-foundation/example/packages/main/package.json +1 -1
  83. package/skills/jskit/references/patterns/app/shell-foundation/example/packages/main/src/client/providers/MainClientProvider.js +3 -0
  84. package/skills/jskit/references/patterns/app/shell-foundation/example/playwright.config.mjs +3 -30
  85. package/skills/jskit/references/patterns/app/shell-foundation/example/server.js +7 -1
  86. package/skills/jskit/references/patterns/app/shell-foundation/example/src/components/menus/SectionTabLinkItem.vue +42 -0
  87. package/skills/jskit/references/patterns/app/shell-foundation/example/src/main.js +3 -2
  88. package/skills/jskit/references/patterns/app/shell-foundation/example/src/pages/home.vue +1 -1
  89. package/skills/jskit/references/patterns/app/shell-foundation/example/src/views/NotFound.vue +1 -1
  90. package/skills/jskit/references/patterns/app/shell-foundation/example/tests/e2e/startup.spec.ts +13 -0
  91. package/skills/jskit/references/patterns/app/shell-foundation/example/vite.config.mjs +3 -1
  92. package/skills/jskit/references/patterns/auth/accounts-hub/example/package.json +2 -2
  93. package/skills/jskit/references/patterns/auth/auth-surface/PATTERN.md +18 -5
  94. package/skills/jskit/references/patterns/auth/auth-surface/example/src/components/AppBrand.vue +17 -0
  95. package/skills/jskit/references/patterns/auth/auth-surface/example/src/pages/auth/login.vue +2 -2
  96. package/skills/jskit/references/patterns/auth/auth-surface/example/src/pages/auth/reset-password.vue +2 -2
  97. package/skills/jskit/references/patterns/auth/auth-surface/example/src/pages/auth/signout.vue +2 -2
  98. package/skills/jskit/references/patterns/auth/auth-surface/example/src/views/auth/LoginView.vue +6 -1
  99. package/skills/jskit/references/patterns/auth/auth-surface/example/src/views/auth/ResetPasswordView.vue +6 -1
  100. package/skills/jskit/references/patterns/auth/auth-surface/example/src/views/auth/SignOutView.vue +6 -1
  101. package/skills/jskit/references/patterns/auth/oidc-sign-in/example/package.json +2 -2
  102. package/skills/jskit/references/patterns/auth/supabase-auth/example/package.json +1 -1
  103. package/skills/jskit/references/patterns/connectors/aws-storage-queries/example/formats/package.json +1 -1
  104. package/skills/jskit/references/patterns/connectors/calendar-cli/example/package.json +4 -4
  105. package/skills/jskit/references/patterns/console/console-surface/example/src/pages/console.vue +1 -1
  106. package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/package.json +2 -2
  107. package/skills/jskit/references/patterns/database/mysql-application/PATTERN.md +34 -24
  108. package/skills/jskit/references/patterns/database/mysql-application/example/package.json +3 -2
  109. package/skills/jskit/references/patterns/database/mysql-application/example/tests/e2e/databases.js +25 -0
  110. package/skills/jskit/references/patterns/database/postgres-application/PATTERN.md +12 -0
  111. package/skills/jskit/references/patterns/database/postgres-application/example/package.json +3 -2
  112. package/skills/jskit/references/patterns/database/postgres-application/example/tests/e2e/databases.js +25 -0
  113. package/skills/jskit/references/patterns/realtime/realtime-application/example/package.json +2 -2
  114. package/skills/jskit/references/patterns/rewards/google-rewarded/example/googlePublisherTag.js +5 -2
  115. package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/package.json +1 -1
  116. package/skills/jskit/references/patterns/shell/application-shell/PATTERN.md +11 -6
  117. package/skills/jskit/references/patterns/shell/application-shell/example/src/components/menus/SectionTabLinkItem.vue +42 -0
  118. package/skills/jskit/references/patterns/shell/application-shell/example/src/pages/home.vue +1 -1
  119. package/skills/jskit/references/patterns/shell/application-shell/example/src/views/NotFound.vue +1 -1
  120. package/skills/jskit/references/patterns/ui/page-and-placement/PATTERN.md +6 -0
  121. package/skills/jskit/references/patterns/ui/page-and-placement/example/src/pages/home/reports.vue +9 -4
  122. package/skills/jskit/references/patterns/ui/page-and-placement/example/src/placementTopology.js +5 -3
  123. package/skills/jskit/references/patterns/users/account-settings/PATTERN.md +24 -3
  124. package/skills/jskit/references/patterns/users/account-settings/example/src/components/account/settings/AccountSettingsNotificationsSection.vue +1 -1
  125. package/skills/jskit/references/patterns/users/account-settings/example/src/components/account/settings/AccountSettingsPreferencesSection.vue +2 -1
  126. package/skills/jskit/references/patterns/users/account-settings/example/src/components/account/settings/AccountSettingsProfileSection.vue +3 -2
  127. package/skills/jskit/references/patterns/users/account-settings/example/src/pages/account/[section].vue +23 -0
  128. package/skills/jskit/references/patterns/users/account-settings/example/src/pages/account.vue +4 -3
  129. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/package.json +2 -2
  130. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/package.json +3 -3
  131. package/skills/jskit/references/patterns/workspaces/workspace-surfaces/example/packages/main/src/client/components/AccountPendingInvitesCue.vue +3 -3
  132. package/skills/jskit/references/patterns/workspaces/workspace-surfaces/example/src/surfaces/admin/root.vue +1 -1
  133. package/skills/jskit/references/patterns/workspaces/workspace-surfaces/example/src/surfaces/app/root.vue +1 -1
  134. package/skills/jskit/references/sanity-checks.md +181 -0
  135. package/skills/jskit/references/translation.md +4 -0
  136. package/skills/jskit/references/typed-features.md +222 -120
  137. package/skills/jskit/references/ui-operations.md +3 -1
  138. package/skills/jskit/references/patterns/database/mysql-application/example/tests/browser/database.js +0 -68
@@ -232,7 +232,7 @@ const ConversationClientProvider = defineProvider({
232
232
  },
233
233
  /** @param {{ components: import("@jskit-ai/kernel/client/types").ClientComponents }} capabilities */
234
234
  setup({ components }) {
235
- components.register("conversation.web.settings.element", ConversationSettingsElement);
235
+ components.register("conversation.web.status.widget", ConversationStatusWidget);
236
236
  }
237
237
  });
238
238
 
@@ -241,6 +241,40 @@ const components = new Map();
241
241
  export function registerComponent(id, component) { components.set(id, component); }
242
242
  ```
243
243
 
244
+ ### Provider subpaths
245
+
246
+ **Export each client provider by a subpath of its own. The provider's file
247
+ imports only what every page needs, such as a widget the shell always shows; a
248
+ screen that only some routes show is imported by those routes.**
249
+
250
+ Why: every page of every app loads the client providers, and what they import,
251
+ before it shows anything. The client bootstrap imports a provider through the
252
+ subpath export that names its file, so the screens `./client` re-exports, and
253
+ their libraries, load only with the routes that show them. A provider without a
254
+ subpath, or a `./client` that declares `routeComponents` or `clientRoutes` or
255
+ uses `export *`, brings all of `./client` to every page.
256
+
257
+ ```json
258
+ // good: package.json
259
+ "exports": {
260
+ "./client": "./src/client/index.js",
261
+ "./client/providers/ConversationClientProvider": "./src/client/providers/ConversationClientProvider.js"
262
+ }
263
+
264
+ // bad: package.json, no subpath for the provider, so every page loads all of ./client
265
+ "exports": {
266
+ "./client": "./src/client/index.js"
267
+ }
268
+ ```
269
+
270
+ ```js
271
+ // good: the provider's file imports the widget every page shows
272
+ import ConversationStatusWidget from "../components/ConversationStatusWidget.vue";
273
+
274
+ // bad: the provider's file imports a screen that only its route shows
275
+ import ConversationSettingsScreen from "../components/ConversationSettingsScreen.vue";
276
+ ```
277
+
244
278
  ### Injected values
245
279
 
246
280
  **`inject()` takes the owner's exported key, never a literal. A required value
@@ -128,7 +128,13 @@ The foundation servers reuse compiled response serializers for identical JSON
128
128
  schemas through `createCachedResponseSerializerFactory` from
129
129
  `@jskit-ai/kernel/server/http`. This reduces duplicate startup compilation and
130
130
  retained memory in applications with many routes. Each Fastify compiler context
131
- keeps its own cache; request data and response payloads are not cached.
131
+ keeps its own cache; request data and response payloads are not cached. A
132
+ serializer is compiled when a response first uses it, in every environment, so
133
+ startup compiles none. Startup still checks every response schema as
134
+ fast-json-stringify would before building it: an invalid schema or a `$ref`
135
+ that does not resolve stops the server. A problem fast-json-stringify only
136
+ finds while generating a serializer fails the first response that uses it, so
137
+ keep a test that calls each route.
132
138
 
133
139
  Existing applications adopt the same setup in their own `server.js` after
134
140
  upgrading JSKIT and declaring the directly used
@@ -44,7 +44,9 @@ How to use it:
44
44
  - [crud-repository-mapping.md](./crud-repository-mapping.md)
45
45
  - row policy, permission filtering, visibility before pagination, recursive CTE, descendants, package cycle, `RowPolicyPlugin`, row policy cost, slow writes through a policy, access check per history item
46
46
  - [row-policies.md](./row-policies.md)
47
- - typed features, server feature, provider, service, repository, collaborator, `typeof`, `= null`, `setProvider`, `register`, `input || {}`, `payload = {}`, `async execute`, `Object.freeze`, `requires`, `optional`, `types.js`, `RuntimeKnex`, `silentLogger`, `create<Name>Actions`, `ActionSpecifications`, request context, `api.resources.x.query`, every row, list stops at 20 rows, `queryAllJsonRestCollectionRows`, `jskit/*` lint findings, `readable-check: allow`, `optional operation:`, `process.env`, `AppError` code, `Object.assign(new Error`, `idempotency`, `…ForTests` export, file or function size, read per row, N+1 queries, slow endpoint, query budget, realistic row counts, slow tests, raised timeout, `migrate.latest()` in tests, test database snapshot
47
+ - typed features, server feature, provider, service, repository, collaborator, `typeof`, `= null`, `setProvider`, `register`, `input || {}`, `payload = {}`, `async execute`, `Object.freeze`, `requires`, `optional`, `types.js`, `RuntimeKnex`, `silentLogger`, `create<Name>Actions`, `ActionSpecifications`, request context, `api.resources.x.query`, every row, list stops at 20 rows, `queryAllJsonRestCollectionRows`, `jskit/*` lint findings, `readable-check: allow`, `optional operation:`, `process.env`, `AppError` code, `Object.assign(new Error`, `idempotency`, `…ForTests` export, file or function size, read per row, N+1 queries, slow endpoint, query budget, realistic row counts, slow tests, raised timeout, `migrate.latest()` in tests, test database snapshot, `--test-concurrency=1`, `node --test`, `jskit test`, `testConcurrency()`, `V64_TEST_CONCURRENCY`, Playwright `workers`, Vitest `maxWorkers`, tests one file at a time, test files colliding in parallel, `jskit-test: alone`, `test/*` findings of `jskit check`
48
48
  - [typed-features.md](./typed-features.md)
49
49
  - client features, composable, `use<Name>`, Vue component, client provider, props, emits, `inject`, `provide`, `ref`, `reactive`, `watch`, `addEventListener`, `setInterval`, module state, `Object.freeze` on a composable, `?.()` in a template, `jskit/typed-inject`, `jskit/collaborator-props`, `jskit/fixed-result-shape`, `jskit/released-listeners`, `jskit/no-module-state`, `jskit/no-frozen-vue-state`, `clientFeaturesConfig`, `shared resource:`
50
50
  - [client-features.md](./client-features.md)
51
+ - sanity checks, is this sane, query count that grows with the rows, a list that stops at 20 or 100 rows, `createDocument()` per row of a batch, `jskit test`, one file at a time, test database preparation, `TEST_DB_NAME`, `mode: "patch"`, a route `rateLimit` nothing tests, `input.workspaceId`, a refactor or consolidation that drops behaviour, a JSKIT update, a changed theme or Vuetify default, `jskit.surface` with a parent, an assistant tool's output contract, a screen that renders everything, feedback covering an action, `NODE_ENV=production`, a production start
52
+ - [Sanity checks](../sanity-checks.md)
@@ -39,9 +39,9 @@ Package composition:
39
39
 
40
40
  Cost:
41
41
 
42
- - a policy's cost follows the rows its query reads: add a join, `EXISTS` or subquery correlated with the row through `column()` and `value()`, on indexed columns
42
+ - a policy's cost follows the rows its query reads: add a join, `EXISTS` or subquery correlated with the row through `column()` and `value()`, on indexed columns; look at its SQL too, since a statement that runs to kilobytes is planned again on every read that carries it
43
43
  - never load the subject's whole history (every source, notice or grant they were ever linked to) to build an `IN` list, and never call another owner's access check per item of it; another package's grant is a predicate contribution
44
- - the policy runs on every single-record read, including the read-back after `createDocument()` and `patchDocumentById()` (`returning: "full"`) and the lookup before `patch` and `delete`, so a write pays it once per row it writes
44
+ - the policy runs on every single-record read, including the read-back after `createDocument()` and `patchDocumentById()` (`returning: "full"`, their default) and the lookup before `patch` and `delete`, so a write pays it once per row it writes
45
45
  - a write decides access once, inside its transaction; a batch write that would pay an expensive read-back per row follows the typed features guide's [Bounded access checks](../typed-features.md#bounded-access-checks)
46
46
 
47
47
  Child-resource rules:
@@ -58,7 +58,7 @@ Testing:
58
58
  - assert hidden direct records behave as not found
59
59
  - assert children and includes do not leak records
60
60
  - assert package and provider dependencies stay one-way
61
- - run one read and one write for a subject with a short history and with a realistic long one (hundreds of linked rows), and assert the same query count
61
+ - count the queries of one read and one write for a subject with a short history and with a realistic long one (hundreds of linked rows), and fail when the count grows with the history
62
62
 
63
63
  AnyAPI boundary:
64
64
 
@@ -63,10 +63,10 @@ ends with the same link.
63
63
  - [One helper per package](../typed-features.md#one-helper-per-package)
64
64
  - `options?.trx || options?.transaction`, a hand-read actor, cursor or created id
65
65
  - [One reader per fact](../typed-features.md#one-reader-per-fact)
66
- - `api.resources.x.query(…)` with no `page` for a list, `queryDocuments(query)` with no `limit` in a service, a list that stops at 20 rows
66
+ - `api.resources.x.query(…)` with no `page` for a list, `queryDocuments(query)` with no `limit` in a service, a list that stops at 20 rows, `page: { size: 500 }` or `limit: 500` that returns 100 rows
67
67
  - [Every row](../typed-features.md#every-row)
68
- - `await getDocumentById(…)`, `queryDocuments(…)` or a service `get` inside a loop or a `Promise.all` over a list, a single-record helper reused for a list, N+1 queries, a slow list endpoint
69
- - [One read per table](../typed-features.md#one-read-per-table)
68
+ - `await getDocumentById(…)`, `queryDocuments(…)` or a service `get` inside a loop or a `Promise.all` over a list, a single-record helper reused for a list, N+1 queries, a list whose query count grows with its rows, a slow list endpoint
69
+ - [Lists read related rows in batches](../typed-features.md#lists-read-related-rows-in-batches)
70
70
  - a `rowPolicy` that loads a person's whole history or calls an access check per item, `createDocument` per row of a batch, a write that gets slower as history grows
71
71
  - [Bounded access checks](../typed-features.md#bounded-access-checks)
72
72
  - a route that queries storage, `boot*.js` holding routes
@@ -85,10 +85,10 @@ ends with the same link.
85
85
  - [Tests are not callers](../typed-features.md#tests-are-not-callers)
86
86
  - `…ForTests`, `__reset…`, `testOnly…` exports (`jskit/no-test-only-export`)
87
87
  - [No test-only exports](../typed-features.md#no-test-only-exports)
88
- - a list, batch or write test with one to three rows, no query count, a slow endpoint first seen with real data
89
- - [Budgets at realistic counts](../typed-features.md#budgets-at-realistic-counts)
90
- - a raised `{ timeout }`, `test.setTimeout()`, `--test-timeout` or Playwright `timeout`, retries or a request loop that absorbs slowness, a slow suite
91
- - [Time limits are not fixes](../typed-features.md#time-limits-are-not-fixes)
88
+ - a list, batch or write test with one to three rows, no query count, a fixed query ceiling tuned to pass, a slow endpoint first seen with real data, a hand-written `knex.on("query")` counter, a patched `Client.prototype.query`, `countQueries`, `countRuntimeQueries`, a route's query count
89
+ - [Query counts at realistic sizes](../typed-features.md#query-counts-at-realistic-sizes)
90
+ - `node --test --test-concurrency=1`, `workers: 1`, `fileParallelism: false`, a suite that runs one file at a time, test files that collide when run together, a Playwright webServer running `npm run develop` or `vite`, a browser suite that takes hours, `test/*` findings of `jskit check`
91
+ - [Test files run in parallel](../typed-features.md#test-files-run-in-parallel)
92
92
  - `knex.migrate.latest()` in each test file, a template database rebuilt by migrating, test preparation slower than its tests
93
93
  - [Test databases are prepared once](../typed-features.md#test-databases-are-prepared-once)
94
94
  - a file over 750 lines, a function over 600 (`max-lines`, `max-lines-per-function`), a data module
@@ -50,30 +50,36 @@ Rules:
50
50
  ## Keep startup incremental
51
51
 
52
52
  Run related cases in one suite invocation. Prepare the isolated database and
53
- fixtures once for that invocation, never once per test. Retain the test schema
54
- and migration ledger, apply only pending migrations, and reset fixture data
55
- deterministically. Restore any migration-owned baseline rows through the test
56
- seed. MySQL applications start from the `database/mysql-application` pattern's
57
- `example/tests/browser/database.js`. Require a repeat-start regression test that
58
- proves unchanged migration history, removal of stale data, pending migration
59
- application, and isolation from the normal database.
60
-
61
- Use `BROWSER_TEST_DB_NAME` for that retained schema and a different
62
- `TEST_DB_NAME` for disposable integration and migration tests. Reject a name
63
- collision before connecting. Include disposable cleanup between browser starts
64
- in the regression: one test's teardown must not erase another test's migrations.
65
-
66
- Do not drop/recreate the database, replay completed migrations, build every
67
- module fixture, or run a production frontend build for each focused check.
68
- Choose small fixture sets for the selected suite; realistic row counts belong
69
- in the database tests that hold each endpoint's query budget. Fresh-schema
53
+ fixtures once for that invocation, never once per test. An app's
54
+ `playwright.config.mjs` is `browserTestConfig()` from
55
+ `@jskit-ai/jskit-catalog/browser-tests`: the production build, made once and
56
+ reused while its sources are unchanged, and one server, port and database per
57
+ worker. Its `prepare` module (the `database/mysql-application` and
58
+ `database/postgres-application` patterns' `example/tests/e2e/databases.js`)
59
+ restores each worker's copy of `BROWSER_TEST_DB_NAME` from the stored schema
60
+ snapshot and runs the fixture seed, which restores any migration-owned
61
+ baseline rows. The migrations are replayed only when one changes, so a second
62
+ run reports a stored snapshot, no stale rows, and an unchanged normal
63
+ database.
64
+
65
+ Use `BROWSER_TEST_DB_NAME` for the browser copies and a different
66
+ `TEST_DB_NAME` for disposable integration and migration tests; the harness
67
+ refuses a collision, or one with the application's database, before
68
+ connecting.
69
+
70
+ Do not replay completed migrations, build every module fixture, or run a
71
+ production frontend build for each focused check. A
72
+ build made once for a whole run is different: it is the fix when a development
73
+ server makes every page load slow, and browser tests never run on the
74
+ development server. Choose small fixture sets for the selected
75
+ suite; realistic row counts belong in the database tests that compare each
76
+ endpoint's query counts. Fresh-schema
70
77
  migration tests and production-artifact tests are separate operations. Measure
71
78
  migration, fixture, and server startup time and fix the slow phase; never retry
72
79
  a slow launch, or raise its deadline or a spec's timeout, to absorb it. A page
73
- that needs a longer timeout to load is a slow endpoint to fix (typed features'
74
- Time limits are not fixes). When a host supplies a managed target, use its
75
- scoped suite command so one startup serves the whole batch and cleanup
76
- restores normal Preview.
80
+ that needs a longer timeout to load is a slow endpoint to fix. When a host
81
+ supplies a managed target, use its scoped suite command so one startup serves
82
+ the whole batch and cleanup restores normal Preview.
77
83
 
78
84
  ## Keep browser evidence focused
79
85
 
@@ -42,7 +42,8 @@ app and never leaves it.
42
42
  - **What the person sees:** one Up arrow on every inner page, also when opened
43
43
  directly: icon only (`$arrowBack`), accessible name "Back" (localized), a
44
44
  tooltip on pointer devices. None on top-level destinations, and no other
45
- back control anywhere; the bar title says where the person is. A list-detail
45
+ back control anywhere; the bar title says where the person is. A new or edit
46
+ form shows a close (X) in the arrow's place, which cancels. A list-detail
46
47
  layout showing the parent beside the page needs no arrow for that step;
47
48
  collapsed to one pane, the record pane gets it back.
48
49
  - **Where Up goes:** to the previous trail entry while it is valid, so inside
@@ -74,11 +75,17 @@ Exactly this, and nothing more:
74
75
  </script>
75
76
  ```
76
77
 
77
- The same key works in a `<route lang="json">` block. A record names its
78
- list, an edit page its record, a sub-page its page. Tabs under a layout
78
+ The same key works in a `<route lang="json">` block. Tabs under a layout
79
79
  (`settings.vue` rendering `<RouterView>` for `settings/`) declare nothing:
80
80
  they inherit the nearest layout's declaration, and moving between them keeps
81
- the trail.
81
+ the trail and the scroll. A folder without a layout (`bookings/` with no
82
+ `bookings.vue`) only groups routes: its pages are different pages, each
83
+ opening at the top. The parent is read from the page up to its surface root, the
84
+ record that declares `jskit.surface`, and stops there. So the surface goes
85
+ only on the surface's layout page (`account.vue`: `ShellLayout` around
86
+ `<RouterView />`) and the parent on the page it renders
87
+ (`account/index.vue`). A package route declares its parent in its
88
+ `package.json` `meta`; under no layout, its component draws `ShellLayout`.
82
89
  2. **Each top-level destination is a main-navigation item** (a `kind: "link"`
83
90
  placement with `target: "shell.primary-nav"` or `"shell.secondary-nav"`) and
84
91
  declares no parent; nor do its tabs. A shell page with no parent is
@@ -90,14 +97,20 @@ Exactly this, and nothing more:
90
97
  `:to="paths.page('/suppliers')"` and `router.push(paths.page(...))` with
91
98
  `usePaths()`, the CRUD runtime's `resolveViewUrl()`, `resolveEditUrl()` and
92
99
  `resolveParams()`, and link placements (`surface` with `scopedSuffix` and
93
- `unscopedSuffix`; `to` only for a global route such as `/auth/login`). The
94
- trail follows the router, so there is no special link component.
100
+ `unscopedSuffix`; `to` only for a global route). A global route
101
+ (`scope: "global"`, on no surface) is
102
+ `paths.page("/auth/login", { scope: "global" })`; `explicitTo` only passes
103
+ through a finished target that arrives as data, never a hand-written path.
104
+ The trail follows the router, so there is no special link component.
95
105
  4. **A custom new or edit page finishes through the shell:**
96
106
  `useShellNavigation()` (`@jskit-ai/shell-web/client/navigation/useShellNavigation`)
97
107
  gives `finish(target)` for Save (the target replaces the form in the trail)
98
108
  and `finish()` for Cancel (back to where the form was opened, else its
99
- parent). A page whose record turns out deleted or forbidden calls
100
- `pageUnavailable()`. JSKIT's CRUD screens already do both.
109
+ parent); `useShellPageClose({ label, close })`
110
+ (`@jskit-ai/shell-web/client/composables/useShellPageClose`) makes the
111
+ arrow its Cancel, never an X beside it. A page whose
112
+ record turns out deleted or forbidden calls `pageUnavailable()`. JSKIT's
113
+ CRUD screens already do all three.
101
114
  5. **A full page load goes through the shell too:** `reloadAppPage(path)`
102
115
  (after sign-in or a user switch, to rebuild client state) and
103
116
  `leaveApp(url)` (an external site in the same tab, such as a payment page),
@@ -113,11 +126,12 @@ Exactly this, and nothing more:
113
126
  | `nav/history` | `history.back()`, `history.go()`, `history.pushState()`/`replaceState()`, `router.back()`, `router.go()` |
114
127
  | `nav/window-location` | `window.location` assignments, `.href =`, `location.assign()`, `location.replace()` |
115
128
  | `nav/raw-link` | a hand-built in-app path in `to`, `href`, `router.push()`, `router.replace()` or a `to:`/`href:` object key outside a placement |
116
- | `nav/back-control` | any back control but the shell's Up that navigates the app: "Back to X", "← Suppliers", back arrows and navigating chevrons, breadcrumbs, a custom app-bar back button, a back component, a `goBack()`-style handler |
117
- | `nav/missing-parent` | an inner page of a shell surface without a declared parent |
129
+ | `nav/back-control` | any back control but the shell's Up that navigates the app: "Back to X", "← Suppliers", back arrows and navigating chevrons, breadcrumbs, a custom app-bar back button, a back component (`<ContextBackButton />` or `<page-back-link>`, and its import), a `goBack()`-style handler |
130
+ | `nav/missing-parent` | an inner page of a shell surface without a declared parent, when it always renders under the shell's top app bar (a layout that also renders its pages without `ShellLayout`, such as a shop device's sign-in in a `v-if`, leaves its pages unjudged) |
118
131
  | `nav/top-level-parent` | a main-navigation destination that declares a parent |
119
132
  | `nav/placement-data` | a main-navigation placement that isn't literal data (no page is judged for a missing parent until it is) |
120
133
  | `nav/missing-shell-layout` | a page that declares a parent with no shell layout rendering it, so its title, Up and actions never show |
134
+ | `nav/surface-parent` | a parent declared beside `jskit.surface`, which the shell never reads |
121
135
  | `nav/allow` | any `m3-check: allow nav/...` comment: nothing excuses a navigation rule |
122
136
 
123
137
  ### Each rule, right and wrong
@@ -175,35 +189,43 @@ Exactly this, and nothing more:
175
189
  - **Type:** Vuetify 4 Material 3 roles (`text-headline-small`,
176
190
  `text-title-large`, `text-body-medium`, `text-label-large`). Never the
177
191
  Material 2 scale (`text-h5`, `text-body-2`) or a page-local type system.
178
- Roboto Flex is the font.
192
+ Material 3 has two typefaces, both Roboto Flex unless the app names its own
193
+ in `src/theme/index.js`:
194
+ `typography: { brand: '"Fraunces", serif', plain: '"Inter", sans-serif' }`.
195
+ `brand` sets display, headline and title-large, `plain` the rest (title-medium
196
+ and -small, body, label); each is a CSS font-family value, and the app loads
197
+ the font. Never set `font-family` or `--v-font-body` in app CSS.
179
198
  - **Icons:** Material Symbols Rounded through Vuetify `$` aliases: Vuetify's
180
199
  standard ones (`$close`, `$menu`, `$delete`, `$prev`, `$next`) and JSKIT's
181
200
  camelCase names (`$add`, `$edit`, `$search`, `$moreVert`, `$filterList`,
182
201
  `$arrowBack`); `$homeFilled`-style `Filled` variants mark active navigation.
183
202
  An app adds an icon under `theme.icons` in `src/theme/index.js` as
184
203
  `{ name: "<24x24 path data>" }` from Material Symbols Rounded, then uses
185
- `$name`. No `mdi-*` strings and no `@mdi/js` imports in app code.
204
+ `$name`. No `mdi-*` strings and no `@mdi/js` imports in app code. Material
205
+ Symbols has no brand marks (Google, GitHub, a payment network): a brand's
206
+ logo is the brand's own artwork, a content image or SVG used as its owner
207
+ publishes it, not an icon alias.
186
208
 
187
209
  ## Exact sizes
188
210
 
189
- JSKIT ships these exact sizes; an app never re-implements or overrides them,
190
- and needs no Sass and no `!important` (Vuetify 4 keeps its CSS in cascade
191
- layers):
211
+ JSKIT ships these exact sizes; an app never re-implements or overrides them:
192
212
 
193
213
  - 40dp buttons with a 48dp touch target, icon buttons included; a smaller
194
214
  button is `size="small"` or `size="x-small"`, which keeps its smaller look
195
- and the 48dp target;
196
- - one-line button labels: a label longer than its button ends in an ellipsis.
197
- Keep labels short, and give a button whose label can run long, such as a
198
- name, the full label as its `title`;
199
- - 12dp cards: elevated (the default) at level 1, flat and tonal at level 0;
215
+ and the 48dp target. No icon button is under 32dp, and a target stops at
216
+ the edge of a button beside it, so each of a row's touching icon buttons
217
+ answers its own press;
218
+ - one-line button labels, ending in an ellipsis: keep labels short, and give
219
+ one that can run long, such as a name, its full label as `title`;
220
+ - 12dp cards: flat (the default) and tonal at level 0, `variant="elevated"`
221
+ at level 1; flat expansion panels;
200
222
  - 28dp dialogs and bottom-sheet top corners, square full-screen dialogs;
201
223
  - 8dp chips, and FABs of 56dp (small 40dp, large 96dp; extended 56dp tall)
202
224
  with 16dp corners;
203
225
  - emphasized easing, with reduced motion honoured.
204
226
 
205
227
  So never set `height` or `min-height` on a button, or a radius on a card,
206
- dialog, chip or FAB. A missing size is a JSKIT defect.
228
+ dialog, chip or FAB.
207
229
 
208
230
  ## The top app bar carries the page
209
231
 
@@ -211,35 +233,46 @@ dialog, chip or FAB. A missing size is a JSKIT defect.
211
233
  `useShellPageTitle(title)` (`@jskit-ai/shell-web/client/composables/useShellPageTitle`)
212
234
  for a runtime title such as the record's name. The shell renders it as the
213
235
  app bar's only `h1`; pages add no `h1`, page header, title block or
214
- standalone heading. A section inside a page uses a title role. A page with
215
- no top app bar (a hero front page, an intro, an invite or hand-off page)
216
- has its own one `h1`.
217
- - **Up** sits in the bar on every inner page (see Navigation and Up). On phones
218
- it replaces the menu icon.
236
+ standalone heading. Route pages own the title: a section or a nested
237
+ component uses a title role (a component's claim gives the title back when
238
+ it goes). A page with no top app bar (a hero front page, an intro, an invite
239
+ or hand-off page) has its own one `h1`.
240
+ - **Up** leads the bar on every inner page; on phones it replaces the menu
241
+ icon.
219
242
  - **A layout that draws its own top app bar** (a workspace bar with the
220
243
  app's own controls, instead of `ShellLayout`) builds it from `ShellLayout`'s
221
244
  own pieces, from `@jskit-ai/shell-web/client`. It calls `useShellAppBar()`
222
245
  once, so its pages enter the trail and their `ShellPageActions` show in its
223
246
  bar, and renders `ShellUpButton` first, then `ShellPageTitle` (the page's one
224
- `h1`: `useShellPageTitle()`, else the route's `jskit.title`, else its `title`
225
- prop) and `ShellPageActionsTarget`. A title that is also a switcher, such as a
247
+ `h1`) and `ShellPageActionsTarget`. A title that is also a switcher, such as a
226
248
  project menu, goes in `ShellPageTitle`'s default slot (given `{ title }`) as
227
- the menu's activator, so it stays the one `h1`. `jskit check` counts it as a shell
228
- layout, so its inner pages need parents; an Up the app draws itself is a
229
- `nav/back-control` finding.
249
+ the menu's activator, so it stays the one `h1`. `jskit check` counts it as a
250
+ shell layout, so its inner pages need parents.
230
251
  - **Actions:** a screen's actions go in the bar inside `ShellPageActions`
231
252
  (`@jskit-ai/shell-web/client/components/ShellPageActions`), each a
232
253
  `ShellPageAction` with `icon`, `label` (also its accessible name and compact
233
- tooltip), and `primary`, `to`, `replace`, `disabled`, `loading` or `color`.
234
- On compact widths every action shows only its icon except the single
235
- `primary` one.
254
+ tooltip), and `primary`, `to`, `replace`, `href` (with `download` for a
255
+ file, such as a CSV export), `disabled`, `loading` or `color` (`error` for a
256
+ destructive one); attributes such as `target` reach its button. On compact
257
+ widths the bar keeps one trailing item after the title: the single
258
+ `primary` action when the page has one (a form's Save), otherwise its own
259
+ item nearest the corner (a top-right placement such as the account avatar).
260
+ Every other action and the bar's other items (a notification bell, tools)
261
+ go to its "More actions" menu, where `overflow` actions always are. Medium
262
+ and wider bars show them all.
236
263
  - **Window size classes:** compact under 600 (navigation bar), medium 600–839
237
- (navigation rail), expanded 840–1199 and large from 1200 (drawer). A surface
238
- with no navigation destinations, such as an account surface, draws no
239
- navigation bar, rail or drawer; its pages keep the app bar. Read them
240
- from `useShellWindowSizeClass()`
264
+ (navigation rail), expanded 840–1199 and large from 1200 (drawer), read from
265
+ `useShellWindowSizeClass()`
241
266
  (`@jskit-ai/shell-web/client/composables/useShellWindowSizeClass`), never from
242
- copied breakpoint numbers.
267
+ copied breakpoint numbers. A surface with fewer than two navigation
268
+ destinations (none, as on an account surface, or Home alone) draws no
269
+ navigation bar, rail or drawer; its pages keep the app bar.
270
+ - **Section tabs:** a page's own sections (`page.section-nav`) are Material 3
271
+ primary tabs: the topology names `ShellSectionTabLinkItem`'s token (the
272
+ starter registers it as `local.main.ui.section-tab-link-item`), and the page
273
+ draws one tab row, `display: flex; overflow-x: auto` over a 1dp
274
+ `outline-variant` bottom border. `ShellTabLinkItem` is the navigation bar's
275
+ destination; placed in a page it draws a section tab.
243
276
 
244
277
  ## Colour
245
278
 
@@ -262,35 +295,41 @@ dialog, chip or FAB. A missing size is a JSKIT defect.
262
295
  control; never a permanent panel of outlined fields. A card opens its record
263
296
  when tapped (a stretched title link); a table row opens on click. Row actions
264
297
  are trailing icon buttons (edit, a `$moreVert` overflow menu, a chevron to
265
- open), never "View" or "Edit" text buttons on every row. "New" is an extended
266
- FAB on compact and medium widths and a filled bar button on expanded. Empty
267
- and error states carry an icon.
298
+ open), never "View" or "Edit" text buttons on every row. "New" is
299
+ `ShellPrimaryAction` (`label`, and `to` or a click handler): an extended FAB
300
+ on compact and medium widths, a filled bar button on expanded. On/off filters
301
+ are `aria-pressed` chips, or chips in a `v-chip-group`; JSKIT draws them as
302
+ filter chips. Empty and error states carry an icon.
268
303
  - **A list never scrolls sideways, at any width.** The list's own width (a
269
304
  container query, not the window) chooses cards or table. Cards flow into a
270
305
  grid; tables use a fixed layout with an ellipsis on long values, and fold
271
306
  low-priority columns into a stronger column's second line. Lists show figures
272
307
  ("120 kg", tabular and right-aligned) and names, never ids.
273
308
  - **Record:** a header (avatar, title, status chips) and value-first detail
274
- lists, never a grid of label/value text. Edit sits in the bar; the record's
309
+ lists, never a grid of label/value text. Edit sits in the bar, its secondary
310
+ actions in the bar's overflow menu, never in the content; the record's
275
311
  next workflow step is the one filled button, shown in context rather than a
276
312
  row of mostly disabled actions.
277
- - **Form:** Cancel and Save sit in the bar. Fields use `hideDetails: "auto"`.
278
- Nothing moves under a finger: a multi-select chip field keeps its input width
279
- when it loses focus. A pick-one-of-N choice is a segmented button with
280
- `role="radiogroup"` and `role="radio"`/`aria-checked` buttons.
313
+ - **Form:** the bar's close (X) cancels and Save is its filled action. Fields
314
+ use `hideDetails: "auto"`. Nothing moves under a finger: a multi-select chip
315
+ field keeps its input width when it loses focus. A pick-one-of-N choice is a
316
+ segmented button (`v-btn-toggle`, drawn by JSKIT, with 12dp segment sides as
317
+ in any `v-btn-group`) with `role="radiogroup"` and
318
+ `role="radio"`/`aria-checked` buttons.
281
319
  - **Dialogs:** plain `v-dialog` and `v-bottom-sheet`; JSKIT gives them their
282
320
  Material 3 shapes. A full-screen dialog (compact and medium) puts close, title
283
321
  and its confirm action in a top bar, never at the bottom where a snackbar
284
322
  covers them.
285
323
  - **Menus and badges:** a menu's list has `role="menu"` and its items
286
324
  `role="menuitem"`, with `aria-disabled` on unavailable ones; a badge has a
287
- `label` such as "3 batches".
325
+ `label` such as "3 batches". JSKIT draws a menu's list and a dialog's card;
326
+ an app sets no `VMenu` or `VDialog` defaults, under which
327
+ Vuetify 4.2.1 to 4.3.2 loses a `v-select`'s typeahead match, or a dialog's
328
+ virtual list, when an ancestor re-renders.
288
329
 
289
330
  ## Components, state, and feedback
290
331
 
291
- - Choose components by purpose. Prefer standard buttons, fields, lists,
292
- tables, sheets, dialogs, menus, navigation, progress, alerts, snackbars, and
293
- tooltips over custom imitations.
332
+ - Choose standard components by purpose over custom imitations.
294
333
  - Give one primary action emphasis. Distinguish destructive, secondary, and
295
334
  icon-only actions without relying on color; give icon controls accessible
296
335
  names and fields meaningful labels, errors, and autocomplete behavior.
@@ -307,6 +346,11 @@ dialog, chip or FAB. A missing size is a JSKIT defect.
307
346
  field. A user-triggered command uses JSKIT's shared action
308
347
  feedback/snackbar path. Never insert a transient command-error alert above
309
348
  page content where it shifts the working layout.
349
+ - A snackbar never covers the FAB or the actions it reports on. `ShellErrorHost`
350
+ centres it above the navigation bar on compact windows and starts it at the
351
+ content's start edge, at most half its width, on wider ones. A FAB takes
352
+ layout room (`<v-fab app layout>`, as `ShellPrimaryAction`'s does), so the
353
+ snackbar shows above it.
310
354
 
311
355
  ## Adaptive layout
312
356
 
@@ -328,16 +372,15 @@ npx jskit check m3 src/pages/home/books # a section
328
372
  npx jskit check m3 $(git diff --name-only --diff-filter=d main) # changed files
329
373
  ```
330
374
 
331
- Plain `jskit check` fails on the [navigation rules](#forbidden-in-app-code);
332
- fix every one, because nothing excuses them. `jskit check m3` lists those and
333
- the visual rules below. The visual rules are a report until every JSKIT app has
334
- been converted to them; the JSKIT release after that makes plain `jskit check`,
335
- and so every app's verify, fail on them too:
375
+ Plain `jskit check` fails on the [navigation rules](#forbidden-in-app-code).
376
+ `jskit check m3` lists those and the visual rules below. The visual rules are a
377
+ report until every JSKIT app has been converted to them; the JSKIT release after
378
+ that makes plain `jskit check`, and so every app's verify, fail on them too:
336
379
 
337
380
  | Rule | Finds |
338
381
  |---|---|
339
- | `m3/duplicate-h1` | a second `h1`, or an in-page `h1` under the shell's page title |
340
- | `m3/raw-hex` | a hex colour outside `src/theme/` (`palettes.js` holds content colours) |
382
+ | `m3/duplicate-h1` | a second `h1` that can show beside the first (one per `v-if` branch is one), or an in-page `h1` under a layout that always shows the shell's page title |
383
+ | `m3/raw-hex` | a hex colour, or one written as channels (`rgba(0, 0, 0, 0.15)`, as in a hand-made shadow), outside `src/theme/` (`palettes.js` holds content colours); `rgba(var(--v-theme-shadow), 0.2)` reads a role and passes |
341
384
  | `m3/dark-scheme` | no dark scheme, or the theme pinned to light |
342
385
  | `m3/outlined-card` | `variant="outlined"` or `border` on a card or sheet |
343
386
  | `m3/mdi-icon` | `mdi-*` icon strings and `@mdi/js` imports |
@@ -390,9 +433,9 @@ What is left, such as a colour inside static SVG markup that can't import,
390
433
  may be excused one line at a time, on its line or the line before: `<!-- m3-check: allow m3/raw-hex - the partner's contractual logo colour -->`
391
434
  (`// m3-check: ...` in scripts, `/* ... */` in CSS). **Never add one on your
392
435
  own:** ask the person first, naming the rule and the reason. A comment with no
393
- reason (`m3/allow-reason`) or an unknown rule (`m3/allow-rule`), and any
394
- `m3-check: allow nav/...` (`nav/allow`), is itself a finding; nothing turns the
395
- check off for a file, a folder or the app. `jskit check m3` lists every
436
+ reason (`m3/allow-reason`) or an unknown rule (`m3/allow-rule`) is itself a
437
+ finding, as any `nav/allow` is; nothing turns the check off for a file, a
438
+ folder or the app. `jskit check m3` lists every
396
439
  exception with its file, line and reason, and their total.
397
440
 
398
441
  ## Material 3 audit
@@ -401,19 +444,16 @@ Run this behavior-preserving audit over every affected screen:
401
444
 
402
445
  1. Identify the owning JSKIT surface, shared screen, shell, placement, theme,
403
446
  and Vuetify components before judging local markup.
404
- 2. Check navigation and Up first, as must-fix: plain `jskit check` passes;
405
- every inner page has a parent and one Up arrow in the bar; main-menu
406
- destinations have none; no inner link is hand-built; nothing writes back
407
- logic.
447
+ 2. Check navigation and Up first, as must-fix: plain `jskit check` passes and
448
+ every rule in Navigation and Up holds.
408
449
  3. Find parallel components, duplicated state chrome, page-local themes, raw
409
450
  visual constants, CSS against Vuetify internals, nested generic cards,
410
- borders instead of tones, Material 2 typography, and desktop-only layout.
411
- 4. Check hierarchy, token use, component purpose, responsive presentation,
412
- interaction states, keyboard/focus behavior, labels, targets, contrast, and
413
- reduced motion against the rules above.
414
- 5. Correct issues at the narrowest established owner. Consolidate only truly
451
+ borders instead of tones, Material 2 typography, and desktop-only layout;
452
+ check states, focus, labels, targets, contrast and reduced motion against
453
+ the rules above.
454
+ 4. Correct issues at the narrowest established owner. Consolidate only truly
415
455
  repeated policy; avoid pass-through wrappers and speculative abstractions.
416
- 6. Run `jskit check` and `jskit check m3` on the affected files, then focused
456
+ 5. Run `jskit check` and `jskit check m3` on the affected files, then focused
417
457
  tests and Playwright at compact, medium, and expanded widths in light and
418
458
  dark (`shellBackButton(page)` and `pressShellBack(page)` from
419
459
  `@jskit-ai/shell-web/test/shellNavigation` drive Up).
@@ -56,8 +56,12 @@ config.tenancyPolicy = {
56
56
  ```
57
57
 
58
58
  Provisioning runs in the users profile-sync lifecycle after registration and
59
- sign-in. The first run creates the personal workspace, its owner membership and
60
- its settings; later runs reuse it. The default name is the user's display name,
59
+ sign-in, at each session refresh, and on a request whose identity's email or
60
+ display name changed. The first run creates the personal workspace, its owner
61
+ membership and its settings; later runs reuse it. An ordinary request only reads
62
+ the existing profile, so a personal workspace, owner membership or settings row
63
+ deleted while the person is signed in comes back at their next sign-in or
64
+ session refresh, not at their next request. The default name is the user's display name,
61
65
  or email, followed by "'s Workspace". To choose another, provide the
62
66
  `workspaces.personal-workspace-name` capability from an application feature. It
63
67
  receives the synchronized profile (`id`, `displayName`, `email`, `username`), and
@@ -174,6 +178,12 @@ capability provides:
174
178
  | `workspaces.repositories.workspaces`, `.workspaceMemberships`, `.workspaceSettings`, `.workspaceInvites` | Repository access inside the application's own transactions |
175
179
  | `workspaces.tenancyProfile` | The resolved tenancy mode and workspace policy |
176
180
 
181
+ To check one person's membership, for example a notification recipient, call
182
+ `workspaces.services.members.getMember(workspace, userId, options)`. It answers
183
+ that member's `listMembers` entry, or `null` for a removed or unknown member,
184
+ and reads only that membership. Do not list every member to find one: the list
185
+ grows with the workspace, and a lookup per row multiplies it.
186
+
177
187
  ### Invitation email
178
188
 
179
189
  To send invitation email, provide the optional `workspaces.invite-mailer`
@@ -452,9 +462,9 @@ Use only a verified empty disposable database named
452
462
  reports its engine version, applies package migrations and drops fixture tables.
453
463
 
454
464
  ```sh
455
- node --test packages/workspaces-core/test/*.test.js
465
+ npx jskit test packages/workspaces-core/test/*.test.js
456
466
  # Set JSKIT_REPOSITORY_TEST_DATABASE_URL to the verified empty disposable target.
457
- node --test packages/workspaces-core/test/repositories.integration.test.js
467
+ npx jskit test packages/workspaces-core/test/repositories.integration.test.js
458
468
  npm run verify
459
469
  ```
460
470