@jskit-ai/agent-docs 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.
@@ -613,6 +613,57 @@ The safe mental model is:
613
613
  - use command runtimes for actions
614
614
  - keep the server as the source of truth for derived state
615
615
 
616
+ ### Choosing the right client request seam
617
+
618
+ When you need client-side HTTP work in JSKIT, do not start with raw `fetch(...)`.
619
+
620
+ Choose the highest-level runtime that matches the interaction:
621
+
622
+ ```js
623
+ // 1. Button/toggle/small mutation
624
+ const command = useCommand({ ... });
625
+
626
+ // 2. List endpoint
627
+ const list = useList({ ... });
628
+
629
+ // 3. Single-record endpoint
630
+ const view = useView({ ... });
631
+
632
+ // 4. Form save flow
633
+ const form = useAddEdit({ ... });
634
+
635
+ // 5. Truly custom endpoint
636
+ const resource = useEndpointResource({ ... });
637
+ ```
638
+
639
+ Use the CRUD wrappers when they fit:
640
+
641
+ - `useCrudList()` for routed CRUD lists
642
+ - `useCrudView()` for routed CRUD record loading
643
+ - `useCrudAddEdit()` for routed CRUD forms
644
+
645
+ Why this is the standard JSKIT shape:
646
+
647
+ - `useCommand()` resolves the correct scoped API path for the current route and surface.
648
+ - The higher-level list, view, add/edit, and command runtimes send requests through the standard HTTP runtime instead of ad hoc request code.
649
+ - The default client runtime uses `usersWebHttpClient`, which already handles credentials and CSRF token behavior.
650
+ - `useEndpointResource()` gives the shared endpoint primitive for loading, saving, and standard load/save error handling. Higher-level runtimes like `useCommand()` and `useAddEdit()` layer UI feedback and field-error behavior on top of that primitive.
651
+
652
+ If you need a custom scoped endpoint path outside the higher-level runtimes, prefer `usePaths().api(...)` rather than hand-building scoped URLs:
653
+
654
+ ```js
655
+ const paths = usePaths();
656
+ const reportsApiPath = computed(() => paths.api("/reports"));
657
+ ```
658
+
659
+ The safe mental model is:
660
+
661
+ - do not raw `fetch(...)` for normal app work
662
+ - do not invent ad hoc local AJAX helpers
663
+ - use the operation/runtime composable that matches the UI interaction
664
+ - drop to `usersWebHttpClient.request(...)` only for exceptional low-level cases
665
+ - use `usePaths().api(...)` when you need a custom scoped API path and the higher-level runtime does not already resolve it for you
666
+
616
667
  ### `_components/CrudAddEditForm.vue`
617
668
 
618
669
  This is the shared rendering shell for the add/edit form.
@@ -678,7 +729,7 @@ The baseline generator output is only the start. As the tutorial's `contacts`, `
678
729
 
679
730
  - `src/server/listQueryValidators.js` when a list needs extra query filters beyond `q`
680
731
  - `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
732
+ - `packages/contacts/src/shared/contactListFilters.js` when the contacts CRUD gains structured list filters that both server and client should share
682
733
  - `src/composables/addresses/useAddressDisplay.js` when addresses need app-specific display formatting
683
734
  - `src/composables/comments/useCommentsListRuntime.js` when an embedded comments list needs local UI state
684
735
 
@@ -686,6 +737,7 @@ That is the right direction of growth:
686
737
 
687
738
  - server customizations stay in the CRUD package
688
739
  - presentation and page-specific UI state stay in app-owned client files
740
+ - shared structured list filters live best in a CRUD-package shared module that both server and client can import
689
741
 
690
742
  ## Search and filters: the deep dive
691
743
 
@@ -782,18 +834,64 @@ Usually nothing changes. The client can keep sending `q`.
782
834
  - Do not dump every text column into search just because you can.
783
835
  - In this tutorial CRUD, `notes` is a good example of a field you might leave out if you want fast, predictable list search.
784
836
 
785
- ### Pattern 3: structured `contacts` filters such as `hasEmail`, `hasPhone`, and `hasNotes`
837
+ ### Pattern 3: structured list filters from one shared definition
786
838
 
787
- This is the first extension that still fits the `contacts` table from the previous chapter with no schema changes.
839
+ This is the default pattern once a CRUD list needs real filters.
788
840
 
789
- #### Client side
841
+ Do not hand-build:
842
+
843
+ - one filter shape in the page
844
+ - a second filter shape in `listQueryValidators.js`
845
+ - and a third filter shape in `repository.js`
846
+
847
+ Instead:
848
+
849
+ 1. define the filters once in a shared CRUD-package module
850
+ 2. build the server runtime from that definition with `createCrudListFilters(...)`
851
+ 3. choose the route/action invalid-value contract explicitly with `createQueryValidator({ invalidValues: "reject" | "discard" })`
852
+ 4. build the client runtime from that same definition with `useCrudListFilters(...)`
853
+
854
+ For example, a shared filter-definition module can look like this:
855
+
856
+ ```js
857
+ export const CONTACTS_LIST_FILTER_DEFINITIONS = Object.freeze({
858
+ onlyStaff: {
859
+ type: "flag",
860
+ label: "Staff"
861
+ },
862
+ onlyVip: {
863
+ type: "flag",
864
+ label: "VIP"
865
+ },
866
+ onlyArchived: {
867
+ type: "flag",
868
+ label: "Archived"
869
+ }
870
+ });
871
+ ```
872
+
873
+ #### Exact file checklist
874
+
875
+ For a generated CRUD, treat this as the concrete file plan:
876
+
877
+ - create `packages/contacts/src/shared/contactListFilters.js`
878
+ - 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`
879
+ - update `packages/contacts/src/server/actions.js` so the list action input validator includes that same explicit contract choice
880
+ - update `packages/contacts/src/server/repository.js` so the list query path builds the `createCrudListFilters(...)` runtime and calls `applyQuery(...)`
881
+ - 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
882
+
883
+ The only file in that list that is normally **new** is the shared module:
790
884
 
