@jskit-ai/agent-docs 0.1.4 → 0.1.6

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 (45) hide show
  1. package/DISTR_AGENT.md +2 -0
  2. package/guide/agent/generators/advanced-cruds.md +307 -51
  3. package/package.json +2 -1
  4. package/patterns/INDEX.md +33 -0
  5. package/patterns/child-cruds.md +30 -0
  6. package/patterns/crud-links.md +36 -0
  7. package/patterns/filters.md +92 -0
  8. package/patterns/live-actions.md +33 -0
  9. package/patterns/placements.md +28 -0
  10. package/patterns/surfaces.md +30 -0
  11. package/reference/autogen/KERNEL_MAP.md +27 -0
  12. package/reference/autogen/packages/agent-docs.md +1 -1
  13. package/reference/autogen/packages/assistant-core.md +1 -1
  14. package/reference/autogen/packages/assistant-runtime.md +22 -9
  15. package/reference/autogen/packages/assistant.md +1 -1
  16. package/reference/autogen/packages/auth-core.md +1 -1
  17. package/reference/autogen/packages/auth-provider-supabase-core.md +1 -1
  18. package/reference/autogen/packages/auth-web.md +1 -1
  19. package/reference/autogen/packages/console-core.md +1 -1
  20. package/reference/autogen/packages/console-web.md +1 -1
  21. package/reference/autogen/packages/crud-core.md +23 -1
  22. package/reference/autogen/packages/crud-server-generator.md +1 -1
  23. package/reference/autogen/packages/crud-ui-generator.md +2 -1
  24. package/reference/autogen/packages/database-runtime-mysql.md +1 -1
  25. package/reference/autogen/packages/database-runtime-postgres.md +1 -1
  26. package/reference/autogen/packages/database-runtime.md +1 -1
  27. package/reference/autogen/packages/http-runtime.md +1 -1
  28. package/reference/autogen/packages/kernel.md +28 -1
  29. package/reference/autogen/packages/realtime.md +1 -1
  30. package/reference/autogen/packages/shell-web.md +1 -1
  31. package/reference/autogen/packages/storage-runtime.md +1 -1
  32. package/reference/autogen/packages/ui-generator.md +1 -1
  33. package/reference/autogen/packages/uploads-image-web.md +1 -1
  34. package/reference/autogen/packages/uploads-runtime.md +1 -1
  35. package/reference/autogen/packages/users-core.md +1 -1
  36. package/reference/autogen/packages/users-web.md +36 -1
  37. package/reference/autogen/packages/workspaces-core.md +5 -1
  38. package/reference/autogen/packages/workspaces-web.md +9 -1
  39. package/reference/autogen/tooling/config-eslint.md +1 -1
  40. package/reference/autogen/tooling/create-app.md +1 -1
  41. package/reference/autogen/tooling/jskit-catalog.md +1 -1
  42. package/reference/autogen/tooling/jskit-cli.md +1 -1
  43. package/reference/autogen/tooling/test-support.md +1 -1
  44. package/reference/autogen/tooling/testUtils.md +1 -1
  45. package/templates/app/AGENTS.md +2 -0
package/DISTR_AGENT.md CHANGED
@@ -13,6 +13,7 @@ Use these references on demand:
13
13
 
14
14
  - `reference/autogen/KERNEL_MAP.md`
15
15
  - `reference/autogen/README.md`
16
+ - `patterns/INDEX.md`
16
17
  - `guide/agent/index.md`
17
18
  - `site/guide/index.md` when compressed guidance is ambiguous or missing nuance
18
19
  - `templates/APP_BLUEPRINT.md`
@@ -28,6 +29,7 @@ Core rules:
28
29
  - `Why this sticks: ...`
29
30
  - `Not doing: ...`
30
31
  - Keep that checkpoint compact. Do not expand it into a long preamble unless the developer asks for detail.
32
+ - When a request involves JSKIT UI, routing, surfaces, CRUDs, filters, placements, live actions, or similar implementation details, scan `patterns/INDEX.md` for matching keywords and read only the relevant pattern files.
31
33
  - Reuse existing JSKIT helpers and runtime seams before adding new local helpers.
32
34
  - A freshly scaffolded JSKIT app can still be in Stage 1. If platform decisions are not settled yet, continue with the initialize workflow before adding runtime packages.
33
35
  - Do not treat a missing `config.tenancyMode` line or an untouched minimal scaffold as a final tenancy decision.
@@ -678,7 +678,7 @@ The baseline generator output is only the start. As the tutorial's `contacts`, `
678
678
 
