@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.
- package/README.md +19 -13
- package/dist/application-cmd.d.ts +1 -2
- package/dist/application-cmd.js +9 -10
- package/dist/bundler-adapter.d.ts +2 -3
- package/dist/bundler-adapter.js +2 -3
- package/dist/cdn-policy.d.ts +178 -0
- package/dist/cdn-policy.js +344 -0
- package/dist/cli.d.ts +10 -2
- package/dist/cli.js +179 -16
- package/dist/content-addressed-assets.d.ts +69 -0
- package/dist/content-addressed-assets.js +206 -0
- package/dist/dev-session.d.ts +2 -2
- package/dist/dev-session.js +51 -16
- package/dist/document-build.js +1 -2
- package/dist/document-check.d.ts +1 -1
- package/dist/document-check.js +4 -4
- package/dist/document-cmd.d.ts +1 -2
- package/dist/document-cmd.js +2 -3
- package/dist/document-designs.js +31 -3
- package/dist/document-enrich.js +2 -3
- package/dist/document-evidence.d.ts +4 -4
- package/dist/document-evidence.js +20 -12
- package/dist/document-integrate.d.ts +0 -1
- package/dist/document-integrate.js +0 -1
- package/dist/document-interactive.d.ts +11 -11
- package/dist/document-interactive.js +12 -13
- package/dist/document-locale.d.ts +1 -1
- package/dist/document-locale.js +1 -1
- package/dist/document-markdown.d.ts +1 -2
- package/dist/document-markdown.js +13 -7
- package/dist/document-scan.d.ts +4 -4
- package/dist/document-scan.js +4 -4
- package/dist/document-schema.d.ts +28 -29
- package/dist/document-schema.js +28 -29
- package/dist/explain-cmd.js +3 -3
- package/dist/index.d.ts +349 -789
- package/dist/index.js +109 -85
- package/dist/invocation.d.ts +68 -0
- package/dist/invocation.js +169 -0
- package/dist/locale-check.d.ts +19 -3
- package/dist/locale-check.js +130 -6
- package/dist/locale-cmd.js +6 -7
- package/dist/locale-delivery.d.ts +38 -38
- package/dist/locale-delivery.js +39 -40
- package/dist/locale-router.d.ts +39 -40
- package/dist/locale-router.js +39 -40
- package/dist/locale-runtime.d.ts +39 -39
- package/dist/locale-runtime.js +44 -45
- package/dist/locale-schema.d.ts +1 -2
- package/dist/locale-schema.js +1 -2
- package/dist/locale-tooling.d.ts +13 -13
- package/dist/locale-tooling.js +14 -15
- package/dist/log.d.ts +1 -1
- package/dist/log.js +1 -1
- package/dist/packages.d.ts +1 -2
- package/dist/packages.js +1 -2
- package/dist/plugin-host.d.ts +11 -3
- package/dist/plugin-host.js +21 -5
- package/dist/port.d.ts +10 -0
- package/dist/port.js +46 -0
- package/dist/production-observability.d.ts +286 -0
- package/dist/production-observability.js +469 -0
- package/dist/production-test-pack.d.ts +158 -0
- package/dist/production-test-pack.js +452 -0
- package/dist/refactor-cmd.d.ts +1 -1
- package/dist/refactor-cmd.js +6 -6
- package/dist/release-cmd.d.ts +8 -0
- package/dist/release-cmd.js +126 -0
- package/dist/release-pack.d.ts +96 -0
- package/dist/release-pack.js +337 -0
- package/dist/resolve-native-cli.d.ts +14 -0
- package/dist/resolve-native-cli.js +84 -0
- package/dist/resolve.d.ts +1 -2
- package/dist/resolve.js +1 -2
- package/dist/site-delivery.d.ts +134 -0
- package/dist/site-delivery.js +345 -0
- package/dist/static-emit.d.ts +136 -0
- package/dist/static-emit.js +463 -0
- package/dist/test-cmd.d.ts +2 -2
- package/dist/test-cmd.js +37 -17
- package/dist/watch-diff.d.ts +1 -1
- package/dist/watch-diff.js +1 -1
- 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
|
+
}
|
package/dist/refactor-cmd.d.ts
CHANGED
package/dist/refactor-cmd.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
/**
|
|
3
|
-
* `vmz refactor` —
|
|
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
|
|
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(`
|
|
139
|
+
console.log(` precondition: ${p}`);
|
|
140
140
|
}
|
|
141
141
|
for (const e of plan.edits || []) {
|
|
142
|
-
console.log(`
|
|
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(`
|
|
145
|
+
console.log(` ${d.severity || 'info'}: ${d.message}`);
|
|
146
146
|
}
|
|
147
147
|
if ((plan.edits || []).length === 0) {
|
|
148
|
-
console.log('
|
|
148
|
+
console.log(' edits: (none)');
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
return plan.status === 'rejected' ? 1 : 0;
|
|
@@ -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
|
+
}
|