@vmz/vmz 0.0.0 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +48 -2
  2. package/bin/vmz.js +4 -0
  3. package/dist/application-cmd.d.ts +22 -0
  4. package/dist/application-cmd.js +348 -0
  5. package/dist/bundler-adapter.d.ts +64 -0
  6. package/dist/bundler-adapter.js +111 -0
  7. package/dist/cli.d.ts +15 -0
  8. package/dist/cli.js +370 -0
  9. package/dist/dev-session.d.ts +35 -0
  10. package/dist/dev-session.js +290 -0
  11. package/dist/document-build.d.ts +99 -0
  12. package/dist/document-build.js +274 -0
  13. package/dist/document-check.d.ts +44 -0
  14. package/dist/document-check.js +246 -0
  15. package/dist/document-cmd.d.ts +9 -0
  16. package/dist/document-cmd.js +147 -0
  17. package/dist/document-designs.d.ts +9 -0
  18. package/dist/document-designs.js +126 -0
  19. package/dist/document-enrich.d.ts +23 -0
  20. package/dist/document-enrich.js +234 -0
  21. package/dist/document-evidence.d.ts +49 -0
  22. package/dist/document-evidence.js +501 -0
  23. package/dist/document-integrate.d.ts +35 -0
  24. package/dist/document-integrate.js +89 -0
  25. package/dist/document-interactive.d.ts +69 -0
  26. package/dist/document-interactive.js +255 -0
  27. package/dist/document-locale.d.ts +31 -0
  28. package/dist/document-locale.js +59 -0
  29. package/dist/document-markdown.d.ts +13 -0
  30. package/dist/document-markdown.js +39 -0
  31. package/dist/document-scan.d.ts +21 -0
  32. package/dist/document-scan.js +151 -0
  33. package/dist/document-schema.d.ts +87 -0
  34. package/dist/document-schema.js +88 -0
  35. package/dist/explain-cmd.d.ts +5 -0
  36. package/dist/explain-cmd.js +123 -0
  37. package/dist/index.d.ts +808 -0
  38. package/dist/index.js +569 -0
  39. package/dist/locale-check.d.ts +106 -0
  40. package/dist/locale-check.js +737 -0
  41. package/dist/locale-cmd.d.ts +5 -0
  42. package/dist/locale-cmd.js +443 -0
  43. package/dist/locale-delivery.d.ts +298 -0
  44. package/dist/locale-delivery.js +444 -0
  45. package/dist/locale-router.d.ts +207 -0
  46. package/dist/locale-router.js +508 -0
  47. package/dist/locale-runtime.d.ts +406 -0
  48. package/dist/locale-runtime.js +542 -0
  49. package/dist/locale-schema.d.ts +9 -0
  50. package/dist/locale-schema.js +10 -0
  51. package/dist/locale-tooling.d.ts +118 -0
  52. package/dist/locale-tooling.js +358 -0
  53. package/dist/log.d.ts +19 -0
  54. package/dist/log.js +42 -0
  55. package/dist/packages.d.ts +27 -0
  56. package/dist/packages.js +147 -0
  57. package/dist/plugin-host.d.ts +30 -0
  58. package/dist/plugin-host.js +370 -0
  59. package/dist/refactor-cmd.d.ts +8 -0
  60. package/dist/refactor-cmd.js +156 -0
  61. package/dist/resolve.d.ts +25 -0
  62. package/dist/resolve.js +56 -0
  63. package/dist/test-cmd.d.ts +9 -0
  64. package/dist/test-cmd.js +343 -0
  65. package/dist/test-compile.d.ts +2 -0
  66. package/dist/test-compile.js +3 -0
  67. package/dist/test-discover.d.ts +2 -0
  68. package/dist/test-discover.js +3 -0
  69. package/dist/test-logic.d.ts +2 -0
  70. package/dist/test-logic.js +3 -0
  71. package/dist/test-protocol.d.ts +2 -0
  72. package/dist/test-protocol.js +3 -0
  73. package/dist/watch-diff.d.ts +17 -0
  74. package/dist/watch-diff.js +56 -0
  75. package/package.json +86 -4