791
- Keep filter state in a small app-owned composable, then pass it into `useCrudList()` as `queryParams`.
885
+ - `packages/contacts/src/shared/contactListFilters.js`
792
886
 
793
- Example shape:
887
+ The others are normally edits to the generated CRUD package and the app-owned page layer that already exist.
888
+
889
+ #### Client side
890
+
891
+ Build the filter runtime directly from the shared definitions:
794
892
 
795
893
  ```js
796
- const listFilters = useContactsListFilters();
894
+ const listFilters = useCrudListFilters(CONTACTS_LIST_FILTER_DEFINITIONS);
797
895
 
798
896
  const records = useCrudList({
799
897
  resource: uiResource,
@@ -802,9 +900,7 @@ const records = useCrudList({
802
900
  enabled: true,
803
901
  mode: "query"
804
902
  },
805
- queryParams: computed(() => ({
806
- ...listFilters.filters
807
- })),
903
+ queryParams: listFilters.queryParams,
808
904
  syncToRoute: {
809
905
  enabled: true,
810
906
  search: true,
@@ -814,65 +910,274 @@ const records = useCrudList({
814
910
  });
815
911
  ```
816
912
 
817
- This keeps the filters:
913
+ That gives you, from one place:
914
+
915
+ - `listFilters.values`
916
+ - `listFilters.queryParams`
917
+ - `listFilters.presets`
918
+ - `listFilters.activeChips`
919
+ - `listFilters.hasActiveFilters`
920
+ - `listFilters.clearChip(...)`
921
+ - `listFilters.clearFilters()`
922
+ - `listFilters.toggle(...)` for flag filters
923
+ - `listFilters.applyPreset(...)`
924
+ - `listFilters.matchesPreset(...)`
818
925
 
819
- - visible in the URL
820
- - restorable on refresh
821
- - preserved when opening a record and coming back
926
+ So the same runtime owns:
927
+
928
+ - URL-synced query params
929
+ - filter chips
930
+ - reset logic
931
+ - preset application
932
+ - preset active-state matching
933
+ - small flag toggles
934
+
935
+ 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:
936
+
937
+ ```js
938
+ const listFilters = useCrudListFilters(
939
+ RECEIVAL_LIST_FILTER_DEFINITIONS,
940
+ {
941
+ presets: [
942
+ {
943
+ key: "today",
944
+ label: "Today",
945
+ resolveValues({ presetKey }) {
946
+ const today = formatDateInputValue(new Date());
947
+ return {
948
+ arrivalDate: {
949
+ from: today,
950
+ to: today
951
+ }
952
+ };
953
+ }
954
+ },
955
+ {
956
+ key: "last7",
957
+ label: "Last 7 Days",
958
+ resolveValues({ values }) {
959
+ const today = formatDateInputValue(new Date());
960
+ return {
961
+ arrivalDate: {
962
+ from: shiftDateInputValue(today, -6),
963
+ to: today
964
+ }
965
+ };
966
+ }
967
+ }
968
+ ]
969
+ }
970
+ );
971
+ ```
972
+
973
+ ```vue
974
+ <v-chip
975
+ v-for="preset in listFilters.presets"
976
+ :key="preset.key"
977
+ :variant="listFilters.matchesPreset(preset.key) ? 'flat' : 'outlined'"
978
+ @click="listFilters.applyPreset(preset.key, { mode: 'merge' })"
979
+ >
980
+ {{ preset.label }}
981
+ </v-chip>
982
+ ```
983
+
984
+ 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.
985
+
986
+ `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
987
 
823
988
  #### Server side
824
989
 
825
- Add a dedicated query validator:
990
+ Build the server runtime from the same shared definitions:
826
991
 
827
992
  ```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;
993
+ const contactsListFiltersRuntime = createCrudListFilters(
994
+ CONTACTS_LIST_FILTER_DEFINITIONS,
995
+ {
996
+ columns: {
997
+ onlyStaff: "is_staff",
998
+ onlyVip: "vip",
999
+ onlyArchived: "archived"
1000
+ }
843
1001
  }
1002
+ );
1003
+ ```
1004
+
1005
+ 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.
1006
+
1007
+ Strict contract example:
1008
+
1009
+ ```js
1010
+ const contactsListFiltersQueryValidator = contactsListFiltersRuntime.createQueryValidator({
1011
+ invalidValues: "reject" // malformed filter values should fail validation and return 400
1012
+ });
1013
+ ```
1014
+
1015
+ Lenient contract example:
1016
+
1017
+ ```js
1018
+ const contactsListFiltersQueryValidator = contactsListFiltersRuntime.createQueryValidator({
1019
+ invalidValues: "discard" // malformed filter values should be ignored and dropped by normalize()
844
1020
  });
845
1021
  ```
846
1022
 
847
- Wire it into the route and action validators for the list operation, then apply the filter in `repository.js`:
1023
+ Wire the runtime into the list validator and the repository:
1024
+
1025
+ ```js
1026
+ queryValidator: [
1027
+ listCursorPaginationQueryValidator,
1028
+ listSearchQueryValidator,
1029
+ contactsListFiltersQueryValidator,
1030
+ listParentFilterQueryValidator,
1031
+ lookupIncludeQueryValidator
1032
+ ]
1033
+ ```
1034
+
1035
+ 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.
1036
+
1037
+ Choose the invalid-value contract deliberately:
1038
+
1039
+ - there is no default mode and no fallback alias, so every route or action that validates structured filters must call `createQueryValidator({ invalidValues: ... })` explicitly
1040
+ - use `invalidValues: "reject"` when malformed filter values should fail validation and produce a 400-style contract error
1041
+ - use `invalidValues: "discard"` when malformed filter values should be ignored and normalization should drop them
1042
+ - route query validation runs before auth, so this choice changes whether malformed unauthenticated requests fail at validation or fall through to auth
1043
+ - 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
1044
+ - `jskit doctor` flags `createQueryValidator(...)` calls that do not spell out `invalidValues` directly at the call site
848
1045
 
849
1046
  ```js
