@jskit-ai/agent-docs 0.1.15 → 0.1.17

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 (35) hide show
  1. package/guide/agent/app-extras/assistant.md +2 -2
  2. package/guide/agent/app-setup/initial-scaffolding.md +1 -3
  3. package/guide/agent/app-setup/multi-homing.md +21 -0
  4. package/guide/agent/generators/advanced-cruds.md +208 -110
  5. package/guide/agent/generators/crud-generators.md +4 -0
  6. package/guide/agent/index.md +1 -1
  7. package/package.json +1 -1
  8. package/patterns/INDEX.md +1 -1
  9. package/patterns/client-requests.md +3 -0
  10. package/patterns/crud-repository-mapping.md +45 -23
  11. package/patterns/filters.md +8 -8
  12. package/reference/autogen/KERNEL_MAP.md +94 -40
  13. package/reference/autogen/README.md +3 -0
  14. package/reference/autogen/packages/assistant-core.md +0 -12
  15. package/reference/autogen/packages/assistant-runtime.md +11 -3
  16. package/reference/autogen/packages/auth-core.md +34 -23
  17. package/reference/autogen/packages/auth-provider-supabase-core.md +4 -7
  18. package/reference/autogen/packages/auth-web.md +3 -0
  19. package/reference/autogen/packages/console-core.md +6 -29
  20. package/reference/autogen/packages/crud-core.md +35 -28
  21. package/reference/autogen/packages/crud-server-generator.md +28 -50
  22. package/reference/autogen/packages/crud-ui-generator.md +12 -7
  23. package/reference/autogen/packages/http-runtime.md +171 -21
  24. package/reference/autogen/packages/json-rest-api-core.md +54 -0
  25. package/reference/autogen/packages/kernel.md +118 -55
  26. package/reference/autogen/packages/realtime.md +1 -0
  27. package/reference/autogen/packages/resource-core.md +35 -0
  28. package/reference/autogen/packages/resource-crud-core.md +51 -0
  29. package/reference/autogen/packages/shell-web.md +31 -0
  30. package/reference/autogen/packages/users-core.md +34 -77
  31. package/reference/autogen/packages/users-web.md +29 -20
  32. package/reference/autogen/packages/workspaces-core.md +44 -76
  33. package/reference/autogen/packages/workspaces-web.md +1 -3
  34. package/reference/autogen/tooling/jskit-cli.md +5 -4
  35. package/workflow/feature-delivery.md +2 -1
@@ -181,7 +181,7 @@ For example:
181
181
  require: "all",
182
182
  permissions: ["workspace.members.invite"]
183
183
  },
