cabloy 5.1.124 → 5.1.126

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 (109) hide show
  1. package/.cabloy-version +1 -1
  2. package/.claude/skills/cabloy-backend-scaffold/SKILL.md +1 -1
  3. package/.claude/skills/cabloy-backend-scaffold/evals/evals.json +6 -0
  4. package/.claude/skills/cabloy-backend-scaffold/references/follow-up-checklist.md +13 -4
  5. package/.claude/skills/cabloy-workflow/SKILL.md +4 -2
  6. package/.claude/skills/cabloy-workflow/evals/evals.json +14 -2
  7. package/.claude/skills/cabloy-worktree-environment/SKILL.md +206 -0
  8. package/.claude/skills/cabloy-worktree-environment/evals/evals.json +77 -0
  9. package/.github/workflows/vona-test-pg.yml +3 -0
  10. package/.github/workflows/vona-test-sqlite3.yml +3 -0
  11. package/CHANGELOG.md +21 -0
  12. package/CLAUDE.md +1 -1
  13. package/cabloy-docs/.vitepress/config.mjs +2 -0
  14. package/cabloy-docs/ai/playbook-technical-blog-authoring.md +158 -0
  15. package/cabloy-docs/backend/backend-contract-emission-specimen.md +2 -2
  16. package/cabloy-docs/backend/dto-guide.md +10 -7
  17. package/cabloy-docs/backend/dto-infer-generation.md +127 -7
  18. package/cabloy-docs/backend/foundation.md +1 -1
  19. package/cabloy-docs/backend/introduction.md +1 -0
  20. package/cabloy-docs/backend/module-dependencies.md +104 -0
  21. package/cabloy-docs/backend/serialization-guide.md +6 -0
  22. package/cabloy-docs/fullstack/parallel-worktree-environment.md +72 -34
  23. package/cabloy-docs/fullstack/suites-and-modules.md +1 -0
  24. package/cabloy-docs/reference/package-map.md +1 -0
  25. package/e2e/specs/a-commerce/commerce.spec.ts +89 -0
  26. package/package.json +2 -1
  27. package/scripts/init.ts +7 -12
  28. package/scripts/initAppName.test.ts +72 -0
  29. package/scripts/initAppName.ts +50 -0
  30. package/vona/packages-vona/vona/package.json +1 -1
  31. package/vona/pnpm-lock.yaml +48 -146
  32. package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressMineCreate.tsx +6 -27
  33. package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressMineItem.tsx +6 -31
  34. package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressMineUpdate.tsx +6 -2
  35. package/vona/src/suite/a-commerce/modules/commerce-member/src/lib/addressMine.ts +16 -0
  36. package/vona/src/suite/a-commerce/modules/commerce-member/src/service/address.ts +4 -17
  37. package/vona/src/suite/a-commerce/modules/commerce-member/test/addressOwnership.test.ts +63 -19
  38. package/vona/src/suite/a-commerce/modules/commerce-payment/package.json +2 -1
  39. package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/payScene.commerceOrder.ts +11 -2
  40. package/vona/src/suite/a-commerce/modules/commerce-payment/src/service/commercePayScene.ts +26 -0
  41. package/vona/src/suite/a-commerce/modules/commerce-payment/test/paymentAttempt.test.ts +19 -2
  42. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/config/locale/en-us.ts +5 -2
  43. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/config/locale/zh-cn.ts +5 -2
  44. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateCreate.tsx +26 -35
  45. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateUpdate.tsx +6 -1
  46. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/entity/couponTemplate.tsx +40 -4
  47. package/vona/src/suite/a-commerce/modules/commerce-promotion/test/couponTemplateFormLayout.test.ts +85 -0
  48. package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +1 -1
  49. package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/order.ts +3 -5
  50. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderSummary.tsx +7 -40
  51. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/shipmentView.tsx +6 -15
  52. package/vona/src/suite/a-commerce/modules/commerce-trade/src/lib/order.ts +9 -0
  53. package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/order.ts +121 -11
  54. package/vona/src/suite/a-commerce/modules/commerce-trade/test/order.test.ts +25 -0
  55. package/vona/src/suite/a-commerce/modules/commerce-trade/test/paymentOutcome.test.ts +19 -2
  56. package/vona/src/suite/a-training/modules/training-record/src/dto/detailRecordSubjectResItem.tsx +6 -1
  57. package/vona/src/suite/a-training/modules/training-record/src/dto/recordSelectResItem.tsx +3 -1
  58. package/vona/src/suite/a-training/modules/training-record/src/dto/recordView.tsx +4 -1
  59. package/vona/src/suite/a-training/modules/training-record/test/record.test.ts +45 -0
  60. package/vona/src/suite/a-training/modules/training-student/src/dto/detailRecordResItem.tsx +6 -1
  61. package/vona/src/suite/a-training/modules/training-student/src/dto/studentSummary.tsx +5 -23
  62. package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +37 -5
  63. package/vona/src/suite-vendor/a-pay/modules/a-pay/package.json +1 -1
  64. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/.metadata/index.ts +29 -0
  65. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.index.ts +3 -0
  66. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.redlock.ts +3 -1
  67. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.version.ts +7 -0
  68. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/queue.outboxDispatch.ts +9 -4
  69. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/schedule.providerOperationDispatch.ts +11 -0
  70. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/outboxEvent.tsx +11 -4
  71. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/providerOperation.tsx +12 -0
  72. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/webhookInbox.tsx +6 -0
  73. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/outbox.ts +12 -5
  74. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/paymentSession.ts +1 -37
  75. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/providerOperation.ts +355 -1
  76. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/refundOperation.ts +226 -0
  77. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/webhook.ts +148 -50
  78. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/types/payScene.ts +2 -1
  79. package/vona/src/suite-vendor/a-pay/modules/pay-mock/package.json +1 -1
  80. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/.metadata/index.ts +39 -22
  81. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/bean/payProvider.mock.ts +42 -13
  82. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/controller/mockPayment.ts +11 -0
  83. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/dto/mockRefundComplete.tsx +13 -0
  84. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/dto/mockRefundReceipt.tsx +16 -0
  85. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/service/payMock.ts +56 -0
  86. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/package.json +2 -1
  87. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/bean/payProvider.paypal.ts +117 -8
  88. package/vona/src/suite-vendor/a-pay/modules/pay-stripe/package.json +1 -1
  89. package/vona/src/suite-vendor/a-pay/package.json +5 -5
  90. package/vona/src/suite-vendor/a-vona/modules/a-orm/package.json +1 -1
  91. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/dto/dtoGet.ts +5 -5
  92. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  93. package/zova/src/suite/a-commerce/modules/commerce-member/src/api/openapi/schemas.ts +54 -12
  94. package/zova/src/suite/a-commerce/modules/commerce-member/src/api/openapi/types.ts +625 -64
  95. package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/types.ts +55 -15
  96. package/zova/src/suite/a-commerce/modules/commerce-trade/cli/openapi.config.ts +1 -1
  97. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/commerceTradeOrder.ts +15 -17
  98. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/schemas.ts +12 -8
  99. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/types.ts +111 -26
  100. package/zova/src/suite/a-commerce/modules/commerce-trade/src/apiSchema/commerceTradeOrder.ts +3 -3
  101. package/zova/src/suite/a-commerce/modules/commerce-trade/src/component/tableCellActionRefund/controller.tsx +1 -8
  102. package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/order.ts +5 -9
  103. package/zova/src/suite/a-training/modules/training-student/src/.metadata/index.ts +1 -1
  104. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/schemas.ts +301 -17
  105. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/types.ts +5461 -1788
  106. package/vona/src/suite-vendor/a-pay/modules/a-pay/test/outbox.test.ts +0 -209
  107. package/vona/src/suite-vendor/a-pay/modules/a-pay/test/paymentSession.test.ts +0 -153
  108. package/vona/src/suite-vendor/a-pay/modules/a-pay/test/webhook.test.ts +0 -278
  109. package/vona/src/suite-vendor/a-pay/modules/pay-mock/test/payMock.test.ts +0 -141
