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