@jskit-ai/agent-docs 0.1.257 → 0.1.258

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/guide/agent/app-extras/assistant-conversation.md +88 -35
  2. package/guide/agent/app-extras/assistant.md +28 -28
  3. package/guide/agent/app-setup/authentication.md +36 -17
  4. package/guide/agent/app-setup/database-layer.md +116 -17
  5. package/guide/agent/app-setup/existing-application-migration.md +29 -25
  6. package/guide/agent/app-setup/multi-homing.md +94 -48
  7. package/guide/agent/app-setup/run-an-accounts-hub.md +6 -4
  8. package/guide/agent/framework/client-features.md +496 -0
  9. package/guide/agent/framework/crud-operations.md +5 -5
  10. package/guide/agent/framework/translation.md +4 -3
  11. package/guide/agent/framework/typed-features.md +1104 -0
  12. package/guide/agent/framework/ui-operations.md +2 -2
  13. package/guide/agent/index.md +6 -1
  14. package/package.json +1 -1
  15. package/patterns/feature-package/PATTERN.md +104 -10
  16. package/patterns/feature-package/example/booking-engine/package.json +9 -4
  17. package/patterns/feature-package/example/booking-engine/src/server/BookingEngineFeature.js +24 -0
  18. package/patterns/feature-package/example/booking-engine/src/server/bookingAvailability.js +10 -0
  19. package/patterns/feature-package/example/booking-engine/src/server/bookingEngine.js +29 -0
  20. package/patterns/feature-package/example/booking-engine/src/server/bookingEngineActions.js +26 -0
  21. package/patterns/feature-package/example/booking-engine/src/server/inputSchemas.js +1 -2
  22. package/patterns/feature-package/example/booking-engine/src/server/types.js +12 -0
  23. package/patterns/feature-package/example/variations/AvailabilityEngineFeature.js +19 -0
  24. package/patterns/feature-package/example/variations/InvoiceRollupFeature.js +23 -0
  25. package/patterns/feature-package/example/variations/availabilityEngineActions.js +26 -0
  26. package/patterns/feature-package/example/variations/customKnexRepository.js +32 -26
  27. package/patterns/feature-package/example/variations/invoiceRollupActions.js +26 -0
  28. package/patterns/feature-package/example/variations/orchestratorService.js +24 -19
  29. package/patterns/feature-package/example/variations/types.js +7 -0
  30. package/patterns/minimal-foundation/PATTERN.md +30 -1
  31. package/patterns/minimal-foundation/example/AGENTS.md +14 -0
  32. package/patterns/minimal-foundation/example/bin/develop.js +10 -5
  33. package/patterns/minimal-foundation/example/eslint.config.mjs +21 -2
  34. package/patterns/minimal-foundation/example/package.json +5 -4
  35. package/patterns/minimal-foundation/example/playwright.config.mjs +1 -1
  36. package/patterns/minimal-foundation/example/server/lib/envFile.js +19 -0
  37. package/patterns/minimal-foundation/example/server/lib/loggerOptions.js +15 -0
  38. package/patterns/minimal-foundation/example/server/lib/runtimeEnv.js +17 -17
  39. package/patterns/minimal-foundation/example/server.js +32 -29
  40. package/patterns/minimal-foundation/example/tests/server/smoke.test.js +2 -1
  41. package/patterns/shell-foundation/PATTERN.md +29 -0
  42. package/patterns/shell-foundation/example/AGENTS.md +14 -0
  43. package/patterns/shell-foundation/example/bin/develop.js +10 -5
  44. package/patterns/shell-foundation/example/eslint.config.mjs +21 -2
  45. package/patterns/shell-foundation/example/package.json +6 -5
  46. package/patterns/shell-foundation/example/packages/main/package.json +1 -1
  47. package/patterns/shell-foundation/example/packages/main/src/client/providers/MainClientProvider.js +1 -0
  48. package/patterns/shell-foundation/example/playwright.config.mjs +1 -1
  49. package/patterns/shell-foundation/example/server/lib/envFile.js +19 -0
  50. package/patterns/shell-foundation/example/server/lib/loggerOptions.js +15 -0
  51. package/patterns/shell-foundation/example/server/lib/runtimeEnv.js +17 -17
  52. package/patterns/shell-foundation/example/server.js +32 -29
  53. package/patterns/shell-foundation/example/tests/server/smoke.test.js +2 -1
  54. package/reference/autogen/KERNEL_MAP.md +14 -8
  55. package/reference/autogen/PATTERN_INDEX.md +37 -37
  56. package/reference/autogen/README.md +2 -0
  57. package/reference/autogen/packages/agent-docs.md +70 -26
  58. package/reference/autogen/packages/agent-runtime.md +129 -0
  59. package/reference/autogen/packages/agent.md +149 -0
  60. package/reference/autogen/packages/architecture-inspector.md +97 -44
  61. package/reference/autogen/packages/assistant-core.md +786 -292
  62. package/reference/autogen/packages/assistant-runtime.md +182 -57
  63. package/reference/autogen/packages/assistant-voice-sherpa.md +4 -0
  64. package/reference/autogen/packages/assistant-voice.md +54 -8
  65. package/reference/autogen/packages/auth-core.md +44 -13
  66. package/reference/autogen/packages/auth-provider-local-core.md +86 -34
  67. package/reference/autogen/packages/auth-provider-local-db-core.md +1 -2
  68. package/reference/autogen/packages/auth-provider-oidc-core.md +10 -4
  69. package/reference/autogen/packages/auth-provider-supabase-core.md +52 -66
  70. package/reference/autogen/packages/auth-web.md +19 -11
  71. package/reference/autogen/packages/connectors-catalog.md +71 -43
  72. package/reference/autogen/packages/connectors-core.md +42 -4
  73. package/reference/autogen/packages/console-core.md +27 -14
  74. package/reference/autogen/packages/crud-core.md +178 -170
  75. package/reference/autogen/packages/database-runtime-mysql.md +0 -2
  76. package/reference/autogen/packages/database-runtime-postgres.md +0 -2
  77. package/reference/autogen/packages/database-runtime.md +39 -10
  78. package/reference/autogen/packages/http-runtime.md +79 -57
  79. package/reference/autogen/packages/json-rest-api-core.md +11 -8
  80. package/reference/autogen/packages/kernel.md +144 -83
  81. package/reference/autogen/packages/oidc-issuer-core.md +49 -20
  82. package/reference/autogen/packages/payments-core.md +20 -3
  83. package/reference/autogen/packages/realtime.md +47 -22
  84. package/reference/autogen/packages/resource-core.md +1 -1
  85. package/reference/autogen/packages/resource-crud-core.md +64 -26
  86. package/reference/autogen/packages/rewarded-core.md +47 -36
  87. package/reference/autogen/packages/shell-web.md +7 -10
  88. package/reference/autogen/packages/storage-runtime.md +6 -2
  89. package/reference/autogen/packages/uploads-runtime.md +10 -2
  90. package/reference/autogen/packages/users-core.md +73 -71
  91. package/reference/autogen/packages/workspaces-core.md +121 -106
  92. package/reference/autogen/packages/workspaces-web.md +1 -2
  93. package/reference/autogen/tooling/config-eslint.md +302 -0
  94. package/reference/autogen/tooling/jskit-catalog.md +28 -18
  95. package/reference/autogen/tooling/jskit-release.md +35 -20
  96. package/reference/autogen/tooling/testUtils.md +3 -1
  97. package/skills/jskit/SKILL.md +3 -2
  98. package/skills/jskit/references/app-operations.md +1 -1
  99. package/skills/jskit/references/client-features.md +491 -0
  100. package/skills/jskit/references/crud-form-actions.md +1 -1
  101. package/skills/jskit/references/crud-operations.md +7 -7
  102. package/skills/jskit/references/existing-application-migration.md +31 -27
  103. package/skills/jskit/references/heuristics/INDEX.md +5 -1
  104. package/skills/jskit/references/heuristics/child-cruds.md +1 -1
  105. package/skills/jskit/references/heuristics/client-features.md +66 -0
  106. package/skills/jskit/references/heuristics/client-requests.md +1 -1
  107. package/skills/jskit/references/heuristics/crud-authoring.md +12 -2
  108. package/skills/jskit/references/heuristics/crud-links.md +1 -1
  109. package/skills/jskit/references/heuristics/crud-repository-mapping.md +3 -3
  110. package/skills/jskit/references/heuristics/filters.md +1 -1
  111. package/skills/jskit/references/heuristics/live-actions.md +1 -1
  112. package/skills/jskit/references/heuristics/page-redirects.md +1 -1
  113. package/skills/jskit/references/heuristics/page-scaffolding.md +1 -1
  114. package/skills/jskit/references/heuristics/placements.md +1 -1
  115. package/skills/jskit/references/heuristics/row-policies.md +1 -1
  116. package/skills/jskit/references/heuristics/server-search.md +1 -1
  117. package/skills/jskit/references/heuristics/surfaces.md +1 -1
  118. package/skills/jskit/references/heuristics/typed-features.md +100 -0
  119. package/skills/jskit/references/heuristics/ui-contract.md +1 -1
  120. package/skills/jskit/references/heuristics/ui-testing.md +4 -4
  121. package/skills/jskit/references/material-3.md +2 -2
  122. package/skills/jskit/references/multi-homing.md +96 -50
  123. package/skills/jskit/references/pattern-index.md +37 -37
  124. package/skills/jskit/references/patterns/app/minimal-foundation/PATTERN.md +30 -1
  125. package/skills/jskit/references/patterns/app/minimal-foundation/example/AGENTS.md +14 -0
  126. package/skills/jskit/references/patterns/app/minimal-foundation/example/bin/develop.js +10 -5
  127. package/skills/jskit/references/patterns/app/minimal-foundation/example/eslint.config.mjs +21 -2
  128. package/skills/jskit/references/patterns/app/minimal-foundation/example/package.json +5 -4
  129. package/skills/jskit/references/patterns/app/minimal-foundation/example/playwright.config.mjs +1 -1
  130. package/skills/jskit/references/patterns/app/minimal-foundation/example/server/lib/envFile.js +19 -0
  131. package/skills/jskit/references/patterns/app/minimal-foundation/example/server/lib/loggerOptions.js +15 -0
  132. package/skills/jskit/references/patterns/app/minimal-foundation/example/server/lib/runtimeEnv.js +17 -17
  133. package/skills/jskit/references/patterns/app/minimal-foundation/example/server.js +32 -29
  134. package/skills/jskit/references/patterns/app/minimal-foundation/example/tests/server/smoke.test.js +2 -1
  135. package/skills/jskit/references/patterns/app/shell-foundation/PATTERN.md +29 -0
  136. package/skills/jskit/references/patterns/app/shell-foundation/example/AGENTS.md +14 -0
  137. package/skills/jskit/references/patterns/app/shell-foundation/example/bin/develop.js +10 -5
  138. package/skills/jskit/references/patterns/app/shell-foundation/example/eslint.config.mjs +21 -2
  139. package/skills/jskit/references/patterns/app/shell-foundation/example/package.json +6 -5
  140. package/skills/jskit/references/patterns/app/shell-foundation/example/packages/main/package.json +1 -1
  141. package/skills/jskit/references/patterns/app/shell-foundation/example/packages/main/src/client/providers/MainClientProvider.js +1 -0
  142. package/skills/jskit/references/patterns/app/shell-foundation/example/playwright.config.mjs +1 -1
  143. package/skills/jskit/references/patterns/app/shell-foundation/example/server/lib/envFile.js +19 -0
  144. package/skills/jskit/references/patterns/app/shell-foundation/example/server/lib/loggerOptions.js +15 -0
  145. package/skills/jskit/references/patterns/app/shell-foundation/example/server/lib/runtimeEnv.js +17 -17
  146. package/skills/jskit/references/patterns/app/shell-foundation/example/server.js +32 -29
  147. package/skills/jskit/references/patterns/app/shell-foundation/example/tests/server/smoke.test.js +2 -1
  148. package/skills/jskit/references/patterns/assistant/assistant-surface/example/src/ApplicationAiFeature.js +4 -2
  149. package/skills/jskit/references/patterns/auth/accounts-hub/PATTERN.md +2 -1
  150. package/skills/jskit/references/patterns/auth/accounts-hub/example/package.json +2 -2
  151. package/skills/jskit/references/patterns/auth/accounts-hub/example/server/hubPaths.js +9 -3
  152. package/skills/jskit/references/patterns/auth/auth-surface/PATTERN.md +25 -12
  153. package/skills/jskit/references/patterns/auth/auth-surface/example/tools/preview-identity +7 -1
  154. package/skills/jskit/references/patterns/auth/oidc-sign-in/example/package.json +2 -2
  155. package/skills/jskit/references/patterns/auth/supabase-auth/example/package.json +1 -1
  156. package/skills/jskit/references/patterns/connectors/ai-connections/PATTERN.md +16 -1
  157. package/skills/jskit/references/patterns/connectors/ai-connections/example/ai-model.js +12 -3
  158. package/skills/jskit/references/patterns/connectors/ai-connections/example/perplexity-answer.js +15 -4
  159. package/skills/jskit/references/patterns/connectors/api-key-connection/PATTERN.md +9 -1
  160. package/skills/jskit/references/patterns/connectors/api-key-connection/example/connections.js +5 -2
  161. package/skills/jskit/references/patterns/connectors/aws-storage-queries/PATTERN.md +13 -3
  162. package/skills/jskit/references/patterns/connectors/aws-storage-queries/example/s3-transfer.js +14 -3
  163. package/skills/jskit/references/patterns/connectors/calendar-cli/PATTERN.md +8 -0
  164. package/skills/jskit/references/patterns/connectors/calendar-cli/example/knexfile.js +1 -1
  165. package/skills/jskit/references/patterns/connectors/calendar-cli/example/package.json +4 -4
  166. package/skills/jskit/references/patterns/connectors/calendar-cli/example/scripts/calendar.js +8 -4
  167. package/skills/jskit/references/patterns/connectors/event-delivery/PATTERN.md +1 -1
  168. package/skills/jskit/references/patterns/connectors/firebase-messaging/PATTERN.md +10 -0
  169. package/skills/jskit/references/patterns/connectors/firebase-messaging/example/server/notifications.js +8 -0
  170. package/skills/jskit/references/patterns/connectors/google-ads-search/PATTERN.md +18 -7
  171. package/skills/jskit/references/patterns/connectors/google-ads-search/example/ads-setup.js +13 -5
  172. package/skills/jskit/references/patterns/connectors/oauth-connection/PATTERN.md +1 -1
  173. package/skills/jskit/references/patterns/connectors/paddle-catalogue/PATTERN.md +11 -2
  174. package/skills/jskit/references/patterns/connectors/paddle-catalogue/example/create-products.js +5 -2
  175. package/skills/jskit/references/patterns/connectors/public-image/PATTERN.md +10 -1
  176. package/skills/jskit/references/patterns/connectors/public-image/example/logo-url.js +4 -1
  177. package/skills/jskit/references/patterns/connectors/redshift-queries/PATTERN.md +1 -1
  178. package/skills/jskit/references/patterns/connectors/source-scanning/PATTERN.md +13 -2
  179. package/skills/jskit/references/patterns/connectors/source-scanning/example/source-scanner.js +9 -0
  180. package/skills/jskit/references/patterns/crud/json-api-resource-package/PATTERN.md +102 -23
  181. package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/package.json +3 -2
  182. package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/src/server/types.js +5 -0
  183. package/skills/jskit/references/patterns/crud/resource-contract/PATTERN.md +54 -1
  184. package/skills/jskit/references/patterns/database/mysql-application/PATTERN.md +16 -5
  185. package/skills/jskit/references/patterns/database/mysql-application/example/knexfile.js +2 -2
  186. package/skills/jskit/references/patterns/database/mysql-application/example/package.json +1 -1
  187. package/skills/jskit/references/patterns/database/mysql-application/example/scripts/prepare-database.js +1 -1
  188. package/skills/jskit/references/patterns/database/postgres-application/PATTERN.md +16 -5
  189. package/skills/jskit/references/patterns/database/postgres-application/example/knexfile.js +2 -2
  190. package/skills/jskit/references/patterns/database/postgres-application/example/package.json +1 -1
  191. package/skills/jskit/references/patterns/database/postgres-application/example/scripts/prepare-database.js +1 -1
  192. package/skills/jskit/references/patterns/realtime/realtime-application/example/package.json +2 -2
  193. package/skills/jskit/references/patterns/rewards/google-rewarded/example/GoogleRewardedDeliveryProvider.js +1 -1
  194. package/skills/jskit/references/patterns/rewards/google-rewarded/example/googlePublisherTag.js +5 -11
  195. package/skills/jskit/references/patterns/server/feature-package/PATTERN.md +104 -10
  196. package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/package.json +9 -4
  197. package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/src/server/BookingEngineFeature.js +24 -0
  198. package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/src/server/bookingAvailability.js +10 -0
  199. package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/src/server/bookingEngine.js +29 -0
  200. package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/src/server/bookingEngineActions.js +26 -0
  201. package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/src/server/inputSchemas.js +1 -2
  202. package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/src/server/types.js +12 -0
  203. package/skills/jskit/references/patterns/server/feature-package/example/variations/AvailabilityEngineFeature.js +19 -0
  204. package/skills/jskit/references/patterns/server/feature-package/example/variations/InvoiceRollupFeature.js +23 -0
  205. package/skills/jskit/references/patterns/server/feature-package/example/variations/availabilityEngineActions.js +26 -0
  206. package/skills/jskit/references/patterns/server/feature-package/example/variations/customKnexRepository.js +32 -26
  207. package/skills/jskit/references/patterns/server/feature-package/example/variations/invoiceRollupActions.js +26 -0
  208. package/skills/jskit/references/patterns/server/feature-package/example/variations/orchestratorService.js +24 -19
  209. package/skills/jskit/references/patterns/server/feature-package/example/variations/types.js +7 -0
  210. package/skills/jskit/references/patterns/users/user-administration-server/PATTERN.md +16 -1
  211. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/package.json +3 -2
  212. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/server/types.js +5 -0
  213. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/package.json +4 -3
  214. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/server/UsersWorkspaceFeature.js +2 -9
  215. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/server/types.js +5 -0
  216. package/skills/jskit/references/patterns/workspaces/workspace-server/PATTERN.md +42 -17
  217. package/skills/jskit/references/patterns/workspaces/workspace-server/example/packages/main/src/server/WorkspaceInvitationsFeature.js +27 -0
  218. package/skills/jskit/references/patterns/workspaces/workspace-server/example/packages/main/src/server/email/workspaceInviteEmail.js +11 -10
  219. package/skills/jskit/references/patterns/workspaces/workspace-server/example/packages/main/src/server/email/workspaceInviteMailer.js +27 -0
  220. package/skills/jskit/references/translation.md +5 -4
  221. package/skills/jskit/references/typed-features.md +1099 -0
  222. package/skills/jskit/references/ui-operations.md +3 -3
  223. package/patterns/feature-package/example/booking-engine/src/server/BookingEngineProvider.js +0 -33
  224. package/patterns/feature-package/example/booking-engine/src/server/actions.js +0 -24
  225. package/patterns/feature-package/example/variations/AvailabilityEngineProvider.js +0 -31
  226. package/patterns/feature-package/example/variations/InvoiceRollupProvider.js +0 -36
  227. package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/src/server/BookingEngineProvider.js +0 -33
  228. package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/src/server/actions.js +0 -24
  229. package/skills/jskit/references/patterns/server/feature-package/example/variations/AvailabilityEngineProvider.js +0 -31
  230. package/skills/jskit/references/patterns/server/feature-package/example/variations/InvoiceRollupProvider.js +0 -36
