cabloy 5.1.158 → 5.1.159

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 (88) hide show
  1. package/.cabloy-version +1 -1
  2. package/.claude/skills/cabloy-contract-loop/SKILL.md +24 -7
  3. package/.claude/skills/cabloy-contract-loop/evals/evals.json +12 -0
  4. package/.claude/skills/cabloy-contract-loop/references/contract-loop-map.md +28 -0
  5. package/.claude/skills/cabloy-contract-loop/references/verification-checklist.md +19 -2
  6. package/.claude/skills/cabloy-resource-field-update/SKILL.md +18 -0
  7. package/.claude/skills/cabloy-zova-source-reading/SKILL.md +14 -0
  8. package/.claude/skills/cabloy-zova-source-reading/references/core-reading-paths.md +10 -0
  9. package/.github/workflows/docs-pages.yml +18 -2
  10. package/CHANGELOG.md +19 -0
  11. package/package.json +2 -1
  12. package/repo-docs/.vitepress/config.mjs +34 -4
  13. package/repo-docs/.vitepress/theme/components/GitHubRepositoriesNav.vue +255 -0
  14. package/repo-docs/.vitepress/theme/custom.css +21 -0
  15. package/repo-docs/.vitepress/theme/index.js +7 -1
  16. package/repo-docs/backend/resource-field-update.md +48 -1
  17. package/repo-docs/blogs/index.md +2 -0
  18. package/repo-docs/frontend/form-layout-guide.md +6 -6
  19. package/repo-docs/frontend/page-guide.md +12 -0
  20. package/repo-docs/frontend/page-meta-guide.md +27 -0
  21. package/repo-docs/frontend/ssr-architecture-overview.md +4 -0
  22. package/repo-docs/frontend/ssr-build-deploy-guide.md +1 -1
  23. package/repo-docs/frontend/zova-form-source-reading-map.md +2 -2
  24. package/repo-docs/frontend/zova-form-under-the-hood.md +1 -1
  25. package/repo-docs/frontend/zova-reactivity-under-the-hood.md +65 -9
  26. package/repo-docs/fullstack/contract-loop-playbook.md +2 -0
  27. package/repo-docs/fullstack/ssr-site-and-flavor-setup.md +198 -0
  28. package/repo-docs/fullstack/vona-zova-integration.md +2 -0
  29. package/repo-e2e/docs/playwright.config.ts +37 -0
  30. package/repo-e2e/docs/specs/blogs-index.spec.ts +180 -0
  31. package/repo-e2e/docs/specs/github-repositories-nav.spec.ts +69 -0
  32. package/repo-e2e/docs/test-results/.last-run.json +4 -0
  33. package/repo-e2e/specs/cabloy-basic.spec.ts +136 -2
  34. package/vona/packages-cli/cli/package.json +1 -1
  35. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/entity/{{resourceName}}.tsx_ +1 -1
  36. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/entity/{{resourceName}}.tsx_ +1 -1
  37. package/vona/packages-cli/cli-set-api/package.json +1 -1
  38. package/vona/packages-vona/vona/package.json +1 -1
  39. package/vona/pnpm-lock.yaml +21 -80
  40. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/en-us.ts +3 -0
  41. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/zh-cn.ts +3 -0
  42. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productCreate.tsx +2 -0
  43. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productUpdate.tsx +2 -0
  44. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productView.tsx +2 -0
  45. package/vona/src/suite/a-training/modules/training-record/src/dto/recordCreate.tsx +1 -0
  46. package/vona/src/suite/a-training/modules/training-record/src/dto/recordSelectReq.tsx +3 -7
  47. package/vona/src/suite/a-training/modules/training-record/src/dto/recordUpdate.tsx +1 -0
  48. package/vona/src/suite/a-training/modules/training-record/src/dto/recordView.tsx +1 -0
  49. package/vona/src/suite/a-training/modules/training-record/src/entity/record.tsx +8 -0
  50. package/vona/src/suite/a-training/modules/training-record/test/record.test.ts +51 -0
  51. package/vona/src/suite/a-training/modules/training-student/src/config/locale/en-us.ts +3 -0
  52. package/vona/src/suite/a-training/modules/training-student/src/config/locale/zh-cn.ts +3 -0
  53. package/vona/src/suite/a-training/modules/training-student/src/dto/studentCreate.tsx +1 -0
  54. package/vona/src/suite/a-training/modules/training-student/src/dto/studentSelectResItem.tsx +1 -0
  55. package/vona/src/suite/a-training/modules/training-student/src/dto/studentUpdate.tsx +1 -0
  56. package/vona/src/suite/a-training/modules/training-student/src/dto/studentView.tsx +1 -0
  57. package/vona/src/suite/a-training/modules/training-student/src/entity/student.tsx +10 -1
  58. package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +99 -1
  59. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
  60. package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
  61. package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
  62. package/vona/src/suite-vendor/a-vona/modules/a-web/package.json +1 -1
  63. package/vona/src/suite-vendor/a-vona/modules/a-web/src/bean/pipe.filter.ts +85 -57
  64. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  65. package/zova/packages-cli/cli/package.json +2 -2
  66. package/zova/packages-cli/cli-set-front/cli/templates/rest/render.ts +2 -0
  67. package/zova/packages-cli/cli-set-front/cli/templates/rest/rest.ts +13 -3
  68. package/zova/packages-cli/cli-set-front/package.json +1 -1
  69. package/zova/packages-zova/zova/package.json +2 -2
  70. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockPage/controller.tsx +31 -11
  71. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockTable/controller.tsx +3 -0
  72. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/component/blockPageEntry/controller.tsx +1 -1
  73. package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/table/render.tsx +68 -8
  74. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
  75. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +4 -0
  76. package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
  77. package/zova/src/suite-vendor/a-zova/modules/a-form/package.json +1 -1
  78. package/zova/src/suite-vendor/a-zova/modules/a-form/src/lib/formLayout.ts +0 -6
  79. package/zova/src/suite-vendor/a-zova/modules/a-form/test/lib/formLayout.test.ts +16 -9
  80. package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
  81. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/model/sdk.ts +4 -0
  82. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/rest.ts +13 -2
  83. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/schema.ts +1 -0
  84. package/zova/src/suite-vendor/a-zova/modules/a-table/package.json +1 -1
  85. package/zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/controller.tsx +50 -0
  86. package/zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/render.tsx +60 -7
  87. package/zova/src/suite-vendor/a-zova/modules/a-table/src/types/table.ts +6 -4
  88. package/zova/src/suite-vendor/a-zova/package.json +4 -4
