@jskit-ai/agent-docs 0.1.5 → 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.
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.5",
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
@@ -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,6 +15,7 @@ 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`
@@ -30,6 +31,7 @@ Core rules:
30
31
  - `Why this sticks: ...`
31
32
  - `Not doing: ...`
32
33
  - Keep that checkpoint compact. Do not expand it into a long preamble unless the developer asks for detail.
34
+ - When a request involves JSKIT UI, routing, surfaces, CRUDs, filters, placements, live actions, or similar implementation details, scan `node_modules/@jskit-ai/agent-docs/patterns/INDEX.md` for matching keywords and read only the relevant pattern files.
33
35
  - Reuse existing JSKIT helpers and runtime seams before adding new local helpers.
34
36
  - A freshly scaffolded JSKIT app can still be in Stage 1. If the app was just created and platform decisions are not settled yet, continue with the initialize workflow before adding runtime packages.
35
37
  - Do not treat a missing `config.tenancyMode` line or an untouched minimal scaffold as a final tenancy decision.