@@ -0,0 +1,1104 @@
1
+ <!-- Generated by `npm run agent-docs:build` from `packages/agent-docs/site/guide/framework/typed-features.md`. Do not edit manually. -->
2
+
3
+ ---
4
+ title: Typed features
5
+ description: How JSKIT server code is written, rule by rule with a good and a bad example, and the lint rules that check it.
6
+ ---
7
+
8
+ # Typed features
9
+
10
+ Read this page before writing or reviewing JSKIT server code: a feature,
11
+ provider, action, service, repository, route, error or test. Each rule says
12
+ what to do, why in one line, and shows good and bad code. `npm run lint`
13
+ reports the rules a machine can see ([checks](#checks)); the others are
14
+ checked by reading. The `server/feature-package` pattern in the
15
+ [source pattern library](/patterns/) is the smallest complete feature to copy.
16
+
17
+ The [typed features heuristic](https://github.com/mobily-enterprises/jskit-ai/blob/main/packages/agent-docs/heuristics/typed-features.md)
18
+ maps keywords and lint findings to the section that fixes them. Client code
19
+ (composables, components, client factories and providers) follows the same
20
+ rule in [client features](/guide/framework/client-features).
21
+
22
+ ## The rule
23
+
24
+ > Every collaborator a feature uses arrives through its declared JSKIT
25
+ > capabilities, and is typed where it is received and required, never probed,
26
+ > so each call has exactly one visible destination for a reader and for the
27
+ > code map.
28
+
29
+ - A **capability** is a value a feature `provides`, `requires` or may use
30
+ (`optional`), named by a **token** such as `"feature.invoices"`.
31
+ - A **collaborator** is an object or function with behaviour that code calls:
32
+ a service, store, repository, runtime, router, publisher, catalogue, logger
33
+ or clock. Plain data (an input, a record, a list, an options bag) is not one.
34
+ - A **slot** is a place that holds one value someone else supplies.
35
+ - A **refusal** is an error meant for the caller: bad input, not found,
36
+ forbidden, conflict.
37
+
38
+ Following a call then takes one step: the parameter names its type, the type
39
+ names its file. The architecture inspector's code map takes the same step, so
40
+ it reports the call as exact instead of guessing from the method name.
41
+
42
+ ## Features
43
+
44
+ ### Feature files
45
+
46
+ **A `defineFeature` lives in `<Name>Feature.js`, its keys in one order, one
47
+ per line: `id, domain, requires, optional, provides, setup, actions, boot,
48
+ shutdown`.** A `defineProvider` lives in `<Name>Provider.js`.
49
+
50
+ - `setup` builds each provided value with a named `create<Name>` factory from
51
+ a module of its own, never inline.
52
+ - `actions` is a method that returns `create<Name>Actions({ … })`.
53
+ - The hooks take `capabilities`, destructured and typed.
54
+
55
+ Why: every feature reads the same way, top to bottom.
56
+
57
+ ```js
58
+ // good
59
+ const InvoicesFeature = defineFeature({
60
+ id: "feature.invoices",
61
+ requires: {
62
+ database: "runtime.database"
63
+ },
64
+ provides: {
65
+ invoices: "feature.invoices"
66
+ },
67
+ /** @param {{ database: import("@jskit-ai/database-runtime/server/types").RuntimeDatabase }} capabilities */
68
+ setup({ database }) {
69
+ return { invoices: createInvoices({ knex: database.knex }) };
70
+ },
71
+ actions({ invoices }) {
72
+ return createInvoiceActions({ invoices });
73
+ }
74
+ });
75
+
76
+ // bad: the value built inline, the actions an inline array
77
+ setup: ({ database }) => ({ invoices: { list: () => database.knex("invoices") } }),
78
+ actions: ({ invoices }) => [{ id: "invoices.list", execute: () => invoices.list() }]
79
+ ```
80
+
81
+ ### Capability types
82
+
83
+ **Each provided token has one `@typedef` in its package's `src/server/types.js`,
84
+ exported as `./server/types`. A type used in two or more files is named once
85
+ there.**
86
+
87
+ - Derive it from the factory, `ReturnType<typeof createInvoices>`, and name it
88
+ after the token in PascalCase (`feature.invoices` is `Invoices`). Each token
89
+ gets its own name, even when it aliases another type.
90
+ - Import a type from the package that owns it; never re-alias it through
91
+ another module. A JSDoc `import("…")` is type-only, so a package cycle is no
92
+ reason to hand-write one.
93
+ - Hand-write a contract only for a token with several implementations (such as
94
+ `auth.service`), in its owning package, with optional operations marked
95
+ optional.
96
+ - JSKIT's names: `RuntimeKnex`, `RuntimeKnexTransaction`,
97
+ `RuntimeKnexQueryBuilder`, `RuntimeRouter`, `RuntimeLogger`, `RuntimeEnv`,
98
+ `CrudService`, `CrudRepository`.
99
+
100
+ Why: a typed parameter tells a reader and the code map which file a call
101
+ reaches.
102
+
103
+ ```js
104
+ // good: src/server/types.js
105
+ /** @typedef {ReturnType<typeof import("./invoices.js").createInvoices>} Invoices */
106
+ export {};
107
+ // a consumer in another package
108
+ /** @param {{ invoices: import("@local/invoices/server/types").Invoices }} dependencies */
109
+
110
+ // bad: spelled out at every use, or a hand-written interface of `any`
111
+ /** @param {{ invoices: ReturnType<typeof import("../../invoices/src/server/invoices.js").createInvoices> }} dependencies */
112
+ /** @typedef {{ list: (...args: any[]) => Promise<any> }} InvoiceReads */
113
+ ```
114
+
115
+ ### Typed collaborators
116
+
117
+ **Every factory, constructor, hook and helper that calls a collaborator
118
+ destructures it in its signature and types it with a JSDoc `@param`.**
119
+
120
+ - That includes `setup()`, `boot()` and `shutdown()`, route registrars,
121
+ formatters and repository helpers. A hook that only passes capabilities on
122
+ needs no type.
123
+ - Pure helpers (validators, mappers, id builders) are imported, never passed
124
+ in.
125
+
126
+ Why: the type is what turns a call into one step.
127
+
128
+ ```js
129
+ // good
130
+ /**
131
+ * The invoices the feature provides.
132
+ * @param {{ knex: import("@jskit-ai/database-runtime/server/types").RuntimeKnex }} dependencies
133
+ */
134
+ function createInvoices({ knex }) {
135
+
136
+ // bad: untyped, read from a bag in the body
137
+ function createInvoices(dependencies) {
138
+ const knex = dependencies.knex;
139
+ ```
140
+
141
+ ### Required collaborators
142
+
143
+ **A collaborator is required: no `= null`, `= {}` or no-op default, no `typeof
144
+ x.method === "function"` probe, no `x?.method?.()` and no "x is required"
145
+ guard.**
146
+
147
+ - A default that is the production implementation stays: `fetchImpl =
148
+ globalThis.fetch`, `now = Date.now`. One clock per factory.
149
+ - An *optional operation* is a contract method some real implementer lacks.
150
+ An operation every implementer has, including one that refuses while its
151
+ feature is switched off, is required and never probed.
152
+ - A per-call callback in an options bag is data: resolve it once with a named
153
+ no-op, `const { onProgress = reportNoProgress } = options`.
154
+ - A public entry point that checks what an application hands it, such as
155
+ `defineFeature()` checking a definition, is input validation and stays, with
156
+ its [exception comment](#exceptions).
157
+
158
+ Why: the capability runtime already refuses to start without a required
159
+ token; a probe gives each call a second, invisible outcome.
160
+
161
+ ```js
162
+ // good
163
+ function createReminders({ mailer, now = Date.now }) {
164
+ async function send(invoice) {
165
+ await mailer.send(invoice.email, { sentAt: now() });
166
+ }
167
+
168
+ // bad
169
+ function createReminders({ mailer = null, onSent }) {
170
+ async function send(invoice) {
171
+ if (typeof mailer?.send !== "function") return;
172
+ await mailer.send(invoice.email);
173
+ onSent?.(invoice);
174
+ }
175
+ ```
176
+
177
+ ### One shape per value
178
+
179
+ **A value arrives in one shape. Never accept `capability.service ||
180
+ capability` or `x?.value ?? x`.**
181
+
182
+ Why: production takes one branch and the tests take the other, so a reader
183
+ follows two.
184
+
185
+ ```js
186
+ // good: a CRUD capability's service, and a test double with the same shape
187
+ const audits = auditsCapability.service;
188
+ createAuditing({ auditsCapability: { service: auditsDouble } });
189
+
190
+ // bad
191
+ const audits = auditsCapability.service || auditsCapability;
192
+ ```
193
+
194
+ ### Environment from the caller
195
+
196
+ **Code reads the environment its caller passes, typed `RuntimeEnv`. Only the
197
+ application's entry point reads `process.env`, and passes it on.**
198
+
199
+ - A third-party client is a named default resolved once in the factory that
200
+ owns it: `mailClient = createMailClient(config)`.
201
+ - An application loads `.env` once per process, in one module every entry
202
+ point calls.
203
+
204
+ Why: a function that falls back to the process environment hides a caller
205
+ that forgot to pass one, and every test inherits the developer's shell.
206
+
207
+ ```js
208
+ // good
209
+ /** @param {{ env: import("@jskit-ai/kernel/server/types").RuntimeEnv }} options */
210
+ function createMailer({ env }) {
211
+ // server.js, the entry point
212
+ const mailer = createMailer({ env: resolveRuntimeEnv() });
213
+
214
+ // bad
215
+ function createMailer({ env = process.env } = {}) {
216
+ ```
217
+
218
+ ### Optional tokens
219
+
220
+ **A slot is an `optional` token with a named default, resolved once in
221
+ `setup()` with `??`.**
222
+
223
+ - The declaring package names the default beside the slot's type
224
+ (`sequentialNumbering`).
225
+ - `X | null` appears only at the receiving hook; what `setup()` builds
226
+ receives the value as required. A disabled implementation is a named value
227
+ (`DISABLED_MAILER`), never `null`.
228
+ - Anything that holds one value is a slot, however it is named (`set*`,
229
+ `register*`, `attach*`). Catalogues stay catalogues: routes on
230
+ `http.router`, contributors on `runtime.bootstrap`, listeners on
231
+ `runtime.events`, actions and placements.
232
+ - When the slot's supplier requires the slot's owner, the two form a cycle.
233
+ Split the part of the owner the supplier needs into its own feature. Until
234
+ then the setter stays, with a `cycle:` [exception comment](#exceptions), and
235
+ is cleared at shutdown.
236
+
237
+ Why: a slot filled later makes a call's destination depend on what ran
238
+ before.
239
+
240
+ ```js
241
+ // good: the owner declares the slot and resolves it once
242
+ optional: {
243
+ numbering: "feature.invoices.numbering"
244
+ },
245
+ /** @param {{ numbering: import("./types.js").InvoiceNumbering | null }} capabilities */
246
+ setup({ numbering }) {
247
+ return { invoices: createInvoices({ numbering: numbering ?? sequentialNumbering }) };
248
+ },
249
+ // another package fills it by providing the token
250
+ const YearlyNumberingFeature = defineFeature({
251
+ id: "feature.yearly-numbering",
252
+ provides: {
253
+ numbering: "feature.invoices.numbering"
254
+ },
255
+ setup: () => ({ numbering: yearlyNumber })
256
+ });
257
+
258
+ // bad
259
+ let numbering = sequentialNumbering;
260
+ function setNumbering(next) {
261
+ numbering = next;
262
+ }
263
+ ```
264
+
265
+ ### Owned state
266
+
267
+ **A capability owns its state: built in `setup()`, started in `boot()`,
268
+ stopped in `shutdown()`. No module-level registry, and no module state
269
+ filled after construction.**
270
+
271
+ - A private memo cache that nothing fills after construction is fine, and so
272
+ is a private concurrency guard over a process-wide resource, with a comment
273
+ saying so.
274
+ - No late-bound closures between two tokens of one feature: the consumer
275
+ requires the other token.
276
+
277
+ Why: module state is shared by every runtime in the process and outlives
278
+ `shutdown()`.
279
+
280
+ ```js
281
+ // good
282
+ setup() {
283
+ return { jobs: createJobQueue() };
284
+ },
285
+ boot({ jobs }) {
286
+ jobs.start();
287
+ },
288
+
289
+ // bad
290
+ const handlers = new Map();
291
+ export function registerHandler(name, handler) {
292
+ handlers.set(name, handler);
293
+ }
294
+ ```
295
+
296
+ ### Shutdown mirrors setup
297
+
298
+ **`shutdown()` takes the same early returns as `setup()` and releases exactly
299
+ what `setup()` registered, with no `?.()` or `?? []` guard.**
300
+
301
+ - When a `register()` returns its release, keep it and call it at shutdown.
302
+
303
+ Why: a guarded release hides a setup that never ran, or one that ran twice.
304
+
305
+ ```js
306
+ // good
307
+ setup({ events }) {
308
+ if (!config.remindersEnabled) return {};
309
+ return { reminders: createReminders({ events }) };
310
+ },
311
+ shutdown({ reminders }) {
312
+ if (!config.remindersEnabled) return;
313
+ return reminders.stop();
314
+ }
315
+
316
+ // bad
317
+ shutdown({ reminders }) {
318
+ return reminders?.stop?.();
319
+ }
320
+ ```
321
+
322
+ ### Loggers
323
+
324
+ **The logger is required, typed `RuntimeLogger`, and called pino-style:
325
+ `logger.warn({ error, ...fields }, "Sentence.")`.**
326
+
327
+ - Only where a real caller may omit it does it default to the kernel's
328
+ `silentLogger` (`@jskit-ai/kernel/server/runtime/logger`). Never `logger =
329
+ null` with `if (!logger) return`, and never `logger = console`.
330
+ - The foundations' logger options serialize `error` like `err`, so the message
331
+ and stack are kept.
332
+
333
+ Why: one shape keeps every log line searchable by its fields.
334
+
335
+ ```js
336
+ // good
337
+ logger.warn({ error, invoiceId }, "The reminder failed; the next run retries it.");
338
+
339
+ // bad
340
+ logger?.warn?.(`Reminder failed: ${error.message}`);
341
+ ```
342
+
343
+ ## Actions
344
+
345
+ ### Action creators
346
+
347
+ **A feature's actions come from one `create<Name>Actions({ <capability> })`,
348
+ destructured, in `<name>Actions.js`. Each definition is a frozen literal
349
+ inside it, and so is the list.**
350
+
351
+ - Each id is a string literal or an `ACTION_*` module constant, exported for
352
+ the routes that run it.
353
+ - Actions of another concern live in a file named after that concern.
354
+ - The capability keeps the provider's name (`invoices`, not
355
+ `invoicesService`); only a second version of one capability gets a role name
356
+ (`unauditedInvoices`).
357
+
358
+ Why: a reader finds every action, and the method it reaches, in one place.
359
+
360
+ ```js
361
+ // good
362
+ const ACTION_LIST_INVOICES = "invoices.list";
363
+
364
+ /** @param {{ invoices: import("./types.js").Invoices }} dependencies */
365
+ function createInvoiceActions({ invoices }) {
366
+ return Object.freeze([
367
+ Object.freeze({
368
+ ...QUERY_ACTION,
369
+ id: ACTION_LIST_INVOICES,
370
+ input: listInvoicesInput,
371
+ execute: (input, context) => invoices.list(input, { context })
372
+ })
373
+ ]);
374
+ }
375
+
376
+ export { ACTION_LIST_INVOICES, createInvoiceActions };
377
+
378
+ // bad: positional, and built by a helper
379
+ function createInvoiceActions(invoices) {
380
+ return [queryAction("invoices.list", invoices.list)];
381
+ }
382
+ ```
383
+
384
+ ### One-line execute
385
+
386
+ **`execute: (input, context) => service.method(…)` makes exactly one call:
387
+ no `async` or `await`, no body, no envelope, no helper that builds or strips
388
+ it, and no `input || {}`.**
389
+
390
+ - The service returns the final result, its JSON:API document included.
391
+ - A key that must not reach the service is destructured in the parameters:
392
+ `execute: ({ workspaceSlug, ...changes }, context) => invoices.update(changes, { context })`.
393
+ - Flags go into the input or the context, never into a third argument.
394
+ - A CRUD custom action keeps its declaration's own
395
+ `execute: ({ input, context, service }) => service.method(…)`, and its map
396
+ key names it; never write an id equal to the default crud-core builds.
397
+
398
+ Why: each action shows the one method it reaches.
399
+
400
+ ```js
401
+ // good
402
+ execute: (input, context) => invoices.send(input, { context })
403
+
404
+ // bad
405
+ execute: async (input = {}, context) => {
406
+ const invoice = await invoices.send(input || {}, { context });
407
+ return { data: invoice };
408
+ }
409
+ ```
410
+
411
+ ### Shared action fields
412
+
413
+ **Fields many actions share (`version`, `kind`, `channels`, `surfaces`) come
414
+ from one frozen constant per kind, spread first into each literal. The other
415
+ keys follow in this order: `id, input, output, permission, events, execute`.**
416
+
417
+ - An application keeps one query and one command constant in its shared
418
+ package; a package defines its own only when its values differ.
419
+ - An empty `events` is left out.
420
+
421
+ Why: each literal reads complete, and one edit changes every action's
422
+ channels.
423
+
424
+ ```js
425
+ // good
426
+ const QUERY_ACTION = Object.freeze({
427
+ version: 1,
428
+ kind: "query",
429
+ channels: ["api", "automation", "internal"],
430
+ surfaces: ["app"]
431
+ });
432
+
433
+ // bad: half the fields in the feature's actionDefaults, half in each literal
434
+ defineFeature({ actionDefaults: { channels: ["api"] }, … });
435
+ Object.freeze({ id: ACTION_LIST_INVOICES, kind: "query", … });
436
+ ```
437
+
438
+ ### Strict action inputs
439
+
440
+ **An action's input is validated in create mode, the default: `required` is
441
+ enforced and defaults are applied. Only an action that updates part of a
442
+ record declares `mode: "patch"`; a command never does.**
443
+
444
+ - Patch mode skips `required`, so a service keeps its guard for a field a
445
+ patch input lists as required.
446
+
447
+ Why: a command whose required field is never enforced reaches the service
448
+ without it.
449
+
450
+ ```js
451
+ // good
452
+ input: { schema: sendInvoiceSchema } // a command: invoiceId is enforced
453
+ input: { schema: invoiceChangesSchema, mode: "patch" } // a partial update
454
+
455
+ // bad: a command in patch mode, so its required invoiceId is never enforced
456
+ input: { schema: sendInvoiceSchema, mode: "patch" }
457
+ ```
458
+
459
+ ### No idempotency
460
+
461
+ **An action declares no idempotency; the kernel refuses the field at startup.
462
+ An action that must tolerate a repeated request handles it in its own domain
463
+ (a unique key, an upsert, a state check) and says so in a comment where that
464
+ is not obvious.**
465
+
466
+ Why: a label nothing enforces promises a guarantee nobody keeps.
467
+
468
+ ```js
469
+ // good: the service returns an invoice that is already sent unchanged
470
+ execute: (input, context) => invoices.send(input, { context })
471
+
472
+ // bad
473
+ idempotency: "optional",
474
+ ```
475
+
476
+ ### The action policy
477
+
478
+ **A rule every action of an application obeys (who may run it, the record it
479
+ acts within, the scope it runs in) is data in each definition's
480
+ `extensions`, applied by the application's one `runtime.actions.policy`.
481
+ Never a helper that wraps every definition.**
482
+
483
+ - `input(definition)` runs once, when the action is registered, and returns
484
+ the input schema the action is validated and listed with.
485
+ - `execute({ definition, input, context, run })` runs on every execution,
486
+ after the kernel's channel, surface, permission and input checks, and calls
487
+ `run(input, context)` with what the action should receive. It can refuse,
488
+ adjust the input or context, or run the action inside a scope.
489
+ - The policy comes from a provider (a feature requires `runtime.actions`,
490
+ which would close a cycle), and passes every action whose `extensions` it
491
+ does not recognise straight to `run`.
492
+
493
+ Why: a wrapper makes every definition's `execute` the wrapper's, so neither a
494
+ reader nor the code map sees the call it makes.
495
+
496
+ ```js
497
+ // good
498
+ Object.freeze({ ...COMMAND_ACTION, id: ACTION_UPDATE_NOTE, extensions: { records: OWNER_ONLY }, execute: (input) => notes.update(input) })
499
+ const RecordScopePolicyProvider = defineProvider({
500
+ id: "app.action-policy",
501
+ provides: {
502
+ policy: "runtime.actions.policy"
503
+ },
504
+ setup: () => ({ policy: recordScopePolicy })
505
+ });
506
+
507
+ // bad
508
+ withRecordScope({ id: ACTION_UPDATE_NOTE, …, execute: (input) => notes.update(input) }, { ownerOnly: true })
509
+ ```
510
+
511
+ ### Request context
512
+
513
+ **A package reads the request, the actor and the workspace from an action
514
+ context one way, through the kernel's readers, and its actions pass the
515
+ context one way.**
516
+
517
+ - `resolveRequest(context)`, `resolveActor(context)` and
518
+ `resolveActorId(context)`, from
519
+ `@jskit-ai/kernel/server/actions/actionContributorHelpers`.
520
+ - The workspace comes from the context only. A record's workspace may be
521
+ checked against it, never used as a fallback.
522
+ - `?.` and `|| null` variants count as different forms.
523
+
524
+ Why: each form treats an absent actor differently, so one action behaves
525
+ differently depending on its path.
526
+
527
+ ```js
528
+ // good
529
+ const actorId = resolveActorId(context);
530
+
531
+ // bad
532
+ const actorId = context?.actor?.id || options.context?.user?.id || null;
533
+ ```
534
+
535
+ ## Services and repositories
536
+
537
+ ### Validated input as is
538
+
539
+ **A method that receives an action's validated input takes it as it is: no
540
+ `input = {}`, no `input?.x` and no second parse of what the schema checked.**
541
+
542
+ - Destructure with the schema's absent value: `{ name, note = "" }`.
543
+ - A contract operation other callers reach drops `= {}` and `?.` too, since
544
+ the contract types its input, and still validates what it receives.
545
+
546
+ Why: two validators drift apart, and the service seems to accept values the
547
+ action refuses.
548
+
549
+ ```js
550
+ // good
551
+ async function rename({ invoiceId, name }, { context }) {
552
+
553
+ // bad
554
+ async function rename(input = {}, options = {}) {
555
+ const name = String(input?.name || "").trim();
556
+ ```
557
+
558
+ ### Options and defaults
559
+
560
+ **Resolve each default once, at the feature or the public entry point; below
561
+ it, destructure and use.**
562
+
563
+ - An options bag whose fields are optional by contract keeps `options = {}`,
564
+ with no `?.` or second `|| {}` on it. Pass it on as `options`, or
565
+ `{ ...options, trx }` inside a transaction.
566
+ - A parameter or bag no caller passes is dropped and its values written in
567
+ place. A helper's argument takes no default unless a real caller omits it,
568
+ and a default never comes before a required parameter.
569
+ - Two behaviours are two methods: no boolean mode parameter and no `skip*`
570
+ flag. A construction-time setting, and a boolean that is column data
571
+ (`setArchived({ archived })` behind `archive` and `restore`), are fine.
572
+
573
+ Why: a value with two defaults has two meanings, and a mode flag hides a
574
+ second method inside the first.
575
+
576
+ ```js
577
+ // good
578
+ async function list(query, options = {}) {
579
+ return resolveRepoClient(knex, options)("invoices").where(query);
580
+ }
581
+
582
+ // bad
583
+ async function list(query = {}, options = {}) {
584
+ const client = options?.trx || knex;
585
+ }
586
+ async function save(invoice, skipAudit = false) {
587
+ ```
588
+
589
+ ### Frozen results
590
+
591
+ **Every `create*` returns `Object.freeze({ … })` of named inner functions.
592
+ Action definitions and their lists are frozen too.**
593
+
594
+ Why: a caller cannot replace a member, and each member has a name the code
595
+ map shows.
596
+
597
+ ```js
598
+ // good
599
+ function createInvoices({ knex }) {
600
+ async function list(query) { … }
601
+ async function send(input, options) { … }
602
+ return Object.freeze({
603
+ list,
604
+ send
605
+ });
606
+ }
607
+
608
+ // bad
609
+ return { list(query) { … }, send: async (input) => { … } };
610
+ ```
611
+
612
+ ### Named members
613
+
614
+ **A factory exposes another factory's functions by name, never by spreading
615
+ its result.**
616
+
617
+ - Spreading is for a `decorateService` or `decorateRepository` over the JSKIT
618
+ CRUD service or repository, whose members are the contract's, and for data.
619
+
620
+ Why: a spread hides where a member comes from, and a later spread silently
621
+ replaces an earlier member.
622
+
623
+ ```js
624
+ // good
625
+ return Object.freeze({
626
+ list: reads.list,
627
+ send: writes.send
628
+ });
629
+
630
+ // bad
631
+ return Object.freeze({ ...createInvoiceReads(dependencies), ...createInvoiceWrites(dependencies) });
632
+ ```
633
+
634
+ ### One helper per package
635
+
636
+ **Each pure helper exists once per package and is imported. A helper the
637
+ kernel owns is imported from it. A non-pure check two packages make the same
638
+ way (is this user an operator) is a member of its owner's capability.**
639
+
640
+ - The kernel's `@jskit-ai/kernel/shared/support/normalize` holds
641
+ `normalizeText` (every falsy value is empty), `toText` (only `null` and
642
+ `undefined` are), `isRecord` and their siblings; check each call site's
643
+ meaning of 0 and false before replacing a copy.
644
+ - A helper two packages of an application use lives in its shared package,
645
+ never inside one of them.
646
+
647
+ Why: copies drift, and the next reader cannot tell which one is right.
648
+
649
+ ```js
650
+ // good
651
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
652
+
653
+ // bad: a third copy, with its own idea of empty
654
+ function normalizeText(value) {
655
+ return String(value || "").trim();
656
+ }
657
+ ```
658
+
659
+ ### One reader per fact
660
+
661
+ **A fact that a call's options or context carry is read through its owner's
662
+ helper, never by hand.**
663
+
664
+ | Fact | Reader |
665
+ | --- | --- |
666
+ | the call's transaction, or the client to query through | `resolveTransaction(options)`, `resolveRepoClient(knex, options)` (`@jskit-ai/database-runtime/shared`) |
667
+ | what a service hands its repository | `repositoryCallOptions(options)` (`@jskit-ai/crud-core/server/jsonApiService`), only at the repository call |
668
+ | the signed-in user | `resolveActor(context)`, `resolveActorId(context)` |
669
+ | a tagged result's records, or its document | `simplifyJsonApiResult(result)`, `jsonApiResultValue(result)` (`@jskit-ai/http-runtime/shared`) |
670
+ | a collection's next-page cursor | `jsonApiNextCursor(document)` (`@jskit-ai/http-runtime/shared`) |
671
+ | a created record's id | `resolveCrudJsonApiEntityIdFromResult(result)` (`@jskit-ai/crud-core/server/serviceEvents`) |
672
+ | a CRUD record change's event | `buildCrudRecordChangedEvent({ namespace, operation, entityId, audience })` (`@jskit-ai/crud-core/server/serviceEvents`) |
673
+ | the workspace a request asks for, before any action context | `requestedWorkspaceScope(request)` (`@jskit-ai/workspaces-core/server/support/workspaceRouteInput`) |
674
+ | a context's workspace id | `resolveWorkspaceScopeValue(context)` (`@jskit-ai/json-rest-api-core/server/jsonRestApiHost`) |
675
+
676
+ Why: each hand-written copy treats a missing value its own way.
677
+
678
+ ```js
679
+ // good
680
+ const trx = resolveTransaction(options);
681
+
682
+ // bad
683
+ const trx = options?.trx || options?.transaction || null;
684
+ ```
685
+
686
+ ### Routes
687
+
688
+ **A route does HTTP work only (status, cookies, redirects) and runs an action
689
+ for anything that reads or writes the domain.**
690
+
691
+ - Registration lives in `registerRoutes.js`, or in `<concern>Routes.js`
692
+ exporting `register<Concern>Routes` when there are several.
693
+ - One handler shape: a named async function that sends what
694
+ `request.executeAction({ actionId: ACTION_X, input })` returns.
695
+ - A route that bypasses actions (a provider's callback) gets its own named
696
+ registrar. Summaries are sentences.
697
+
698
+ Why: an action is the one place a domain operation's policy, input and
699
+ events are declared.
700
+
701
+ ```js
702
+ // good
703
+ router.register("POST", "/api/invoices/:invoiceId/send", { auth: "required", meta: { summary: "Sends an invoice." } },
704
+ async function sendInvoice(request, reply) {
705
+ reply.code(200).send(await request.executeAction({ actionId: ACTION_SEND_INVOICE, input: request.input.params }));
706
+ });
707
+
708
+ // bad: the route reads storage itself
709
+ router.register("GET", "/api/invoices", {}, async (request, reply) => reply.send(await knex("invoices")));
710
+ ```
711
+
712
+ ## Errors
713
+
714
+ ### Refusals carry their own code
715
+
716
+ **A refusal is `new AppError(status, message, { code, params })`, or a
717
+ subclass of `AppError`, with a code of its own: one code per distinct
718
+ sentence, and `params` for the values in it.**
719
+
720
+ - One code convention per package (`billing.invoice.notFound`); codes are
721
+ stable and never numbered.
722
+ - Where callers branch on a shared code, keep it and add a `messageCode` (and
723
+ `messageParams`) per sentence. Input validation that carries per-field
724
+ `details` may share one code.
725
+ - An error class extends `AppError` and gives each refusal its own code. One
726
+ that takes extra fields destructures them and forwards the rest to `super`
727
+ (`constructor(message, { approval, ...options })`), so each construction
728
+ passes its own code. A project's coded builder, such as
729
+ `billingError(message, code)`, is fine when every call passes its own code. A function converting a caught error is
730
+ `to<What>Error(error, …)`; one returning refusal text is `*Message`.
731
+ - The kernel's `NotFoundError`, `ConflictError` and `DomainError` default to
732
+ one shared code, so each call passes its own.
733
+
734
+ Why: a client tells refusals apart by their code and shows each in the
735
+ user's language ([translation](/guide/framework/translation)).
736
+
737
+ ```js
738
+ // good
739
+ throw new AppError(409, "The invoice is already sent.", { code: "billing.invoice.alreadySent" });
740
+ throw new AppError(422, "The note is too long.", { code: "billing.note.tooLong", params: { max: 500 } });
741
+
742
+ // bad
743
+ throw new AppError(409, "The invoice is already sent.");
744
+ throw new NotFoundError("Invoice not found.");
745
+ ```
746
+
747
+ ### Internal failures are plain errors
748
+
749
+ **An internal invariant or an unexpected failure is `throw new Error(message)`.
750
+ The caller sees "Internal server error." and the message stays in the log.**
751
+
752
+ - JSKIT's HTTP handler shows an `AppError`'s message at any status, so an
753
+ `AppError` at 5xx is only for a failure the caller can act on.
754
+
755
+ Why: an internal failure dressed as a refusal leaks its details and promises
756
+ the caller a fix it cannot make.
757
+
758
+ ```js
759
+ // good
760
+ throw new Error(`Invoice ${invoiceId} has no ledger entry.`);
761
+
762
+ // bad
763
+ throw new AppError(500, `Invoice ${invoiceId} has no ledger entry.`, { code: "billing.ledger.missing" });
764
+ ```
765
+
766
+ ### One construction per error
767
+
768
+ **An error gets its status and code when it is constructed, never patched on
769
+ afterwards.**
770
+
771
+ Why: a patched `Error` is neither a coded refusal nor a hidden failure, and
772
+ the next edit drops the patch.
773
+
774
+ ```js
775
+ // good
776
+ throw new AppError(404, "Invoice was not found.", { code: "billing.invoice.notFound" });
777
+
778
+ // bad
779
+ throw Object.assign(new Error("Invoice was not found."), { statusCode: 404 });
780
+ ```
781
+
782
+ ### Failures are logged, not swallowed
783
+
784
+ **A best-effort `.catch` around a collaborator call logs the error and
785
+ carries on.**
786
+
787
+ - A silent catch stays only for cleanup on platform objects
788
+ (`response.body.cancel()`), for queue tails whose error reaches the queued
789
+ caller, and for client best effort, each with a short comment saying so.
790
+
791
+ Why: `() => {}` hides a wiring `TypeError` along with the failure.
792
+
793
+ ```js
794
+ // good
795
+ publisher.publish(event).catch((error) => logger.warn({ error, eventId: event.id }, "Publishing failed; the record is saved."));
796
+
797
+ // bad
798
+ publisher.publish(event).catch(() => {});
799
+ ```
800
+
801
+ ## Tests
802
+
803
+ ### Doubles use documented seams
804
+
805
+ **A test double replaces an owner only through its documented seams (an
806
+ injected collaborator, a provider client, an option), with the real shape.
807
+ It never copies an internal protocol such as an id format or a stream
808
+ frame.**
809
+
810
+ - Complete a double with the method the code calls, returning its neutral
811
+ value; never shape production code around a double.
812
+
813
+ Why: a double that mirrors internals pins them, so the test breaks on a
814
+ refactor and passes on a real bug.
815
+
816
+ ```js
817
+ // good
818
+ const invoices = createInvoices({ knex, mailer: { send: async () => ({ accepted: true }) } });
819
+
820
+ // bad: production code that exists for the double
821
+ if (typeof mailer.send !== "function") return;
822
+ ```
823
+
824
+ ### Tests are not callers
825
+
826
+ **A parameter, option or default that only tests pass, or a method only test
827
+ doubles lack, is no reason to keep it: drop the seam and complete the
828
+ double.**
829
+
830
+ - One named clock per factory (`now = Date.now`) is a platform default, not a
831
+ test seam.
832
+
833
+ Why: a seam kept for tests is a shape no real caller uses, and a reader
834
+ cannot tell it apart from one that matters.
835
+
836
+ ```js
837
+ // good
838
+ function createReminders({ mailer, now = Date.now }) {
839
+
840
+ // bad
841
+ function createReminders({ mailer, now = Date.now, skipDelay = false, testHooks = {} }) {
842
+ ```
843
+
844
+ ### No test-only exports
845
+
846
+ **A module exports only what other production modules import; an actions
847
+ file exports its creator and its `ACTION_*` ids. Tests reach everything else
848
+ through the feature, and pure helpers through `__testables`.**
849
+
850
+ - `__testables` holds pure helpers only, never one that touches a
851
+ collaborator or reads request facts.
852
+
853
+ Why: a test-only export widens the module's API and invites production
854
+ callers.
855
+
856
+ ```js
857
+ // good
858
+ const __testables = Object.freeze({ nextInvoiceNumber });
859
+ export { createInvoices, __testables };
860
+
861
+ // bad
862
+ export function resetInvoiceCacheForTests() {
863
+ cache.clear();
864
+ }
865
+ ```
866
+
867
+ ## Files and layout
868
+
869
+ ### Size limits
870
+
871
+ **Files stay under 750 lines and functions under 600, in all hand-written
872
+ code: server, shared, client (`.vue` and `.js`) and scripts. One concern per
873
+ factory.**
874
+
875
+ Why: a reader holds one concern at a time; a 2,000-line closure holds ten.
876
+
877
+ ```text
878
+ good: invoices.js (list, send), invoiceNumbering.js, invoiceReminders.js
879
+ bad: invoices.js, 1,900 lines: listing, sending, numbering, reminders and PDF rendering
880
+ ```
881
+
882
+ ### Data modules
883
+
884
+ **A generated or static data module (a catalogue, a lookup table) is not
885
+ code. It is exempt from the size limits, and its first line says it is data
886
+ and how it is produced.**
887
+
888
+ Why: the limits measure code a person reads; a table is scanned, not read.
889
+
890
+ ```js
891
+ // good
892
+ /* eslint-disable max-lines -- data: the country list, generated by scripts/build-countries.mjs */
893
+
894
+ // bad: a module of functions excused as data
895
+ /* eslint-disable max-lines -- data: invoice helpers */
896
+ ```
897
+
898
+ ### Formatter
899
+
900
+ **An application has one formatter config, `formatterConfig(globs)`: width
901
+ 140, semicolons, double quotes, no trailing commas, one property per line in
902
+ an object that spans lines, braces once an `if` spans lines.** `eslint --fix`
903
+ applies it.
904
+
905
+ - A returned frozen result and a `requires`, `optional` or `provides` map put
906
+ each property on its own line. A short leaf literal of at most three scalar
907
+ properties (`{ type: "string", required: true }`) may stay on one line.
908
+ - Long prose stays inline in its string or template; never split a sentence
909
+ across `+` to fit the width.
910
+
911
+ Why: one layout lets a diff show only what changed.
912
+
913
+ ```js
914
+ // good
915
+ return Object.freeze({
916
+ list,
917
+ send
918
+ });
919
+
920
+ // bad
921
+ return Object.freeze({ list, send });
922
+ ```
923
+
924
+ ### Names
925
+
926
+ **Factories and action creators are `create*`; a pure value builder is
927
+ `build*` or `to*`; a domain operation that creates a thing keeps `create*`
928
+ (`createInvoice`). Files are named for what they hold: `<Name>Feature.js`,
929
+ `<Name>Provider.js`, `<name>Runtime.js`, `<name>Actions.js`,
930
+ `<name>Routes.js`, `types.js`.**
931
+
932
+ - Sibling factories are named alike (`createInvoiceReads`,
933
+ `createInvoiceWrites`). An error builder keeps its `*Error` suffix.
934
+
935
+ Why: a name says what a call returns before anyone opens the file.
936
+
937
+ ```js
938
+ // good
939
+ function buildInvoiceTotals(lines) {
940
+
941
+ // bad: a pure builder named like a factory
942
+ function createInvoiceTotals(lines) {
943
+ ```
944
+
945
+ ### Comments
946
+
947
+ **A function's description is a `/** … */` block, with the description
948
+ inside it. Comments describe the code, never its history or its tests.**
949
+
950
+ - A single-factory file puts its description in the factory's JSDoc, not in a
951
+ `//` file header.
952
+ - No "was", "now" or "until X is converted", no dead probes, and no shim left
953
+ for a removed option.
954
+
955
+ Why: a comment that narrates history is wrong the day after it is written.
956
+
957
+ ```js
958
+ // good
959
+ /**
960
+ * Sends an invoice; a repeated send returns it unchanged.
961
+ * @param {{ invoiceId: string }} input
962
+ */
963
+
964
+ // bad
965
+ // Sends an invoice (moved here from billing.js)
966
+ /** @param {{ invoiceId: string }} input */
967
+ ```
968
+
969
+ ## Checks
970
+
971
+ ### Running the checks
972
+
973
+ `npm run lint` runs ESLint with `@jskit-ai/config-eslint`, and `npm run
974
+ verify` runs `jskit check` (the package graph), lint, the tests and the
975
+ build. The foundations' `eslint.config.mjs` already holds a new
976
+ application's code to every check:
977
+
978
+ ```js
979
+ import {
980
+ baseConfig,
981
+ clientFeaturesConfig,
982
+ formatterConfig,
983
+ nodeConfig,
984
+ sizeLimitsConfig,
985
+ typedFeaturesConfig,
986
+ vueConfig,
987
+ webConfig
988
+ } from "@jskit-ai/config-eslint/server";
989
+
990
+ const SERVER_SOURCES = ["packages/*/src/server/**", "packages/*/src/shared/**", "server/**", "server.js"];
991
+ const CLIENT_SOURCES = ["packages/*/src/client/**", "src/**"];
992
+ const HAND_WRITTEN_SOURCES = ["packages/*/src/**", "src/**", "config/**", "server/**", "server.js", "bin/**"];
993
+
994
+ export default [
995
+ ...baseConfig,
996
+ ...typedFeaturesConfig(SERVER_SOURCES),
997
+ ...sizeLimitsConfig(HAND_WRITTEN_SOURCES),
998
+ ...vueConfig,
999
+ ...clientFeaturesConfig(CLIENT_SOURCES),
1000
+ ...webConfig,
1001
+ ...nodeConfig,
1002
+ ...formatterConfig([...SERVER_SOURCES, "config/**", "bin/**"])
1003
+ ];
1004
+ ```
1005
+
1006
+ - `typedFeaturesConfig(globs)` makes every `jskit/*` rule and the size limits
1007
+ errors for the server and shared code the globs select (tests and client
1008
+ directories excluded).
1009
+ - `sizeLimitsConfig(globs)` holds the rest of the hand-written code, client
1010
+ code included, to the size limits.
1011
+ - `formatterConfig(globs)` is the formatter.
1012
+ - `baseConfig` reports the `jskit/*` rules as warnings on any
1013
+ `**/src/server/**`, so an existing application sees them before it lists a
1014
+ package; it lists each package as it converts it.
1015
+ - `clientFeaturesConfig(globs)` holds client code to the same rules; see
1016
+ [client features](/guide/framework/client-features#running-the-checks).
1017
+
1018
+ Every finding ends with a link to the section of this page that explains it.
1019
+
1020
+ ### Lint rules
1021
+
1022
+ | Rule | Reports | Section |
1023
+ | --- | --- | --- |
1024
+ | `jskit/typed-dependencies` | a factory or constructor that receives collaborators without a JSDoc `@param` type | [Typed collaborators](#typed-collaborators) |
1025
+ | `jskit/required-collaborators` | an absent default on a collaborator, a `typeof … === "function"` probe, an optional call (`x.y?.()`, `received?.()`, `receivedService?.y()`) | [Required collaborators](#required-collaborators) |
1026
+ | `jskit/no-dual-shape` | a value accepted in two shapes, `x.service \|\| x` or `x?.value ?? x` | [One shape per value](#one-shape-per-value) |
1027
+ | `jskit/no-env-default` | `env = process.env`, `env \|\| process.env`, `env ?? process.env` | [Environment from the caller](#environment-from-the-caller) |
1028
+ | `jskit/no-slot-setter` | a `set<Name>()` that fills a slot after construction | [Optional tokens](#optional-tokens) |
1029
+ | `jskit/no-module-registry` | a module-level `register<Name>()` that writes module state | [Owned state](#owned-state) |
1030
+ | `jskit/named-repeated-type` | a `ReturnType<…>` or `X["y"]` JSDoc type written in two or more files of a package | [Capability types](#capability-types) |
1031
+ | `jskit/action-creator` | an action definition outside a `create<Name>Actions` creator, or a creator that takes its capabilities positionally | [Action creators](#action-creators) |
1032
+ | `jskit/action-execute-shape` | an `execute` that is not one arrow making one call: a body, `execute: helper(…)`, a built result | [One-line execute](#one-line-execute) |
1033
+ | `jskit/one-line-action` | `input = {}`, `input \|\| {}` or `input?.` in an action, an `async` `execute`, an unfrozen definition or list | [One-line execute](#one-line-execute) |
1034
+ | `jskit/no-idempotency` | an `idempotency` field on an action definition or its shared-field constant | [No idempotency](#no-idempotency) |
1035
+ | `jskit/no-optional-after-default` | `options?.` or a second `options \|\| {}` on a parameter that has a default | [Options and defaults](#options-and-defaults) |
1036
+ | `jskit/frozen-factory-result` | a `create*` result that is not frozen, or a member defined inline in it | [Frozen results](#frozen-results) |
1037
+ | `jskit/no-factory-spread` | a `create*()` result spread into a factory's API or a token value | [Named members](#named-members) |
1038
+ | `jskit/no-duplicate-helper` | a small helper, function or arrow, repeated in another file of the package | [One helper per package](#one-helper-per-package) |
1039
+ | `jskit/app-error-code` | an `AppError`, `DomainError`, `ConflictError` or `NotFoundError` (or a subclass the module declares) without its own code, and one code naming two sentences in a module | [Refusals carry their own code](#refusals-carry-their-own-code) |
1040
+ | `jskit/no-patched-error` | `Object.assign(new Error(…), { statusCode })`, or an error builder's result given its status afterwards | [One construction per error](#one-construction-per-error) |
1041
+ | `jskit/no-test-only-export` | an export whose name says only tests use it (`…ForTests`, `__reset…`; `__testables` aside) | [No test-only exports](#no-test-only-exports) |
1042
+ | `jskit/one-property-per-line` | two properties on one line in a returned `Object.freeze({ … })` or a `requires` / `optional` / `provides` map (`eslint --fix` splits them) | [Formatter](#formatter) |
1043
+ | `jskit/jsdoc-description-inside` | a `//` description directly above a `/** */` block | [Comments](#comments) |
1044
+ | `jskit/allow-comment-form` | a `readable-check: allow` reason without a prefix from the table below, or one that cites tests or the state of a project | [Exceptions](#exceptions) |
1045
+ | `max-lines`, `max-lines-per-function` | files over 750 lines, functions over 600 | [Size limits](#size-limits) |
1046
+
1047
+ The rules see shapes, not intent. Feature files, owned state beyond
1048
+ registries, shutdown, loggers, shared action fields, strict inputs, the action
1049
+ policy, request context, validated input, one reader per fact, routes,
1050
+ internal failures, swallowed failures, test doubles, tests as callers, data
1051
+ modules and names are checked by reading.
1052
+
1053
+ ### Exceptions
1054
+
1055
+ A justified exception carries a comment on the line before it, naming the
1056
+ rule or the finding, with a reason that says what is true of the code, never
1057
+ the state of a project or its tests:
1058
+
1059
+ ```js
1060
+ // readable-check: allow method-probe - input validation: an application supplies the serializer builder
1061
+ if (typeof buildSerializer !== "function") {
1062
+ ```
1063
+
1064
+ | Reason prefix | The exception |
1065
+ | --- | --- |
1066
+ | `input validation:` | a public library entry point checks a value an application passes it, and refuses a wrong one with its own error |
1067
+ | `data:` | probes or defaults data (a route definition, a transport, a parsed record, an options list), not a collaborator; it never excuses a function itself, such as a repeated helper |
1068
+ | `platform object:` | probes a host or library object reached internally, such as `Headers`, a fetch `Response` or a Knex transaction (`platform object: a Knex transaction`) |
1069
+ | `optional operation:` | calls a contract method that some real implementer lacks, which the contract marks optional |
1070
+ | `safety invariant: <what goes wrong>` | keeps a guard that protects an internal invariant, saying what goes wrong without it; the retired `test asserts error:` is reported |
1071
+ | `not a slot:` | matched a name that fills no slot |
1072
+ | `cycle: <supplier> requires <owner>` | keeps a setter because the slot's supplier requires its owner |
1073
+ | `deferred until boot: <reason>` | keeps a capability handed out at setup and filled in `boot()`, such as the deferred `auth.service`: a two-phase value that is neither a cycle nor a setter |
1074
+ | `filled by <repository>; convert with its supplier` | keeps a setter that another repository's code calls |
1075
+ | `cross-repository slot <token>` | keeps a probe for a token another repository supplies |
1076
+ | `shared resource:` | keeps client module state that every instance on the page shares on purpose ([client features](/guide/framework/client-features#module-state)) |
1077
+
1078
+ One kind of probe gets one prefix and one wording everywhere, and a bare
1079
+ `typeof x` guard of that kind carries the same comment even where no rule
1080
+ reports it. The size limits are ESLint's own rules: excuse them with
1081
+ `// eslint-disable-next-line max-lines-per-function -- <reason>`, or a data
1082
+ module's header.
1083
+
1084
+ ## A whole feature
1085
+
1086
+ The `server/feature-package` pattern is a complete feature that passes every
1087
+ check. Its layout:
1088
+
1089
+ ```text
1090
+ packages/booking-engine/
1091
+ package.json exports ./server/types; lists the capability it provides
1092
+ src/server/
1093
+ types.js BookingEngine, BookingEngineAvailability
1094
+ BookingEngineFeature.js optional slot, provided engine, setup, actions
1095
+ bookingEngine.js createBookingEngine({ availability }): frozen named functions
1096
+ bookingAvailability.js everyScopeOpen, the slot's named default
1097
+ bookingEngineActions.js BOOKING_QUERY and createBookingEngineActions({ bookingEngine })
1098
+ inputSchemas.js the actions' input validators
1099
+ ```
1100
+
1101
+ Following a call takes one step each time: a route names
1102
+ `ACTION_GET_STATUS`; that action's `execute` calls `bookingEngine.readStatus`;
1103
+ `BookingEngine` is `ReturnType<typeof createBookingEngine>`, so the call
1104
+ reaches `bookingEngine.js`.