850
1047
  async function list(query = {}, callOptions = {}) {
851
1048
  return crudRepositoryList(repositoryRuntime, knex, query, options, callOptions, {
852
1049
  modifyQuery(dbQuery, context = {}) {
853
- const sourceQuery = context.query || {};
1050
+ return contactsListFiltersRuntime.applyQuery(dbQuery, context.query || {});
1051
+ }
1052
+ });
1053
+ }
1054
+ ```
854
1055
 
855
- if (sourceQuery.hasEmail !== undefined) {
856
- dbQuery.whereNotNull("email").where("email", "<>", "");
857
- }
858
- if (sourceQuery.hasPhone !== undefined) {
859
- dbQuery.whereNotNull("phone").where("phone", "<>", "");
1056
+ #### Best practices
1057
+
1058
+ - Put the filter definitions in the CRUD package, not the page. Both server and client need them.
1059
+ - Keep the filter keys identical all the way through: definition key, query param key, and repository meaning.
1060
+ - Do not expect a default `runtime.queryValidator` to exist. Every structured-filter validator must be created explicitly with `createQueryValidator({ invalidValues: ... })`.
1061
+ - 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(...)`.
1062
+ - Use `createCrudListFilters(...)` unless the list semantics are truly unusual.
1063
+ - Use `q` for free-text and explicit query params for structured filters.
1064
+ - 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.
1065
+
1066
+ ### Pattern 4: lookup-backed structured filters
1067
+
1068
+ This is the next real-world step: filters like `supplierContactId`, `locationId`, or `contactId` where the user needs:
1069
+
1070
+ - remote autocomplete search
1071
+ - URL-synced selected ids
1072
+ - readable chip labels instead of raw ids
1073
+
1074
+ The right pattern is:
1075
+
1076
+ 1. keep the lookup filter in the shared definitions
1077
+ 2. use `useCrudListFilters(...)` for state, chips, and query params
1078
+ 3. use `useCrudListFilterLookups(...)` for option loading and label resolution
1079
+
1080
+ Example shared definition:
1081
+
1082
+ ```js
1083
+ export const RECEIVAL_LIST_FILTER_DEFINITIONS = Object.freeze({
1084
+ supplierContactId: {
1085
+ type: "recordIdMany",
1086
+ label: "Supplier",
1087
+ lookup: {
1088
+ namespace: "contacts"
1089
+ }
1090
+ },
1091
+ pollenTypeId: {
1092
+ type: "recordIdMany",
1093
+ label: "Pollen Type",
1094
+ lookup: {
1095
+ namespace: "pollen-types",
1096
+ labelKey: "name"
1097
+ }
1098
+ }
1099
+ });
1100
+ ```
1101
+
1102
+ #### Exact file checklist
1103
+
1104
+ Lookup-backed filters do **not** change the ownership model from Pattern 3. The file plan is still:
1105
+
1106
+ - keep the shared definition in `packages/receivals/src/shared/receivalListFilters.js`
1107
+ - update the same server validator and repository files from Pattern 3
1108
+ - update the app-owned list page or list-runtime composable so it creates both `useCrudListFilters(...)` and `useCrudListFilterLookups(...)`
1109
+ - bind the lookup UI, such as `v-autocomplete`, from `filterLookups.resolveLookup(...)`
1110
+
1111
+ 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.
1112
+
1113
+ #### Client side
1114
+
1115
+ ```js
1116
+ let filterLookups = null;
1117
+
1118
+ const listFilters = useCrudListFilters(
1119
+ RECEIVAL_LIST_FILTER_DEFINITIONS,
1120
+ {
1121
+ labelResolvers: {
1122
+ supplierContactId(value) {
1123
+ return filterLookups?.resolveLookupLabel("supplierContactId", value, "Supplier") || "";
860
1124
  }
861
- if (sourceQuery.hasNotes !== undefined) {
862
- dbQuery.whereNotNull("notes").where("notes", "<>", "");
1125
+ }
1126
+ }
1127
+ );
1128
+
1129
+ filterLookups = useCrudListFilterLookups(
1130
+ RECEIVAL_LIST_FILTER_DEFINITIONS,
1131
+ {
1132
+ values: listFilters.values,
1133
+ queryKeyPrefix: ["ui-generator", "receivals", "filters"],
1134
+ placementSourcePrefix: "ui-generator.receivals.list.filters",
1135
+ requestQueryParams: {
1136
+ supplierContactId: { limit: 25 }
1137
+ },
1138
+ labelResolvers: {
1139
+ supplierContactId(item = {}) {
1140
+ return `${item.firstName} ${item.lastName}`.trim();
863
1141
  }
864
1142
  }
865
- });
866
- }
1143
+ }
1144
+ );
1145
+
1146
+ const supplierFilterLookup = filterLookups.resolveLookup("supplierContactId");
1147
+ ```
1148
+
1149
+ Then bind the autocomplete:
1150
+
1151
+ ```vue
1152
+ <v-autocomplete
1153
+ v-model="listFilters.values.supplierContactId"
1154
+ :items="supplierFilterLookup.options"
1155
+ :search="supplierFilterLookup.searchQuery"
1156
+ :loading="supplierFilterLookup.isLoading"
1157
+ item-title="label"
1158
+ item-value="value"
1159
+ multiple
1160
+ chips
1161
+ no-filter
1162
+ @update:search="supplierFilterLookup.setSearch"
1163
+ />
867
1164
  ```
868
1165
 
1166
+ #### Why this is better than a page-local `useList()` wrapper
1167
+
1168
+ - the CRUD filter state still lives in `useCrudListFilters(...)`
1169
+ - the autocomplete loading logic lives in one reusable helper
1170
+ - the label resolution used by filter chips and the autocomplete stays consistent
1171
+ - a second screen can reuse the same pattern instead of rewriting it
1172
+
869
1173
  #### Best practices
870
1174
 
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.
1175
+ - Put lookup metadata in the shared filter definitions.
1176
+ - Use `useCrudListFilterLookups(...)` for remote filter autocompletes instead of building a custom `useList()` wrapper per screen.
1177
+ - Keep lookup label formatting on the client side. It is UI presentation, not repository logic.
1178
+ - Keep unusual SQL semantics, such as `pending = whereNull(...)`, in the server runtime `apply` override.
874
1179
 