@@ -13,6 +13,7 @@ Use it when you need to:
13
13
  - add a stored field to an existing resource;
14
14
  - refine validation, titles, OpenAPI metadata, or `ZovaRender.field(...)` / `ZovaRender.cell(...)` metadata for an existing field;
15
15
  - add enum-like field constraints;
16
+ - filter or sort an existing foreign-key field by a display column from its related table without replacing the stored foreign-key contract;
16
17
  - decide whether a persisted field change increments `vonaModule.fileVersion`;
17
18
  - add a custom form-field or table-cell renderer because shared renderer options are insufficient.
18
19
 
@@ -93,6 +94,43 @@ For projected fields, use `$makeMetadata(...)` for metadata-only refinement and
93
94
 
94
95
  Read [DTO Infer and Generation](/backend/dto-infer-generation) when inference cannot express the intended contract. If serialization metadata controls the returned value, also confirm that the target controller action opts into `@Core.serializer()`.
95
96
 
97
+ ## Filter and sort a relation by its display field
98
+
99
+ A resource list can search and order a relation by a human-readable column from its related table without changing the persisted relation contract. Keep the entity field as the foreign-key identity and place the join mapping on that entity field. For example, `studentId` remains a `TableIdentity` even though the list filter accepts part of the student's name:
100
+
101
+ ```ts
102
+ @Api.field(
103
+ v.filter({
104
+ table: 'trainingStudent',
105
+ joinType: 'innerJoin',
106
+ joinOn: ['studentId', 'trainingStudent.id'],
107
+ originalName: 'name',
108
+ op: '_includesI_',
109
+ }),
110
+ ZovaRender.column({ enableSorting: true }),
111
+ v.tableIdentity(),
112
+ )
113
+ studentId: TableIdentity;
114
+ ```
115
+
116
+ `table` and `joinOn` identify the relation, while `originalName` resolves both the `studentId` filter and order key to `trainingStudent.name`. `_includesI_` makes the filter a case-insensitive partial-name match. `ZovaRender.column({ enableSorting: true })` exposes the sortable table column; the query pipeline uses the same field metadata to add the join and rewrite `orders: [['studentId', 'asc']]` to the related display column.
117
+
118
+ Use `innerJoin` deliberately when the relation is required and unmatched rows should not participate in this filter/order path. For an optional relation whose unmatched rows must remain visible, choose the join behavior explicitly instead of copying the required-relation example.
119
+
120
+ Keep `studentId` as the public filter and order key. In the select request DTO, override only its query-input schema and renderer so a text fragment reaches the join-backed filter; the entity and create/update contracts remain foreign-key identities:
121
+
122
+ ```ts
123
+ fields: {
124
+ studentId: $makeSchema(
125
+ ZovaRender.field('basic-input:formFieldInput'),
126
+ v.optional(),
127
+ z.string(),
128
+ ),
129
+ },
130
+ ```
131
+
132
+ Do not add a parallel `studentName` query field unless the API intentionally needs distinct ID and display-name filtering semantics. For the underlying query behavior, see [ORM Select Guide](/backend/orm-select-guide). For the list-page query path, see [Filter to Query to Select Data Flow](/frontend/filter-query-select-data-flow-guide).
133
+
96
134
  ## Choose the renderer branch
97
135
 
98
136
  Prefer the smallest rendering change that expresses the requirement:
@@ -141,6 +179,14 @@ Minimum backend coverage generally includes:
141
179
  - get-by-id or view response;
142
180
  - delete behavior when it is relevant.
143
181
 
182
+ For join-backed relation filtering and sorting, also verify:
183
+
184
+ - the projected request field is optional text while the entity remains a foreign-key identity;
185
+ - direct DTO metadata retains the intended `table`, `joinType`, `joinOn`, `originalName`, and operator;
186
+ - the select action exposes the intended query parameter without an unintended parallel display-name parameter;
187
+ - filtering returns records whose related display value matches the text fragment;
188
+ - ascending and descending orders return the expected related-display ordering, including the selected join behavior for unmatched rows.
189
+
144
190
  For constrained enum-like values, add a negative test that proves an invalid value is rejected. Test-local persisted resources must be deleted in `finally` using precise owned identities and reverse dependency order.
145
191
 
