@vmz/vmz 0.0.4 → 0.1.0

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,34 @@
1
+ /**
2
+ * Emit RouteId × LocaleId realization (+ PageMeta / hreflang seed) into dist/_vmz.
3
+ * Consumed by serve-host + static-emit; LocaleId stays out of stable RouteId.
4
+ */
5
+ export declare const LOCALE_ROUTE_REALIZATION_ARTIFACT_SCHEMA = "vmz.locale.route_realization.v0";
6
+ /**
7
+ * @param {string} projectRoot
8
+ * @param {string} distDir
9
+ * @param {{ origin?: string }} [opts]
10
+ */
11
+ export declare function emitLocaleRouteRealization(projectRoot: any, distDir: any, opts?: {}): {
12
+ ok: boolean;
13
+ written: any[];
14
+ diagnostics: any;
15
+ artifact: any;
16
+ } | {
17
+ ok: boolean;
18
+ written: string[];
19
+ artifact: {
20
+ schema: string;
21
+ defaultLocale: any;
22
+ locales: any;
23
+ routing: {
24
+ strategy: any;
25
+ defaultPrefix: any;
26
+ defaultLocale: any;
27
+ };
28
+ origin: string;
29
+ routes: any;
30
+ realizations: any[];
31
+ pageMetas: any[];
32
+ };
33
+ diagnostics: any[];
34
+ };
@@ -0,0 +1,134 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Emit RouteId × LocaleId realization (+ PageMeta / hreflang seed) into dist/_vmz.
4
+ * Consumed by serve-host + static-emit; LocaleId stays out of stable RouteId.
5
+ */
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { checkLocales, localeHasErrors } from './locale-check.js';
9
+ import { buildLocalePageMeta, buildLocaleRouteRealizationTable } from './locale-router.js';
10
+ export const LOCALE_ROUTE_REALIZATION_ARTIFACT_SCHEMA = 'vmz.locale.route_realization.v0';
11
+ /**
12
+ * @param {string} chunkId
13
+ */
14
+ function pathPatternFromChunk(chunkId) {
15
+ const rel = String(chunkId || '').replace(/^pages\//, '');
16
+ const parts = rel.split('/').filter(Boolean);
17
+ const segs = [];
18
+ for (let i = 0; i < parts.length; i++) {
19
+ const p = parts[i];
20
+ if (p === 'index' && i === parts.length - 1)
21
+ continue;
22
+ segs.push(p);
23
+ }
24
+ return segs.length ? `/${segs.join('/')}` : '/';
25
+ }
26
+ /**
27
+ * Layouts are not public RouteNodes for locale realization / SEO.
28
+ * @param {string} chunkId
29
+ */
30
+ function isPublicPageChunk(chunkId) {
31
+ const id = String(chunkId || '');
32
+ if (!id.startsWith('pages/'))
33
+ return false;
34
+ if (/(^|\/)Layout$/.test(id))
35
+ return false;
36
+ return true;
37
+ }
38
+ /**
39
+ * @param {string} projectRoot
40
+ * @param {string} distDir
41
+ * @param {{ origin?: string }} [opts]
42
+ */
43
+ export function emitLocaleRouteRealization(projectRoot, distDir, opts = {}) {
44
+ const report = checkLocales({ projectRoot, checkUnused: false });
45
+ if (localeHasErrors(report)) {
46
+ return { ok: false, written: [], diagnostics: report.diagnostics || [], artifact: null };
47
+ }
48
+ // Missing /locales is warning (not error) — still never silent; no realization artifact yet.
49
+ if (!report.manifest) {
50
+ return { ok: true, written: [], artifact: null, diagnostics: report.diagnostics || [] };
51
+ }
52
+ const deploymentPath = path.join(distDir, 'vmz-deployment.json');
53
+ if (!fs.existsSync(deploymentPath)) {
54
+ return {
55
+ ok: false,
56
+ written: [],
57
+ diagnostics: [{ severity: 'error', code: 'locale.route.missing_deployment', message: 'missing vmz-deployment.json' }],
58
+ artifact: null,
59
+ };
60
+ }
61
+ const deployment = JSON.parse(fs.readFileSync(deploymentPath, 'utf8'));
62
+ const pages = (deployment.units || []).filter((u) => u.kind === 'page' && isPublicPageChunk(String(u.chunkId)));
63
+ const routes = pages.map((u) => ({
64
+ routeId: String(u.chunkId),
65
+ path: pathPatternFromChunk(String(u.chunkId)),
66
+ }));
67
+ const localeEntries = report.manifest?.locales || [];
68
+ const locales = localeEntries.map((l) => l.id);
69
+ const directions = Object.fromEntries(localeEntries.map((l) => [l.id, l.direction || 'ltr']));
70
+ const defaultLocale = report.manifest?.defaultLocale;
71
+ const routing = report.manifest?.routing || { strategy: 'prefix', defaultPrefix: 'include' };
72
+ const table = buildLocaleRouteRealizationTable({
73
+ routes,
74
+ locales,
75
+ defaultLocale,
76
+ routing,
77
+ });
78
+ if (table.status === 'failed') {
79
+ return { ok: false, written: [], diagnostics: table.diagnostics || [], artifact: null };
80
+ }
81
+ const origin = String(opts.origin || process.env.VMZ_SITE_ORIGIN || 'https://example.test').replace(/\/$/, '');
82
+ /** @type {any[]} */
83
+ const pageMetas = [];
84
+ for (const route of routes) {
85
+ for (const loc of locales) {
86
+ const meta = buildLocalePageMeta({
87
+ routeId: route.routeId,
88
+ localeId: loc,
89
+ direction: directions[loc],
90
+ title: route.routeId,
91
+ origin,
92
+ realizations: table.realizations,
93
+ locales,
94
+ defaultLocale,
95
+ });
96
+ pageMetas.push(meta);
97
+ }
98
+ }
99
+ const artifact = {
100
+ schema: LOCALE_ROUTE_REALIZATION_ARTIFACT_SCHEMA,
101
+ defaultLocale,
102
+ locales: localeEntries.map((l) => ({
103
+ id: l.id,
104
+ label: l.label || l.id,
105
+ direction: l.direction || 'ltr',
106
+ })),
107
+ routing: {
108
+ strategy: routing.strategy || 'prefix',
109
+ defaultPrefix: routing.defaultPrefix || 'include',
110
+ defaultLocale,
111
+ },
112
+ origin,
113
+ routes,
114
+ realizations: table.realizations,
115
+ pageMetas,
116
+ };
117
+ const vmzDir = path.join(distDir, '_vmz');
118
+ fs.mkdirSync(vmzDir, { recursive: true });
119
+ const outPath = path.join(vmzDir, 'locale-route-realization.json');
120
+ fs.writeFileSync(outPath, `${JSON.stringify(artifact, null, 2)}\n`, 'utf8');
121
+ const manifestOut = path.join(vmzDir, 'locale-manifest.json');
122
+ fs.writeFileSync(manifestOut, `${JSON.stringify({
123
+ schema: 'vmz.locale.manifest.v0',
124
+ defaultLocale,
125
+ locales: artifact.locales,
126
+ routing: artifact.routing,
127
+ }, null, 2)}\n`, 'utf8');
128
+ return {
129
+ ok: true,
130
+ written: ['_vmz/locale-route-realization.json', '_vmz/locale-manifest.json'],
131
+ artifact,
132
+ diagnostics: [],
133
+ };
134
+ }
@@ -116,6 +116,26 @@ export declare function parseLocaleFromPath(pathname: any, supportedLocales: any
116
116
  localeId: string;
117
117
  restPath: string;
118
118
  };
119
+ /**
120
+ * Rewrite a same-app href to the given LocaleId via route realization.
121
+ * Stable path (no locale) is recovered first, then re-realized — Link never hardcodes locale.
122
+ * @param {string} href
123
+ * @param {string} localeId
124
+ * @param {{
125
+ * locales?: Array<{ id: string }|string>,
126
+ * defaultLocale?: string,
127
+ * routing?: { strategy?: string, defaultPrefix?: string, defaultLocale?: string },
128
+ * }} artifact
129
+ */
130
+ export declare function localizeSameAppHref(href: any, localeId: any, artifact: any): any;
131
+ /**
132
+ * Rewrite `<a data-vmz-route href>` in HTML body to retain `localeId`.
133
+ * @param {string} html
134
+ * @param {string} localeId
135
+ * @param {Parameters<typeof localizeSameAppHref>[2]} artifact
136
+ * @param {(s: string) => string} [escapeAttr]
137
+ */
138
+ export declare function localizeBodyLinks(html: any, localeId: any, artifact: any, escapeAttr?: (s: any) => string): any;
119
139
  /**
120
140
  * Plan redirect / negotiation for an incoming URL (omit-prefix aware).
121
141
  * @param {{
@@ -260,6 +260,74 @@ export function parseLocaleFromPath(pathname, supportedLocales) {
260
260
  }
261
261
  return { localeId: null, restPath: normalizePath(pathname) };
262
262
  }
263
+ /**
264
+ * Rewrite a same-app href to the given LocaleId via route realization.
265
+ * Stable path (no locale) is recovered first, then re-realized — Link never hardcodes locale.
266
+ * @param {string} href
267
+ * @param {string} localeId
268
+ * @param {{
269
+ * locales?: Array<{ id: string }|string>,
270
+ * defaultLocale?: string,
271
+ * routing?: { strategy?: string, defaultPrefix?: string, defaultLocale?: string },
272
+ * }} artifact
273
+ */
274
+ export function localizeSameAppHref(href, localeId, artifact) {
275
+ if (!href || !localeId || !artifact)
276
+ return href;
277
+ if (href.startsWith('#') || /^(mailto|tel|javascript):/i.test(href))
278
+ return href;
279
+ if (/^[a-z][a-z0-9+.-]*:/i.test(href) && !href.startsWith('/'))
280
+ return href;
281
+ let pathname = String(href);
282
+ let search = '';
283
+ let hash = '';
284
+ const hashIdx = pathname.indexOf('#');
285
+ if (hashIdx >= 0) {
286
+ hash = pathname.slice(hashIdx);
287
+ pathname = pathname.slice(0, hashIdx);
288
+ }
289
+ const qIdx = pathname.indexOf('?');
290
+ if (qIdx >= 0) {
291
+ search = pathname.slice(qIdx);
292
+ pathname = pathname.slice(0, qIdx);
293
+ }
294
+ if (!pathname)
295
+ pathname = '/';
296
+ const supported = (artifact.locales || []).map((l) => (typeof l === 'string' ? l : l.id)).filter(Boolean);
297
+ const defaultLocale = artifact.defaultLocale || artifact.routing?.defaultLocale;
298
+ const routing = {
299
+ strategy: artifact.routing?.strategy || 'prefix',
300
+ defaultPrefix: artifact.routing?.defaultPrefix || 'include',
301
+ defaultLocale,
302
+ };
303
+ const parsed = parseLocaleFromPath(pathname, supported);
304
+ const rest = parsed.restPath || '/';
305
+ const realized = realizeRoutePath(localeId, rest, routing);
306
+ return `${realized.path}${search}${hash}`;
307
+ }
308
+ /**
309
+ * Rewrite `<a data-vmz-route href>` in HTML body to retain `localeId`.
310
+ * @param {string} html
311
+ * @param {string} localeId
312
+ * @param {Parameters<typeof localizeSameAppHref>[2]} artifact
313
+ * @param {(s: string) => string} [escapeAttr]
314
+ */
315
+ export function localizeBodyLinks(html, localeId, artifact, escapeAttr = (s) => String(s).replace(/&/g, '&amp;').replace(/"/g, '&quot;')) {
316
+ if (!html || !localeId || !artifact)
317
+ return html;
318
+ return String(html).replace(/<a\b([^>]*)>/gi, (full, attrs) => {
319
+ if (!/\bdata-vmz-route\s*=/.test(attrs))
320
+ return full;
321
+ const hm = attrs.match(/\bhref\s*=\s*"([^"]*)"/i);
322
+ if (!hm)
323
+ return full;
324
+ const next = localizeSameAppHref(hm[1], localeId, artifact);
325
+ if (next === hm[1])
326
+ return full;
327
+ const newAttrs = attrs.replace(/\bhref\s*=\s*"[^"]*"/i, `href="${escapeAttr(next)}"`);
328
+ return `<a${newAttrs}>`;
329
+ });
330
+ }
263
331
  /**
264
332
  * Plan redirect / negotiation for an incoming URL (omit-prefix aware).
265
333
  * @param {{
package/dist/log.d.ts CHANGED
@@ -8,10 +8,10 @@ export declare const log: {
8
8
  warn(...args: any[]): void;
9
9
  /** @param {...unknown} args */
10
10
  error(...args: any[]): void;
11
- /** @param {{ severity: string, path: string, message: string }} d */
11
+ /** @param {{ severity: string, path?: string, message: string, code?: string }} d */
12
12
  diagnostic(d: any): void;
13
13
  /**
14
- * @param {Array<{ severity: string, path: string, message: string }>} diagnostics
14
+ * @param {Array<{ severity: string, path?: string, message: string, code?: string }>} diagnostics
15
15
  * @param {{ denyWarnings?: boolean }} [opts]
16
16
  * @returns {number} failing count (errors, and warnings if denyWarnings)
17
17
  */
package/dist/log.js CHANGED
@@ -19,12 +19,20 @@ export const log = {
19
19
  error(...args) {
20
20
  console.error(stamp('error'), ...args);
21
21
  },
22
- /** @param {{ severity: string, path: string, message: string }} d */
22
+ /** @param {{ severity: string, path?: string, message: string, code?: string }} d */
23
23
  diagnostic(d) {
24
- console.error(`${d.severity}: ${d.path}: ${d.message}`);
24
+ const sev = d.severity || 'error';
25
+ const code = d.code ? `${d.code}: ` : '';
26
+ const loc = d.path ? ` (${d.path})` : '';
27
+ // Prefer `vmz warn|error CODE: message (path)` so locale warnings are visible, not silent.
28
+ if (sev === 'warning') {
29
+ console.error(`${stamp('warn')} ${code}${d.message}${loc}`);
30
+ return;
31
+ }
32
+ console.error(`${stamp(sev === 'error' ? 'error' : sev)} ${code}${d.path ? `${d.path}: ` : ''}${d.message}`);
25
33
  },
26
34
  /**
27
- * @param {Array<{ severity: string, path: string, message: string }>} diagnostics
35
+ * @param {Array<{ severity: string, path?: string, message: string, code?: string }>} diagnostics
28
36
  * @param {{ denyWarnings?: boolean }} [opts]
29
37
  * @returns {number} failing count (errors, and warnings if denyWarnings)
30
38
  */
package/dist/pack.d.ts ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * B4 — Pack stage: consume Deployment IR (VPG-owned units), emit pack manifest.
3
+ * Full oxc minify/chunk-split lands progressively; this stage always runs and
4
+ * records integrity digests so Assemble/Prove never skip the pack contract.
5
+ */
6
+ export declare const PACK_MANIFEST_SCHEMA = "vmz.pack.manifest.v0";
7
+ /**
8
+ * Ensure dom split companions sit next to vmz-dom.js (barrel imports ./dom-core.js).
9
+ * Always refresh from `@vmz/core` when present so SSR/runtime fixes are not sticky in outDir.
10
+ * @param {string} outDir
11
+ * @param {string | null | undefined} coreDist `@vmz/core` dist root
12
+ * @returns {string[]} copied relative names
13
+ */
14
+ export declare function ensureRuntimeCompanions(outDir: any, coreDist: any): any[];
15
+ /**
16
+ * @param {string} outDir
17
+ * @param {{
18
+ * release?: boolean,
19
+ * profileId?: string,
20
+ * assembly?: string,
21
+ * preferredClientFace?: string,
22
+ * coreDist?: string | null,
23
+ * }} [opts]
24
+ */
25
+ export declare function packFromDeploymentIr(outDir: any, opts?: {}): {
26
+ manifest: {
27
+ schema: string;
28
+ profileId: any;
29
+ assembly: any;
30
+ release: boolean;
31
+ preferredClientFace: any;
32
+ deploymentSchema: any;
33
+ unitCount: number;
34
+ units: any[];
35
+ minify: string;
36
+ treeShakeBasis: string;
37
+ bundler: string;
38
+ };
39
+ path: string;
40
+ };
package/dist/pack.js ADDED
@@ -0,0 +1,108 @@
1
+ /**
2
+ * B4 — Pack stage: consume Deployment IR (VPG-owned units), emit pack manifest.
3
+ * Full oxc minify/chunk-split lands progressively; this stage always runs and
4
+ * records integrity digests so Assemble/Prove never skip the pack contract.
5
+ */
6
+ // @ts-nocheck
7
+ import crypto from 'node:crypto';
8
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
9
+ import path from 'node:path';
10
+ import { loadDeploymentIr, planBundleInputs } from './bundler-adapter.js';
11
+ export const PACK_MANIFEST_SCHEMA = 'vmz.pack.manifest.v0';
12
+ /**
13
+ * Ensure dom split companions sit next to vmz-dom.js (barrel imports ./dom-core.js).
14
+ * Always refresh from `@vmz/core` when present so SSR/runtime fixes are not sticky in outDir.
15
+ * @param {string} outDir
16
+ * @param {string | null | undefined} coreDist `@vmz/core` dist root
17
+ * @returns {string[]} copied relative names
18
+ */
19
+ export function ensureRuntimeCompanions(outDir, coreDist) {
20
+ if (!coreDist)
21
+ return [];
22
+ const names = ['dom-core.js', 'dom-ssr.js', 'dom.client.js'];
23
+ const copied = [];
24
+ for (const name of names) {
25
+ const src = path.join(coreDist, name);
26
+ if (!existsSync(src))
27
+ continue;
28
+ const dest = path.join(outDir, name);
29
+ copyFileSync(src, dest);
30
+ copied.push(name);
31
+ }
32
+ return copied;
33
+ }
34
+ /**
35
+ * @param {string} outDir
36
+ * @param {{
37
+ * release?: boolean,
38
+ * profileId?: string,
39
+ * assembly?: string,
40
+ * preferredClientFace?: string,
41
+ * coreDist?: string | null,
42
+ * }} [opts]
43
+ */
44
+ export function packFromDeploymentIr(outDir, opts = {}) {
45
+ ensureRuntimeCompanions(outDir, opts.coreDist);
46
+ const ir = loadDeploymentIr(outDir);
47
+ const inputs = planBundleInputs(outDir, ir);
48
+ const units = [];
49
+ for (const entry of inputs) {
50
+ const abs = entry.entry;
51
+ let digest = null;
52
+ let bytes = 0;
53
+ let present = false;
54
+ if (existsSync(abs)) {
55
+ present = true;
56
+ const buf = readFileSync(abs);
57
+ bytes = buf.length;
58
+ digest = crypto.createHash('sha256').update(buf).digest('hex');
59
+ }
60
+ units.push({
61
+ chunkId: entry.chunkId,
62
+ kind: entry.kind,
63
+ entry: path.relative(outDir, abs).replace(/\\/g, '/'),
64
+ programIr: path.relative(outDir, entry.programIr).replace(/\\/g, '/'),
65
+ source: entry.source,
66
+ present,
67
+ bytes,
68
+ digest,
69
+ rebuilt: Boolean(entry.rebuilt),
70
+ });
71
+ }
72
+ const body = {
73
+ schema: PACK_MANIFEST_SCHEMA,
74
+ profileId: opts.profileId || null,
75
+ assembly: opts.assembly || null,
76
+ release: Boolean(opts.release),
77
+ preferredClientFace: opts.preferredClientFace || '@vmz/core/dom/client',
78
+ deploymentSchema: ir.schema,
79
+ unitCount: units.length,
80
+ units,
81
+ minify: opts.release ? 'oxc-pending' : 'dev-identity',
82
+ treeShakeBasis: 'vpg-deployment-ir',
83
+ bundler: 'vmz-pack',
84
+ };
85
+ body.packDigest = sha256Hex(stableStringify({ ...body }));
86
+ const vmzDir = path.join(outDir, '_vmz');
87
+ mkdirSync(vmzDir, { recursive: true });
88
+ const file = path.join(vmzDir, 'pack-manifest.json');
89
+ writeFileSync(file, `${JSON.stringify(body, null, 2)}\n`, 'utf8');
90
+ return { manifest: body, path: file };
91
+ }
92
+ function stableStringify(value) {
93
+ return JSON.stringify(sortKeys(value));
94
+ }
95
+ function sortKeys(value) {
96
+ if (Array.isArray(value))
97
+ return value.map(sortKeys);
98
+ if (value && typeof value === 'object') {
99
+ const out = {};
100
+ for (const k of Object.keys(value).sort())
101
+ out[k] = sortKeys(value[k]);
102
+ return out;
103
+ }
104
+ return value;
105
+ }
106
+ function sha256Hex(text) {
107
+ return crypto.createHash('sha256').update(text, 'utf8').digest('hex');
108
+ }
@@ -15,7 +15,7 @@ export declare function importMaybeTs(full: any): Promise<any>;
15
15
  * @returns {Promise<{
16
16
  * plugins: import('@vmz/plugin').VmzPlugin[],
17
17
  * engines: import('@vmz/plugin').VmzEngines,
18
- * delivery: import('@vmz/plugin').SiteDeliveryAuthoring | null,
18
+ * delivery: import('@vmz/plugin').DeliveryAuthoring | null,
19
19
  * application: { id?: string } | null,
20
20
  * path: string | null,
21
21
  * pluginPath: string | null,
@@ -35,7 +35,7 @@ export async function importMaybeTs(full) {
35
35
  * @returns {Promise<{
36
36
  * plugins: import('@vmz/plugin').VmzPlugin[],
37
37
  * engines: import('@vmz/plugin').VmzEngines,
38
- * delivery: import('@vmz/plugin').SiteDeliveryAuthoring | null,
38
+ * delivery: import('@vmz/plugin').DeliveryAuthoring | null,
39
39
  * application: { id?: string } | null,
40
40
  * path: string | null,
41
41
  * pluginPath: string | null,
@@ -46,7 +46,7 @@ export async function loadVmzConfig(project) {
46
46
  const plugins = [];
47
47
  /** @type {import('@vmz/plugin').VmzEngines} */
48
48
  let engines = {};
49
- /** @type {import('@vmz/plugin').SiteDeliveryAuthoring | null} */
49
+ /** @type {import('@vmz/plugin').DeliveryAuthoring | null} */
50
50
  let delivery = null;
51
51
  /** @type {{ id?: string } | null} */
52
52
  let application = null;
@@ -19,8 +19,6 @@ export declare function browserProductionScenarioPack(): {
19
19
  runner: string;
20
20
  required: boolean;
21
21
  detail?: undefined;
22
- quarantine?: undefined;
23
- reason?: undefined;
24
22
  } | {
25
23
  scenarioId: string;
26
24
  category: string;
@@ -29,18 +27,6 @@ export declare function browserProductionScenarioPack(): {
29
27
  runner: string;
30
28
  required: boolean;
31
29
  detail: string;
32
- quarantine?: undefined;
33
- reason?: undefined;
34
- } | {
35
- scenarioId: string;
36
- category: string;
37
- fixture: any;
38
- modes: string[];
39
- runner: string;
40
- required: boolean;
41
- quarantine: boolean;
42
- reason: string;
43
- detail?: undefined;
44
30
  })[];
45
31
  };
46
32
  /** Deterministic CI profile for production-test (no flaky disguise). */
@@ -126,56 +126,51 @@ export function browserProductionScenarioPack() {
126
126
  required: true,
127
127
  detail: 'pack digest + CURRENT/PREVIOUS rollback',
128
128
  },
129
- // Explicit quarantinemust never be reported as passed.
129
+ // Previously quarantinednow required with real serve-host/browser runners.
130
130
  {
131
131
  scenarioId: 'production.ui.field.submit',
132
132
  category: 'field',
133
- fixture: null,
133
+ fixture: 'packages/examples/production-inspector',
134
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)',
135
+ runner: 'serve-host-browser',
136
+ required: true,
137
+ detail: 'Field input + validation error on inspector',
139
138
  },
140
139
  {
141
140
  scenarioId: 'production.ui.dialog.focus',
142
141
  category: 'dialog',
143
- fixture: null,
142
+ fixture: 'packages/examples/production-inspector',
144
143
  modes: ['browser'],
145
- runner: 'quarantine',
146
- required: false,
147
- quarantine: true,
148
- reason: 'A4: Dialog focus enter/loop/restore + outside dismiss not assembled',
144
+ runner: 'serve-host-browser',
145
+ required: true,
146
+ detail: 'Dialog open/focus/Escape dismiss on inspector',
149
147
  },
150
148
  {
151
149
  scenarioId: 'production.locale.switch-rtl',
152
150
  category: 'locale',
153
- fixture: null,
151
+ fixture: 'packages/examples/production-router',
154
152
  modes: ['browser'],
155
- runner: 'quarantine',
156
- required: false,
157
- quarantine: true,
158
- reason: 'A4: locale switch / fallback forbidden / RTL not in this pack',
153
+ runner: 'serve-host-browser',
154
+ required: true,
155
+ detail: 'LocaleTransition commit + inspector RTL dir toggle',
159
156
  },
160
157
  {
161
158
  scenarioId: 'production.theme.missing-token',
162
159
  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',
160
+ fixture: 'temp:missing-token',
161
+ modes: ['compile'],
162
+ runner: 'vmz-build',
163
+ required: true,
164
+ detail: 'missing semantic token → build fails unknown_design_token',
169
165
  },
170
166
  {
171
167
  scenarioId: 'production.mount.child-failure',
172
168
  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',
169
+ fixture: 'temp:application-isolation',
170
+ modes: ['deployment'],
171
+ runner: 'application-isolation',
172
+ required: true,
173
+ detail: 'ApplicationMount child failure → 503 application_unavailable; siblings survive',
179
174
  },
180
175
  ],
181
176
  };
@@ -241,16 +241,25 @@ export function publishRelease(releasesRoot, distDir, envelope) {
241
241
  if (!digest)
242
242
  throw new Error('publishRelease: envelope missing artifactDigest');
243
243
  const root = path.resolve(releasesRoot);
244
+ const srcDist = path.resolve(distDir);
245
+ // Node fs.cpSync refuses copying a directory into any subdirectory of itself.
246
+ // Releases root must sit beside dist (e.g. .vmz-releases), never under dist/.
244
247
  const dest = path.join(root, digest);
248
+ const destDist = path.join(dest, 'dist');
249
+ if (root === srcDist || root.startsWith(srcDist + path.sep) || destDist.startsWith(srcDist + path.sep)) {
250
+ throw new Error(`publishRelease: releasesRoot must not be under distDir (got releasesRoot=${root}, distDir=${srcDist})`);
251
+ }
245
252
  fs.mkdirSync(dest, { recursive: true });
246
253
  // Immutable snapshot of packed dist (exclude prior releases nesting).
247
- const destDist = path.join(dest, 'dist');
248
254
  fs.rmSync(destDist, { recursive: true, force: true });
249
- fs.cpSync(path.resolve(distDir), destDist, {
255
+ fs.cpSync(srcDist, destDist, {
250
256
  recursive: true,
251
257
  filter: (src) => {
252
258
  const n = src.replace(/\\/g, '/');
253
- return !n.includes('/.vmz-releases/') && !n.includes('/dist/releases');
259
+ return (!n.includes('/.vmz-releases/') &&
260
+ !n.includes('/.vmz-cdn-releases/') &&
261
+ !n.includes('/releases-cdn/') &&
262
+ !/\/dist\/releases(\/|$)/.test(n));
254
263
  },
255
264
  });
256
265
  writeJson(path.join(dest, 'envelope.json'), envelope);