cabloy 5.1.209 → 5.1.211

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 (45) hide show
  1. package/.cabloy-version +1 -1
  2. package/CHANGELOG.md +13 -0
  3. package/package.json +2 -1
  4. package/repo-agent-governance/managed-assets.json +6 -6
  5. package/repo-agent-governance/skills/cabloy-contract-loop/SKILL.md +14 -8
  6. package/repo-agent-governance/skills/cabloy-contract-loop/references/verification-checklist.md +4 -1
  7. package/repo-agent-governance/tests/governance.test.mjs +65 -0
  8. package/repo-docs/ai/playbook-contract-regeneration.md +2 -0
  9. package/repo-docs/frontend/openapi-sdk-guide.md +23 -4
  10. package/repo-docs/fullstack/contract-loop-playbook.md +8 -0
  11. package/repo-docs/fullstack/openapi-to-sdk.md +12 -6
  12. package/repo-docs/reference/repo-scripts.md +3 -0
  13. package/scripts/bootstrapAgentGovernance.mjs +18 -0
  14. package/scripts/generateOpenapiIsolated.test.ts +207 -0
  15. package/scripts/generateOpenapiIsolated.ts +91 -0
  16. package/scripts/runIsolatedTest.test.ts +90 -0
  17. package/scripts/runIsolatedTest.ts +157 -82
  18. package/scripts/upgrade.test.ts +113 -0
  19. package/scripts/upgrade.ts +32 -0
  20. package/vona/packages-cli/cli/package.json +1 -1
  21. package/vona/packages-cli/cli-set-api/package.json +1 -1
  22. package/vona/packages-vona/vona/package.json +1 -1
  23. package/vona/packages-vona/vona-core/package.json +1 -1
  24. package/vona/packages-vona/vona-core/src/lib/core/testClaim.test.ts +8 -1
  25. package/vona/packages-vona/vona-core/src/lib/core/testClaim.ts +2 -2
  26. package/vona/packages-vona/vona-mock/package.json +1 -1
  27. package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
  28. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  29. package/zova/openapi.config.test.ts +29 -0
  30. package/zova/openapi.config.ts +13 -4
  31. package/zova/packages-cli/cli/package.json +2 -2
  32. package/zova/packages-cli/cli-set-front/cli/templates/openapi/config/boilerplate/project/openapi.config.ts +13 -4
  33. package/zova/packages-cli/cli-set-front/package.json +1 -1
  34. package/zova/packages-zova/zova/package.json +2 -2
  35. package/zova/src/suite/a-training/modules/training-student/src/.metadata/index.ts +7 -7
  36. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/schemas.ts +12 -0
  37. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/types.ts +173 -72
  38. package/zova/src/suite/a-training/modules/training-student/src/api/trainingStudent.ts +8 -1
  39. package/zova/src/suite/cabloy-basic/modules/basic-input/src/component/formFieldInput/controller.tsx +2 -1
  40. package/zova/src/suite-vendor/a-zova/modules/a-form/package.json +1 -1
  41. package/zova/src/suite-vendor/a-zova/modules/a-form/src/component/formField/controller.tsx +4 -5
  42. package/zova/src/suite-vendor/a-zova/modules/a-form/src/lib/schemaRefresh.ts +21 -12
  43. package/zova/src/suite-vendor/a-zova/modules/a-form/src/types/formField.ts +2 -0
  44. package/zova/src/suite-vendor/a-zova/modules/a-form/test/lib/schemaRefreshLifecycle.test.ts +242 -26
  45. package/zova/src/suite-vendor/a-zova/package.json +2 -2
package/.cabloy-version CHANGED
@@ -1 +1 @@
1
- 5.1.209
1
+ 5.1.211
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.1.211
4
+
5
+ ### Features
6
+
7
+ - Add an OpenAPI SDK.
8
+ - Add draft functionality.
9
+
10
+ ## 5.1.210
11
+
12
+ ### Features
13
+
14
+ - Add draft recovery.
15
+
3
16
  ## 5.1.209
4
17
 
5
18
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cabloy",
3
- "version": "5.1.209",
3
+ "version": "5.1.211",
4
4
  "gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
5
5
  "description": "A Node.js fullstack framework",