679
679
  - `src/server/listQueryValidators.js` when a list needs extra query filters beyond `q`
680
680
  - `src/server/service.test.js` once save/delete rules stop being trivial
681
- - `src/composables/contacts/useContactsListFilters.js` when the contacts page gains route-backed filter state
681
+ - `packages/contacts/src/shared/contactListFilters.js` when the contacts CRUD gains structured list filters that both server and client should share
682
682
  - `src/composables/addresses/useAddressDisplay.js` when addresses need app-specific display formatting
683
683
  - `src/composables/comments/useCommentsListRuntime.js` when an embedded comments list needs local UI state
684
684
 
@@ -686,6 +686,7 @@ That is the right direction of growth:
686
686
 
687
687
  - server customizations stay in the CRUD package
688
688
  - presentation and page-specific UI state stay in app-owned client files
689
+ - shared structured list filters live best in a CRUD-package shared module that both server and client can import
689
690
 
690
691
  ## Search and filters: the deep dive
691
692
 
@@ -782,18 +783,64 @@ Usually nothing changes. The client can keep sending `q`.
782
783
  - Do not dump every text column into search just because you can.
783
784
  - In this tutorial CRUD, `notes` is a good example of a field you might leave out if you want fast, predictable list search.
784
785
 
785
- ### Pattern 3: structured `contacts` filters such as `hasEmail`, `hasPhone`, and `hasNotes`
786
+ ### Pattern 3: structured list filters from one shared definition
786
787
 
787
- This is the first extension that still fits the `contacts` table from the previous chapter with no schema changes.
788
+ This is the default pattern once a CRUD list needs real filters.
788
789
 
789
- #### Client side
790
+ Do not hand-build:
791
+
792
+ - one filter shape in the page
793
+ - a second filter shape in `listQueryValidators.js`
794
+ - and a third filter shape in `repository.js`
795
+
796
+ Instead:
797
+
798
+ 1. define the filters once in a shared CRUD-package module
799
+ 2. build the server runtime from that definition with `createCrudListFilters(...)`
800
+ 3. choose the route/action invalid-value contract explicitly with `createQueryValidator({ invalidValues: "reject" | "discard" })`
801
+ 4. build the client runtime from that same definition with `useCrudListFilters(...)`
802
+
803
+ For example, a shared filter-definition module can look like this:
804
+
805
+ ```js
806
+ export const CONTACTS_LIST_FILTER_DEFINITIONS = Object.freeze({
807
+ onlyStaff: {
808
+ type: "flag",
809
+ label: "Staff"
810
+ },
811
+ onlyVip: {
812
+ type: "flag",
813
+ label: "VIP"
814
+ },
815
+ onlyArchived: {
816
+ type: "flag",
817
+ label: "Archived"
818
+ }
819
+ });
820
+ ```
790
821
 
791
- Keep filter state in a small app-owned composable, then pass it into `useCrudList()` as `queryParams`.
822
+ #### Exact file checklist
792
823
 
793
- Example shape:
824
+ For a generated CRUD, treat this as the concrete file plan:
825
+
826
+ - create `packages/contacts/src/shared/contactListFilters.js`
827
+ - 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`
828
+ - update `packages/contacts/src/server/actions.js` so the list action input validator includes that same explicit contract choice
829
+ - update `packages/contacts/src/server/repository.js` so the list query path builds the `createCrudListFilters(...)` runtime and calls `applyQuery(...)`
830
+ - 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
831
+
832
+ The only file in that list that is normally **new** is the shared module:
833
+
834
+ - `packages/contacts/src/shared/contactListFilters.js`
835
+
836
+ The others are normally edits to the generated CRUD package and the app-owned page layer that already exist.
837
+
838
+ #### Client side
839
+
840
+ Build the filter runtime directly from the shared definitions:
794
841
 
795
842
  ```js
796
- const listFilters = useContactsListFilters();
843
+ const listFilters = useCrudListFilters(CONTACTS_LIST_FILTER_DEFINITIONS);
797
844
 
