@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
@@ -443,6 +443,9 @@ record declares `mode: "patch"`; a command never does.**
443
443
 
444
444
  - Patch mode skips `required`, so a service keeps its guard for a field a
445
445
  patch input lists as required.
446
+ - An input field lists its allowed values with `enum`; the kernel refuses
447
+ `oneOf` on an input field at startup, because json-rest-schema checks no
448
+ `oneOf` and the field would accept any value.
446
449
 
447
450
  Why: a command whose required field is never enforced reaches the service
448
451
  without it.
@@ -644,7 +647,7 @@ way (is this user an operator) is a member of its owner's capability.**
644
647
  - A helper two packages of an application use lives in its shared package,
645
648
  never inside one of them.
646
649
 
647
- Why: copies drift, and the next reader cannot tell which one is right.
650
+ Why: the kernel's helpers fix one meaning of empty, and a copy brings its own.
648
651
 
649
652
  ```js
650
653
  // good
@@ -673,7 +676,8 @@ helper, never by hand.**
673
676
  | the workspace a request asks for, before any action context | `requestedWorkspaceScope(request)` (`@jskit-ai/workspaces-core/server/support/workspaceRouteInput`) |
674
677
  | a context's workspace id | `resolveWorkspaceScopeValue(context)` (`@jskit-ai/json-rest-api-core/server/jsonRestApiHost`) |
675
678
 
676
- Why: each hand-written copy treats a missing value its own way.
679
+ Why: each reader knows every place JSKIT keeps its fact (`trx` or
680
+ `transaction`, a tagged result's wrapper), and a hand read misses one.
677
681
 
678
682
  ```js
679
683
  // good
@@ -693,11 +697,17 @@ page means "the first 20 rows".**
693
697
  - json-rest-api answers a query that names no page with the resource's
694
698
  default page, 20 rows, and says nothing about the rest. The CRUD
695
699
  repository's `queryDocuments(query)` without a `limit` is the same first
696
- page.
697
- - The helper reads cursor pages of the resource's maximum size
698
- (`queryMaxLimit`, 100 unless the resource sets it) in id order until none
699
- follows, keeping the call's transaction and context, so no row is skipped
700
- or repeated. It sets the page and sort itself; sort the rows it returns.
700
+ page, and one with a `limit` above its `queryMaxLimit()` (100) throws,
701
+ because json-rest-api would answer only 100. A CRUD read of every record
702
+ uses the repository's `queryEveryDocument(query, options)`, which answers
703
+ one JSON:API document `{ data, included }` with no `limit`, `cursor` or
704
+ `sort`.
705
+ - The helper reads cursor pages of 1,000 rows (json-rest-api's server-only
706
+ `pageSizeLimit`; a resource whose `queryMaxLimit` is larger uses that) in id
707
+ order until none follows, keeping the call's transaction and context, so no
708
+ row is skipped or repeated. Each page pays the row policy and linkage
709
+ queries once, so 480 rows cost 3 statements, not 15. It sets the page and
710
+ sort itself; sort the rows it returns.
701
711
  - A lookup by a unique key (an id, a slug, a token hash, a unique pair such
702
712
  as workspace and user) matches at most one row and may read `rows[0]` of a
703
713
  plain query. A read of the newest or first few rows names its own `sort`
@@ -724,33 +734,35 @@ const rows = extractJsonRestCollectionRows(await api.resources.workspaceMembersh
724
734
  }, createJsonRestContext(options.context)));