146
192
  ## Verification checklist
@@ -151,6 +197,7 @@ Choose checks that match the layers changed:
151
197
  - run the narrow resource test and relevant typecheck;
152
198
  - run `npm run test` for any `meta.version.ts` change so the test database is recreated and migration consistency is exercised;
153
199
  - run the relevant frontend metadata/build/dependency synchronization when custom renderer resources are involved;
154
- - verify action-level serializer behavior with an API test when `v.serializer*` metadata changes returned fields.
200
+ - verify action-level serializer behavior with an API test when `v.serializer*` metadata changes returned fields;
201
+ - for join-backed filters and sorting, inspect transformed `where`, `orders`, and `joins`, then exercise the resource endpoint so metadata-only success cannot hide incorrect query behavior.
155
202
 
156
203
  Finish by confirming the backend contract, frontend resources, generated handoff, and user-visible locale labels all describe the same field behavior.
@@ -1,5 +1,7 @@
1
1
  ---
2
2
  title: Blogs
3
+ pageClass: cabloy-blogs-index
4
+ aside: false
3
5
  ---
4
6
 
5
7
  # Blogs
@@ -219,11 +219,11 @@ Set `layout: 'flow'` when compact fields should appear from left to right withou
219
219
 
220
220
  ## How the resolver handles the declared tree
221
221
 
222
- Before rendering, `resolveFormLayout(...)` reconciles `formLayout` with the current scene's resolved schema properties. This makes the declaration a **placement overlay**, not an allow-list. The preceding OpenAPI normalization step is explained in [OpenAPI Runtime Under the Hood](/frontend/a-openapi-under-the-hood#scene-overlays-fieldsource-and-preserved-aliases).
222
+ Before rendering, `resolveFormLayout(...)` reconciles `formLayout` with the current scene's resolved schema properties. A configured Form Layout is an **explicit allow-list**: it renders only successfully resolved declared fields and explicit block nodes. The preceding OpenAPI normalization step is explained in [OpenAPI Runtime Under the Hood](/frontend/a-openapi-under-the-hood#scene-overlays-fieldsource-and-preserved-aliases).
223
223
 
224
- ### Eligible and omitted fields
224
+ ### Eligible and declared fields
225
225
 
226
- Only schema properties with `rest.visible !== false` are eligible. When an eligible visible field is absent from `formLayout`, the resolver appends it as a root-level field after the declared nodes, in schema-property order.
226
+ Only schema properties with `rest.visible !== false` are eligible for a field declaration. Eligibility alone does not render a field: a visible field absent from `formLayout.children` does not render and produces no diagnostic.
227
227
 
228
228
  A field that uses `fieldSource` is represented at runtime by its nested canonical source key. The loader stores that source path in `key`, retains the first differing original schema name in `schemaKey`, and retains further coalesced names in `schemaKeys`. A property already named by its canonical key retains that identity in `key`. These preserved names are aliases for declaration matching, not separate fields or bindings.
229
229
 
@@ -237,9 +237,9 @@ For example, a real relation declaration `studentContentForm` can resolve to the
237
237
 
238
238
  An alias or relation prefix must resolve to exactly one eligible canonical source. If multiple visible source keys match, it is unresolved and receives `unknownField`; declare the exact canonical source key instead. Exact canonical matches win over colliding aliases. Invisible properties contribute neither eligible fields nor usable aliases.
239
239
 
240
- Duplicate declarations are detected by canonical key, so an alias and `content.descriptionMarkdown` cannot render the same field twice. Resolved field names, duplicate identity, and tab paths all use the canonical key. If a declaration is removed because it is unknown, invisible, ambiguous, or duplicate, otherwise unplaced eligible canonical fields are still appended at the root.
240
+ Duplicate declarations are detected by canonical key, so an alias and `content.descriptionMarkdown` cannot render the same field twice. Resolved field names, duplicate identity, and tab paths all use the canonical key. If a declaration is removed because it is unknown, invisible, ambiguous, or duplicate, it does not render and no other eligible fields are added as a fallback.
241
241
 
242
- If a field must not render, make it invisible in schema metadata. Leaving it out of `formLayout.children` is not enough.
242
+ Use `rest.visible: false` when a field must be ineligible in the schema scene. In a configured Form Layout, leave a visible field out of `formLayout.children` when it should not render there.
243
243
 
244
244
  ### Invalid declarations and diagnostics
245
245
 
@@ -384,7 +384,7 @@ Here `formFieldLayout.inline: true` controls how each field wrapper is presented
384
384
  5. Use `layout`, `formFieldLayout`, `options`, or provider behaviors when the requirement is one field's wrapper or renderer.
385
385
  6. Keep entry actions in page-entry toolbar blocks. Keep filter action semantics in `basic-page:blockFilterActions`; place that block inside Form Layout when the actions must share structural Grid or flow placement with fields.
386
386
  7. For maintained Cabloy Basic list filters, prefer one inline flow section that explicitly lists every real filter-schema field in schema order and ends with one embedded `basic-page:blockFilterActions` block. Do not add synthetic request-only filter fields, alter filter transforms, or combine it with a sibling action block.
387
- 8. Review field names against the scene-specific schema. Unlisted visible fields are appended; unknown and duplicate declarations are silently pruned from the rendered plan.
387
+ 8. Review field names against the scene-specific schema. Explicitly list every field that should render; omitted visible fields do not render, while unknown and duplicate declarations are silently pruned from the rendered plan.
388
388
  9. Prefer an exact canonical source path when an alias or relation prefix could match multiple visible fields. Do not list both an alias and its canonical key; canonical duplicate detection keeps only the first declaration.
