@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,52 @@
1
+ /**
2
+ * B5 Assemble dispatch + B6 build-proof (per-build semantic id slots).
3
+ */
4
+ export declare const BUILD_PROOF_SCHEMA = "vmz.build.proof.v0";
5
+ export declare const ASSEMBLE_MANIFEST_SCHEMA = "vmz.assemble.manifest.v0";
6
+ /**
7
+ * @param {string} outDir
8
+ * @param {any} ctx
9
+ */
10
+ export declare function assembleDelivery(outDir: any, ctx: any): Promise<{
11
+ manifest: {
12
+ schema: string;
13
+ profileId: any;
14
+ assembly: any;
15
+ serverRuntime: any;
16
+ steps: any[];
17
+ };
18
+ path: string;
19
+ }>;
20
+ /**
21
+ * @param {string} outDir
22
+ * @param {any} ctx
23
+ */
24
+ export declare function emitBuildProof(outDir: any, ctx: any): {
25
+ proof: {
26
+ schema: string;
27
+ profileId: any;
28
+ assembly: any;
29
+ selectionDigest: any;
30
+ packDigest: any;
31
+ assembleDigest: any;
32
+ release: boolean;
33
+ semanticIds: string[];
34
+ slots: {
35
+ 'server-host': {
36
+ status: string;
37
+ };
38
+ 'static-delivery': {
39
+ status: string;
40
+ };
41
+ 'site-fallback': {
42
+ status: string;
43
+ };
44
+ 'asset-graph': {
45
+ status: string;
46
+ };
47
+ };
48
+ productionReadyClaim: boolean;
49
+ note: string;
50
+ };
51
+ path: string;
52
+ };
@@ -0,0 +1,191 @@
1
+ /**
2
+ * B5 Assemble dispatch + B6 build-proof (per-build semantic id slots).
3
+ */
4
+ // @ts-nocheck
5
+ import { mkdirSync, writeFileSync } from 'node:fs';
6
+ import path from 'node:path';
7
+ import { semanticIdsForAssembly, sha256Hex, canonicalJson } from './delivery-profile.js';
8
+ import { emitServerArtifact } from './server-artifact.js';
9
+ import { emitEmbeddedPackaging } from './embedded-packaging.js';
10
+ import { emitSiteDelivery } from './site-delivery.js';
11
+ import { emitWebStatic } from './static-emit.js';
12
+ export const BUILD_PROOF_SCHEMA = 'vmz.build.proof.v0';
13
+ export const ASSEMBLE_MANIFEST_SCHEMA = 'vmz.assemble.manifest.v0';
14
+ /**
15
+ * @param {string} outDir
16
+ * @param {any} ctx
17
+ */
18
+ export async function assembleDelivery(outDir, ctx) {
19
+ const { selection, profile } = ctx;
20
+ const assembly = selection.assembly;
21
+ const result = {
22
+ schema: ASSEMBLE_MANIFEST_SCHEMA,
23
+ profileId: selection.profileId,
24
+ assembly,
25
+ serverRuntime: selection.serverRuntime || null,
26
+ steps: [],
27
+ };
28
+ if (assembly === 'static-cdn' || assembly === 'cdn+server') {
29
+ const staticResult = await emitWebStatic(outDir, { origin: ctx.origin });
30
+ result.steps.push({
31
+ kind: 'static-cdn',
32
+ digest: staticResult.digest,
33
+ htmlFiles: staticResult.htmlFiles?.length ?? 0,
34
+ skipped: staticResult.skipped?.length ?? 0,
35
+ });
36
+ result.staticDelivery = {
37
+ digest: staticResult.digest,
38
+ htmlFiles: staticResult.htmlFiles,
39
+ skipped: staticResult.skipped,
40
+ };
41
+ }
42
+ if (assembly === 'local-static') {
43
+ result.steps.push({
44
+ kind: 'local-static',
45
+ status: 'modules-ready',
46
+ note: 'client modules packed; no ServerArtifact',
47
+ });
48
+ }
49
+ if (assembly === 'server-host' || assembly === 'cdn+server') {
50
+ const server = emitServerArtifact(outDir, {
51
+ profileId: selection.profileId,
52
+ assembly,
53
+ serverRuntime: selection.serverRuntime || 'node',
54
+ packDigest: ctx.pack?.packDigest || null,
55
+ });
56
+ result.steps.push({
57
+ kind: 'server-host',
58
+ status: 'emitted',
59
+ digest: server.artifact.artifactDigest,
60
+ publicRoutes: server.artifact.publicRoutes?.length ?? 0,
61
+ internalCapabilities: server.artifact.internalCapabilities?.length ?? 0,
62
+ httpContractDigest: server.httpContractDigest,
63
+ });
64
+ result.serverArtifact = {
65
+ digest: server.artifact.artifactDigest,
66
+ httpContractDigest: server.httpContractDigest,
67
+ schema: server.artifact.schema,
68
+ selectedRuntime: server.artifact.selectedRuntime,
69
+ };
70
+ }
71
+ const siteAuthoring = profile.sources || null;
72
+ if (siteAuthoring || assembly === 'rust-embedded') {
73
+ if (!siteAuthoring) {
74
+ throw new Error('rust-embedded requires delivery sources (SiteDeliveryContract); cannot assemble without embedded|filesystem|remote baselines');
75
+ }
76
+ const site = emitSiteDelivery(outDir, siteAuthoring, {
77
+ siteId: ctx.siteId,
78
+ });
79
+ result.steps.push({
80
+ kind: 'site-delivery',
81
+ digest: site.contract.contractDigest,
82
+ });
83
+ result.siteDelivery = {
84
+ digest: site.contract.contractDigest,
85
+ schema: site.contract.schema,
86
+ };
87
+ if (assembly === 'rust-embedded') {
88
+ const pack = emitEmbeddedPackaging(outDir, {
89
+ siteId: ctx.siteId,
90
+ contractDigest: site.contract.contractDigest,
91
+ });
92
+ result.steps.push({
93
+ kind: 'embedded-packaging',
94
+ digest: pack.index.indexDigest,
95
+ objectCount: pack.index.objectCount,
96
+ });
97
+ result.embeddedPackaging = {
98
+ digest: pack.index.indexDigest,
99
+ objectCount: pack.index.objectCount,
100
+ schema: pack.index.schema,
101
+ };
102
+ }
103
+ }
104
+ result.packDigest = ctx.pack?.packDigest || null;
105
+ result.assembleDigest = sha256Hex(canonicalJson({ ...result, assembleDigest: undefined }));
106
+ const vmzDir = path.join(outDir, '_vmz');
107
+ mkdirSync(vmzDir, { recursive: true });
108
+ const file = path.join(vmzDir, 'assemble-manifest.json');
109
+ writeFileSync(file, `${JSON.stringify(result, null, 2)}\n`, 'utf8');
110
+ return { manifest: result, path: file };
111
+ }
112
+ /**
113
+ * @param {string} outDir
114
+ * @param {any} ctx
115
+ */
116
+ export function emitBuildProof(outDir, ctx) {
117
+ const semanticIds = semanticIdsForAssembly(ctx.selection.assembly);
118
+ const slots = {
119
+ 'server-host': { status: 'not-applicable' },
120
+ 'static-delivery': { status: 'not-applicable' },
121
+ 'site-fallback': { status: 'not-applicable' },
122
+ 'asset-graph': { status: 'not-applicable' },
123
+ };
124
+ for (const id of semanticIds) {
125
+ if (id === 'static-delivery') {
126
+ const step = (ctx.assemble?.steps || []).find((s) => s.kind === 'static-cdn');
127
+ slots[id] = step
128
+ ? { status: 'emitted', detail: `digest=${String(step.digest).slice(0, 12)}` }
129
+ : { status: 'pending', detail: 'assembly requires static emit' };
130
+ }
131
+ else if (id === 'site-fallback') {
132
+ const siteStep = (ctx.assemble?.steps || []).find((s) => s.kind === 'site-delivery');
133
+ const packStep = (ctx.assemble?.steps || []).find((s) => s.kind === 'embedded-packaging');
134
+ if (siteStep && packStep) {
135
+ slots[id] = {
136
+ status: 'emitted',
137
+ detail: `contract=${String(siteStep.digest).slice(0, 12)}; pack=${String(packStep.digest).slice(0, 12)}; objects=${packStep.objectCount ?? '?'}`,
138
+ };
139
+ }
140
+ else if (siteStep && siteStep.status !== 'skipped') {
141
+ slots[id] = {
142
+ status: 'pending',
143
+ detail: 'site contract emitted; embedded packaging missing',
144
+ };
145
+ }
146
+ else {
147
+ slots[id] = { status: 'pending', detail: siteStep?.reason || 'no site sources' };
148
+ }
149
+ }
150
+ else if (id === 'server-host') {
151
+ const step = (ctx.assemble?.steps || []).find((s) => s.kind === 'server-host');
152
+ slots[id] =
153
+ step && step.status === 'emitted'
154
+ ? {
155
+ status: 'emitted',
156
+ detail: `digest=${String(step.digest).slice(0, 12)}; routes=${step.publicRoutes ?? 0}; internal=${step.internalCapabilities ?? 0}`,
157
+ }
158
+ : {
159
+ status: 'pending',
160
+ detail: step?.note || 'ServerArtifact not emitted',
161
+ };
162
+ }
163
+ else if (id === 'asset-graph') {
164
+ slots[id] = {
165
+ status: ctx.pack?.packDigest ? 'pack-digest' : 'pending',
166
+ detail: ctx.pack?.packDigest
167
+ ? `pack=${String(ctx.pack.packDigest).slice(0, 12)} units=${ctx.pack.unitCount ?? '?'}`
168
+ : 'pack missing',
169
+ };
170
+ }
171
+ }
172
+ const body = {
173
+ schema: BUILD_PROOF_SCHEMA,
174
+ profileId: ctx.selection.profileId,
175
+ assembly: ctx.selection.assembly,
176
+ selectionDigest: ctx.selection.digest || null,
177
+ packDigest: ctx.pack?.packDigest || null,
178
+ assembleDigest: ctx.assemble?.assembleDigest || null,
179
+ release: Boolean(ctx.release),
180
+ semanticIds,
181
+ slots,
182
+ productionReadyClaim: false,
183
+ note: 'Aggregate production-ready requires browser-production + cleared production-proof gaps (08)',
184
+ };
185
+ body.proofDigest = sha256Hex(canonicalJson({ ...body, proofDigest: undefined }));
186
+ const vmzDir = path.join(outDir, '_vmz');
187
+ mkdirSync(vmzDir, { recursive: true });
188
+ const file = path.join(vmzDir, 'build-proof.json');
189
+ writeFileSync(file, `${JSON.stringify(body, null, 2)}\n`, 'utf8');
190
+ return { proof: body, path: file };
191
+ }
@@ -9,9 +9,13 @@ export declare const CACHE_HTML = "public, max-age=0, must-revalidate";
9
9
  export declare const CACHE_ASSET_IMMUTABLE = "public, max-age=31536000, immutable";
