@jskit-ai/agent-docs 0.1.7 → 0.1.8

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.
@@ -914,6 +914,129 @@ That explains why the login screen and auth guard runtime both care about `/api/
914
914
 
915
915
  It is also why the shell widget can react cleanly to auth state without storing raw session tokens in client state. The browser just asks the app for the current session view, and the app derives that from its cookies plus Supabase.
916
916
 
917
+ ### Authenticated Playwright testing with the dev auth bypass
918
+
919
+ JSKIT now ships a development-only auth bootstrap path specifically so authenticated UI can be verified in Playwright without depending on a real live login flow through Supabase.
920
+
921
+ This is the standard path the agent should use for authenticated browser tests:
922
+
923
+ - enable the dev auth bypass in development
924
+ - create a session for an existing user through the local app
925
+ - let the browser keep the resulting HTTP-only cookies
926
+ - navigate to the protected page and verify the feature
927
+ - record the Playwright run through `jskit app verify-ui` so `jskit doctor` can verify the receipt later
928
+
929
+ The feature is intentionally narrow.
930
+
931
+ - It is development-only.
932
+ - It must never be enabled in production.
933
+ - JSKIT rejects boot if `AUTH_DEV_BYPASS_ENABLED=true` while `NODE_ENV=production`.
934
+ - The route only looks up an existing user. It does not create one.
935
+
936
+ The environment variables are:
937
+
938
+ ```bash
939
+ AUTH_DEV_BYPASS_ENABLED=true
940
+ AUTH_DEV_BYPASS_SECRET=replace-this-with-a-local-dev-secret
941
+ ```
942
+
943
+ When enabled outside production, the app exposes:
944
+
945
+ ```text
946
+ POST /api/dev-auth/login-as
947
+ ```
948
+
949
+ The request body must contain either:
950
+
951
+ ```json
952
+ { "userId": "7" }
953
+ ```
954
+
955
+ or:
956
+
957
+ ```json
958
+ { "email": "ada@example.com" }
959
+ ```
960
+
961
+ The response is intentionally small:
962
+
963
+ ```json
964
+ {
965
+ "ok": true,
966
+ "userId": "7",
967
+ "username": "Ada Example",
968
+ "email": "ada@example.com"
969
+ }
970
+ ```
971
+
972
+ Behind the scenes, JSKIT creates the same HTTP-only auth cookies that the normal login flow would create. That means Playwright should not try to read raw tokens. It should bootstrap the session in the browser context, then navigate normally.
973
+
974
+ One subtle point matters here:
975
+
976
+ - `/api/dev-auth/login-as` is still an unsafe `POST`
977
+ - JSKIT still expects a CSRF token
978
+ - the browser can get that token from `/api/session`
979
+
980
+ So the normal Playwright shape is:
981
+
982
+ 1. open a same-origin page first
983
+ 2. call `/api/session` to read `csrfToken`
984
+ 3. call `/api/dev-auth/login-as` with `credentials: "include"` and the `csrf-token` header
985
+ 4. navigate to the protected route and run the assertions
986
+
987
+ For example:
988
+
989
+ ```ts
990
+ await page.goto("/");
991
+
992
+ await page.evaluate(async ({ email }) => {
993
+ const sessionResponse = await fetch("/api/session", {
994
+ credentials: "include"
995
+ });
996
+ if (!sessionResponse.ok) {
997
+ throw new Error(`Session bootstrap failed: ${sessionResponse.status}`);
998
+ }
999
+
1000
+ const sessionPayload = await sessionResponse.json();
1001
+ const csrfToken = String(sessionPayload?.csrfToken || "");
1002
+ if (!csrfToken) {
1003
+ throw new Error("Missing csrfToken from /api/session.");
1004
+ }
1005
+
1006
+ const loginResponse = await fetch("/api/dev-auth/login-as", {
1007
+ method: "POST",
1008
+ credentials: "include",
1009
+ headers: {
1010
+ "content-type": "application/json",
1011
+ "csrf-token": csrfToken
1012
+ },
1013
+ body: JSON.stringify({ email })
1014
+ });
1015
+
1016
+ if (!loginResponse.ok) {
1017
+ throw new Error(`Dev login failed: ${loginResponse.status} ${await loginResponse.text()}`);
1018
+ }
1019
+ }, { email: "ada@example.com" });
1020
+
1021
+ await page.goto("/w/acme/admin/contacts");
1022
+ ```
1023
+
1024
+ In practice, the preferred wrapper is:
1025
+
1026
+ ```bash
1027
+ npx jskit app verify-ui \
1028
+ --command "npx playwright test tests/e2e/contacts.spec.ts -g filters" \
1029
+ --feature "contacts filters" \
1030
+ --auth-mode dev-auth-login-as
1031
+ ```
1032
+
1033
+ That flow is preferable to driving the real sign-in form in feature tests because it keeps the test focused on the UI feature being added, not on an external auth dependency. If a chunk changes user-facing UI and the flow requires login, the expected JSKIT review standard is:
1034
+
1035
+ - use Playwright
1036
+ - record the run with `jskit app verify-ui`
1037
+ - use the local dev auth bypass or another local session bootstrap path
1038
+ - exercise the actual changed behavior, not only page load
1039
+
917
1040
  ## What appears in Supabase
918
1041
 
919
1042
  It is important to separate **Supabase auth data** from **JSKIT app-owned data**.
@@ -666,6 +666,7 @@ In the current CLI, that includes checks such as:
666
666
  - whether managed files recorded in the lock still exist
667
667
  - whether installed packages are still visible in the package registry
668
668
  - certain JSKIT-specific app checks, such as invalid raw `mdi-*` icon literals in Vue templates when the app uses Vuetify's `mdi-svg` iconset
669
+ - UI verification receipts for current dirty UI files in git, via `.jskit/verification/ui.json`
669
670
 
670
671
  That last check is narrower than it sounds. `doctor` is looking for the broken case in direct Vue templates, such as:
671
672
 
@@ -726,6 +727,24 @@ Good times to run it manually include:
726
727
 
727
728
  One important nuance: `doctor` is checking for broken JSKIT ownership and visibility, not trying to stop you from editing app-owned files. For example, a managed file that still exists but whose contents changed is normally fine. The problem is when JSKIT expects a managed file to exist and it is gone, or when the installed package state no longer resolves cleanly.
728
729
 
730
+ There is one intentional exception now for user-facing UI work.
731
+
732
+ If the current git working tree has changed UI files under the app's normal UI paths, `doctor` expects a matching `.jskit/verification/ui.json` receipt. The intended way to create that receipt is:
733
+
734
+ ```bash
735
+ npx jskit app verify-ui \
736
+ --command "npx playwright test tests/e2e/contacts.spec.ts -g filters" \
737
+ --feature "contacts filters" \
738
+ --auth-mode dev-auth-login-as
739
+ ```
740
+
741
+ That command does two things:
742
+
743
+ - runs the targeted Playwright command you give it
744
+ - writes a receipt describing the verified feature, auth mode, and current dirty UI file set
745
+
746
+ `doctor` then compares the current changed UI files to that receipt. If you edit the UI again afterwards, the receipt is stale and `doctor` tells you to rerun `jskit app verify-ui`.
747
+
729
748
  ### Why `--json` exists
730
749
 
731
750
  If you want machine-readable output, use:
@@ -834,6 +834,75 @@ Usually nothing changes. The client can keep sending `q`.
834
834
  - Do not dump every text column into search just because you can.
835
835
  - In this tutorial CRUD, `notes` is a good example of a field you might leave out if you want fast, predictable list search.
836
836
 
837
+ ### Pattern 2B: repository mapping and computed output fields
838
+
839
+ Do not treat the output schema as if it also defined database storage.
840
+
841
+ In JSKIT CRUD:
842
+
843
+ - the schema defines the API contract
844
+ - `resource.fieldMeta` defines storage mapping
845
+ - the repository runtime owns computed SQL projections
846
+
847
+ Use these rules:
848
+
849
+ - for explicit DB column overrides, use `repository.column`
850
+ - for computed output fields, use `repository.storage: "virtual"`
851
+ - do not put computed fields in create/patch write schemas
852
+
853
+ Example field metadata:
854
+
855
+ ```js
856
+ RESOURCE_FIELD_META.push({
857
+ key: "createdAt",
858
+ repository: {
859
+ column: "created_at"
860
+ }
861
+ });
862
+
863
+ RESOURCE_FIELD_META.push({
864
+ key: "remainingBatchWeight",
865
+ repository: {
866
+ storage: "virtual"
867
+ }
868
+ });
869
+ ```
870
+
871
+ Register the computed projection once in the repository runtime:
872
+
873
+ ```js
874
+ const repositoryRuntime = createCrudResourceRuntime(resource, {
875
+ context: "receivals repository",
876
+ list: LIST_CONFIG,
877
+ virtualFields: {
878
+ remainingBatchWeight: {
879
+ applyProjection(dbQuery, { knex, tableName, alias }) {
880
+ const { sql, bindings } = getRemainingBatchWeightSqlParts({ tableName });
881
+ dbQuery.select(knex.raw(`${sql} as ??`, [...bindings, alias]));
882
+ }
883
+ }
884
+ }
885
+ });
886
+ ```
887
+
888
+ Once registered there:
889
+
890
+ - generic CRUD `list`
891
+ - generic CRUD `findById`
892
+ - generic CRUD `listByIds`
893
+ - generic CRUD `listByForeignIds`
894
+
895
+ all pick up the projection automatically, so you should not hand-patch `clearSelect()` / re-select logic into each method.
896
+
897
+ Important limits:
898
+
899
+ - `virtual` fields are output-only in v1
900
+ - fallback search derivation only uses column-backed fields
901
+ - parent-filter fallback derivation only uses column-backed fields
902
+ - `listByIds(..., { valueKey })` requires a column-backed `valueKey`
903
+
904
+ For the agent-facing quick rule, see `patterns/crud-repository-mapping.md`.
905
+
837
906
  ### Pattern 3: structured list filters from one shared definition
838
907
 
839
908
  This is the default pattern once a CRUD list needs real filters.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/agent-docs",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Distributed JSKIT agent workflows, guides, and generated reference maps.",
5
5
  "type": "module",
6
6
  "files": [
package/patterns/INDEX.md CHANGED
@@ -22,8 +22,12 @@ How to use it:
22
22
  - `live-actions.md`
23
23
  - ajax, fetch, API call, request, endpoint, HTTP client, `useList()`, `useView()`, `useAddEdit()`, `useEndpointResource()`, `usersWebHttpClient`
24
24
  - `client-requests.md`
25
+ - playwright, browser test, e2e, ui verification, authenticated ui test, test auth, dev login as, dev auth bypass
26
+ - `ui-testing.md`
25
27
  - filter, filters, search facets, chips, date range, enum filter, lookup filter, `useCrudListFilters`, `createCrudListFilters`
26
28
  - `filters.md`
29
+ - fieldMeta, `repository.column`, computed field, virtual field, projection, `createCrudResourceRuntime`, `remainingBatchWeight`
30
+ - `crud-repository-mapping.md`
27
31
 
28
32
  ## Current Patterns
29
33
 
@@ -33,4 +37,6 @@ How to use it:
33
37
  - [crud-links.md](./crud-links.md)
34
38
  - [live-actions.md](./live-actions.md)
35
39
  - [client-requests.md](./client-requests.md)
40
+ - [ui-testing.md](./ui-testing.md)
36
41
  - [filters.md](./filters.md)
42
+ - [crud-repository-mapping.md](./crud-repository-mapping.md)
@@ -0,0 +1,81 @@
1
+ # CRUD Repository Mapping
2
+
3
+ Use when:
4
+ - a CRUD resource needs explicit DB column mapping
5
+ - a field should exist in API output but is not a real DB column
6
+ - the user mentions computed fields, projections, or `remainingBatchWeight`
7
+
8
+ Default JSKIT pattern:
9
+ 1. Treat the schema as the API contract only.
10
+ 2. Put persistence mapping in `resource.fieldMeta`.
11
+ 3. Use `repository.column` for explicit DB column overrides.
12
+ 4. Use `repository.storage: "virtual"` for computed output fields.
13
+ 5. Register computed SQL projections once in the repository runtime with `virtualFields`.
14
+ 6. Let generic CRUD read paths apply those projections automatically.
15
+
16
+ Field meta rules:
17
+ - for a normal override, write:
18
+
19
+ ```js
20
+ RESOURCE_FIELD_META.push({
21
+ key: "createdAt",
22
+ repository: {
23
+ column: "created_at"
24
+ }
25
+ });
26
+ ```
27
+
28
+ - for a computed output field, write:
29
+
30
+ ```js
31
+ RESOURCE_FIELD_META.push({
32
+ key: "remainingBatchWeight",
33
+ repository: {
34
+ storage: "virtual"
35
+ }
36
+ });
37
+ ```
38
+
39
+ - omit `repository` entirely when the field is column-backed and the default snake_case mapping is correct
40
+ - `repository.storage: "virtual"` cannot also define `repository.column`
41
+ - `repository.storage: "virtual"` fields must not appear in create/patch write schemas
42
+
43
+ Repository pattern:
44
+ - build the runtime with `createCrudResourceRuntime(resource, { ... })`
45
+ - register computed projections in `virtualFields`
46
+ - keep SQL in the repository, not in shared metadata
47
+
48
+ Example:
49
+
50
+ ```js
51
+ const repositoryRuntime = createCrudResourceRuntime(resource, {
52
+ context: "receivals repository",
53
+ list: LIST_CONFIG,
54
+ virtualFields: {
55
+ remainingBatchWeight: {
56
+ applyProjection(dbQuery, { knex, tableName, alias }) {
57
+ const { sql, bindings } = getRemainingBatchWeightSqlParts({ tableName });
58
+ dbQuery.select(knex.raw(`${sql} as ??`, [...bindings, alias]));
59
+ }
60
+ }
61
+ }
62
+ });
63
+ ```
64
+
65
+ What CRUD core now does for you:
66
+ - default select columns include only column-backed output fields
67
+ - `list`, `findById`, `listByIds`, and `listByForeignIds` apply registered virtual projections automatically
68
+ - search and parent-filter fallback derivation only use column-backed fields
69
+ - `listByIds(..., { valueKey })` requires that `valueKey` be column-backed
70
+
71
+ Avoid:
72
+ - manual `clearSelect()` / re-select hacks in individual repository methods just to add computed fields
73
+ - putting SQL fragments or joins into shared `fieldMeta`
74
+ - inventing `repository.storage` modes beyond the documented `virtual` contract
75
+
76
+ Review checks:
77
+ - schema defines contract, not storage
78
+ - `fieldMeta.repository` owns mapping
79
+ - computed fields use `repository.storage: "virtual"`
80
+ - repository runtime registers matching `virtualFields`
81
+ - no per-method projection duplication when generic CRUD reads already cover the field
@@ -0,0 +1,79 @@
1
+ # UI Testing Pattern
2
+
3
+ Use when:
4
+
5
+ - deciding how to verify a new or changed user-facing screen
6
+ - Playwright or browser-driven verification
7
+ - authenticated UI flows
8
+ - test auth, session bootstrap, dev login-as, or dev auth bypass
9
+
10
+ Rules:
11
+
12
+ - Any chunk that adds or changes user-facing UI must include a Playwright flow that exercises the changed behavior before the chunk is done.
13
+ - Record that Playwright run with `jskit app verify-ui --command "<playwright command>" --feature "<label>" --auth-mode <mode>`.
14
+ - `jskit doctor` expects `.jskit/verification/ui.json` to match the current dirty UI file set when UI files are changed.
15
+ - Do not rely on a live external auth provider for Playwright verification of normal app features.
16
+ - For authenticated UI in the standard JSKIT auth stack, use the development-only dev auth bypass route instead.
17
+ - The standard route is `POST /api/dev-auth/login-as`.
18
+ - The request body must include either `{ userId }` or `{ email }`.
19
+ - The route is available only when `AUTH_DEV_BYPASS_ENABLED=true` and `AUTH_DEV_BYPASS_SECRET` is set outside production.
20
+ - This route must never be enabled in production.
21
+ - Because the route is an unsafe POST, fetch `csrfToken` from `/api/session` first and send it back as the `csrf-token` header.
22
+ - Make the bootstrap request from the same browser context that will run the assertions so the auth cookies land in the page session.
23
+ - Use stable seeded users or fixtures for Playwright. Do not depend on whatever account happens to exist in a developer's browser or external auth provider.
24
+
25
+ Recommended flow:
26
+
27
+ ```bash
28
+ npx jskit app verify-ui \
29
+ --command "npx playwright test tests/e2e/contacts.spec.ts -g filters" \
30
+ --feature "contacts filters" \
31
+ --auth-mode dev-auth-login-as
32
+ ```
33
+
34
+ The Playwright command itself should follow this setup shape when login is needed:
35
+
36
+ ```ts
37
+ await page.goto("/");
38
+
39
+ await page.evaluate(async ({ email }) => {
40
+ const sessionResponse = await fetch("/api/session", {
41
+ credentials: "include"
42
+ });
43
+ if (!sessionResponse.ok) {
44
+ throw new Error(`Session bootstrap failed: ${sessionResponse.status}`);
45
+ }
46
+
47
+ const sessionPayload = await sessionResponse.json();
48
+ const csrfToken = String(sessionPayload?.csrfToken || "");
49
+ if (!csrfToken) {
50
+ throw new Error("Missing csrfToken from /api/session.");
51
+ }
52
+
53
+ const loginResponse = await fetch("/api/dev-auth/login-as", {
54
+ method: "POST",
55
+ credentials: "include",
56
+ headers: {
57
+ "content-type": "application/json",
58
+ "csrf-token": csrfToken
59
+ },
60
+ body: JSON.stringify({ email })
61
+ });
62
+
63
+ if (!loginResponse.ok) {
64
+ throw new Error(`Dev login failed: ${loginResponse.status} ${await loginResponse.text()}`);
65
+ }
66
+ }, { email: "ada@example.com" });
67
+ ```
68
+
69
+ After that bootstrap:
70
+
71
+ - navigate to the protected page
72
+ - exercise the new UI behavior
73
+ - assert the actual outcome the chunk introduced
74
+
75
+ Do not mark the chunk done if:
76
+
77
+ - the feature changed user-facing UI but no Playwright flow ran
78
+ - the Playwright flow skipped the changed behavior itself
79
+ - authenticated UI work was left untested because no local auth bootstrap path was available and that gap was not called out
@@ -202,12 +202,19 @@ Exports
202
202
  - `loginResponseValidator`
203
203
  - `otpVerifyResponseValidator`
204
204
  - `oauthCompleteResponseValidator`
205
+ - `devLoginAsResponseValidator`
205
206
  - `logoutResponseValidator`
206
207
  - `oauthProviderCatalogEntryValidator`
207
208
  - `sessionResponseValidator`
208
209
  - `sessionUnavailableResponseValidator`
209
210
  - `createCommandMessages({ fields = {}, defaultMessage = "Invalid value." } = {})`
210
211
 
212
+ ### `src/shared/commands/authDevLoginAsCommand.js`
213
+ Exports
214
+ - `AUTH_DEV_LOGIN_AS_MESSAGES`
215
+ - `authDevLoginAsBodyValidator`
216
+ - `authDevLoginAsCommand`
217
+
211
218
  ### `src/shared/commands/authLoginOAuthCompleteCommand.js`
212
219
  Exports
213
220
  - `authLoginOAuthCompleteBodyValidator`
@@ -300,6 +307,7 @@ Exports
300
307
  - `authLoginOtpVerifyCommand`
301
308
  - `authLoginOAuthStartCommand`
302
309
  - `authLoginOAuthCompleteCommand`
310
+ - `authDevLoginAsCommand`
303
311
  - `authPasswordResetRequestCommand`
304
312
  - `authPasswordRecoveryCompleteCommand`
305
313
  - `authPasswordResetCommand`
@@ -26,7 +26,9 @@ Local functions
26
26
 
27
27
  ### `src/server/lib/actions/auth.contributor.js`
28
28
  Exports
29
- - `authActions`
29
+ - `baseAuthActions`
30
+ - `buildAuthActions({ includeDevLoginAs = false } = {})`
31
+ - `devLoginAsAction`
30
32
  Local functions
31
33
  - `requireRequestContext(context, actionId)`
32
34
 
@@ -106,6 +108,30 @@ Exports
106
108
  Exports
107
109
  - `createAuthSessionEventsService()`
108
110
 
111
+ ### `src/server/lib/devAuthBootstrap.js`
112
+ Exports
113
+ - `assertDevAuthBootstrapConfig(config, { usersRepository = null } = {})`
114
+ - `authenticateDevAuthRequest({ request, accessToken = "", refreshToken = "" }, { config, usersRepository = null } = {})`
115
+ - `createDevAuthSession(profile, config)`
116
+ - `ensureDevAuthBootstrapAvailable(config, request)`
117
+ - `isDevAuthToken(token)`
118
+ - `resolveDevAuthConfig({ enabled = false, secret = "", nodeEnv = "development", jwtAudience = "authenticated", accessTtlSeconds = DEFAULT_DEV_AUTH_ACCESS_TTL_SECONDS, refreshTtlSeconds = DEFAULT_DEV_AUTH_REFRESH_TTL_SECONDS } = {})`
119
+ - `resolveDevAuthProfile(input = {}, { usersRepository = null, validationError } = {})`
120
+ Local functions
121
+ - `parseBoolean(value, fallback = false)`
122
+ - `normalizePositiveInteger(value, fallback)`
123
+ - `normalizeRequestHostname(request)`
124
+ - `resolveDirectRemoteAddress(request)`
125
+ - `normalizeLoopbackIp(value)`
126
+ - `isLoopbackIp(value)`
127
+ - `isLoopbackHostname(value)`
128
+ - `isLocalDevAuthRequest(request)`
129
+ - `stripDevAuthTokenPrefix(token)`
130
+ - `buildProfileFromTokenClaims(payload)`
131
+ - `resolveProfileFromTokenClaims(payload, { usersRepository = null } = {})`
132
+ - `signDevAuthToken(kind, profile, config)`
133
+ - `verifyDevAuthToken(token, kind, config)`
134
+
109
135
  ### `src/server/lib/flowGuards.js`
110
136
  Exports
111
137
  - `requireNoFieldErrors(parsed, validationError)`
@@ -117,7 +143,9 @@ Exports
117
143
  Exports
118
144
  - `createService`
119
145
  - `__testables`
120
- - `authActions`
146
+ - `baseAuthActions`
147
+ - `buildAuthActions`
148
+ - `devLoginAsAction`
121
149
 
122
150
  ### `src/server/lib/oauthFlows.js`
123
151
  Exports
@@ -206,14 +234,18 @@ Exports
206
234
  - `AuthSupabaseServiceProvider`
207
235
  Local functions
208
236
  - `splitCsv(value)`
237
+ - `parseBoolean(value, fallback = false)`
209
238
  - `normalizeRecord(value)`
210
239
  - `normalizeOAuthProviderConfigList(value)`
211
240
  - `resolveOAuthConfigFromAppConfig(appConfig)`
212
241
  - `resolveAllowedReturnToOrigins({ appConfig = {}, appPublicUrl = "" } = {})`
213
242
  - `resolveAuthProviderConfig(env, appConfig = {})`
214
243
  - `resolveAuthProfileMode(env)`
244
+ - `isDevAuthBypassEnabledForRegistration(env)`
245
+ - `isDevAuthBypassRequested(env)`
215
246
  - `createInMemoryUserSettingsRepository()`
216
247
  - `resolveCommonDependencies(scope)`
248
+ - `resolveRuntimeEnv(scope)`
217
249
  - `resolveOptionalRepositories(scope)`
218
250
 
219
251
  ### root
@@ -217,7 +217,7 @@ Exports
217
217
 
218
218
  ### `src/server/routes/authRoutes.js`
219
219
  Exports
220
- - `buildRoutes(controller)`
220
+ - `buildRoutes(controller, { includeDevLoginAs = false } = {})`
221
221
 
222
222
  ### `src/server/services/AuthWebService.js`
223
223
  Exports
@@ -70,7 +70,7 @@ Exports
70
70
 
71
71
  ### `src/server/createCrudRepositoryFromResource.js`
72
72
  Exports
73
- - `createCrudRepositoryFromResource(resource = {}, { context = "crudRepository", list = {} } = {})`
73
+ - `createCrudRepositoryFromResource(resource = {}, { context = "crudRepository", list = {}, virtualFields = {} } = {})`
74
74
 
75
75
  ### `src/server/createCrudServiceFromResource.js`
76
76
  Exports
@@ -177,13 +177,13 @@ Local functions
177
177
  - `normalizeIncludePaths(include, { defaultInclude = DEFAULT_LOOKUP_INCLUDE } = {})`
178
178
  - `resolveChildIncludeFromPaths(paths = [])`
179
179
  - `buildLookupHydrationPlan(runtime = {}, include, { mode = "list", context = "crudRepository", includeWasExplicit = false, skippedNamespaces = new Set() } = {})`
180
- - `resolveLookupProviderResolver(repositoryOptions = {}, callOptions = {}, { context = "crudRepository" } = {})`
180
+ - `resolveLookupResolver(repositoryOptions = {}, callOptions = {}, { context = "crudRepository" } = {})`
181
181
  - `resolveLookupDepthRuntime(runtime = {}, repositoryOptions = {}, callOptions = {})`
182
182
  - `buildLookupGroupKey(relation = {})`
183
183
  - `normalizeLookupRelationValues(records = [], entries = [], childIncludeByKey = {})`
184
184
  - `resolveGroupChildInclude(group = {})`
185
- - `normalizeLookupProvider(provider, relation = {}, { context = "crudRepository" } = {})`
186
- - `resolveLookupProviderOwnershipFilter(provider = {}, { context = "crudRepository" } = {})`
185
+ - `normalizeLookup(provider, relation = {}, { context = "crudRepository" } = {})`
186
+ - `resolveLookupOwnershipFilter(provider = {}, { context = "crudRepository" } = {})`
187
187
  - `resolveLookupVisibilityContext(provider = {}, relation = {}, callOptions = {}, { context = "crudRepository" } = {})`
188
188
  - `buildLookupRecordMap(records = [], valueKey = "")`
189
189
  - `buildLookupCollectionMap(records = [], foreignKey = "")`
@@ -193,21 +193,21 @@ Local functions
193
193
  Exports
194
194
  - `normalizeCrudLookupApiPath`
195
195
  - `normalizeCrudLookupNamespace`
196
- - `requireCrudLookupNamespace(value = "", { context = "crudLookupProvider" } = {})`
197
- - `resolveCrudLookupNamespaceFromRelation(relation = {}, { context = "crudLookupProvider" } = {})`
198
- - `resolveCrudLookupProviderToken(namespace = "", { context = "crudLookupProvider" } = {})`
196
+ - `requireCrudLookupNamespace(value = "", { context = "crudLookup" } = {})`
197
+ - `resolveCrudLookupNamespaceFromRelation(relation = {}, { context = "crudLookup" } = {})`
198
+ - `resolveCrudLookupToken(namespace = "", { context = "crudLookup" } = {})`
199
199
 
200
- ### `src/server/lookupProviders.js`
200
+ ### `src/server/lookups.js`
201
201
  Exports
202
- - `resolveCrudLookupProviderToken`
203
- - `createCrudLookupProviderResolver(scope, { context = "crudLookupProvider" } = {})`
204
- - `createCrudLookupProvider(repository, { context = "crudLookupProvider", ownershipFilter = "" } = {})`
202
+ - `resolveCrudLookupToken`
203
+ - `createCrudLookupResolver(scope, { context = "crudLookup" } = {})`
204
+ - `createCrudLookup(repository, { context = "crudLookup", ownershipFilter = "" } = {})`
205
205
  Local functions
206
- - `normalizeLookupProviderOwnershipFilter(value, { context = "crudLookupProvider ownershipFilter" } = {})`
206
+ - `normalizeLookupOwnershipFilter(value, { context = "crudLookup ownershipFilter" } = {})`
207
207
 
208
208
  ### `src/server/repositoryMethods.js`
209
209
  Exports
210
- - `createCrudRepositoryRuntime(resource = {}, { context = "crudRepository", list = {} } = {})`
210
+ - `createCrudResourceRuntime(resource = {}, { context = "crudRepository", list = {}, virtualFields = {} } = {})`
211
211
  - `crudRepositoryList(runtime, knex, query = {}, repositoryOptions = {}, callOptions = {}, hooks = null)`
212
212
  - `crudRepositoryFindById(runtime, knex, recordId, repositoryOptions = {}, callOptions = {}, hooks = null)`
213
213
  - `crudRepositoryListByIds(runtime, knex, ids = [], repositoryOptions = {}, callOptions = {}, hooks = null)`
@@ -218,6 +218,8 @@ Exports
218
218
  Local functions
219
219
  - `requireCrudRecordId(value, { context = "crudRepository" } = {})`
220
220
  - `resolveRepositoryDefaults(resource = {}, repositoryMapping = {})`
221
+ - `normalizeCrudVirtualFieldHandlers(virtualFields = {}, repositoryMapping = {}, { context = "crudRepository" } = {})`
222
+ - `applyCrudRepositoryVirtualProjections(dbQuery, runtime = {}, { knex, tableName } = {})`
221
223
  - `normalizeSearchColumns(searchColumns = [], fallbackColumns = [])`
222
224
  - `normalizeListOrderDirection(value = LIST_ORDER_DIRECTION_ASC)`
223
225
  - `normalizeListOrderNulls(value = LIST_ORDER_NULLS_LAST)`
@@ -262,8 +264,9 @@ Exports
262
264
  - `buildWritePayload(sourcePayload = {}, fieldKeys = [], overrides = {})`
263
265
  - `resolveColumnName(fieldKey, overrides = {})`
264
266
  - `resolveCrudIdColumn(idColumn, { fallback = "id" } = {})`
265
- - `buildRepositoryColumnMetadata({ outputKeys = [], writeKeys = [], columnOverrides = {} } = {})`
267
+ - `buildRepositoryColumnMetadata({ outputKeys = [], writeKeys = [], columnOverrides = {}, fieldStorageByKey = {} } = {})`
266
268
  Local functions
269
+ - `resolveOptionalObjectSchemaProperties(schema, options = {})`
267
270
  - `requireObjectSchemaProperties(schema, { context = "crudRepository", schemaLabel = "schema" } = {})`
268
271
  - `normalizeResourceFieldMetaEntries(fieldMeta = [])`
269
272
  - `schemaIncludesStringType(schema = {})`
@@ -286,11 +289,14 @@ Local functions
286
289
 
287
290
  ### `src/shared/crudFieldMetaSupport.js`
288
291
  Exports
292
+ - `CRUD_FIELD_REPOSITORY_STORAGE_COLUMN`
293
+ - `CRUD_FIELD_REPOSITORY_STORAGE_VIRTUAL`
289
294
  - `CRUD_LOOKUP_FORM_CONTROL_AUTOCOMPLETE`
290
295
  - `CRUD_LOOKUP_FORM_CONTROL_SELECT`
291
296
  - `CRUD_RUNTIME_LOOKUPS_FIELD_KEY`
292
297
  - `checkCrudLookupFormControl(value, { context = "crud fieldMeta ui.formControl", defaultValue = CRUD_LOOKUP_FORM_CONTROL_AUTOCOMPLETE } = {})`
293
298
  - `isCrudRuntimeOutputOnlyFieldKey(value = "", { lookupContainerKey = CRUD_RUNTIME_LOOKUPS_FIELD_KEY } = {})`
299
+ - `normalizeCrudFieldRepositoryConfig(fieldMetaEntry = {}, { context = "crud fieldMeta repository", fieldKey = "" } = {})`
294
300
 
295
301
  ### `src/shared/crudNamespaceSupport.js`
296
302
  Exports
@@ -28,6 +28,7 @@ Exports
28
28
 
29
29
  ### `src/server/cliRuntime/appState.js`
30
30
  Exports
31
+ - `directoryLooksLikeJskitAppRoot(directoryPath)`
31
32
  - `resolveAppRootFromCwd(cwd)`
32
33
  - `loadAppPackageJson(appRoot)`
33
34
  - `createDefaultLock()`
@@ -41,8 +42,6 @@ Exports
41
42
  - `upsertEnvValue(content, key, value)`
42
43
  - `removeEnvValue(content, key, expectedValue, previous)`
43
44
  - `writeJsonFile`
44
- Local functions
45
- - `directoryLooksLikeJskitAppRoot(directoryPath)`
46
45
 
47
46
  ### `src/server/cliRuntime/capabilitySupport.js`
48
47
  Exports
@@ -379,6 +378,7 @@ Exports
379
378
  - `normalizeText(value = "")`
380
379
  - `isTruthyFlag(rawValue = "")`
381
380
  - `runExternalCommand(command, args = [], { cwd = "", env = {}, stdout, stderr, quiet = false, createCliError } = {})`
381
+ - `runExternalShellCommand(commandText, { cwd = "", env = {}, stdout, stderr, quiet = false, createCliError } = {})`
382
382
  - `formatUtcReleaseTimestamp(date = new Date())`
383
383
  - `resolveLocalJskitBin(appRoot = "")`
384
384
  - `runLocalJskit(appRoot, args = [], { stdout, stderr, createCliError, quiet = false } = {})`
@@ -402,6 +402,10 @@ Local functions
402
402
  Exports
403
403
  - `runAppVerifyCommand(ctx = {}, { appRoot = "", options = {}, stdout, stderr })`
404
404
 
405
+ ### `src/server/commandHandlers/appCommands/verifyUi.js`
406
+ Exports
407
+ - `runAppVerifyUiCommand(ctx = {}, { appRoot = "", options = {}, stdout, stderr })`
408
+
405
409
  ### `src/server/commandHandlers/completion.js`
406
410
  Exports
407
411
  - `createCompletionCommands(ctx = {})`
@@ -672,6 +676,22 @@ Exports
672
676
  Local functions
673
677
  - `resolveCatalogPackagesPath()`
674
678
 
679
+ ### `src/server/shared/uiVerification.js`
680
+ Exports
681
+ - `UI_VERIFICATION_AUTH_MODES`
682
+ - `UI_VERIFICATION_RECEIPT_RELATIVE_PATH`
683
+ - `UI_VERIFICATION_RECEIPT_VERSION`
684
+ - `UI_VERIFICATION_RUNNER`
685
+ - `isUiVerificationAuthMode(value = "")`
686
+ - `isUiVerificationPath(relativePath = "")`
687
+ - `isValidUiVerificationReceipt(receipt)`
688
+ - `normalizeUiVerificationReceipt(rawReceipt = {})`
689
+ - `resolveChangedUiFilesFromGit(appRoot = "")`
690
+ - `sortUniqueStrings(values = [])`
691
+ Local functions
692
+ - `normalizeText(value = "")`
693
+ - `readGitPathList(appRoot = "", args = [])`
694
+
675
695
  ### bin
676
696
 
677
697
  ### `bin/jskit.js`
@@ -45,6 +45,7 @@ Before calling a chunk done, report:
45
45
  - `Deslop review: ...`
46
46
  - `JSKIT review: ...`
47
47
  - `Material/Vuetify review: ...`
48
+ - `Playwright: ...` for any chunk that adds or changes user-facing UI
48
49
  - `Verification: ...`
49
50
  - `Files changed: ...`
50
51
  - `Commands run: ...`
@@ -71,6 +72,11 @@ Core rules:
71
72
  - Use the real env var names or option names instead of vague requests for "credentials". Values such as `DB_NAME`, `DB_USER`, `DB_PASSWORD`, `AUTH_SUPABASE_URL`, `AUTH_SUPABASE_PUBLISHABLE_KEY`, and `APP_PUBLIC_URL` are routine setup inputs when the relevant package stack needs them.
72
73
  - 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.
73
74
  - Every user-facing screen must pass a Material Design and Vuetify quality review before the chunk is considered done.
75
+ - Any chunk that adds or changes user-facing UI must include a Playwright check that exercises the changed behavior before the chunk is considered done.
76
+ - Record that Playwright run with `jskit app verify-ui --command "<playwright command>" --feature "<label>" --auth-mode <mode>` so `jskit doctor` can verify the receipt.
77
+ - If the UI flow requires login, use the app's development-only auth bypass or session bootstrap path instead of a live external auth login flow.
78
+ - In JSKIT apps using the standard auth stack, that means enabling the dev auth bypass in development and using `POST /api/dev-auth/login-as` during Playwright setup.
79
+ - If authenticated UI work has no usable local auth bootstrap path yet, treat that as a testability gap and call it out before the chunk can be considered complete.
74
80
  - Do not implement app features before the blueprint has the database, surfaces, ownership model, and route/screen plan written down.
75
81
  - 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.
76
82
  - 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.
@@ -22,7 +22,7 @@ For each chunk, follow this order:
22
22
  5. Deslop the chunk.
23
23
  6. Review the chunk against JSKIT reuse and best practices.
24
24
  7. Review user-facing screens against Material Design and Vuetify best practices, and improve any screens that do not meet that bar.
25
- 8. Verify the chunk with the relevant commands, including Playwright for meaningful UI flows.
25
+ 8. Verify the chunk with the relevant commands. Any chunk that adds or changes user-facing UI must include a Playwright flow that exercises the changed behavior, and that run must be recorded with `jskit app verify-ui`.
26
26
  9. Update `.jskit/WORKBOARD.md` with status, commands run, and anything still unverified.
27
27
  10. Only then move to the next chunk.
28
28
 
@@ -59,5 +59,9 @@ After the last chunk:
59
59
 
60
60
  Playwright note:
61
61
 
62
- - When login is required, prefer a test-only impersonation or session bootstrap path over dependence on a live external auth provider.
62
+ - When login is required, use a test-only auth bypass or session bootstrap path instead of dependence on a live external auth provider.
63
+ - Record the run through `jskit app verify-ui --command "<playwright command>" --feature "<label>" --auth-mode <mode>` so `jskit doctor` can verify the receipt against the current changed UI files.
64
+ - In the standard JSKIT auth stack, the default development path is `POST /api/dev-auth/login-as`, guarded by `AUTH_DEV_BYPASS_ENABLED=true` and `AUTH_DEV_BYPASS_SECRET=...`.
65
+ - That route is development-only and must not be enabled in production.
66
+ - Because it is still an unsafe POST, fetch `csrfToken` from `/api/session`, send it as the `csrf-token` header, and make the request in the same browser context that will run the Playwright assertions so the session cookies land in the page session.
63
67
  - If such a path does not exist yet, treat that as a testability gap and decide whether the chunk must add it before the feature is considered complete.
@@ -31,8 +31,11 @@ Before calling a chunk or a whole changeset done, review it in four passes:
31
31
  4. Verification review
32
32
  - run the smallest relevant verification commands for a chunk
33
33
  - run the widest relevant verification commands for a whole changeset
34
- - include Playwright for meaningful user-facing flows
35
- - if login is required, verify the chosen test-auth path or explicitly record that the lack of one is an open gap
34
+ - any added or changed user-facing UI must be exercised with Playwright
35
+ - that Playwright run should normally be recorded through `jskit app verify-ui`
36
+ - if login is required, use the chosen local test-auth path instead of live external auth
37
+ - in the standard JSKIT auth stack, prefer the development-only `POST /api/dev-auth/login-as` path
38
+ - if there is no usable local auth bootstrap path, explicitly record that as a blocking testability gap
36
39
  - note anything left unverified
37
40
 
38
41
  Minimum expectation: