cabloy 5.1.227 → 5.1.228

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 (42) hide show
  1. package/.cabloy-version +1 -1
  2. package/CHANGELOG.md +16 -0
  3. package/CONTRIBUTING.md +1 -1
  4. package/package.json +2 -2
  5. package/repo-agent-governance/managed-assets.json +30 -30
  6. package/repo-agent-governance/policies/repository.md +1 -1
  7. package/repo-agent-governance/skills/cabloy-backend-scaffold/SKILL.md +3 -3
  8. package/repo-agent-governance/skills/cabloy-backend-scaffold/references/follow-up-checklist.md +2 -2
  9. package/repo-agent-governance/skills/cabloy-frontend-scaffold/SKILL.md +1 -0
  10. package/repo-agent-governance/skills/cabloy-master-detail/SKILL.md +2 -1
  11. package/repo-agent-governance/skills/cabloy-resource-field-update/SKILL.md +2 -0
  12. package/repo-agent-governance/skills/cabloy-resource-field-update/references/follow-up-checklist.md +11 -5
  13. package/repo-agent-governance/skills/cabloy-resource-field-update/references/verification-checklist.md +10 -14
  14. package/repo-agent-governance/skills/cabloy-spec-execution/evals/evals.json +4 -2
  15. package/repo-agent-governance/skills/cabloy-spec-execution/references/execution-protocol.md +1 -1
  16. package/repo-agent-governance/tests/evidence-paths.test.mjs +84 -13
  17. package/repo-agent-governance/tools/evidence-paths/sanitize.mjs +38 -8
  18. package/repo-docs/backend/migration-and-changes.md +21 -14
  19. package/repo-docs/backend/quickstart.md +1 -1
  20. package/repo-docs/backend/resource-field-update.md +3 -2
  21. package/repo-docs/backend/unit-testing.md +9 -3
  22. package/repo-docs/frontend/table-cell-cookbook.md +32 -0
  23. package/repo-docs/fullstack/quickstart.md +1 -1
  24. package/repo-docs/reference/repo-scripts.md +2 -2
  25. package/repo-e2e/specs/a-commerce.spec.ts +83 -9
  26. package/scripts/init.ts +14 -0
  27. package/vona/pnpm-lock.yaml +25 -82
  28. package/vona/src/suite/a-commerce/modules/commerce-trade/src/config/locale/en-us.ts +2 -0
  29. package/vona/src/suite/a-commerce/modules/commerce-trade/src/config/locale/zh-cn.ts +2 -0
  30. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderShip.tsx +4 -2
  31. package/zova/package.original.json +1 -1
  32. package/zova/packages-cli/cli/package.json +5 -2
  33. package/zova/packages-cli/cli-set-front/package.json +5 -2
  34. package/zova/packages-cli/cli-set-front/src/lib/bean/cli.bin.buildRest.ts +5 -1
  35. package/zova/pnpm-lock.yaml +434 -276
  36. package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +23 -23
  37. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/schemas.ts +28 -12
  38. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/types.ts +245 -85
  39. package/zova/src/suite/a-commerce/modules/commerce-trade/src/component/tableCellActionAdjustStock/controller.tsx +3 -2
  40. package/zova/src/suite/a-commerce/modules/commerce-trade/src/component/tableCellActionShip/controller.tsx +82 -34
  41. package/zova/src/suite/a-commerce/modules/commerce-trade/src/config/locale/en-us.ts +1 -0
  42. package/zova/src/suite/a-commerce/modules/commerce-trade/src/config/locale/zh-cn.ts +1 -0
@@ -9,14 +9,15 @@ export const LINE_LIMIT = 1024 * 1024;
9
9
  export const FILE_LIMIT = 8 * 1024 * 1024;
10
10
 