6
6
  "keywords": [
@@ -47,6 +47,7 @@
47
47
  "start:one": "cd vona && npm run start:one",
48
48
  "test": "cd vona && npm run test",
49
49
  "test:e2e": "node scripts/runIsolatedTest.ts e2e",
50
+ "contract:generate:isolated": "node scripts/generateOpenapiIsolated.ts",
50
51
  "test:docs:e2e": "npm run docs:build && playwright test --config repo-e2e/docs/playwright.config.ts",
51
52
  "db:reset": "cd vona && npm run db:reset",
52
53
  "tsc": "npm run tsc:zova && pnpm --dir vona run tsc",
@@ -53,14 +53,14 @@
53
53
  {
54
54
  "adapter": "codex",
55
55
  "category": "skill",
56
- "sha256": "004d81369d47f539092bf054cf5604f3185805391110553f349dd53247bed386",
56
+ "sha256": "bf8de5cbd1602d00aadaa1c1b91c15f4a2272fb03d88faa9a428d96e4486bbe4",
57
57
  "source": "skills/cabloy-contract-loop/references/verification-checklist.md",
58
58
  "target": ".agents/skills/cabloy-contract-loop/references/verification-checklist.md"
59
59
  },
60
60
  {
61
61
  "adapter": "codex",
62
62
  "category": "skill",
63
- "sha256": "897a2bda7df448cc2f8468c9e23a090f0bd16bcd49fbb30427c95b11340b64c8",
63
+ "sha256": "4aeb0477e3cb316b14bca7c4a3d1e277329be4a8f8e14ef31a3ceb858b6b0698",
64
64
  "source": "skills/cabloy-contract-loop/SKILL.md",
65
65
  "target": ".agents/skills/cabloy-contract-loop/SKILL.md"
66
66
  },
@@ -389,14 +389,14 @@
389
389
  {
390
390
  "adapter": "claude",
391
391
  "category": "skill",
392
- "sha256": "004d81369d47f539092bf054cf5604f3185805391110553f349dd53247bed386",
392
+ "sha256": "bf8de5cbd1602d00aadaa1c1b91c15f4a2272fb03d88faa9a428d96e4486bbe4",
393
393
  "source": "skills/cabloy-contract-loop/references/verification-checklist.md",
394
394
  "target": ".claude/skills/cabloy-contract-loop/references/verification-checklist.md"
395
395
  },
396
396
  {
397
397
  "adapter": "claude",
398
398
  "category": "skill",
399
- "sha256": "897a2bda7df448cc2f8468c9e23a090f0bd16bcd49fbb30427c95b11340b64c8",
399
+ "sha256": "4aeb0477e3cb316b14bca7c4a3d1e277329be4a8f8e14ef31a3ceb858b6b0698",
400
400
  "source": "skills/cabloy-contract-loop/SKILL.md",
401
401
  "target": ".claude/skills/cabloy-contract-loop/SKILL.md"
402
402
  },
@@ -739,14 +739,14 @@
739
739
  {
740
740
  "adapter": "cursor",
741
741
  "category": "skill",
742
- "sha256": "004d81369d47f539092bf054cf5604f3185805391110553f349dd53247bed386",
742
+ "sha256": "bf8de5cbd1602d00aadaa1c1b91c15f4a2272fb03d88faa9a428d96e4486bbe4",
743
743
  "source": "skills/cabloy-contract-loop/references/verification-checklist.md",
744
744
  "target": ".cursor/skills/cabloy-contract-loop/references/verification-checklist.md"
745
745
  },
746
746
  {
747
747
  "adapter": "cursor",
748
748
  "category": "skill",
749
- "sha256": "897a2bda7df448cc2f8468c9e23a090f0bd16bcd49fbb30427c95b11340b64c8",
749
+ "sha256": "4aeb0477e3cb316b14bca7c4a3d1e277329be4a8f8e14ef31a3ceb858b6b0698",
750
750
  "source": "skills/cabloy-contract-loop/SKILL.md",
751
751
  "target": ".cursor/skills/cabloy-contract-loop/SKILL.md"
752
752
  },
@@ -206,7 +206,7 @@ That may include:
206
206
  - confirming DTO and validation alignment
207
207
  - checking Swagger/OpenAPI output
208
208
  - confirming that the changed endpoint or schema now reflects the intended contract
209
- - if local OpenAPI generation depends on a running Swagger source, starting the backend service first — in Cabloy Basic, `npm run dev` is the normal path and exposes `http://localhost:7102/swagger/json?version=V31`
209
+ - for AI-driven local OpenAPI generation, using the managed isolated service by default as described in Path A below; do not make a separately running development service a prerequisite
210
210
 
211
211
  If the backend contract output is wrong, frontend regeneration will only spread the mistake.
212
212
 
@@ -218,17 +218,23 @@ Once the backend contract is correct, decide how the frontend should consume it.
218
218
 
219
219
  Use this when the frontend consumes generated API contracts.
220
220
 
221
- Typical Zova commands include:
221
+ For AI development, **default to managed isolated generation**, even when a development Swagger service is currently reachable. An independently managed development service may stop at any time; owning the service lifecycle keeps the generation task continuous and its contract source consistent.
222
222
 
223
- - `npm run zova :openapi:config ...`
224
- - `npm run zova :openapi:generate ...`
223
+ 1. Detect the edition and verify the isolated entrypoint in the active root `package.json`. Cabloy Basic provides:
225
224
 
226
- Preflight reminder:
225
+ ```bash
226
+ npm run contract:generate:isolated -- <module-name>
227
+ ```
227
228
 
228
- - if `:openapi:generate` reads from a local Swagger endpoint, do not assume the generator itself is broken when fetch fails
229
- - first start the backend service, typically with `npm run dev`, and confirm `http://localhost:7102/swagger/json?version=V31` is reachable
229
+ For Cabloy Start or another version, verify the actual command and configuration support rather than assuming the Basic entrypoint exists.
230
230
 
231
- When the target is a module-local SDK, constrain `openapi.config.ts` with `operations.match` unless the module intentionally owns a broad API surface. This prevents unrelated APIs from being generated into the module.
231
+ 2. Initialize module configuration with `npm run zova :openapi:config <module-name>` when needed. Keep module ownership constrained with `operations.match` unless the module intentionally owns a broad API surface; the isolated command reuses the existing generator and ownership checks.
232
+ 3. Confirm the effective source uses the isolated service. The command supplies a child-scoped `OPENAPI_SOURCE_URL` to the project default source resolver; explicit module sources or project `modules` overrides retain their precedence. Inspect the project resolver and align conflicting sources deliberately before claiming isolated generation. Do not edit `.env` files or SDK runtime API targets to inject the temporary port.
233
+ 4. Coordinate shared output writes. The service and claim-backed test resources are isolated, but generated SDK/schema/metadata files remain shared workspace output: serialize generation and do not assume automatic rollback. Successful runs clean their owned resources after shutdown; failures retain evidence and must remain visible, not trigger a silent fallback to development Swagger.
234
+
235
+ Use `npm run zova :openapi:generate <module-name>` only as a deliberate exception, such as an explicit user request or a contract source that must come from a specific external or development environment. Record the reason and verify the configured source and its availability; current reachability alone is not a reason to override the AI default. If isolated support is absent, report that limitation and establish an explicit alternative rather than silently relying on a development service.
236
+
237
+ See [OpenAPI SDK Guide](../../../repo-docs/frontend/openapi-sdk-guide.md) for source precedence and configuration compatibility, and [Contract Loop Playbook](../../../repo-docs/fullstack/contract-loop-playbook.md#forward-chain-playbook) for the full forward-chain workflow.
232
238
 
233
239
  ### Path B: REST/type generation by flavor
234
240
 
@@ -15,7 +15,10 @@ After a contract-loop change, verify the branch that actually applies.
15
15
  - DTO and validation align
16
16
  - OpenAPI output reflects the intended shape
17
17
  - module ownership is constrained
18
- - regeneration commands completed successfully
18
+ - AI-driven SDK generation used the source-confirmed isolated entrypoint by default, regardless of development-service reachability; any direct-source exception has an explicit reason
19
+ - the effective project/module source resolves to the intended contract source; explicit source overrides did not silently bypass isolation
20
+ - regeneration commands completed successfully; isolated failures remained visible rather than falling back silently to development Swagger
21
+ - isolated resources were cleaned after success or retained as failure evidence, and shared SDK/schema/metadata writes were coordinated and inspected without assuming automatic rollback
19
22
  - generated SDK or schema outputs are updated
20
23
  - thin model facades and downstream consumers still align with the regenerated contract
21
24
  - `npm run tsc`
@@ -211,6 +211,71 @@ test('initializer preserves a legacy-customized adapter during a staged upgrade'
211
211
  });
212
212
  });