@@ -0,0 +1,542 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Locale I2 runtime: LocaleContext · FormatterContext · negotiation ·
4
+ * atomic LocaleTransition · SSR/client parity.
5
+ * Design: 规划设计/vmz/28 §5–§8
6
+ *
7
+ * Not an I18n IR — ApplicationContext + Delivery projections over VPG Message views.
8
+ */
9
+ import { createHash } from 'node:crypto';
10
+ import { DIAG_FORMATTER_CONTEXT_INCOMPLETE, DIAG_FORMATTER_VERSION_MISMATCH, DIAG_LOCALE_DIGEST_MISMATCH, DIAG_LOCALE_MACHINE_DEFAULT_FORBIDDEN, DIAG_LOCALE_STALE_GENERATION, DIAG_LOCALE_TRANSITION_LOAD_FAILED, DIAG_LOCALE_TRANSITION_PARTIAL, DIAG_LOCALE_TRANSITION_UNSUPPORTED, DIAG_MESSAGE_MIXED_LOCALE, FORMATTER_DATA_VERSION, LOCALE_APPLICATION_CONTEXT_SCHEMA, LOCALE_FALLBACK_RESOLUTION_SCHEMA, LOCALE_FORMATTER_CONTEXT_SCHEMA, LOCALE_RUNTIME_CHECK_SCHEMA, LOCALE_TRANSITION_SCHEMA, } from './locale-schema.js';
11
+ /**
12
+ * Fixed negotiation priority (doc 28 §6). Host only supplies candidates.
13
+ * @param {{
14
+ * supportedLocales: string[],
15
+ * defaultLocale: string,
16
+ * routeLocale?: string|null,
17
+ * userChoice?: string|null,
18
+ * preference?: string|null,
19
+ * hostCandidates?: string[],
20
+ * }} input
21
+ */
22
+ export function negotiateLocale(input) {
23
+ const supported = new Set(input.supportedLocales || []);
24
+ /** @param {string|null|undefined} id */
25
+ const accept = (id) => {
26
+ if (!id || typeof id !== 'string')
27
+ return null;
28
+ // Exact LocaleId only — no zh-TW → zh-hant guessing.
29
+ return supported.has(id) ? id : null;
30
+ };
31
+ return (accept(input.routeLocale) ||
32
+ accept(input.userChoice) ||
33
+ accept(input.preference) ||
34
+ (input.hostCandidates || []).map(accept).find(Boolean) ||
35
+ input.defaultLocale);
36
+ }
37
+ /**
38
+ * @param {{
39
+ * applicationId: string,
40
+ * deliveryId: string,
41
+ * localeId: string,
42
+ * timeZone: string,
43
+ * calendar?: string,
44
+ * numberingSystem?: string,
45
+ * direction?: string,
46
+ * generation?: number,
47
+ * }} opts
48
+ */
49
+ export function buildApplicationContext(opts) {
50
+ return {
51
+ schema: LOCALE_APPLICATION_CONTEXT_SCHEMA,
52
+ applicationId: opts.applicationId,
53
+ deliveryId: opts.deliveryId,
54
+ localeId: opts.localeId,
55
+ timeZone: opts.timeZone,
56
+ calendar: opts.calendar ?? 'gregory',
57
+ numberingSystem: opts.numberingSystem ?? 'latn',
58
+ direction: opts.direction ?? 'ltr',
59
+ generation: opts.generation ?? 1,
60
+ };
61
+ }
62
+ /**
63
+ * @param {ReturnType<typeof buildApplicationContext>} app
64
+ * @param {{ currency?: string }} [opts]
65
+ */
66
+ export function buildFormatterContext(app, opts = {}) {
67
+ return {
68
+ schema: LOCALE_FORMATTER_CONTEXT_SCHEMA,
69
+ localeId: app.localeId,
70
+ timeZone: app.timeZone,
71
+ calendar: app.calendar || 'gregory',
72
+ numberingSystem: app.numberingSystem || 'latn',
73
+ currency: opts.currency ?? null,
74
+ formatterDataVersion: FORMATTER_DATA_VERSION,
75
+ };
76
+ }
77
+ /**
78
+ * Stable digest for Resume / SSR↔client parity.
79
+ * @param {ReturnType<typeof buildFormatterContext>} formatter
80
+ */
81
+ export function formatterContextDigest(formatter) {
82
+ const canonical = {
83
+ schema: formatter.schema,
84
+ localeId: formatter.localeId,
85
+ timeZone: formatter.timeZone,
86
+ calendar: formatter.calendar,
87
+ numberingSystem: formatter.numberingSystem,
88
+ currency: formatter.currency ?? null,
89
+ formatterDataVersion: formatter.formatterDataVersion,
90
+ };
91
+ return createHash('sha256').update(JSON.stringify(canonical)).digest('hex').slice(0, 32);
92
+ }
93
+ /**
94
+ * Server must not read machine locale/timezone defaults.
95
+ * @param {ReturnType<typeof buildFormatterContext>} formatter
96
+ * @param {{ allowMachineDefault?: boolean }} [opts]
97
+ */
98
+ export function validateFormatterContext(formatter, opts = {}) {
99
+ /** @type {Array<{code:string,severity:string,message:string}>} */
100
+ const diagnostics = [];
101
+ if (!formatter?.localeId || !formatter?.timeZone) {
102
+ diagnostics.push({
103
+ code: DIAG_FORMATTER_CONTEXT_INCOMPLETE,
104
+ severity: 'error',
105
+ message: 'FormatterContext requires localeId and timeZone',
106
+ });
107
+ }
108
+ if (!formatter?.calendar || !formatter?.numberingSystem) {
109
+ diagnostics.push({
110
+ code: DIAG_FORMATTER_CONTEXT_INCOMPLETE,
111
+ severity: 'error',
112
+ message: 'FormatterContext requires calendar and numberingSystem',
113
+ });
114
+ }
115
+ if (formatter?.formatterDataVersion && formatter.formatterDataVersion !== FORMATTER_DATA_VERSION) {
116
+ diagnostics.push({
117
+ code: DIAG_FORMATTER_VERSION_MISMATCH,
118
+ severity: 'error',
119
+ message: `formatterDataVersion ${formatter.formatterDataVersion} != ${FORMATTER_DATA_VERSION}`,
120
+ });
121
+ }
122
+ if (!opts.allowMachineDefault) {
123
+ const tz = String(formatter?.timeZone || '');
124
+ if (!tz || tz === 'local' || tz === 'system' || tz === 'Etc/Unknown') {
125
+ diagnostics.push({
126
+ code: DIAG_LOCALE_MACHINE_DEFAULT_FORBIDDEN,
127
+ severity: 'error',
128
+ message: `server FormatterContext forbids machine timezone ${JSON.stringify(tz)}`,
129
+ });
130
+ }
131
+ }
132
+ return {
133
+ ok: diagnostics.length === 0,
134
+ diagnostics,
135
+ };
136
+ }
137
+ /**
138
+ * Resolve one MessageBinding to a single locale variant (whole-message, no mix).
139
+ * @param {{
140
+ * messageId: string,
141
+ * requestedLocale: string,
142
+ * variants: Record<string, { template: string }>,
143
+ * fallback?: Record<string, string[]>,
144
+ * }} input
145
+ */
146
+ export function resolveMessageVariant(input) {
147
+ const fallback = input.fallback || {};
148
+ const chain = [input.requestedLocale, ...(fallback[input.requestedLocale] || [])];
149
+ for (const loc of chain) {
150
+ const variant = input.variants?.[loc];
151
+ if (variant?.template != null) {
152
+ return {
153
+ schema: LOCALE_FALLBACK_RESOLUTION_SCHEMA,
154
+ messageId: input.messageId,
155
+ requestedLocale: input.requestedLocale,
156
+ resolvedLocale: loc,
157
+ fallbackPath: chain.slice(0, chain.indexOf(loc) + 1),
158
+ template: variant.template,
159
+ ok: true,
160
+ };
161
+ }
162
+ }
163
+ return {
164
+ schema: LOCALE_FALLBACK_RESOLUTION_SCHEMA,
165
+ messageId: input.messageId,
166
+ requestedLocale: input.requestedLocale,
167
+ resolvedLocale: null,
168
+ fallbackPath: chain,
169
+ template: null,
170
+ ok: false,
171
+ };
172
+ }
173
+ /**
174
+ * Minimal ICU MessageFormat subset for I2 parity proofs (params + plural + #).
175
+ * @param {string} template
176
+ * @param {Record<string, any>} [args]
177
+ */
178
+ export function formatMessageTemplate(template, args = {}) {
179
+ let text = String(template || '');
180
+ // plural / selectordinal / select blocks first
181
+ text = text.replace(/\{(\w+),\s*(plural|selectordinal|select)\s*,\s*((?:[^={}]+\{[^{}]*\}\s*)+)\}/g, (_m, name, _kind, body) => {
182
+ const n = Number(args[name]);
183
+ const pick = (key) => {
184
+ const re = new RegExp(`${key}\\s*\\{([^{}]*)\\}`);
185
+ const hit = body.match(re);
186
+ return hit ? hit[1] : null;
187
+ };
188
+ let branch = null;
189
+ if (Number.isFinite(n) && Object.hasOwn(args, name)) {
190
+ branch = pick(`=${n}`) || (n === 1 ? pick('one') : null) || pick('other');
191
+ }
192
+ else {
193
+ branch = pick(String(args[name])) || pick('other');
194
+ }
195
+ if (branch == null)
196
+ return _m;
197
+ return branch.replace(/#/g, String(Number.isFinite(n) ? n : (args[name] ?? '')));
198
+ });
199
+ text = text.replace(/\{(\w+)(?:,\s*\w+)?\}/g, (_m, name) => {
200
+ if (Object.hasOwn(args, name))
201
+ return String(args[name]);
202
+ return _m;
203
+ });
204
+ return text;
205
+ }
206
+ /**
207
+ * @param {{
208
+ * applicationId: string,
209
+ * deliveryId: string,
210
+ * supportedLocales: string[],
211
+ * defaultLocale: string,
212
+ * fallback?: Record<string, string[]>,
213
+ * directions?: Record<string, string>,
214
+ * messages: Record<string, { variants: Record<string, { template: string }> }>,
215
+ * initialLocaleId: string,
216
+ * timeZone: string,
217
+ * generation?: number,
218
+ * loadedChunks?: Set<string>|string[],
219
+ * }} opts
220
+ */
221
+ export function createLocaleSession(opts) {
222
+ const supported = new Set(opts.supportedLocales);
223
+ const directions = opts.directions || {};
224
+ const fallback = opts.fallback || {};
225
+ let generation = opts.generation ?? 1;
226
+ /** @type {Set<string>} */
227
+ const loaded = new Set(opts.loadedChunks || [opts.initialLocaleId]);
228
+ let app = buildApplicationContext({
229
+ applicationId: opts.applicationId,
230
+ deliveryId: opts.deliveryId,
231
+ localeId: opts.initialLocaleId,
232
+ timeZone: opts.timeZone,
233
+ direction: directions[opts.initialLocaleId] || 'ltr',
234
+ generation,
235
+ });
236
+ let formatter = buildFormatterContext(app);
237
+ let digest = formatterContextDigest(formatter);
238
+ function snapshot() {
239
+ return {
240
+ applicationContext: { ...app },
241
+ formatterContext: { ...formatter },
242
+ formatterDigest: digest,
243
+ loadedChunks: [...loaded],
244
+ generation,
245
+ };
246
+ }
247
+ /**
248
+ * Render all known messages under current locale (atomic surface proof).
249
+ * @param {Record<string, Record<string, any>>} [argMap]
250
+ */
251
+ function renderAll(argMap = {}) {
252
+ /** @type {Record<string, { text: string, resolvedLocale: string }>} */
253
+ const out = {};
254
+ /** @type {string[]} */
255
+ const resolvedLocales = [];
256
+ for (const [messageId, node] of Object.entries(opts.messages)) {
257
+ const res = resolveMessageVariant({
258
+ messageId,
259
+ requestedLocale: app.localeId,
260
+ variants: node.variants,
261
+ fallback,
262
+ });
263
+ if (!res.ok)
264
+ continue;
265
+ out[messageId] = {
266
+ text: formatMessageTemplate(res.template, argMap[messageId] || {}),
267
+ resolvedLocale: res.resolvedLocale,
268
+ };
269
+ resolvedLocales.push(res.resolvedLocale);
270
+ }
271
+ const unique = [...new Set(resolvedLocales)];
272
+ return { bindings: out, resolvedLocales: unique };
273
+ }
274
+ /**
275
+ * Atomic LocaleTransition (doc 28 §5).
276
+ * @param {string} targetLocaleId
277
+ * @param {{
278
+ * generation?: number,
279
+ * loadChunk?: (localeId: string) => boolean|Promise<boolean>,
280
+ * timeZone?: string,
281
+ * }} [transitionOpts]
282
+ */
283
+ async function transition(targetLocaleId, transitionOpts = {}) {
284
+ const fromLocale = app.localeId;
285
+ const expectedGen = transitionOpts.generation ?? generation;
286
+ if (expectedGen !== generation) {
287
+ return {
288
+ schema: LOCALE_TRANSITION_SCHEMA,
289
+ status: 'cancelled',
290
+ fromLocale,
291
+ toLocale: targetLocaleId,
292
+ reason: 'stale_generation',
293
+ diagnostics: [
294
+ {
295
+ code: DIAG_LOCALE_STALE_GENERATION,
296
+ severity: 'error',
297
+ message: `transition generation ${expectedGen} != session ${generation}`,
298
+ },
299
+ ],
300
+ snapshot: snapshot(),
301
+ };
302
+ }
303
+ if (!supported.has(targetLocaleId)) {
304
+ return {
305
+ schema: LOCALE_TRANSITION_SCHEMA,
306
+ status: 'rejected',
307
+ fromLocale,
308
+ toLocale: targetLocaleId,
309
+ reason: 'unsupported',
310
+ diagnostics: [
311
+ {
312
+ code: DIAG_LOCALE_TRANSITION_UNSUPPORTED,
313
+ severity: 'error',
314
+ message: `LocaleId ${targetLocaleId} not in supportedLocales`,
315
+ },
316
+ ],
317
+ snapshot: snapshot(),
318
+ };
319
+ }
320
+ let loadOk = true;
321
+ if (typeof transitionOpts.loadChunk === 'function') {
322
+ try {
323
+ loadOk = Boolean(await transitionOpts.loadChunk(targetLocaleId));
324
+ }
325
+ catch {
326
+ loadOk = false;
327
+ }
328
+ }
329
+ else if (!loaded.has(targetLocaleId)) {
330
+ // Without a loader, missing chunk cannot commit.
331
+ loadOk = false;
332
+ }
333
+ if (!loadOk) {
334
+ return {
335
+ schema: LOCALE_TRANSITION_SCHEMA,
336
+ status: 'rolled_back',
337
+ fromLocale,
338
+ toLocale: targetLocaleId,
339
+ reason: 'load_failed',
340
+ diagnostics: [
341
+ {
342
+ code: DIAG_LOCALE_TRANSITION_LOAD_FAILED,
343
+ severity: 'error',
344
+ message: `locale chunk ${targetLocaleId} failed to load; keeping ${fromLocale}`,
345
+ },
346
+ ],
347
+ snapshot: snapshot(),
348
+ };
349
+ }
350
+ // Commit only after resources are ready — single transaction.
351
+ loaded.add(targetLocaleId);
352
+ generation += 1;
353
+ app = buildApplicationContext({
354
+ applicationId: opts.applicationId,
355
+ deliveryId: opts.deliveryId,
356
+ localeId: targetLocaleId,
357
+ timeZone: transitionOpts.timeZone || opts.timeZone,
358
+ direction: directions[targetLocaleId] || 'ltr',
359
+ generation,
360
+ });
361
+ formatter = buildFormatterContext(app);
362
+ digest = formatterContextDigest(formatter);
363
+ const rendered = renderAll();
364
+ // After commit, every binding must resolve under the new requested locale
365
+ // (or its explicit fallback chain) — never a mix of independent "half pages".
366
+ const foreign = rendered.resolvedLocales.filter((loc) => loc !== targetLocaleId && !(fallback[targetLocaleId] || []).includes(loc));
367
+ /** @type {any[]} */
368
+ const diagnostics = [];
369
+ if (foreign.length) {
370
+ diagnostics.push({
371
+ code: DIAG_MESSAGE_MIXED_LOCALE,
372
+ severity: 'error',
373
+ message: `post-transition mixed locales: ${foreign.join(',')}`,
374
+ });
375
+ diagnostics.push({
376
+ code: DIAG_LOCALE_TRANSITION_PARTIAL,
377
+ severity: 'error',
378
+ message: 'LocaleTransition produced a partial language surface',
379
+ });
380
+ }
381
+ return {
382
+ schema: LOCALE_TRANSITION_SCHEMA,
383
+ status: diagnostics.length ? 'failed' : 'committed',
384
+ fromLocale,
385
+ toLocale: targetLocaleId,
386
+ reason: diagnostics.length ? 'partial' : 'ok',
387
+ diagnostics,
388
+ snapshot: snapshot(),
389
+ rendered,
390
+ };
391
+ }
392
+ return {
393
+ negotiateLocale,
394
+ snapshot,
395
+ renderAll,
396
+ transition,
397
+ get applicationContext() {
398
+ return { ...app };
399
+ },
400
+ get formatterContext() {
401
+ return { ...formatter };
402
+ },
403
+ get formatterDigest() {
404
+ return digest;
405
+ },
406
+ };
407
+ }
408
+ /**
409
+ * SSR and client must share the same resolved FormatterContext digest and texts.
410
+ * @param {{
411
+ * ssr: { localeId: string, formatterDigest: string, formatterDataVersion?: string, texts: Record<string, string> },
412
+ * client: { localeId: string, formatterDigest: string, formatterDataVersion?: string, texts: Record<string, string> },
413
+ * }} input
414
+ */
415
+ export function checkSsrClientParity(input) {
416
+ /** @type {Array<{code:string,severity:string,message:string}>} */
417
+ const diagnostics = [];
418
+ const { ssr, client } = input;
419
+ if (ssr.localeId !== client.localeId) {
420
+ diagnostics.push({
421
+ code: DIAG_LOCALE_DIGEST_MISMATCH,
422
+ severity: 'error',
423
+ message: `localeId SSR ${ssr.localeId} != client ${client.localeId}`,
424
+ });
425
+ }
426
+ if (ssr.formatterDigest !== client.formatterDigest) {
427
+ diagnostics.push({
428
+ code: DIAG_LOCALE_DIGEST_MISMATCH,
429
+ severity: 'error',
430
+ message: `formatterDigest SSR ${ssr.formatterDigest} != client ${client.formatterDigest}`,
431
+ });
432
+ }
433
+ const ssrVer = ssr.formatterDataVersion || FORMATTER_DATA_VERSION;
434
+ const clientVer = client.formatterDataVersion || FORMATTER_DATA_VERSION;
435
+ if (ssrVer !== clientVer) {
436
+ diagnostics.push({
437
+ code: DIAG_FORMATTER_VERSION_MISMATCH,
438
+ severity: 'error',
439
+ message: `formatterDataVersion SSR ${ssrVer} != client ${clientVer}`,
440
+ });
441
+ }
442
+ const keys = new Set([...Object.keys(ssr.texts || {}), ...Object.keys(client.texts || {})]);
443
+ for (const k of keys) {
444
+ if ((ssr.texts || {})[k] !== (client.texts || {})[k]) {
445
+ diagnostics.push({
446
+ code: DIAG_LOCALE_DIGEST_MISMATCH,
447
+ severity: 'error',
448
+ message: `text mismatch for ${k}: SSR=${JSON.stringify((ssr.texts || {})[k])} client=${JSON.stringify((client.texts || {})[k])}`,
449
+ });
450
+ }
451
+ }
452
+ return {
453
+ schema: LOCALE_RUNTIME_CHECK_SCHEMA,
454
+ status: diagnostics.length ? 'failed' : 'ready',
455
+ kind: 'ssr_client_parity',
456
+ diagnostics,
457
+ };
458
+ }
459
+ /**
460
+ * Aggregate I2 runtime proof for a project check report + explicit contexts.
461
+ * @param {{
462
+ * manifest: { defaultLocale: string, locales: Array<{id:string,direction?:string}>, fallback?: Record<string,string[]> },
463
+ * messages: Array<{ messageId: string, variants: Record<string, { template: string }> }>,
464
+ * applicationId?: string,
465
+ * deliveryId?: string,
466
+ * timeZone?: string,
467
+ * }} input
468
+ */
469
+ export function checkLocaleRuntime(input) {
470
+ /** @type {any[]} */
471
+ const diagnostics = [];
472
+ const supported = (input.manifest?.locales || []).map((l) => l.id);
473
+ const defaultLocale = input.manifest?.defaultLocale;
474
+ const fallback = input.manifest?.fallback || {};
475
+ const directions = Object.fromEntries((input.manifest?.locales || []).map((l) => [l.id, l.direction || 'ltr']));
476
+ /** @type {Record<string, { variants: Record<string, { template: string }> }>} */
477
+ const messages = {};
478
+ for (const m of input.messages || []) {
479
+ messages[m.messageId] = { variants: m.variants };
480
+ }
481
+ const negotiated = negotiateLocale({
482
+ supportedLocales: supported,
483
+ defaultLocale,
484
+ routeLocale: null,
485
+ userChoice: null,
486
+ preference: null,
487
+ hostCandidates: ['zh-TW', 'en'], // must NOT dark-guess
488
+ });
489
+ if (negotiated !== defaultLocale) {
490
+ diagnostics.push({
491
+ code: DIAG_LOCALE_DIGEST_MISMATCH,
492
+ severity: 'error',
493
+ message: `host candidates must not invent LocaleId; got ${negotiated}`,
494
+ });
495
+ }
496
+ const session = createLocaleSession({
497
+ applicationId: input.applicationId || 'app.locales',
498
+ deliveryId: input.deliveryId || 'delivery.web',
499
+ supportedLocales: supported,
500
+ defaultLocale,
501
+ fallback,
502
+ directions,
503
+ messages,
504
+ initialLocaleId: defaultLocale,
505
+ timeZone: input.timeZone || 'Asia/Shanghai',
506
+ loadedChunks: [defaultLocale],
507
+ });
508
+ const fmtCheck = validateFormatterContext(session.formatterContext);
509
+ diagnostics.push(...fmtCheck.diagnostics);
510
+ const ssrSnap = session.snapshot();
511
+ const ssrRender = session.renderAll({
512
+ 'account.greeting': { name: 'Ada' },
513
+ 'account.itemCount': { count: 2 },
514
+ });
515
+ const clientSnap = {
516
+ localeId: ssrSnap.applicationContext.localeId,
517
+ formatterDigest: ssrSnap.formatterDigest,
518
+ formatterDataVersion: ssrSnap.formatterContext.formatterDataVersion,
519
+ texts: Object.fromEntries(Object.entries(ssrRender.bindings).map(([k, v]) => [k, v.text])),
520
+ };
521
+ const parity = checkSsrClientParity({
522
+ ssr: {
523
+ localeId: ssrSnap.applicationContext.localeId,
524
+ formatterDigest: ssrSnap.formatterDigest,
525
+ formatterDataVersion: ssrSnap.formatterContext.formatterDataVersion,
526
+ texts: clientSnap.texts,
527
+ },
528
+ client: clientSnap,
529
+ });
530
+ diagnostics.push(...parity.diagnostics);
531
+ return {
532
+ schema: LOCALE_RUNTIME_CHECK_SCHEMA,
533
+ status: diagnostics.some((d) => d.severity === 'error') ? 'failed' : 'ready',
534
+ negotiatedLocale: negotiated,
535
+ applicationContext: ssrSnap.applicationContext,
536
+ formatterContext: ssrSnap.formatterContext,
537
+ formatterDigest: ssrSnap.formatterDigest,
538
+ formatterDataVersion: FORMATTER_DATA_VERSION,
539
+ diagnostics,
540
+ session,
541
+ };
542
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Locale I0–I5 schema constants & diagnostic codes.
3
+ * Design: 规划设计/vmz/28
4
+ */
5
+ export { FORMATTER_DATA_VERSION, LOCALE_APPLICATION_CONTEXT_SCHEMA, LOCALE_CHECK_SCHEMA, LOCALE_CHUNK_MANIFEST_SCHEMA, LOCALE_CONFORMANCE_SCHEMA, LOCALE_DELIVERY_CHECK_SCHEMA, LOCALE_DELIVERY_RESOLUTION_SCHEMA, LOCALE_DIAG_CACHE_KEY_STEALS_CONTENT as DIAG_LOCALE_CACHE_KEY_STEALS_CONTENT, LOCALE_DIAG_CANONICAL_MISSING as DIAG_LOCALE_CANONICAL_MISSING, LOCALE_DIAG_CATALOG_CONFLICT as DIAG_CATALOG_CONFLICT, LOCALE_DIAG_CATALOG_PARSE as DIAG_CATALOG_PARSE, LOCALE_DIAG_CHUNK_HASH_MISMATCH as DIAG_LOCALE_CHUNK_HASH_MISMATCH, LOCALE_DIAG_CONFORMANCE_DIVERGENCE as DIAG_LOCALE_CONFORMANCE_DIVERGENCE, LOCALE_DIAG_DEFAULT_MISSING as DIAG_DEFAULT_MISSING, LOCALE_DIAG_DELIVERY_FULL_BUNDLE as DIAG_LOCALE_DELIVERY_FULL_BUNDLE, LOCALE_DIAG_DIGEST_MISMATCH as DIAG_LOCALE_DIGEST_MISMATCH, LOCALE_DIAG_DIR_MISSING as DIAG_DIR_MISSING, LOCALE_DIAG_DIR_ORPHAN as DIAG_DIR_ORPHAN, LOCALE_DIAG_EXPLAIN_UNKNOWN as DIAG_LOCALE_EXPLAIN_UNKNOWN, LOCALE_DIAG_FALLBACK_CYCLE as DIAG_FALLBACK_CYCLE, LOCALE_DIAG_FALLBACK_UNKNOWN as DIAG_FALLBACK_UNKNOWN, LOCALE_DIAG_FORMATTER_CONTEXT_INCOMPLETE as DIAG_FORMATTER_CONTEXT_INCOMPLETE, LOCALE_DIAG_FORMATTER_VERSION_MISMATCH as DIAG_FORMATTER_VERSION_MISMATCH, LOCALE_DIAG_HARDCODED_TEXT as DIAG_LOCALE_HARDCODED_TEXT, LOCALE_DIAG_HOST_MESSAGE_DIVERGENCE as DIAG_LOCALE_HOST_MESSAGE_DIVERGENCE, LOCALE_DIAG_HREFLANG_INCOMPLETE as DIAG_LOCALE_HREFLANG_INCOMPLETE, LOCALE_DIAG_ID_COLLISION as DIAG_ID_COLLISION, LOCALE_DIAG_ID_INVALID as DIAG_ID_INVALID, LOCALE_DIAG_LAYOUT_ILLEGAL as DIAG_LAYOUT_ILLEGAL, LOCALE_DIAG_LINK_HARDCODED_PATH as DIAG_LOCALE_LINK_HARDCODED_PATH, LOCALE_DIAG_MACHINE_DEFAULT_FORBIDDEN as DIAG_LOCALE_MACHINE_DEFAULT_FORBIDDEN, LOCALE_DIAG_MANIFEST_MISSING as DIAG_MANIFEST_MISSING, LOCALE_DIAG_MESSAGE_ARRAY_FORBIDDEN as DIAG_MESSAGE_ARRAY_FORBIDDEN, LOCALE_DIAG_MESSAGE_DYNAMIC_ID_UNBOUNDED as DIAG_MESSAGE_DYNAMIC_ID_UNBOUNDED, LOCALE_DIAG_MESSAGE_HTML_FORBIDDEN as DIAG_MESSAGE_HTML_FORBIDDEN, LOCALE_DIAG_MESSAGE_MISSING_DEFAULT as DIAG_MESSAGE_MISSING_DEFAULT, LOCALE_DIAG_MESSAGE_MISSING_VARIANT as DIAG_MESSAGE_MISSING_VARIANT, LOCALE_DIAG_MESSAGE_MIXED_LOCALE as DIAG_MESSAGE_MIXED_LOCALE, LOCALE_DIAG_MESSAGE_PARAMETER_MISMATCH as DIAG_MESSAGE_PARAMETER_MISMATCH, LOCALE_DIAG_MESSAGE_SYNTAX_INVALID as DIAG_MESSAGE_SYNTAX_INVALID, LOCALE_DIAG_MESSAGE_UNUSED as DIAG_MESSAGE_UNUSED, LOCALE_DIAG_META_LOCALE_MISMATCH as DIAG_LOCALE_META_LOCALE_MISMATCH, LOCALE_DIAG_MINI_CROSS_PACKAGE_UNPROVEN as DIAG_LOCALE_MINI_CROSS_PACKAGE_UNPROVEN, LOCALE_DIAG_NATIVE_PACK_APP_MISMATCH as DIAG_LOCALE_NATIVE_PACK_APP_MISMATCH, LOCALE_DIAG_NATIVE_PACK_HAS_JS as DIAG_LOCALE_NATIVE_PACK_HAS_JS, LOCALE_DIAG_NATIVE_PACK_UNSIGNED as DIAG_LOCALE_NATIVE_PACK_UNSIGNED, LOCALE_DIAG_PREFIX_OMIT_WITHOUT_REDIRECT as DIAG_LOCALE_PREFIX_OMIT_WITHOUT_REDIRECT, LOCALE_DIAG_PSEUDO_PRODUCTION_FORBIDDEN as DIAG_LOCALE_PSEUDO_PRODUCTION_FORBIDDEN, LOCALE_DIAG_ROUTE_COLLISION as DIAG_LOCALE_ROUTE_COLLISION, LOCALE_DIAG_SERVER_FORMAT_WITHOUT_CONTEXT as DIAG_LOCALE_SERVER_FORMAT_WITHOUT_CONTEXT, LOCALE_DIAG_SERVER_TRANSLATED_ERROR as DIAG_LOCALE_SERVER_TRANSLATED_ERROR, LOCALE_DIAG_STALE_GENERATION as DIAG_LOCALE_STALE_GENERATION, LOCALE_DIAG_TRANSITION_LOAD_FAILED as DIAG_LOCALE_TRANSITION_LOAD_FAILED, LOCALE_DIAG_TRANSITION_PARTIAL as DIAG_LOCALE_TRANSITION_PARTIAL, LOCALE_DIAG_TRANSITION_UNSUPPORTED as DIAG_LOCALE_TRANSITION_UNSUPPORTED, LOCALE_DIFF_SCHEMA, LOCALE_EXPLAIN_SCHEMA, LOCALE_EXTRACT_SCHEMA, LOCALE_FALLBACK_RESOLUTION_SCHEMA, LOCALE_FORMATTER_CONTEXT_SCHEMA, LOCALE_LINK_RESOLUTION_SCHEMA, LOCALE_MANIFEST_SCHEMA, LOCALE_MESSAGE_CATALOG_SCHEMA as MESSAGE_CATALOG_SCHEMA, LOCALE_MESSAGE_NODE_SCHEMA as MESSAGE_NODE_SCHEMA, LOCALE_MINI_PACKAGE_PROOF_SCHEMA, LOCALE_NATIVE_PACK_SCHEMA, LOCALE_PAGE_META_SCHEMA, LOCALE_PROTOCOL, LOCALE_PSEUDO_SCHEMA, LOCALE_RENAME_SCHEMA, LOCALE_ROUTE_REALIZATION_SCHEMA, LOCALE_ROUTER_CHECK_SCHEMA, LOCALE_RUNTIME_CHECK_SCHEMA, LOCALE_SERVER_ERROR_ENVELOPE_SCHEMA, LOCALE_TRANSITION_SCHEMA, LOCALE_TYPED_MODULE_SCHEMA, LOCALE_VIRTUAL_MODULE_PREFIX, } from '@vmz/protocol';
6
+ /** Reserved first-level names under /locales (not LocaleId dirs). */
7
+ export declare const LOCALE_RESERVED_TOP: Set<string>;
8
+ /** Lowercase ASCII BCP 47-ish with `-` only. */
9
+ export declare const LOCALE_ID_RE: RegExp;
@@ -0,0 +1,10 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Locale I0–I5 schema constants & diagnostic codes.
4
+ * Design: 规划设计/vmz/28
5
+ */
6
+ export { FORMATTER_DATA_VERSION, LOCALE_APPLICATION_CONTEXT_SCHEMA, LOCALE_CHECK_SCHEMA, LOCALE_CHUNK_MANIFEST_SCHEMA, LOCALE_CONFORMANCE_SCHEMA, LOCALE_DELIVERY_CHECK_SCHEMA, LOCALE_DELIVERY_RESOLUTION_SCHEMA, LOCALE_DIAG_CACHE_KEY_STEALS_CONTENT as DIAG_LOCALE_CACHE_KEY_STEALS_CONTENT, LOCALE_DIAG_CANONICAL_MISSING as DIAG_LOCALE_CANONICAL_MISSING, LOCALE_DIAG_CATALOG_CONFLICT as DIAG_CATALOG_CONFLICT, LOCALE_DIAG_CATALOG_PARSE as DIAG_CATALOG_PARSE, LOCALE_DIAG_CHUNK_HASH_MISMATCH as DIAG_LOCALE_CHUNK_HASH_MISMATCH, LOCALE_DIAG_CONFORMANCE_DIVERGENCE as DIAG_LOCALE_CONFORMANCE_DIVERGENCE, LOCALE_DIAG_DEFAULT_MISSING as DIAG_DEFAULT_MISSING, LOCALE_DIAG_DELIVERY_FULL_BUNDLE as DIAG_LOCALE_DELIVERY_FULL_BUNDLE, LOCALE_DIAG_DIGEST_MISMATCH as DIAG_LOCALE_DIGEST_MISMATCH, LOCALE_DIAG_DIR_MISSING as DIAG_DIR_MISSING, LOCALE_DIAG_DIR_ORPHAN as DIAG_DIR_ORPHAN, LOCALE_DIAG_EXPLAIN_UNKNOWN as DIAG_LOCALE_EXPLAIN_UNKNOWN, LOCALE_DIAG_FALLBACK_CYCLE as DIAG_FALLBACK_CYCLE, LOCALE_DIAG_FALLBACK_UNKNOWN as DIAG_FALLBACK_UNKNOWN, LOCALE_DIAG_FORMATTER_CONTEXT_INCOMPLETE as DIAG_FORMATTER_CONTEXT_INCOMPLETE, LOCALE_DIAG_FORMATTER_VERSION_MISMATCH as DIAG_FORMATTER_VERSION_MISMATCH, LOCALE_DIAG_HARDCODED_TEXT as DIAG_LOCALE_HARDCODED_TEXT, LOCALE_DIAG_HOST_MESSAGE_DIVERGENCE as DIAG_LOCALE_HOST_MESSAGE_DIVERGENCE, LOCALE_DIAG_HREFLANG_INCOMPLETE as DIAG_LOCALE_HREFLANG_INCOMPLETE, LOCALE_DIAG_ID_COLLISION as DIAG_ID_COLLISION, LOCALE_DIAG_ID_INVALID as DIAG_ID_INVALID, LOCALE_DIAG_LAYOUT_ILLEGAL as DIAG_LAYOUT_ILLEGAL, LOCALE_DIAG_LINK_HARDCODED_PATH as DIAG_LOCALE_LINK_HARDCODED_PATH, LOCALE_DIAG_MACHINE_DEFAULT_FORBIDDEN as DIAG_LOCALE_MACHINE_DEFAULT_FORBIDDEN, LOCALE_DIAG_MANIFEST_MISSING as DIAG_MANIFEST_MISSING, LOCALE_DIAG_MESSAGE_ARRAY_FORBIDDEN as DIAG_MESSAGE_ARRAY_FORBIDDEN, LOCALE_DIAG_MESSAGE_DYNAMIC_ID_UNBOUNDED as DIAG_MESSAGE_DYNAMIC_ID_UNBOUNDED, LOCALE_DIAG_MESSAGE_HTML_FORBIDDEN as DIAG_MESSAGE_HTML_FORBIDDEN, LOCALE_DIAG_MESSAGE_MISSING_DEFAULT as DIAG_MESSAGE_MISSING_DEFAULT, LOCALE_DIAG_MESSAGE_MISSING_VARIANT as DIAG_MESSAGE_MISSING_VARIANT, LOCALE_DIAG_MESSAGE_MIXED_LOCALE as DIAG_MESSAGE_MIXED_LOCALE, LOCALE_DIAG_MESSAGE_PARAMETER_MISMATCH as DIAG_MESSAGE_PARAMETER_MISMATCH, LOCALE_DIAG_MESSAGE_SYNTAX_INVALID as DIAG_MESSAGE_SYNTAX_INVALID, LOCALE_DIAG_MESSAGE_UNUSED as DIAG_MESSAGE_UNUSED, LOCALE_DIAG_META_LOCALE_MISMATCH as DIAG_LOCALE_META_LOCALE_MISMATCH, LOCALE_DIAG_MINI_CROSS_PACKAGE_UNPROVEN as DIAG_LOCALE_MINI_CROSS_PACKAGE_UNPROVEN, LOCALE_DIAG_NATIVE_PACK_APP_MISMATCH as DIAG_LOCALE_NATIVE_PACK_APP_MISMATCH, LOCALE_DIAG_NATIVE_PACK_HAS_JS as DIAG_LOCALE_NATIVE_PACK_HAS_JS, LOCALE_DIAG_NATIVE_PACK_UNSIGNED as DIAG_LOCALE_NATIVE_PACK_UNSIGNED, LOCALE_DIAG_PREFIX_OMIT_WITHOUT_REDIRECT as DIAG_LOCALE_PREFIX_OMIT_WITHOUT_REDIRECT, LOCALE_DIAG_PSEUDO_PRODUCTION_FORBIDDEN as DIAG_LOCALE_PSEUDO_PRODUCTION_FORBIDDEN, LOCALE_DIAG_ROUTE_COLLISION as DIAG_LOCALE_ROUTE_COLLISION, LOCALE_DIAG_SERVER_FORMAT_WITHOUT_CONTEXT as DIAG_LOCALE_SERVER_FORMAT_WITHOUT_CONTEXT, LOCALE_DIAG_SERVER_TRANSLATED_ERROR as DIAG_LOCALE_SERVER_TRANSLATED_ERROR, LOCALE_DIAG_STALE_GENERATION as DIAG_LOCALE_STALE_GENERATION, LOCALE_DIAG_TRANSITION_LOAD_FAILED as DIAG_LOCALE_TRANSITION_LOAD_FAILED, LOCALE_DIAG_TRANSITION_PARTIAL as DIAG_LOCALE_TRANSITION_PARTIAL, LOCALE_DIAG_TRANSITION_UNSUPPORTED as DIAG_LOCALE_TRANSITION_UNSUPPORTED, LOCALE_DIFF_SCHEMA, LOCALE_EXPLAIN_SCHEMA, LOCALE_EXTRACT_SCHEMA, LOCALE_FALLBACK_RESOLUTION_SCHEMA, LOCALE_FORMATTER_CONTEXT_SCHEMA, LOCALE_LINK_RESOLUTION_SCHEMA, LOCALE_MANIFEST_SCHEMA, LOCALE_MESSAGE_CATALOG_SCHEMA as MESSAGE_CATALOG_SCHEMA, LOCALE_MESSAGE_NODE_SCHEMA as MESSAGE_NODE_SCHEMA, LOCALE_MINI_PACKAGE_PROOF_SCHEMA, LOCALE_NATIVE_PACK_SCHEMA, LOCALE_PAGE_META_SCHEMA, LOCALE_PROTOCOL, LOCALE_PSEUDO_SCHEMA, LOCALE_RENAME_SCHEMA, LOCALE_ROUTE_REALIZATION_SCHEMA, LOCALE_ROUTER_CHECK_SCHEMA, LOCALE_RUNTIME_CHECK_SCHEMA, LOCALE_SERVER_ERROR_ENVELOPE_SCHEMA, LOCALE_TRANSITION_SCHEMA, LOCALE_TYPED_MODULE_SCHEMA, LOCALE_VIRTUAL_MODULE_PREFIX, } from '@vmz/protocol';
7
+ /** Reserved first-level names under /locales (not LocaleId dirs). */
8
+ export const LOCALE_RESERVED_TOP = new Set(['locales.json5', 'locales.json', 'package.json']);
9
+ /** Lowercase ASCII BCP 47-ish with `-` only. */
10
+ export const LOCALE_ID_RE = /^[a-z]{2,3}(-[a-z0-9]+)*$/;
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Explain one MessageId: definition, params, variants, fallback, delivery reachability.
3
+ * @param {{
4
+ * messageId: string,
5
+ * locale?: string|null,
6
+ * deliveryId?: string|null,
7
+ * checkReport: any,
8
+ * }} input
9
+ */
10
+ export declare function explainLocaleMessage(input: any): {
11
+ schema: string;
12
+ status: string;
13
+ messageId: any;
14
+ diagnostics: any[];
15
+ catalogId?: undefined;
16
+ params?: undefined;
17
+ variants?: undefined;
18
+ requestedLocale?: undefined;
19
+ resolvedLocale?: undefined;
20
+ fallbackPath?: undefined;
21
+ formatterDataVersion?: undefined;
22
+ delivery?: undefined;
23
+ } | {
24
+ schema: string;
25
+ status: string;
26
+ messageId: any;
27
+ catalogId: any;
28
+ params: any;
29
+ variants: {
30
+ [k: string]: {
31
+ template: any;
32
+ path: any;
33
+ params: any;
34
+ };
35
+ };
36
+ requestedLocale: any;
37
+ resolvedLocale: any;
38
+ fallbackPath: any[];
39
+ formatterDataVersion: string;
40
+ delivery: {
41
+ deliveryId: any;
42
+ reachable: boolean;
43
+ catalogHash: any;
44
+ bundledLocales: any[];
45
+ };
46
+ diagnostics: any[];
47
+ };
48
+ /**
49
+ * Diff two locales' catalogs.
50
+ * @param {{
51
+ * baseLocale: string,
52
+ * targetLocale: string,
53
+ * messages: Array<{ messageId: string, variants: Record<string, { template: string, params?: any[] }> }>,
54
+ * }} input
55
+ */
56
+ export declare function diffLocaleCatalogs(input: any): {
57
+ schema: string;
58
+ status: string;
59
+ baseLocale: any;
60
+ targetLocale: any;
61
+ missingInTarget: any[];
62
+ missingInBase: any[];
63
+ changed: any[];
64
+ paramMismatches: any[];
65
+ summary: {
66
+ missingInTarget: number;
67
+ missingInBase: number;
68
+ changed: number;
69
+ paramMismatches: number;
70
+ };
71
+ };
72
+ /**
73
+ * Scan source for likely hardcoded UI text sinks (extract --check).
74
+ * Does not auto-generate MessageIds.
75
+ * @param {string} projectRoot
76
+ * @param {{ check?: boolean }} [opts]
77
+ */
78
+ export declare function extractHardcodedText(projectRoot: any, opts?: {}): {
79
+ schema: string;
80
+ status: string;
81
+ findings: any[];
82
+ diagnostics: any[];
83
+ };
84
+ /**
85
+ * Pseudo-localize a source locale for layout/overflow testing.
86
+ * Preserves ICU placeholders; marks provenance — never a production fallback.
87
+ * @param {{
88
+ * sourceLocale: string,
89
+ * messages: Array<{ messageId: string, variants: Record<string, { template: string }> }>,
90
+ * production?: boolean,
91
+ * }} input
92
+ */
93
+ export declare function pseudoLocalizeCatalog(input: any): {
94
+ schema: string;
95
+ status: string;
96
+ sourceLocale: any;
97
+ pseudoLocale: string;
98
+ provenance: string;
99
+ catalog: {};
100
+ diagnostics: any[];
101
+ };
102
+ /**
103
+ * Cross-host conformance: same MessageId set + catalog hashes + formatter version.
104
+ * @param {{
105
+ * manifest: any,
106
+ * messages: any[],
107
+ * routeIds?: string[],
108
+ * }} input
109
+ */
110
+ export declare function checkLocaleConformance(input: any): {
111
+ schema: string;
112
+ status: string;
113
+ hosts: string[];
114
+ formatterDataVersion: string;
115
+ messageIds: any;
116
+ catalogHashes: {};
117
+ diagnostics: any[];
118
+ };