@jskit-ai/agent-docs 0.1.293 → 0.1.295

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 (47) hide show
  1. package/guide/agent/app-extras/realtime.md +14 -4
  2. package/guide/agent/app-setup/multi-homing.md +12 -3
  3. package/guide/agent/framework/client-features.md +28 -0
  4. package/guide/agent/framework/crud-operations.md +7 -0
  5. package/guide/agent/framework/data-access.md +75 -9
  6. package/guide/agent/framework/sanity-checks.md +4 -4
  7. package/guide/agent/framework/typed-features.md +12 -3
  8. package/package.json +1 -1
  9. package/patterns/minimal-foundation/example/package.json +2 -2
  10. package/patterns/shell-foundation/example/package.json +3 -3
  11. package/reference/autogen/PATTERN_INDEX.md +35 -35
  12. package/reference/autogen/packages/agent.md +4 -2
  13. package/reference/autogen/packages/assistant-core.md +1 -1
  14. package/reference/autogen/packages/assistant-runtime.md +1 -0
  15. package/reference/autogen/packages/crud-core.md +2 -0
  16. package/reference/autogen/packages/http-web.md +9 -6
  17. package/reference/autogen/packages/realtime.md +1 -0
  18. package/reference/autogen/packages/resource-runtime.md +1 -0
  19. package/reference/autogen/packages/users-core.md +3 -0
  20. package/reference/autogen/packages/workspaces-core.md +11 -2
  21. package/reference/autogen/packages/workspaces-web.md +1 -1
  22. package/reference/autogen/tooling/config-eslint.md +26 -0
  23. package/reference/autogen/tooling/jskit-catalog.md +27 -13
  24. package/skills/jskit/references/client-features.md +28 -0
  25. package/skills/jskit/references/crud-operations.md +7 -0
  26. package/skills/jskit/references/data-access.md +75 -9
  27. package/skills/jskit/references/heuristics/INDEX.md +1 -1
  28. package/skills/jskit/references/heuristics/data-access.md +1 -0
  29. package/skills/jskit/references/heuristics/filters.md +14 -0
  30. package/skills/jskit/references/heuristics/server-search.md +2 -0
  31. package/skills/jskit/references/multi-homing.md +12 -3
  32. package/skills/jskit/references/pattern-index.md +35 -35
  33. package/skills/jskit/references/patterns/app/minimal-foundation/example/package.json +2 -2
  34. package/skills/jskit/references/patterns/app/shell-foundation/example/package.json +3 -3
  35. package/skills/jskit/references/patterns/auth/accounts-hub/example/package.json +1 -1
  36. package/skills/jskit/references/patterns/auth/oidc-sign-in/example/package.json +2 -2
  37. package/skills/jskit/references/patterns/auth/supabase-auth/example/package.json +1 -1
  38. package/skills/jskit/references/patterns/connectors/calendar-cli/example/package.json +2 -2
  39. package/skills/jskit/references/patterns/crud/crud-feature-package/example/packages/books/package.json +2 -2
  40. package/skills/jskit/references/patterns/crud/crud-screen-set/PATTERN.md +9 -0
  41. package/skills/jskit/references/patterns/realtime/realtime-application/example/package.json +2 -2
  42. package/skills/jskit/references/patterns/users/user-administration-server/PATTERN.md +7 -3
  43. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/package.json +3 -3
  44. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/package.json +5 -5
  45. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/server/workspaceMembersOnly.js +69 -17
  46. package/skills/jskit/references/sanity-checks.md +4 -4
  47. package/skills/jskit/references/typed-features.md +12 -3
@@ -103,10 +103,13 @@ audience contributor is registered and a target room is a user or workspace
103
103
  room, when its actor's contributed rooms include a target room or it has not
104
104
  signed in yet. A socket joins only its own actor's contributed rooms, so a
105
105
  publish to `workspace:7` checks that workspace's members' sockets, not every