11
11
  class SafeError extends Error {
12
- constructor(code) {
12
+ constructor(code, details) {
13
13
  super(code);
14
14
  this.code = code;
15
+ this.details = details;
15
16
  }
16
17
  }
17
18
 
18
- function fail(code) {
19
- throw new SafeError(code);
19
+ function fail(code, details) {
20
+ throw new SafeError(code, details);
20
21
  }
21
22
 
22
23
  function rootInfo(value) {
@@ -247,6 +248,7 @@ export async function sanitizeStream(input, output, { matcher = createMatcher()
247
248
  output.on('error', onError);
248
249
  let parts = [];
249
250
  let length = 0;
251
+ let lineNumber = 1;
250
252
  try {
251
253
  for await (const chunk of input) {
252
254
  if (outputFailed) fail('OUTPUT_ERROR');
@@ -262,9 +264,10 @@ export async function sanitizeStream(input, output, { matcher = createMatcher()
262
264
  parts.push(bytes.subarray(start, end));
263
265
  if (newline < 0) break;
264
266
  const line = Buffer.concat(parts, length);
265
- if (binary(line)) fail('BINARY');
267
+ rejectBinary(line, lineNumber);
266
268
  const text = decode(line);
267
269
  await write(output, `${matcher.sanitize(text)}\n`);
270
+ lineNumber++;
268
271
  parts = [];
269
272
  length = 0;
270
273
  start = newline + 1;
@@ -272,7 +275,7 @@ export async function sanitizeStream(input, output, { matcher = createMatcher()
272
275
  }
273
276
  if (length) {
274
277
  const line = Buffer.concat(parts, length);
275
- if (binary(line)) fail('BINARY');
278
+ rejectBinary(line, lineNumber);
276
279
  await write(output, matcher.sanitize(decode(line)));
277
280
  }
278
281
  if (outputFailed) fail('OUTPUT_ERROR');
@@ -293,12 +296,21 @@ function safeFilename(value, matcher) {
293
296
  );
294
297
  }
295
298
 
296
- function binary(bytes) {
297
- return bytes.some(
299
+ function binaryIndex(bytes) {
300
+ return bytes.findIndex(
298
301
  byte => byte === 0 || byte === 127 || (byte < 32 && ![9, 10, 12, 13].includes(byte)),
299
302
  );
300
303
  }
301
304
 
305
+ function binary(bytes) {
306
+ return binaryIndex(bytes) >= 0;
307
+ }
308
+
309
+ function rejectBinary(bytes, line) {
310
+ const index = binaryIndex(bytes);
311
+ if (index >= 0) fail('BINARY', { byte: bytes[index], line, bytePosition: index + 1 });
312
+ }
313
+
302
314
  async function ancestorExclusion(path) {
303
315
  const root = parse(path).root;
304
316
  let current = root;
@@ -465,6 +477,9 @@ paths and unknown absolute roots are left unchanged: this is NOT a universal sec
465
477
  scanner. Use explicit maps for other local roots; inspect output before publication.
466
478
  UTF-8 decoding is fatal, with a 1 MiB byte limit per line (including CR, excluding LF).
467
479
  NUL, DEL and non-text C0 controls are rejected; tab, LF, form feed and CR are allowed.
480
+ sanitize BINARY diagnostics report only the first rejected decimal byte, 1-based input
481
+ line and byte-position within that original UTF-8 line. LF advances the line; CR counts
482
+ as a byte, not a line boundary. No text from the rejected line is reported.
468
483
  Only complete validated lines are emitted; earlier sanitized lines may survive failure.
469
484
  check limits each file to 8 MiB, treats NUL/non-text controls as binary, and reports only
470
485
  scope-N/relative-filename:line CODE, never source lines or absolute scan roots.
@@ -530,8 +545,23 @@ export async function runCli(
530
545
  return result.status;
531
546
  } catch (caught) {
532
547
  const code = caught instanceof SafeError ? caught.code : 'INTERNAL_ERROR';
548
+ let diagnostic = code;
549
+ if (caught instanceof SafeError && code === 'BINARY') {
550
+ const { byte, line, bytePosition } = caught.details ?? {};
551
+ if (
552
+ Number.isInteger(byte) &&
553
+ byte >= 0 &&
554
+ byte <= 255 &&
555
+ Number.isSafeInteger(line) &&
556
+ line > 0 &&
557
+ Number.isSafeInteger(bytePosition) &&
558
+ bytePosition > 0
559
+ ) {
560
+ diagnostic += ` byte=${byte} line=${line} byte-position=${bytePosition}`;
561
+ }
562
+ }
533
563
  try {
534
- await write(error, `evidence-paths: ${code}\n`);
564
+ await write(error, `evidence-paths: ${diagnostic}\n`);
535
565
  } catch {
536
566
  /* no unsafe fallback */
537
567
  }
@@ -71,7 +71,9 @@ When the change belongs in the current version path rather than new migration hi
71
71
  2. fold the schema change into the current version path;
72
72
  3. do not create a new migration branch.
73
73
 
74
- Ask before choosing this strategy; do not infer migration history from the field change alone. Any change to `meta.version.ts` requires `npm run test` so the test database is recreated and schema/data consistency is exercised.
74
+ Ask before choosing this strategy; do not infer migration history from the field change alone. Normal startup does not replay an already-applied positive-numbered migration branch. Editing that branch affects fresh installations and isolated test preparation, not a development database that has already applied it. A no-bump decision is not authorization to reset development data; if existing data must be preserved, agree on a new incremental migration instead.
75
+
76
+ Verify changes to migration, initialization, or seed behavior through managed isolated tests covering the affected behavior, preferably focused. A `meta.version.ts` edit alone does not require the full suite. See [Local development workflow](#local-development-workflow) for the separate verification and development-database decisions.
75
77
 
76
78
  ## `meta.version`
77
79
 
@@ -186,29 +188,33 @@ A practical sequence is:
186
188
  1. decide whether to increment `fileVersion`
187
189
  2. add or adjust entity/model structure
188
190
  3. update the current or new `meta.version` path to match that decision
189
- 4. rerun migration locally
190
- 5. verify the contract through tests and controller actions
191
+ 4. verify migration and resource behavior through managed isolated tests
192
+ 5. separately decide whether and how to update the development database
191
193
 
192
194
  This keeps schema change, backend contract change, and verification tightly connected.
193
195
 
194
196
  ## Local development workflow
195
197
 
196
- One important distinction for local development is:
198
+ Keep two questions separate: **how to verify the change** and **how to update development data**. Isolated tests do not update development databases, and a development reset is not a substitute for tests.
197
199
 
198
- - when iterating locally, you often want to recreate the database and re-run migration logic without bumping `fileVersion`
200
+ | Need | Path | Boundary |
201
+ | -------------------------------------------------------------------------------------- | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------- |
202
+ | Verify migration, initialization, seed, and affected resource behavior | Managed isolated tests, preferably focused | Even a focused run prepares a fresh test database; it does not change dev data. |
203
+ | Apply a new module or pending versioned migration to an existing prepared dev database | Normal application startup | Applies pending versions without resetting existing data; ordinary dev startup does not repeat `seed()`. |
204
+ | Replay an edited, already-applied positive-numbered branch on disposable dev data | Separately approved `npm run db:reset` | Destructive recreation; a no-bump decision does not authorize reset. |
205
+ | Update an existing database while preserving business data | A new incremental migration, then normal startup | Do not rely on editing an already-applied branch or use reset. |
199
206
 
200
- Representative commands:
207
+ For example, from the repository root:
201
208
 
202
209
  ```bash
203
- npm run test
204
- npm run db:reset
210
+ npm run test -- 'src/module/<module-name>/test/**/*.ts'
205
211
  ```
206
212
 
207
- A practical decision rule is:
213
+ Resolve the actual path for suite-owned modules. Select tests that cover the affected migration and contract behavior; expand coverage according to actual impact, not merely the presence of a `meta.version.ts` edit. Fresh-database preparation proves the fresh-install path, not every existing-data upgrade; verify upgrade behavior when the change requires it. See [Unit Testing](/backend/unit-testing#execute-tests).
214
+
215
+ The replay rule above concerns positive-numbered migration branches. The special version `0` and `fileVersion: -1` lifecycle paths retain their always-run semantics; they are not a reason to move a versioned schema change into an always-run branch.
208
216
 
209
- - use `db:reset` to recreate the disposable **dev-mode** databases, rerun migrations and instance initialization, and then invoke module `seed()` hooks; it deletes existing matching development databases, so do not run it against data you need to keep
210
- - `db:reset` requires `database.testDatabase: true` in dev config and a connection outside the disposable database; it refuses to reset a configured non-disposable database
211
- - use `test` when you need **test-mode** verification of migration, controller behavior, and the broader generated contract thread; E2E also remains test-mode
217
+ `db:reset` requires explicit approval, `database.testDatabase: true` in dev config, and a connection outside the disposable database. It refuses to reset a configured non-disposable database. Follow the lifecycle and concurrency precautions below before an approved reset.
212
218
 
213
219
  ### Managed development database preparation
214
220
 
@@ -239,7 +245,8 @@ Also ask:
239
245
 
240
246
  1. does this change require a `fileVersion` increment?
241
247
  2. does `meta.version` need an `update`, `init`, or `seed` branch?
242
- 3. should the local verification path include `test` or `db:reset`?
243
- 4. does the change affect the CRUD-generated thread or frontend-facing API contract as well?
248
+ 3. which managed focused tests verify the affected migration and resource behavior, and does the actual impact require broader coverage?
249
+ 4. does the development database need pending migrations through startup, a separately approved disposable reset, or a new data-preserving migration?
250
+ 5. does the change affect the CRUD-generated thread or frontend-facing API contract as well?
244
251
 
245
252
  That prevents schema changes from being documented in code but never integrated into the module lifecycle.
@@ -19,7 +19,7 @@ Use this page when you are contributing backend work in the framework repository
19
19
  | Name | Version |
20
20
  | ---------- | --------- |
21
21
  | pnpm | >=12.14.0 |
22
- | Node.js | >=24.8.0 |
22
+ | Node.js | >=24.11.0 |
23
23
  | Redis | >=7.2.6 |
24
24
  | Sqlite3 | Built-in |
25
25
  | MySQL | >=8 |
@@ -61,7 +61,7 @@ When the user decides not to create new migration history:
61
61
  2. fold the schema change into the current version path;
62
62
  3. do not create a new migration branch.
63
63
 
64
- For the complete migration lifecycle and test requirement, see [Migration and Changes](/backend/migration-and-changes#decide-whether-to-increment-fileversion).
64
+ Normal startup does not replay an already-applied positive-numbered migration branch. Managed focused tests exercise the edited path on an isolated test database, not the development database. A no-bump decision does not authorize a destructive dev reset; use a new incremental migration when existing data must be preserved. See [Migration and Changes](/backend/migration-and-changes#local-development-workflow) for the verification and development-database decision paths.
65
65
 
66
66
  ## Inspect the existing resource thread
67
67
 
@@ -195,7 +195,8 @@ Choose checks that match the layers changed:
195
195
 
196
196
  - inspect generated schema/OpenAPI after structure-shaping field changes;
197
197
  - run the narrow resource test and relevant typecheck;
198
- - run `npm run test` for any `meta.version.ts` change so the test database is recreated and migration consistency is exercised;
198
+ - verify affected migration, initialization, or seed behavior through managed isolated tests, preferably focused; focused runs still prepare a fresh test database, so a `meta.version.ts` edit alone does not require the full suite;
199
+ - decide development-database updates separately: tests do not update dev data, pending migrations apply through normal startup, and replaying an already-applied positive-numbered branch on disposable dev data requires explicit reset approval;
199
200
  - run the relevant frontend metadata/build/dependency synchronization when custom renderer resources are involved;
200
201
  - verify action-level serializer behavior with an API test when `v.serializer*` metadata changes returned fields;
201
202
  - 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.
@@ -8,7 +8,7 @@ Keep testing proportional to the backend change: cover critical business behavio
8
8
 
9
9
  Test-driven development and targeted unit or framework tests remain useful when they clarify and directly prove new or changed behavior. Exhaustive upfront mock/fake matrices and bespoke harnesses are not a delivery prerequisite.
10
10
 
11
- Preserve applicable authentication/authorization, tenant-isolation, transaction, concurrency, idempotency, migration, and acceptance checks without turning them into an unrelated mandatory matrix. The context, fixture, and resource-isolation rules below still apply, including the required `npm run test` after `meta.version.ts` changes. Missing runtime access must be reported, not worked around with unauthorized resets or substitute test machinery.
11
+ Preserve applicable authentication/authorization, tenant-isolation, transaction, concurrency, idempotency, migration, and acceptance checks without turning them into an unrelated mandatory matrix. The context, fixture, and resource-isolation rules below still apply. Verify changes to migration, initialization, or seed behavior through managed isolated tests covering the affected behavior, preferably focused; a `meta.version.ts` edit alone does not require the full suite. Missing runtime access must be reported, not worked around with unauthorized resets or substitute test machinery.
12
12
 
13
13
  Vona’s testing story is valuable because it is closely integrated with:
14
14
 
@@ -42,7 +42,13 @@ npm run vona :create:test student -- --module=training-student
42
42
 
43
43
  ## Execute tests
44
44
 
45
- From the root repository:
45
+ From the repository root, prefer a focused run for a bounded module change:
46
+
47
+ ```bash
48
+ npm run test -- 'src/module/<module-name>/test/**/*.ts'
49
+ ```
50
+
51
+ Resolve the actual test path for suite-owned modules. Positional patterns select test files, not a reduced database-preparation lifecycle. Focused managed tests still exercise fresh migration, initialization, and seeds; assert the affected resource behavior and cover existing-data upgrades when applicable. Expand to the full suite according to actual impact:
46
52
 
47
53
  ```bash
48
54
  npm run test
@@ -57,7 +63,7 @@ A typical Vona test flow includes:
57
63
  5. run the test files
58
64
  6. close the shared application after the test run
59
65
 
60
- In Cabloy Basic and Cabloy Start, root `npm run test` delegates to `vona/`'s `npm run test`; both allocate an isolated test identity, database and Redis namespace, Vona generated/runtime/public paths, and loopback listener. They can run in the same checkout as an ordinary development server without resetting or mutating that server's business data. Even a targeted run performs the test-mode setup. The Vona `npm run cov` script uses the same isolation; raw `:bin:test` commands do not. Shared build/dependency preparation may still write to shared artifacts, and custom external resources need separate isolation. See [Repo Scripts](/reference/repo-scripts#test-isolation-and-development-data).
66
+ In Cabloy Basic and Cabloy Start, root `npm run test` delegates to `vona/`'s `npm run test`; both allocate an isolated test identity, database and Redis namespace, Vona generated/runtime/public paths, and loopback listener. They can run in the same checkout as an ordinary development server without resetting or mutating that server's business data. Even a targeted run performs the test-mode setup. Passing tests do not update the development database; choose its migration or explicitly approved reset path separately using [Migration and Changes](/backend/migration-and-changes#local-development-workflow). The Vona `npm run cov` script uses the same isolation; raw `:bin:test` commands do not. Shared build/dependency preparation may still write to shared artifacts, and custom external resources need separate isolation. See [Repo Scripts](/reference/repo-scripts#test-isolation-and-development-data).
61
67
 
62
68
  The runner owns this application lifecycle. Test files import the shared app from `vona-mock` and must not create or close it themselves:
63
69
 
@@ -343,6 +343,38 @@ A practical rule is:
343
343
  - let the cell resource own the render interaction
344
344
  - let the command bean or model layer own the actual action semantics
345
345
 
346
+ ### Collect input for a business row action in a dialog
347
+
348
+ When a row action needs additional business input, such as shipment details or a stock-adjustment reason, prefer a compact button in the cell and collect the input in a focused dialog. Keep deliberate inline-editing workflows inline; this is not a requirement to put every table action in a dialog.
349
+
350
+ Use the following interaction boundaries:
351
+
352
+ 1. **Open from the target row.** Keep the cell responsible for the trigger and row context. For caller-owned content, use the ordinary `this.$appModal.dialog(...)` with `slotDefault` and `slotActions`; use a routed dialog only when the workflow needs routed pages. See [Routed Dialog Guide](/frontend/routed-dialog-guide#choose-the-right-primitive).
353
+ 2. **Collect operation-specific input.** Prefer the operation's generated `$apiSchema` or the owning `ModelResource` schema with automatic `ZForm` rendering when the contract already describes the fields. Keep localized schema access reactive, and do not copy backend field metadata into the controller. Use manual or mixed fields only for a concrete interaction need, such as UI-only confirmation. See [API Schema Guide](/frontend/api-schema-guide#direct-schema-driven-form-pattern) and [Form Guide](/frontend/form-guide#preserve-the-automatic-custom-rendering-boundary).
354
+ 3. **Submit through the existing action owner.** Keep the draft local to the interaction, but reuse the existing command or semantic model facade for business execution. For an action on the same resource, retain its existing `ModelResource` mutation and cache owner rather than adding dialog-local fetch/cache state or manual list refreshes. See [Resource Model Cookbook](/frontend/model-resource-cookbook#recipe-4-inside-that-facade-add-a-row-level-custom-mutation).
355
+ 4. **Keep one complete submission lifecycle.** Validate and normalize input as required by the operation, and prevent duplicate submission at the boundary that owns all submit paths. Return or await the whole action through `ZButton.onPerform` for a button-only action; use form submission state and error handling when `ZForm` owns submission. Reflect broader mutation pending state only when it is needed beyond that button's own lifecycle. Choose one error presentation owner, not competing alerts. See [Behavior Guide](/frontend/behavior-guide#perform-a-standalone-asynchronous-button-action).
356
+ 5. **Close only after success.** Await the mutation before calling the dialog's `close()`. On failure, keep the dialog and entered values available for correction or retry; do not close in `finally`. Cancel or dismissal must not initiate a mutation, but closing the dialog does not cancel an already submitted request. Choose confirmation, draft reset/recovery, and dismissal behavior from the business workflow rather than copying one example's checkbox, width, or reset policy.
357
+
358
+ For a schema-driven form, keep its submit controls in `ZForm.slotFooter` when appropriate so that validation and native-form submission remain one path. The dialog's `slotActions` is not a reason to introduce a second submission owner.
359
+
360
+ Apply these schema-driven form details where they fit the operation:
361
+
362
+ - **Submit same-shaped data directly.** When the validated form data has the same shape as the operation's request DTO, pass `data.value` directly to the existing mutation, for example `await mutation.mutateAsync(data.value)`. Build a separate payload only when the operation needs a real projection, conversion, or removal of UI-only fields. Do not enumerate fields or duplicate normalization merely to reconstruct the same request; when the backend already owns normalization, leave it there. Schema validation does not imply that every operation trims its input.
363
+ - **Await business confirmation inside `onSubmitData`.** If the operation requires a confirmation prompt, await it after form validation and before the mutation in the same returned or awaited submit handler. Button submission and Enter then share validation, confirmation, pending state, and error handling. A declined confirmation should return without mutating or closing the form, preserving the draft. Do not put confirmation only in the submit button's click handler or introduce a confirmation checkbox unless the business workflow calls for one.
364
+ - **Prepare lazy schemas at the opening boundary when appropriate.** For a low-frequency dialog, opening can await `$QueryEnsureLoaded(() => this.apiSchemasShip.sdk)` before rendering the form. Resolve `apiSchemasShip` through a getter for the current runtime and derive `requestBody` reactively, such as with `$computed`. Ensure the current locale's SDK on every opening, even if the reactive derivation was installed earlier; do not retain a facade or schema snapshot from a previous locale. If preparation is extracted into a helper, await SDK readiness on every call and use `if (!this.schema)` only to create the computed derivation once. Do not place `if (this.schema) return` before the SDK check: an existing computed does not prove that the current locale's SDK is loaded. Extracting a helper is optional; the important boundary is repeated readiness checks versus one-time derivation setup. This is an optional loading boundary, not a requirement to defer every form's schema until interaction.
365
+
366
+ The Cabloy Basic shipment example is `zova/src/suite/a-commerce/modules/commerce-trade/src/component/tableCellActionShip/controller.tsx`: `_openDialog()` calls `_ensureSchema()` to prepare the current schema, `ZForm` renders the contract fields automatically, and `_submit()` awaits confirmation and `mutateAsync(data.value)` before closing. Its dialog width, draft-reset policy, and backend normalization are operation-specific choices, not additional form rules.
367
+
368
+ Extend an existing critical-flow E2E scenario rather than creating a separate harness:
369
+
370
+ - locate the trigger within the intended order or resource row, then locate fields and submit controls within the opened dialog
371
+ - check that the cell no longer exposes the dialog's input fields, and that invalid input cannot submit
372
+ - check cancellation and the chosen reopen/draft policy
373
+ - check successful submission, dialog closure, and the refreshed resource state
374
+ - when covering a submission failure, assert that the dialog retains the draft, presents the intended error, and allows retry after pending state clears
375
+
376
+ In Cabloy Basic, the ordinary dialog can be located with `getByRole('dialog')`; assert its visible heading rather than assuming the title also supplies its accessible name. Verify the active edition's modal adapter before reusing UI-specific selectors.
377
+
346
378
  ## Pattern 5: Row-action composition with `tableActionRow`
347
379
 
348
380
  Use this pattern when one cell should show several row actions together.
@@ -9,7 +9,7 @@ Before creating a new Cabloy project, make sure your environment has:
9
9
  | Name | Version |
10
10
  | ------------ | ----------- |
11
11
  | `pnpm` | `>=12.14.0` |
12
- | `Node.js` | `>=24.4.0` |
12
+ | `Node.js` | `>=24.11.0` |
13
13
  | `Redis` | `>=7.2.6` |
14
14
  | `SQLite3` | `Built-in` |
15
15
  | `MySQL` | `>=8` |
@@ -74,7 +74,7 @@ Root `npm run tsc` runs `tsc:zova`, then `tsc:vona`; these are the only root `ts
74
74
 
75
75
  In both nested workspaces, `npm run tsc` delegates to `npm run tsc:native`. Native and legacy checks first run `npm run prerun`, then invoke the selected compiler with incremental caching disabled. Root workspace aliases delegate to these scripts too, so dependency and generated metadata preparation is not bypassed. Each workspace's `prerun` runs only its `:tools:deps` command; it does not call `tsc`, avoiding recursive script execution. Preparation may refresh generated metadata or dependency manifests and install changed dependencies, while the compiler step itself emits no declarations or build-info files.
76
76
 
77
- Zova development and build entrypoints call `npm run prerun` before Quasar or REST emission to prepare dependencies and generated metadata without automatically running a typecheck. Run `npm run tsc`, `npm run tsc:native`, or `npm run tsc:legacy` explicitly inside the Zova workspace when source typechecking is needed. The former multi-project Vona CLI command `:bin:tsc` has been removed; use the project checking scripts instead. Module publication configurations and REST emitters are unchanged. Classic TypeScript remains installed for compiler-JavaScript-API consumers, including SWC configuration loading, Rollup integration, and OpenAPI generation; the REST emitter retains the `@typescript/native-preview` dependency name as an npm alias for stable `typescript@7.0.2`, preserving its declaration plugin's compiler lookup.
77
+ Zova development and build entrypoints call `npm run prerun` before Quasar or REST emission to prepare dependencies and generated metadata without automatically running a typecheck. Run `npm run tsc`, `npm run tsc:native`, or `npm run tsc:legacy` explicitly inside the Zova workspace when source typechecking is needed. The former multi-project Vona CLI command `:bin:tsc` has been removed; use the project checking scripts instead. Module publication configurations retain their existing compiler selection. REST emission uses tsdown 0.23.1 with an explicit JavaScript-only pass (`dts: false`) followed by a declaration-only pass (`dts.generator: 'tsgo'`). The frontend CLI and fullstack workflows require Node.js `>=24.11.0`. Classic TypeScript remains installed for compiler-JavaScript-API consumers, including SWC configuration loading, Rollup integration, and OpenAPI generation; the REST emitter retains the `@typescript/native-preview` dependency name as an npm alias for stable `typescript@7.0.2`, preserving its declaration plugin's compiler lookup.
78
78
 
79
79
  The native alias is installed at the repository root, not in either nested workspace. Run `pnpm install` at the root after updating the checkout; a missing alias fails with an installation diagnostic rather than silently selecting the legacy compiler. Do not use a bare root `tsc`/`pnpm exec tsc` to choose a compiler, because both packages declare the same binary name.
80
80
 
@@ -208,7 +208,7 @@ Combine producer stdout/stderr **before** sanitization, never `tee` or spool raw
208
208
 
209
209
  ### Windows and PowerShell
210
210
 
211
- The tool uses Node's native filesystem APIs on Windows, Linux and macOS; it requires the repository's Node.js version (`>=24.4.0`). Path-text recognition is cross-platform independently of the host OS. Native filesystem behavior is checked by the `evidence-platforms` CI matrix on Ubuntu, Windows and macOS. A configured matrix is not a successful run: inspect the actual job results before claiming all three platforms are verified.
211
+ The tool uses Node's native filesystem APIs on Windows, Linux and macOS; use a Node.js version supported by the repository's root `engines.node` declaration. Path-text recognition is cross-platform independently of the host OS. Native filesystem behavior is checked by the `evidence-platforms` CI matrix on Ubuntu, Windows and macOS. A configured matrix is not a successful run: inspect the actual job results before claiming all three platforms are verified.
212
212
 
213
213
  From the repository root, PowerShell can invoke the CLI directly:
214
214
 
@@ -936,22 +936,96 @@ test(
936
936
  await expect(adminPage.getByRole('button', { name: 'Submit', exact: true })).toHaveCount(0);
937
937
  await adminPage.getByRole('button', { name: 'Back', exact: true }).click();
938
938
  await expect(orderRow).toBeVisible();
939
- const carrierInput = orderRow.getByPlaceholder('Carrier');
940
- const trackingNumberInput = orderRow.getByPlaceholder('Tracking number');
941
- const confirmationCheckbox = orderRow.getByRole('checkbox');
939
+ const openShipmentButton = orderRow.getByRole('button', {
940
+ name: 'Ship order',
941
+ exact: true,
942
+ });
943
+ await expect(openShipmentButton).toHaveCount(1);
944
+ await expect(orderRow.locator('input')).toHaveCount(0);
945
+ await openShipmentButton.click();
946
+ const shipmentDialog = adminPage.getByRole('dialog').filter({
947
+ has: adminPage.getByRole('heading', { name: 'Ship order', exact: true }),
948
+ });
949
+ const shipmentConfirmation = adminPage.getByRole('dialog').filter({
950
+ hasText: 'Ship this order with the entered shipment details?',
951
+ });
952
+ await expect(shipmentDialog).toBeVisible();
953
+ const carrierField = shipmentDialog.locator('fieldset').filter({
954
+ has: adminPage.locator('legend').filter({ hasText: /^Carrier$/ }),
955
+ });
956
+ const trackingNumberField = shipmentDialog.locator('fieldset').filter({
957
+ has: adminPage.locator('legend').filter({ hasText: /^Tracking number$/ }),
958
+ });
959
+ const carrierInput = carrierField.getByRole('textbox');
960
+ const trackingNumberInput = trackingNumberField.getByRole('textbox');
961
+ await expect(shipmentDialog.getByRole('checkbox')).toHaveCount(0);
962
+ const submitShipmentButton = shipmentDialog.getByRole('button', {
963
+ name: 'Ship order',
964
+ exact: true,
965
+ });
966
+ let shipmentRequestCount = 0;
967
+ adminPage.on('request', request => {
968
+ if (
969
+ request.method() === 'POST' &&
970
+ new URL(request.url()).pathname === `/api/commerce/trade/order/${checkout.orderId}/ship`
971
+ ) {
972
+ shipmentRequestCount++;
973
+ }
974
+ });
942
975
  await carrierInput.fill(shipmentCarrier);
943
976
  await trackingNumberInput.fill(shipmentTrackingNumber);
944
- await confirmationCheckbox.check();
945
- await expect(carrierInput).toHaveValue(shipmentCarrier);
946
- await expect(trackingNumberInput).toHaveValue(shipmentTrackingNumber);
947
- await expect(confirmationCheckbox).toBeChecked();
977
+ await shipmentDialog.getByRole('button', { name: 'Cancel', exact: true }).click();
978
+ await expect(shipmentDialog).toHaveCount(0);
979
+ await expect(orderRow).toContainText('Paid');
980
+ await openShipmentButton.click();
981
+ await expect(carrierInput).toHaveValue('');
982
+ await expect(trackingNumberInput).toHaveValue('');
983
+ await submitShipmentButton.click();
984
+ await expect(carrierField.locator('.text-error')).toBeVisible();
985
+ await expect(trackingNumberField.locator('.text-error')).toBeVisible();
986
+ await expect(shipmentConfirmation).toHaveCount(0);
987
+ await carrierInput.fill(' ');
988
+ await trackingNumberInput.fill(shipmentTrackingNumber);
989
+ await submitShipmentButton.click();
990
+ await expect(carrierField.locator('.text-error')).toBeVisible();
991
+ await expect(shipmentConfirmation).toHaveCount(0);
992
+ await carrierInput.fill('x'.repeat(101));
993
+ await submitShipmentButton.click();
994
+ await expect(carrierField.locator('.text-error')).toBeVisible();
995
+ await expect(shipmentConfirmation).toHaveCount(0);
996
+ await carrierInput.fill(` ${shipmentCarrier} `);
997
+ await trackingNumberInput.fill('x'.repeat(101));
998
+ await submitShipmentButton.click();
999
+ await expect(trackingNumberField.locator('.text-error')).toBeVisible();
1000
+ await expect(shipmentConfirmation).toHaveCount(0);
1001
+ await trackingNumberInput.fill(` ${shipmentTrackingNumber} `);
1002
+ await submitShipmentButton.click();
1003
+ await expect(shipmentConfirmation).toBeVisible();
1004
+ await expect(submitShipmentButton).toBeDisabled();
1005
+ expect(shipmentRequestCount).toBe(0);
1006
+ await shipmentConfirmation.getByRole('button', { name: 'No', exact: true }).click();
1007
+ await expect(shipmentConfirmation).toHaveCount(0);
1008
+ await expect(carrierInput).toHaveValue(` ${shipmentCarrier} `);
1009
+ await expect(trackingNumberInput).toHaveValue(` ${shipmentTrackingNumber} `);
1010
+ await expect(orderRow).toContainText('Paid');
1011
+ expect(shipmentRequestCount).toBe(0);
1012
+ await expect(submitShipmentButton).toBeEnabled();
1013
+ await trackingNumberInput.press('Enter');
1014
+ await expect(shipmentConfirmation).toBeVisible();
948
1015
  const shipmentResponse = waitForApiResponse(
949
1016
  adminPage,
950
1017
  'POST',
951
1018
  `/api/commerce/trade/order/${checkout.orderId}/ship`,
952
1019
  );
953
- await orderRow.getByRole('button', { name: 'Ship order', exact: true }).click();
954
- expect((await shipmentResponse).ok()).toBeTruthy();
1020
+ await shipmentConfirmation.getByRole('button', { name: 'Yes', exact: true }).click();
1021
+ const shipmentResult = await shipmentResponse;
1022
+ expect(shipmentResult.ok()).toBeTruthy();
1023
+ expect(shipmentResult.request().postDataJSON()).toEqual({
1024
+ carrier: ` ${shipmentCarrier} `,
1025
+ trackingNumber: ` ${shipmentTrackingNumber} `,
1026
+ });
1027
+ expect(shipmentRequestCount).toBe(1);
1028
+ await expect(shipmentDialog).toHaveCount(0);
955
1029
  await expect(orderRow).toContainText('Shipped');
956
1030
  expect(adminPageErrors).toEqual([]);
957
1031
  } finally {
package/scripts/init.ts CHANGED
@@ -19,6 +19,7 @@ const ROOT_DIR = resolve(__dirname, '..');
19
19
  const VONA_DIR = resolve(ROOT_DIR, 'vona');
20
20
  const ZOVA_DIR = resolve(ROOT_DIR, 'zova');
21
21
  const REPO_DOCS_DIR = resolve(ROOT_DIR, 'repo-docs');
22
+ const NODE_VERSION = '24.11.0';
22
23
  const PNPM_VERSION = '12.14.0';
23
24
  const VERSION_MARKER_FILE = '.cabloy-version';
24
25
 
@@ -50,6 +51,18 @@ function pnpmInstall(cwd = ROOT_DIR): void {
50
51
  exec('pnpm install --config.confirmModulesPurge=false --no-frozen-lockfile', cwd);
51
52
  }
52
53
 
54
+ function checkNode(): void {
55
+ const version = process.versions.node;
56
+ const current = version.split('.').map(Number);
57
+ const minimum = NODE_VERSION.split('.').map(Number);
58
+ for (let i = 0; i < minimum.length; i++) {
59
+ if (current[i] > minimum[i]) return;
60
+ if (current[i] < minimum[i]) {
61
+ throw new Error(`Node.js should >= ${NODE_VERSION}, current: ${version}`);
62
+ }
63
+ }
64
+ }
65
+
53
66
  function checkPnpm(): void {
54
67
  const version = execQuiet('pnpm --version').trimEnd();
55
68
  const [major, minor] = version.split('.').map(item => Number.parseInt(item, 10) || 0);
@@ -295,6 +308,7 @@ function initRepoDocs(): void {
295
308
 
296
309
  // --- Main ---
297
310
 
311
+ checkNode();
298
312
  checkPnpm();
299
313
  exec('node scripts/bootstrapAgentGovernance.mjs');
300
314
  pnpmInstall();