@vmz/vmz 0.0.2 → 0.0.4

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 (83) hide show
  1. package/README.md +19 -13
  2. package/dist/application-cmd.d.ts +1 -2
  3. package/dist/application-cmd.js +9 -10
  4. package/dist/bundler-adapter.d.ts +2 -3
  5. package/dist/bundler-adapter.js +2 -3
  6. package/dist/cdn-policy.d.ts +178 -0
  7. package/dist/cdn-policy.js +344 -0
  8. package/dist/cli.d.ts +10 -2
  9. package/dist/cli.js +179 -16
  10. package/dist/content-addressed-assets.d.ts +69 -0
  11. package/dist/content-addressed-assets.js +206 -0
  12. package/dist/dev-session.d.ts +2 -2
  13. package/dist/dev-session.js +51 -16
  14. package/dist/document-build.js +1 -2
  15. package/dist/document-check.d.ts +1 -1
  16. package/dist/document-check.js +4 -4
  17. package/dist/document-cmd.d.ts +1 -2
  18. package/dist/document-cmd.js +2 -3
  19. package/dist/document-designs.js +31 -3
  20. package/dist/document-enrich.js +2 -3
  21. package/dist/document-evidence.d.ts +4 -4
  22. package/dist/document-evidence.js +20 -12
  23. package/dist/document-integrate.d.ts +0 -1
  24. package/dist/document-integrate.js +0 -1
  25. package/dist/document-interactive.d.ts +11 -11
  26. package/dist/document-interactive.js +12 -13
  27. package/dist/document-locale.d.ts +1 -1
  28. package/dist/document-locale.js +1 -1
  29. package/dist/document-markdown.d.ts +1 -2
  30. package/dist/document-markdown.js +13 -7
  31. package/dist/document-scan.d.ts +4 -4
  32. package/dist/document-scan.js +4 -4
  33. package/dist/document-schema.d.ts +28 -29
  34. package/dist/document-schema.js +28 -29
  35. package/dist/explain-cmd.js +3 -3
  36. package/dist/index.d.ts +349 -789
  37. package/dist/index.js +109 -85
  38. package/dist/invocation.d.ts +68 -0
  39. package/dist/invocation.js +169 -0
  40. package/dist/locale-check.d.ts +19 -3
  41. package/dist/locale-check.js +130 -6
  42. package/dist/locale-cmd.js +6 -7
  43. package/dist/locale-delivery.d.ts +38 -38
  44. package/dist/locale-delivery.js +39 -40
  45. package/dist/locale-router.d.ts +39 -40
  46. package/dist/locale-router.js +39 -40
  47. package/dist/locale-runtime.d.ts +39 -39
  48. package/dist/locale-runtime.js +44 -45
  49. package/dist/locale-schema.d.ts +1 -2
  50. package/dist/locale-schema.js +1 -2
  51. package/dist/locale-tooling.d.ts +13 -13
  52. package/dist/locale-tooling.js +14 -15
  53. package/dist/log.d.ts +1 -1
  54. package/dist/log.js +1 -1
  55. package/dist/packages.d.ts +1 -2
  56. package/dist/packages.js +1 -2
  57. package/dist/plugin-host.d.ts +11 -3
  58. package/dist/plugin-host.js +21 -5
  59. package/dist/port.d.ts +10 -0
  60. package/dist/port.js +46 -0
  61. package/dist/production-observability.d.ts +286 -0
  62. package/dist/production-observability.js +469 -0
  63. package/dist/production-test-pack.d.ts +158 -0
  64. package/dist/production-test-pack.js +452 -0
  65. package/dist/refactor-cmd.d.ts +1 -1
  66. package/dist/refactor-cmd.js +6 -6
  67. package/dist/release-cmd.d.ts +8 -0
  68. package/dist/release-cmd.js +126 -0
  69. package/dist/release-pack.d.ts +96 -0
  70. package/dist/release-pack.js +337 -0
  71. package/dist/resolve-native-cli.d.ts +14 -0
  72. package/dist/resolve-native-cli.js +84 -0
  73. package/dist/resolve.d.ts +1 -2
  74. package/dist/resolve.js +1 -2
  75. package/dist/site-delivery.d.ts +134 -0
  76. package/dist/site-delivery.js +345 -0
  77. package/dist/static-emit.d.ts +136 -0
  78. package/dist/static-emit.js +463 -0
  79. package/dist/test-cmd.d.ts +2 -2
  80. package/dist/test-cmd.js +37 -17
  81. package/dist/watch-diff.d.ts +1 -1
  82. package/dist/watch-diff.js +1 -1
  83. package/package.json +32 -17
