@vmz/vmz 0.0.3 → 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.
@@ -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
+ }
@@ -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
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * A3 filesystem release packaging — digests, atomic pointer, rollback, artifact diff.
3
+ *
4
+ * Not the full CDN/StaticDelivery matrix. Same VPG build (`dist/`) is packed into
5
+ * `_vmz/*` manifests + content digests; publish retains previous release for rollback
6
+ * without rebuild.
7
+ */
8
+ export declare const RELEASE_ENVELOPE_SCHEMA = "vmz.release.envelope.v0";
9
+ export declare const APPLICATION_ARTIFACT_SCHEMA = "vmz.application.artifact.v0";
10
+ export declare const DELIVERY_ARTIFACT_MANIFEST_SCHEMA = "vmz.profile.delivery_artifact_manifest.v0";
11
+ export declare const ROUTE_REALIZATION_TABLE_SCHEMA = "vmz.profile.route_realization_table.v0";
12
+ export declare const ARTIFACT_DIFF_SCHEMA = "vmz.artifact.diff.v0";
13
+ /**
14
+ * @param {Buffer | string} data
15
+ */
16
+ export declare function sha256Hex(data: any): string;
17
+ /**
18
+ * @param {string} filePath
19
+ */
20
+ export declare function sha256File(filePath: any): string;
21
+ /**
22
+ * @param {unknown} value
23
+ */
24
+ export declare function canonicalJson(value: any): string;
25
+ /**
26
+ * Pack `dist/` into `_vmz` manifests + release envelope (filesystem Delivery Profile).
27
+ * @param {string} distDir
28
+ * @param {{ applicationId?: string }} [opts]
29
+ */
30
+ export declare function packRelease(distDir: any, opts?: {}): {
31
+ artifactDigest: string;
32
+ schema: string;
33
+ applicationId: any;
34
+ deliveryProfile: string;
35
+ applicationIntegrity: any;
36
+ deploymentDigest: any;
37
+ programDigest: string;
38
+ styleDigest: any;
39
+ routeDigest: string;
40
+ fileDigests: {};
41
+ manifests: {
42
+ applicationArtifact: string;
43
+ deliveryArtifactManifest: string;
44
+ routeRealization: string;
45
+ };
46
+ };
47
+ /**
48
+ * @param {string} pointerPath
49
+ * @param {string} digest
50
+ */
51
+ export declare function atomicWritePointer(pointerPath: any, digest: any): void;
52
+ /**
53
+ * @param {string} pointerPath
54
+ * @returns {string | null}
55
+ */
56
+ export declare function readPointer(pointerPath: any): string;
57
+ /**
58
+ * Publish packed dist into releases root; retain previous pointer for rollback.
59
+ * @param {string} releasesRoot
60
+ * @param {string} distDir
61
+ * @param {ReturnType<typeof packRelease>} envelope
62
+ */
63
+ export declare function publishRelease(releasesRoot: any, distDir: any, envelope: any): {
64
+ digest: any;
65
+ previous: string;
66
+ currentPath: string;
67
+ releaseDir: string;
68
+ };
69
+ /**
70
+ * Rollback CURRENT → PREVIOUS without rebuild.
71
+ * @param {string} releasesRoot
72
+ */
73
+ export declare function rollbackRelease(releasesRoot: any): {
74
+ restored: string;
75
+ demoted: string;
76
+ releaseDir: string;
77
+ };
78
+ /**
79
+ * Structured diff between two release envelopes / digest maps.
80
+ * @param {{ fileDigests?: Record<string, string>, artifactDigest?: string }} a
81
+ * @param {{ fileDigests?: Record<string, string>, artifactDigest?: string }} b
82
+ */
83
+ export declare function diffArtifacts(a: any, b: any): {
84
+ schema: string;
85
+ beforeDigest: any;
86
+ afterDigest: any;
87
+ added: any[];
88
+ removed: any[];
89
+ changed: any[];
90
+ identical: boolean;
91
+ };
92
+ /**
93
+ * @param {string} releasesRoot
94
+ * @param {string} digest
95
+ */
96
+ export declare function loadReleaseEnvelope(releasesRoot: any, digest: any): any;