106
- connected one. To decide, the publish resolves the contributed rooms of every
107
- signed-in socket's actor in one call to each contributor, whatever the number
108
- of sockets. Other sockets are not checked for that publish; the 30-second check
109
- reaches them. Sockets whose authentication requests are identical, such as tabs of
106
+ connected one. To decide, the publish asks each contributor once about the
107
+ actors of every signed-in socket outside the target rooms, whatever their
108
+ number, naming only the target rooms' scopes, so a contributor reads only
109
+ those (a publish to `workspace:7` reads workspace 7's memberships among them,
110
+ and a publish to user rooms reads no memberships); meanwhile it looks up the
111
+ contributed rooms of the actors already inside, for their checks. Other
112
+ sockets are not checked for that publish; the 30-second check reaches them. Sockets whose authentication requests are identical, such as tabs of
110
113
  one browser, share one `authenticateRequest()` call within a publish or a
111
114
  30-second pass, and the sockets a publish or pass checks reuse that one lookup
112
115
  of their contributed rooms; nothing is reused from one publish or pass to the
@@ -140,12 +143,19 @@ them in one query rather than one per actor: it returns, or resolves to, a `Map`
140
143
  from actor id to that actor's event scopes (`{ kind: "user", id }`,
141
144
  `{ kind: "workspace", id }` or `{ kind: "workspace_user", workspaceId, userId }`);
142
145
  an actor it leaves out has none. A single actor is asked as a list of one.
146
+ A publish asking whether actors outside its rooms belong to them calls
147
+ `resolveActorsScopes(actorIds, { scopes })` with those rooms' scopes: the
148
+ contributor may read only those scopes, and realtime keeps only them of its
149
+ answer, so a contributor that ignores `scopes` still answers correctly.
143
150
  Realtime turns each scope into the same room an `event_scope` audience targets. Sockets
144
151
  join those rooms on connection and, on every revalidation, leave the contributed
145
152
  rooms the actor no longer belongs to; rooms joined by anything else are left
146
153
  alone. `@jskit-ai/workspaces-core` contributes the actors' active workspace
147
154
  memberships this way when realtime is installed, with
148
155
  `listActiveWorkspaceIdsByUserIds()`: one memberships query per 1,000 actors.
156
+ Asked about some scopes, it reads only the memberships of the workspaces they
157
+ name, with `listActiveWorkspaceIdsByUserIdsAmong()`, and none when they name
158
+ no workspace.
149
159
 
150
160
  The authentication request includes parsed cookies, the `Host`, `Origin` and
151
161
  `X-Forwarded-Proto` headers when present, and the socket address and TLS state.
@@ -196,12 +196,21 @@ method takes the call's `options` last and joins the transaction of the
196
196
  | Capability | Methods |
197
197
  | --- | --- |
198
198
  | `workspaces` | `findById(id, options)`, `findBySlug(slug, options)` (a deleted workspace is not found, though its slug stays taken), `listSlugsIn`, `findPersonalByOwnerUserId`, `listForUserId(userId, options)`, `insert`, `updateById`, `lock(workspaceId, options)` |
199
- | `workspaces.memberships` | `findByWorkspaceIdAndUserId`, `ensureOwnerMembership`, `upsertMembership(workspaceId, userId, patch, options)`, `addMember({ workspaceId, userId, roleSid }, options)`, `activeMemberIds(workspaceId, options)`, `listActiveByWorkspaceId`, `findActiveByWorkspaceIdAndUserId`, `listActiveWorkspaceIdsByUserId`, `listActiveWorkspaceIdsByUserIds` |
199
+ | `workspaces.memberships` | `findByWorkspaceIdAndUserId`, `ensureOwnerMembership`, `upsertMembership(workspaceId, userId, patch, options)`, `addMember({ workspaceId, userId, roleSid }, options)`, `activeMemberIds(workspaceId, options)`, `listActiveByWorkspaceId`, `findActiveByWorkspaceIdAndUserId`, `listActiveWorkspaceIdsByUserId`, `listActiveWorkspaceIdsByUserIds`, `listActiveWorkspaceIdsByUserIdsAmong(userIds, workspaceIds, options)`, `joinSource({ alias, filters }, options)` |
200
200
 