10
10
  export declare const CACHE_META = "public, max-age=3600";
11
11
  /**
12
- * Build CDNPolicyManifest from StaticDeliveryManifest (+ optional redirects).
12
+ * Build CDNPolicyManifest from StaticDeliveryManifest (+ optional redirects / locale artifact).
13
+ * Locale-prefixed HTML gets LocaleId-encoded cache keys; Accept-Language must not steal body.
13
14
  * @param {Record<string, any>} staticManifest
14
- * @param {{ redirects?: Array<{ from: string, to: string, status?: number, reason?: string }> }} [opts]
15
+ * @param {{
16
+ * redirects?: Array<{ from: string, to: string, status?: number, reason?: string }>,
17
+ * localeArtifact?: Record<string, any> | null,
18
+ * }} [opts]
15
19
  */
16
20
  export declare function buildCdnPolicyManifest(staticManifest: any, opts?: {}): {
17
21
  schema: string;
@@ -35,7 +39,14 @@ export declare function buildCdnPolicyManifest(staticManifest: any, opts?: {}):
35
39
  };
36
40
  })[];
37
41
  errorDocuments: any;
38
- routes: any;
42
+ routes: any[];
43
+ localeCache: {
44
+ strategy: string;
45
+ varyAcceptLanguage: boolean;
46
+ defaultLocale: any;
47
+ routeCount: number;
48
+ locales: any[];
49
+ };
39
50
  };