213
213
 
214
+ for (const scenario of [
215
+ { name: 'adopts a missing command', incoming: true },
216
+ {
217
+ name: 'leaves a matching command byte-identical',
218
+ incoming: true,
219
+ command: 'node scripts/generateOpenapiIsolated.ts',
220
+ },
221
+ {
222
+ name: 'reports and preserves a customized command',
223
+ incoming: true,
224
+ command: 'node project-contract.mjs',
225
+ },
226
+ { name: 'reports and preserves an empty customized command', incoming: true, command: '' },
227
+ { name: 'tolerates an older staged package without the command', incoming: false },
228
+ ]) {
229
+ test(`staged initializer isolated contract bridge ${scenario.name}`, () => {
230
+ withFixture(root => {
231
+ const name = 'contract:generate:isolated';
232
+ const incomingCommand = 'node scripts/generateOpenapiIsolated.ts';
233
+ const fixtureBootstrap = resolve(root, 'scripts/bootstrapAgentGovernance.mjs');
234
+ mkdirSync(dirname(fixtureBootstrap));
235
+ cpSync(resolve(ROOT_DIR, 'scripts/bootstrapAgentGovernance.mjs'), fixtureBootstrap);
236
+
237
+ const upgradeRoot = resolve(root, 'node_modules/.cabloy-upgrade');
238
+ cpSync(GOVERNANCE_DIR, resolve(upgradeRoot, 'repo-agent-governance'), { recursive: true });
239
+ const sourcePackage = JSON.parse(readFileSync(resolve(ROOT_DIR, 'package.json'), 'utf8'));
240
+ delete sourcePackage.scripts[name];
241
+ const projectPackage = structuredClone(sourcePackage);
242
+ projectPackage.scripts['project:custom'] = 'node project-owned.mjs';
243
+ if (Object.hasOwn(scenario, 'command')) projectPackage.scripts[name] = scenario.command;
244
+ if (scenario.incoming) sourcePackage.scripts[name] = incomingCommand;
245
+ writeFileSync(resolve(upgradeRoot, 'package.json'), JSON.stringify(sourcePackage));
246
+ const projectPackagePath = resolve(root, 'package.json');
247
+ const before = `${JSON.stringify(projectPackage)}\n`;
248
+ writeFileSync(projectPackagePath, before);
249
+
250
+ const result = runNode(root, fixtureBootstrap);
251
+ assert.equal(result.status, 0, result.output);
252
+ const customized = Object.hasOwn(scenario, 'command') && scenario.command !== incomingCommand;
253
+ const adopted = scenario.incoming && !Object.hasOwn(scenario, 'command');
254
+ const updated = JSON.parse(readFileSync(projectPackagePath, 'utf8'));
255
+ if (adopted) {
256
+ assert.match(result.output, /Added missing.*contract:generate:isolated/);
257
+ assert.deepEqual(updated, {
258
+ ...projectPackage,
259
+ scripts: { ...projectPackage.scripts, [name]: incomingCommand },
260
+ });
261
+ } else {
262
+ assert.equal(readFileSync(projectPackagePath, 'utf8'), before);
263
+ }
264
+ if (customized) {
265
+ assert.match(result.output, /Preserved customized.*contract:generate:isolated/);
266
+ assert.ok(result.output.includes(incomingCommand), result.output);
267
+ } else {
268
+ assert.doesNotMatch(result.output, /Preserved customized.*contract:generate:isolated/);
269
+ }
270
+ const first = readFileSync(projectPackagePath, 'utf8');
271
+ const second = runNode(root, fixtureBootstrap);
272
+ assert.equal(second.status, 0, second.output);
273
+ assert.equal(readFileSync(projectPackagePath, 'utf8'), first);
274
+ assert.doesNotMatch(second.output, /Added missing.*contract:generate:isolated/);
275
+ });
276
+ });
277
+ }
278
+
214
279
  test('governance adoption adopts byte-identical legacy outputs', () => {
215
280
  withFixture(root => {
216
281
  assert.equal(runGovernance(root, 'render').status, 0);
@@ -54,6 +54,8 @@ Before regenerating frontend artifacts:
54
54
 
55
55
  Use the Zova OpenAPI and REST-generation path rather than manually updating request code first. Follow the canonical [Contract Loop Playbook](/fullstack/contract-loop-playbook#forward-chain-playbook) for the active edition’s complete generation and verification sequence.
56
56
 
57
+ In Cabloy Basic, if no development Swagger service should be started or reused, use `npm run contract:generate:isolated -- <module-name>`. It verifies a fresh test-mode Vona service's signed readiness response and invokes the existing Zova generator against its Swagger source; generated SDK output remains shared workspace output and concurrent generation must be serialized.
58
+
57
59
  Do not treat `build:rest:*` as a standalone current command recipe. Resolve the active edition’s full build path from the canonical playbook and current root scripts so SSR bundle and REST output stay synchronized.
58
60
 
59
61
  For Start, resolve Start-specific flavor paths from that repository.
@@ -59,20 +59,39 @@ Representative use case:
59
59
 
60
60
  Example: generate OpenAPI-based frontend services for module `training-student`.
61
61
 
62
+ The direct command is the fast path when a development Swagger service is already available:
63
+
64
+ ```bash
65
+ npm run zova :openapi:generate training-student
66
+ ```
67
+
68
+ In Cabloy Basic, when generation should use a fresh isolated test-mode Vona service instead:
69
+
62
70
  ```bash
71
+ npm run contract:generate:isolated -- training-student
72
+ ```
73
+
74
+ The isolated command uses the same generator and preserves module ownership validation, but manages a disposable service, claim, and test resources without starting Playwright. Generated files under `zova/src/**` are shared workspace output, not run-owned temporary files; serialize concurrent SDK generation and do not expect automatic rollback.
75
+
76
+ The project-level source resolver accepts `OPENAPI_SOURCE_URL` as a complete OpenAPI document URL. It takes precedence over `API_BASE_URL` for generation only and does not change runtime `OPENAPI_BASE_URL_*` request targets:
77
+
78
+ ```bash
79
+ OPENAPI_SOURCE_URL=https://example.test/swagger/json?version=V31 \
63
80
  npm run zova :openapi:generate training-student
64
81
  ```
65
82
 
83
+ The override changes the project default source. Explicit module sources in module config or project `modules` config retain their existing precedence; remove or align those overrides when the module must use the isolated service. Do not commit source URLs containing credentials or access tokens. Existing project `openapi.config.ts` files are not overwritten by `:openapi:config`; merge the resolver when upgrading an older project manually.
84
+
66
85
  ## Build the rest-contract output
67
86
 
68
- For Cabloy Basic, representative contract-build commands include:
87
+ SDK generation and flavor builds are separate steps. When refreshed frontend output must be handed back to Vona, use the paired SSR/REST build for each affected flavor, then synchronize dependencies. For Cabloy Basic Admin:
69
88
 
70
89
  ```bash
71
- cd zova && npm run build:rest:cabloyBasicAdmin
72
- cd zova && npm run build:rest:cabloyBasicWeb
90
+ npm run build:zova:admin
91
+ npm run deps:vona
73
92
  ```
74
93
 
75
- This matters because SDK generation and rest-contract build steps are related, but not identical.
94
+ Also run `npm run build:zova:web` before dependency synchronization when Web is affected. Do not use `build:rest:*` alone for this reverse handoff; the SSR bundle and REST output must move together. See [Contract Loop Playbook](/fullstack/contract-loop-playbook#reverse-chain-playbook).
76
95
 
77
96
  ## Important convention
78
97
 
@@ -221,6 +221,14 @@ npm run zova :openapi:config <module-name>
221
221
  npm run zova :openapi:generate <module-name>
222
222
  ```
223
223
 
224
+ The direct command is the fast path when a development Swagger service is already available. In Cabloy Basic, when the workflow should own a fresh test-mode service instead, use:
225
+
226
+ ```bash
227
+ npm run contract:generate:isolated -- <module-name>
228
+ ```
229
+
230
+ This isolated path reuses the existing Zova generator and module ownership checks, but starts Vona in an isolated test run without Playwright. Its claim-backed test resources are cleaned after success and retained on failure; generated SDK files remain shared workspace output, so serialize concurrent generation and do not assume rollback.
231
+
224
232
  When the target is a module-local SDK, keep ownership constrained with `operations.match` so the module only generates the API surface it actually owns.
225
233
 
226
234
  ### Stage 4: Thin frontend follow-up
@@ -135,18 +135,24 @@ Representative frontend-side contract-generation commands include:
135
135
  ```bash
136
136
  npm run zova :openapi:config training-student
137
137
  npm run zova :openapi:generate training-student
138
- cd zova && npm run build:rest:cabloyBasicAdmin
139
- cd zova && npm run build:rest:cabloyBasicWeb
140
138
  ```
141
139
 
140
+ When no development service should be started or reused, use the isolated contract path instead:
141
+
142
+ ```bash
143
+ npm run contract:generate:isolated -- training-student
144
+ ```
145
+
146
+ This starts a disposable test-mode Vona service, verifies its signed readiness response and OpenAPI document, runs the same Zova generator, and cleans its claim-backed test resources after success. The generated SDK output is still shared workspace output; serialize concurrent generation and inspect or repair it explicitly rather than expecting rollback.
147
+
142
148
  A practical forward-chain sequence is:
143
149
 
144
150
  1. author or change the backend contract
145
151
  2. emit or inspect backend OpenAPI output
146
- 3. if the frontend generator reads from a local Swagger endpoint, start the backend service first so the endpoint is reachable — in this repo, `npm run dev` is the normal path and exposes Swagger at `http://localhost:7102/swagger/json?version=V31`
152
+ 3. choose the source lifecycle: reuse a reachable development Swagger service for the direct command (`npm run dev` normally exposes `http://localhost:7102/swagger/json?version=V31`), or let the isolated command own a fresh test-mode service
147
153
  4. configure frontend module ownership if needed
148
- 5. generate module-level OpenAPI SDK output
149
- 6. run the rest build for the active flavor when needed
154
+ 5. generate module-level OpenAPI SDK output with the selected command
155
+ 6. when frontend output must be handed back to Vona, run the paired flavor build (`npm run build:zova:admin`, plus `npm run build:zova:web` when affected), then `npm run deps:vona`; REST-only builds are not sufficient for this handoff
150
156
  7. consume the generated contract from frontend code instead of re-declaring it manually
151
157
  8. keep frontend follow-up thin by wrapping generated consumers with semantic facades instead of re-declaring the contract
152
158
  9. when the custom API still belongs to an existing resource, reuse the existing resource-owner instead of creating a competing cache owner
@@ -162,7 +168,7 @@ If both `operations.match` and `operations.ignore` are empty, frontend SDK gener
162
168
  A practical regeneration rule is:
163
169
 
164
170
  - if the backend contract changed, prefer regenerating the SDK/rest layer before hand-editing frontend request code
165
- - if `npm run zova :openapi:generate ...` fails because the local Swagger source is unavailable, first start the backend service and confirm `http://localhost:7102/swagger/json?version=V31` is reachable before treating generation as broken
171
+ - if `npm run zova :openapi:generate ...` fails because the local Swagger source is unavailable, start the backend service and confirm the configured source is reachable, or use `npm run contract:generate:isolated -- <module-name>` instead
166
172
  - if the generated consumer path is already correct, but frontend behavior still looks stale, stop patching generated files and diagnose consumer drift or local dependency drift instead
167
173
 
168
174
  ## `training-student` as a compact forward-chain specimen
@@ -41,6 +41,7 @@ Cabloy Basic exposes these shared root scripts:
41
41
  - `npm run start:one`
42
42
  - `npm run test`
43
43
  - `npm run test:e2e`
44
+ - `npm run contract:generate:isolated`
44
45
  - `npm run db:reset`
45
46
  - `npm run tsc`
46
47
  - `npm run docs:dev`
@@ -79,6 +80,8 @@ Judge preflight conflicts by actual resource overlap, not process names:
79
80
 
80
81
  A process-list match or a free port alone is not proof of either a conflict or safe ownership. Use the managed root entrypoints and retain normal execution-authorization checks; isolation is not a blanket reset or cleanup authorization.
81
82
 
83
+ `npm run contract:generate:isolated -- <module-name>` starts a fresh test-mode Vona service on a run-owned loopback port, verifies its signed readiness response, fetches the Swagger document, and invokes the existing Zova OpenAPI generator without Playwright. The run owns its disposable test resources and claim-backed local roots; successful runs clean them after the service drains, while failures retain the claim and evidence for inspection. The generated SDK files under `zova/src/**` remain shared checkout output, so serialize concurrent generation for the same workspace/module and do not treat this command as an automatic rollback boundary.
84
+
82
85
  For Vona integrated SSR, the allocated E2E origin is supplied to Zova at runtime as server `SSR_API_BASE_URL` and browser `API_BASE_URL`. Do not edit Zova `.env` files or rebuild solely to match the test port. The current root `npm run test:e2e` consumes prebuilt frontend artifacts; refresh affected SSR/REST output and synchronize dependencies when their inputs change. See [Runtime API origins in Vona integrated SSR](/fullstack/ssr-entry-modes#runtime-api-origins-in-vona-integrated-ssr) for configuration overrides and scope.
83
86
 
84
87
  For deliberate **dev-mode** database preparation, `npm run db:reset` is a separate, destructive command: in a single process, it clears validated application-owned Redis keys, drops and recreates managed disposable development databases, reruns migrations and instance initialization, then runs module `seed()` hooks. It requires `database.testDatabase: true` in the dev configuration and a connection outside the disposable database. Ordinary `npm run dev` and `npm run dev:one` only reuse existing managed databases; if any required target is missing, startup fails promptly with an instruction to run `npm run db:reset`. They do not create databases, perform creation-time Redis cleanup, or seed. Never run `db:reset` against development data you need to retain. Upgrades do not reset existing development data or repeat its seeds. See [Development database preparation](#development-database-preparation) and [Migration and Changes](/backend/migration-and-changes#local-development-workflow).
@@ -35,6 +35,24 @@ function reconcileGovernancePackageJson(sourceRoot) {
35
35
  projectPackage.scripts[name] = command;
36
36
  changed = true;
37
37
  }
38
+ // An older upgrader is already running when it installs this bootstrap, so adopt
39
+ // the new root command here too without replacing project-owned customizations.
40
+ const contractScriptName = 'contract:generate:isolated';
41
+ const contractCommand = sourcePackage.scripts?.[contractScriptName];
42
+ if (contractCommand) {
43
+ if (!Object.hasOwn(projectPackage.scripts ?? {}, contractScriptName)) {
44
+ projectPackage.scripts ??= {};
45
+ projectPackage.scripts[contractScriptName] = contractCommand;
46
+ changed = true;
47
+ // eslint-disable-next-line no-console
48
+ console.log(`[init] Added missing package.json scripts.${contractScriptName}`);
49
+ } else if (projectPackage.scripts[contractScriptName] !== contractCommand) {
50
+ // eslint-disable-next-line no-console
51
+ console.log(
52
+ `[init] Preserved customized package.json scripts.${contractScriptName}; incoming command: ${contractCommand}`,
53
+ );
54
+ }
55
+ }
38
56
  if (changed) {
39
57
  writeFileSync(projectPackagePath, `${JSON.stringify(projectPackage, null, 2)}\n`);
40
58
  // eslint-disable-next-line no-console
@@ -0,0 +1,207 @@
1
+ import assert from 'node:assert/strict';
2
+ import { once } from 'node:events';
3
+ import { createServer } from 'node:http';
4
+ import { test } from 'node:test';
5
+
6
+ import type { GuardianResult, OwnedProcess } from './runIsolatedTest.ts';
7
+
8
+ import {
9
+ getOpenapiGeneratorCommand,
10
+ getOpenapiSource,
11
+ validateOpenapiSource,
12
+ } from './generateOpenapiIsolated.ts';
13
+ import { getIsolatedVonaCommand, ROOT_DIR, withIsolatedVona } from './runIsolatedTest.ts';
14
+
15
+ test('builds the isolated Swagger source from the allocated port', () => {
16
+ assert.equal(getOpenapiSource(7123), 'http://127.0.0.1:7123/swagger/json?version=V31');
17
+ });
18
+
19
+ test('forwards OpenAPI module arguments to the existing Zova generator', () => {
20
+ assert.deepEqual(getOpenapiGeneratorCommand(['training-student', '--nometadata']), {
21
+ command: process.execPath,
22
+ args: [
23
+ './zova/packages-cli/cli/src/bin/zova.ts',
24
+ '--projectPath=zova',
25
+ ':openapi:generate',
26
+ 'training-student',
27
+ '--nometadata',
28
+ ],
29
+ cwd: ROOT_DIR,
30
+ });
31
+ });
32
+
33
+ test('rejects project overrides before provisioning isolated resources', () => {
34
+ for (const args of [['--projectPath=other'], ['--projectPath', 'other']]) {
35
+ assert.throws(() => getOpenapiGeneratorCommand(args), /projectPath overrides are unsupported/);
36
+ }
37
+ });
38
+
39
+ test('launches only a loopback single-worker test-mode Vona service', () => {
40
+ const command = getIsolatedVonaCommand(7123, { CUSTOM: 'keep' }, 'test-key');
41
+ assert.equal(command.command, process.execPath);
42
+ assert.deepEqual(command.args, [
43
+ './packages-cli/cli/src/bin/vona.ts',
44
+ ':bin:dev',
45
+ '--workers=1',
46
+ '--flavor=normal',
47
+ '--mode=test',
48
+ ]);
49
+ assert.equal(command.cwd, `${ROOT_DIR}/vona`);
50
+ assert.deepEqual(command.env, {
51
+ CUSTOM: 'keep',
52
+ VONA_TEST_READY_KEY: 'test-key',
53
+ VONA_TEST_SERVER_ORIGIN: 'true',
54
+ SERVER_SERVE_PROTOCOL: 'http',
55
+ SERVER_SERVE_HOST: '127.0.0.1:7123',
56
+ });
57
+ });
58
+
59
+ function lifecycleFixture(options: { readyFailure?: boolean; stopFailure?: boolean } = {}) {
60
+ const events: string[] = [];
61
+ let finish!: (result: GuardianResult) => void;
62
+ const closed = new Promise<GuardianResult>(resolve => {
63
+ finish = resolve;
64
+ });
65
+ const result: GuardianResult = {
66
+ type: 'drained',
67
+ failed: false,
68
+ result: { code: 0, signal: null },
69
+ };
70
+ const server: OwnedProcess = {
71
+ wait: () => closed,
72
+ stop: async () => {
73
+ events.push('stop');
74
+ if (options.stopFailure) throw new Error('shutdown failed');
75
+ finish(result);
76
+ return result;
77
+ },
78
+ };
79
+ return {
80
+ events,
81
+ finish,
82
+ result,
83
+ dependencies: {
84
+ start: async () => {
85
+ events.push('start');
86
+ return server;
87
+ },
88
+ ready: async () => {
89
+ events.push('ready');
90
+ if (options.readyFailure) throw new Error('readiness failed');
91
+ },
92
+ },
93
+ };
94
+ }
95
+
96
+ test('stops Vona after generation succeeds or fails and removes signal handlers', async () => {
97
+ for (const fail of [false, true]) {
98
+ const f = lifecycleFixture();
99
+ const listeners = process.listenerCount('SIGINT');
100
+ const run = withIsolatedVona(
101
+ 7123,
102
+ {},
103
+ async () => {
104
+ f.events.push('generate');
105
+ if (fail) throw new Error('generator failed');
106
+ return 'generated';
107
+ },
108
+ f.dependencies,
109
+ );
110
+ if (fail) await assert.rejects(run, /generator failed/);
111
+ else assert.equal(await run, 'generated');
112
+ assert.deepEqual(f.events, ['start', 'ready', 'generate', 'stop']);
113
+ assert.equal(process.listenerCount('SIGINT'), listeners);
114
+ }
115
+ });
116
+
117
+ test('readiness failure stops Vona without invoking the generator', async () => {
118
+ const f = lifecycleFixture({ readyFailure: true });
119
+ await assert.rejects(
120
+ withIsolatedVona(
121
+ 7123,
122
+ {},
123
+ async () => {
124
+ f.events.push('generate');
125
+ },
126
+ f.dependencies,
127
+ ),
128
+ /readiness failed/,
129
+ );
130
+ assert.deepEqual(f.events, ['start', 'ready', 'stop']);
131
+ });
132
+
133
+ test('preserves both generation and shutdown errors', async () => {
134
+ const f = lifecycleFixture({ stopFailure: true });
135
+ await assert.rejects(
136
+ withIsolatedVona(
137
+ 7123,
138
+ {},
139
+ async () => {
140
+ throw new Error('generator failed');
141
+ },
142
+ f.dependencies,
143
+ ),
144
+ (error: unknown) => {
145
+ assert.ok(error instanceof AggregateError);
146
+ assert.deepEqual(
147
+ error.errors.map(item => item.message),
148
+ ['generator failed', 'shutdown failed'],
149
+ );
150
+ return true;
151
+ },
152
+ );
153
+ });
154
+
155
+ test('an interrupted run or an early server exit cannot succeed', async () => {
156
+ for (const interrupt of [false, true]) {
157
+ const f = lifecycleFixture();
158
+ await assert.rejects(
159
+ withIsolatedVona(
160
+ 7123,
161
+ {},
162
+ async () => {
163
+ if (interrupt) {
164
+ process.emit('SIGINT');
165
+ } else {
166
+ f.finish(f.result);
167
+ await Promise.resolve();
168
+ }
169
+ },
170
+ f.dependencies,
171
+ ),
172
+ /exited early, or run was interrupted/,
173
+ );
174
+ }
175
+ });
176
+
177
+ test('preflight distinguishes HTTP, JSON and OpenAPI shape failures', async () => {
178
+ let status = 200;
179
+ let body = JSON.stringify({ openapi: '3.1.0', info: {}, paths: {} });
180
+ const server = createServer((_req, res) => {
181
+ res.writeHead(status, status === 302 ? { location: '/redirect-target' } : {});
182
+ res.end(body);
183
+ });
184
+ server.listen(0, '127.0.0.1');
185
+ await once(server, 'listening');
186
+ const address = server.address();
187
+ assert.ok(address && typeof address !== 'string');
188
+ const source = getOpenapiSource(address.port);
189
+ try {
190
+ await validateOpenapiSource(source);
191
+ status = 302;
192
+ await assert.rejects(validateOpenapiSource(source), /source request failed/);
193
+ status = 503;
194
+ await assert.rejects(validateOpenapiSource(source), /HTTP 503/);
195
+ status = 200;
196
+ body = 'not-json';
197
+ await assert.rejects(validateOpenapiSource(source), /invalid JSON/);
198
+ for (const value of [null, [], {}, { openapi: '3.1.0', paths: [] }]) {
199
+ body = JSON.stringify(value);
200
+ await assert.rejects(validateOpenapiSource(source), /invalid document/);
201
+ }
202
+ } finally {
203
+ await new Promise<void>((resolve, reject) =>
204
+ server.close(error => (error ? reject(error) : resolve())),
205
+ );
206
+ }
207
+ });