184
- inputValidator: {
184
+ input: {
185
185
  schema: {
186
186
  type: "object",
187
187
  properties: {
@@ -191,7 +191,7 @@ For example:
191
191
  additionalProperties: false
192
192
  }
193
193
  },
194
- outputValidator: {
194
+ output: {
195
195
  schema: {
196
196
  type: "object",
197
197
  properties: {
@@ -576,8 +576,6 @@ The core of that startup path looks like this:
576
576
  ```js
577
577
  async function createServer() {
578
578
  const app = Fastify({ logger: true });
579
- registerTypeBoxFormats();
580
- app.setValidatorCompiler(TypeBoxValidatorCompiler);
581
579
 
582
580
  app.get("/api/health", async () => {
583
581
  return {
@@ -610,7 +608,7 @@ async function createServer() {
610
608
  }
611
609
  ```
612
610
 
613
- The health route is built in, but the more important idea is that the server is already prepared to validate HTTP input, load the JSKIT provider runtime from the app itself, and constrain requests by surface.
611
+ The health route is built in, but the more important idea is that the server is already prepared to validate HTTP input with Fastify's normal JSON Schema path, load the JSKIT provider runtime from the app itself, and constrain requests by surface.
614
612
 
615
613
  You will also notice `config/server.js`. In the base shell it is intentionally almost empty. It is there to reserve a clear place for server-side configuration as backend features are added, without pretending the starter app already has server behavior it does not yet need.
616
614
 
@@ -612,6 +612,27 @@ For this chapter's `tenancyMode = "personal"` setup, that contributor now does r
612
612
 
613
613
  So even in this chapter, the package boundary is already correct: users owns user creation/sync, and workspaces reacts through an extension point.
614
614
 
615
+ ### Workspace auth context comes from the auth policy resolver registry
616
+
617
+ There is a second registry seam in this chapter that matters just as much for the runtime model.
618
+
619
+ Workspace-aware routes do not hard-code workspace lookup inside the auth plugin itself. Instead, `workspaces-core` registers an auth policy context resolver contribution, and `auth-core` composes the registered resolvers at request time.
620
+
621
+ The important consequence is:
622
+
623
+ - `auth-core` stays generic
624
+ - `workspaces-core` contributes workspace-specific context
625
+ - the request only resolves workspace membership and permissions when a route actually asks for auth context or permissions
626
+
627
+ So the flow is:
628
+
629
+ 1. a workspace-aware route declares auth and, when needed, permission requirements
630
+ 2. the auth policy runtime authenticates the actor
631
+ 3. the composed auth policy context resolver asks the workspace layer for the current workspace context
632
+ 4. the request gains normalized `workspace`, `membership`, and `permissions` values
633
+
634
+ That is why the workspace package does not need to patch the auth plugin directly, and it is also why this setup scales better than a one-off global hook. It uses the same tagged-registry pattern as other JSKIT extension seams, but for request-time auth context instead of bootstrap payloads or lifecycle events.
635
+
615
636
  ### Why this chapter is the real routing pivot
616
637
 
617
638
  Earlier chapters added features inside a flat top-level app.
@@ -211,9 +211,7 @@ packages/contacts/
211
211
  package.json
212
212
  package.descriptor.mjs
213
213
  src/server/ContactsProvider.js
214
- src/server/actionIds.js
215
214
  src/server/actions.js
216
- src/server/listConfig.js
217
215
  src/server/registerRoutes.js
218
216
  src/server/repository.js
219
217
  src/server/service.js
@@ -265,15 +263,61 @@ If you come from an ORM stack, this is the key adjustment:
265
263
  The resource file owns:
266
264
 
267
265
  - the resource name and table name
268
- - the id column
269
- - input and output validators
270
- - operation metadata for `list`, `view`, `create`, `patch`, and `delete`
266
+ - the canonical `schema`
267
+ - `searchSchema`
268
+ - `defaultSort`
269
+ - `autofilter`
271
270
  - lookup contract configuration
272
- - messages and realtime event declarations
273
- - field metadata when column overrides or richer metadata are needed
271
+ - messages
272
+ - field metadata, including which fields participate in `output`, `create`, `replace`, and `patch`
274
273
 
275
274
  This file is the bridge between the server and the client. The UI generator reads it, and the server runtime also depends on it.
276
275
 
276
+ For a standard CRUD resource like `contacts`, the authored file is intentionally compact. It uses `defineCrudResource(...)` from `@jskit-ai/resource-crud-core`:
277
+
278
+ ```js
279
+ import { defineCrudResource } from "@jskit-ai/resource-crud-core/shared/crudResource";
280
+
281
+ const resource = defineCrudResource({
282
+ namespace: "contacts",
283
+ tableName: "contacts",
284
+ schema: {
285
+ name: {
286
+ type: "string",
287
+ maxLength: 190,
288
+ required: true,
289
+ search: true,
290
+ operations: {
291
+ output: { required: true },
292
+ create: { required: true },
293
+ patch: { required: false }
294
+ }
295
+ }
296
+ },
297
+ searchSchema: {
298
+ id: { type: "id", actualField: "id" }
299
+ },
300
+ defaultSort: ["-createdAt"],
301
+ autofilter: "workspace",
302
+ messages: {
303
+ saveSuccess: "Record saved."
304
+ },
305
+ contract: {
306
+ lookup: {
307
+ containerKey: "lookups"
308
+ }
309
+ }
310
+ });
311
+ ```
312
+
313
+ `defineCrudResource(...)` derives the standard CRUD operation contracts once at module load time and exposes them on `resource.operations`. That means:
314
+
315
+ - you author the canonical resource shape once
316
+ - JSKIT derives the standard `list` / `view` / `create` / `replace` / `patch` / `delete` contracts
317
+ - routes, actions, client code, and generators can keep reading `resource.operations.*` without each resource file repeating that boilerplate
318
+
319
+ For non-CRUD or heavily custom resources, use `defineResource(...)` from `@jskit-ai/resource-core` instead. That keeps standard CRUD derivation and custom operation bundles clearly separated.
320
+
277
321
  ### `src/shared/index.js`
278
322
 
279
323
  This is just the shared package barrel. It re-exports the resource contract and shared symbols.
@@ -292,22 +336,6 @@ It owns:
292
336
 
293
337
  It is wiring, not business logic. If you need to change how contacts are validated or saved, this is usually **not** the file to edit first.
294
338
 
295
- ### `src/server/actionIds.js`
296
-
297
- This is the stable list of action ids:
298
-
299
- ```js
300
- const actionIds = Object.freeze({
301
- list: "crud.contacts.list",
302
- view: "crud.contacts.view",
303
- create: "crud.contacts.create",
304
- update: "crud.contacts.update",
305
- delete: "crud.contacts.delete"
306
- });
307
- ```
308
-
309
- Keep this file boring. It is identity, not behavior.
310
-
311
339
  ### `src/server/actions.js`
312
340
 
313
341
  This is the action contract boundary.
@@ -344,33 +372,6 @@ In other words:
344
372
 
345
373
  Those are related, but not the same concern.
346
374
 
347
- ### `src/server/listConfig.js`
348
-
349
- This is the baseline list behavior for the repository.
350
-
351
- It owns things like:
352
-
353
- - `defaultLimit`
354
- - `maxLimit`
355
- - `orderBy`
356
- - `searchColumns`
357
-
358
- One subtle but important detail: `searchColumns` are **database column names**, not camelCase resource field keys.
359
-
360
- Example:
361
-
362
- ```js
363
- const LIST_CONFIG = Object.freeze({
364
- searchColumns: ["full_name", "email", "phone"],
365
- orderBy: [
366
- {
367
- column: "created_at",
368
- direction: "desc"
369
- }
370
- ]
371
- });
372
- ```
373
-
374
375
  ### `src/server/repository.js`
375
376
 
376
377
  This is the data-access layer.
@@ -656,6 +657,44 @@ const paths = usePaths();
656
657
  const reportsApiPath = computed(() => paths.api("/reports"));
657
658
  ```
658
659
 
660
+ Use `requestQueryParams` when a runtime needs endpoint query parameters. Do not put query strings into `apiUrlTemplate`; URL templates are for path shape only.
661
+
662
+ For routed CRUD edit forms, pass request query params through `addEditOptions`:
663
+
664
+ ```js
665
+ const formRuntime = useCrudAddEdit({
666
+ resource: uiResource,
667
+ operationName: "patch",
668
+ formFields: UI_EDIT_FORM_FIELDS,
669
+ addEditOptions: {
670
+ apiUrlTemplate: "/products/:productId",
671
+ requestQueryParams: {
672
+ include: "serviceId,bookingSteps,bookingSteps.requiredRoleId"
673
+ }
674
+ }
675
+ });
676
+ ```
677
+
678
+ `requestQueryParams` may also be a callback. The add/edit callback receives the same scoped request context used by view/list, plus the current record id and model:
679
+
680
+ ```js
681
+ const formRuntime = useCrudAddEdit({
682
+ resource: uiResource,
683
+ operationName: "patch",
684
+ formFields: UI_EDIT_FORM_FIELDS,
685
+ addEditOptions: {
686
+ apiUrlTemplate: "/products/:productId",
687
+ requestQueryParams({ recordId, model }) {
688
+ return {
689
+ include: "serviceId,bookingSteps,bookingSteps.requiredRoleId"
690
+ };
691
+ }
692
+ }
693
+ });
694
+ ```
695
+
696
+ For add/edit runtimes, these request query params apply to both the initial load and the save request path. That keeps the saved response shape aligned with the loaded form shape when an endpoint supports response includes.
697
+
659
698
  The safe mental model is:
660
699
 
661
700
  - do not raw `fetch(...)` for normal app work
@@ -663,6 +702,7 @@ The safe mental model is:
663
702
  - use the operation/runtime composable that matches the UI interaction
664
703
  - drop to `usersWebHttpClient.request(...)` only for exceptional low-level cases
665
704
  - use `usePaths().api(...)` when you need a custom scoped API path and the higher-level runtime does not already resolve it for you
705
+ - keep `apiUrlTemplate` path-only and put endpoint query strings in `requestQueryParams`
666
706
 
667
707
  ### `_components/CrudAddEditForm.vue`
668
708
 
@@ -703,7 +743,7 @@ A generated CRUD works because several layers cooperate:
703
743
  4. the route executes an action from `actions.js`
704
744
  5. the action delegates to the service in `service.js`
705
745
  6. the service calls the repository in `repository.js`
706
- 7. the repository uses `crud-core` helpers and the resource contract to talk to the database
746
+ 7. the repository uses the shared resource contract, `crud-core` helpers, and the internal JSON:API host to talk to the database
707
747
  8. the response comes back through validators and is rendered by the page
708
748
 
709
749
  That is why the generated route files are mostly containers: they are the outermost layer of a larger pipeline.
@@ -714,10 +754,10 @@ Use this rule of thumb when deciding where to edit:
714
754
 
715
755
  | Need | Primary owner | Why |
716
756
  | --- | --- | --- |
717
- | Change API/input/output contract | `contactResource.js` and `actions.js` | This is where operation shape and validators live |
757
+ | Change API/input/output contract | `contactResource.js` first, then `actions.js` only if the action boundary must diverge | Standard CRUD validators are derived from the resource; `actions.js` owns channels, permissions, and any transport-specific input composition |
718
758
  | Change route path or HTTP transport | `registerRoutes.js` | This is the HTTP layer |
719
759
  | Change permissions or channels | `actions.js` | This is the action contract boundary |
720
- | Change default ordering, limits, or searchable DB columns | `listConfig.js` | This is list runtime configuration |
760
+ | Change default ordering, searchable fields, or ownership autofilter | `contactResource.js` | The shared resource is the single source of truth for both CRUD contract and internal JSON:API resource config |
721
761
  | Change SQL, joins, parent filters, or advanced search | `repository.js` | This is the data-access layer |
722
762
  | Add cross-record or domain rules on save/delete | `service.js` | This is business logic |
723
763
  | Change page layout and display behavior | the route pages and app-owned composables | This is presentation |
@@ -788,8 +828,8 @@ The client runtime debounces the search input, writes the query string to `q`, a
788
828
  The generic CRUD stack already understands `q`.
789
829
 
790
830
  - `listSearchQueryValidator` reads and normalizes the `q` query param
791
- - the repository applies search to `list.searchColumns`
792
- - if `searchColumns` is not configured, CRUD falls back to a derived set of searchable string columns from the resource output schema
831
+ - the repository applies search through `resource.searchSchema.q`
832
+ - the generated CRUD starts with an explicit `q` search definition in `contactResource.js`, so you edit that definition directly when you want to narrow or expand the search surface
793
833
 
794
834
  For the tutorial `contacts` table, that usually means the columns behind:
795
835
 
@@ -800,27 +840,31 @@ For the tutorial `contacts` table, that usually means the columns behind:
800
840
 
801
841
  #### Best practices
802
842
 
803
- - Once the UX is stable, set explicit `searchColumns` instead of relying on the fallback.
804
- - Keep search focused on the columns users actually expect.
805
- - Remember that `searchColumns` are database columns.
843
+ - Once the UX is stable, edit `contactResource.js` and set `resource.searchSchema.q.oneOf` explicitly instead of relying on the initial generated defaults.
844
+ - Keep search focused on the fields users actually expect.
845
+ - Remember that these are JSON:API/internal resource search keys, not a free-form UI concern.
806
846
 
807
- ### Pattern 2: explicit `contacts` search columns
847
+ ### Pattern 2: explicit `contacts` search fields
808
848
 
809
- This is the first thing to do when the fallback search becomes too broad or too accidental.
849
+ This is the first thing to do when the generated `q` search becomes too broad or too accidental.
810
850
 
811
851
  #### Server side
812
852
 
813
- Set the searchable columns explicitly in `listConfig.js`:
853
+ Set the searchable fields explicitly in `contactResource.js`:
814
854
 
815
855
  ```js
816
- const LIST_CONFIG = Object.freeze({
817
- searchColumns: ["full_name", "email", "phone"],
818
- orderBy: [
819
- {
820
- column: "created_at",
821
- direction: "desc"
856
+ const resource = Object.freeze({
857
+ searchSchema: {
858
+ id: { type: "id", actualField: "id" },
859
+ q: {
860
+ type: "string",
861
+ oneOf: ["fullName", "email", "phone"],
862
+ filterOperator: "like",
863
+ splitBy: " ",
864
+ matchAll: true
822
865
  }
823
- ]
866
+ },
867
+ defaultSort: ["-createdAt"]
824
868
  });
825
869
  ```
826
870
 
@@ -841,30 +885,58 @@ Do not treat the output schema as if it also defined database storage.
841
885
  In JSKIT CRUD:
842
886
 
843
887
  - the schema defines the API contract
844
- - `resource.fieldMeta` defines storage mapping
888
+ - field definitions may also carry storage/lookup/ui metadata
845
889
  - the repository runtime owns computed SQL projections
846
890
 
847
891
  Use these rules:
848
892
 
849
- - for explicit DB column overrides, use `repository.column`
850
- - for computed output fields, use `repository.storage: "virtual"`
893
+ - for explicit DB column overrides, use `actualField`
894
+ - standard writable `date-time` fields are serialized automatically during CRUD writes
895
+ - use `storage.writeSerializer` only for non-default DB write serialization
896
+ - for computed output fields, use `storage: { virtual: true }`
851
897
  - do not put computed fields in create/patch write schemas
852
898
 
853
899
  Example field metadata:
854
900
 
855
901
  ```js
856
- RESOURCE_FIELD_META.push({
857
- key: "createdAt",
858
- repository: {
859
- column: "created_at"
860
- }
861
- });
862
-
863
- RESOURCE_FIELD_META.push({
864
- key: "remainingBatchWeight",
865
- repository: {
866
- storage: "virtual"
867
- }
902
+ import { defineCrudResource } from "@jskit-ai/resource-crud-core/shared/crudResource";
903
+
904
+ const resource = defineCrudResource({
905
+ namespace: "receivals",
906
+ tableName: "receivals",
907
+ schema: {
908
+ createdAt: {
909
+ type: "dateTime",
910
+ required: true,
911
+ actualField: "created_at",
912
+ operations: {
913
+ output: { required: true }
914
+ }
915
+ },
916
+ arrivalDatetime: {
917
+ type: "dateTime",
918
+ required: true,
919
+ storage: {
920
+ writeSerializer: "datetime-utc"
921
+ },
922
+ operations: {
923
+ output: { required: true },
924
+ create: { required: true },
925
+ patch: { required: false }
926
+ }
927
+ },
928
+ remainingBatchWeight: {
929
+ type: "number",
930
+ required: true,
931
+ storage: {
932
+ virtual: true
933
+ },
934
+ operations: {
935
+ output: { required: true }
936
+ }
937
+ }
938
+ },
939
+ crudOperations: ["list", "view", "create", "patch"]
868
940
  });
869
941
  ```
870
942
 
@@ -892,6 +964,8 @@ Once registered there:
892
964
  - generic CRUD `listByIds`
893
965
  - generic CRUD `listByForeignIds`
894
966
 
967
+ and generic CRUD writes automatically serialize standard writable `date-time` fields during create/update payload mapping, so normal datetime DB formatting does not need per-field metadata or repository-specific `preparePayload` hooks. Keep `storage.writeSerializer` for non-default cases only.
968
+
895
969
  all pick up the projection automatically, so you should not hand-patch `clearSelect()` / re-select logic into each method.
896
970
 
897
971
  Important limits:
@@ -917,7 +991,7 @@ Instead:
917
991
 
918
992
  1. define the filters once in a shared CRUD-package module
919
993
  2. build the server runtime from that definition with `createCrudListFilters(...)`
920
- 3. choose the route/action invalid-value contract explicitly with `createQueryValidator({ invalidValues: "reject" | "discard" })`
994
+ 3. choose the route/action invalid-value contract explicitly on each structured filter field with `createCrudListFilterQueryField(...)`
921
995
  4. build the client runtime from that same definition with `useCrudListFilters(...)`
922
996
 
923
997
  For example, a shared filter-definition module can look like this:
@@ -944,7 +1018,7 @@ export const CONTACTS_LIST_FILTER_DEFINITIONS = Object.freeze({
944
1018
  For a generated CRUD, treat this as the concrete file plan:
945
1019
 
946
1020
  - create `packages/contacts/src/shared/contactListFilters.js`
947
- - update `packages/contacts/src/server/registerRoutes.js` so the list route query validator includes an explicit `createQueryValidator({ invalidValues: ... })` choice, unless you already extracted list-query composition into `packages/contacts/src/server/listQueryValidators.js`
1021
+ - update `packages/contacts/src/server/registerRoutes.js` so the list route query validator lists structured filter params explicitly with `createCrudListFilterQueryField(...)`, unless you already extracted list-query composition into `packages/contacts/src/server/listQueryValidators.js`
948
1022
  - update `packages/contacts/src/server/actions.js` so the list action input validator includes that same explicit contract choice
949
1023
  - update `packages/contacts/src/server/repository.js` so the list query path builds the `createCrudListFilters(...)` runtime and calls `applyQuery(...)`
950
1024
  - update the app-owned list page or list-runtime composable, usually under `src/pages/.../contacts/` or `src/composables/...`, so it builds `useCrudListFilters(...)`, passes `queryParams` into `useCrudList(...)`, and renders chips / reset behavior from that runtime
@@ -1071,46 +1145,66 @@ const contactsListFiltersRuntime = createCrudListFilters(
1071
1145
  );
1072
1146
  ```
1073
1147
 
1074
- There is no default query-validator mode and no `runtime.queryValidator` alias. Create the validator that matches the contract you want at that route or action boundary.
1148
+ There is no default query-validator mode or route-runtime alias. Keep the filter runtime for repository semantics, but author route/action query params explicitly.
1075
1149
 
1076
1150
  Strict contract example:
1077
1151
 
1078
1152
  ```js
1079
- const contactsListFiltersQueryValidator = contactsListFiltersRuntime.createQueryValidator({
1080
- invalidValues: "reject" // malformed filter values should fail validation and return 400
1081
- });
1153
+ const contactsListFilters = CONTACTS_LIST_FILTER_DEFINITIONS;
1154
+
1155
+ const contactsListFiltersQueryValidator = {
1156
+ schema: createCrudListFilterQuerySchema({
1157
+ status: createCrudListFilterQueryField(contactsListFilters.status, {
1158
+ invalidValues: "reject"
1159
+ }),
1160
+ arrivalDate: createCrudListFilterQueryField(contactsListFilters.arrivalDate, {
1161
+ invalidValues: "reject"
1162
+ })
1163
+ }),
1164
+ mode: "patch"
1165
+ };
1082
1166
  ```
1083
1167
 
1084
1168
  Lenient contract example:
1085
1169
 
1086
1170
  ```js
1087
- const contactsListFiltersQueryValidator = contactsListFiltersRuntime.createQueryValidator({
1088
- invalidValues: "discard" // malformed filter values should be ignored and dropped by normalize()
1089
- });
1171
+ const contactsListFiltersQueryValidator = {
1172
+ schema: createCrudListFilterQuerySchema({
1173
+ status: createCrudListFilterQueryField(CONTACTS_LIST_FILTER_DEFINITIONS.status, {
1174
+ invalidValues: "discard"
1175
+ }),
1176
+ arrivalDate: createCrudListFilterQueryField(CONTACTS_LIST_FILTER_DEFINITIONS.arrivalDate, {
1177
+ invalidValues: "discard"
1178
+ })
1179
+ }),
1180
+ mode: "patch"
1181
+ };
1090
1182
  ```
1091
1183
 
1092
- Wire the runtime into the list validator and the repository:
1184
+ Wire the explicit query contract into the list validator and the repository:
1093
1185
 
1094
1186
  ```js
1095
- queryValidator: [
1187
+ const listRouteQueryValidator = composeSchemaDefinitions([
1096
1188
  listCursorPaginationQueryValidator,
1097
1189
  listSearchQueryValidator,
1098
1190
  contactsListFiltersQueryValidator,
1099
1191
  listParentFilterQueryValidator,
1100
1192
  lookupIncludeQueryValidator
1101
- ]
1193
+ ], {
1194
+ mode: "patch"
1195
+ });
1102
1196
  ```
1103
1197
 
1104
- Use that same `contactsListFiltersQueryValidator` anywhere else the list query is validated, such as the list action input validator if your CRUD package validates query shape at both the route and action boundaries.
1198
+ Use that same explicit `contactsListFiltersQueryValidator` anywhere else the list query is validated, such as the composed list action input validator if your CRUD package validates query shape at both the route and action boundaries.
1105
1199
 
1106
1200
  Choose the invalid-value contract deliberately:
1107
1201
 
1108
- - there is no default mode and no fallback alias, so every route or action that validates structured filters must call `createQueryValidator({ invalidValues: ... })` explicitly
1202
+ - there is no default mode and no fallback alias, so every structured filter field must choose `invalidValues: "reject"` or `invalidValues: "discard"` explicitly
1109
1203
  - use `invalidValues: "reject"` when malformed filter values should fail validation and produce a 400-style contract error
1110
1204
  - use `invalidValues: "discard"` when malformed filter values should be ignored and normalization should drop them
1111
1205
  - route query validation runs before auth, so this choice changes whether malformed unauthenticated requests fail at validation or fall through to auth
1112
- - for normal HTTP CRUD handlers, route-level `discard` means the handler receives already-normalized query input, so the action layer will not see those discarded bad values again later
1113
- - `jskit doctor` flags `createQueryValidator(...)` calls that do not spell out `invalidValues` directly at the call site
1206
+ - for normal HTTP CRUD handlers, route-level `discard` means the handler receives already-parsed filter values for the explicit fields you listed, so the action layer will not see those discarded bad values again later
1207
+ - the filter runtime is still a deliberate two-phase exception: schema parsing owns query-field values, then `applyQuery(...)` maps those parsed values back through filter keys and SQL semantics
1114
1208
 
1115
1209
  ```js
1116
1210
  async function list(query = {}, callOptions = {}) {
@@ -1126,11 +1220,11 @@ async function list(query = {}, callOptions = {}) {
1126
1220
 
1127
1221
  - Put the filter definitions in the CRUD package, not the page. Both server and client need them.
1128
1222
  - Keep the filter keys identical all the way through: definition key, query param key, and repository meaning.
1129
- - Do not expect a default `runtime.queryValidator` to exist. Every structured-filter validator must be created explicitly with `createQueryValidator({ invalidValues: ... })`.
1223
+ - Prefer `createCrudListFilterQueryField(...)` plus `createCrudListFilterQuerySchema(...)` at route/action boundaries so accepted structured-filter params stay visible in app code.
1130
1224
  - Use `type: "presence"` for null/not-null filters such as assigned vs unassigned storage. Do not model those as custom enums plus `applyQuery(...)` overrides unless the SQL semantics are genuinely different from `whereNotNull(...)` / `whereNull(...)`.
1131
1225
  - Use `createCrudListFilters(...)` unless the list semantics are truly unusual.
1132
1226
  - Use `q` for free-text and explicit query params for structured filters.
1133
- - Run `jskit doctor` after wiring filters. It flags inline/local filter-definition objects passed into `useCrudListFilters(...)` or `createCrudListFilters(...)`, and it flags `createQueryValidator(...)` calls that hide the invalid-value policy.
1227
+ - Run `jskit doctor` after wiring filters. It flags inline/local filter-definition objects passed into `useCrudListFilters(...)` or `createCrudListFilters(...)`.
1134
1228
 
1135
1229
  ### Pattern 4: lookup-backed structured filters
1136
1230
 
@@ -1290,14 +1384,18 @@ The CRUD stack can derive parent filter keys from the resource contract via `cre
1290
1384
  For the tutorial `addresses` table, the list search itself can stay very simple:
1291
1385
 
1292
1386
  ```js
1293
- const LIST_CONFIG = Object.freeze({
1294
- searchColumns: ["label", "line_1", "line_2", "suburb", "state", "postcode"],
1295
- orderBy: [
1296
- {
1297
- column: "created_at",
1298
- direction: "desc"
1387
+ const resource = Object.freeze({
1388
+ searchSchema: {
1389
+ id: { type: "id", actualField: "id" },
1390
+ q: {
1391
+ type: "string",
1392
+ oneOf: ["label", "line1", "line2", "suburb", "state", "postcode"],
1393
+ filterOperator: "like",
1394
+ splitBy: " ",
1395
+ matchAll: true
1299
1396
  }
1300
- ]
1397
+ },
1398
+ defaultSort: ["-createdAt"]
1301
1399
  });
1302
1400
  ```
1303
1401
 
@@ -1406,14 +1504,14 @@ Touch:
1406
1504
  - `CrudAddEditFormFields.js`
1407
1505
  - the relevant page/table display
1408
1506
 
1409
- Use `scaffold-field` when it fits, then review the generated result.
1507
+ Use `scaffold-field` when it fits, then review the generated result. It patches the canonical `schema` in the shared resource file; the standard CRUD validators are derived from that schema automatically.
1410
1508
 
1411
1509
  ### "I want a new boolean or enum list filter."
1412
1510
 
1413
1511
  Touch:
1414
1512
 
1415
1513
  - `packages/<crud>/src/shared/<crud>ListFilters.js` and make it the only authored filter-definition module
1416
- - `packages/<crud>/src/server/registerRoutes.js` and `packages/<crud>/src/server/actions.js` so the list validator includes an explicit `createQueryValidator({ invalidValues: ... })` choice, or `packages/<crud>/src/server/listQueryValidators.js` if you extracted list-query composition there
1514
+ - `packages/<crud>/src/server/registerRoutes.js` and `packages/<crud>/src/server/actions.js` so the list validator lists structured filter params explicitly with `createCrudListFilterQueryField(...)`, or `packages/<crud>/src/server/listQueryValidators.js` if you extracted list-query composition there
1417
1515
  - `packages/<crud>/src/server/repository.js` so the list query applies the runtime
1418
1516
  - the app-owned list page or list-runtime composable that calls `useCrudList(...)`
1419
1517
 
@@ -284,6 +284,8 @@ That file is the shared CRUD contract. The UI generator reads it to decide:
284
284
 
285
285
  So even though the server scaffold writes many files, the resource file is the bridge between the server and UI halves.
286
286
 
287
+ For standard CRUDs, that file is now intentionally compact. It uses `defineCrudResource(...)` from `@jskit-ai/resource-crud-core`, authors the canonical `schema` / `searchSchema` / `defaultSort` / `autofilter` shape once, and lets JSKIT derive the standard CRUD operation contracts from it.
288
+
287
289
  ### Step 3: scaffold the UI
288
290
 
289
291
  Once the resource file exists, generate the UI route tree:
@@ -566,6 +568,8 @@ Use this when:
566
568
 
567
569
  It is a maintenance tool, not the first step in the workflow.
568
570
 
571
+ It patches the canonical `schema` inside the shared resource file. That matters because the standard CRUD validators are derived from that canonical schema; you are no longer maintaining separate authored `create` / `patch` / `view` validator blocks by hand.
572
+
569
573
  ## Summary
570
574
 
571
575
  The important thing is not memorizing commands. It is learning the three shapes:
@@ -16,8 +16,8 @@ It starts with the smallest scaffold, then explains how to work with the JSKIT C
16
16
  - [Authentication](/guide/app-setup/authentication)
17
17
  - [Database Layer](/guide/app-setup/database-layer)
18
18
  - [Users](/guide/app-setup/users)
19
- - [Console](/guide/app-setup/console)
20
19
  - [Multi-homing](/guide/app-setup/multi-homing)
20
+ - [Console](/guide/app-setup/console)
21
21
 
22
22
  ### App Extras
23
23
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/agent-docs",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "Distributed JSKIT agent workflows, guides, and generated reference maps.",
5
5
  "type": "module",
6
6
  "files": [
package/patterns/INDEX.md CHANGED
@@ -28,7 +28,7 @@ How to use it:
28
28
  - `ui-testing.md`
29
29
  - filter, filters, search facets, chips, date range, enum filter, lookup filter, `useCrudListFilters`, `createCrudListFilters`
30
30
  - `filters.md`
31
- - fieldMeta, `repository.column`, computed field, virtual field, projection, `createCrudResourceRuntime`, `remainingBatchWeight`
31
+ - `actualField`, `storage.writeSerializer`, `storage.virtual`, computed field, virtual field, projection, `createCrudResourceRuntime`, `remainingBatchWeight`, datetime write serialization
32
32
  - `crud-repository-mapping.md`
33
33
 
34
34
  ## Current Patterns
@@ -47,6 +47,8 @@ Why this is the standard JSKIT shape:
47
47
  - The higher-level list, view, add/edit, and command runtimes send requests through the shared HTTP runtime.
48
48
  - `usersWebHttpClient` already handles credentials and CSRF behavior.
49
49
  - `useEndpointResource()` is the shared endpoint primitive for loading, saving, and standard load/save error handling. Higher-level runtimes add UI feedback and field-error handling on top.
50
+ - Use `requestQueryParams` for endpoint query strings on list, view, and add/edit runtimes.
51
+ - Keep `apiUrlTemplate` path-only. Do not put `?include=...` or other query strings in URL templates.
50
52
 
51
53
  Avoid:
52
54
 
@@ -54,3 +56,4 @@ Avoid:
54
56
  - page-local HTTP helpers that duplicate JSKIT runtime seams
55
57
  - manually concatenating scoped route params into API URLs
56
58
  - using a lower-level seam when a higher-level routed CRUD or command runtime already fits
59
+ - smuggling query params into `apiUrlTemplate`