201
201
  `addMember` makes the user an active member with the role (`member` when none
202
202
  is given), adding the membership or reactivating it. `activeMemberIds` answers
203
- the active members' user ids at the same cost however many members there
204
- are, without including their profiles. `lock` takes
203
+ every active member's user id, without their profiles: it reads all of the
204
+ workspace's active memberships, so its cost grows with the workspace, and so
205
+ do `listActiveByWorkspaceId`'s and
206
+ `workspaces.services.members.listMembers(workspace, options)`'s. Use them where
207
+ the work needs every member, such as a job, a delivery's recipients or a
208
+ realtime audience. A screen or list reads a page instead:
209
+ `workspaces.services.members.listMembersPage(workspace, { cursor, limit }, options)`
210
+ answers one page of active members by display name (`limit`, 20 when none)
211
+ with the `nextCursor` that asks for the next, null on the last, and an app's
212
+ own list of members pages them in SQL with one report over the memberships'
213
+ `joinSource`. `lock` takes
205
214
  the workspace's row lock until the caller's transaction ends, so work that must
206
215
  not interleave for one workspace takes turns; it runs only inside a
207
216
  transaction, given its `txOptions`, and answers `{ id }`, or `null` when there
@@ -519,6 +519,33 @@ let attachmentClientSequence = 0;
519
519
  let pendingDraft = "";
520
520
  ```
521
521
 
522
+ ### Search on the server
523
+
524
+ **A search over a list's records runs on the server: the list's search
525
+ (`useList({ search: { enabled: true } })`) sends what the person types as `q`,
526
+ and the table resource defines `searchSchema.q`. An autocomplete over a list's
527
+ items or a lookup's options takes `no-filter` and sends the typed text with
528
+ `@update:search` (a lookup's `setLookupSearch`).**
529
+
530
+ Why: a list holds the page it loaded, so a search over its items misses every
531
+ row past that page.
532
+
533
+ ```vue
534
+ <!-- good: the server searches every record -->
535
+ <SelectField v-model="formState.serviceId" searchable no-filter
536
+ :search="resolveLookupSearch('serviceId')"
537
+ @update:search="setLookupSearch('serviceId', $event)"
538
+ :items="resolveLookupItems('serviceId', { selectedValue: formState.serviceId })" />
539
+
540
+ <!-- bad: Vuetify searches only the options already loaded -->
541
+ <v-autocomplete v-model="serviceId" :items="services.items" />
542
+ ```
543
+
544
+ ```js
545
+ // bad: a search over the page the list loaded
546
+ const visible = computed(() => list.items.filter((service) => service.name.includes(search.value)));
547
+ ```
548
+
522
549
  ## Helpers and sizes
523
550
 
524
551
  ### One helper per package
@@ -587,6 +614,7 @@ client-only rules below. The server's action and error rules do not apply.
587
614
  | `jskit/released-listeners` | a listener, interval, observer or subscription the file starts and never releases | [Released listeners](#released-listeners) |
588
615
  | `jskit/no-module-state` | a module-level `let`, `ref()` or `reactive()` | [Module state](#module-state) |
589
616
  | `jskit/selection-field` | a `v-select`, `v-autocomplete` or `v-combobox` whose items are not fixed, and a `SelectField` whose items are not fixed that binds no `loading` | [Selection fields](#selection-fields) |
617
+ | `jskit/search-on-server` | a `computed()` that filters a list composable's items by a search ref; an autocomplete over a list's items or a lookup's options without `no-filter` or an `update:search` handler | [Search on the server](#search-on-the-server) |
590
618
  | `vue/require-prop-types`, `vue/require-default-prop`, `vue/require-valid-default-prop`, `vue/no-required-prop-with-default` | a prop without a type, optional without a default, or both | [Collaborator props](#collaborator-props) |
591
619
  | `vue/require-explicit-emits` | an emitted event the component does not declare | [Declared emits](#declared-emits) |
592
620
 
@@ -256,6 +256,13 @@ mapping in the resource; do not add page-local filtering or duplicate request
256
256
  code. The full Books source example maps `q` to title, author and notes; narrow
257
257
  it when adapting the example to fewer fields.
258
258
 
259
+ Every CRUD list also takes `id`, one record id or a list of them
260
+ (`filter[id]` on the JSON:API transport), and answers only those of its rows,
261
+ in the caller's scope. A picker whose selected values are not on the page it
262
+ loaded reads them this way, all in one request, instead of showing their raw
263
+ ids or reading pages until it finds them; `useCrudListFilterLookups()` does it
264
+ for a lookup filter's chips and options.
265
+
259
266
  The browser page's route query and internal CRUD query use plain `q`. The
260
267
  JSON:API HTTP transport uses `filter[q]`. Shared CRUD clients encode and decode
261
268
  this automatically. Raw HTTP/API tests must send the transport key themselves:
@@ -65,7 +65,9 @@ A grouped `aggregate` answers its groups from one statement, at most
65
65
  `pageLimits().request` of them (the resource's `queryMaxLimit`). When there
66
66
  are more it throws a 400 coded `RESOURCE_GROUP_LIMIT` instead of answering
67
67
  part of them. Narrow the filters, or read the groups in pages, `page: { size,
68
- number }` with `number` from 1, until a page answers fewer than `size` rows.
68
+ number }` with `number` from 1. A page answers as a `query` page does, `{ rows,
69
+ page: { size, number, hasMore, next } }`, never a bare array that could pass
70
+ for every group: read the next while `page.hasMore`.
69
71
  Page numbers are offsets, so read every page before work that changes what