389
389
 
390
390
  ## Source-reading and verification path
@@ -66,6 +66,18 @@ class ControllerPageCounter {
66
66
  }
67
67
  ```
68
68
 
69
+ ### Keep callbacks on the reactive bean path
70
+
71
+ Use ordinary controller methods as the default for actions that change controller state. Do not expose a state-mutating class-field arrow callback such as `onIncrement = () => this.count++`: it captures construction-time `this` before Zova exposes the controller's reactive/proxied bean.
72
+
73
+ At a TSX or component-prop boundary, forward to the method instead:
74
+
75
+ ```typescript
76
+ onClick={() => this.increment()}
77
+ ```
78
+
79
+ When an external API requires a stable callback reference, create the closure in `__init__()` after the controller has been prepared. See [Zova Reactivity Under the Hood](/frontend/zova-reactivity-under-the-hood#construction-time-this-and-the-exposed-reactive-bean) for the lifecycle reason and safe pattern.
80
+
69
81
  ## Add render logic
70
82
 
71
83
  Representative TSX render pattern:
@@ -90,6 +90,33 @@ export interface IPageMeta {
90
90
 
91
91
  In the current Basic source, this is typically what the Admin layout uses for the level-2 item label instead of the broader menu-backed workspace title.
92
92
 
93
+ ### Resource entry `pageTitleKey`
94
+
95
+ For schema-driven Resource entry pages, `basic-pageentry:blockPageEntry` derives `pageTitle` from the current form data. Its default `pageTitleKey` is `name`.
96
+
97
+ When the resource's human-readable identifier uses another top-level field, configure that field explicitly on the page-entry block in the DTO:
98
+
99
+ ```typescript
100
+ @Dto({
101
+ blocks: [
102
+ ZovaRender.block('basic-pageentry:blockPageEntry', {
103
+ pageTitleKey: 'title',
104
+ blocks: [
105
+ // form and toolbar blocks
106
+ ],
107
+ }),
108
+ ],
109
+ })
110
+ ```
111
+
112
+ Use the same key in the create, update, and view DTOs when those scenes should show a consistent task title. Common choices include `title` and `code`; do not add a synthetic `name` field merely to satisfy the default.
113
+
114
+ Keep these boundaries in mind:
115
+
116
+ - `pageTitleKey` controls the routed-shell level-2 task title, not the resource or OpenAPI schema title and not the browser document title.
117
+ - The current implementation reads one exact top-level form-data key. It is not a dotted-path resolver or a formatter.
118
+ - When the source field is `null` or `undefined`, the current page-entry runtime falls back to the resolved form DTO schema's OpenAPI `title`. Give the create DTO an operation-specific title such as `openapi: { title: $locale('CreateProduct') }` when it needs an initial task label; a populated source field takes precedence. The fallback is not used for an empty string.
119
+
93
120
  ### `pageDirty`
94
121
 
95
122
  `pageDirty` tells the routed shell whether the current work item should appear dirty.
@@ -187,6 +187,10 @@ What can differ by edition is usually:
187
187
 
188
188
  So the architecture model is shared, while some concrete frontend examples remain edition-sensitive.
189
189
 
190
+ ## Adding an independent SSR site
191
+
192
+ The architecture maps one Vona SSR site to one built Zova flavor bundle. When a new deployable mount needs its own flavor and Vona dispatch registration, follow [Independent SSR Site and Flavor Setup](/fullstack/ssr-site-and-flavor-setup). It covers the matching site ID, public path, bundle path, generated REST types, paired build wrapper, and hydration proof.
193
+
190
194
  ## Recommended reading order
191
195
 
192
196
  Use this order when you need the shortest path from mental model to implementation detail:
@@ -130,7 +130,7 @@ If you need deeper script control or need to verify the exact Zova flavor path,
130
130
 
131
131
  ### Build frontend SSR output only
132
132
 
133
- Use this when you need the frontend SSR artifacts refreshed but do not yet need the full backend build flow.
133
+ Use this when you need the frontend SSR artifacts refreshed but do not yet need the full backend build flow. If the task creates a new independently mounted site rather than refreshing an existing one, first follow [Independent SSR Site and Flavor Setup](/fullstack/ssr-site-and-flavor-setup): the new site needs its own source-confirmed wrapper that pairs its SSR and REST outputs, not a renamed default Admin or Web wrapper.
134
134
 
135
135
  Representative current Basic command:
136
136
 
@@ -233,7 +233,7 @@ Use this path when you are asking questions like:
233
233
  - where does `formLayout` come from in a resource DTO?
234
234
  - how are fields, embedded blocks, sections, groups, and tabs normalized before rendering?
235
235
  - how do canonical keys, preserved schema aliases, and unique relation-prefix shorthand resolve to one field?
236
- - why are omitted visible fields appended or duplicate fields removed?
236
+ - why do omitted fields not render, or duplicate fields get removed?
237
237
  - where does Cabloy Basic render responsive grids and tab error badges?
238
238
 
239
239
  ### Read the docs first
@@ -266,7 +266,7 @@ Use this path when you are asking questions like:
266
266
  - the resolver filters visible fields, resolves exact canonical keys before unique aliases and unique prefixes, and records duplicate identity and tab paths by canonical key
267
267
  - the Basic block controller renders sections, groups, tabs, field spans, and embedded blocks while delegating canonical field names to `$$form.renderField(...)`
268
268
  - `blockFilterActions` shows how a block rendered inside Form Layout reuses the inherited form CEL scope to invoke `$$filter`
269
- - the OpenAPI and Form Layout unit tests verify canonicalization, alias precedence, ambiguity, duplicates, visibility, root append, and tab paths
269
+ - the OpenAPI and Form Layout unit tests verify canonicalization, explicit field inclusion, alias precedence, ambiguity, duplicates, visibility, and tab paths
270
270
  - the Student test verifies emitted metadata nesting, columns, spans, embedded action blocks, and optional IDs; it is not a browser rendering test
271
271
 
272
272
  ## 8. Resource-driven CRUD page integration
@@ -454,7 +454,7 @@ That means automatic schema-driven rendering is not happening magically in the w
454
454
 
455
455
  When `ZForm` receives a nonempty block list, the render bean delegates body rendering to those blocks instead of iterating schema fields directly. For Cabloy Basic structural forms, `basic-form:blockFormLayout` resolves `formLayout` against the form's current schema properties and calls `$$form.renderField(...)` for each surviving layout field.
456
456
 
457
- For a field declaration, the shared resolver first accepts an exact canonical key, then a uniquely mapped preserved schema alias, and then a unique relation-prefix shorthand. It rewrites an alias or shorthand to the canonical key before calling `$$form.renderField(...)`. Exact canonical keys win over colliding aliases; ambiguous aliases or prefixes are reported as `unknownField`. Duplicate tracking and tab-path bookkeeping use the canonical key, while visible canonical fields not placed by a surviving declaration are appended afterward. See [Form Layout Guide](/frontend/form-layout-guide#how-the-resolver-handles-the-declared-tree) for the full DTO authoring rules.
457
+ For a field declaration, the shared resolver first accepts an exact canonical key, then a uniquely mapped preserved schema alias, and then a unique relation-prefix shorthand. It rewrites an alias or shorthand to the canonical key before calling `$$form.renderField(...)`. Exact canonical keys win over colliding aliases; ambiguous aliases or prefixes are reported as `unknownField`. A configured Form Layout is an explicit allow-list: only fields represented by surviving declarations render. Duplicate tracking and tab-path bookkeeping use the canonical key. See [Form Layout Guide](/frontend/form-layout-guide#how-the-resolver-handles-the-declared-tree) for the full DTO authoring rules.
458
458
 
459
459
  Form Layout also supports a leaf `block` node. It wraps an existing resource block descriptor and the Basic renderer invokes it with the inherited `IJsxRenderContextForm`, including the same JSX runtime and CEL scope. The node has no schema property or field value; for example, a filter can place `basic-page:blockFilterActions` inside a flow section while that action block continues to read `$$filter` from the filter-owned form scope.
460
460
 
@@ -151,6 +151,62 @@ This is one of the most important source-level facts for understanding Zova.
151
151
 
152
152
  Zova does not require the business author to write `reactive({ ... })` around controller state, because the framework already treats the controller bean as the reactive object.
153
153
 
154
+ #### Construction-time `this` and the exposed reactive bean
155
+
156
+ There is one lifecycle boundary that matters when a callback closes over `this`:
157
+
158
+ 1. the container first constructs the raw class instance with `new BeanClass(...)`;
159
+ 2. it then exposes the framework-managed reactive/proxied bean; and
160
+ 3. it invokes `__init__()` only after that preparation completes.
161
+
162
+ That distinction is normally invisible when a controller uses ordinary methods and render-time `this.member` access. It matters for a class-field arrow callback that mutates controller state, because the arrow is created during construction and lexically captures the construction-time raw `this`:
163
+
164
+ ```typescript
165
+ class ControllerPageCounter {
166
+ count = 0;
167
+
168
+ // Avoid for a state-mutating controller callback.
169
+ onIncrement = () => {
170
+ this.count++;
171
+ };
172
+ }
173
+ ```
174
+
175
+ The field can change on the raw instance, but the mutation bypasses the reactive/proxied bean through which render dependencies were collected. Vue therefore receives no invalidation notification. A later update to another dependency can rerender the component and reveal the changed underlying value, which can make this look intermittent.
176
+
177
+ Prefer an ordinary controller method for a normal TSX action:
178
+
179
+ ```typescript
180
+ class ControllerPageCounter {
181
+ count = 0;
182
+
183
+ increment() {
184
+ this.count++;
185
+ }
186
+
187
+ protected render() {
188
+ return <button onClick={() => this.increment()}>Increment</button>;
189
+ }
190
+ }
191
+ ```
192
+
193
+ If an external API requires a stable stored callback, create that closure in `__init__()` instead. At that point, its lexical `this` is the framework-exposed reactive/proxied bean:
194
+
195
+ ```typescript
196
+ class ControllerPageCounter {
197
+ count = 0;
198
+ onIncrement: () => void;
199
+
200
+ protected async __init__() {
201
+ this.onIncrement = () => {
202
+ this.count++;
203
+ };
204
+ }
205
+ }
206
+ ```
207
+
208
+ This is not a rule that arrow functions are generally non-reactive. The narrow hazard is a **class-field arrow callback that captures construction-time `this` and mutates bean state**.
209
+
154
210
  ### 4. `$computed()` is an instance-scoped wrapper around Vue `computed(...)`
155
211
 
156
212
  In:
@@ -238,21 +294,21 @@ A practical reading takeaway is:
238
294
 
239
295
  ### 7. Field mutation becomes normal reactive invalidation and rerender
240
296
 
241
- Once the controller bean is reactive and render has read its fields, changes such as:
297
+ Once render has read fields through the framework-exposed reactive/proxied controller, a mutation through that same identity, such as:
242
298
 
243
299
  ```typescript