725
735
  ```
726
736
 
727
- ### One read per table
737
+ ### Lists read related rows in batches
728
738
 
729
- **A read that builds a list reads each table once for the whole list: one
730
- query per table with an id-list filter, the rows grouped in memory. It never
731
- reads one row, or runs one lookup, per item of the list.**
739
+ **A read that builds a list fetches its related rows in batches, an id-list
740
+ query per related table, and groups them in memory, so its query count follows
741
+ the tables and pages it reads, not the rows.**
732
742
 
733
- - Collect the ids from the first read, then read each related table once with
734
- an id-list filter: a `searchSchema` entry
743
+ - Collect the ids from the first read, then read each related table with an
744
+ id-list filter: a `searchSchema` entry
735
745
  `{ type: "array", actualField: "<column>", filterOperator: "in" }`, read
736
746
  through `queryAllJsonRestCollectionRows()`, or the resource's `include`.
737
747
  Group the rows by id with `Map.groupBy()`.
738
- - `getDocumentById()`, `queryDocuments()`, a service `get` or another owner's
739
- single-record read inside a loop over the list is a read per row, however
740
- the loop is spelled (`for`, `map` with `Promise.all`, a helper called per
741
- item). So is a single-record helper reused for a list.
748
+ - A count that grows with the rows means something runs per row:
749
+ `getDocumentById()`, `queryDocuments()`, a service `get` or another owner's
750
+ single-record read inside a loop over the list, however the loop is spelled
751
+ (`for`, `map` with `Promise.all`, a helper called per item), or a
752
+ single-record helper reused for a list.
742
753
  - Work derived from a row (hashing, preparing a form, assembling a snapshot)
743
754
  runs once per distinct input in the call, not once per row it appears in.
744
- - The list's query count follows its tables and pages (100 rows a page unless
745
- the resource sets `queryMaxLimit`), never its rows: three hundred invoices
746
- and their lines take a handful of queries, not three hundred more (see
747
- [Budgets at realistic counts](#budgets-at-realistic-counts)).
755
+ - A JSON:API read also costs a few lookup and visibility queries per page and
756
+ relationship field, and pages hold the resource's `queryMaxLimit` (100 rows
757
+ unless it sets one). That cost follows pages, so three hundred invoices take
758
+ a few more queries than ten, not three hundred more ([Query counts at
759
+ realistic sizes](#query-counts-at-realistic-sizes)).
748
760
 
749
- Why: a read per row costs nothing with three test rows and seconds with a few
750
- hundred real ones, and every lookup and visibility check inside it repeats too.
761
+ Why: each JSKIT single-record read runs its lookups and row policy too, so a
762
+ read per row repeats all of them.
751
763
 
752
764
  ```js