40
51
  /**
41
52
  * Write CDN policy + adapter projections under dist/_vmz.
@@ -66,7 +77,14 @@ export declare function emitCdnPolicy(distDir: any, staticManifest: any, opts?:
66
77
  };
67
78
  })[];
68
79
  errorDocuments: any;
69
- routes: any;
80
+ routes: any[];
81
+ localeCache: {
82
+ strategy: string;
83
+ varyAcceptLanguage: boolean;
84
+ defaultLocale: any;
85
+ routeCount: number;
86
+ locales: any[];
87
+ };
70
88
  };
71
89
  adapters: {
72
90
  'local-static': {
@@ -7,6 +7,7 @@ import crypto from 'node:crypto';
7
7
  import fs from 'node:fs';
8
8
  import http from 'node:http';
9
9
  import path from 'node:path';
10
+ import { assertLocaleCacheKey, localeAwareCacheKey } from './locale-router.js';
10
11
  export const CDN_POLICY_MANIFEST_SCHEMA = 'vmz.cdn.policy_manifest.v0';
11
12
  export const CDN_ADAPTER_PROJECTION_SCHEMA = 'vmz.cdn.adapter_projection.v0';
12
13
  /** HTML: revalidate. Hashed/static assets: long immutable. */
@@ -14,13 +15,23 @@ export const CACHE_HTML = 'public, max-age=0, must-revalidate';
14
15
  export const CACHE_ASSET_IMMUTABLE = 'public, max-age=31536000, immutable';