@@ -0,0 +1,452 @@
1
+ /**
2
+ * A4: Production scenario pack + deterministic CI profile.
3
+ * Assembles real user-path scenarios for `pnpm verify -- production-test`.
4
+ * Does not invent Vitest/Jest/Playwright semantics.
5
+ */
6
+ // @ts-nocheck
7
+ import crypto from 'node:crypto';
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import { canonicalJson, sha256Hex } from './release-pack.js';
11
+ export const PRODUCTION_SCENARIO_PACK_SCHEMA = 'vmz.production.scenario_pack.v0';
12
+ export const PRODUCTION_CI_PROFILE_SCHEMA = 'vmz.production.ci_profile.v0';
13
+ export const PRODUCTION_TEST_REPORT_SCHEMA = 'vmz.production.test_report.v0';
14
+ /** Official Browser Production Profile v1 scenario pack (thin slice). */
15
+ export function browserProductionScenarioPack() {
16
+ return {
17
+ schema: PRODUCTION_SCENARIO_PACK_SCHEMA,
18
+ id: 'browser-production.v1',
19
+ title: 'Browser Production Profile — production user paths',
20
+ scenarios: [
21
+ {
22
+ scenarioId: 'production.catalog.compile.list',
23
+ category: 'compile',
24
+ fixture: 'packages/examples/production-catalog',
25
+ modes: ['compile'],
26
+ runner: 'vmz-test',
27
+ required: true,
28
+ },
29
+ {
30
+ scenarioId: 'production.catalog.logic.list',
31
+ category: 'logic',
32
+ fixture: 'packages/examples/production-catalog',
33
+ modes: ['logic'],
34
+ runner: 'vmz-test',
35
+ required: true,
36
+ },
37
+ {
38
+ scenarioId: 'production.catalog.ssr.list',
39
+ category: 'ssr',
40
+ fixture: 'packages/examples/production-catalog',
41
+ modes: ['ssr'],
42
+ runner: 'vmz-test',
43
+ required: true,
44
+ },
45
+ {
46
+ scenarioId: 'production.catalog.resume.chip',
47
+ category: 'resume',
48
+ fixture: 'packages/examples/production-catalog',
49
+ modes: ['resume'],
50
+ runner: 'vmz-test',
51
+ required: true,
52
+ },
53
+ {
54
+ scenarioId: 'production.catalog.browser.select',
55
+ category: 'browser',
56
+ fixture: 'packages/examples/production-catalog',
57
+ modes: ['browser'],
58
+ runner: 'vmz-test',
59
+ required: true,
60
+ },
61
+ {
62
+ scenarioId: 'resume.resume.island',
63
+ category: 'island-resume',
64
+ fixture: 'packages/examples/island',
65
+ modes: ['resume'],
66
+ runner: 'vmz-test',
67
+ required: true,
68
+ },
69
+ {
70
+ scenarioId: 'resume.event.entry',
71
+ category: 'island-resume',
72
+ fixture: 'packages/examples/island',
73
+ modes: ['resume'],
74
+ runner: 'vmz-test',
75
+ required: true,
76
+ },
77
+ {
78
+ scenarioId: 't3.deployment.island.resume',
79
+ category: 'deployment',
80
+ fixture: 'packages/examples/island',
81
+ modes: ['deployment'],
82
+ runner: 'vmz-test',
83
+ required: true,
84
+ },
85
+ {
86
+ scenarioId: 't3.deployment.usercard.isolation',
87
+ category: 'server-capability',
88
+ fixture: 'packages/examples/fullstack',
89
+ modes: ['deployment'],
90
+ runner: 'vmz-test',
91
+ required: true,
92
+ },
93
+ {
94
+ scenarioId: 'production.router.access',
95
+ category: 'auth-access',
96
+ fixture: 'packages/examples/production-router',
97
+ modes: ['ssr'],
98
+ runner: 'serve-host',
99
+ required: true,
100
+ detail: 'Page.access allow/deny/not-found/redirect',
101
+ },
102
+ {
103
+ scenarioId: 'production.router.action',
104
+ category: 'loader-action',
105
+ fixture: 'packages/examples/production-router',
106
+ modes: ['ssr'],
107
+ runner: 'serve-host',
108
+ required: true,
109
+ detail: 'Page.action POST + redirect',
110
+ },
111
+ {
112
+ scenarioId: 'production.router.loader-cancel',
113
+ category: 'loader-action',
114
+ fixture: 'packages/examples/production-router',
115
+ modes: ['logic'],
116
+ runner: 'in-process',
117
+ required: true,
118
+ detail: 'AbortSignal cancel + stale generation supersede',
119
+ },
120
+ {
121
+ scenarioId: 'production.release.rollback',
122
+ category: 'artifact-rollback',
123
+ fixture: 'packages/examples/production-router',
124
+ modes: ['deployment'],
125
+ runner: 'release-pack',
126
+ required: true,
127
+ detail: 'pack digest + CURRENT/PREVIOUS rollback',
128
+ },
129
+ // Explicit quarantine — must never be reported as passed.
130
+ {
131
+ scenarioId: 'production.ui.field.submit',
132
+ category: 'field',
133
+ fixture: null,
134
+ modes: ['browser'],
135
+ runner: 'quarantine',
136
+ required: false,
137
+ quarantine: true,
138
+ reason: 'A4: Field input/submit/dup-submit/cancel not assembled (@vmz/ui UI1)',
139
+ },
140
+ {
141
+ scenarioId: 'production.ui.dialog.focus',
142
+ category: 'dialog',
143
+ fixture: null,
144
+ modes: ['browser'],
145
+ runner: 'quarantine',
146
+ required: false,
147
+ quarantine: true,
148
+ reason: 'A4: Dialog focus enter/loop/restore + outside dismiss not assembled',
149
+ },
150
+ {
151
+ scenarioId: 'production.locale.switch-rtl',
152
+ category: 'locale',
153
+ fixture: null,
154
+ modes: ['browser'],
155
+ runner: 'quarantine',
156
+ required: false,
157
+ quarantine: true,
158
+ reason: 'A4: locale switch / fallback forbidden / RTL not in this pack',
159
+ },
160
+ {
161
+ scenarioId: 'production.theme.missing-token',
162
+ category: 'theme',
163
+ fixture: null,
164
+ modes: ['browser'],
165
+ runner: 'quarantine',
166
+ required: false,
167
+ quarantine: true,
168
+ reason: 'A4: theme switch + missing semantic token not in this pack',
169
+ },
170
+ {
171
+ scenarioId: 'production.mount.child-failure',
172
+ category: 'mount',
173
+ fixture: null,
174
+ modes: ['browser'],
175
+ runner: 'quarantine',
176
+ required: false,
177
+ quarantine: true,
178
+ reason: 'A4: ApplicationMount child failure isolation not in this pack',
179
+ },
180
+ ],
181
+ };
182
+ }
183
+ /** Deterministic CI profile for production-test (no flaky disguise). */
184
+ export function browserProductionCiProfile(overrides = {}) {
185
+ return normalizeCiProfile({
186
+ schema: PRODUCTION_CI_PROFILE_SCHEMA,
187
+ id: 'browser-production.ci.v1',
188
+ seed: 0x176bff,
189
+ workers: 1,
190
+ sort: 'scenarioId',
191
+ retry: { enabled: false, maxAttempts: 1, promoteFlakyPass: false },
192
+ quarantine: {
193
+ policy: 'explicit',
194
+ neverCountAsPassed: true,
195
+ },
196
+ artifacts: {
197
+ onFailure: true,
198
+ retain: ['report', 'trace', 'artifactManifest'],
199
+ dir: 'dist/production-reports/production-test',
200
+ },
201
+ toolchain: {
202
+ node: true,
203
+ rust: true,
204
+ chrome: true,
205
+ },
206
+ forbiddenRunners: ['vitest', 'jest', 'playwright', '@playwright/test'],
207
+ ...overrides,
208
+ });
209
+ }
210
+ /**
211
+ * @param {unknown} raw
212
+ * @returns {{ ok: true, pack: Record<string, any> } | { ok: false, diagnostics: Array<{ code: string, message: string }> }}
213
+ */
214
+ export function normalizeScenarioPack(raw) {
215
+ /** @type {Array<{ code: string, message: string }>} */
216
+ const diagnostics = [];
217
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
218
+ return { ok: false, diagnostics: [{ code: 'pack.invalid', message: 'pack must be a plain object' }] };
219
+ }
220
+ const p = /** @type {Record<string, any>} */ (raw);
221
+ if (p.schema !== PRODUCTION_SCENARIO_PACK_SCHEMA) {
222
+ diagnostics.push({
223
+ code: 'pack.schema',
224
+ message: `schema want ${PRODUCTION_SCENARIO_PACK_SCHEMA}, got ${JSON.stringify(p.schema)}`,
225
+ });
226
+ }
227
+ if (typeof p.id !== 'string' || !p.id.trim()) {
228
+ diagnostics.push({ code: 'pack.id', message: 'pack.id required' });
229
+ }
230
+ if (!Array.isArray(p.scenarios) || p.scenarios.length < 1) {
231
+ diagnostics.push({ code: 'pack.scenarios', message: 'scenarios must be a non-empty array' });
232
+ }
233
+ const scenarios = [];
234
+ const seen = new Set();
235
+ for (const [i, s] of (p.scenarios || []).entries()) {
236
+ if (!s || typeof s !== 'object') {
237
+ diagnostics.push({ code: 'pack.scenario', message: `scenarios[${i}] must be an object` });
238
+ continue;
239
+ }
240
+ const scenarioId = String(s.scenarioId || '').trim();
241
+ if (!scenarioId) {
242
+ diagnostics.push({ code: 'pack.scenarioId', message: `scenarios[${i}].scenarioId required` });
243
+ continue;
244
+ }
245
+ if (seen.has(scenarioId)) {
246
+ diagnostics.push({ code: 'pack.scenarioId.dup', message: `duplicate scenarioId ${scenarioId}` });
247
+ continue;
248
+ }
249
+ seen.add(scenarioId);
250
+ const quarantine = s.quarantine === true;
251
+ const required = s.required !== false && !quarantine;
252
+ if (quarantine && s.status === 'passed') {
253
+ diagnostics.push({
254
+ code: 'pack.quarantine.passed',
255
+ message: `${scenarioId}: quarantine must never be authored as passed`,
256
+ });
257
+ }
258
+ scenarios.push({
259
+ scenarioId,
260
+ category: String(s.category || 'other'),
261
+ fixture: s.fixture == null ? null : String(s.fixture),
262
+ modes: Array.isArray(s.modes) ? s.modes.map(String) : [],
263
+ runner: String(s.runner || 'vmz-test'),
264
+ required,
265
+ quarantine,
266
+ reason: s.reason ? String(s.reason) : null,
267
+ detail: s.detail ? String(s.detail) : null,
268
+ });
269
+ }
270
+ // Deterministic order for CI.
271
+ scenarios.sort((a, b) => (a.scenarioId < b.scenarioId ? -1 : a.scenarioId > b.scenarioId ? 1 : 0));
272
+ if (diagnostics.length)
273
+ return { ok: false, diagnostics };
274
+ return {
275
+ ok: true,
276
+ pack: {
277
+ schema: PRODUCTION_SCENARIO_PACK_SCHEMA,
278
+ id: String(p.id).trim(),
279
+ title: typeof p.title === 'string' ? p.title : p.id,
280
+ scenarios,
281
+ },
282
+ };
283
+ }
284
+ /**
285
+ * @param {unknown} raw
286
+ */
287
+ export function normalizeCiProfile(raw) {
288
+ const d = raw && typeof raw === 'object' && !Array.isArray(raw) ? /** @type {Record<string, any>} */ (raw) : {};
289
+ const retry = d.retry && typeof d.retry === 'object' ? d.retry : {};
290
+ const quarantine = d.quarantine && typeof d.quarantine === 'object' ? d.quarantine : {};
291
+ const artifacts = d.artifacts && typeof d.artifacts === 'object' ? d.artifacts : {};
292
+ const seed = typeof d.seed === 'number' && Number.isFinite(d.seed) ? d.seed : 0x176bff;
293
+ return {
294
+ schema: PRODUCTION_CI_PROFILE_SCHEMA,
295
+ id: typeof d.id === 'string' && d.id ? d.id : 'browser-production.ci.v1',
296
+ seed,
297
+ workers: d.workers === 1 ? 1 : 1, // production CI profile forces serial for determinism
298
+ sort: d.sort === 'scenarioId' ? 'scenarioId' : 'scenarioId',
299
+ retry: {
300
+ enabled: retry.enabled === true,
301
+ maxAttempts: Math.max(1, Number(retry.maxAttempts) || 1),
302
+ promoteFlakyPass: false, // hard rule: never disguise flaky as passed
303
+ },
304
+ quarantine: {
305
+ policy: quarantine.policy === 'implicit' ? 'implicit' : 'explicit',
306
+ neverCountAsPassed: quarantine.neverCountAsPassed !== false,
307
+ },
308
+ artifacts: {
309
+ onFailure: artifacts.onFailure !== false,
310
+ retain: Array.isArray(artifacts.retain) ? artifacts.retain.map(String) : ['report', 'trace', 'artifactManifest'],
311
+ dir: typeof artifacts.dir === 'string' ? artifacts.dir : 'dist/production-reports/production-test',
312
+ },
313
+ toolchain: {
314
+ node: true,
315
+ rust: d.toolchain?.rust !== false,
316
+ chrome: d.toolchain?.chrome !== false,
317
+ },
318
+ forbiddenRunners: Array.isArray(d.forbiddenRunners)
319
+ ? d.forbiddenRunners.map(String)
320
+ : ['vitest', 'jest', 'playwright', '@playwright/test'],
321
+ };
322
+ }
323
+ export function scenarioPackDigest(pack) {
324
+ return sha256Hex(canonicalJson(pack));
325
+ }
326
+ export function ciProfileDigest(profile) {
327
+ return sha256Hex(canonicalJson(profile));
328
+ }
329
+ /**
330
+ * Build a production test report from scenario results.
331
+ * Quarantine entries must be status=`quarantined` (never `passed`).
332
+ * @param {{ pack: Record<string, any>, profile: Record<string, any>, results: Array<Record<string, any>>, artifactsDir?: string }} input
333
+ */
334
+ export function buildProductionTestReport(input) {
335
+ const pack = input.pack;
336
+ const profile = input.profile;
337
+ const byId = new Map((input.results || []).map((r) => [r.scenarioId, r]));
338
+ const tests = [];
339
+ /** @type {string[]} */
340
+ const errors = [];
341
+ for (const s of pack.scenarios) {
342
+ const r = byId.get(s.scenarioId);
343
+ if (s.quarantine) {
344
+ const status = r?.status === 'passed' ? 'illegal-passed' : r?.status || 'quarantined';
345
+ if (status === 'illegal-passed' || status === 'passed') {
346
+ errors.push(`${s.scenarioId}: quarantine must not count as passed`);
347
+ }
348
+ tests.push({
349
+ scenarioId: s.scenarioId,
350
+ category: s.category,
351
+ status: 'quarantined',
352
+ required: false,
353
+ quarantine: true,
354
+ reason: s.reason || r?.reason || null,
355
+ detail: r?.detail || null,
356
+ artifacts: r?.artifacts || null,
357
+ attempts: r?.attempts || 1,
358
+ flaky: false,
359
+ });
360
+ continue;
361
+ }
362
+ if (!r) {
363
+ if (s.required)
364
+ errors.push(`${s.scenarioId}: missing result`);
365
+ tests.push({
366
+ scenarioId: s.scenarioId,
367
+ category: s.category,
368
+ status: 'missing',
369
+ required: s.required,
370
+ quarantine: false,
371
+ reason: null,
372
+ detail: null,
373
+ artifacts: null,
374
+ attempts: 0,
375
+ flaky: false,
376
+ });
377
+ continue;
378
+ }
379
+ const attempts = Math.max(1, Number(r.attempts) || 1);
380
+ const flaky = r.flaky === true || (attempts > 1 && r.status === 'passed');
381
+ // Hard rule: retry success must not be reported as stable passed.
382
+ let status = String(r.status || 'error');
383
+ if (flaky && status === 'passed') {
384
+ status = 'flaky';
385
+ errors.push(`${s.scenarioId}: flaky pass must not be reported as passed`);
386
+ }
387
+ if (s.required && status !== 'passed') {
388
+ errors.push(`${s.scenarioId}: required status=${status}`);
389
+ }
390
+ tests.push({
391
+ scenarioId: s.scenarioId,
392
+ category: s.category,
393
+ status,
394
+ required: s.required,
395
+ quarantine: false,
396
+ reason: r.reason || null,
397
+ detail: r.detail || null,
398
+ artifacts: r.artifacts || null,
399
+ attempts,
400
+ flaky,
401
+ });
402
+ }
403
+ // Deterministic listing order.
404
+ tests.sort((a, b) => (a.scenarioId < b.scenarioId ? -1 : a.scenarioId > b.scenarioId ? 1 : 0));
405
+ const failed = tests.some((t) => t.required && !['passed', 'quarantined'].includes(t.status));
406
+ const report = {
407
+ schema: PRODUCTION_TEST_REPORT_SCHEMA,
408
+ status: errors.length || failed ? 'failed' : 'passed',
409
+ packId: pack.id,
410
+ packDigest: scenarioPackDigest(pack),
411
+ ciProfileDigest: ciProfileDigest(profile),
412
+ seed: profile.seed,
413
+ workers: profile.workers,
414
+ retry: profile.retry,
415
+ quarantine: profile.quarantine,
416
+ artifactsDir: input.artifactsDir || profile.artifacts.dir,
417
+ generatedAt: null, // filled by emit for wall-clock; digests exclude this field
418
+ tests,
419
+ errors,
420
+ };
421
+ return report;
422
+ }
423
+ /** Stable digest of report (excludes generatedAt). */
424
+ export function productionTestReportDigest(report) {
425
+ const { generatedAt: _g, ...rest } = report;
426
+ return sha256Hex(canonicalJson(rest));
427
+ }
428
+ /**
429
+ * Write report + pack/profile snapshots under artifactsDir.
430
+ * @returns {{ reportPath: string, packPath: string, profilePath: string, report: Record<string, any> }}
431
+ */
432
+ export function emitProductionTestArtifacts(root, report, pack, profile) {
433
+ const dir = path.join(root, profile.artifacts.dir);
434
+ fs.mkdirSync(dir, { recursive: true });
435
+ const stamped = { ...report, generatedAt: new Date().toISOString() };
436
+ const reportPath = path.join(dir, 'report.json');
437
+ const packPath = path.join(dir, 'scenario-pack.json');
438
+ const profilePath = path.join(dir, 'ci-profile.json');
439
+ fs.writeFileSync(reportPath, `${JSON.stringify(stamped, null, 2)}\n`, 'utf8');
440
+ fs.writeFileSync(packPath, `${JSON.stringify(pack, null, 2)}\n`, 'utf8');
441
+ fs.writeFileSync(profilePath, `${JSON.stringify(profile, null, 2)}\n`, 'utf8');
442
+ return { reportPath, packPath, profilePath, report: stamped };
443
+ }
444
+ /** Assert CI profile forbids JS test-runner disguise. */
445
+ export function assertNoForbiddenRunners(profile, importedNames = []) {
446
+ const forbidden = new Set((profile.forbiddenRunners || []).map((s) => s.toLowerCase()));
447
+ const hits = importedNames.filter((n) => forbidden.has(String(n).toLowerCase()));
448
+ return hits;
449
+ }
450
+ export function sha256Text(text) {
451
+ return crypto.createHash('sha256').update(String(text), 'utf8').digest('hex');
452
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `vmz refactor` — X1 RouteId/field safe rename (plan + atomic apply).
2
+ * `vmz refactor` — RouteId/field safe rename (plan + atomic apply).
3
3
  */
4
4
  /**
5
5
  * @param {string[]} argv args after `refactor`
@@ -1,6 +1,6 @@
1
1
  // @ts-nocheck
2
2
  /**
3
- * `vmz refactor` — X1 RouteId/field safe rename (plan + atomic apply).
3
+ * `vmz refactor` — RouteId/field safe rename (plan + atomic apply).
4
4
  */
5
5
  import { createWorkspace } from './index.js';
6
6
  import { log } from './log.js';
@@ -23,7 +23,7 @@ export async function cmdRefactor(argv) {
23
23
  return 1;
24
24
  }
25
25
  function printRefactorHelp() {
26
- console.log(`vmz refactor — workspace edit plans (X1)
26
+ console.log(`vmz refactor — workspace edit plans
27
27
 
28
28
  Usage:
29
29
  vmz refactor rename --kind <route_id|field|method|component|capability> --from <id> --to <id> [path]
@@ -136,16 +136,16 @@ function cmdRename(argv) {
136
136
  else {
137
137
  log.info(`rename ${kind} \`${from}\` → \`${to}\` → ${plan.status}`);
138
138
  for (const p of plan.preconditions || []) {
139
- console.log(` precondition: ${p}`);
139
+ console.log(` precondition: ${p}`);
140
140
  }
141
141
  for (const e of plan.edits || []) {
142
- console.log(` edit ${e.path} @${e.start}..${e.end} → ${JSON.stringify(e.newText)}`);
142
+ console.log(` edit ${e.path} @${e.start}..${e.end} → ${JSON.stringify(e.newText)}`);
143
143
  }
144
144
  for (const d of plan.diagnostics || []) {
145
- console.log(` ${d.severity || 'info'}: ${d.message}`);
145
+ console.log(` ${d.severity || 'info'}: ${d.message}`);
146
146
  }
147
147
  if ((plan.edits || []).length === 0) {
148
- console.log(' edits: (none)');
148
+ console.log(' edits: (none)');
149
149
  }
150
150
  }
151
151
  return plan.status === 'rejected' ? 1 : 0;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * `vmz artifact` — pack / publish / rollback / diff (A3 filesystem Delivery Profile).
3
+ */
4
+ /**
5
+ * @param {string[]} argv
6
+ * @param {{ root?: string }} [ctx]
7
+ */
8
+ export declare function cmdArtifact(argv: any, ctx?: {}): 0 | 1;
@@ -0,0 +1,126 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * `vmz artifact` — pack / publish / rollback / diff (A3 filesystem Delivery Profile).
4
+ */
5
+ import path from 'node:path';
6
+ import { log } from './log.js';
7
+ import { diffArtifacts, loadReleaseEnvelope, packRelease, publishRelease, readPointer, rollbackRelease } from './release-pack.js';
8
+ function usage() {
9
+ console.log(`vmz artifact — filesystem release packaging (A3)
10
+
11
+ Usage:
12
+ vmz artifact pack [dist] Write dist/_vmz manifests + envelope
13
+ vmz artifact publish [dist] Pack + publish under dist/releases (atomic CURRENT)
14
+ vmz artifact rollback [releases] Restore PREVIOUS pointer (no rebuild)
15
+ vmz artifact diff <aDigest> <bDigest> Structured file digest diff
16
+ vmz artifact current [releases] Print CURRENT digest
17
+
18
+ Options:
19
+ --releases <dir> Releases root (default: <app>/dist/releases)
20
+ --app-id <id> applicationId for envelope
21
+ --json Machine-readable stdout
22
+ `);
23
+ }
24
+ /**
25
+ * @param {string[]} argv
26
+ * @param {{ root?: string }} [ctx]
27
+ */
28
+ export function cmdArtifact(argv, ctx = {}) {
29
+ const args = [...argv];
30
+ if (!args.length || args[0] === 'help' || args[0] === '--help') {
31
+ usage();
32
+ return 0;
33
+ }
34
+ const sub = args.shift();
35
+ /** @type {Record<string, string | boolean>} */
36
+ const flags = {};
37
+ /** @type {string[]} */
38
+ const pos = [];
39
+ for (let i = 0; i < args.length; i++) {
40
+ const a = args[i];
41
+ if (a === '--json') {
42
+ flags.json = true;
43
+ continue;
44
+ }
45
+ if (a === '--releases' || a === '--app-id') {
46
+ flags[a.slice(2)] = args[++i];
47
+ continue;
48
+ }
49
+ if (a.startsWith('--')) {
50
+ const eq = a.indexOf('=');
51
+ if (eq !== -1)
52
+ flags[a.slice(2, eq)] = a.slice(eq + 1);
53
+ else
54
+ flags[a.slice(2)] = true;
55
+ continue;
56
+ }
57
+ pos.push(a);
58
+ }
59
+ const cwd = path.resolve(ctx.root || process.cwd());
60
+ const dist = path.resolve(cwd, pos[0] || 'dist');
61
+ const releases = path.resolve(typeof flags.releases === 'string' ? flags.releases : path.join(cwd, 'dist', 'releases'));
62
+ const appId = typeof flags['app-id'] === 'string' ? flags['app-id'] : undefined;
63
+ const asJson = Boolean(flags.json);
64
+ try {
65
+ if (sub === 'pack') {
66
+ const envelope = packRelease(dist, { applicationId: appId });
67
+ if (asJson)
68
+ console.log(JSON.stringify(envelope, null, 2));
69
+ else
70
+ log.info(`artifact pack: digest=${envelope.artifactDigest} files=${Object.keys(envelope.fileDigests || {}).length}`);
71
+ return 0;
72
+ }
73
+ if (sub === 'publish') {
74
+ const envelope = packRelease(dist, { applicationId: appId });
75
+ const pub = publishRelease(releases, dist, envelope);
76
+ const out = { ...pub, artifactDigest: envelope.artifactDigest };
77
+ if (asJson)
78
+ console.log(JSON.stringify(out, null, 2));
79
+ else
80
+ log.info(`artifact publish: CURRENT=${pub.digest} PREVIOUS=${pub.previous || '-'}`);
81
+ return 0;
82
+ }
83
+ if (sub === 'rollback') {
84
+ const root = pos[0] ? path.resolve(cwd, pos[0]) : releases;
85
+ const rb = rollbackRelease(root);
86
+ if (asJson)
87
+ console.log(JSON.stringify(rb, null, 2));
88
+ else
89
+ log.info(`artifact rollback: restored=${rb.restored} demoted=${rb.demoted || '-'}`);
90
+ return 0;
91
+ }
92
+ if (sub === 'current') {
93
+ const root = pos[0] ? path.resolve(cwd, pos[0]) : releases;
94
+ const cur = readPointer(path.join(root, 'CURRENT'));
95
+ if (asJson)
96
+ console.log(JSON.stringify({ current: cur }, null, 2));
97
+ else
98
+ console.log(cur || '');
99
+ return cur ? 0 : 1;
100
+ }
101
+ if (sub === 'diff') {
102
+ const aDig = pos[0];
103
+ const bDig = pos[1];
104
+ if (!aDig || !bDig) {
105
+ log.error('artifact diff requires <aDigest> <bDigest>');
106
+ return 1;
107
+ }
108
+ const a = loadReleaseEnvelope(releases, aDig);
109
+ const b = loadReleaseEnvelope(releases, bDig);
110
+ const diff = diffArtifacts(a, b);
111
+ if (asJson)
112
+ console.log(JSON.stringify(diff, null, 2));
113
+ else {
114
+ log.info(`artifact diff: identical=${diff.identical} changed=${diff.changed.length} added=${diff.added.length} removed=${diff.removed.length}`);
115
+ }
116
+ return 0;
117
+ }
118
+ log.error(`unknown artifact subcommand: ${sub}`);
119
+ usage();
120
+ return 1;
121
+ }
122
+ catch (e) {
123
+ log.error(String(e && e.message ? e.message : e));
124
+ return 1;
125
+ }
126
+ }