875
- ### Pattern 4: free-text search plus structured `contacts` filters together
1180
+ ### Pattern 5: free-text search plus structured filters together
876
1181
 
877
1182
  This is the most common real-world CRUD list.
878
1183
 
@@ -895,7 +1200,7 @@ Let the generic list search handle `q`, and let your custom validator/repository
895
1200
  - Preserve the current route query when linking to view/edit pages so users can return to the same filtered list state.
896
1201
  - Let list changes reset pagination; `useCrudList()` already does this for search and query-param changes.
897
1202
 
898
- ### Pattern 5: parent-scoped child CRUD search for `addresses`
1203
+ ### Pattern 6: parent-scoped child CRUD search for `addresses`
899
1204
 
900
1205
  For nested CRUDs such as the `addresses` resource from the previous chapter, parent scoping and search usually work together.
901
1206
 
@@ -935,7 +1240,7 @@ That keeps child-list filtering grounded in the actual resource definition inste
935
1240
  - Let the resource contract define parent filter shape.
936
1241
  - Treat parent-scoped filtering as repository/query behavior, not as presentation logic.
937
1242
 
938
- ### Pattern 6: local-only search for embedded `comments`
1243
+ ### Pattern 7: local-only search for embedded `comments`
939
1244
 
940
1245
  Sometimes server search is unnecessary.
941
1246
 
@@ -1038,10 +1343,12 @@ Use `scaffold-field` when it fits, then review the generated result.
1038
1343
 
1039
1344
  Touch:
1040
1345
 