753
- // good: one read per table, however many invoices there are
765
+ // good: a read per related table, however many invoices there are
754
766
  const invoiceRows = await queryAllJsonRestCollectionRows(api.resources.invoices, {
755
767
  queryParams: { filters: { workspace: workspaceId } },
756
768
  transaction: resolveTransaction(options),
@@ -784,21 +796,25 @@ paying a read policy for every row it writes.**
784
796
  [row policies heuristic](https://github.com/mobily-enterprises/jskit-ai/blob/main/packages/agent-docs/heuristics/row-policies.md)
785
797
  has the policy contract and its package composition.
786
798
  - The policy runs on every single-record read, and a CRUD write includes one:
787
- `createDocument()` and `patchDocumentById()` write with `returning: "full"`
788
- and read the record back through the policy, and `patch` and `delete` first
789
- look the record up through it. A write therefore pays the read policy for
790
- each row it writes.
791
- - A write path decides access once, inside its transaction. A batch write (a
792
- delivery, an import, a fan-out) whose per-row read-back would pay an
793
- expensive policy writes through a repository method added with
794
- `decorateRepository`, on `resolveRepoClient(database.knex, options)`,
795
- setting the same columns the resource would, its ownership columns
796
- included, and reading back only what it returns.
797
- - Its test runs one read and one write for a subject with a short history and
798
- with a realistic long one, and asserts the same query count.
799
-
800
- Why: a check sized by history grows with every record the subject receives,
801
- so the same write that takes milliseconds in a test takes seconds a year later.
799
+ `createDocument()` and `patchDocumentById()` default to `returning: "full"`,
800
+ which reads the record back through the policy, and `patch` and `delete`
801
+ first look the record up through it.
802
+ - A write path decides access once, inside its transaction. A write that does
803
+ not use its result passes `returning: "minimal"` or `"none"`, and a batch of
804
+ creates (a delivery, an import, a fan-out) goes through
805
+ `createDocuments(payloads, { trx, context })`, which reads nothing back
806
+ unless asked
807
+ ([writes that return less](https://mobily-enterprises.github.io/jskit-ai/guide/app-setup/database-layer#writes-that-return-less)).
808
+ Any other batch whose per-row lookup would pay an expensive policy writes
809
+ through a repository method added with `decorateRepository`, on
810
+ `resolveRepoClient(database.knex, options)`, setting the same columns the
811
+ resource would, its ownership columns included.
812
+ - Its test counts the queries of one read and one write for a subject with a
813
+ short history and for one with a long one ([Query counts at realistic
814
+ sizes](#query-counts-at-realistic-sizes)).
815
+
816
+ Why: the row policy runs on every read, write read-back and lookup, so every
817
+ request pays what it costs.
802
818
 
803
819
  ```js
804
820
  // good: one predicate on the row being read
@@ -935,7 +951,8 @@ carries on.**
935
951
  (`response.body.cancel()`), for queue tails whose error reaches the queued
936
952
  caller, and for client best effort, each with a short comment saying so.
937
953
 
938
- Why: `() => {}` hides a wiring `TypeError` along with the failure.
954
+ Why: a collaborator call's failure can be a wiring `TypeError`, and
955
+ `() => {}` hides it with the rest.
939
956
 
940
957
  ```js
941
958
  // good
@@ -957,8 +974,8 @@ frame.**
957
974
  - Complete a double with the method the code calls, returning its neutral
958
975
  value; never shape production code around a double.
959
976
 
960
- Why: a double that mirrors internals pins them, so the test breaks on a
961
- refactor and passes on a real bug.
977
+ Why: an owner's seams are its contract; its id formats and stream frames
978
+ change with its package.
962
979
 
963
980
  ```js
964
981
  // good
@@ -977,8 +994,8 @@ double.**
977
994
  - One named clock per factory (`now = Date.now`) is a platform default, not a
978
995
  test seam.
979
996
 
980
- Why: a seam kept for tests is a shape no real caller uses, and a reader
981
- cannot tell it apart from one that matters.
997
+ Why: a factory's parameters are its contract with the features that call it,
998
+ and a test is not one of them.
982
999
 
983
1000
  ```js
984
1001
  // good
@@ -997,8 +1014,8 @@ through the feature, and pure helpers through `__testables`.**
997
1014
  - `__testables` holds pure helpers only, never one that touches a
998
1015
  collaborator or reads request facts.
999
1016
 
1000
- Why: a test-only export widens the module's API and invites production
1001
- callers.
1017
+ Why: `__testables` is the one way a test reaches inside a module, so every
1018
+ other export is production API.
1002
1019
 
1003
1020
  ```js
1004
1021
  // good
@@ -1011,107 +1028,171 @@ export function resetInvoiceCacheForTests() {
1011
1028
  }
1012
1029
  ```
1013
1030
 
1014
- ### Budgets at realistic counts
1015
-
1016
- **A list, batch or write path has a database test at the row counts and
1017
- history a real workspace holds, and that test fails above a fixed query
1018
- budget, one that a read per row or a history-sized check cannot meet.**
1019
-
1020
- - Seed hundreds of records and long per-person histories, not one to three
1021
- rows: a read per row and a history-sized policy cost nothing at three.
1022
- - Count the queries with the test Knex instance's `query` event and assert a
1023
- ceiling: the count the correct implementation needs at that size, which a
1024
- read per row overshoots by hundreds. A path that pages nothing (one read,
1025
- one write) asserts the same count at a short and a long history. A query
1026
- count is stable in CI; also report the time the call took.
1027
- - Meet the budget by fixing the reads ([One read per
1028
- table](#one-read-per-table), [Bounded access checks](#bounded-access-checks)),
1029
- never by shrinking the data or raising the budget.
1030
-
1031
- Why: without a budget at real counts, a path that does one read per row ships,
1032
- and its first sign is a request that takes seconds in production.
1031
+ ### Query counts at realistic sizes
1032
+
1033
+ **A list, batch or write path's database test counts its queries with
1034
+ `countQueries()` at a few rows and at a realistic number, and fails when the
1035
+ count grows with the rows instead of the pages.**
1036
+
1037
+ - `countQueries(knex, work)`, from
1038
+ `@jskit-ai/database-runtime/server/testDatabases`, runs `work` and returns
1039
+ its `result`, the `count` of statements the Knex instance or transaction
1040
+ sent meanwhile (a transaction's `BEGIN` and `COMMIT` included) and their
1041
+ SQL in `queries`. Pass the Knex the path runs on: the one the test built its
1042
+ repository or service with.
1043
+ - A route is counted through the server the test boots in its own process
1044
+ (`createServer()`, then `app.inject()`): `countRuntimeQueries(work)`, from
1045
+ the same module, records what the app's own `runtime.database` sends while
1046
+ `work` runs, and leaves out the test's own Knex. A server in another process
1047
+ is out of its reach, so the route's guard boots it in the test.
1048
+ - Pages add a few queries; rows should add none. A path that pages nothing
1049
+ (one read, one write) compares a short and a long history.
1050
+ - When the counts differ more than the pages explain, read `queries`: the
1051
+ statement that repeats once per row is the one to batch. Judge the
1052
+ comparison by what the path has to read; never tune a fixed ceiling until
1053
+ it passes.
1054
+ - When it fails, fix the reads ([Lists read related rows in
1055
+ batches](#lists-read-related-rows-in-batches), [Bounded access
1056
+ checks](#bounded-access-checks)).
1057
+
1058
+ Why: JSON:API reads add queries per page and relationship field, so comparing
1059
+ two sizes separates that cost from a read per row, where one fixed number
1060
+ cannot.
1033
1061
 
1034
1062
  ```js
1035
1063
  // good
1036
- test("the invoice list reads each table once", async () => {
1037
- await seedInvoices(knex, { workspace, count: 300, linesEach: 5 });
1038
- let queries = 0;
1039
- const count = () => { queries += 1; };
1040
- knex.on("query", count);
1041
- try {
1042
- await invoices.listWithLines(workspace, { context });
1043
- } finally {
1044
- knex.off("query", count);
1045
- }
1046
- // A page of each table at a time; one read per invoice would add 300.
1047
- assert.ok(queries <= 40, `${queries} queries`);
1064
+ import { countQueries, countRuntimeQueries } from "@jskit-ai/database-runtime/server/testDatabases";
1065
+
1066
+ test("the invoice list's queries follow its pages, not its invoices", async () => {
1067
+ const listAt = async (count) => {
1068
+ const workspace = await createWorkspace(knex);
1069
+ await seedInvoices(knex, { workspace, count, linesEach: 5 });
1070
+ return countQueries(knex, () => invoices.listWithLines(workspace, { context }));
1071
+ };
1072
+ const few = await listAt(10);
1073
+ const many = await listAt(300);
1074
+ // Thirty times the invoices may add pages; a read per invoice would add about 290 queries.
1075
+ assert.ok(many.count <= few.count * 2,
1076
+ `${few.count} queries for 10 invoices, ${many.count} for 300:\n${many.queries.join("\n")}`);
1077
+ });
1078
+
1079
+ // good: the same comparison for the route, through the server this test booted
1080
+ test("GET /api/invoices reads per page, not per invoice", async () => {
1081
+ const app = await createServer({ runtimeEnv });
1082
+ const listAt = async (count) => {
1083
+ const workspace = await createWorkspace(knex);
1084
+ await seedInvoices(knex, { workspace, count, linesEach: 5 });
1085
+ return countRuntimeQueries(() => app.inject({ url: invoicesUrl(workspace), headers: signedIn }));
1086
+ };
1087
+ const few = await listAt(10);
1088
+ const many = await listAt(300);
1089
+ assert.ok(many.count <= few.count * 2,
1090
+ `${few.count} queries for 10 invoices, ${many.count} for 300:\n${many.queries.join("\n")}`);
1048
1091
  });
1049
1092
 
1050
- // bad: three invoices and no budget, so a read per invoice passes
1093
+ // bad: three invoices, nothing counted, so a read per invoice passes
1051
1094
  test("the invoice list has its lines", async () => {
1052
1095
  await seedInvoices(knex, { workspace, count: 3, linesEach: 1 });
1053
1096
  assert.equal((await invoices.listWithLines(workspace, { context })).length, 3);
1054
1097
  });
1055
1098
  ```
1056
1099
 
1057
- ### Time limits are not fixes
1058
-
1059
- **A slow test, suite, preparation or request is a defect to find, never a time
1060
- limit to raise. Measure where the time goes, fix that cause, and keep the
1061
- default limits.**
1062
-
1063
- - Raising `--test-timeout`, a test's `{ timeout }`, Playwright's `timeout`,
1064
- `test.setTimeout()` or an `expect` timeout, adding retries, or calling a
1065
- request in a loop until it answers hides the cause and lets it grow.
1066
- - Read the preparation phases' durations and the query counts first; the
1067
- cause is usually a read per row, a history-sized policy, or a database
1068
- prepared by replaying migrations ([Test databases are prepared
1069
- once](#test-databases-are-prepared-once)).
1070
- - A longer limit is right only for work that is long by nature, such as a
1071
- fixture of deliberate size, and the test names that work.
1072
- - A time limit still bounds every run, so a hang ends; it is not a speed
1073
- setting.
1074
-
1075
- Why: a raised limit absorbs today's slowness and every later one, until a run
1076
- takes hours and nobody knows which change made it slow.
1100
+ ### Test files run in parallel
1101
+
1102
+ **A test suite runs its files in parallel, as many at a time as the machine's
1103
+ test budget allows: never one file at a time, and never more than the machine
1104
+ can take. Node's test runner runs through `jskit test`; Playwright's `workers`
1105
+ and Vitest's `maxWorkers` come from `testConcurrency()`.**
1106
+
1107
+ - The budget is `V64_TEST_CONCURRENCY` when it is a positive whole number (a
1108
+ test runner that hands out slots exports it for each run), otherwise half of
1109
+ the machine's CPUs, rounded down and at least 1: a 2-vCPU VPS runs one file
1110
+ at a time, an 8-thread machine four.
1111
+ - `jskit test` (from `@jskit-ai/jskit-catalog`) passes its arguments to
1112
+ `node --test` and sets `--test-concurrency` to the budget.
1113
+ `testConcurrency()` from `@jskit-ai/jskit-catalog/test-budget` returns the
1114
+ same number for a config.
1115
+ - No script passes a fixed concurrency or worker count. `jskit check` fails a
1116
+ package script that runs `node --test` directly or passes one, and a
1117
+ Playwright or Vitest config those scripts use whose workers do not come from
1118
+ `testConcurrency()` ([test-run rules](#test-run-rules)).
1119
+ - Each file isolates what it shares: a port the system picks (`listen(0)`), a
1120
+ directory from `mkdtemp`, its own database or schema with a name no other
1121
+ file uses, its own Vite `cacheDir` for each dev server it starts (Vite
1122
+ replaces a shared dependency cache under servers still reading it), and no
1123
+ writes into the source tree. Files that collide are isolated, never
1124
+ serialised. A database test file gets its own copy of `TEST_DB_NAME` from
1125
+ `jskit test --databases`
1126
+ ([integration test databases](https://mobily-enterprises.github.io/jskit-ai/guide/app-setup/database-layer#integration-test-databases)).
1127
+ - An application's `playwright.config.mjs` is
1128
+ `defineConfig(browserTestConfig({ testDir: "./tests/e2e" }))` from
1129
+ `@jskit-ai/jskit-catalog/browser-tests`. It builds the production frontend
1130
+ once (reused while its sources are unchanged) and gives each worker its own
1131
+ server, port and, through its `prepare` module, its own seeded database; a
1132
+ spec file's tests run in order in one worker. With `PLAYWRIGHT_BASE_URL`, the
1133
+ tests use that managed target as its host serves it. Every run is bounded by
1134
+ a `globalTimeout` (20 minutes, or the suite's own measured bound) and ends
1135
+ with its 10 slowest tests. With a `signIn` module, each worker's server is
1136
+ signed in once and every test of that worker starts signed in, instead of
1137
+ each test signing in. `jskit check` fails a webServer that runs the development server
1138
+ (`npm run develop`, `vite`), one webServer shared by every worker, and a
1139
+ config without a `globalTimeout`.
1140
+ - A file that truly cannot run beside others, because it holds a resource of
1141
+ the whole machine, says why on its first line,
1142
+ `// jskit-test: alone - <reason>`. `jskit test` runs it after the others,
1143
+ one such file at a time, and fails on a marker without a reason or below the
1144
+ first line.
1145
+ - `jskit test` ends with its 10 slowest test files and tests, records each
1146
+ file's time in `node_modules/.cache/jskit-test/file-times.json`, and fails
1147
+ when a file takes more than 10 minutes or the whole command more than 45:
1148
+ bounds only a clearly broken suite reaches, such as a file waiting out its
1149
+ timeouts. A suite whose measured work needs more sets `runMs`, `fileMs` or a
1150
+ file's own bound in `test-time-bounds.json`, with the measurement in the
1151
+ commit that raises it; a file that got slow is fixed instead. A file still
1152
+ running at twice its bound, or sooner at ten times the time it last finished
1153
+ in (at least two minutes), is stopped and fails the run by name, so the other
1154
+ files still finish and report: look for what it leaves open (a server, child
1155
+ process, timer or socket) or a test without a timeout. A file with its own
1156
+ bound is stopped at twice that bound.
1157
+
1158
+ Why: `jskit test` and `testConcurrency()` read one budget, so every runner
1159
+ uses the machine without starving the other runs on it.
1077
1160
 
1078
1161
  ```js
1079
- // good: the slow step was found and fixed, so the default limit holds
1080
- test("delivers a batch of notices", async () => {
1081
- await deliveries.processBatch(workspace, { context });
1082
- });
1162
+ // good: package.json "test": "jskit test", and playwright.config.mjs
1163
+ import { browserTestConfig } from "@jskit-ai/jskit-catalog/browser-tests";
1083
1164
 
1084
- // bad
1085
- test("delivers a batch of notices", { timeout: 600_000 }, async () => {
1086
- await deliveries.processBatch(workspace, { context });
1087
- });
1165
+ export default defineConfig(browserTestConfig({ testDir: "./tests/e2e" }));
1166
+
1167
+ // bad: "test": "node --test --test-concurrency=1", and
1168
+ export default defineConfig({ workers: 1, webServer: { command: "npm run develop" } });
1088
1169
  ```
1089
1170
 
1090
1171
  ### Test databases are prepared once
1091
1172
 
1092
- **A test run prepares each database once, from a stored snapshot of its
1093
- schema, migration ledger and seed rows checked by a digest of their sources,
1094
- and never replays the whole migration history inside the run.**
1173
+ **A test run prepares each database once and restores it from a stored
1174
+ snapshot of the schema and the rows the migrations write, migration ledger
1175
+ included, checked by a digest of the migrations.**
1095
1176
 
1096
- - The snapshot is rebuilt only when the digest of the migration and seed
1097
- sources changes, once and outside the timed tests; every disposable
1177
+ - The snapshot is rebuilt only when the digest of the migrations' names and
1178
+ contents changes, once and outside the timed tests; every disposable
1098
1179
  database, and every database a test migrated up or down, is restored from
1099
- it.
1180
+ it. A browser run's fixture seed runs after each restore.
1100
1181
  - The full migration history runs once, in its own fresh-schema check
1101
1182
  ([database verification](https://mobily-enterprises.github.io/jskit-ai/guide/app-setup/database-layer#verification)).
1102
- - The harness reports each preparation phase and its duration, and the suite
1103
- has a time budget, so slow preparation shows instead of hiding inside the
1183
+ - The harness reports each preparation phase and its duration, and those
1184
+ durations are read, so slow preparation shows instead of hiding inside the
1104
1185
  next test that waits for it.
1105
1186
  - The database layer guide's [integration test
1106
1187
  databases](https://mobily-enterprises.github.io/jskit-ai/guide/app-setup/database-layer#integration-test-databases)
1107
1188
  gives the snapshot's contents per engine.
1108
1189
 
1109
- Why: the migration history only grows, so a run that replays it whenever a
1110
- schema changed gets slower with every migration the application adds.
1190
+ Why: an application's migrations include those of every installed package, so
1191
+ replaying them costs more with each package as well as each migration.
1111
1192
 
1112
1193
  ```js
1113
- // good: the application's test support restores the snapshot its digest names
1114
- before(async () => restoreTestDatabase(knex, await testDatabaseSnapshot()));
1194
+ // good: package.json "test": "jskit test --databases"; each file gets a restored copy of TEST_DB_NAME
1195
+ const { databases } = await prepareTestDatabases({ knexConfig, environment: process.env, count: workers, seed });
1115
1196
 
1116
1197
  // bad: every file replays every migration
1117
1198
  before(() => knex.migrate.latest());
@@ -1125,6 +1206,10 @@ before(() => knex.migrate.latest());
1125
1206
  code: server, shared, client (`.vue` and `.js`) and scripts. One concern per
1126
1207
  factory.**
1127
1208
 
1209
+ - The limit is an outer bound, not a size to aim for: a file near it usually
1210
+ holds more than one concern. Split it by concern; never trim lines or join
1211
+ statements to fit.
1212
+
1128
1213
  Why: a reader holds one concern at a time; a 2,000-line closure holds ten.
1129
1214
 
1130
1215
  ```text
@@ -1224,8 +1309,9 @@ Why: a comment that narrates history is wrong the day after it is written.
1224
1309
  ### Running the checks
1225
1310
 
1226
1311
  `npm run lint` runs ESLint with `@jskit-ai/config-eslint`, and `npm run
1227
- verify` runs `jskit check` (the package graph), lint, the tests and the
1228
- build. The foundations' `eslint.config.mjs` already holds a new
1312
+ verify` runs `jskit check` (the package graph, navigation and [test
1313
+ runs](#test-run-rules)), lint, the tests and the build. The foundations'
1314
+ `eslint.config.mjs` already holds a new
1229
1315
  application's code to every check:
1230
1316
 
1231
1317
  ```js
@@ -1297,6 +1383,22 @@ Every finding ends with a link to the section of this page that explains it.
1297
1383
  | `jskit/allow-comment-form` | a `readable-check: allow` reason without a prefix from the table below, or one that cites tests or the state of a project | [Exceptions](#exceptions) |
1298
1384
  | `max-lines`, `max-lines-per-function` | files over 750 lines, functions over 600 | [Size limits](#size-limits) |
1299
1385
 
1386
+ ### Test-run rules
1387
+
1388
+ `jskit check` reports these in every package script, and in the Playwright
1389
+ and Vitest configs those scripts use. No comment excuses them; a test file
1390
+ that must run alone says so on its own first line instead.
1391
+
1392
+ | Rule | Reports | Section |
1393
+ | --- | --- | --- |
1394
+ | `test/node-test` | a script that runs `node --test` directly instead of `jskit test` | [Test files run in parallel](#test-files-run-in-parallel) |
1395
+ | `test/fixed-concurrency` | `--test-concurrency` in a script, or a script that pins `V64_TEST_CONCURRENCY` or `PLAYWRIGHT_WORKERS` | [Test files run in parallel](#test-files-run-in-parallel) |
1396
+ | `test/playwright-workers` | `--workers` or `-j` passed to `playwright test`, its config without `workers: testConcurrency()` or `browserTestConfig()`, or with `workers` set over `browserTestConfig()`'s | [Test files run in parallel](#test-files-run-in-parallel) |
1397
+ | `test/playwright-dev-server` | a Playwright webServer command that runs the development server (`npm run develop`, `npm run dev`, `bin/develop.js`, `vite`, `vite dev`) | [Test files run in parallel](#test-files-run-in-parallel) |
1398
+ | `test/playwright-isolation` | a Playwright webServer every worker shares, without `browserTestConfig()` | [Test files run in parallel](#test-files-run-in-parallel) |
1399
+ | `test/playwright-global-timeout` | a Playwright config without a `globalTimeout` (or `browserTestConfig()`), or with `globalTimeout: 0` | [Test files run in parallel](#test-files-run-in-parallel) |
1400
+ | `test/vitest-workers` | `--maxWorkers`, `--minWorkers` or `--no-file-parallelism` passed to Vitest, or its config without `maxWorkers: testConcurrency()` or with `fileParallelism: false` | [Test files run in parallel](#test-files-run-in-parallel) |
1401
+
1300
1402
  The rules see shapes, not intent. Feature files, owned state beyond
1301
1403
  registries, shutdown, loggers, shared action fields, strict inputs, the action
1302
1404
  policy, request context, validated input, one reader per fact, routes,
@@ -36,7 +36,9 @@ Cancel, Close, and a wizard's Previous step; keep existing unsaved-change
36
36
  guards.
37
37
 
38
38
  `createShellRouter()` owns scroll position (`shell/application-shell`
39
- pattern); never reset scroll in page components.
39
+ pattern); never reset scroll in page components. An app's own scroll behaviour
40
+ is its `scrollBehavior` option, never a `createRouter` wrapper's (that throws);
41
+ a hash target opens below its `scroll-margin-top`.
40
42
 
41
43
  ## Application-owned files
42
44
 
@@ -35,6 +35,9 @@ apply or adapt. It does not prescribe an agent host or project orchestrator.
35
35
  - [Material 3](/guide/framework/material-3)
36
36
  - [UI pass](/guide/framework/ui-pass): make a task's changes, a section or the
37
37
  whole app exact Material 3
38
+ - [Sanity checks](/guide/framework/sanity-checks): JSKIT's own pitfalls, and
39
+ the JSKIT calls that check data access, tests, a refactor, a screen or a
40
+ deployment before calling it done
38
41
  - [Translating a JSKIT app](/guide/framework/translation)
39
42
  - [Complete source pattern library](/patterns/)
40
43
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/agent-docs",
3
- "version": "0.1.279",
3
+ "version": "0.1.281",
4
4
  "description": "Distributed JSKIT agent references, prompts, guides, and generated reference maps.",
5
5
  "type": "module",
6
6
  "files": [
@@ -10,7 +10,7 @@
10
10
  "skills"
11
11
  ],
12
12
  "scripts": {
13
- "test": "node --test --test-concurrency=1 test/*.test.js"
13
+ "test": "jskit test test/*.test.js"
14
14
  },
15
15
  "engines": {
16
16
  "node": "^22.13.0 || ^24.0.0 || ^26.0.0"
@@ -5,7 +5,7 @@
5
5
  "private": true,
6
6
  "type": "module",
7
7
  "dependencies": {
8
- "@jskit-ai/kernel": "0.2.10",
8
+ "@jskit-ai/kernel": "0.2.12",
9
9
  "json-rest-schema": "^1.0.17"
10
10
  },
11
11
  "exports": {
@@ -18,8 +18,12 @@ tests, browser tests, and the normal build entrypoints.
18
18
 
19
19
  The example is a concrete application named `reading-room`. Copy the useful
20
20
  files into the project root, rename the application in normal source and
21
- package metadata, and then edit the product-facing page. The files become
22
- ordinary application source immediately.
21
+ package metadata, and then edit the product-facing page. The name is spelt in
22
+ `package.json`, `app.json` and `index.html`'s `<title>` (the app's name, which
23
+ the shell's messages use), and, once the app adds sign-in, in the
24
+ `auth/auth-surface` pattern's `src/components/AppBrand.vue` (the brand on the
25
+ sign-in screens); rename them together. The files become ordinary application
26
+ source immediately.
23
27
 
24
28
  ## Do not use when
25
29
 
@@ -29,11 +29,28 @@ Do not install an unrelated runtime package only to read documentation.
29
29
  Before adding or changing browser-test setup, read the browser verification
30
30
  guide at
31
31
  `https://mobily-enterprises.github.io/jskit-ai/guide/framework/ui-operations#browser-verification`.
32
- Routine tests reuse the isolated schema and prepare fixtures once per suite
33
- invocation. Keep `BROWSER_TEST_DB_NAME` distinct from disposable `TEST_DB_NAME`
34
- and prove that disposable cleanup preserves browser migrations. Require
35
- repeat-start regression proof; do not rebuild the database or production
36
- frontend for each focused browser check.
32
+ `playwright.config.mjs` is `browserTestConfig()` from
33
+ `@jskit-ai/jskit-catalog/browser-tests`: every Playwright worker gets its own
34
+ server on the production build, its own port and, through the config's
35
+ `prepare` module, its own copy of the browser database, restored from a schema
36
+ snapshot and seeded. A local run never starts the development server; with
37
+ `PLAYWRIGHT_BASE_URL`, the tests use that managed target as its host serves
38
+ it. Keep `BROWSER_TEST_DB_NAME` distinct from disposable `TEST_DB_NAME`. The build and
39
+ the snapshot are reused while their sources are unchanged; do not rebuild the
40
+ database or production frontend for each focused browser check. Require
41
+ repeat-start regression proof.
42
+
43
+ Tests run in parallel within the machine's test budget: run node's test
44
+ runner through `jskit test` (`npm test`), with `--databases` once the app has a
45
+ database so each file running at once gets its own copy of `TEST_DB_NAME`,
46
+ Playwright through `browserTestConfig()`, and take Vitest's `maxWorkers` from
47
+ `testConcurrency()` of
48
+ `@jskit-ai/jskit-catalog/test-budget`; never one file at a time or a fixed
49
+ count. Isolate files that collide (their own port, temporary directory and
50
+ database) instead of serialising them; a file that truly must run alone says
51
+ why on its first line, `// jskit-test: alone - <reason>`. Plain `jskit check`
52
+ fails anything else
53
+ (`https://mobily-enterprises.github.io/jskit-ai/guide/framework/typed-features#test-files-run-in-parallel`).
37
54
 
38
55
  Keep verification output small. Run the relevant cases together, save complete
39
56
  stdout/stderr to local artifacts, and preserve the exit status. Report counts,