244
300
  this.count++;
245
301
  ```
246
302
 
247
- behave the way a Vue reader would expect at the reactive-engine level:
303
+ behaves the way a Vue reader would expect at the reactive-engine level:
248
304
 
249
305
  - the field change invalidates dependencies
250
306
  - computed values depending on that field are recomputed
251
307
  - the next render sees the updated values
252
308
 
253
- So the runtime behavior is still recognizably Vue-like.
309
+ The construction-time class-field arrow case described above is the exception: its raw `this` can change the underlying field without triggering the dependency that render collected through the exposed bean.
254
310
 
255
- The architectural surface is what changed.
311
+ So the runtime behavior is still recognizably Vue-like. The architectural surface, including the bean lifecycle and identity boundary, is what changed.
256
312
 
257
313
  ## A compact call-flow sketch
258
314
 
@@ -260,13 +316,13 @@ The architectural surface is what changed.
260
316
  useControllerPage(...)
261
317
  -> _useController(...)
262
318
  -> ctx.bean._newBeanInner(..., markReactive = true)
263
- -> BeanContainer creates controller bean
264
- -> BeanContainer applies reactive(...)
265
- -> controller __init__ wires $computed / $watch helpers
319
+ -> BeanContainer constructs the raw controller bean
320
+ -> BeanContainer exposes the reactive/proxied controller bean
321
+ -> controller __init__ wires $computed / $watch helpers and any stored callbacks
266
322
  -> component render is patched toward controller/render bean
267
323
  -> render-time controller data update refreshes page route data
268
- -> render reads controller fields
269
- -> field mutation invalidates dependencies
324
+ -> render reads fields through the exposed bean
325
+ -> mutation through that same identity invalidates dependencies
270
326
  -> rerender produces updated UI
271
327
  ```
