@cosmicdrift/kumiko-framework 0.362.0 → 0.364.0

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 (259) hide show
  1. package/AGENTS.md +307 -0
  2. package/agents/examples/access-control.md +355 -0
  3. package/agents/examples/anonymous-access-multitenant.md +443 -0
  4. package/agents/examples/anonymous-access.md +300 -0
  5. package/agents/examples/apex-landing.md +528 -0
  6. package/agents/examples/apex-surface-auth.md +355 -0
  7. package/agents/examples/api-worker-split.md +434 -0
  8. package/agents/examples/auth-claims.md +351 -0
  9. package/agents/examples/auth-foundation-providers.md +155 -0
  10. package/agents/examples/auth-mfa.md +323 -0
  11. package/agents/examples/basic-entity.md +420 -0
  12. package/agents/examples/boot-check.md +122 -0
  13. package/agents/examples/chat-channels.md +254 -0
  14. package/agents/examples/compliance-profiles-demo.md +307 -0
  15. package/agents/examples/cross-feature-events.md +317 -0
  16. package/agents/examples/crypto-shredding-hr.md +314 -0
  17. package/agents/examples/currencies-global.md +609 -0
  18. package/agents/examples/currencies-per-tenant.md +453 -0
  19. package/agents/examples/custom-fields-basic.md +314 -0
  20. package/agents/examples/custom-handlers.md +397 -0
  21. package/agents/examples/delivery-notifications.md +432 -0
  22. package/agents/examples/designer-demo.md +86 -0
  23. package/agents/examples/e2e-generator.md +211 -0
  24. package/agents/examples/embedded-entity-form.md +598 -0
  25. package/agents/examples/embedded.md +649 -0
  26. package/agents/examples/encrypted-tenant-config.md +405 -0
  27. package/agents/examples/error-contract.md +539 -0
  28. package/agents/examples/event-sourcing.md +1287 -0
  29. package/agents/examples/feature-toggles.md +263 -0
  30. package/agents/examples/field-access.md +271 -0
  31. package/agents/examples/files-post-processing.md +433 -0
  32. package/agents/examples/i18n.md +166 -0
  33. package/agents/examples/idempotency.md +179 -0
  34. package/agents/examples/inbound-mail-triage.md +315 -0
  35. package/agents/examples/lane-routing.md +246 -0
  36. package/agents/examples/ledger-banking.md +214 -0
  37. package/agents/examples/ledger-invoicing.md +235 -0
  38. package/agents/examples/legal-pages.md +393 -0
  39. package/agents/examples/lifecycle-hooks.md +368 -0
  40. package/agents/examples/managed-config.md +428 -0
  41. package/agents/examples/managed-pages.md +353 -0
  42. package/agents/examples/notes-history-basic.md +345 -0
  43. package/agents/examples/ownership.md +396 -0
  44. package/agents/examples/pdf-audit-export.md +270 -0
  45. package/agents/examples/pipeline-basics.md +792 -0
  46. package/agents/examples/public-share-token.md +351 -0
  47. package/agents/examples/rate-limiting.md +211 -0
  48. package/agents/examples/realtime-sse.md +187 -0
  49. package/agents/examples/record-detail-layout.md +297 -0
  50. package/agents/examples/reference-data.md +151 -0
  51. package/agents/examples/relations.md +280 -0
  52. package/agents/examples/screens-nav.md +353 -0
  53. package/agents/examples/search.md +233 -0
  54. package/agents/examples/secrets-demo.md +417 -0
  55. package/agents/examples/security-baseline.md +187 -0
  56. package/agents/examples/seed-migration.md +99 -0
  57. package/agents/examples/session-revocation.md +259 -0
  58. package/agents/examples/state-machine.md +920 -0
  59. package/agents/examples/tags-basic.md +401 -0
  60. package/agents/examples/tenant-isolation.md +173 -0
  61. package/agents/examples/tenant-settings.md +275 -0
  62. package/agents/examples/tier-admin.md +347 -0
  63. package/agents/examples/timezones.md +366 -0
  64. package/agents/examples/user-data-rights.md +410 -0
  65. package/agents/examples/user-profile.md +226 -0
  66. package/agents/examples/webhook-step.md +597 -0
  67. package/agents/examples/wizard-form.md +379 -0
  68. package/agents/examples/workflow-engine.md +1731 -0
  69. package/agents/examples/writeform-section.md +300 -0
  70. package/agents/examples.md +74 -0
  71. package/agents/features.md +768 -0
  72. package/agents/widgets.md +181 -0
  73. package/dist/api/auth-middleware.d.ts +2 -0
  74. package/dist/api/auth-middleware.js +14 -5
  75. package/dist/api/auth-routes.js +5 -3
  76. package/dist/api/extra-route.d.ts +3 -0
  77. package/dist/api/index.d.ts +2 -2
  78. package/dist/api/index.js +1 -1
  79. package/dist/api/redis-sse-broker.js +10 -8
  80. package/dist/api/routes.js +48 -19
  81. package/dist/api/server.d.ts +2 -1
  82. package/dist/api/server.js +18 -6
  83. package/dist/api/session-cookies.d.ts +1 -0
  84. package/dist/api/session-cookies.js +20 -5
  85. package/dist/api/sse-route.js +16 -1
  86. package/dist/bun-db/index.d.ts +1 -1
  87. package/dist/bun-db/index.js +1 -1
  88. package/dist/bun-db/query.d.ts +12 -5
  89. package/dist/bun-db/query.js +88 -94
  90. package/dist/crypto/kek-source.js +5 -3
  91. package/dist/crypto/pii-field-encryption.d.ts +4 -1
  92. package/dist/crypto/pii-field-encryption.js +2 -2
  93. package/dist/db/apply-entity-event.js +2 -2
  94. package/dist/db/collect-table-metas.js +11 -7
  95. package/dist/db/cursor.js +2 -1
  96. package/dist/db/dialect.d.ts +1 -0
  97. package/dist/db/dialect.js +28 -13
  98. package/dist/db/eagerload.js +2 -2
  99. package/dist/db/entity-table-from-registry.js +1 -1
  100. package/dist/db/entity-table-meta.js +5 -13
  101. package/dist/db/event-store-executor-context.d.ts +5 -6
  102. package/dist/db/event-store-executor-context.js +29 -27
  103. package/dist/db/event-store-executor-read.js +40 -25
  104. package/dist/db/event-store-executor-write.js +18 -20
  105. package/dist/db/index.d.ts +1 -1
  106. package/dist/db/index.js +1 -1
  107. package/dist/db/lock-guard.d.ts +4 -0
  108. package/dist/db/lock-guard.js +14 -0
  109. package/dist/db/migrate-generator.js +6 -4
  110. package/dist/db/migrate-runner.js +32 -35
  111. package/dist/db/parent-ref-clause.js +2 -2
  112. package/dist/db/queries/backfill-pii.js +11 -9
  113. package/dist/db/queries/es-ops.js +1 -1
  114. package/dist/db/queries/event-consumer.js +17 -17
  115. package/dist/db/queries/event-store-admin.js +6 -6
  116. package/dist/db/queries/event-store-transfer.js +2 -2
  117. package/dist/db/queries/event-store.js +2 -2
  118. package/dist/db/queries/pii-backfill-state.js +2 -2
  119. package/dist/db/queries/raw-sql.js +1 -1
  120. package/dist/db/queries/test-stack.js +1 -1
  121. package/dist/db/reference-data.js +3 -5
  122. package/dist/db/table-builder.js +6 -4
  123. package/dist/db/table-introspection.d.ts +16 -0
  124. package/dist/db/table-introspection.js +76 -0
  125. package/dist/db/tenant-db-runner.d.ts +4 -0
  126. package/dist/db/tenant-db-runner.js +17 -0
  127. package/dist/db/tenant-db.d.ts +5 -2
  128. package/dist/db/tenant-db.js +34 -14
  129. package/dist/derivatives/derivatives-context.js +1 -1
  130. package/dist/derivatives/variant-key.js +2 -1
  131. package/dist/engine/boot-validator/access-declarations.js +2 -2
  132. package/dist/engine/boot-validator/api-ext.js +3 -3
  133. package/dist/engine/boot-validator/boot-check.js +1 -1
  134. package/dist/engine/boot-validator/config-deps.js +13 -13
  135. package/dist/engine/boot-validator/detail-screens.js +1 -1
  136. package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
  137. package/dist/engine/boot-validator/entity-handler.js +84 -35
  138. package/dist/engine/boot-validator/entity-list-screens.js +3 -3
  139. package/dist/engine/boot-validator/handler-field-references.js +19 -9
  140. package/dist/engine/boot-validator/i18n-keys.js +2 -2
  141. package/dist/engine/boot-validator/nav.js +1 -1
  142. package/dist/engine/boot-validator/pii-retention.js +9 -9
  143. package/dist/engine/boot-validator/projection-list-screens.js +5 -5
  144. package/dist/engine/boot-validator/query-output-columns.js +7 -7
  145. package/dist/engine/boot-validator/screens.d.ts +2 -1
  146. package/dist/engine/boot-validator/screens.js +155 -96
  147. package/dist/engine/boot-validator/url-prefill-fields.js +54 -35
  148. package/dist/engine/changeset-changes.js +3 -3
  149. package/dist/engine/define-handler.d.ts +2 -2
  150. package/dist/engine/define-handler.js +11 -7
  151. package/dist/engine/embedded-derived.js +10 -6
  152. package/dist/engine/extensions/escape-hatch-usage.js +2 -1
  153. package/dist/engine/factories.js +5 -6
  154. package/dist/engine/feature-ast/extractors/events.js +43 -33
  155. package/dist/engine/feature-ast/extractors/handlers.js +2 -0
  156. package/dist/engine/feature-ast/patcher.d.ts +3 -3
  157. package/dist/engine/feature-ast/patcher.js +3 -3
  158. package/dist/engine/feature-ast/pattern-change-schema.d.ts +10 -8
  159. package/dist/engine/feature-ast/pattern-change-schema.js +4 -3
  160. package/dist/engine/feature-ast/patterns.d.ts +3 -3
  161. package/dist/engine/feature-ast/render.js +16 -5
  162. package/dist/engine/feature-changelog.d.ts +2 -1
  163. package/dist/engine/feature-changelog.js +8 -6
  164. package/dist/engine/feature-config-events-jobs.d.ts +17 -7
  165. package/dist/engine/feature-config-events-jobs.js +20 -5
  166. package/dist/engine/feature-entity-handlers.d.ts +2 -2
  167. package/dist/engine/feature-entity-handlers.js +12 -3
  168. package/dist/engine/feature-ui-extensions.js +2 -3
  169. package/dist/engine/handler-field-references.d.ts +21 -2
  170. package/dist/engine/handler-field-references.js +55 -17
  171. package/dist/engine/index.d.ts +3 -2
  172. package/dist/engine/index.js +1 -0
  173. package/dist/engine/ownership.js +7 -20
  174. package/dist/engine/pattern-library/mixed-schemas.js +0 -1
  175. package/dist/engine/registry-ingest.d.ts +2 -1
  176. package/dist/engine/registry-ingest.js +85 -2
  177. package/dist/engine/registry-validate.d.ts +1 -0
  178. package/dist/engine/registry-validate.js +38 -8
  179. package/dist/engine/registry.js +2 -1
  180. package/dist/engine/schema-builder.js +3 -4
  181. package/dist/engine/soft-delete-cleanup.js +12 -4
  182. package/dist/engine/steps/call-feature.js +1 -3
  183. package/dist/engine/steps/retry.js +2 -1
  184. package/dist/engine/steps/unsafe-projection-delete.js +2 -2
  185. package/dist/engine/steps/unsafe-projection-upsert.js +4 -15
  186. package/dist/engine/template-variables.d.ts +32 -0
  187. package/dist/engine/template-variables.js +80 -0
  188. package/dist/engine/tier-resolver-extension.js +2 -3
  189. package/dist/engine/types/index.d.ts +2 -2
  190. package/dist/engine/validate-projection-allowlist.js +12 -13
  191. package/dist/engine/where-rule-lint.js +8 -7
  192. package/dist/env/_zod-introspect.d.ts +1 -0
  193. package/dist/errors/classes.js +1 -0
  194. package/dist/errors/kumiko-error.d.ts +2 -0
  195. package/dist/errors/kumiko-error.js +2 -0
  196. package/dist/errors/reasons.d.ts +1 -0
  197. package/dist/errors/reasons.js +2 -0
  198. package/dist/errors/write-error-info.js +2 -0
  199. package/dist/event-store/tenant-db-append.js +2 -2
  200. package/dist/files/assert-file-refs-belong-to-tenant.d.ts +4 -5
  201. package/dist/files/assert-file-refs-belong-to-tenant.js +7 -15
  202. package/dist/files/bind-pending-file-refs.d.ts +2 -0
  203. package/dist/files/bind-pending-file-refs.js +3 -3
  204. package/dist/files/bind-referenced-files.write.d.ts +1 -1
  205. package/dist/files/bind-referenced-files.write.js +22 -13
  206. package/dist/files/create-file-ref-from-bytes.d.ts +6 -3
  207. package/dist/files/create-file-ref-from-bytes.js +67 -4
  208. package/dist/files/create-file-ref.d.ts +22 -0
  209. package/dist/files/create-file-ref.js +53 -25
  210. package/dist/files/file-handle.d.ts +1 -1
  211. package/dist/files/file-handle.js +8 -0
  212. package/dist/files/file-routes.js +2 -2
  213. package/dist/files/index.d.ts +2 -2
  214. package/dist/files/index.js +1 -1
  215. package/dist/files/load-save-file-refs.d.ts +10 -0
  216. package/dist/files/load-save-file-refs.js +47 -0
  217. package/dist/files/local-provider.js +2 -1
  218. package/dist/files/purge-expired-file-ref-bytes.d.ts +4 -1
  219. package/dist/files/purge-expired-file-ref-bytes.js +45 -19
  220. package/dist/i18n/index.d.ts +1 -1
  221. package/dist/i18n/index.js +1 -1
  222. package/dist/i18n/mail-registry.d.ts +2 -0
  223. package/dist/i18n/mail-registry.js +7 -0
  224. package/dist/jobs/index.d.ts +1 -1
  225. package/dist/jobs/job-runner.d.ts +4 -1
  226. package/dist/jobs/job-runner.js +13 -9
  227. package/dist/observability/escape-hatch-report.d.ts +1 -1
  228. package/dist/observability/escape-hatch-report.js +17 -5
  229. package/dist/pipeline/cascade-handler.js +5 -2
  230. package/dist/pipeline/dispatch-batch.js +79 -31
  231. package/dist/pipeline/dispatch-query.js +2 -3
  232. package/dist/pipeline/dispatch-shared.d.ts +7 -2
  233. package/dist/pipeline/dispatch-shared.js +142 -34
  234. package/dist/pipeline/dispatch-write.js +34 -22
  235. package/dist/pipeline/event-dispatcher-delivery.js +2 -3
  236. package/dist/pipeline/index.d.ts +2 -0
  237. package/dist/pipeline/index.js +2 -0
  238. package/dist/pipeline/member-read-only-transaction.js +1 -1
  239. package/dist/pipeline/notify-scope.d.ts +13 -0
  240. package/dist/pipeline/notify-scope.js +32 -0
  241. package/dist/pipeline/pre-transaction-reservations.d.ts +3 -1
  242. package/dist/pipeline/pre-transaction-reservations.js +28 -9
  243. package/dist/pipeline/system-hooks.js +2 -1
  244. package/dist/pipeline/system-identity-switch.js +2 -2
  245. package/dist/rate-limit/bucket.d.ts +3 -3
  246. package/dist/rate-limit/bucket.js +14 -4
  247. package/dist/redis/cache-sync-bus.js +9 -3
  248. package/dist/search/meilisearch-adapter.js +39 -13
  249. package/dist/secrets/stored-envelope.js +7 -7
  250. package/dist/stack/push-entity-projection-tables.js +2 -3
  251. package/dist/stack/table-helpers.js +10 -26
  252. package/dist/stack/test-stack.js +2 -5
  253. package/dist/testing/multipart-helper.d.ts +0 -6
  254. package/dist/testing/multipart-helper.js +5 -5
  255. package/dist/testing/without-ambient-temporal.js +4 -4
  256. package/dist/utils/is-plain-object.d.ts +1 -2
  257. package/dist/utils/is-plain-object.js +1 -4
  258. package/package.json +7 -5
  259. package/src/changes.json +251 -8