@@ -101,8 +101,8 @@ The next source to read is:
101
101
  Representative source facts:
102
102
 
103
103
  - `@Dto<IDtoOptionsStudentSummary>()`
104
- - fields such as `id`, `name`, `mobile`, `level`, `levelTitle`, `description`, `descriptionLength`, and `summaryText`
105
- - `@Api.field(...)` metadata on each field
104
+ - the `ModelStudent` projection supplies `id`, `name`, `mobile`, `level`, and `description`
105
+ - direct `@Api.field(...)` metadata declares the computed `levelTitle`, `descriptionLength`, and `summaryText` fields
106
106
 
107
107
  This file answers the second emission question:
108
108
 
@@ -148,7 +148,9 @@ A practical split is:
148
148
  - use inferred DTOs when the contract closely follows model structure or query shape
149
149
  - wrap inferred DTOs in a named DTO class when reuse or discoverability becomes more important
150
150
 
151
- Advanced inferred DTO shaping can also stay named and reusable through helper options such as `dtoClass`, especially for relation-aware contracts and nested DTO surfaces. For the inference side, see [DTO Infer and Generation](/backend/dto-infer-generation).
151
+ When stable Entity, Model, relation, or query truth already exists, prefer inference first: use `$Dto.*` with `columns`, `include`, or `dtoClass` to define the projection; use `@Dto({ fields })` with `$makeMetadata(...)` for metadata-only differences or `$makeSchema(...)` for schema and validation differences; add an `@Api.field(...)` member only for a genuinely new field absent from the inferred contract. Keep an explicit DTO as a deliberate fallback when no suitable upstream truth exists or the inferred composition is less clear.
152
+
153
+ Advanced inferred DTO shaping can also stay named and reusable through helper options such as `dtoClass`, especially for relation-aware contracts and nested DTO surfaces. `$Dto.get(...)` otherwise keeps the complete model-aware read shape by default; use `columns` or `dtoClass` for a genuine business projection, not merely to remove `iid` or `deleted`. For the canonical authoring sequence, see [Default-first three-layer DTO authoring](/backend/dto-infer-generation#default-first-three-layer-dto-authoring) and [Default read shape versus a public projection](/backend/dto-infer-generation#default-read-shape-versus-a-public-projection).
152
154
 
153
155
  ## Relationship to ORM and controller contracts
154
156
 
@@ -166,12 +168,13 @@ That means DTO design should often be read together with:
166
168
 
167
169
  When creating DTOs:
168
170
 
169
- 1. prefer reuse through mapped-class helpers when the shape is derived from existing classes
170
- 2. keep DTO validation and OpenAPI concerns aligned through `@Api.field`
171
- 3. decide whether the contract should be an explicit DTO class or an inferred DTO
172
- 4. avoid re-declaring fields manually if Vona’s DTO-generation or class-derivation tools already solve the problem
173
- 5. treat DTO design as part of the contract between backend handlers, models, and frontend integration
174
- 6. choose explicit DTOs when named long-lived contracts matter, and inferred DTOs when the model/query shape already expresses the contract clearly
171
+ 1. check whether an Entity, Model, relation, or query shape already provides suitable upstream contract truth
172
+ 2. define the inferred projection before redeclaring fields: choose the `$Dto.*` helper and, where needed, `columns`, `include`, or `dtoClass`
173
+ 3. use `$makeMetadata(...)` for metadata-only refinement and `$makeSchema(...)` for schema or validation refinement of an inferred field
174
+ 4. use `@Api.field(...)` for a genuinely new field, not as a second declaration of an inferred field
175
+ 5. keep DTO validation and OpenAPI concerns aligned, and keep the final structure-defining schema last when using schema-like composition
176
+ 6. decide whether the contract should remain inferred, be wrapped in a named inferred DTO, or deliberately fall back to an explicit DTO
177
+ 7. treat DTO design as part of the contract between backend handlers, models, and frontend integration
175
178
 
176
179
  ## Where to read next
177
180
 
@@ -94,6 +94,16 @@ A practical mental model is:
94
94
 
95
95
  This matters because different ORM operations naturally produce different API contracts.
96
96
 
97
+ ## Default read shape versus a public projection
98
+
99
+ `$Dto.get(() => ModelX)` defaults to the complete model-aware Entity read shape, including inherited framework fields such as `iid` and `deleted` when the Entity defines them. This is the ordinary read baseline, not an automatic public-payload minimization policy.
100
+
101
+ Use `columns` when an endpoint genuinely needs a smaller one-off business, query, or response projection. Use `dtoClass` when that curated read surface is named and reusable, especially for nested relation contracts. Do not add either solely to remove `iid` or `deleted` from an otherwise complete `$Dto.get(...)` shape.
102
+
103
+ This differs from top-level `$Dto.create(...)` and `$Dto.update(...)`. Their default omission of identity, active-instance, soft-deletion, and lifecycle fields establishes a write-input authorization boundary: callers must not supply those framework-owned values. That write policy does not imply that ordinary read DTOs need the same narrowing.
104
+
105
+ A narrowed DTO or emitted OpenAPI schema declares the supported contract, but does not by itself prove that an already-built HTTP response object is projected or stripped at runtime. When actual payload minimization is required, deliberately shape the returned data or apply a verified response-output policy, then verify the action response. See [Serialization Guide](/backend/serialization-guide) for response transformation behavior.
106
+
97
107
  ## When inference should replace handwritten DTOs
98
108
 
99
109
  A practical rule is:
@@ -106,12 +116,117 @@ A practical reading takeaway is:
106
116
 
107
117
  > the best default is often not “handwritten DTO or no DTO.” It is “named DTO class backed by the right inferred helper.”
108
118
 
119
+ ## Default-first three-layer DTO authoring
120
+
121
+ The three DTO shapes above classify the public artifact: explicit named DTO, inline inference, or a named class that wraps inference. They do not describe how to author an inferred contract.
122
+
123
+ When an Entity, Model, relation, or query shape already owns stable contract truth, start from that truth instead of redeclaring its fields. Define the DTO projection first, then add only the smallest DTO-local difference. This preserves inherited validation, titles, OpenAPI metadata, and render metadata wherever they still apply.
124
+
125
+ ### Layer 1: project upstream truth
126
+
127
+ Use the operation-appropriate `$Dto.*` helper to establish the baseline contract. Then choose the narrowest projection mechanism:
128
+
129
+ - use the default inferred shape when all relevant model-aware fields belong in the DTO
130
+ - use `columns` for a simple local field subset, including the exclusion of server-owned fields
131
+ - use `include` to bring a relation-aware shape into the DTO
132
+ - use `dtoClass` when a top-level or nested relation needs a reusable, named field surface
133
+
134
+ For example, `DtoStudentSelectReq` starts from an Entity-backed query projection:
135
+
136
+ ```typescript
137
+ export class DtoStudentSelectReq extends $Dto.queryPage(EntityStudent, [
138
+ 'name',
139
+ 'level',
140
+ 'createdAt',
141
+ ]) {}
142
+ ```
143
+
144
+ A relation-aware DTO can use `columns` and `include` to curate its baseline without restating each field decorator:
145
+
146
+ ```typescript
147
+ export class DtoDetailRecordBase extends $Dto.get(() => ModelRecord, {
148
+ columns: ['id', 'name', 'subjectCount', 'totalScore', 'averageScore'],
149
+ include: { trainingRecordSubjects: true },
150
+ }) {}
151
+ ```
152
+
153
+ Use `dtoClass` when that curated surface is itself reusable. For example, a detail mutate DTO can take its field surface from `DtoDetailRecordBase`, and a parent create DTO can include that named detail contract:
154
+
155
+ ```typescript
156
+ export class DtoDetailRecordMutate extends $Dto.mutate(() => ModelRecord, {
157
+ dtoClass: DtoDetailRecordBase,
158
+ include: { trainingRecordSubjects: { dtoClass: DtoDetailRecordSubjectMutate } },
159
+ }) {}
160
+
161
+ export class DtoStudentCreate extends $Dto.create(() => ModelStudent, {
162
+ include: { trainingRecords: { dtoClass: DtoDetailRecordMutate } },
163
+ }) {}
164
+ ```
165
+
166
+ When a field genuinely does not belong to an API's business read or write contract, exclude it through the projection; do not rely on visibility metadata to hide a field that the API must not accept or return. Do not add a projection solely to remove framework read fields such as `iid` or `deleted` from an otherwise complete `$Dto.get(...)` shape; see [Default read shape versus a public projection](#default-read-shape-versus-a-public-projection).
167
+
168
+ ### Layer 2: overlay metadata or refine the schema
169
+
170
+ For a field already supplied by the projection, use the `fields` map on `@Dto(...)` rather than redeclaring the property:
171
+
172
+ - use `$makeMetadata(...)` when only field metadata changes, such as title, order, visibility, serialization, or render behavior; the inherited schema and validation remain authoritative
173
+ - use `$makeSchema(...)` when optionality, validation, enum members, preprocess/transform behavior, or another schema structure must change; it replaces or refines the runtime field schema while preserving the framework's inherited OpenAPI metadata merge
174
+
175
+ `DtoStudentSelectReq` demonstrates schema refinement for query input:
176
+
177
+ ```typescript
178
+ @Dto({
179
+ fields: {
180
+ name: $makeSchema(v.optional(), z.string()),
181
+ level: $makeSchema(v.optional(), z.number()),
182
+ createdAt: $makeSchema(v.filterTransform('a-web:dateRange'), v.optional(), z.string()),
183
+ },
184
+ })
185
+ export class DtoStudentSelectReq extends $Dto.queryPage(EntityStudent, [
186
+ 'name',
187
+ 'level',
188
+ 'createdAt',
189
+ ]) {}
190
+ ```
191
+
192
+ `$makeSchema(...)` applies schema-like arguments right-to-left. Keep the final structure-defining schema, such as `z.string()`, `z.number()`, `v.object(...)`, or `v.array(...)`, last in authoring order. Treat optionality, nullability, defaults, preprocess/transform wrappers, objects, and arrays as structure-shaping rather than metadata-only, and verify emitted schema/OpenAPI output after changing them.
193
+
194
+ `@Dto({ fields })` changes the runtime contract and metadata. It does not rewrite the TypeScript property type inferred from the `$Dto.*` base class. Do not add a duplicate `declare` field or a second field decorator solely to mirror a runtime schema restriction unless a separate static contract is genuinely required and is type-compatible with the inferred base.
195
+
196
+ ### Layer 3: add contract-only fields
197
+
198
+ Declare a class member with `@Api.field(...)` only when the field is absent from the inferred projection. Typical examples are serializer-produced response fields, operation-local helper fields, or a separate nested representation.
199
+
200
+ ```typescript
201
+ export class DtoStudentCreate extends $Dto.create(() => ModelStudent, {
202
+ include: { trainingRecords: { dtoClass: DtoDetailRecordMutate } },
203
+ }) {
204
+ @Api.field(v.optional(), v.array(DtoDetailRecordResItem))
205
+ _trainingRecords?: DtoDetailRecordResItem[];
206
+ }
207
+ ```
208
+
209
+ Do not manually redeclare an inferred field merely to adjust its title, renderer, validation, or schema. Use Layer 2 instead.
210
+
211
+ ### When to use an explicit DTO from scratch
212
+
213
+ Inference first is a default strategy, not a requirement to force every DTO through `$Dto.*`. Use an explicit DTO deliberately when no stable upstream Entity, Model, relation, or query shape is suitable, for example:
214
+
215
+ - authentication, captcha, or behavior-focused commands
216
+ - webhook and third-party protocol payloads
217
+ - aggregate commands assembled from unrelated resources
218
+ - a public contract that must stay intentionally decoupled from persistence structure
219
+
220
+ If the inferred baseline would require pervasive exceptions or no longer makes the contract clearer, an explicit DTO is the better design.
221
+
109
222
  ## `training-student` as the decision specimen
110
223
 
111
224
  The current `training-student` module is a strong specimen because it shows several different DTO choices in one compact family.
112
225
 
113
226
  Relevant source files include:
114
227
 
228
+ - `vona/src/suite/a-training/modules/training-student/src/dto/detailRecordBase.tsx`
229
+ - `vona/src/suite/a-training/modules/training-student/src/dto/detailRecordMutate.tsx`
115
230
  - `vona/src/suite/a-training/modules/training-student/src/dto/studentCreate.tsx`
116
231
  - `vona/src/suite/a-training/modules/training-student/src/dto/studentUpdate.tsx`
117
232
  - `vona/src/suite/a-training/modules/training-student/src/dto/studentView.tsx`
@@ -163,10 +278,12 @@ Representative source facts:
163
278
 
164
279
  This makes it a strong specimen of a **query DTO that still wraps inference, but adds operation-specific shaping**.
165
280
 
281
+ Its `fields` entries use `$makeSchema(...)` because the query contract changes the projected fields' runtime schema: query values become optional, `level` is normalized before validation, and `createdAt` accepts the date-range filter representation. Keep the final structure-defining schema last in each `$makeSchema(...)` call, then verify the emitted schema/OpenAPI result after a structure-shaping change.
282
+
166
283
  A practical reading takeaway is:
167
284
 
168
285
  - inference gives the structural baseline
169
- - explicit field metadata adds the operation-specific contract behavior
286
+ - `$makeSchema(...)` refines projected fields when their query contract behavior differs
170
287
 
171
288
  ### Row-item response DTO
172
289
 
@@ -241,7 +358,7 @@ A practical rule is:
241
358
 
242
359
  ## Use `dtoClass` to shape inferred fields
243
360
 
244
- When inferred DTOs should still follow a reusable named field surface, pass `dtoClass` to the helper options.
361
+ When inferred DTOs should still follow a reusable named field surface, pass `dtoClass` to the helper options. This is the reusable projection choice from [Layer 1: project upstream truth](#layer-1-project-upstream-truth).
245
362
 
246
363
  This is useful when:
247
364
 
@@ -357,11 +474,14 @@ For the bridge step that carries this backend-authored contract across the stack
357
474
 
358
475
  When evaluating a return shape or input contract that closely follows model structure, ask:
359
476
 
360
- 1. should this DTO be inferred instead of handwritten?
361
- 2. does model relationship structure already contain enough information?
362
- 3. is the contract get/list/query/create/update/aggregate/group oriented?
477
+ 1. does an Entity, Model, relation, or query shape already provide stable upstream contract truth?
478
+ 2. is the contract get/list/query/create/update/aggregate/group oriented, and which `$Dto.*` helper matches it?
479
+ 3. should `columns`, `include`, `with`, or `dtoClass` define the projection boundary?
363
480
  4. should the inferred DTO stay inline or be wrapped in a named DTO class?
364
- 5. does the resulting DTO also affect OpenAPI and frontend generation paths?
365
- 6. is CRUD generation already giving enough contract structure that another handwritten DTO would be redundant?
481
+ 5. for every local difference, is it metadata-only (`$makeMetadata(...)`) or schema-affecting (`$makeSchema(...)`)?
482
+ 6. is every `@Api.field(...)` member genuinely new instead of a redeclared inferred field?
483
+ 7. if `$makeSchema(...)` is used, is the structure-defining schema last and is emitted schema/OpenAPI verification planned?
484
+ 8. does the resulting DTO also affect OpenAPI and frontend generation paths?
485
+ 9. is CRUD generation already giving enough contract structure that another handwritten DTO would be redundant, or is an explicit DTO clearer?
366
486
 
367
487
  That helps reduce redundant type work and keeps contracts closer to the model truth.
@@ -263,7 +263,7 @@ Using `this.$scope.<fixedModule>`, `app.scope(...)`, or `this.app.scope(...)` do
263
263
 
264
264
  Scope lookup also does not compose, install, load, or order an absent module. The target must already be available through suite/application composition. Declare `vonaModule.dependencies` only when the caller has a genuine requirement for a target module's availability, dependency-first ordering, or minimum compatible version—not merely because it looks up that module's service, model, config, locale, or another resource.
265
265
 
266
- For the package, suite, and module dependency distinction, see [Package Map](/reference/package-map).
266
+ For the canonical availability, ordering, and version decision guide, see [Vona Module Dependencies](/backend/module-dependencies). For the package, suite, and module topology distinction, see [Package Map](/reference/package-map).
267
267
 
268
268
  ## Suite / module / package boundaries
269
269
 
@@ -30,6 +30,7 @@ Use this page as the main backend hub, then choose the family that matches your
30
30
  Start here when you need the core backend mental model first:
31
31
 
32
32
  - [Backend Foundation](/backend/foundation)
33
+ - [Vona Module Dependencies](/backend/module-dependencies)
33
34
  - [Backend Essentials](/backend/backend-essentials)
34
35
  - [Backend CLI](/backend/cli)
35
36
  - [Service Guide](/backend/service-guide)
@@ -0,0 +1,104 @@
1
+ # Vona Module Dependencies
2
+
3
+ Use `vonaModule.dependencies` when one Vona module has a real requirement for another module's availability, dependency-first ordering, or minimum compatible version.
4
+
5
+ It is not a general declaration for every cross-module reference.
6
+
7
+ ## What a module dependency means
8
+
9
+ A dependency in a module package manifest expresses all of these conditions:
10
+
11
+ - the target module must be present and enabled
12
+ - the target module is ordered before the dependent module
13
+ - the declared version is the minimum compatible target version
14
+
15
+ For example:
16
+
17
+ ```json
18
+ {
19
+ "vonaModule": {
20
+ "dependencies": {
21
+ "a-vona": "5.0.0",
22
+ "a-telemetry": "5.0.0"
23
+ }
24
+ }
25
+ }
26
+ ```
27
+
28
+ This is a module lifecycle contract. It is stronger than an import or a resource lookup.
29
+
30
+ ## Four different relationships
31
+
32
+ Keep these concerns separate.
33
+
34
+ | Relationship | Purpose | Typical surface |
35
+ | -------------------------------- | ---------------------------------------------------------------------- | ---------------------------------- |
36
+ | Package dependency | Makes a package available to package tooling | `package.json` `dependencies` |
37
+ | Suite or application composition | Includes module packages in the application composition | suite/application package metadata |
38
+ | Module dependency | Requires a target module's availability, order, and compatible version | `vonaModule.dependencies` |
39
+ | Runtime resource lookup | Resolves a resource from an already composed module | `this.$scope`, `app.scope(...)` |
40
+
41
+ A module dependency does not install a package or compose an otherwise absent module. Ensure package and suite/application composition separately.
42
+
43
+ ## Lookup is not a dependency edge
44
+
45
+ The following forms look up resources from modules that are already composed into the active application:
46
+
47
+ ```ts
48
+ this.scope.model.order;
49
+ this.$scope.commerceCatalog.model.product;
50
+ app.scope('commerce-catalog').model.product;
51
+ ```
52
+
53
+ Lookup alone does not require adding the target to `vonaModule.dependencies`. That includes lookup of another module's service, model, config, locale, entity, or other scoped resource.
54
+
55
+ The same distinction applies to a named ORM relation or a relation included by an inferred DTO. A relation such as:
56
+
57
+ ```ts
58
+ $relation.belongsTo('commerce-trade:order', 'commerce-member:user', 'userId');
59
+ ```
60
+
61
+ is not, by itself, a reason to add a module dependency edge.
62
+
63
+ ## Decision guide
64
+
65
+ | Question | Decision |
66
+ | -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
67
+ | Does the code only look up a resource from an already composed module? | Do not add an edge solely for the lookup. |
68
+ | Can the feature operate when the target module is absent? | Do not add a required dependency; make optional behavior explicit. |
69
+ | Must the target module be present and enabled for the feature to work? | Add a module dependency and verify composition separately. |
70
+ | Must the target initialize before the dependent module? | Add a module dependency. |
71
+ | Does startup, lifecycle, or `monkey.ts` integration depend on deterministic target ordering? | Add a module dependency. |
72
+ | Does the feature require a minimum compatible target-module version? | Add a module dependency with that minimum version. |
73
+ | Would the proposed edge create a cycle? | Revisit module ownership or composition instead of encoding a circular dependency. |
74
+
75
+ `monkey.ts` and lifecycle integration are common reasons to require ordering, but they are not the only valid reason. The decisive question is whether the target module is a required availability, ordering, or version contract.
76
+
77
+ ## Avoid speculative edges
78
+
79
+ Do not add `vonaModule.dependencies` merely to:
80
+
81
+ - document an import
82
+ - document a scope lookup
83
+ - make a named ORM relation appear explicit
84
+ - compensate for an assumed circular lookup
85
+ - encode a transient implementation detail that should remain inside the framework
86
+
87
+ A speculative edge can make a module unavailable when its target is disabled, impose an unnecessary ordering constraint, and turn a manageable design issue into a cycle.
88
+
89
+ ## Verification
90
+
91
+ When adding or changing a module dependency:
92
+
93
+ 1. verify that package and suite/application composition makes the target available
94
+ 2. verify the target module's relative name and minimum compatible version
95
+ 3. confirm that the dependency graph remains acyclic
96
+ 4. run the relevant Vona metadata, build, or startup path that consumes module metadata
97
+ 5. run the narrowest meaningful tests and type checks for the affected feature
98
+
99
+ ## Related guides
100
+
101
+ - [Backend Foundation](/backend/foundation#scope-lookup-vs-module-dependencies)
102
+ - [Package Map](/reference/package-map)
103
+ - [Backend Startup Guide](/backend/startup-guide)
104
+ - [Backend CLI](/backend/cli)
@@ -51,6 +51,12 @@ A practical debugging rule is:
51
51
 
52
52
  - if a field-level serializer appears correct but the response still returns the raw value, first check whether the controller action uses `@Core.serializer()`
53
53
 
54
+ ### Declared response shape versus physical output fields
55
+
56
+ A DTO or `@Api.body(...)` schema declares the response contract and emitted OpenAPI shape. It is not, by itself, an unknown-field stripping pass over an object that a controller action has already returned.
57
+
58
+ When an API must physically omit fields from its payload, deliberately return the intended projection or apply a verified response-output policy, then verify the action response. Field-level serializer metadata is part of such output behavior only when the action enables `@Core.serializer()`; do not assume a narrower DTO declaration alone sanitizes a response. For deciding whether a read DTO itself needs a narrower business projection, see [Default read shape versus a public projection](/backend/dto-infer-generation#default-read-shape-versus-a-public-projection).
59
+
54
60
  ## Serializer transforms
55
61
 
56
62
  Vona supports custom serializer transforms through `@SerializerTransform(...)`.
@@ -1,81 +1,119 @@
1
1
  # Parallel Worktree Environment
2
2
 
3
- Use worktree-local environment overrides when two Cabloy Basic worktrees need to run ordinary local development or managed E2E checks at the same time.
3
+ Use worktree-local environment overrides when two Cabloy Basic or Cabloy Start worktrees need to run ordinary local development or managed E2E checks at the same time.
4
4
 
5
- This workflow creates an isolated local runtime without changing committed environment defaults. It covers normal Vona and Zova development, `npm run test`, and `npm run test:e2e:XXX:clean`. It does not automatically isolate every external dependency.
5
+ This Cabloy Basic page is the canonical shared recipe for both editions. Detect the active edition before selecting commands, but keep the same two-file environment model.
6
+
7
+ This workflow creates a standard isolated local runtime without changing committed environment defaults. It does not automatically isolate every external dependency.
6
8
 
7
9
  ## Before creating local overrides
8
10
 
9
- 1. Confirm that the checkout is a separate worktree.
10
- 2. Check `vona/env/` and `zova/env/` for applicable, more-specific `.env.*.local` files. A more-specific local override can take precedence over `.env.local`.
11
- 3. Choose one unique worktree name and unused Vona, Zova, and HMR ports.
11
+ 1. Create and enter a separate linked worktree.
12
+ 2. Explicitly invoke `/cabloy-worktree-environment` to receive a deterministic environment proposal and confirm it before any local file is written.
12
13
 
13
- The repository ignores `**/env/.env*.local`, so these files remain local to the worktree and must not be committed.
14
+ The repository ignores `**/env/.env*.local`, but this workflow may create or change only these broad files:
14
15
 
15
- ## Minimum configuration
16
+ - `vona/env/.env.local`
17
+ - `zova/env/.env.local`
16
18
 
17
- Create these files in the new worktree.
19
+ Do not create or modify flavor-, mode-, app-mode-, or runtime-specific `.env.*.local` files for worktree isolation.
18
20
 
19
- ### Vona
21
+ ## Deterministic, secret-safe recommendations
20
22
 
21
- `vona/env/.env.local`
23
+ The explicit setup skill does not inspect `.env`, `.env.local`, `.env.*.local`, sibling configuration, process environment, listening processes, or external services while recommending values. Therefore secrets in local environment files are not supplied to the AI/model, printed to the console, or included in this workflow’s diagnostic output.
22
24
 
23
- ```dotenv
24
- APP_NAME = cabloy-basic-my-worktree
25
- SERVER_LISTEN_PORT = 7113
26
- ```
25
+ The recommendation uses only Git worktree metadata and the fixed defaults below. The primary checkout has ordinal `0`; the first linked worktree has ordinal `1`, the second has ordinal `2`, and so on. For the first proposal, add the linked-worktree ordinal to each baseline port.
26
+
27
+ | Setting | Baseline | First-proposal rule |
28
+ | --------------------- | -------: | --------------------------------- |
29
+ | `SERVER_LISTEN_PORT` | `7102` | `7102 + linked-worktree ordinal` |
30
+ | `DEV_SERVER_PORT` | `9000` | `9000 + linked-worktree ordinal` |
31
+ | `DEV_SERVER_HMR_PORT` | `24679` | `24679 + linked-worktree ordinal` |
32
+
33
+ `APP_NAME` is the current linked worktree directory name. `API_BASE_URL` is regenerated as `http://localhost:<SERVER_LISTEN_PORT>`.
27
34
 
28
- ### Zova
35
+ Every valid explicit setup proposes the same complete tuple and both local files:
29
36
 
30
- `zova/env/.env.local`
37
+ ```dotenv
38
+ # vona/env/.env.local
39
+ APP_NAME = cabloy-worktree-name
40
+ SERVER_LISTEN_PORT = 7103
41
+ ```
31
42
 
32
43
  ```dotenv
33
- APP_NAME = cabloy-basic-my-worktree
34
- API_BASE_URL = http://localhost:7113
35
- DEV_SERVER_PORT = 9013
36
- DEV_SERVER_HMR_PORT = 24693
44
+ # zova/env/.env.local
45
+ APP_NAME = cabloy-worktree-name
46
+ API_BASE_URL = http://localhost:7103
47
+ DEV_SERVER_PORT = 9001
48
+ DEV_SERVER_HMR_PORT = 24680
37
49
  ```
38
50
 
39
- Keep these invariants:
51
+ The user-facing summary is:
52
+
53
+ > 环境隔离信息:Vona 开发 + Zova 开发
54
+
55
+ If the proposal is unsuitable, say **“再换一批”**. The skill increases every listener port by exactly `+1`, regenerates `API_BASE_URL` from the new Vona port, and presents the next tuple for confirmation. It never writes during this step.
56
+
57
+ This deterministic scheme is not a port reservation or a live collision check. If an application later reports that a port is occupied, request another batch before setup or create another linked worktree; successful application startup remains the final authority.
58
+
59
+ ## Configuration boundaries
60
+
61
+ After final confirmation, the skill always writes the two shown files. Keep these invariants:
40
62
 
41
63
  - Vona and Zova use the same unique `APP_NAME`.
42
- - `API_BASE_URL` points to the selected Vona `SERVER_LISTEN_PORT`.
43
- - `SERVER_LISTEN_PORT`, `DEV_SERVER_PORT`, and `DEV_SERVER_HMR_PORT` are unique among concurrently running worktrees.
64
+ - `API_BASE_URL` points to the generated Vona `SERVER_LISTEN_PORT`.
65
+ - `SERVER_LISTEN_PORT`, `DEV_SERVER_PORT`, and `DEV_SERVER_HMR_PORT` are unique within the generated worktree tuple.
66
+ - The skill writes no settings beyond the five shown assignments and creates no flavor-specific local file.
67
+
68
+ Admin and Web are alternative commands that share the generated Zova environment. Do not run both frontend development commands concurrently in one worktree. Configure another linked worktree for concurrent development of the other flavor.
69
+
70
+ The enabled Zova Quasar extension maps `DEV_SERVER_HMR_PORT` to Vite's client HMR/WebSocket listener. It is a separate listener when it differs from `DEV_SERVER_PORT`, so every concurrently running Zova development worktree needs its own unique HMR port.
71
+
72
+ For privacy, the skill writes only to absent or empty permitted broad local files. If either target already contains content, it stops without reading or changing either file; manage the existing local configuration outside this workflow or use a fresh linked worktree.
44
73
 
45
- The base Zova environment defines `SSR_API_BASE_URL = $API_BASE_URL`, so SSR uses the same Vona target in the normal baseline. Add an explicit local `SSR_API_BASE_URL` only if an applicable, more-specific environment file changes that relationship.
74
+ ## Edition-aware commands
75
+
76
+ Use commands from the active repository root.
77
+
78
+ | Edition | Marker | Frontend command | Managed clean E2E |
79
+ | ------------ | ------------------ | -------------------------------------------------- | ------------------------------ |
80
+ | Cabloy Basic | `__CABLOY_BASIC__` | `npm run dev:zova:admin` or `npm run dev:zova:web` | `npm run test:e2e:basic:clean` |
81
+ | Cabloy Start | `__CABLOY_START__` | `npm run dev:zova:admin` or `npm run dev:zova:web` | `npm run test:e2e:start:clean` |
82
+
83
+ Run one frontend command, not both, in each worktree. Both managed clean E2E workflows read Vona's effective local `SERVER_LISTEN_PORT`, then reset, start, and target that local runtime.
46
84
 
47
85
  ## What this isolates
48
86
 
49
87
  A unique `APP_NAME` separates the application identity used by ordinary framework-managed local resources, including managed test database names and Redis-related key prefixes. Worktree-local generated files, runtime files, build output, and coverage output are already separated by their worktree paths.
50
88
 
51
- The selected Vona, Zova, and HMR ports let the worktrees run their ordinary local processes concurrently. The matching Zova API URL prevents the frontend in one worktree from calling the Vona process in another.
89
+ The generated Vona, Zova development HTTP, and Zova HMR ports let worktrees run their ordinary local processes concurrently. The matching Zova API URL prevents the frontend in one worktree from calling the Vona process in another.
52
90
 
53
91
  This is sufficient for the normal local workflows:
54
92
 
55
93
  ```bash
56
94
  npm run dev
95
+ # Run one frontend process in this worktree:
57
96
  npm run dev:zova:admin
97
+ # or
58
98
  npm run dev:zova:web
59
99
  npm run test
60
- npm run test:e2e:basic:clean
61
100
  ```
62
101
 
63
- The managed clean E2E workflow reads Vona's effective local `SERVER_LISTEN_PORT`, then resets, starts, and targets that local runtime.
102
+ ## Resources requiring separate design
64
103
 
65
- ## Add configuration only when the work uses it
66
-
67
- Do not add isolation settings preemptively. Extend the local overrides only when the selected workflow actually shares one of these resources:
104
+ Do not extend this universal five-value setup with additional listener or provider settings. An explicit separate design is required for:
68
105
 
69
106
  - an explicitly named database or a separately configured Redis target
70
107
  - mail, payment, webhook, object-storage, or other external providers
71
- - a standalone mock-build process, using `MOCK_BUILD_PORT`
72
- - an SSR preview or production process, using `SSR_PROD_PORT`
108
+ - standalone mock-build, SSR preview, or production listeners
73
109
 
74
110
  For example, `APP_NAME` namespaces ordinary framework Redis keys, but it does not create a separate Redis server or automatically isolate custom, unprefixed keys. Likewise, an explicitly configured external database remains shared until it is configured separately.
75
111
 
76
- ## Edition-aware note
112
+ ## Guided setup and initialization
113
+
114
+ For confirmation-gated setup, explicitly invoke `/cabloy-worktree-environment`. The skill validates the linked worktree and edition, derives a secret-safe proposal from Git metadata and fixed port baselines, and writes both allowed broad local files after final confirmation.
77
115
 
78
- This page describes the current Cabloy Basic workflow. For Cabloy Start, detect the active edition first and inspect the Start repository's current scripts, flavors, environment files, and generated-output paths before applying this recipe.
116
+ After local overrides are validated, decide separately whether to run `npm run init`. It is not an environment allocator: it installs dependencies and runs generation/build-related work. When it runs from a linked Git worktree rooted at the project, it preserves the tracked base `APP_NAME` defaults in `vona/env/.env` and `zova/env/.env`; the generated broad `.env.local` files supply the worktree-specific identity. Primary-checkout and non-Git project initialization may still establish base `APP_NAME` from the project directory.
79
117
 
80
118
  ## Read together with
81
119
 
@@ -297,6 +297,7 @@ For most real business scenarios, the correct answer is to create the suite firs
297
297
  This guide should be read together with:
298
298
 
299
299
  - [Package Map](/reference/package-map)
300
+ - [Vona Module Dependencies](/backend/module-dependencies)
300
301
  - [Backend Directory Structure](/reference/backend-directory-structure)
301
302
  - [Frontend Directory Structure](/reference/frontend-directory-structure)
302
303
  - [Backend CLI](/backend/cli)
@@ -81,6 +81,7 @@ Use this package map together with:
81
81
  - [Suites and Modules](/fullstack/suites-and-modules)
82
82
  - [Backend Essentials](/backend/backend-essentials)
83
83
  - [Backend Foundation](/backend/foundation)
84
+ - [Vona Module Dependencies](/backend/module-dependencies)
84
85
  - [Backend CLI](/backend/cli)
85
86
  - [Backend Scripts](/backend/scripts)
86
87
  - [Backend Directory Structure](/reference/backend-directory-structure)
@@ -796,6 +796,95 @@ test(
796
796
  },
797
797
  );
798
798
 
799
+ test(
800
+ 'ATP-SPC-01: Coupon Template renders semantic Admin field controls',
801
+ { tag: ['@admin', '@flow', '@fia'] },
802
+ async ({ browser }) => {
803
+ test.setTimeout(60_000);
804
+ const adminContext = await browser.newContext();
805
+ const adminPage = await adminContext.newPage();
806
+ const adminPageErrors = collectPageErrors(adminPage);
807
+ try {
808
+ await adminPage.setViewportSize({ width: 1440, height: 900 });
809
+ await login(adminPage, '/commerce-admin/', 'admin', '123456', 'commerceAdmin');
810
+ await adminPage.goto(
811
+ '/commerce-admin/rest/resource/commerce-promotion%3AcouponTemplate/create',
812
+ {
813
+ waitUntil: 'load',
814
+ },
815
+ );
816
+ await expect(adminPage).toHaveURL(
817
+ /\/commerce-admin\/rest\/resource\/commerce-promotion(?:%3A|:|%253A)couponTemplate\/create(?:[/?#]|$)/,
818
+ );
819
+
820
+ for (const groupName of [
821
+ 'Basic Information',
822
+ 'Discount Policy',
823
+ 'Validity Window',
824
+ 'Usage Limits',
825
+ ]) {
826
+ await expect(adminPage.getByRole('group', { name: groupName })).toBeVisible();
827
+ }
828
+
829
+ const state = adminPage
830
+ .getByRole('group', { name: 'Template State *' })
831
+ .getByRole('combobox');
832
+ await expect(state).toBeVisible();
833
+ await state.selectOption({ label: 'Active' });
834
+ await expect(state).toHaveValue('active');
835
+
836
+ const discountInput = adminPage
837
+ .getByRole('group', { name: 'Fixed Discount *' })
838
+ .getByRole('textbox');
839
+ const minSpendInput = adminPage
840
+ .getByRole('group', { name: 'Minimum Spend *' })
841
+ .getByRole('textbox');
842
+ await discountInput.fill('12.34');
843
+ await minSpendInput.fill('45.67');
844
+ await expect(discountInput).toHaveValue('12.34');
845
+ await expect(minSpendInput).toHaveValue('45.67');
846
+ await expect(
847
+ adminPage.getByRole('group', { name: 'Valid From *' }).locator('input[type="date"]'),
848
+ ).toBeVisible();
849
+ await expect(
850
+ adminPage.getByRole('group', { name: 'Valid Until *' }).locator('input[type="date"]'),
851
+ ).toBeVisible();
852
+
853
+ await adminPage
854
+ .getByRole('group', { name: 'Name *' })
855
+ .getByRole('textbox')
856
+ .fill('E2E Coupon Template');
857
+ await adminPage.getByRole('group', { name: 'Currency *' }).getByRole('textbox').fill('USD');
858
+ await adminPage
859
+ .getByRole('group', { name: 'Valid From *' })
860
+ .getByRole('textbox')
861
+ .fill('2026-08-04');
862
+ await adminPage
863
+ .getByRole('group', { name: 'Valid Until *' })
864
+ .getByRole('textbox')
865
+ .fill('2026-12-31');
866
+ await adminPage
867
+ .getByRole('group', { name: 'Total Issue Limit' })
868
+ .getByRole('textbox')
869
+ .fill('10');
870
+ await adminPage
871
+ .getByRole('group', { name: 'Total Usage Limit' })
872
+ .getByRole('textbox')
873
+ .fill('10');
874
+ await adminPage
875
+ .getByRole('group', { name: 'Per-customer Issue Limit' })
876
+ .getByRole('textbox')
877
+ .fill('1');
878
+
879
+ await expect(adminPage.getByRole('button', { name: 'Submit', exact: true })).toBeVisible();
880
+ await expect(adminPage.getByRole('button', { name: 'Back', exact: true })).toBeVisible();
881
+ expect(adminPageErrors).toEqual([]);
882
+ } finally {
883
+ await adminContext.close().catch(() => {});
884
+ }
885
+ },
886
+ );
887
+
799
888
  test(
800
889
  'Commerce Cart: anonymous browser is redirected to login',
801
890
  { tag: ['@web', '@cart'] },