@jskit-ai/agent-docs 0.1.278 → 0.1.280
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.md +123 -0
- package/guide/agent/app-setup/authentication.md +46 -4
- package/guide/agent/app-setup/database-layer.md +35 -6
- package/guide/agent/app-setup/material-3-migration.md +53 -10
- package/guide/agent/app-setup/multi-homing.md +8 -2
- package/guide/agent/app-setup/upgrading-jskit.md +30 -0
- package/guide/agent/app-setup/users.md +9 -1
- package/guide/agent/framework/material-3.md +153 -71
- package/guide/agent/framework/sanity-checks.md +180 -0
- package/guide/agent/framework/translation.md +4 -0
- package/guide/agent/framework/typed-features.md +158 -105
- 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 +10 -0
- package/patterns/minimal-foundation/example/package.json +6 -6
- package/patterns/minimal-foundation/example/playwright.config.mjs +3 -0
- package/patterns/minimal-foundation/example/src/main.js +3 -2
- 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 +10 -0
- 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 -0
- 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/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/assistant-runtime.md +27 -2
- package/reference/autogen/packages/auth-provider-local-core.md +1 -1
- package/reference/autogen/packages/auth-web.md +10 -0
- package/reference/autogen/packages/crud-core.md +3 -0
- package/reference/autogen/packages/http-web.md +0 -6
- package/reference/autogen/packages/json-rest-api-core.md +17 -4
- package/reference/autogen/packages/kernel.md +10 -1
- package/reference/autogen/packages/material-web.md +2 -1
- package/reference/autogen/packages/shell-web.md +95 -4
- package/reference/autogen/packages/users-web.md +9 -2
- package/reference/autogen/packages/workspaces-core.md +2 -0
- package/reference/autogen/tooling/jskit-catalog.md +55 -6
- package/reference/autogen/tooling/testUtils.md +15 -1
- package/skills/jskit/SKILL.md +15 -5
- 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 +8 -7
- package/skills/jskit/references/material-3.md +153 -71
- package/skills/jskit/references/multi-homing.md +8 -2
- 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 +10 -0
- 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 -0
- package/skills/jskit/references/patterns/app/minimal-foundation/example/src/main.js +3 -2
- 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 +10 -0
- 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 -0
- 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/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/example/package.json +1 -1
- package/skills/jskit/references/patterns/database/postgres-application/example/package.json +1 -1
- 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 +28 -2
- 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 +23 -0
- 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 +180 -0
- package/skills/jskit/references/translation.md +4 -0
- package/skills/jskit/references/typed-features.md +158 -105
- package/skills/jskit/references/ui-operations.md +3 -1
|
@@ -438,6 +438,9 @@ record declares `mode: "patch"`; a command never does.**
|
|
|
438
438
|
|
|
439
439
|
- Patch mode skips `required`, so a service keeps its guard for a field a
|
|
440
440
|
patch input lists as required.
|
|
441
|
+
- An input field lists its allowed values with `enum`; the kernel refuses
|
|
442
|
+
`oneOf` on an input field at startup, because json-rest-schema checks no
|
|
443
|
+
`oneOf` and the field would accept any value.
|
|
441
444
|
|
|
442
445
|
Why: a command whose required field is never enforced reaches the service
|
|
443
446
|
without it.
|
|
@@ -639,7 +642,7 @@ way (is this user an operator) is a member of its owner's capability.**
|
|
|
639
642
|
- A helper two packages of an application use lives in its shared package,
|
|
640
643
|
never inside one of them.
|
|
641
644
|
|
|
642
|
-
Why:
|
|
645
|
+
Why: the kernel's helpers fix one meaning of empty, and a copy brings its own.
|
|
643
646
|
|
|
644
647
|
```js
|
|
645
648
|
// good
|
|
@@ -668,7 +671,8 @@ helper, never by hand.**
|
|
|
668
671
|
| the workspace a request asks for, before any action context | `requestedWorkspaceScope(request)` (`@jskit-ai/workspaces-core/server/support/workspaceRouteInput`) |
|
|
669
672
|
| a context's workspace id | `resolveWorkspaceScopeValue(context)` (`@jskit-ai/json-rest-api-core/server/jsonRestApiHost`) |
|
|
670
673
|
|
|
671
|
-
Why: each
|
|
674
|
+
Why: each reader knows every place JSKIT keeps its fact (`trx` or
|
|
675
|
+
`transaction`, a tagged result's wrapper), and a hand read misses one.
|
|
672
676
|
|
|
673
677
|
```js
|
|
674
678
|
// good
|
|
@@ -688,11 +692,17 @@ page means "the first 20 rows".**
|
|
|
688
692
|
- json-rest-api answers a query that names no page with the resource's
|
|
689
693
|
default page, 20 rows, and says nothing about the rest. The CRUD
|
|
690
694
|
repository's `queryDocuments(query)` without a `limit` is the same first
|
|
691
|
-
page
|
|
692
|
-
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
695
|
+
page, and one with a `limit` above its `queryMaxLimit()` (100) throws,
|
|
696
|
+
because json-rest-api would answer only 100. A CRUD read of every record
|
|
697
|
+
uses the repository's `queryEveryDocument(query, options)`, which answers
|
|
698
|
+
one JSON:API document `{ data, included }` with no `limit`, `cursor` or
|
|
699
|
+
`sort`.
|
|
700
|
+
- The helper reads cursor pages of 1,000 rows (json-rest-api's server-only
|
|
701
|
+
`pageSizeLimit`; a resource whose `queryMaxLimit` is larger uses that) in id
|
|
702
|
+
order until none follows, keeping the call's transaction and context, so no
|
|
703
|
+
row is skipped or repeated. Each page pays the row policy and linkage
|
|
704
|
+
queries once, so 480 rows cost 3 statements, not 15. It sets the page and
|
|
705
|
+
sort itself; sort the rows it returns.
|
|
696
706
|
- A lookup by a unique key (an id, a slug, a token hash, a unique pair such
|
|
697
707
|
as workspace and user) matches at most one row and may read `rows[0]` of a
|
|
698
708
|
plain query. A read of the newest or first few rows names its own `sort`
|
|
@@ -719,33 +729,35 @@ const rows = extractJsonRestCollectionRows(await api.resources.workspaceMembersh
|
|
|
719
729
|
}, createJsonRestContext(options.context)));
|
|
720
730
|
```
|
|
721
731
|
|
|
722
|
-
###
|
|
732
|
+
### Lists read related rows in batches
|
|
723
733
|
|
|
724
|
-
**A read that builds a list
|
|
725
|
-
query per table
|
|
726
|
-
|
|
734
|
+
**A read that builds a list fetches its related rows in batches, an id-list
|
|
735
|
+
query per related table, and groups them in memory, so its query count follows
|
|
736
|
+
the tables and pages it reads, not the rows.**
|
|
727
737
|
|
|
728
|
-
- Collect the ids from the first read, then read each related table
|
|
729
|
-
|
|
738
|
+
- Collect the ids from the first read, then read each related table with an
|
|
739
|
+
id-list filter: a `searchSchema` entry
|
|
730
740
|
`{ type: "array", actualField: "<column>", filterOperator: "in" }`, read
|
|
731
741
|
through `queryAllJsonRestCollectionRows()`, or the resource's `include`.
|
|
732
742
|
Group the rows by id with `Map.groupBy()`.
|
|
733
|
-
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
743
|
+
- A count that grows with the rows means something runs per row:
|
|
744
|
+
`getDocumentById()`, `queryDocuments()`, a service `get` or another owner's
|
|
745
|
+
single-record read inside a loop over the list, however the loop is spelled
|
|
746
|
+
(`for`, `map` with `Promise.all`, a helper called per item), or a
|
|
747
|
+
single-record helper reused for a list.
|
|
737
748
|
- Work derived from a row (hashing, preparing a form, assembling a snapshot)
|
|
738
749
|
runs once per distinct input in the call, not once per row it appears in.
|
|
739
|
-
-
|
|
740
|
-
the resource
|
|
741
|
-
|
|
742
|
-
|
|
750
|
+
- A JSON:API read also costs a few lookup and visibility queries per page and
|
|
751
|
+
relationship field, and pages hold the resource's `queryMaxLimit` (100 rows
|
|
752
|
+
unless it sets one). That cost follows pages, so three hundred invoices take
|
|
753
|
+
a few more queries than ten, not three hundred more ([Query counts at
|
|
754
|
+
realistic sizes](#query-counts-at-realistic-sizes)).
|
|
743
755
|
|
|
744
|
-
Why:
|
|
745
|
-
|
|
756
|
+
Why: each JSKIT single-record read runs its lookups and row policy too, so a
|
|
757
|
+
read per row repeats all of them.
|
|
746
758
|
|
|
747
759
|
```js
|
|
748
|
-
// good:
|
|
760
|
+
// good: a read per related table, however many invoices there are
|
|
749
761
|
const invoiceRows = await queryAllJsonRestCollectionRows(api.resources.invoices, {
|
|
750
762
|
queryParams: { filters: { workspace: workspaceId } },
|
|
751
763
|
transaction: resolveTransaction(options),
|
|
@@ -779,21 +791,25 @@ paying a read policy for every row it writes.**
|
|
|
779
791
|
[row policies heuristic](heuristics/row-policies.md)
|
|
780
792
|
has the policy contract and its package composition.
|
|
781
793
|
- The policy runs on every single-record read, and a CRUD write includes one:
|
|
782
|
-
`createDocument()` and `patchDocumentById()`
|
|
783
|
-
|
|
784
|
-
look the record up through it.
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
delivery, an import, a fan-out)
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
794
|
+
`createDocument()` and `patchDocumentById()` default to `returning: "full"`,
|
|
795
|
+
which reads the record back through the policy, and `patch` and `delete`
|
|
796
|
+
first look the record up through it.
|
|
797
|
+
- A write path decides access once, inside its transaction. A write that does
|
|
798
|
+
not use its result passes `returning: "minimal"` or `"none"`, and a batch of
|
|
799
|
+
creates (a delivery, an import, a fan-out) goes through
|
|
800
|
+
`createDocuments(payloads, { trx, context })`, which reads nothing back
|
|
801
|
+
unless asked
|
|
802
|
+
([writes that return less](https://mobily-enterprises.github.io/jskit-ai/guide/app-setup/database-layer#writes-that-return-less)).
|
|
803
|
+
Any other batch whose per-row lookup would pay an expensive policy writes
|
|
804
|
+
through a repository method added with `decorateRepository`, on
|
|
805
|
+
`resolveRepoClient(database.knex, options)`, setting the same columns the
|
|
806
|
+
resource would, its ownership columns included.
|
|
807
|
+
- Its test counts the queries of one read and one write for a subject with a
|
|
808
|
+
short history and for one with a long one ([Query counts at realistic
|
|
809
|
+
sizes](#query-counts-at-realistic-sizes)).
|
|
810
|
+
|
|
811
|
+
Why: the row policy runs on every read, write read-back and lookup, so every
|
|
812
|
+
request pays what it costs.
|
|
797
813
|
|
|
798
814
|
```js
|
|
799
815
|
// good: one predicate on the row being read
|
|
@@ -930,7 +946,8 @@ carries on.**
|
|
|
930
946
|
(`response.body.cancel()`), for queue tails whose error reaches the queued
|
|
931
947
|
caller, and for client best effort, each with a short comment saying so.
|
|
932
948
|
|
|
933
|
-
Why:
|
|
949
|
+
Why: a collaborator call's failure can be a wiring `TypeError`, and
|
|
950
|
+
`() => {}` hides it with the rest.
|
|
934
951
|
|
|
935
952
|
```js
|
|
936
953
|
// good
|
|
@@ -952,8 +969,8 @@ frame.**
|
|
|
952
969
|
- Complete a double with the method the code calls, returning its neutral
|
|
953
970
|
value; never shape production code around a double.
|
|
954
971
|
|
|
955
|
-
Why:
|
|
956
|
-
|
|
972
|
+
Why: an owner's seams are its contract; its id formats and stream frames
|
|
973
|
+
change with its package.
|
|
957
974
|
|
|
958
975
|
```js
|
|
959
976
|
// good
|
|
@@ -972,8 +989,8 @@ double.**
|
|
|
972
989
|
- One named clock per factory (`now = Date.now`) is a platform default, not a
|
|
973
990
|
test seam.
|
|
974
991
|
|
|
975
|
-
Why: a
|
|
976
|
-
|
|
992
|
+
Why: a factory's parameters are its contract with the features that call it,
|
|
993
|
+
and a test is not one of them.
|
|
977
994
|
|
|
978
995
|
```js
|
|
979
996
|
// good
|
|
@@ -992,8 +1009,8 @@ through the feature, and pure helpers through `__testables`.**
|
|
|
992
1009
|
- `__testables` holds pure helpers only, never one that touches a
|
|
993
1010
|
collaborator or reads request facts.
|
|
994
1011
|
|
|
995
|
-
Why: a test
|
|
996
|
-
|
|
1012
|
+
Why: `__testables` is the one way a test reaches inside a module, so every
|
|
1013
|
+
other export is production API.
|
|
997
1014
|
|
|
998
1015
|
```js
|
|
999
1016
|
// good
|
|
@@ -1006,87 +1023,105 @@ export function resetInvoiceCacheForTests() {
|
|
|
1006
1023
|
}
|
|
1007
1024
|
```
|
|
1008
1025
|
|
|
1009
|
-
###
|
|
1026
|
+
### Query counts at realistic sizes
|
|
1010
1027
|
|
|
1011
|
-
**A list, batch or write path
|
|
1012
|
-
|
|
1013
|
-
|
|
1028
|
+
**A list, batch or write path's database test counts its queries with the
|
|
1029
|
+
test Knex instance's `query` event at a few rows and at a realistic number,
|
|
1030
|
+
and fails when the count grows with the rows instead of the pages.**
|
|
1014
1031
|
|
|
1015
|
-
-
|
|
1016
|
-
|
|
1017
|
-
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
one write) asserts the same count at a short and a long history. A query
|
|
1021
|
-
count is stable in CI; also report the time the call took.
|
|
1022
|
-
- Meet the budget by fixing the reads ([One read per
|
|
1023
|
-
table](#one-read-per-table), [Bounded access checks](#bounded-access-checks)),
|
|
1024
|
-
never by shrinking the data or raising the budget.
|
|
1032
|
+
- Pages add a few queries; rows should add none. A path that pages nothing
|
|
1033
|
+
(one read, one write) compares a short and a long history.
|
|
1034
|
+
- When it fails, fix the reads ([Lists read related rows in
|
|
1035
|
+
batches](#lists-read-related-rows-in-batches), [Bounded access
|
|
1036
|
+
checks](#bounded-access-checks)).
|
|
1025
1037
|
|
|
1026
|
-
Why:
|
|
1027
|
-
|
|
1038
|
+
Why: JSON:API reads add queries per page and relationship field, so comparing
|
|
1039
|
+
two sizes separates that cost from a read per row, where one fixed number
|
|
1040
|
+
cannot.
|
|
1028
1041
|
|
|
1029
1042
|
```js
|
|
1030
1043
|
// good
|
|
1031
|
-
|
|
1032
|
-
await seedInvoices(knex, { workspace, count: 300, linesEach: 5 });
|
|
1044
|
+
async function countQueries(knex, work) {
|
|
1033
1045
|
let queries = 0;
|
|
1034
1046
|
const count = () => { queries += 1; };
|
|
1035
1047
|
knex.on("query", count);
|
|
1036
1048
|
try {
|
|
1037
|
-
await
|
|
1049
|
+
await work();
|
|
1038
1050
|
} finally {
|
|
1039
1051
|
knex.off("query", count);
|
|
1040
1052
|
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1053
|
+
return queries;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
test("the invoice list's queries follow its pages, not its invoices", async () => {
|
|
1057
|
+
const listAt = async (count) => {
|
|
1058
|
+
const workspace = await createWorkspace(knex);
|
|
1059
|
+
await seedInvoices(knex, { workspace, count, linesEach: 5 });
|
|
1060
|
+
return countQueries(knex, () => invoices.listWithLines(workspace, { context }));
|
|
1061
|
+
};
|
|
1062
|
+
const few = await listAt(10);
|
|
1063
|
+
const many = await listAt(300);
|
|
1064
|
+
// Thirty times the invoices may add pages; a read per invoice would add about 290 queries.
|
|
1065
|
+
assert.ok(many <= few * 2, `${few} queries for 10 invoices, ${many} for 300`);
|
|
1043
1066
|
});
|
|
1044
1067
|
|
|
1045
|
-
// bad: three invoices
|
|
1068
|
+
// bad: three invoices, nothing counted, so a read per invoice passes
|
|
1046
1069
|
test("the invoice list has its lines", async () => {
|
|
1047
1070
|
await seedInvoices(knex, { workspace, count: 3, linesEach: 1 });
|
|
1048
1071
|
assert.equal((await invoices.listWithLines(workspace, { context })).length, 3);
|
|
1049
1072
|
});
|
|
1050
1073
|
```
|
|
1051
1074
|
|
|
1052
|
-
###
|
|
1053
|
-
|
|
1054
|
-
**A
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1075
|
+
### Test files run in parallel
|
|
1076
|
+
|
|
1077
|
+
**A test suite runs its files in parallel, as many at a time as the machine's
|
|
1078
|
+
test budget allows: never one file at a time, and never more than the machine
|
|
1079
|
+
can take. Node's test runner runs through `jskit test`; Playwright's `workers`
|
|
1080
|
+
and Vitest's `maxWorkers` come from `testConcurrency()`.**
|
|
1081
|
+
|
|
1082
|
+
- The budget is `V64_TEST_CONCURRENCY` when it is a positive whole number (a
|
|
1083
|
+
test runner that hands out slots exports it for each run), otherwise half of
|
|
1084
|
+
the machine's CPUs, rounded down and at least 1: a 2-vCPU VPS runs one file
|
|
1085
|
+
at a time, an 8-thread machine four.
|
|
1086
|
+
- `jskit test` (from `@jskit-ai/jskit-catalog`) passes its arguments to
|
|
1087
|
+
`node --test` and sets `--test-concurrency` to the budget.
|
|
1088
|
+
`testConcurrency()` from `@jskit-ai/jskit-catalog/test-budget` returns the
|
|
1089
|
+
same number for a config.
|
|
1090
|
+
- No script passes a fixed concurrency or worker count. `jskit check` fails a
|
|
1091
|
+
package script that runs `node --test` directly or passes one, and a
|
|
1092
|
+
Playwright or Vitest config those scripts use whose workers do not come from
|
|
1093
|
+
`testConcurrency()` ([test-run rules](#test-run-rules)).
|
|
1094
|
+
- Each file isolates what it shares: a port the system picks (`listen(0)`), a
|
|
1095
|
+
directory from `mkdtemp`, its own database or schema with a name no other
|
|
1096
|
+
file uses, its own Vite `cacheDir` for each dev server it starts (Vite
|
|
1097
|
+
replaces a shared dependency cache under servers still reading it), and no
|
|
1098
|
+
writes into the source tree. Files that collide are isolated, never
|
|
1099
|
+
serialised. A Playwright spec works on its own records, so
|
|
1100
|
+
specs can share one server.
|
|
1101
|
+
- A file that truly cannot run beside others, because it holds a resource of
|
|
1102
|
+
the whole machine, says why on its first line,
|
|
1103
|
+
`// jskit-test: alone - <reason>`. `jskit test` runs it after the others,
|
|
1104
|
+
one such file at a time, and fails on a marker without a reason or below the
|
|
1105
|
+
first line.
|
|
1106
|
+
|
|
1107
|
+
Why: `jskit test` and `testConcurrency()` read one budget, so every runner
|
|
1108
|
+
uses the machine without starving the other runs on it.
|
|
1072
1109
|
|
|
1073
1110
|
```js
|
|
1074
|
-
// good:
|
|
1075
|
-
|
|
1076
|
-
await deliveries.processBatch(workspace, { context });
|
|
1077
|
-
});
|
|
1111
|
+
// good: package.json "test": "jskit test", and playwright.config.mjs
|
|
1112
|
+
import { testConcurrency } from "@jskit-ai/jskit-catalog/test-budget";
|
|
1078
1113
|
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
});
|
|
1114
|
+
export default defineConfig({ workers: testConcurrency() });
|
|
1115
|
+
|
|
1116
|
+
// bad: "test": "node --test --test-concurrency=1", and
|
|
1117
|
+
export default defineConfig({ workers: 1 });
|
|
1083
1118
|
```
|
|
1084
1119
|
|
|
1085
1120
|
### Test databases are prepared once
|
|
1086
1121
|
|
|
1087
|
-
**A test run prepares each database once
|
|
1088
|
-
schema, migration ledger and seed rows checked by a digest of
|
|
1089
|
-
|
|
1122
|
+
**A test run prepares each database once and restores it from a stored
|
|
1123
|
+
snapshot of the schema, migration ledger and seed rows, checked by a digest of
|
|
1124
|
+
their sources.**
|
|
1090
1125
|
|
|
1091
1126
|
- The snapshot is rebuilt only when the digest of the migration and seed
|
|
1092
1127
|
sources changes, once and outside the timed tests; every disposable
|
|
@@ -1094,15 +1129,15 @@ and never replays the whole migration history inside the run.**
|
|
|
1094
1129
|
it.
|
|
1095
1130
|
- The full migration history runs once, in its own fresh-schema check
|
|
1096
1131
|
([database verification](https://mobily-enterprises.github.io/jskit-ai/guide/app-setup/database-layer#verification)).
|
|
1097
|
-
- The harness reports each preparation phase and its duration, and
|
|
1098
|
-
|
|
1132
|
+
- The harness reports each preparation phase and its duration, and those
|
|
1133
|
+
durations are read, so slow preparation shows instead of hiding inside the
|
|
1099
1134
|
next test that waits for it.
|
|
1100
1135
|
- The database layer guide's [integration test
|
|
1101
1136
|
databases](https://mobily-enterprises.github.io/jskit-ai/guide/app-setup/database-layer#integration-test-databases)
|
|
1102
1137
|
gives the snapshot's contents per engine.
|
|
1103
1138
|
|
|
1104
|
-
Why:
|
|
1105
|
-
|
|
1139
|
+
Why: an application's migrations include those of every installed package, so
|
|
1140
|
+
replaying them costs more with each package as well as each migration.
|
|
1106
1141
|
|
|
1107
1142
|
```js
|
|
1108
1143
|
// good: the application's test support restores the snapshot its digest names
|
|
@@ -1120,6 +1155,10 @@ before(() => knex.migrate.latest());
|
|
|
1120
1155
|
code: server, shared, client (`.vue` and `.js`) and scripts. One concern per
|
|
1121
1156
|
factory.**
|
|
1122
1157
|
|
|
1158
|
+
- The limit is an outer bound, not a size to aim for: a file near it usually
|
|
1159
|
+
holds more than one concern. Split it by concern; never trim lines or join
|
|
1160
|
+
statements to fit.
|
|
1161
|
+
|
|
1123
1162
|
Why: a reader holds one concern at a time; a 2,000-line closure holds ten.
|
|
1124
1163
|
|
|
1125
1164
|
```text
|
|
@@ -1219,8 +1258,9 @@ Why: a comment that narrates history is wrong the day after it is written.
|
|
|
1219
1258
|
### Running the checks
|
|
1220
1259
|
|
|
1221
1260
|
`npm run lint` runs ESLint with `@jskit-ai/config-eslint`, and `npm run
|
|
1222
|
-
verify` runs `jskit check` (the package graph
|
|
1223
|
-
build. The foundations'
|
|
1261
|
+
verify` runs `jskit check` (the package graph, navigation and [test
|
|
1262
|
+
runs](#test-run-rules)), lint, the tests and the build. The foundations'
|
|
1263
|
+
`eslint.config.mjs` already holds a new
|
|
1224
1264
|
application's code to every check:
|
|
1225
1265
|
|
|
1226
1266
|
```js
|
|
@@ -1292,6 +1332,19 @@ Every finding ends with a link to the section of this page that explains it.
|
|
|
1292
1332
|
| `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) |
|
|
1293
1333
|
| `max-lines`, `max-lines-per-function` | files over 750 lines, functions over 600 | [Size limits](#size-limits) |
|
|
1294
1334
|
|
|
1335
|
+
### Test-run rules
|
|
1336
|
+
|
|
1337
|
+
`jskit check` reports these in every package script, and in the Playwright
|
|
1338
|
+
and Vitest configs those scripts use. No comment excuses them; a test file
|
|
1339
|
+
that must run alone says so on its own first line instead.
|
|
1340
|
+
|
|
1341
|
+
| Rule | Reports | Section |
|
|
1342
|
+
| --- | --- | --- |
|
|
1343
|
+
| `test/node-test` | a script that runs `node --test` directly instead of `jskit test` | [Test files run in parallel](#test-files-run-in-parallel) |
|
|
1344
|
+
| `test/fixed-concurrency` | `--test-concurrency` in a script | [Test files run in parallel](#test-files-run-in-parallel) |
|
|
1345
|
+
| `test/playwright-workers` | `--workers` or `-j` passed to `playwright test`, or its config without `workers: testConcurrency()` | [Test files run in parallel](#test-files-run-in-parallel) |
|
|
1346
|
+
| `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) |
|
|
1347
|
+
|
|
1295
1348
|
The rules see shapes, not intent. Feature files, owned state beyond
|
|
1296
1349
|
registries, shutdown, loggers, shared action fields, strict inputs, the action
|
|
1297
1350
|
policy, request context, validated input, one reader per fact, routes,
|
|
@@ -31,7 +31,9 @@ Cancel, Close, and a wizard's Previous step; keep existing unsaved-change
|
|
|
31
31
|
guards.
|
|
32
32
|
|
|
33
33
|
`createShellRouter()` owns scroll position (`shell/application-shell`
|
|
34
|
-
pattern); never reset scroll in page components.
|
|
34
|
+
pattern); never reset scroll in page components. An app's own scroll behaviour
|
|
35
|
+
is its `scrollBehavior` option, never a `createRouter` wrapper's (that throws);
|
|
36
|
+
a hash target opens below its `scroll-margin-top`.
|
|
35
37
|
|
|
36
38
|
## Application-owned files
|
|
37
39
|
|