@@ -0,0 +1,379 @@
1
+ <!-- Generated by `bun run gen:agent-docs` from samples/recipes/wizard-form. Do not edit. -->
2
+
3
+ # wizard-form
4
+
5
+ EditLayout.mode: "wizard" — a multi-step entity form with a review step that reads live form values through an extension section, and draft: true resuming the in-progress form via the bundled form-draft feature.
6
+
7
+ ## README
8
+
9
+ # Wizard form
10
+
11
+ An `entityEdit` screen with `EditLayout.mode: "wizard"` — one section per
12
+ step instead of one long form. The final step is a review section
13
+ (`kind: "extension"`) that reads the wizard's live values instead of
14
+ re-fetching them, and `draft: true` resumes an abandoned wizard from where
15
+ the user left off.
16
+
17
+ ## What it shows
18
+
19
+ - **`mode: "wizard"`** — the same three sections a `"single"` layout would
20
+ render all at once instead render one per step, with a progress
21
+ indicator and per-step validation. The boot-validator requires at least
22
+ two sections, each with a non-empty `title`.
23
+ - **A review step as an extension section** — the third step
24
+ (`kind: "extension"`) mounts `ListingReviewSection`, a client component
25
+ resolved by the `__component` name at render time. It receives the host
26
+ form's current values through `ExtensionSectionProps.values` (the same
27
+ live snapshot the other steps edit) and renders them read-only via
28
+ `DetailList` — no second fetch, no duplicated state.
29
+ - **`draft: true`** — persists the in-progress wizard as a resumable draft.
30
+ This flag only does two things in the feature: it requires
31
+ `mode: "wizard"` and it requires the bundled `form-draft` feature to be
32
+ mounted alongside this one (both enforced at boot). The actual
33
+ save/resume/discard wiring happens automatically inside `RenderEdit` —
34
+ this recipe never calls a form-draft handler from its own code.
35
+
36
+ ## Feature composition
37
+
38
+ ```
39
+ listing — the entity being created through the wizard
40
+ form-draft — bundled feature backing draft: true (mounted, never called
41
+ directly by this feature — RenderEdit wires it client-side)
42
+ config — form-draft requires this for its retention-days setting;
43
+ mount createConfigFeature() alongside form-draft
44
+ ```
45
+
46
+ ## Flow
47
+
48
+ 1. A user opens the `listing-wizard` screen and fills in the "Basics"
49
+ step, then "Pricing".
50
+ 2. If they navigate away mid-wizard, `RenderEdit` has already saved a
51
+ draft under a client-minted `${screen.id}:new:${draftId}` key
52
+ (sessionStorage-backed); reopening the same tab resumes those values
53
+ instead of starting over. A new tab has no `draftId` yet — it falls
54
+ back to `form-draft:query:list` and either auto-adopts a single
55
+ in-progress draft or offers a picker when there's more than one.
56
+ 3. On the "Review" step, `ListingReviewSection` reads the values entered
57
+ so far straight from the host form and displays them read-only — no
58
+ extra network round-trip.
59
+ 4. Submitting creates the `listing` through the standard CRUD create and
60
+ discards the draft.
61
+
62
+ ## When to reach for it
63
+
64
+ A form is long enough that one page of fields overwhelms the user, or
65
+ losing everything on an accidental tab close is a real cost — onboarding,
66
+ a multi-part application, anything a user is likely to fill in over more
67
+ than one sitting. Use a `"single"`-mode layout instead when the form is
68
+ short enough to fit on one screen; wizard mode's per-step validation and
69
+ draft persistence are overhead a short form doesn't need.
70
+
71
+ ## Source
72
+
73
+ The feature entry point is `src/feature.ts`; the review-step component is
74
+ `src/web/listing-review-section.tsx`. `src/web/index.ts` exports
75
+ `listingsClient`, a `ClientFeatureDefinition` that registers
76
+ `ListingReviewSection` under the `__component` name `feature.ts`'s review
77
+ section points at — an app mounting this recipe must include
78
+ `listingsClient` in `createKumikoApp`'s `clientFeatures`, or the review
79
+ step falls back to `RenderEdit`'s unregistered-extension placeholder
80
+ instead of rendering the real component. The integration test under
81
+ `src/__tests__/` covers the entity's CRUD create through the wizard's
82
+ fields and the form-draft save/get/discard round-trip that backs
83
+ `draft: true`. `e2e/wizard.spec.ts` is a Playwright spec against the real
84
+ client bundle (mounted via `listingsClient`, the same registration an app
85
+ would use) — step navigation, per-step validation blocking Next on empty required fields,
86
+ draft-resume after a page reload, and that a successful submit discards
87
+ the draft (reopening the wizard starts fresh on step 1).
88
+
89
+ ## Source files
90
+
91
+ ### src/__tests__/feature.integration.test.ts
92
+
93
+ ```ts
94
+ // Wizard Form Sample — Integration Test
95
+ // Proves: the listing entity + wizard-mode screen boot together (the
96
+ // boot-validator's wizard rules — >= 2 titled sections, draft: true only
97
+ // with mode: "wizard" and form-draft mounted — all pass for this feature
98
+ // combination), the entity's CRUD create works end-to-end, and the
99
+ // form-draft save/get/discard round-trip that backs `draft: true` behaves
100
+ // exactly as a resumed-then-submitted wizard would use it.
101
+
102
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
103
+ import { createConfigFeature } from "@cosmicdrift/kumiko-bundled-features/config";
104
+ import {
105
+ FormDraftHandlers,
106
+ FormDraftQueries,
107
+ formDraftEntity,
108
+ formDraftFeature,
109
+ } from "@cosmicdrift/kumiko-bundled-features/form-draft";
110
+ import type { SaveContext } from "@cosmicdrift/kumiko-framework/engine";
111
+ import {
112
+ createTestUser,
113
+ setupTestStack,
114
+ type TestStack,
115
+ TestUsers,
116
+ unsafeCreateEntityTable,
117
+ } from "@cosmicdrift/kumiko-framework/stack";
118
+ import { listingEntity, listingsFeature } from "../feature";
119
+
120
+ let stack: TestStack;
121
+
122
+ const admin = TestUsers.admin;
123
+ const otherUser = createTestUser({ id: 2, roles: ["Admin"] });
124
+
125
+ // Arbitrary draftKey for exercising the handler contract directly — this
126
+ // suite calls FormDraftHandlers/FormDraftQueries with an explicit key, it
127
+ // doesn't go through RenderEdit's client-side derivation (which mints its
128
+ // own `${screenId}:new:${draftId}` for a create-mode wizard, issue #1913).
129
+ const DRAFT_KEY = "listing-wizard";
130
+
131
+ beforeAll(async () => {
132
+ // form-draft requires the "config" feature (its retention-days setting).
133
+ stack = await setupTestStack({
134
+ features: [listingsFeature, formDraftFeature, createConfigFeature()],
135
+ });
136
+ await unsafeCreateEntityTable(stack.db, listingEntity);
137
+ await unsafeCreateEntityTable(stack.db, formDraftEntity);
138
+ });
139
+
140
+ afterAll(async () => {
141
+ await stack.cleanup();
142
+ });
143
+
144
+ describe("listing:create", () => {
145
+ test("creates a listing with the wizard's fields", async () => {
146
+ const created = await stack.http.writeOk<SaveContext>(
147
+ "listings:write:listing:create",
148
+ { title: "Vintage desk lamp", category: "furniture", price: 42, condition: "used" },
149
+ admin,
150
+ );
151
+
152
+ expect(created.data["title"]).toBe("Vintage desk lamp");
153
+ expect(created.data["category"]).toBe("furniture");
154
+ expect(created.data["price"]).toBe(42);
155
+ expect(created.data["condition"]).toBe("used");
156
+ });
157
+
158
+ test("condition defaults to used when omitted", async () => {
159
+ const created = await stack.http.writeOk<SaveContext>(
160
+ "listings:write:listing:create",
161
+ { title: "Old bicycle", category: "vehicles", price: 80 },
162
+ admin,
163
+ );
164
+
165
+ expect(created.data["condition"]).toBe("used");
166
+ });
167
+ });
168
+
169
+ describe("wizard draft resume", () => {
170
+ test("save persists the in-progress steps, get resumes them", async () => {
171
+ await stack.http.writeOk(
172
+ FormDraftHandlers.save,
173
+ {
174
+ draftKey: DRAFT_KEY,
175
+ values: { title: "Draft title", category: "electronics" },
176
+ stepIndex: 1,
177
+ },
178
+ admin,
179
+ );
180
+
181
+ const resumed = await stack.http.queryOk<{ draft: { values: Record<string, unknown> } | null }>(
182
+ FormDraftQueries.get,
183
+ { draftKey: DRAFT_KEY },
184
+ admin,
185
+ );
186
+
187
+ expect(resumed.draft?.values["title"]).toBe("Draft title");
188
+ expect(resumed.draft?.values["category"]).toBe("electronics");
189
+ });
190
+
191
+ test("a later save overwrites the earlier one, not duplicates it", async () => {
192
+ const key = "listing-wizard:overwrite";
193
+ await stack.http.writeOk(
194
+ FormDraftHandlers.save,
195
+ { draftKey: key, values: { title: "First pass" }, stepIndex: 0 },
196
+ admin,
197
+ );
198
+ await stack.http.writeOk(
199
+ FormDraftHandlers.save,
200
+ { draftKey: key, values: { title: "Second pass", category: "other" }, stepIndex: 1 },
201
+ admin,
202
+ );
203
+
204
+ const resumed = await stack.http.queryOk<{ draft: { values: Record<string, unknown> } | null }>(
205
+ FormDraftQueries.get,
206
+ { draftKey: key },
207
+ admin,
208
+ );
209
+
210
+ expect(resumed.draft?.values["title"]).toBe("Second pass");
211
+ });
212
+
213
+ test("discard clears the draft — the next get resolves null, same as a completed submit", async () => {
214
+ const key = "listing-wizard:discard";
215
+ await stack.http.writeOk(
216
+ FormDraftHandlers.save,
217
+ { draftKey: key, values: { title: "About to submit" }, stepIndex: 2 },
218
+ admin,
219
+ );
220
+
221
+ await stack.http.writeOk(FormDraftHandlers.discard, { draftKey: key }, admin);
222
+
223
+ const afterDiscard = await stack.http.queryOk<{ draft: unknown }>(
224
+ FormDraftQueries.get,
225
+ { draftKey: key },
226
+ admin,
227
+ );
228
+ expect(afterDiscard.draft).toBeNull();
229
+ });
230
+
231
+ test("a foreign user's get never resolves another user's draft", async () => {
232
+ const key = "listing-wizard:isolated";
233
+ await stack.http.writeOk(
234
+ FormDraftHandlers.save,
235
+ { draftKey: key, values: { title: "Only mine" }, stepIndex: 0 },
236
+ admin,
237
+ );
238
+
239
+ const foreign = await stack.http.queryOk<{ draft: unknown }>(
240
+ FormDraftQueries.get,
241
+ { draftKey: key },
242
+ otherUser,
243
+ );
244
+ expect(foreign.draft).toBeNull();
245
+ });
246
+ });
247
+ ```
248
+
249
+ ### src/entities/listing.ts
250
+
251
+ ```ts
252
+ import {
253
+ createEntity,
254
+ createNumberField,
255
+ createSelectField,
256
+ createTextField,
257
+ } from "@cosmicdrift/kumiko-framework/engine";
258
+
259
+ export const listingEntity = createEntity({
260
+ table: "read_sample_listings",
261
+ fields: {
262
+ title: createTextField({
263
+ personal: false,
264
+ reason: "is_business_data",
265
+ required: true,
266
+ searchable: true,
267
+ }),
268
+ category: createSelectField({
269
+ options: ["electronics", "furniture", "vehicles", "other"] as const,
270
+ required: true,
271
+ }),
272
+ price: createNumberField({ required: true }),
273
+ condition: createSelectField({
274
+ options: ["new", "used"] as const,
275
+ default: "used",
276
+ }),
277
+ },
278
+ });
279
+ ```
280
+
281
+ ### src/feature.ts
282
+
283
+ ```ts
284
+ // Wizard Form Sample
285
+ // Shows: EditLayout.mode: "wizard" splitting one entityEdit screen into
286
+ // steps, a review step (kind: "extension") that reads the host form's
287
+ // live values through ExtensionSectionProps.values instead of its own
288
+ // fetch, and draft: true resuming an in-progress wizard via the bundled
289
+ // form-draft feature — the actual save/get/discard wiring is automatic
290
+ // client-side, this feature only turns the two flags on. form-draft itself
291
+ // requires the bundled "config" feature (its retention-days setting), so
292
+ // any app mounting this recipe must mount that too.
293
+
294
+ import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
295
+ import { listingEntity } from "./entities/listing";
296
+
297
+ export { listingEntity } from "./entities/listing";
298
+
299
+ const editorWrite = { access: { roles: ["Admin", "User"] } } as const;
300
+ const openRead = {
301
+ access: {
302
+ openToAll: {
303
+ reason:
304
+ "any signed-in user may list and view listings; writes are still gated by editorWrite",
305
+ },
306
+ },
307
+ } as const;
308
+
309
+ export const listingsFeature = defineFeature("listings", (r) => {
310
+ r.crud("listing", listingEntity, { write: editorWrite, read: openRead });
311
+
312
+ r.screen({
313
+ id: "listing-wizard",
314
+ type: "entityEdit",
315
+ entity: "listing",
316
+ layout: {
317
+ mode: "wizard",
318
+ draft: true,
319
+ sections: [
320
+ { title: "Basics", fields: ["title", "category"] },
321
+ { title: "Pricing", fields: ["price", "condition"] },
322
+ {
323
+ kind: "extension",
324
+ title: "Review",
325
+ component: { react: { __component: "ListingReviewSection" } },
326
+ },
327
+ ],
328
+ },
329
+ access: editorWrite.access,
330
+ });
331
+ });
332
+ ```
333
+
334
+ ### src/web/index.ts
335
+
336
+ ```ts
337
+ // Client-side registration for the review step's extension section (see
338
+ // ../feature.ts's `component: { react: { __component: "ListingReviewSection" } }`).
339
+ // A `kind: "extension"` section only resolves through
340
+ // clientFeatures.extensionSectionComponents (createKumikoApp → ExtensionSectionsProvider)
341
+ // — never imported directly from feature.ts, which keeps the server file
342
+ // free of the renderer-web graph. Without registering this export, an app
343
+ // mounting this recipe gets RenderEdit's fallback placeholder instead of
344
+ // the real review step.
345
+
346
+ import type { ClientFeatureDefinition } from "@cosmicdrift/kumiko-renderer-web";
347
+ import { ListingReviewSection } from "./listing-review-section";
348
+
349
+ export const listingsClient: ClientFeatureDefinition = {
350
+ name: "listings",
351
+ extensionSectionComponents: { ListingReviewSection },
352
+ };
353
+ ```
354
+
355
+ ### src/web/listing-review-section.tsx
356
+
357
+ ```tsx
358
+ import type { ExtensionSectionProps } from "@cosmicdrift/kumiko-renderer";
359
+ import { DetailList } from "@cosmicdrift/kumiko-renderer-web";
360
+ import type { ReactNode } from "react";
361
+
362
+ // Registered under the `__component` name the feature's review section
363
+ // points at (see feature.ts) — never imported by feature.ts itself, the
364
+ // string name is the only link so the server file stays free of the
365
+ // renderer-web graph.
366
+ export function ListingReviewSection({ values }: ExtensionSectionProps): ReactNode {
367
+ return (
368
+ <DetailList
369
+ testId="listing-review"
370
+ rows={[
371
+ { label: "Title", value: String(values?.["title"] ?? "") },
372
+ { label: "Category", value: String(values?.["category"] ?? "") },
373
+ { label: "Price", value: String(values?.["price"] ?? "") },
374
+ { label: "Condition", value: String(values?.["condition"] ?? "") },
375
+ ]}
376
+ />
377
+ );
378
+ }
379
+ ```