1041
- - a client filter composable or page state
1042
- - `useCrudList({ queryParams: ... })`
1043
- - a dedicated server query validator
1044
- - `repository.js`
1346
+ - `packages/<crud>/src/shared/<crud>ListFilters.js` and make it the only authored filter-definition module
1347
+ - `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
1348
+ - `packages/<crud>/src/server/repository.js` so the list query applies the runtime
1349
+ - the app-owned list page or list-runtime composable that calls `useCrudList(...)`
1350
+
1351
+ If the filter is lookup-backed, touch that same client file again to wire `useCrudListFilterLookups(...)`.
1045
1352
 
1046
1353
  ### "I want a new save rule."
1047
1354
 
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@jskit-ai/agent-docs",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
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,36 @@
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
+ - ajax, fetch, API call, request, endpoint, HTTP client, `useList()`, `useView()`, `useAddEdit()`, `useEndpointResource()`, `usersWebHttpClient`
24
+ - `client-requests.md`
25
+ - filter, filters, search facets, chips, date range, enum filter, lookup filter, `useCrudListFilters`, `createCrudListFilters`
26
+ - `filters.md`
27
+
28
+ ## Current Patterns
29
+
30
+ - [placements.md](./placements.md)
31
+ - [surfaces.md](./surfaces.md)
32
+ - [child-cruds.md](./child-cruds.md)
33
+ - [crud-links.md](./crud-links.md)
34
+ - [live-actions.md](./live-actions.md)
35
+ - [client-requests.md](./client-requests.md)
36
+ - [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,56 @@
1
+ # Client Request Patterns
2
+
3
+ Use when:
4
+
5
+ - deciding which JSKIT client function to use for HTTP work
6
+ - custom endpoint reads or writes
7
+ - AJAX questions
8
+ - replacing raw `fetch(...)` calls
9
+ - choosing between `useCommand()`, `useList()`, `useView()`, `useAddEdit()`, and `useEndpointResource()`
10
+
11
+ Rules:
12
+
13
+ - Prefer the highest-level JSKIT runtime that matches the UI interaction.
14
+ - Do not hand-roll local AJAX helpers when an existing JSKIT runtime already fits.
15
+ - Do not use raw `fetch(...)` for normal app work.
16
+ - Use `usePaths().api(...)` for custom scoped API paths instead of concatenating route params into URLs by hand.
17
+ - Drop to `usersWebHttpClient.request(...)` only for exceptional low-level cases.
18
+
19
+ Choose the function like this:
20
+
21
+ ```js
22
+ // 1. Button/toggle/small mutation
23
+ const command = useCommand({ ... });
24
+
25
+ // 2. List endpoint
26
+ const list = useList({ ... });
27
+
28
+ // 3. Single-record endpoint
29
+ const view = useView({ ... });
30
+
31
+ // 4. Form save flow
32
+ const form = useAddEdit({ ... });
33
+
34
+ // 5. Truly custom endpoint
35
+ const resource = useEndpointResource({ ... });
36
+ ```
37
+
38
+ Use the CRUD wrappers when they fit:
39
+
40
+ - `useCrudList()` for routed CRUD lists
41
+ - `useCrudView()` for routed CRUD record loading
42
+ - `useCrudAddEdit()` for routed CRUD forms
43
+
44
+ Why this is the standard JSKIT shape:
45
+
46
+ - `useCommand()` resolves the scoped API path for the current route and surface.
47
+ - The higher-level list, view, add/edit, and command runtimes send requests through the shared HTTP runtime.
48
+ - `usersWebHttpClient` already handles credentials and CSRF behavior.
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
+
51
+ Avoid:
52
+
53
+ - raw `fetch(...)` for standard page or component work
54
+ - page-local HTTP helpers that duplicate JSKIT runtime seams
55
+ - manually concatenating scoped route params into API URLs
56
+ - using a lower-level seam when a higher-level routed CRUD or command runtime already fits
@@ -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
@@ -0,0 +1,33 @@
1
+ # Live Action Patterns
2
+
3
+ Use when:
4
+
5
+ - wiring checkboxes
6
+ - toggles
7
+ - archive/delete/reopen/publish actions
8
+ - small inline PATCH/POST/DELETE actions
9
+
10
+ Rules:
11
+
12
+ - Prefer `useCommand()` for live actions.
13
+ - Prefer form runtimes such as `useCrudAddEdit()` or `useAddEdit()` for real forms.
14
+ - Prefer `useCrudList()` and `useCrudView()` for routed CRUD loading and URL resolution.
15
+
16
+ Good live-action pattern:
17
+
18
+ - build a narrow payload
19
+ - call `command.run()`
20
+ - disable only the busy control while the command is running
21
+ - invalidate the relevant query key on success
22
+ - keep derived business rules on the server
23
+
24
+ Examples:
25
+
26
+ - checkbox toggles
27
+ - inline status changes
28
+ - quick destructive or publish/unpublish actions
29
+
30
+ Avoid:
31
+
32
+ - manually hand-rolling fetch logic for a standard live action when `useCommand()` fits
33
+ - pushing derived write rules into the client just because the action is small
@@ -0,0 +1,28 @@
1
+ # Placement Patterns
2
+
3
+ Use when:
4
+
5
+ - changing tab icons
6
+ - changing menu link icons or labels
7
+ - moving shell links
8
+ - changing profile or settings menu entries
9
+ - debugging why a visible link does not match the page component
10
+
11
+ Check first:
12
+
13
+ - app `src/placement.js`
14
+ - package placement contributions
15
+ - linked component token props
16
+
17
+ Rules:
18
+
19
+ - In JSKIT, tab-like links and shell/menu entries are often placement-owned, not page-owned.
20
+ - When asked to change a tab or menu icon, inspect placement metadata before editing page components.
21
+ - Look at placement `props`, especially fields such as `icon`, `prependIcon`, `appendIcon`, or props passed into the linked component token.
22
+ - If the visible entry is contributed by a package, update the owning package placement contribution instead of patching a local page component.
23
+ - If the request is really about where a link appears, check the placement `target`, `surfaces`, `order`, and `when` fields before changing UI markup.
24
+
25
+ Avoid:
26
+
27
+ - editing the routed page just to change a shell/tab/menu icon that is actually placement-owned
28
+ - assuming a rendered tab label/icon lives in the page component
@@ -0,0 +1,30 @@
1
+ # Surface Patterns
2
+
3
+ Use when:
4
+
5
+ - adding a new screen or feature
6
+ - generating pages
7
+ - adding menu entries or widgets
8
+ - deciding whether something belongs in `home`, `app`, `admin`, `console`, or another surface
9
+
10
+ Check first:
11
+
12
+ - the blueprint surface plan
13
+ - existing route roots under `src/pages`
14
+ - placement visibility by `surfaces`
15
+
16
+ Rules:
17
+
18
+ - Always ask which surface the feature belongs to before generating routes, placements, or related UI.
19
+ - Do not silently default new functionality to `app`.
20
+ - Surface choice is architectural, not cosmetic. It controls routes, access, placement visibility, and often data ownership expectations.
21
+ - When a link or widget should only appear on certain surfaces, use placement `surfaces` first and keep runtime `when` conditions for behavior-specific gating.
22
+
23
+ Ask explicitly about:
24
+
25
+ - route pages
26
+ - menu entries
27
+ - top-left/top-right widgets
28
+ - settings pages
29
+ - CRUD screens
30
+ - helper components that only make sense on one surface
@@ -25,6 +25,33 @@ For the full repo inventory, read `reference/autogen/README.md` and the package
25
25
  Exports
26
26
  - `isContainerToken(value)`
27
27
 
28
+ ### `support/crudListFilters.js`
29
+ Exports
30
+ - `CRUD_LIST_FILTER_TYPE_FLAG`
31
+ - `CRUD_LIST_FILTER_TYPE_ENUM`
32
+ - `CRUD_LIST_FILTER_TYPE_ENUM_MANY`
33
+ - `CRUD_LIST_FILTER_TYPE_RECORD_ID`
34
+ - `CRUD_LIST_FILTER_TYPE_RECORD_ID_MANY`
35
+ - `CRUD_LIST_FILTER_TYPE_DATE`
36
+ - `CRUD_LIST_FILTER_TYPE_DATE_RANGE`
37
+ - `CRUD_LIST_FILTER_TYPE_NUMBER_RANGE`
38
+ - `CRUD_LIST_FILTER_TYPE_PRESENCE`
39
+ - `CRUD_LIST_FILTER_TYPES`
40
+ - `CRUD_LIST_FILTER_PRESENCE_PRESENT`
41
+ - `CRUD_LIST_FILTER_PRESENCE_MISSING`
42
+ - `CRUD_LIST_FILTER_PRESENCE_OPTIONS`
43
+ - `defineCrudListFilters(definitions = {})`
44
+ - `resolveCrudListFilterQueryKeys(definition = {})`
45
+ - `resolveCrudListFilterOptionLabel(definition = {}, value = "", { fallback = "" } = {})`
46
+ Local functions
47
+ - `normalizeCrudListFilterType(value = "")`
48
+ - `normalizeCrudListFilterOption(rawOption = null, { context = "filter option" } = {})`
49
+ - `normalizeCrudListFilterOptions(rawOptions = [], { context = "filter options" } = {})`
50
+ - `normalizeCrudListFilterPresenceOptions(rawOptions = [])`
51
+ - `normalizeCrudListFilterLookup(rawLookup = null)`
52
+ - `resolveCrudListFilterOptionSet(rawDefinition = {}, type = "")`
53
+ - `normalizeCrudListFilterDefinition(rawKey = "", rawDefinition = null)`
54
+
28
55
  ### `support/crudLookup.js`
29
56
  Exports
30
57
  - `DEFAULT_CRUD_LOOKUP_CONTAINER_KEY`
@@ -55,6 +55,13 @@ Exports
55
55
  Exports
56
56
  - `AssistantClientProvider`
57
57
 
58
+ ### `src/client/support/workspaceScopeSupport.js`
59
+ Exports
60
+ - `EMPTY_WORKSPACE_WEB_SCOPE_SUPPORT`
61
+ - `WORKSPACES_WEB_SCOPE_SUPPORT_INJECTION_KEY`
62
+ - `isWorkspaceWebScopeSupport(value)`
63
+ - `useWorkspaceWebScopeSupport({ required = false } = {})`
64
+
58
65
  ### `src/server/actionIds.js`
59
66
  Exports
60
67
  - `actionIds`
@@ -79,16 +86,16 @@ Exports
79
86
  Exports
80
87
  - `registerRoutes(app)`
81
88
  Local functions
82
- - `buildRouteParamsValidator(requiresWorkspace)`
83
- - `buildConversationMessagesRouteParamsValidator(requiresWorkspace)`
84
- - `readWorkspaceInput(request, requiresWorkspace)`
89
+ - `buildRouteParamsValidator(requiresWorkspace, workspaceScopeSupport = null)`
90
+ - `buildConversationMessagesRouteParamsValidator(requiresWorkspace, workspaceScopeSupport = null)`
91
+ - `readWorkspaceInput(request, requiresWorkspace, workspaceScopeSupport = null)`
85
92
  - `requireAssistantSurface(appConfig = {}, targetSurfaceId = "")`
86
93
  - `requireHostSurfaceId(request)`
87
94
  - `shouldExposeAppErrorDetails(errorCode = "")`
88
95
  - `sendPreStreamErrorResponse(reply, error)`
89
- - `resolveRouteRequestState(request, { resolveCurrentAppConfig = () => ({}), kind = "runtime", requiresWorkspace = false } = {})`
90
- - `registerSettingsRoutes(router, resolveCurrentAppConfig, { requiresWorkspace = false } = {})`
91
- - `registerRuntimeRoutes(router, resolveCurrentAppConfig, { requiresWorkspace = false } = {})`
96
+ - `resolveRouteRequestState(request, { resolveCurrentAppConfig = () => ({}), kind = "runtime", requiresWorkspace = false, workspaceScopeSupport = null } = {})`
97
+ - `registerSettingsRoutes(router, resolveCurrentAppConfig, { requiresWorkspace = false, workspaceScopeSupport = null } = {})`
98
+ - `registerRuntimeRoutes(router, resolveCurrentAppConfig, { requiresWorkspace = false, workspaceScopeSupport = null } = {})`
92
99
 
93
100
  ### `src/server/repositories/assistantConfigRepository.js`
94
101
  Exports
@@ -128,11 +135,11 @@ Local functions
128
135
 
129
136
  ### `src/server/services/assistantConfigService.js`
130
137
  Exports
131
- - `createService({ assistantConfigRepository, consoleService = null, appConfig = {}, resolveAppConfig = null } = {})`
138
+ - `createService({ assistantConfigRepository, consoleService = null, appConfig = {}, resolveAppConfig = null, workspaceScopeSupport = null } = {})`
132
139
 
133
140
  ### `src/server/services/chatService.js`
134
141
  Exports
135
- - `createChatService({ aiClientFactory, transcriptService, serviceToolCatalog, assistantConfigService, appConfig = {}, resolveAppConfig = null } = {})`
142
+ - `createChatService({ aiClientFactory, transcriptService, serviceToolCatalog, assistantConfigService, appConfig = {}, resolveAppConfig = null, workspaceScopeSupport = null } = {})`
136
143
  Local functions
137
144
  - `normalizeConversationId(value)`
138
145
  - `normalizeHistory(history = [])`
@@ -183,6 +190,12 @@ Local functions
183
190
  - `buildCatalogOptions(appConfig = {}, surfaceId = "")`
184
191
  - `requireContextSurfaceId(context = {})`
185
192
 
193
+ ### `src/server/support/workspaceScopeSupport.js`
194
+ Exports
195
+ - `WORKSPACES_SERVER_SCOPE_SUPPORT_TOKEN`
196
+ - `isWorkspaceServerScopeSupport(value)`
197
+ - `resolveWorkspaceServerScopeSupport(scope = null, { required = false, caller = "assistant-runtime" } = {})`
198
+
186
199
  ### `src/shared/assistantRuntimeConfig.js`
187
200
  Exports
188
201
  - `assistantRuntimeConfig`
@@ -132,6 +132,28 @@ Local functions
132
132
  - `resolveRoleMatrixPolicy(matrix = {}, operation = "readable", input = {})`
133
133
  - `applyReadableFieldPolicyToRecord(record, allowedFields, outputRules = null, { context = "crudFieldAccess" } = {})`
134
134
 
135
+ ### `src/server/listFilters.js`
136
+ Exports
137
+ - `CRUD_LIST_FILTER_INVALID_VALUES_REJECT`
138
+ - `CRUD_LIST_FILTER_INVALID_VALUES_DISCARD`
139
+ - `createCrudListFilters(definitions = {}, { columns = {}, apply = {} } = {})`
140
+ Local functions
141
+ - `normalizeCrudListFilterInvalidValues(value = "")`
142
+ - `createSingleOrMultiValueSchema(itemSchema)`
143
+ - `firstValue(value)`
144
+ - `normalizeDateFilterValue(value)`
145
+ - `normalizeNumberFilterValue(value)`
146
+ - `normalizeRecordIdFilterValue(value)`
147
+ - `normalizeRecordIdFilterValues(value)`
148
+ - `resolveAllowedOptionValues(filter = {})`
149
+ - `normalizeAllowedTextValue(value, allowedValues = new Set())`
150
+ - `normalizeAllowedTextValues(value, allowedValues = new Set())`
151
+ - `addDaysToDateFilterValue(value = "", days = 0)`
152
+ - `createFilterQuerySchema(filter = {}, { invalidValues = CRUD_LIST_FILTER_INVALID_VALUES_REJECT } = {})`
153
+ - `normalizeFilterValue(filter = {}, source = {})`
154
+ - `normalizeColumnsMap(columns = {})`
155
+ - `applyDefaultFilterQuery(queryBuilder, filter = {}, value, column = "")`
156
+
135
157
  ### `src/server/listQueryValidators.js`
136
158
  Exports
137
159
  - `createCrudCursorPaginationQueryValidator(list = {})`
@@ -82,6 +82,7 @@ Local functions
82
82
  - `toLookupRelation(fieldMetaMap = {}, fieldKey = "", { lookupContainerKey = "lookups" } = {})`
83
83
  - `resolveFormInputType(fieldType, fieldFormat)`
84
84
  - `resolveFormFieldComponent(fieldType, relation = null)`
85
+ - `buildDefaultNullableBooleanOptions()`
85
86
  - `toPositiveInteger(value)`
86
87
  - `toAccessorExpression(baseName, fieldKey)`
87
88
  - `toOptionalAccessorExpression(baseName, fieldKey)`
@@ -199,6 +199,33 @@ Exports
199
199
  Exports
200
200
  - `isContainerToken(value)`
201
201
 
202
+ ### `shared/support/crudListFilters.js`
203
+ Exports
204
+ - `CRUD_LIST_FILTER_TYPE_FLAG`
205
+ - `CRUD_LIST_FILTER_TYPE_ENUM`
206
+ - `CRUD_LIST_FILTER_TYPE_ENUM_MANY`
207
+ - `CRUD_LIST_FILTER_TYPE_RECORD_ID`
208
+ - `CRUD_LIST_FILTER_TYPE_RECORD_ID_MANY`
209
+ - `CRUD_LIST_FILTER_TYPE_DATE`
210
+ - `CRUD_LIST_FILTER_TYPE_DATE_RANGE`
211
+ - `CRUD_LIST_FILTER_TYPE_NUMBER_RANGE`
212
+ - `CRUD_LIST_FILTER_TYPE_PRESENCE`
213
+ - `CRUD_LIST_FILTER_TYPES`
214
+ - `CRUD_LIST_FILTER_PRESENCE_PRESENT`
215
+ - `CRUD_LIST_FILTER_PRESENCE_MISSING`
216
+ - `CRUD_LIST_FILTER_PRESENCE_OPTIONS`
217
+ - `defineCrudListFilters(definitions = {})`
218
+ - `resolveCrudListFilterQueryKeys(definition = {})`
219
+ - `resolveCrudListFilterOptionLabel(definition = {}, value = "", { fallback = "" } = {})`
220
+ Local functions
221
+ - `normalizeCrudListFilterType(value = "")`
222
+ - `normalizeCrudListFilterOption(rawOption = null, { context = "filter option" } = {})`
223
+ - `normalizeCrudListFilterOptions(rawOptions = [], { context = "filter options" } = {})`
224
+ - `normalizeCrudListFilterPresenceOptions(rawOptions = [])`
225
+ - `normalizeCrudListFilterLookup(rawLookup = null)`
226
+ - `resolveCrudListFilterOptionSet(rawDefinition = {}, type = "")`
227
+ - `normalizeCrudListFilterDefinition(rawKey = "", rawDefinition = null)`
228
+
202
229
  ### `shared/support/crudLookup.js`
203
230
  Exports
204
231
  - `DEFAULT_CRUD_LOOKUP_CONTAINER_KEY`
@@ -104,6 +104,7 @@ Local functions
104
104
  - `resolveStableFieldErrorList(fieldKey, message)`
105
105
  - `resolveFormFieldType(field = {})`
106
106
  - `resolveFormFieldFormat(field = {})`
107
+ - `isNullableFormField(field = {})`
107
108
  - `padDateTimePart(value)`
108
109
  - `normalizeTimeWhitespace(value)`
109
110
  - `toTimeInputValue(value)`
@@ -112,6 +113,18 @@ Local functions
112
113
  - `resolveFormFieldInitialValue(field = {})`
113
114
  - `shouldSerializeClearedFieldAsNull(field = {})`
114
115
 
116
+ ### `src/client/composables/internal/crudListFilterLookupSupport.js`
117
+ Exports
118
+ - `normalizeLookupQueryKeyPrefix(value = [])`
119
+ - `normalizeLookupLabelResolverMap(value = {})`
120
+ - `normalizeLookupRequestQueryParamsMap(value = {})`
121
+ - `resolveLookupSelectedValues(filter = {}, rawValue = undefined)`
122
+ - `createLookupOptionsFromItems(items = [], filter = {}, labelResolver = null)`
123
+ - `mergeSelectedLookupOptions(options = [], selectedValues = [], cachedOptions = new Map())`
124
+ - `resolveLookupOptionLabel(options = [], cachedOptions = new Map(), value = "", fallback = "Option")`
125
+ Local functions
126
+ - `createLookupOptionFromItem(item = {}, filter = {}, labelResolver = null)`
127
+
115
128
  ### `src/client/composables/internal/crudListParentTitleSupport.js`
116
129
  Exports
117
130
  - `resolveCrudListParentDescriptor({ resource = {}, route = null, recordIdParam = "recordId" } = {})`
@@ -339,6 +352,28 @@ Exports
339
352
  Exports
340
353
  - `useCommand({ ownershipFilter = ROUTE_VISIBILITY_WORKSPACE, surfaceId = "", access = "auto", apiSuffix = "", runPermissions = [], writeMethod = "POST", placementSource = "users-web.command", fallbackRunError = "Unable to complete action.", fieldErrorKeys = [], clearOnRouteChange = true, model, parseInput, buildRawPayload, buildCommandPayload, buildCommandOptions, onRunSuccess, onRunError, suppressSuccessMessage = false, messages = {}, realtime = null } = {})`
341
354
 
355
+ ### `src/client/composables/useCrudListFilterLookups.js`
356
+ Exports
357
+ - `useCrudListFilterLookups(definitions = {}, { values = {}, adapter = null, recordIdParam = "recordId", queryKeyPrefix = [], placementSourcePrefix = "", requestQueryParams = {}, labelResolvers = {} } = {})`
358
+
359
+ ### `src/client/composables/useCrudListFilters.js`
360
+ Exports
361
+ - `useCrudListFilters(definitions = {}, { labelResolvers = {}, chipLabels = {}, presets = [] } = {})`
362
+ Local functions
363
+ - `normalizeFunctionMap(value = {})`
364
+ - `createInitialFilterValue(filter = {})`
365
+ - `normalizePresetEntries(presets = [])`
366
+ - `resolvePresetValues(preset = {}, { values = {}, filters = {} } = {})`
367
+ - `normalizePresetFilterValue(filter = {}, rawValue)`
368
+ - `normalizeCurrentManyFilterValues(value)`
369
+ - `matchArrayValues(currentValue = [], expectedValue = [])`
370
+ - `matchesPresetFilterValue(filter = {}, currentValue, rawExpectedValue)`
371
+ - `resetFilterValue(values, filter = {})`
372
+ - `applyPresetFilterValue(values, filter = {}, rawValue)`
373
+ - `createQueryParams(values, filterEntries = [])`
374
+ - `resolveAtomicValueLabel(filter = {}, value = "", labelResolvers = {})`
375
+ - `defaultChipLabel(filter = {}, value, labelResolvers = {})`
376
+
342
377
  ### `src/client/composables/useCrudListParentTitle.js`
343
378
  Exports
344
379
  - `useCrudListParentTitle({ listRuntime = null, resource = {}, adapter = null, recordIdParam = "recordId", queryKeyPrefix = ["users-web", "crud-list-parent-title"], placementSource = "users-web.crud-list-parent-title", fallbackLoadError = "Unable to load parent record.", notFoundMessage = "Parent record not found.", route = null, viewRuntimeFactory = useView } = {})`
@@ -154,6 +154,10 @@ Exports
154
154
  - `readWorkspaceSlugFromRouteParams(params = {})`
155
155
  - `buildWorkspaceInputFromRouteParams(params = {})`
156
156
 
157
+ ### `src/server/support/workspaceServerScopeSupport.js`
158
+ Exports
159
+ - `createWorkspaceServerScopeSupport()`
160
+
157
161
  ### `src/server/workspaceBootstrapContributor.js`
158
162
  Exports
159
163
  - `createWorkspaceBootstrapContributor({ workspaceService, workspacePendingInvitationsService, usersRepository, workspaceInvitationsEnabled = false, appConfig = {}, tenancyProfile = null } = {})`
@@ -313,6 +313,14 @@ Exports
313
313
  Exports
314
314
  - `buildWorkspaceQueryKey(kind = "", surfaceId = "", workspaceSlug = "")`
315
315
 
316
+ ### `src/client/support/workspaceScopeSupport.js`
317
+ Exports
318
+ - `WORKSPACES_WEB_SCOPE_SUPPORT_INJECTION_KEY`
319
+ - `createWorkspaceScopeSupport()`
320
+ - `readWorkspaceRouteScope(routeContext = {})`
321
+ Local functions
322
+ - `unwrapRefValue(value)`
323
+
316
324
  ### `src/shared/toolsOutletContracts.js`
317
325
  Exports
318
326
  - `DEFAULT_TOOLS_LINK_COMPONENT_TOKEN`
@@ -15,12 +15,43 @@ Use these references on demand:
15
15
 
16
16
  - `node_modules/@jskit-ai/agent-docs/reference/autogen/KERNEL_MAP.md`
17
17
  - `node_modules/@jskit-ai/agent-docs/reference/autogen/README.md`
18
+ - `node_modules/@jskit-ai/agent-docs/patterns/INDEX.md`
18
19
  - `node_modules/@jskit-ai/agent-docs/guide/agent/index.md`
19
20
  - `node_modules/@jskit-ai/agent-docs/site/guide/index.md` when compressed guidance is ambiguous or missing nuance
20
21
  - `node_modules/@jskit-ai/agent-docs/templates/APP_BLUEPRINT.md`
21
22
  - `node_modules/@jskit-ai/agent-docs/templates/WORKBOARD.md`
22
23
  - `node_modules/@jskit-ai/agent-docs/skills/jskit-review/SKILL.md` for review passes when your Codex environment supports packaged skills
23
24
 
25
+ ## Mandatory Start Gate
26
+
27
+ Before any non-trivial change:
28
+
29
+ 1. Read this file and the workflow files above in the current turn. Do not rely on memory alone.
30
+ 2. If the task involves JSKIT UI, routing, surfaces, CRUDs, filters, placements, live actions, or similar implementation details, scan `node_modules/@jskit-ai/agent-docs/patterns/INDEX.md` and read the relevant pattern files before editing.
31
+ 3. For substantial or multi-chunk work, create or update `.jskit/WORKBOARD.md` before editing.
32
+ 4. Before editing, print a compact read receipt with:
33
+ - `Read receipt: ...`
34
+ - `Active chunk: ...`
35
+ - `Generator decision: ...`
36
+ - `Relevant patterns: ...`
37
+ - `Active rules from docs: ...`
38
+ 5. When files need to be created, prefer `jskit generate ...` over creating them from scratch, even if the generated output will need follow-up adaptation. If not using a generator, say why.
39
+ 6. Do not edit code until the read receipt is printed and the workboard step is complete when it applies.
40
+
41
+ ## Mandatory Done Gate
42
+
43
+ Before calling a chunk done, report:
44
+
45
+ - `Deslop review: ...`
46
+ - `JSKIT review: ...`
47
+ - `Material/Vuetify review: ...`
48
+ - `Verification: ...`
49
+ - `Files changed: ...`
50
+ - `Commands run: ...`
51
+ - `Remaining unverified: ...`
52
+
53
+ If a feature spans more than one chunk, repeat those passes on the whole changeset after the final chunk.
54
+
24
55
  Core rules:
25
56
 
26
57
  - Inspect the workspace before assuming a JSKIT app exists.
@@ -41,7 +72,6 @@ Core rules:
41
72
  - For CRUD chunks, ask the developer which operations are allowed, which fields belong in the list view if one exists, and the intended look of the view and edit/new forms before generating code.
42
73
  - Every user-facing screen must pass a Material Design and Vuetify quality review before the chunk is considered done.
43
74
  - Do not implement app features before the blueprint has the database, surfaces, ownership model, and route/screen plan written down.
44
- - For substantial or multi-chunk work, create or update `.jskit/WORKBOARD.md` as the per-request execution tracker.
45
75
  - Break planned work into reviewable chunks before implementing. One CRUD is usually one chunk, but auth/platform setup, shell work, and cross-cutting integrations can be their own chunks.
46
76
  - Do not move to the next chunk until the current chunk has passed implementation, deslop review, JSKIT best-practices review, Material Design/Vuetify review, and verification.
47
77
  - If a feature spans more than one chunk, run a final whole-changeset deslop pass, JSKIT pass, Material Design/Vuetify pass, and verification pass after the last chunk.