70
72
  the filters match.
71
73
 
@@ -715,12 +717,12 @@ takes first.
715
717
  // good: the lock is the transaction's first read, so the bookings read after it are current
716
718
  const reserve = (seatId, options) => seats.withTransaction(options, async (txOptions) => {
717
719
  await seats.lock(seatId, { ...txOptions, fields: ["status"] });
718
- const taken = await bookings.queryAll({ filters: { seatIds: [seatId] } }, txOptions);
719
- return taken.length > 0 ? null : bookings.create({ seatId }, { ...txOptions, returning: "id" });
720
+ const [taken] = await bookings.queryAll({ filters: { seatIds: [seatId] }, take: 1 }, txOptions);
721
+ return taken ? null : bookings.create({ seatId }, { ...txOptions, returning: "id" });
720
722
  });
721
723
 
722
724
  // bad: the read before the lock fixed the snapshot, so a booking committed while the lock waited is missed
723
- const free = await bookings.queryAll({ filters: { seatIds: [seatId] } }, txOptions);
725
+ const [booked] = await bookings.queryAll({ filters: { seatIds: [seatId] }, take: 1 }, txOptions);
724
726
  await seats.lock(seatId, txOptions);
725
727
  ```
726
728
 
@@ -931,7 +933,11 @@ async function activeDays(memberIds, options) {
931
933
  `page.number` reads with OFFSET, which slows as the rows grow, so anything
932
934
  that grows pages with `page.after`. Each page is one statement, with no
933
935
  count: never walk pages to reach one.
934
- - A screen list never uses `queryAll`.
936
+ - A screen list never uses `queryAll`, and neither does a picker: it reads
937
+ one page searched on the server as the person types (`q`), narrowed in the
938
+ query by what it depends on (the parent record, `active`), and reads its
939
+ selected values past that page by id. Reading every row for a dropdown, or
940
+ narrowing a page in code, either grows with the table or misses rows.
935
941
  - Related rows come by id list (`filters: { id: ids }`, or an `in` filter on
936
942
  another field) or `include`, grouped in memory. Never a query per row, however
937
943
  the loop is spelled (`for`, `map` with `Promise.all`, a helper called per
@@ -987,6 +993,66 @@ const { records: firstPage } = await subscriptions.query({ filters: { newsletter
987
993
 
988
994
  Bad: a list read 100-row pages in a loop for each of 40 related sets.
989
995
 
996
+ ## Narrow in the query
997
+
998
+ A read asks the store for exactly the rows the code uses. The filter, sort,
999
+ limit, existence check and count go in the read, never in code over the rows
1000
+ it answered: a read of every row then narrowed is slow, and a page or a `take`
1001
+ then narrowed is also wrong, since the row wanted can sit past it.
1002
+
1003
+ - Filter with the resource's searchSchema: a field
1004
+ (`filters: { status: "open" }`), an id list (`filters: { id: ids }`, or an
1005
+ `in` filter on another field), or `q` for typed text, whose `oneOf` names the
1006
+ fields it searches.
1007
+ - One row: a unique filter with `take: 1` or `page: { size: 1 }`, which also
1008
+ answers whether one exists. A count: `aggregate()`.
1009
+ - Never filter, find, slice or count the rows a read answered, nor skip some
1010
+ in a loop. Mapping them and grouping them by a key (`Map.groupBy`) narrow
1011
+ nothing, and neither does code that needs every row it read: rows handed on
1012
+ whole (returned, passed, stored, mapped or walked in full) with a subset
1013
+ derived beside them, and rows matched against other items in a loop or a
1014
+ callback (an in-memory join, one read instead of one per item).
1015
+ - A lookup field's picker and a list's Search box send what the person types
1016
+ as `q` to the server, which searches every row; `useList` does with
1017
+ `search: { enabled: true }`. The table resource defines `searchSchema.q`.
1018
+ Never search the page a list loaded in the browser: it misses every row past
1019
+ it.
1020
+ - A CRUD list refuses a query key it does not take with a 400
1021
+ (`crud.query.undeclaredKeys`). A list filter is declared in the feature's
1022
+ `listFilterQueryValidator` and the resource's searchSchema.
1023
+ - Prove a capped read with a test that seeds past the cap and makes the row
1024
+ the cap leaves out (the oldest, for a newest-first read) the one that
1025
+ matches: the read that asks for it finds it; a read narrowed in code does
1026
+ not.
1027
+
1028
+ ```js
1029
+ // good: the store answers one module's sections
1030
+ const { records } = await sections.query({ filters: { moduleId }, sort: ["-createdAt"], page: { size: 50 } }, options);
1031
+
1032
+ // bad: the newest 200 sections of every module, then one module's kept in code
1033
+ const { records: recent } = await sections.query({ sort: ["-createdAt"], page: { size: 200 } }, options);
1034
+ const moduleSections = recent.filter((section) => section.moduleId === moduleId);
1035
+
1036
+ // good: one row says who holds a unique username
1037
+ const [holder] = await profiles.queryAll({ filters: { usernames: [username] }, take: 1 }, options);
1038
+ ```
1039
+
1040
+ `jskit/narrow-in-query` reports `.filter()`, `.find()`, `.some()`,
1041
+ `.every()`, `.slice()`, a lone `.length` and a loop that skips rows with
1042
+ `continue` over the rows a read answered in the same function, unless the
1043
+ code also uses those rows whole, runs the narrowing once per item of
1044
+ something else, read at most one row, or slices a read that asked for one row
1045
+ more than it keeps (`limit(size + 1)`) to learn whether more follow.
1046
+ `jskit/search-on-server` reports a client search over a list's loaded items.
1047
+ `jskit check` fails a lookup field or a CRUD list screen whose app table
1048
+ resource has no `searchSchema.q`, on the resource or its owns entry
1049
+ (`data/search-q`). Other code that needs every row a read answers says why on
1050
+ the line above the read or the narrowing:
1051
+
1052
+ ```js
1053
+ // readable-check: allow narrow-in-query - every row: a conversation holds at most keep + 1 summaries, since each save deletes the older ones
1054
+ ```
1055
+
990
1056
  ## What changed since
991
1057
 
992
1058
  A page that catches up on a chat, a watch that acts on changes and a sync to
@@ -1082,10 +1148,10 @@ const SettlementFeature = defineFeature({
1082
1148
  async function settleDue(options) {
1083
1149
  const size = cohorts.pageLimits().request;
1084
1150
  const due = [];
1085
- for (let number = 1; ; number += 1) {
1086
- const page = await cohorts.aggregate({ measures: { cohorts: { count: "id" } }, groupBy: ["workspaceId"], filters: { status: "finished" }, page: { size, number } }, options);
1087
- due.push(...page);
1088
- if (page.length < size) break;
1151
+ for (let number = 1; number; ) {
1152
+ const { rows, page } = await cohorts.aggregate({ measures: { cohorts: { count: "id" } }, groupBy: ["workspaceId"], filters: { status: "finished" }, page: { size, number } }, options);
1153
+ due.push(...rows);
1154
+ number = page.next;
1089
1155
  }
1090
1156
  for (const { workspaceId } of due) {
1091
1157
  await settleWorkspace({ context: systemContext("settle finished weekly leagues for every user").forWorkspace(workspaceId) });
@@ -67,13 +67,13 @@ expects the relationship id or an unselected field never gets it.
67
67
  ### Every row
68
68
 
69
69
  - A handle's `query()` answers one page and refuses a missing or oversized
70
- `page.size`, where json-rest-api would cut the list silently. A read its
71
- callers treat as complete uses `queryAll()`, or the missing rows vanish from
72
- every picker and delivery built on it
70
+ `page.size`, where json-rest-api would cut the list silently. A delivery
71
+ reaching every row uses `queryAll()`; a picker reads a page searched with
72
+ `q` and its selected values by `id`
73
73
  ([Reads](/guide/framework/data-access#reads-shaped-for-the-screen)).
74
74
  - A grouped `aggregate()` answers every group or throws
75
75
  `RESOURCE_GROUP_LIMIT`; past `pageLimits().request` groups, page it with
76
- `page: { size, number }`
76
+ `page: { size, number }` (answers `{ rows, page }`)
77
77
  ([The handle's calls](/guide/framework/data-access#the-handle-s-calls)).
78
78
  - A lookup by a unique key matches at most one row: `get(id)`, or a `query()`
79
79
  of page size 1.
@@ -652,9 +652,9 @@ Why: a value with two defaults has two meanings, and a mode flag hides a
652
652
  second method inside the first.
653
653
 
654
654
  ```js