15
16
  export const CACHE_META = 'public, max-age=3600';
16
17
  /**
17
- * Build CDNPolicyManifest from StaticDeliveryManifest (+ optional redirects).
18
+ * Build CDNPolicyManifest from StaticDeliveryManifest (+ optional redirects / locale artifact).
19
+ * Locale-prefixed HTML gets LocaleId-encoded cache keys; Accept-Language must not steal body.
18
20
  * @param {Record<string, any>} staticManifest
19
- * @param {{ redirects?: Array<{ from: string, to: string, status?: number, reason?: string }> }} [opts]
21
+ * @param {{
22
+ * redirects?: Array<{ from: string, to: string, status?: number, reason?: string }>,
23
+ * localeArtifact?: Record<string, any> | null,
24
+ * }} [opts]
20
25
  */
21
26
  export function buildCdnPolicyManifest(staticManifest, opts = {}) {
22
27
  const origin = String(staticManifest.origin || '');
23
- const redirects = [{ from: '/home', to: '/', status: 301, reason: 'canonical-alias' }, ...(opts.redirects || [])];
28
+ const localeArt = opts.localeArtifact || null;
29
+ const localeRedirects = buildOmitPrefixRedirects(staticManifest, localeArt);
30
+ const redirects = [
31
+ { from: '/home', to: '/', status: 301, reason: 'canonical-alias' },
32
+ ...localeRedirects,
33
+ ...(opts.redirects || []),
34
+ ];
24
35
  const headers = [
25
36
  { match: '**/*.html', headers: { 'cache-control': CACHE_HTML } },
26
37
  {
@@ -34,6 +45,47 @@ export function buildCdnPolicyManifest(staticManifest, opts = {}) {
34
45
  { match: '**/robots.txt', headers: { 'cache-control': CACHE_META } },
35
46
  ];
36
47
  const errorDocuments = Array.isArray(staticManifest.errorDocuments) ? staticManifest.errorDocuments : [{ status: 404, path: '404.html' }];
48
+ /** @type {any[]} */
49
+ const routes = [];
50
+ /** @type {any[]} */
51
+ const cacheKeyDiagnostics = [];
52
+ for (const r of staticManifest.routes || []) {
53
+ const localeId = r.localeId || null;
54
+ const alternates = Array.isArray(r.seo?.alternates) ? r.seo.alternates : [];
55
+ const cacheKey = localeId
56
+ ? localeAwareCacheKey({ routeId: String(r.routeId), localeId: String(localeId), path: String(r.path) })
57
+ : `route=${r.routeId}|path=${r.path}`;
58
+ // When LocaleId is bound, prove the key would still be safe even if Vary: Accept-Language were set.
59
+ if (localeId) {
60
+ const assert = assertLocaleCacheKey({
61
+ cacheKey,
62
+ varyAcceptLanguage: true,
63
+ localeId: String(localeId),
64
+ });
65
+ if (!assert.ok) {
66
+ cacheKeyDiagnostics.push(...(assert.diagnostics || []));
67
+ }
68
+ }
69
+ routes.push({
70
+ routeId: r.routeId,
71
+ path: r.path,
72
+ htmlPath: r.htmlPath,
73
+ canonical: r.seo?.canonical || null,
74
+ localeId,
75
+ cacheKey,
76
+ varyAcceptLanguage: false,
77
+ hreflang: alternates.map((a) => ({
78
+ hreflang: a.hreflang,
79
+ href: a.href,
80
+ localeId: a.localeId || null,
81
+ })),
82
+ });
83
+ }
84
+ if (cacheKeyDiagnostics.length) {
85
+ const msg = cacheKeyDiagnostics.map((d) => d.message || d.code).join('; ');
86
+ throw new Error(`buildCdnPolicyManifest: locale cache key contract failed: ${msg}`);
87
+ }
88
+ const localeRoutes = routes.filter((r) => r.localeId);
37
89
  const body = {
38
90
  schema: CDN_POLICY_MANIFEST_SCHEMA,
39
91
  applicationId: staticManifest.applicationId || null,
@@ -44,16 +96,48 @@ export function buildCdnPolicyManifest(staticManifest, opts = {}) {
44
96
  redirects,
45
97
  headers,
46
98
  errorDocuments,
47
- routes: (staticManifest.routes || []).map((r) => ({
48
- routeId: r.routeId,
49
- path: r.path,
50
- htmlPath: r.htmlPath,
51
- canonical: r.seo?.canonical || null,
52
- })),
99
+ routes,
100
+ localeCache: {
101
+ strategy: 'path-locale',
102
+ varyAcceptLanguage: false,
103
+ defaultLocale: localeArt?.defaultLocale || null,
104
+ routeCount: localeRoutes.length,
105
+ locales: [...new Set(localeRoutes.map((r) => r.localeId))],
106
+ },
53
107
  };
54
108
  body.policyDigest = sha256Hex(canonicalJson(body));
55
109
  return body;
56
110
  }
111
+ /**
112
+ * Omit-prefix: /{defaultLocale}/… → canonical unprefixed path (CDN redirect, not Accept-Language).
113
+ * @param {Record<string, any>} staticManifest
114
+ * @param {Record<string, any> | null} localeArt
115
+ */
116
+ function buildOmitPrefixRedirects(staticManifest, localeArt) {
117
+ const routing = localeArt?.routing || {};
118
+ const defaultLocale = localeArt?.defaultLocale || routing.defaultLocale;
119
+ if (!defaultLocale || routing.defaultPrefix !== 'omit')
120
+ return [];
121
+ /** @type {Array<{ from: string, to: string, status: number, reason: string }>} */
122
+ const out = [];
123
+ const seen = new Set();
124
+ for (const r of staticManifest.routes || []) {
125
+ if (r.localeId !== defaultLocale)
126
+ continue;
127
+ const canonical = String(r.path || '/');
128
+ const from = canonical === '/' ? `/${defaultLocale}` : `/${defaultLocale}${canonical.startsWith('/') ? canonical : `/${canonical}`}`;
129
+ if (seen.has(from))
130
+ continue;
131
+ seen.add(from);
132
+ out.push({
133
+ from,
134
+ to: canonical,
135
+ status: 301,
136
+ reason: 'locale-omit-prefix-default',
137
+ });
138
+ }
139
+ return out;
140
+ }
57
141
  /**
58
142
  * Write CDN policy + adapter projections under dist/_vmz.
59
143
  * @param {string} distDir
@@ -61,7 +145,8 @@ export function buildCdnPolicyManifest(staticManifest, opts = {}) {
61
145
  * @param {{ redirects?: Array<{ from: string, to: string, status?: number, reason?: string }> }} [opts]
62
146
  */
63
147
  export function emitCdnPolicy(distDir, staticManifest, opts = {}) {
64
- const policy = buildCdnPolicyManifest(staticManifest, opts);
148
+ const localeArtifact = loadLocaleArtifact(distDir);
149
+ const policy = buildCdnPolicyManifest(staticManifest, { ...opts, localeArtifact });
65
150
  const vmzDir = path.join(distDir, '_vmz');
66
151
  fs.mkdirSync(vmzDir, { recursive: true });
67
152
  fs.writeFileSync(path.join(vmzDir, 'cdn-policy-manifest.json'), `${JSON.stringify(policy, null, 2)}\n`, 'utf8');
@@ -76,6 +161,20 @@ export function emitCdnPolicy(distDir, staticManifest, opts = {}) {
76
161
  fs.writeFileSync(path.join(adaptersDir, 'netlify', '_redirects'), String(netlify.files['_redirects'] || ''), 'utf8');
77
162
  return { policy, adapters: { 'local-static': local, netlify } };
78
163
  }
164
+ /**
165
+ * @param {string} distDir
166
+ */
167
+ function loadLocaleArtifact(distDir) {
168
+ const p = path.join(distDir, '_vmz', 'locale-route-realization.json');
169
+ if (!fs.existsSync(p))
170
+ return null;
171
+ try {
172
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
173
+ }
174
+ catch {
175
+ return null;
176
+ }
177
+ }
79
178
  /**
80
179
  * Project vendor-neutral policy to an adapter. Must preserve redirect targets and forbid SPA fallback.
81
180
  * @param {Record<string, any>} policy
package/dist/cli.js CHANGED
@@ -16,14 +16,16 @@ import { cmdDocument } from './document-cmd.js';
16
16
  import { buildIntegratedDocuments, projectHasDocuments } from './document-integrate.js';
17
17
  import { cmdLocale } from './locale-cmd.js';
18
18
  import { emitLocaleRuntimeModules, localeHasErrors } from './locale-check.js';
19
+ import { emitLocaleRouteRealization } from './locale-route-emit.js';
19
20
  import { cmdApplication } from './application-cmd.js';
20
21
  import { cmdArtifact } from './release-cmd.js';
21
22
  import { cmdRefactor } from './refactor-cmd.js';
22
23
  import { cmdExplain } from './explain-cmd.js';
23
24
  import { resolveNativeVmzCli } from './resolve-native-cli.js';
24
- import { emitWebStatic } from './static-emit.js';
25
- import { emitSiteDelivery } from './site-delivery.js';
26
25
  import { loadVmzConfig } from './plugin-host.js';
26
+ import { normalizeDeliveryAuthoring, selectBuildProfile } from './delivery-profile.js';
27
+ import { packFromDeploymentIr } from './pack.js';
28
+ import { assembleDelivery, emitBuildProof } from './build-assemble.js';
27
29
  /**
28
30
  * @param {string[]} argv
29
31
  */
@@ -120,9 +122,9 @@ Usage:
120
122
 
121
123
  Options:
122
124
  --out-dir, -o <dir> Output directory (default: dist)
123
- --release Release build (build only)
124
- --profile <name> Delivery profile after build (web-static)
125
- --origin <url> Site origin for web-static canonical/sitemap
125
+ --release Release build (omit serve-host; pack minify slot; proof)
126
+ --profile <name> Delivery profile (default from config; builtins: web-ssr|web-static|web-client|web-hybrid)
127
+ --origin <url> Site origin for static-cdn canonical/sitemap
126
128
  --host <host> Listen host (default: 127.0.0.1)
127
129
  --port <port> Listen port (dev: omit = auto from 5173; set = lock)
128
130
  --poll-ms <ms> Dev watch poll interval (default: 300)
@@ -281,7 +283,11 @@ function cmdCheck(args) {
281
283
  try {
282
284
  return runWithPlugins(ws, project, outDir, async () => {
283
285
  const report = ws.check();
284
- const errors = log.diagnostics(report.diagnostics ?? []);
286
+ const { checkLocales, localeHasErrors } = await import('./locale-check.js');
287
+ const localeReport = checkLocales({ projectRoot: project, checkUnused: false });
288
+ // Locale policy is first-class: missing /locales is warning (not silent), hard errors still fail.
289
+ const errors = log.diagnostics([...(report.diagnostics ?? []), ...(localeReport.diagnostics ?? [])]) ||
290
+ (localeHasErrors(localeReport) ? 1 : 0);
285
291
  log.info(`checked ${report.filesChecked} file(s)`);
286
292
  return errors ? 1 : 0;
287
293
  });
@@ -290,6 +296,23 @@ function cmdCheck(args) {
290
296
  ws.dispose();
291
297
  }
292
298
  }
299
+ /**
300
+ * Dedupe locale/build diagnostics by code+path+message (runtime + route emit both report missing manifest).
301
+ * @param {Array<{ code?: string, path?: string, message?: string, severity?: string }>} list
302
+ */
303
+ function dedupeDiagnostics(list) {
304
+ const seen = new Set();
305
+ /** @type {typeof list} */
306
+ const out = [];
307
+ for (const d of list || []) {
308
+ const key = `${d.code || ''}\0${d.path || ''}\0${d.message || ''}\0${d.severity || ''}`;
309
+ if (seen.has(key))
310
+ continue;
311
+ seen.add(key);
312
+ out.push(d);
313
+ }
314
+ return out;
315
+ }
293
316
  /**
294
317
  * @param {import('./index.js').Workspace} ws
295
318
  * @param {string} project
@@ -316,6 +339,21 @@ async function cmdBuild(args) {
316
339
  log.info(`build ${project} → ${outDir}`);
317
340
  const ws = createWorkspace({ root: project, outDir });
318
341
  try {
342
+ const cfg = await loadVmzConfig(project);
343
+ const cliProfile = typeof args.profile === 'string' ? args.profile : '';
344
+ const norm = normalizeDeliveryAuthoring(cfg.delivery ?? null);
345
+ if (!norm.ok) {
346
+ log.diagnostics(norm.diagnostics ?? []);
347
+ log.error('delivery authoring invalid');
348
+ return 1;
349
+ }
350
+ const selected = selectBuildProfile(norm.table, cliProfile);
351
+ if (!selected.ok) {
352
+ log.diagnostics(selected.diagnostics ?? []);
353
+ log.error(`unknown build --profile ${cliProfile || norm.table.default}`);
354
+ return 1;
355
+ }
356
+ log.info(`delivery profile ${selected.selection.profileId} (assembly=${selected.selection.assembly})`);
319
357
  const code = await runWithPlugins(ws, project, outDir, () => {
320
358
  const report = ws.build(Boolean(args.release));
321
359
  const errors = log.diagnostics(report.diagnostics ?? []);
@@ -330,47 +368,91 @@ async function cmdBuild(args) {
330
368
  if (code !== 0)
331
369
  return code;
332
370
  const localeEmit = emitLocaleRuntimeModules(project, outDir);
371
+ const localeRoutes = emitLocaleRouteRealization(project, outDir, {
372
+ origin: typeof args.origin === 'string' ? args.origin : undefined,
373
+ });
374
+ // Always surface locale diagnostics (warnings included) — missing /locales must not be silent.
375
+ // Dedupe: runtime emit + route realization both report the same missing-manifest warning.
376
+ const localeDiags = dedupeDiagnostics([
377
+ ...(localeEmit.diagnostics ?? []),
378
+ ...(localeRoutes.diagnostics ?? []),
379
+ ]);
380
+ log.diagnostics(localeDiags);
333
381
  if (!localeEmit.ok || localeHasErrors({ diagnostics: localeEmit.diagnostics })) {
334
- log.diagnostics(localeEmit.diagnostics ?? []);
335
382
  log.error('locale runtime emit failed');
336
383
  return 1;
337
384
  }
338
385
  if (localeEmit.written.length) {
339
386
  log.info(`locale runtime emit (${localeEmit.written.length} module(s))`);
340
387
  }
388
+ if (!localeRoutes.ok) {
389
+ log.error('locale route realization emit failed');
390
+ return 1;
391
+ }
392
+ if (localeRoutes.written.length) {
393
+ log.info(`locale route realization (${localeRoutes.written.length} artifact(s))`);
394
+ }
341
395
  if (projectHasDocuments(project)) {
342
396
  const docs = await buildIntegratedDocuments({ projectRoot: project, outDir });
343
397
  if (!docs.ok)
344
398
  return 1;
345
399
  }
346
- const cfg = await loadVmzConfig(project);
347
- if (cfg.delivery) {
348
- log.info(`site-delivery emit ${outDir}`);
349
- const site = emitSiteDelivery(outDir, cfg.delivery, {
350
- siteId: cfg.application?.id || undefined,
400
+ let pack = null;
401
+ try {
402
+ pack = packFromDeploymentIr(outDir, {
403
+ release: Boolean(args.release),
404
+ profileId: selected.selection.profileId,
405
+ assembly: selected.selection.assembly,
406
+ coreDist: resolveCoreRuntimeDist(),
351
407
  });
352
- log.info(`site-delivery ok (digest=${String(site.contract.contractDigest).slice(0, 12)}…)`);
408
+ log.info(`pack ok (units=${pack.manifest.unitCount}, digest=${String(pack.manifest.packDigest).slice(0, 12)}…)`);
353
409
  }
354
- const profile = typeof args.profile === 'string' ? args.profile : '';
355
- if (profile === 'web-static') {
356
- const origin = typeof args.origin === 'string' ? args.origin : undefined;
357
- log.info(`web-static emit ${outDir}`);
358
- const result = await emitWebStatic(outDir, { origin });
359
- log.info(`web-static ok (${result.htmlFiles.length} html, ${result.skipped.length} skipped, digest=${result.digest.slice(0, 12)}…)`);
410
+ catch (err) {
411
+ log.error(`pack failed: ${err instanceof Error ? err.message : String(err)}`);
412
+ return 1;
413
+ }
414
+ const origin = typeof args.origin === 'string' ? args.origin : undefined;
415
+ let assemble = null;
416
+ try {
417
+ if (selected.selection.assembly === 'static-cdn') {
418
+ log.info(`web-static emit ${outDir}`);
419
+ }
420
+ assemble = await assembleDelivery(outDir, {
421
+ selection: selected.selection,
422
+ profile: {
423
+ ...selected.profile,
424
+ sources: selected.profile.sources || (norm.table.sugar ? norm.table.profiles[norm.table.default]?.sources : null),
425
+ },
426
+ siteId: cfg.application?.id || undefined,
427
+ origin,
428
+ pack: pack.manifest,
429
+ });
430
+ for (const step of assemble.manifest.steps || []) {
431
+ if (step.kind === 'static-cdn') {
432
+ log.info(`web-static ok (${step.htmlFiles} html, ${step.skipped} skipped, digest=${String(step.digest).slice(0, 12)}…)`);
433
+ }
434
+ else if (step.kind === 'site-delivery' && step.digest) {
435
+ log.info(`site-delivery ok (digest=${String(step.digest).slice(0, 12)}…)`);
436
+ }
437
+ }
360
438
  }
361
- else if (profile) {
362
- log.error(`unknown build --profile ${profile} (supported: web-static)`);
439
+ catch (err) {
440
+ log.error(`assemble failed: ${err instanceof Error ? err.message : String(err)}`);
363
441
  return 1;
364
442
  }
443
+ const proof = emitBuildProof(outDir, {
444
+ selection: selected.selection,
445
+ pack: pack.manifest,
446
+ assemble: assemble.manifest,
447
+ release: Boolean(args.release),
448
+ });
449
+ log.info(`build-proof ok (profile=${proof.proof.profileId}, slots=${proof.proof.semanticIds.join(',')})`);
365
450
  return 0;
366
451
  }
367
452
  finally {
368
453
  ws.dispose();
369
454
  }
370
455
  }
371
- /**
372
- * @param {Record<string, string | boolean> & { _: string[] }} args
373
- */
374
456
  async function cmdServe(args) {
375
457
  const pathArg = args._[0] ?? '.';
376
458
  if (args.build) {