272
328
 
@@ -146,6 +146,8 @@ Then use the **reverse chain**:
146
146
 
147
147
  See [Frontend Metadata Back to Backend](/fullstack/frontend-metadata-to-backend) for the end-to-end reverse-chain bridge from frontend-owned truth to backend-visible shared handoff.
148
148
 
149
+ When the frontend change creates a new independently mounted SSR site rather than refreshing an existing consumer, use [Independent SSR Site and Flavor Setup](/fullstack/ssr-site-and-flavor-setup). The new Vona consumer needs a matched flavor, paired SSR/REST build wrapper, site registration, and dispatch/hydration proof.
150
+
149
151
  ### 3. Do generated artifacts look stale?
150
152
 
151
153
  Examples:
@@ -0,0 +1,198 @@
1
+ # Independent SSR Site and Flavor Setup
2
+
3
+ Use this guide when adding a new deployable Zova SSR surface that Vona must dispatch and serve. It covers a new **site + flavor** pair, not merely a page or route inside an existing site.
4
+
5
+ The durable outcome is one aligned identity tuple:
6
+
7
+ ```text
8
+ Zova flavor
9
+
10
+ flavor env/config + Zova SSR/REST scripts
11
+
12
+ root paired build wrapper
13
+
14
+ SSR release directory + generated REST package
15
+
16
+ Vona @SsrSite registration
17
+ ```
18
+
19
+ Do not substitute identifiers from an existing Admin, Web, or business site. Select and verify every member of the tuple together.
20
+
21
+ ## Before you start
22
+
23
+ ### Detect the active edition
24
+
25
+ Read the repository marker first:
26
+
27
+ - `__CABLOY_BASIC__` → use the current Basic scripts, flavors, UI baselines, site modules, and paths.
28
+ - `__CABLOY_START__` → inspect the active Start repository before naming a flavor, wrapper, generated package, or site module.
29
+ - both markers → stop: the checkout is ambiguous.
30
+
31
+ The framework model is shared, but the exact flavor names, site baselines, UI layer, assets, scripts, and generated output paths can differ by edition. See [Edition Detection](/editions/detection) and [Edition Collaboration Differences](/fullstack/edition-collaboration-differences).
32
+
33
+ ### Confirm that a new site is needed
34
+
35
+ | Change | Normal boundary |
36
+ | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
37
+ | New page or route inside an existing SSR site | Page, route, model, and existing site verification; normally no new flavor or `@SsrSite`. |
38
+ | Frontend metadata/resource handoff to an existing backend consumer | Reverse contract loop; rebuild every affected existing flavor pair, then run `npm run deps:vona`. |
39
+ | New deployable URL mount, SSR admission policy, assets, or independent frontend composition | New independent flavor and Vona SSR site; continue with this guide. |
40
+
41
+ A new site is an ownership decision, not a convenient alias for an existing Admin or Web surface.
42
+
43
+ ## Define the identity tuple
44
+
45
+ Choose these values before editing source. Keep their spelling and ownership consistent:
46
+
47
+ | Identity | Role |
48
+ | ---------------------- | --------------------------------------------------------------------------------------------------------------- |
49
+ | `<site-flavor>` | Exact Zova flavor identifier used in env/config, lower-level scripts, build output, and generated REST package. |
50
+ | `<site-id>` | Vona SSR site identity and Zova `SITE_ID`; unique among enabled sites. |
51
+ | `<public-path>` | External mount path; aligned between Zova `APP_PUBLIC_PATH` and Vona `publicPath`; unique among enabled sites. |
52
+ | `<site-module>` | Vona module that owns the site bean and copied SSR assets. |
53
+ | `<bundle-path>` | Copied SSR release directory under `<site-module>/assets/site`; exactly matches Vona `bundlePath`. |
54
+ | `<zova-rest-package>` | Flavor-specific generated REST/type package imported by the Vona site bean. |
55
+ | `<root-build-wrapper>` | Root command that produces the selected SSR bundle **and** REST/type package. |
56
+
57
+ Only one enabled SSR site may own the empty/root public path. A non-root site’s route aliases are relative to the router mount base; do not repeat `<public-path>` inside those aliases.
58
+
59
+ ## Configure Zova
60
+
61
+ ### 1. Add a tracked flavor environment
62
+
63
+ Keep shared defaults in `zova/env/.env`; add the site’s tracked flavor override at:
64
+
65
+ ```text
66
+ zova/env/.env.<site-flavor>
67
+ ```
68
+
69
+ The override must select a non-empty `SITE_ID`, the matching `APP_PUBLIC_PATH`, an SSR rendering profile, and the build-copy targets. For a Vona-integrated SSR site, set `SSR_WITH_VONA=true`.
70
+
71
+ Choose `SSR_PROFILE` from the rendering contract:
72
+
73
+ - use a public profile for cookie-free, cache-safe first paint;
74
+ - choose a session/private profile deliberately for cookie-backed admission, personalized first paint, or private SSR data.
75
+
76
+ Point the build destinations to the selected Vona site module and generated package workspace:
77
+
78
+ ```text
79
+ BUILD_COPY_RELEASE = ../vona/src/.../modules/<site-module>/assets/site
80
+ BUILD_REST_COPY_DIST = ../vona/.zova-rest
81
+ ```
82
+
83
+ Do not change ports or worktree-local environment overrides to make a new site work. Follow the normal worktree-environment workflow only when that separate setup is explicitly required.
84
+
85
+ ### 2. Add flavor-specific frontend configuration only where necessary
86
+
87
+ Use `zova/src/front/config/config/config.<site-flavor>.ts` for site-specific route aliases, route exclusions, layouts, or app configuration. Keep shared API, locale, theme, SSR, and app assembly in base `config.ts`.
88
+
89
+ For mounted routing:
90
+
91
+ - configure an unnamed canonical route through `config.routes.path`;
92
+ - configure a named dynamic route through `config.routes.name`;
93
+ - generate a named alias with `$router.getAliasPath(...)` rather than assuming `$router.getPagePath(...)` selects an alias;
94
+ - do not duplicate the external mount prefix in an alias because `APP_PUBLIC_PATH` supplies that boundary.
95
+
96
+ Choose layouts, title, locales, assets, route exclusions, `requiresAuth`, and redirects from the new site’s contract. Existing Admin and Web sites are specimens, not defaults to copy blindly.
97
+
98
+ ### 3. Add durable scripts and root wrappers
99
+
100
+ Inspect the active root `package.json` and `zova/package.json` first. Add lower-level Zova scripts in the durable source manifest, normally including:
101
+
102
+ ```text
103
+ dev:ssr:<site-flavor>
104
+ build:ssr:<site-flavor>
105
+ build:rest:<site-flavor>
106
+ preview:ssr:<site-flavor>
107
+ ```
108
+
109
+ The SSR and REST scripts must pass the same flavor. Add a paired batch and an explicit root build wrapper that runs both outputs in order:
110
+
111
+ ```bash
112
+ npm run build:ssr:<site-flavor>
113
+ npm run build:rest:<site-flavor>
114
+ ```
115
+
116
+ Use the root wrapper for normal site-level builds and in Vona diagnostics. Add the flavor to an aggregate build only when it belongs to that edition’s default shipped artifact set.
117
+
118
+ Edit durable manifests such as root `package.json` and `zova/package.original.json`; do not hand-edit generated `zova/package.json`, generated REST output, copied bundles, or generated dependency state. Do not run `npm run init` merely to synchronize a manifest.
119
+
120
+ ## Register the Vona SSR site
121
+
122
+ Create or refine an independently packaged Vona site module. It must expose its assets, source entry point, locale metadata, and an SSR-site bean.
123
+
124
+ The site bean should:
125
+
126
+ 1. import `IPagePathRecord` and `IIconRecord` from `<zova-rest-package>`;
127
+ 2. augment the SSR site-ID and public-path type records;
128
+ 3. define typed page/page-data options around the generated route record;
129
+ 4. extend `BeanSsrSiteBase` and register `@SsrSite(...)`;
130
+ 5. provide a localized title, `<site-id>`, `<public-path>`, `<bundle-path>`, and `<root-build-wrapper>` diagnostics command.
131
+
132
+ Conceptually:
133
+
134
+ ```ts
135
+ @SsrSite({
136
+ siteId: '<site-id>',
137
+ publicPath: '<public-path>',
138
+ bundlePath: '<bundle-path>',
139
+ diagnostics: { buildCommand: 'npm run <root-build-wrapper>' },
140
+ })
141
+ export class SsrSiteExample extends BeanSsrSiteBase<...> {}
142
+ ```
143
+
144
+ The default release identity is normally `ssr-<site-flavor>-<app-version>`. Unless you intentionally override it, use that exact copied directory as `<bundle-path>`. Vona resolves the bundle from the owning site module’s `assets/site/<bundle-path>` directory.
145
+
146
+ Ensure the site module is discoverable through its durable suite/module manifest. Let normal tooling generate metadata and dependency closures; do not hand-edit generated metadata.
147
+
148
+ ## Build the artifact pair and sync Vona
149
+
150
+ For every Vona-consumed independent SSR site, this sequence is required:
151
+
152
+ ```bash
153
+ npm run <root-build-wrapper>
154
+ npm run deps:vona
155
+ ```
156
+
157
+ The wrapper must create both:
158
+
159
+ ```text
160
+ SSR bundle and client assets
161
+ + generated flavor REST/type package
162
+ ```
163
+
164
+ `build:rest:<site-flavor>` alone is not a valid Vona SSR handoff: Vona needs the bundle, assets, and typed REST package to move together. Likewise, a default Admin or Web wrapper does not prove an independently named site was rebuilt.
165
+
166
+ After generation, verify that:
167
+
168
+ - the copied release directory matches `<bundle-path>`;
169
+ - the generated package name and Vona import match `<zova-rest-package>`;
170
+ - the Vona local workspace can discover the package after `npm run deps:vona`;
171
+ - the site bean’s diagnostics command names the same root wrapper that produced the artifacts.
172
+
173
+ If the generated artifacts are correct and `npm run deps:vona` completed but Vona still sees stale local types, diagnose [local dependency drift](/fullstack/contract-loop-playbook#recovery-path-for-local-dependency-drift) only then.
174
+
175
+ ## Verify through the Vona boundary
176
+
177
+ A standalone Zova development server can help with page iteration, but it does not prove copied artifacts, Vona site matching, generated type handoff, or production-like hydration.
178
+
179
+ Run the narrowest meaningful checks first:
180
+
181
+ ```bash
182
+ npm run tsc:zova
183
+ npm run <root-build-wrapper>
184
+ npm run deps:vona
185
+ pnpm --dir vona run tsc
186
+ ```
187
+
188
+ Then prove the Vona-served site:
189
+
190
+ 1. request the exact public mount path and assert the intended Vona SSR site handled it;
191
+ 2. inspect raw HTML for server-rendered content and the correct cache/admission behavior;
192
+ 3. verify client assets load from the mounted site;
193
+ 4. open the same route in a browser and assert hydration completes without errors or SSR/client mismatch;
194
+ 5. verify canonical and alias route behavior, including no duplicated mount path;
195
+ 6. exercise the site’s public/private admission or redirect contract;
196
+ 7. retain focused browser/E2E evidence for the new site rather than relying only on compilation.
197
+
198
+ For more detail, read [SSR Architecture Overview](/frontend/ssr-architecture-overview), [SSR Build and Deploy Guide](/frontend/ssr-build-deploy-guide), [Vona + Zova Integration](/fullstack/vona-zova-integration), and [Contract Loop Playbook](/fullstack/contract-loop-playbook).
@@ -64,6 +64,8 @@ Because Start can differ in UI layer, module composition, SSR site baselines, an
64
64
  2. the Start repo’s `package.json`
65
65
  3. the exact Zova flavor names and generated output paths used there
66
66
 
67
+ For a new independently mounted SSR surface, use [Independent SSR Site and Flavor Setup](/fullstack/ssr-site-and-flavor-setup). It defines the linked Zova flavor, paired SSR/REST artifacts, Vona `@SsrSite` registration, and browser proof without assuming that either edition’s default Admin or Web wrapper applies.
68
+
67
69
  ## Recommended integration workflow
68
70
 
69
71
  ### 1. Detect the edition
@@ -0,0 +1,37 @@
1
+ import { defineConfig } from '@playwright/test';
2
+
3
+ const rootDir = new URL('../..', import.meta.url).pathname;
4
+ const baseURL = 'http://127.0.0.1:4173';
5
+
6
+ export default defineConfig({
7
+ testDir: `${rootDir}/repo-e2e/docs/specs`,
8
+ outputDir: `${rootDir}/repo-e2e/docs/test-results`,
9
+ fullyParallel: false,
10
+ workers: 1,
11
+ forbidOnly: !!process.env.CI,
12
+ retries: process.env.CI ? 2 : 0,
13
+ reporter: process.env.CI
14
+ ? [
15
+ ['html', { open: 'never', outputFolder: `${rootDir}/repo-e2e/docs/playwright-report` }],
16
+ ['list'],
17
+ ]
18
+ : 'list',
19
+ use: {
20
+ baseURL,
21
+ viewport: { width: 1440, height: 900 },
22
+ trace: 'on-first-retry',
23
+ },
24
+ webServer: {
25
+ command: 'pnpm --dir repo-docs docs:preview -- --host 127.0.0.1 --port 4173 --strictPort',
26
+ cwd: rootDir,
27
+ url: `${baseURL}/`,
28
+ timeout: 120_000,
29
+ reuseExistingServer: false,
30
+ stdout: 'pipe',
31
+ stderr: 'pipe',
32
+ gracefulShutdown: {
33
+ signal: 'SIGINT',
34
+ timeout: 10_000,
35
+ },
36
+ },
37
+ });