@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.
- package/guide/agent/app-extras/assistant-conversation.md +5 -1
- package/guide/agent/app-extras/assistant.md +123 -0
- package/guide/agent/app-extras/realtime.md +20 -10
- package/guide/agent/app-setup/authentication.md +54 -4
- package/guide/agent/app-setup/database-layer.md +108 -35
- package/guide/agent/app-setup/existing-application-migration.md +7 -1
- package/guide/agent/app-setup/material-3-migration.md +38 -9
- package/guide/agent/app-setup/multi-homing.md +14 -4
- package/guide/agent/app-setup/upgrading-jskit.md +105 -0
- package/guide/agent/app-setup/users.md +9 -1
- package/guide/agent/framework/client-features.md +35 -1
- package/guide/agent/framework/material-3.md +111 -71
- package/guide/agent/framework/sanity-checks.md +181 -0
- package/guide/agent/framework/translation.md +4 -0
- package/guide/agent/framework/typed-features.md +222 -120
- package/guide/agent/framework/ui-operations.md +3 -1
- package/guide/agent/index.md +3 -0
- package/package.json +2 -2
- package/patterns/feature-package/example/booking-engine/package.json +1 -1
- package/patterns/minimal-foundation/PATTERN.md +6 -2
- package/patterns/minimal-foundation/example/AGENTS.md +22 -5
- package/patterns/minimal-foundation/example/package.json +6 -6
- package/patterns/minimal-foundation/example/playwright.config.mjs +3 -30
- package/patterns/minimal-foundation/example/server.js +7 -1
- package/patterns/minimal-foundation/example/src/main.js +3 -2
- package/patterns/minimal-foundation/example/tests/e2e/startup.spec.ts +13 -0
- package/patterns/minimal-foundation/example/vite.config.mjs +3 -1
- package/patterns/shell-foundation/PATTERN.md +4 -1
- package/patterns/shell-foundation/example/AGENTS.md +22 -5
- package/patterns/shell-foundation/example/package.json +7 -7
- package/patterns/shell-foundation/example/packages/main/package.json +1 -1
- package/patterns/shell-foundation/example/packages/main/src/client/providers/MainClientProvider.js +3 -0
- package/patterns/shell-foundation/example/playwright.config.mjs +3 -30
- package/patterns/shell-foundation/example/server.js +7 -1
- package/patterns/shell-foundation/example/src/components/menus/SectionTabLinkItem.vue +42 -0
- package/patterns/shell-foundation/example/src/main.js +3 -2
- package/patterns/shell-foundation/example/src/pages/home.vue +1 -1
- package/patterns/shell-foundation/example/src/views/NotFound.vue +1 -1
- package/patterns/shell-foundation/example/tests/e2e/startup.spec.ts +13 -0
- package/patterns/shell-foundation/example/vite.config.mjs +3 -1
- package/reference/autogen/PATTERN_INDEX.md +37 -37
- package/reference/autogen/packages/agent-docs.md +4 -0
- package/reference/autogen/packages/architecture-inspector.md +1 -0
- package/reference/autogen/packages/assistant-runtime.md +28 -3
- package/reference/autogen/packages/auth-provider-local-core.md +2 -1
- package/reference/autogen/packages/auth-web.md +10 -0
- package/reference/autogen/packages/connectors-catalog.md +8 -0
- package/reference/autogen/packages/crud-core.md +3 -0
- package/reference/autogen/packages/database-runtime.md +33 -0
- package/reference/autogen/packages/http-web.md +0 -4
- package/reference/autogen/packages/json-rest-api-core.md +17 -4
- package/reference/autogen/packages/kernel.md +37 -2
- package/reference/autogen/packages/material-web.md +2 -1
- package/reference/autogen/packages/oidc-issuer-core.md +0 -1
- package/reference/autogen/packages/realtime.md +8 -5
- package/reference/autogen/packages/shell-web.md +88 -3
- package/reference/autogen/packages/users-web.md +5 -2
- package/reference/autogen/packages/workspaces-core.md +3 -1
- package/reference/autogen/tooling/jskit-catalog.md +122 -5
- package/reference/autogen/tooling/testUtils.md +15 -1
- package/skills/jskit/SKILL.md +17 -5
- package/skills/jskit/references/client-features.md +35 -1
- package/skills/jskit/references/existing-application-migration.md +7 -1
- package/skills/jskit/references/heuristics/INDEX.md +3 -1
- package/skills/jskit/references/heuristics/row-policies.md +3 -3
- package/skills/jskit/references/heuristics/typed-features.md +7 -7
- package/skills/jskit/references/heuristics/ui-testing.md +27 -21
- package/skills/jskit/references/material-3.md +111 -71
- package/skills/jskit/references/multi-homing.md +14 -4
- package/skills/jskit/references/pattern-index.md +37 -37
- package/skills/jskit/references/patterns/app/minimal-foundation/PATTERN.md +6 -2
- package/skills/jskit/references/patterns/app/minimal-foundation/example/AGENTS.md +22 -5
- package/skills/jskit/references/patterns/app/minimal-foundation/example/package.json +6 -6
- package/skills/jskit/references/patterns/app/minimal-foundation/example/playwright.config.mjs +3 -30
- package/skills/jskit/references/patterns/app/minimal-foundation/example/server.js +7 -1
- package/skills/jskit/references/patterns/app/minimal-foundation/example/src/main.js +3 -2
- package/skills/jskit/references/patterns/app/minimal-foundation/example/tests/e2e/startup.spec.ts +13 -0
- package/skills/jskit/references/patterns/app/minimal-foundation/example/vite.config.mjs +3 -1
- package/skills/jskit/references/patterns/app/shell-foundation/PATTERN.md +4 -1
- package/skills/jskit/references/patterns/app/shell-foundation/example/AGENTS.md +22 -5
- package/skills/jskit/references/patterns/app/shell-foundation/example/package.json +7 -7
- package/skills/jskit/references/patterns/app/shell-foundation/example/packages/main/package.json +1 -1
- package/skills/jskit/references/patterns/app/shell-foundation/example/packages/main/src/client/providers/MainClientProvider.js +3 -0
- package/skills/jskit/references/patterns/app/shell-foundation/example/playwright.config.mjs +3 -30
- package/skills/jskit/references/patterns/app/shell-foundation/example/server.js +7 -1
- package/skills/jskit/references/patterns/app/shell-foundation/example/src/components/menus/SectionTabLinkItem.vue +42 -0
- package/skills/jskit/references/patterns/app/shell-foundation/example/src/main.js +3 -2
- package/skills/jskit/references/patterns/app/shell-foundation/example/src/pages/home.vue +1 -1
- package/skills/jskit/references/patterns/app/shell-foundation/example/src/views/NotFound.vue +1 -1
- package/skills/jskit/references/patterns/app/shell-foundation/example/tests/e2e/startup.spec.ts +13 -0
- package/skills/jskit/references/patterns/app/shell-foundation/example/vite.config.mjs +3 -1
- package/skills/jskit/references/patterns/auth/accounts-hub/example/package.json +2 -2
- package/skills/jskit/references/patterns/auth/auth-surface/PATTERN.md +18 -5
- package/skills/jskit/references/patterns/auth/auth-surface/example/src/components/AppBrand.vue +17 -0
- package/skills/jskit/references/patterns/auth/auth-surface/example/src/pages/auth/login.vue +2 -2
- package/skills/jskit/references/patterns/auth/auth-surface/example/src/pages/auth/reset-password.vue +2 -2
- package/skills/jskit/references/patterns/auth/auth-surface/example/src/pages/auth/signout.vue +2 -2
- package/skills/jskit/references/patterns/auth/auth-surface/example/src/views/auth/LoginView.vue +6 -1
- package/skills/jskit/references/patterns/auth/auth-surface/example/src/views/auth/ResetPasswordView.vue +6 -1
- package/skills/jskit/references/patterns/auth/auth-surface/example/src/views/auth/SignOutView.vue +6 -1
- package/skills/jskit/references/patterns/auth/oidc-sign-in/example/package.json +2 -2
- package/skills/jskit/references/patterns/auth/supabase-auth/example/package.json +1 -1
- package/skills/jskit/references/patterns/connectors/aws-storage-queries/example/formats/package.json +1 -1
- package/skills/jskit/references/patterns/connectors/calendar-cli/example/package.json +4 -4
- package/skills/jskit/references/patterns/console/console-surface/example/src/pages/console.vue +1 -1
- package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/package.json +2 -2
- package/skills/jskit/references/patterns/database/mysql-application/PATTERN.md +34 -24
- package/skills/jskit/references/patterns/database/mysql-application/example/package.json +3 -2
- package/skills/jskit/references/patterns/database/mysql-application/example/tests/e2e/databases.js +25 -0
- package/skills/jskit/references/patterns/database/postgres-application/PATTERN.md +12 -0
- package/skills/jskit/references/patterns/database/postgres-application/example/package.json +3 -2
- package/skills/jskit/references/patterns/database/postgres-application/example/tests/e2e/databases.js +25 -0
- package/skills/jskit/references/patterns/realtime/realtime-application/example/package.json +2 -2
- package/skills/jskit/references/patterns/rewards/google-rewarded/example/googlePublisherTag.js +5 -2
- package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/package.json +1 -1
- package/skills/jskit/references/patterns/shell/application-shell/PATTERN.md +11 -6
- package/skills/jskit/references/patterns/shell/application-shell/example/src/components/menus/SectionTabLinkItem.vue +42 -0
- package/skills/jskit/references/patterns/shell/application-shell/example/src/pages/home.vue +1 -1
- package/skills/jskit/references/patterns/shell/application-shell/example/src/views/NotFound.vue +1 -1
- package/skills/jskit/references/patterns/ui/page-and-placement/PATTERN.md +6 -0
- package/skills/jskit/references/patterns/ui/page-and-placement/example/src/pages/home/reports.vue +9 -4
- package/skills/jskit/references/patterns/ui/page-and-placement/example/src/placementTopology.js +5 -3
- package/skills/jskit/references/patterns/users/account-settings/PATTERN.md +24 -3
- package/skills/jskit/references/patterns/users/account-settings/example/src/components/account/settings/AccountSettingsNotificationsSection.vue +1 -1
- package/skills/jskit/references/patterns/users/account-settings/example/src/components/account/settings/AccountSettingsPreferencesSection.vue +2 -1
- package/skills/jskit/references/patterns/users/account-settings/example/src/components/account/settings/AccountSettingsProfileSection.vue +3 -2
- package/skills/jskit/references/patterns/users/account-settings/example/src/pages/account/[section].vue +23 -0
- package/skills/jskit/references/patterns/users/account-settings/example/src/pages/account.vue +4 -3
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/package.json +2 -2
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/package.json +3 -3
- package/skills/jskit/references/patterns/workspaces/workspace-surfaces/example/packages/main/src/client/components/AccountPendingInvitesCue.vue +3 -3
- package/skills/jskit/references/patterns/workspaces/workspace-surfaces/example/src/surfaces/admin/root.vue +1 -1
- package/skills/jskit/references/patterns/workspaces/workspace-surfaces/example/src/surfaces/app/root.vue +1 -1
- package/skills/jskit/references/sanity-checks.md +181 -0
- package/skills/jskit/references/translation.md +4 -0
- package/skills/jskit/references/typed-features.md +222 -120
- package/skills/jskit/references/ui-operations.md +3 -1
- 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:
|
|
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
|
|
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
|
-
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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
|
-
###
|
|
737
|
+
### Lists read related rows in batches
|
|
728
738
|
|
|
729
|
-
**A read that builds a list
|
|
730
|
-
query per table
|
|
731
|
-
|
|
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
|
|
734
|
-
|
|
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
|
-
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
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
|
-
-
|
|
745
|
-
the resource
|
|
746
|
-
|
|
747
|
-
|
|
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:
|
|
750
|
-
|
|
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:
|
|
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()`
|
|
788
|
-
|
|
789
|
-
look the record up through it.
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
delivery, an import, a fan-out)
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
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:
|
|
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:
|
|
961
|
-
|
|
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
|
|
981
|
-
|
|
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
|
|
1001
|
-
|
|
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
|
-
###
|
|
1015
|
-
|
|
1016
|
-
**A list, batch or write path
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
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
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
const
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
//
|
|
1047
|
-
assert.ok(
|
|
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
|
|
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
|
-
###
|
|
1058
|
-
|
|
1059
|
-
**A
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
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:
|
|
1080
|
-
|
|
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
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
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
|
|
1093
|
-
|
|
1094
|
-
|
|
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
|
|
1097
|
-
|
|
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
|
|
1103
|
-
|
|
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:
|
|
1110
|
-
|
|
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:
|
|
1114
|
-
|
|
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
|
|
1228
|
-
build. The foundations'
|
|
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
|
|
package/guide/agent/index.md
CHANGED
|
@@ -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.
|
|
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": "
|
|
13
|
+
"test": "jskit test test/*.test.js"
|
|
14
14
|
},
|
|
15
15
|
"engines": {
|
|
16
16
|
"node": "^22.13.0 || ^24.0.0 || ^26.0.0"
|
|
@@ -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
|
|
22
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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,
|