798
845
  const records = useCrudList({
799
846
  resource: uiResource,
@@ -802,9 +849,7 @@ const records = useCrudList({
802
849
  enabled: true,
803
850
  mode: "query"
804
851
  },
805
- queryParams: computed(() => ({
806
- ...listFilters.filters
807
- })),
852
+ queryParams: listFilters.queryParams,
808
853
  syncToRoute: {
809
854
  enabled: true,
810
855
  search: true,
@@ -814,65 +859,274 @@ const records = useCrudList({
814
859
  });
815
860
  ```
816
861
 
817
- This keeps the filters:
862
+ That gives you, from one place:
863
+
864
+ - `listFilters.values`
865
+ - `listFilters.queryParams`
866
+ - `listFilters.presets`
867
+ - `listFilters.activeChips`
868
+ - `listFilters.hasActiveFilters`
869
+ - `listFilters.clearChip(...)`
870
+ - `listFilters.clearFilters()`
871
+ - `listFilters.toggle(...)` for flag filters
872
+ - `listFilters.applyPreset(...)`
873
+ - `listFilters.matchesPreset(...)`
874
+
875
+ So the same runtime owns:
876
+
877
+ - URL-synced query params
878
+ - filter chips
879
+ - reset logic
880
+ - preset application
881
+ - preset active-state matching
882
+ - small flag toggles
883
+
884
+ For relative-date quick filters, keep the date math in runtime presets instead of page-local helper state. `resolveValues(...)` runs at preset-apply time and receives `{ values, filters, presetKey, preset }`, so the preset can derive values from the current filter state and the normalized preset metadata:
885
+
886
+ ```js
887
+ const listFilters = useCrudListFilters(
888
+ RECEIVAL_LIST_FILTER_DEFINITIONS,
889
+ {
890
+ presets: [
891
+ {
892
+ key: "today",
893
+ label: "Today",
894
+ resolveValues({ presetKey }) {
895
+ const today = formatDateInputValue(new Date());
896
+ return {
897
+ arrivalDate: {
898
+ from: today,
899
+ to: today
900
+ }
901
+ };
902
+ }
903
+ },
904
+ {
905
+ key: "last7",
906
+ label: "Last 7 Days",
907
+ resolveValues({ values }) {
908
+ const today = formatDateInputValue(new Date());
909
+ return {
910
+ arrivalDate: {
911
+ from: shiftDateInputValue(today, -6),
912
+ to: today
913
+ }
914
+ };
915
+ }
916
+ }
917
+ ]
918
+ }
919
+ );
920
+ ```
921
+
922
+ ```vue
923
+ <v-chip
924
+ v-for="preset in listFilters.presets"
925
+ :key="preset.key"
926
+ :variant="listFilters.matchesPreset(preset.key) ? 'flat' : 'outlined'"
927
+ @click="listFilters.applyPreset(preset.key, { mode: 'merge' })"
928
+ >
929
+ {{ preset.label }}
930
+ </v-chip>
931
+ ```
932
+
933
+ Use `mode: "merge"` when a preset should only change one filter group, such as the arrival date range, and should not clear the rest of the page's active filters.
818
934
 
819
- - visible in the URL
820
- - restorable on refresh
821
- - preserved when opening a record and coming back
935
+ `matchesPreset(...)` is strict by design. It compares the preset against the full current filter state after basic normalization, and it does **not** silently drop extra `enumMany` or `recordIdMany` values that were hydrated from the route and still appear as chips. If the URL contains `status=archived&status=bogus`, a preset for only `archived` should render as inactive until the extra `bogus` value is cleared.
822
936
 
823
937
  #### Server side
824
938
 
825
- Add a dedicated query validator:
939
+ Build the server runtime from the same shared definitions:
826
940
 
827
941
  ```js
828
- const contactsListFiltersQueryValidator = Object.freeze({
829
- schema: Type.Object(
830
- {
831
- hasEmail: Type.Optional(...),
832
- hasPhone: Type.Optional(...),
833
- hasNotes: Type.Optional(...)
834
- },
835
- { additionalProperties: false }
836
- ),
837
- normalize(payload = {}) {
838
- const normalized = {};
839
- if (Object.hasOwn(payload, "hasEmail")) normalized.hasEmail = 1;
840
- if (Object.hasOwn(payload, "hasPhone")) normalized.hasPhone = 1;
841
- if (Object.hasOwn(payload, "hasNotes")) normalized.hasNotes = 1;
842
- return normalized;
942
+ const contactsListFiltersRuntime = createCrudListFilters(
943
+ CONTACTS_LIST_FILTER_DEFINITIONS,
944
+ {
945
+ columns: {
946
+ onlyStaff: "is_staff",
947
+ onlyVip: "vip",
948
+ onlyArchived: "archived"
949
+ }
843
950
  }
951
+ );
952
+ ```
953
+
954
+ 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.
955
+
956
+ Strict contract example:
957
+
958
+ ```js
959
+ const contactsListFiltersQueryValidator = contactsListFiltersRuntime.createQueryValidator({
960
+ invalidValues: "reject" // malformed filter values should fail validation and return 400
961
+ });
962
+ ```
963
+
964
+ Lenient contract example:
965
+
966
+ ```js
967
+ const contactsListFiltersQueryValidator = contactsListFiltersRuntime.createQueryValidator({
968
+ invalidValues: "discard" // malformed filter values should be ignored and dropped by normalize()
844
969
  });
845
970
  ```
846
971
 
847
- Wire it into the route and action validators for the list operation, then apply the filter in `repository.js`:
972
+ Wire the runtime into the list validator and the repository:
973
+
974
+ ```js
975
+ queryValidator: [
976
+ listCursorPaginationQueryValidator,
977
+ listSearchQueryValidator,
978
+ contactsListFiltersQueryValidator,
979
+ listParentFilterQueryValidator,
980
+ lookupIncludeQueryValidator
981
+ ]
982
+ ```
983
+
984
+ 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.
985
+
986
+ Choose the invalid-value contract deliberately:
987
+
988
+ - there is no default mode and no fallback alias, so every route or action that validates structured filters must call `createQueryValidator({ invalidValues: ... })` explicitly
989
+ - use `invalidValues: "reject"` when malformed filter values should fail validation and produce a 400-style contract error
990
+ - use `invalidValues: "discard"` when malformed filter values should be ignored and normalization should drop them
991
+ - route query validation runs before auth, so this choice changes whether malformed unauthenticated requests fail at validation or fall through to auth
992
+ - 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
993
+ - `jskit doctor` flags `createQueryValidator(...)` calls that do not spell out `invalidValues` directly at the call site
848
994
 
849
995
  ```js
850
996
  async function list(query = {}, callOptions = {}) {
851
997
  return crudRepositoryList(repositoryRuntime, knex, query, options, callOptions, {
852
998
  modifyQuery(dbQuery, context = {}) {
853
- const sourceQuery = context.query || {};
999
+ return contactsListFiltersRuntime.applyQuery(dbQuery, context.query || {});
1000
+ }
1001
+ });
1002
+ }
1003
+ ```
854
1004
 
855
- if (sourceQuery.hasEmail !== undefined) {
856
- dbQuery.whereNotNull("email").where("email", "<>", "");
857
- }
858
- if (sourceQuery.hasPhone !== undefined) {
859
- dbQuery.whereNotNull("phone").where("phone", "<>", "");
1005
+ #### Best practices
1006
+
1007
+ - Put the filter definitions in the CRUD package, not the page. Both server and client need them.
1008
+ - Keep the filter keys identical all the way through: definition key, query param key, and repository meaning.
1009
+ - Do not expect a default `runtime.queryValidator` to exist. Every structured-filter validator must be created explicitly with `createQueryValidator({ invalidValues: ... })`.
1010
+ - 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(...)`.
1011
+ - Use `createCrudListFilters(...)` unless the list semantics are truly unusual.
1012
+ - Use `q` for free-text and explicit query params for structured filters.
1013
+ - 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.
1014
+
1015
+ ### Pattern 4: lookup-backed structured filters
1016
+
1017
+ This is the next real-world step: filters like `supplierContactId`, `locationId`, or `contactId` where the user needs:
1018
+
1019
+ - remote autocomplete search
1020
+ - URL-synced selected ids
1021
+ - readable chip labels instead of raw ids
1022
+
1023
+ The right pattern is:
1024
+
1025
+ 1. keep the lookup filter in the shared definitions
1026
+ 2. use `useCrudListFilters(...)` for state, chips, and query params
1027
+ 3. use `useCrudListFilterLookups(...)` for option loading and label resolution
1028
+
1029
+ Example shared definition:
1030
+
1031
+ ```js
1032
+ export const RECEIVAL_LIST_FILTER_DEFINITIONS = Object.freeze({
1033
+ supplierContactId: {
1034
+ type: "recordIdMany",
1035
+ label: "Supplier",
1036
+ lookup: {
1037
+ namespace: "contacts"
1038
+ }
1039
+ },
1040
+ pollenTypeId: {
1041
+ type: "recordIdMany",
1042
+ label: "Pollen Type",
1043
+ lookup: {
1044
+ namespace: "pollen-types",
1045
+ labelKey: "name"
1046
+ }
1047
+ }
1048
+ });
1049
+ ```
1050
+
1051
+ #### Exact file checklist
1052
+
1053
+ Lookup-backed filters do **not** change the ownership model from Pattern 3. The file plan is still:
1054
+
1055
+ - keep the shared definition in `packages/receivals/src/shared/receivalListFilters.js`
1056
+ - update the same server validator and repository files from Pattern 3
1057
+ - update the app-owned list page or list-runtime composable so it creates both `useCrudListFilters(...)` and `useCrudListFilterLookups(...)`
1058
+ - bind the lookup UI, such as `v-autocomplete`, from `filterLookups.resolveLookup(...)`
1059
+
1060
+ Do **not** create a second page-local filter schema just because the UI needs remote autocomplete. The shared definition file stays the source of truth.
1061
+
1062
+ #### Client side
1063
+
1064
+ ```js
1065
+ let filterLookups = null;
1066
+
1067
+ const listFilters = useCrudListFilters(
1068
+ RECEIVAL_LIST_FILTER_DEFINITIONS,
1069
+ {
1070
+ labelResolvers: {
1071
+ supplierContactId(value) {
1072
+ return filterLookups?.resolveLookupLabel("supplierContactId", value, "Supplier") || "";
860
1073
  }
861
- if (sourceQuery.hasNotes !== undefined) {
862
- dbQuery.whereNotNull("notes").where("notes", "<>", "");
1074
+ }
1075
+ }
1076
+ );
1077
+
1078
+ filterLookups = useCrudListFilterLookups(
1079
+ RECEIVAL_LIST_FILTER_DEFINITIONS,
1080
+ {
1081
+ values: listFilters.values,
1082
+ queryKeyPrefix: ["ui-generator", "receivals", "filters"],
1083
+ placementSourcePrefix: "ui-generator.receivals.list.filters",
1084
+ requestQueryParams: {
1085
+ supplierContactId: { limit: 25 }
1086
+ },
1087
+ labelResolvers: {
1088
+ supplierContactId(item = {}) {
1089
+ return `${item.firstName} ${item.lastName}`.trim();
863
1090
  }
864
1091
  }
865
- });
866
- }
1092
+ }
1093
+ );
1094
+
1095
+ const supplierFilterLookup = filterLookups.resolveLookup("supplierContactId");
867
1096
  ```
868
1097
 
1098
+ Then bind the autocomplete:
1099
+
1100
+ ```vue
1101
+ <v-autocomplete
1102
+ v-model="listFilters.values.supplierContactId"
1103
+ :items="supplierFilterLookup.options"
1104
+ :search="supplierFilterLookup.searchQuery"
1105
+ :loading="supplierFilterLookup.isLoading"
1106
+ item-title="label"
1107
+ item-value="value"
1108
+ multiple
1109
+ chips
1110
+ no-filter
1111
+ @update:search="supplierFilterLookup.setSearch"
1112
+ />
1113
+ ```
1114
+
1115
+ #### Why this is better than a page-local `useList()` wrapper
1116
+
1117
+ - the CRUD filter state still lives in `useCrudListFilters(...)`
1118
+ - the autocomplete loading logic lives in one reusable helper
1119
+ - the label resolution used by filter chips and the autocomplete stays consistent
1120
+ - a second screen can reuse the same pattern instead of rewriting it
1121
+
869
1122
  #### Best practices
870
1123
 
871
- - Keep the client keys, validator keys, and repository keys identical.
872
- - Use dedicated query params for booleans and facets. Do not overload `q`.
873
- - Put SQL in the repository, not in the page.
1124
+ - Put lookup metadata in the shared filter definitions.
1125
+ - Use `useCrudListFilterLookups(...)` for remote filter autocompletes instead of building a custom `useList()` wrapper per screen.
1126
+ - Keep lookup label formatting on the client side. It is UI presentation, not repository logic.
1127
+ - Keep unusual SQL semantics, such as `pending = whereNull(...)`, in the server runtime `apply` override.
874
1128
 
875
- ### Pattern 4: free-text search plus structured `contacts` filters together
1129
+ ### Pattern 5: free-text search plus structured filters together
876
1130
 
877
1131
  This is the most common real-world CRUD list.
878
1132
 
@@ -895,7 +1149,7 @@ Let the generic list search handle `q`, and let your custom validator/repository
895
1149
  - Preserve the current route query when linking to view/edit pages so users can return to the same filtered list state.
896
1150
  - Let list changes reset pagination; `useCrudList()` already does this for search and query-param changes.
897
1151
 
898
- ### Pattern 5: parent-scoped child CRUD search for `addresses`
1152
+ ### Pattern 6: parent-scoped child CRUD search for `addresses`
899
1153
 
900
1154
  For nested CRUDs such as the `addresses` resource from the previous chapter, parent scoping and search usually work together.
901
1155
 
@@ -935,7 +1189,7 @@ That keeps child-list filtering grounded in the actual resource definition inste
935
1189
  - Let the resource contract define parent filter shape.
936
1190
  - Treat parent-scoped filtering as repository/query behavior, not as presentation logic.
937
1191
 
938
- ### Pattern 6: local-only search for embedded `comments`
1192
+ ### Pattern 7: local-only search for embedded `comments`
939
1193
 
940
1194
  Sometimes server search is unnecessary.
941
1195
 
@@ -1038,10 +1292,12 @@ Use `scaffold-field` when it fits, then review the generated result.
1038
1292
 
1039
1293
  Touch:
1040
1294
 
1041
- - a client filter composable or page state
1042
- - `useCrudList({ queryParams: ... })`
1043
- - a dedicated server query validator
1044
- - `repository.js`
1295
+ - `packages/<crud>/src/shared/<crud>ListFilters.js` and make it the only authored filter-definition module
1296
+ - `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
1297
+ - `packages/<crud>/src/server/repository.js` so the list query applies the runtime
1298
+ - the app-owned list page or list-runtime composable that calls `useCrudList(...)`
1299
+
1300
+ If the filter is lookup-backed, touch that same client file again to wire `useCrudListFilterLookups(...)`.
1045
1301
 
1046
1302
  ### "I want a new save rule."
1047
1303
 
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@jskit-ai/agent-docs",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Distributed JSKIT agent workflows, guides, and generated reference maps.",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "DISTR_AGENT.md",
8
8
  "guide",
9
+ "patterns",
9
10
  "reference",
10
11
  "skills",
11
12
  "templates",
@@ -0,0 +1,33 @@
1
+ # JSKIT Patterns Index
2
+
3
+ Use this index as the first stop for recurring JSKIT implementation heuristics and workflow traps.
4
+
5
+ How to use it:
6
+
7
+ - Match the user request against the keywords below.
8
+ - Open only the relevant pattern files.
9
+ - Use patterns as implementation guidance, not as permission to skip normal scoping or user clarification.
10
+
11
+ ## Keyword Map
12
+
13
+ - tabs, menu items, icons, shell links, profile links, subpage tabs, placements
14
+ - `placements.md`
15
+ - surfaces, app/admin/home/console, "which surface", route ownership, placement visibility
16
+ - `surfaces.md`
17
+ - child crud, nested crud, embedded list, subroute, separate page, parent/child layout
18
+ - `child-cruds.md`
19
+ - CRUD links, record placeholders, `paths.page()`, `resolveViewUrl`, `resolveEditUrl`, `resolveParams`
20
+ - `crud-links.md`
21
+ - live actions, checkbox, toggle, patch button, inline action, `useCommand()`
22
+ - `live-actions.md`
23
+ - filter, filters, search facets, chips, date range, enum filter, lookup filter, `useCrudListFilters`, `createCrudListFilters`
24
+ - `filters.md`
25
+
26
+ ## Current Patterns
27
+
28
+ - [placements.md](./placements.md)
29
+ - [surfaces.md](./surfaces.md)
30
+ - [child-cruds.md](./child-cruds.md)
31
+ - [crud-links.md](./crud-links.md)
32
+ - [live-actions.md](./live-actions.md)
33
+ - [filters.md](./filters.md)
@@ -0,0 +1,30 @@
1
+ # Child CRUD Layout Patterns
2
+
3
+ Use when:
4
+
5
+ - planning nested CRUDs
6
+ - adding records under a parent record
7
+ - deciding whether child records need their own page or stay inside the parent view
8
+
9
+ Rules:
10
+
11
+ - Before generating a child CRUD, ask how the user wants the child records laid out.
12
+ - Do not assume one layout pattern by default.
13
+
14
+ Clarify these options:
15
+
16
+ - embedded in the parent view
17
+ - often a list rendered directly inside the parent page
18
+ - embedded as child subroutes
19
+ - the parent page becomes a routed container or subpage host
20
+ - totally separate route/page
21
+ - the child list/view/edit flow lives in its own route area
22
+
23
+ Why this matters:
24
+
25
+ - the answer changes route structure, placements, host containers, and which generator flow fits best
26
+ - child CRUD layout mistakes are expensive to unwind later
27
+
28
+ Avoid:
29
+
30
+ - generating nested CRUD routes before the parent/child layout is agreed
@@ -0,0 +1,36 @@
1
+ # CRUD Link Resolution Patterns
2
+
3
+ Use when:
4
+
5
+ - customizing generated CRUD pages
6
+ - wiring buttons or links on list/view/edit pages
7
+ - building nested CRUD links
8
+
9
+ Rules:
10
+
11
+ - Use `paths.page()` for surface-aware navigation that only needs surface params.
12
+ - Do not use `paths.page()` with CRUD record placeholders such as `:contactId`, `:addressId`, `:todoListId`, or `:todoItemId` in CRUD-bound route suffixes.
13
+ - For CRUD-bound links, use the runtime that owns the current CRUD scope.
14
+
15
+ Prefer:
16
+
17
+ - list pages
18
+ - `records.resolveViewUrl(record)`
19
+ - `records.resolveEditUrl(record)`
20
+ - `records.resolveParams(template, extraParams)`
21
+ - view pages
22
+ - `view.listUrl`
23
+ - `view.editUrl`
24
+ - `view.resolveParams(template, extraParams)`
25
+ - add/edit pages
26
+ - `formRuntime.addEdit.resolveParams(template, extraParams)`
27
+
28
+ Scope rule:
29
+
30
+ - Use the runtime anchored to the record that owns the action.
31
+ - On a parent record view with nested child routes, parent actions should still resolve from the parent `view` runtime even while a child route is active.
32
+
33
+ Avoid:
34
+
35
+ - `paths.page("/lists/:todoListId/items/new")`
36
+ - `paths.page("/lists/:todoListId/edit")`
@@ -0,0 +1,92 @@
1
+ # Structured Filters
2
+
3
+ Use when:
4
+ - a CRUD list needs flags, enums, date ranges, record-id filters, or lookup-backed filters
5
+ - the user asks to "add a filter", "make the list filterable", or "keep filters in the URL"
6
+ - a screen mixes free-text search with structured facets
7
+
8
+ Ask first:
9
+ - which fields are filterable
10
+ - whether each filter is a flag, enum, multi-enum, date/date-range, number-range, record id, or lookup-backed record id
11
+ - whether filters must sync to the route query
12
+ - whether the screen needs chips and clear/reset behavior
13
+ - whether lookup filters need remote autocomplete search
14
+ - whether there are presets such as "Today", "Last 7 Days", or "Only Archived"
15
+
16
+ Default JSKIT pattern:
17
+ 1. Put shared filter definitions in the CRUD package.
18
+ Example path: `packages/<crud>/src/shared/<crud>ListFilters.js`
19
+ 2. Build the server runtime from that module with `createCrudListFilters(...)`.
20
+ 3. Build route/action validators explicitly with `createQueryValidator({ invalidValues: "reject" | "discard" })`, and use `applyQuery(...)` in the repository. There is no default validator mode and no `runtime.queryValidator` alias.
21
+ 4. Build the client runtime from the same shared definitions with `useCrudListFilters(...)`. Presets can use static `values` or dynamic `resolveValues({ values, filters, presetKey, preset })`.
22
+ 5. Pass `listFilters.queryParams` into `useCrudList(...)`.
23
+ 6. For lookup-backed filters, use `useCrudListFilterLookups(...)` instead of hand-rolling `useList()` in each page.
24
+
25
+ Exact file checklist:
26
+ - create `packages/<crud>/src/shared/<crud>ListFilters.js`
27
+ - update `packages/<crud>/src/server/registerRoutes.js` and `packages/<crud>/src/server/actions.js` so the list query validator includes an explicit `createQueryValidator({ invalidValues: ... })` choice, or update `packages/<crud>/src/server/listQueryValidators.js` if that package already extracts list-query composition there
28
+ - update `packages/<crud>/src/server/repository.js` so list queries call the runtime's `applyQuery(...)`
29
+ - update the app-owned list page or list-runtime composable under `src/pages/...` or `src/composables/...` so it builds `useCrudListFilters(...)`, passes `queryParams` into `useCrudList(...)`, and binds chips / reset behavior
30
+ - for lookup-backed filters, update that same client file to build `useCrudListFilterLookups(...)` and bind the lookup control from `resolveLookup(...)`
31
+
32
+ Validation mode is part of the contract:
33
+ - there is no default mode and no fallback alias, so every route/action boundary that validates structured filters must call `createQueryValidator({ invalidValues: ... })` explicitly
34
+ - use `invalidValues: "reject"` when malformed filter values should fail validation and return a 400-style contract error
35
+ - use `invalidValues: "discard"` when malformed filter values should be ignored and normalization should drop them
36
+ - route query validation runs before auth, so this choice affects whether malformed unauthenticated requests fail at validation or fall through to auth
37
+ - for normal HTTP CRUD handlers, route-level `discard` means the action layer receives already-normalized query input; do not assume route `discard` plus action `reject` will still reject malformed HTTP query strings later
38
+ - `jskit doctor` flags `createQueryValidator(...)` calls that do not spell out `invalidValues` directly at the call site
39
+
40
+ Keep separate:
41
+ - free-text search uses `records.searchQuery` and `q`
42
+ - structured filters use explicit query params through `useCrudListFilters(...)`
43
+
44
+ Use `useCrudListFilterLookups(...)` when:
45
+ - a filter is `recordId` or `recordIdMany`
46
+ - the UI needs remote autocomplete search
47
+ - chips should show readable labels instead of raw ids
48
+
49
+ Use built-in `presence` when:
50
+ - a filter means null vs not-null, such as assigned vs unassigned storage
51
+ - the UI wants custom labels like "Assigned" / "Unassigned" but the transport contract can stay `present` / `missing`
52
+ - you do **not** need custom SQL beyond `whereNotNull(...)` / `whereNull(...)`
53
+
54
+ Use runtime presets when:
55
+ - the page has quick filters such as "Today", "Last 7 Days", or "Only Archived"
56
+ - the preset should reuse the same filter state/reset/query-param runtime as the rest of the page
57
+ - relative-date presets need dynamic `resolveValues()` instead of hard-coded dates
58
+ - the active-state UI should reflect the full current filter state through `matchesPreset(...)`, including extra route-hydrated values that still appear as chips
59
+
60
+ Put unusual SQL semantics on the server:
61
+ - examples: `pending` meaning `whereNull("ccp1_passed")`, or business-specific status buckets that combine multiple columns
62
+ - implement those in `createCrudListFilters(..., { apply: { ... } })`
63
+ - do not use custom `apply` just for null/not-null checks when `type: "presence"` fits
64
+
65
+ Avoid:
66
+ - local filter composables that duplicate the same keys the server already knows about
67
+ - a custom validator shape that does not match the page state
68
+ - assuming a default `runtime.queryValidator` exists; always create the validator explicitly with `createQueryValidator({ invalidValues: ... })`
69
+ - hand-rolled preset apply/reset/active-state helpers when `useCrudListFilters(..., { presets })`, `applyPreset(...)`, and `matchesPreset(...)` fit
70
+ - per-screen `useList()` wrappers for lookup-backed filters when `useCrudListFilterLookups(...)` fits
71
+ - a second page-local filter-definition file when `packages/<crud>/src/shared/<crud>ListFilters.js` should be the source of truth
72
+ - overloading `q` with structured filter meaning
73
+ - local or inline filter-definition objects passed into `useCrudListFilters(...)` or `createCrudListFilters(...)`; `jskit doctor` expects those runtimes to import from a CRUD shared `*ListFilters` module
74
+
75
+ Good shape:
76
+ - `packages/receivals/src/shared/receivalListFilters.js`
77
+ - `createCrudListFilters(RECEIVAL_LIST_FILTER_DEFINITIONS, ...)`
78
+ - `const listFilters = useCrudListFilters(RECEIVAL_LIST_FILTER_DEFINITIONS, { presets: [...] })`
79
+ - `const filterLookups = useCrudListFilterLookups(RECEIVAL_LIST_FILTER_DEFINITIONS, { values: listFilters.values, ... })`
80
+ - `queryParams: listFilters.queryParams`
81
+
82
+ Preset contract notes:
83
+ - `resolveValues({ values, filters, presetKey, preset })` receives the current filter state and the normalized preset metadata, so relative-date presets can derive values at apply time without page-local helper state
84
+ - `matchesPreset(...)` compares against the full current filter state after basic normalization; it does **not** silently drop extra `enumMany` or `recordIdMany` values that were hydrated from the route and still show up as active chips
85
+ - if the URL contains `status=archived&status=bogus`, a preset for only `archived` should not render as active while the `bogus` chip is still visible
86
+
87
+ Review checks:
88
+ - one shared filter definition source of truth
89
+ - server validator/repository logic derived from that source
90
+ - client query params/chips/reset logic derived from that source
91
+ - lookup-backed filters use the shared lookup helper, not a page-local mini-framework
92
+ - `jskit doctor` stays clean for filter ownership and explicit validator policy