655
- // good
656
- async function list(filters, options = {}) {
657
- return invoices.queryAll({ filters }, options);
655
+ // good: one page, as a list shows it
656
+ async function list(filters, page, options = {}) {
657
+ return invoices.query({ filters, page }, options);
658
658
  }
659
659
 
660
660
  // bad
@@ -776,6 +776,13 @@ handle's `queryAll(params, options)`. A `query()` names its page size, and
776
776
  answers one page.** [Reads shaped for the screen](/guide/framework/data-access#reads-shaped-for-the-screen)
777
777
  gives the rules and examples.
778
778
 
779
+ ### Narrow in the query
780
+
781
+ **A read asks for exactly the rows the code uses: its filter, sort, limit,
782
+ existence check and count go in the read, never in code over the rows it
783
+ answered.** [Narrow in the query](/guide/framework/data-access#narrow-in-the-query)
784
+ gives the rules and examples.
785
+
779
786
  ### Lists read related rows in batches
780
787
 
781
788
  **A read that builds a list fetches its related rows in batches, an id-list
@@ -1277,6 +1284,7 @@ Every finding ends with a link to the section of this page that explains it.
1277
1284
  | `jskit/app-error-code` | an `AppError`, `DomainError`, `ConflictError` or `NotFoundError` (or a subclass the module declares) without its own code, and one code naming two sentences in a module | [Refusals carry their own code](#refusals-carry-their-own-code) |
1278
1285
  | `jskit/no-patched-error` | `Object.assign(new Error(…), { statusCode })`, or an error builder's result given its status afterwards | [One construction per error](#one-construction-per-error) |
1279
1286
  | `jskit/no-test-only-export` | an export whose name says only tests use it (`…ForTests`, `__reset…`; `__testables` aside) | [No test-only exports](#no-test-only-exports) |
1287
+ | `jskit/narrow-in-query` | `.filter()`, `.find()`, `.some()`, `.every()`, `.slice()`, a lone `.length` or a loop that skips with `continue`, over the rows a read in the same function answered and uses no other way | [Narrow in the query](#narrow-in-the-query) |
1280
1288
  | `jskit/one-property-per-line` | two properties on one line in a returned `Object.freeze({ … })` or a `requires` / `optional` / `provides` map (`eslint --fix` splits them) | [Formatter](#formatter) |
1281
1289
  | `jskit/jsdoc-description-inside` | a `//` description directly above a `/** */` block | [Comments](#comments) |
1282
1290
  | `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) |
@@ -1328,6 +1336,7 @@ if (typeof buildSerializer !== "function") {
1328
1336
  | `filled by <repository>; convert with its supplier` | keeps a setter that another repository's code calls |
1329
1337
  | `cross-repository slot <token>` | keeps a probe for a token another repository supplies |
1330
1338
  | `shared resource:` | keeps client module state that every instance on the page shares on purpose ([client features](/guide/framework/client-features#module-state)) |
1339
+ | `every row: <why>` | code needs every row a read answers and narrows them for its own use, such as a read the data itself keeps small (a conversation's few summaries) whose oldest ones it deletes (`jskit/narrow-in-query`) |
1331
1340
  | `infrastructure: <what the module is>` | excuses a whole file whose job is data access itself from `jskit/no-direct-knex` or `jskit/no-transaction-reader`, one line per rule before its first line of code ([data access](/guide/framework/data-access#exceptions)) |
1332
1341
 
1333
1342
  One kind of probe gets one prefix and one wording everywhere, and a bare
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/agent-docs",
3
- "version": "0.1.293",
3
+ "version": "0.1.295",
4
4
  "description": "Distributed JSKIT agent references, prompts, guides, and generated reference maps.",
5
5
  "type": "module",
6
6
  "files": [
@@ -49,8 +49,8 @@
49
49
  "@vue/devtools-api": "^8.2.1"
50
50
  },
51
51
  "devDependencies": {
52
- "@jskit-ai/config-eslint": "0.1.249",
53
- "@jskit-ai/jskit-catalog": "0.2.45",
52
+ "@jskit-ai/config-eslint": "0.1.250",
53
+ "@jskit-ai/jskit-catalog": "0.2.47",
54
54
  "@playwright/test": "1.61.1",
55
55
  "@vitejs/plugin-vue": "^6.0.7",
56
56
  "eslint": "^10.8.0",
@@ -45,13 +45,13 @@
45
45
  "vue-router": "^5.3.1",
46
46
  "vuetify": "^4.3.0",
47
47
  "@jskit-ai/http-runtime": "0.2.18",
48
- "@jskit-ai/shell-web": "0.2.22",
48
+ "@jskit-ai/shell-web": "0.2.23",
49
49
  "@fastify/fast-json-stringify-compiler": "^5.1.0",
50
50
  "@vue/devtools-api": "^8.2.1"
51
51
  },
52
52
  "devDependencies": {
53
- "@jskit-ai/config-eslint": "0.1.249",
54
- "@jskit-ai/jskit-catalog": "0.2.45",
53
+ "@jskit-ai/config-eslint": "0.1.250",
54
+ "@jskit-ai/jskit-catalog": "0.2.47",
55
55
  "@playwright/test": "1.61.1",
56
56
  "@vitejs/plugin-vue": "^6.0.7",
57
57
  "eslint": "^10.8.0",