@vmz/vmz 0.0.3 → 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.
Files changed (52) hide show
  1. package/README.md +6 -4
  2. package/dist/build-assemble.d.ts +52 -0
  3. package/dist/build-assemble.js +191 -0
  4. package/dist/cdn-policy.d.ts +196 -0
  5. package/dist/cdn-policy.js +443 -0
  6. package/dist/cli.js +152 -11
  7. package/dist/content-addressed-assets.d.ts +69 -0
  8. package/dist/content-addressed-assets.js +206 -0
  9. package/dist/delivery-profile.d.ts +74 -0
  10. package/dist/delivery-profile.js +279 -0
  11. package/dist/dev-session.js +49 -13
  12. package/dist/document-build.js +9 -4
  13. package/dist/document-designs.js +30 -2
  14. package/dist/document-enrich.js +8 -0
  15. package/dist/embedded-packaging.d.ts +22 -0
  16. package/dist/embedded-packaging.js +113 -0
  17. package/dist/index.d.ts +19 -3
  18. package/dist/index.js +35 -15
  19. package/dist/invocation.d.ts +8 -31
  20. package/dist/invocation.js +12 -33
  21. package/dist/locale-check.d.ts +16 -0
  22. package/dist/locale-check.js +131 -3
  23. package/dist/locale-cmd.js +2 -2
  24. package/dist/locale-route-emit.d.ts +34 -0
  25. package/dist/locale-route-emit.js +134 -0
  26. package/dist/locale-router.d.ts +20 -0
  27. package/dist/locale-router.js +68 -0
  28. package/dist/log.d.ts +2 -2
  29. package/dist/log.js +11 -3
  30. package/dist/pack.d.ts +40 -0
  31. package/dist/pack.js +108 -0
  32. package/dist/plugin-host.d.ts +10 -1
  33. package/dist/plugin-host.js +19 -2
  34. package/dist/port.d.ts +10 -0
  35. package/dist/port.js +46 -0
  36. package/dist/production-observability.d.ts +286 -0
  37. package/dist/production-observability.js +469 -0
  38. package/dist/production-test-pack.d.ts +144 -0
  39. package/dist/production-test-pack.js +447 -0
  40. package/dist/release-cmd.d.ts +8 -0
  41. package/dist/release-cmd.js +126 -0
  42. package/dist/release-pack.d.ts +96 -0
  43. package/dist/release-pack.js +346 -0
  44. package/dist/server-artifact.d.ts +140 -0
  45. package/dist/server-artifact.js +205 -0
  46. package/dist/server-language-backend.d.ts +89 -0
  47. package/dist/server-language-backend.js +121 -0
  48. package/dist/site-delivery.d.ts +134 -0
  49. package/dist/site-delivery.js +345 -0
  50. package/dist/static-emit.d.ts +145 -0
  51. package/dist/static-emit.js +577 -0
  52. package/package.json +13 -13
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Server Language DSL backends (lang on `<script server>`).
3
+ * Author surface is a VMZ DSL flavor — not full target-language source.
4
+ */
5
+ /** @typedef {'ts' | 'rust' | 'python' | 'java'} ServerLangId */
6
+ export declare const SERVER_LANG_IDS: readonly string[];
7
+ /** @type {Record<string, ServerLangId>} */
8
+ export declare const SERVER_LANG_ALIASES: Readonly<{
9
+ ts: "ts";
10
+ typescript: "ts";
11
+ rust: "rust";
12
+ python: "python";
13
+ java: "java";
14
+ }>;
15
+ /**
16
+ * @typedef {{
17
+ * langId: ServerLangId,
18
+ * aliases: string[],
19
+ * compatibleRuntimes: string[],
20
+ * implemented: boolean,
21
+ * artifactRoot: string,
22
+ * }} ServerLanguageBackendMeta
23
+ */
24
+ /** @type {Record<ServerLangId, ServerLanguageBackendMeta>} */
25
+ export declare const SERVER_LANGUAGE_BACKENDS: Readonly<{
26
+ ts: {
27
+ langId: string;
28
+ aliases: string[];
29
+ compatibleRuntimes: string[];
30
+ implemented: boolean;
31
+ artifactRoot: string;
32
+ };
33
+ rust: {
34
+ langId: string;
35
+ aliases: string[];
36
+ compatibleRuntimes: string[];
37
+ implemented: boolean;
38
+ artifactRoot: string;
39
+ };
40
+ python: {
41
+ langId: string;
42
+ aliases: string[];
43
+ compatibleRuntimes: string[];
44
+ implemented: boolean;
45
+ artifactRoot: string;
46
+ };
47
+ java: {
48
+ langId: string;
49
+ aliases: string[];
50
+ compatibleRuntimes: string[];
51
+ implemented: boolean;
52
+ artifactRoot: string;
53
+ };
54
+ }>;
55
+ /**
56
+ * Resolve author `lang` attr (or null/undefined for default TS).
57
+ * @param {string | null | undefined} raw
58
+ * @returns {{
59
+ * ok: true, langId: ServerLangId, backend: ServerLanguageBackendMeta
60
+ * } | {
61
+ * ok: false, code: string, message: string
62
+ * }}
63
+ */
64
+ export declare function resolveServerLanguage(raw: any): {
65
+ ok: boolean;
66
+ code: string;
67
+ message: string;
68
+ langId?: undefined;
69
+ backend?: undefined;
70
+ } | {
71
+ ok: boolean;
72
+ langId: any;
73
+ backend: any;
74
+ code?: undefined;
75
+ message?: undefined;
76
+ };
77
+ /**
78
+ * @param {ServerLangId} langId
79
+ * @param {string | null | undefined} serverRuntime
80
+ */
81
+ export declare function assertLangRuntimePair(langId: any, serverRuntime: any): {
82
+ ok: boolean;
83
+ code: string;
84
+ message: string;
85
+ } | {
86
+ ok: boolean;
87
+ code?: undefined;
88
+ message?: undefined;
89
+ };
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Server Language DSL backends (lang on `<script server>`).
3
+ * Author surface is a VMZ DSL flavor — not full target-language source.
4
+ */
5
+ // @ts-nocheck
6
+ /** @typedef {'ts' | 'rust' | 'python' | 'java'} ServerLangId */
7
+ export const SERVER_LANG_IDS = Object.freeze(['ts', 'rust', 'python', 'java']);
8
+ /** @type {Record<string, ServerLangId>} */
9
+ export const SERVER_LANG_ALIASES = Object.freeze({
10
+ ts: 'ts',
11
+ typescript: 'ts',
12
+ rust: 'rust',
13
+ python: 'python',
14
+ java: 'java',
15
+ });
16
+ /**
17
+ * @typedef {{
18
+ * langId: ServerLangId,
19
+ * aliases: string[],
20
+ * compatibleRuntimes: string[],
21
+ * implemented: boolean,
22
+ * artifactRoot: string,
23
+ * }} ServerLanguageBackendMeta
24
+ */
25
+ /** @type {Record<ServerLangId, ServerLanguageBackendMeta>} */
26
+ export const SERVER_LANGUAGE_BACKENDS = Object.freeze({
27
+ ts: {
28
+ langId: 'ts',
29
+ aliases: ['ts', 'typescript'],
30
+ compatibleRuntimes: ['node', 'worker', 'deno', 'bun'],
31
+ implemented: true,
32
+ artifactRoot: 'dist/#server',
33
+ },
34
+ rust: {
35
+ langId: 'rust',
36
+ aliases: ['rust'],
37
+ compatibleRuntimes: ['rust-host'],
38
+ implemented: true,
39
+ artifactRoot: 'target/vmz/server-rust',
40
+ },
41
+ python: {
42
+ langId: 'python',
43
+ aliases: ['python'],
44
+ compatibleRuntimes: ['python-host'],
45
+ implemented: false,
46
+ artifactRoot: 'target/vmz/server-python',
47
+ },
48
+ java: {
49
+ langId: 'java',
50
+ aliases: ['java'],
51
+ compatibleRuntimes: ['jvm-host'],
52
+ implemented: false,
53
+ artifactRoot: 'target/vmz/server-java',
54
+ },
55
+ });
56
+ /**
57
+ * Resolve author `lang` attr (or null/undefined for default TS).
58
+ * @param {string | null | undefined} raw
59
+ * @returns {{
60
+ * ok: true, langId: ServerLangId, backend: ServerLanguageBackendMeta
61
+ * } | {
62
+ * ok: false, code: string, message: string
63
+ * }}
64
+ */
65
+ export function resolveServerLanguage(raw) {
66
+ const trimmed = raw == null ? '' : String(raw).trim();
67
+ if (!trimmed) {
68
+ return { ok: true, langId: 'ts', backend: SERVER_LANGUAGE_BACKENDS.ts };
69
+ }
70
+ const langId = SERVER_LANG_ALIASES[trimmed];
71
+ if (!langId) {
72
+ return {
73
+ ok: false,
74
+ code: 'vmz::server::unknown_language',
75
+ message: `unknown script language \`${trimmed}\`; use ts|typescript|rust|python|java`,
76
+ };
77
+ }
78
+ const backend = SERVER_LANGUAGE_BACKENDS[langId];
79
+ if (!backend.implemented) {
80
+ return {
81
+ ok: false,
82
+ code: 'vmz::server::language_backend_unimplemented',
83
+ message: `\`<script server lang="${langId}">\` is registered but not implemented yet`,
84
+ };
85
+ }
86
+ return { ok: true, langId, backend };
87
+ }
88
+ /**
89
+ * @param {ServerLangId} langId
90
+ * @param {string | null | undefined} serverRuntime
91
+ */
92
+ export function assertLangRuntimePair(langId, serverRuntime) {
93
+ const backend = SERVER_LANGUAGE_BACKENDS[langId];
94
+ if (!backend) {
95
+ return {
96
+ ok: false,
97
+ code: 'vmz::server::unknown_language',
98
+ message: `unknown langId ${langId}`,
99
+ };
100
+ }
101
+ if (langId === 'ts') {
102
+ // TS stays compatible with existing node/worker defaults; rust-host rejects ts.
103
+ if (serverRuntime === 'rust-host' || serverRuntime === 'python-host' || serverRuntime === 'jvm-host') {
104
+ return {
105
+ ok: false,
106
+ code: 'vmz::server::lang_runtime_mismatch',
107
+ message: `lang=ts is incompatible with serverRuntime=${serverRuntime}`,
108
+ };
109
+ }
110
+ return { ok: true };
111
+ }
112
+ const rt = String(serverRuntime || '').trim();
113
+ if (!backend.compatibleRuntimes.includes(rt)) {
114
+ return {
115
+ ok: false,
116
+ code: 'vmz::server::lang_runtime_mismatch',
117
+ message: `lang=${langId} requires serverRuntime in [${backend.compatibleRuntimes.join('|')}] (got ${rt || '(none)'})`,
118
+ };
119
+ }
120
+ return { ok: true };
121
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * A3-site: SiteDeliveryContract — embedded | filesystem | remote selection + release fallback.
3
+ * Authoring via defineConfig({ delivery }) / defineSite(...); pure data only.
4
+ */
5
+ export declare const SITE_DELIVERY_CONTRACT_SCHEMA = "vmz.site.delivery_contract.v0";
6
+ export declare const SITE_DELIVERY_RESOLUTION_SCHEMA = "vmz.site.delivery_resolution.v0";
7
+ /**
8
+ * Pure-data helper for `defineConfig({ delivery: defineSite(...) })`.
9
+ * Not a second config entry — CLI never auto-discovers `vmz.site.ts`.
10
+ * @param {Record<string, unknown>} delivery
11
+ */
12
+ export declare function defineSite(delivery: any): any;
13
+ /**
14
+ * Normalize authoring delivery → frozen SiteDeliveryContract.
15
+ * @param {unknown} raw
16
+ * @param {{ siteId?: string, projectRoot?: string }} [opts]
17
+ * @returns {{ ok: true, contract: Record<string, any> } | { ok: false, diagnostics: Array<{ code: string, message: string }> }}
18
+ */
19
+ export declare function normalizeSiteDelivery(raw: any, opts?: {}): {
20
+ ok: boolean;
21
+ diagnostics: any[];
22
+ contract?: undefined;
23
+ } | {
24
+ ok: boolean;
25
+ contract: {
26
+ schema: string;
27
+ schemaVersion: string;
28
+ siteId: any;
29
+ artifact: string;
30
+ expectedCompatibility: any;
31
+ sources: any[];
32
+ resolutionPolicy: {
33
+ mode: string;
34
+ fallback: any;
35
+ fileLevelMix: boolean;
36
+ };
37
+ failurePolicy: any;
38
+ updatePolicy: any;
39
+ rollbackPolicy: any;
40
+ securityPolicy: any;
41
+ activation: string;
42
+ };
43
+ diagnostics?: undefined;
44
+ };
45
+ /**
46
+ * Probe one physical source for release-level readiness (not per-URL).
47
+ * @param {{
48
+ * available?: boolean,
49
+ * artifactDigest?: string | null,
50
+ * integrityOk?: boolean,
51
+ * signatureOk?: boolean,
52
+ * objectClosureOk?: boolean,
53
+ * mixedDigestObjects?: boolean,
54
+ * error?: string | null,
55
+ * }} [probe]
56
+ */
57
+ export declare function normalizeSourceProbe(probe?: {}): {
58
+ available: boolean;
59
+ artifactDigest: any;
60
+ integrityOk: boolean;
61
+ signatureOk: boolean;
62
+ objectClosureOk: boolean;
63
+ mixedDigestObjects: boolean;
64
+ error: any;
65
+ };
66
+ /**
67
+ * Resolve one complete release per SiteDeliveryContract (no file-level mix).
68
+ * @param {Record<string, any>} contract
69
+ * @param {Record<string, ReturnType<typeof normalizeSourceProbe>>} probes by sourceId
70
+ */
71
+ export declare function resolveSiteRelease(contract: any, probes?: {}): {
72
+ schema: string;
73
+ status: string;
74
+ selectedSourceId: any;
75
+ selectedKind: any;
76
+ selectedDigest: any;
77
+ fallbackReason: string;
78
+ attempted: any[];
79
+ fileLevelMix: boolean;
80
+ activation: string;
81
+ contractDigest: any;
82
+ } | {
83
+ schema: string;
84
+ status: string;
85
+ selectedSourceId: any;
86
+ selectedKind: any;
87
+ selectedDigest: any;
88
+ fallbackReason: string;
89
+ attempted: any[];
90
+ fileLevelMix: boolean;
91
+ activation: string;
92
+ contractDigest: any;
93
+ resolutionDigest: any;
94
+ };
95
+ /**
96
+ * Read a packed release directory probe (expects _vmz/release-envelope.json + object closure).
97
+ * @param {string} releaseDir absolute path to a release snapshot (contains dist/ or is dist/)
98
+ */
99
+ export declare function probeReleaseDirectory(releaseDir: any): {
100
+ available: boolean;
101
+ artifactDigest: any;
102
+ integrityOk: boolean;
103
+ signatureOk: boolean;
104
+ objectClosureOk: boolean;
105
+ mixedDigestObjects: boolean;
106
+ error: any;
107
+ };
108
+ /**
109
+ * Emit normalized contract (+ optional resolution) under dist/_vmz.
110
+ * @param {string} outDir
111
+ * @param {unknown} deliveryRaw
112
+ * @param {{ siteId?: string, probes?: Record<string, any> }} [opts]
113
+ */
114
+ export declare function emitSiteDelivery(outDir: any, deliveryRaw: any, opts?: {}): {
115
+ contract: {
116
+ schema: string;
117
+ schemaVersion: string;
118
+ siteId: any;
119
+ artifact: string;
120
+ expectedCompatibility: any;
121
+ sources: any[];
122
+ resolutionPolicy: {
123
+ mode: string;
124
+ fallback: any;
125
+ fileLevelMix: boolean;
126
+ };
127
+ failurePolicy: any;
128
+ updatePolicy: any;
129
+ rollbackPolicy: any;
130
+ securityPolicy: any;
131
+ activation: string;
132
+ };
133
+ resolution: any;
134
+ };
@@ -0,0 +1,345 @@
1
+ /**
2
+ * A3-site: SiteDeliveryContract — embedded | filesystem | remote selection + release fallback.
3
+ * Authoring via defineConfig({ delivery }) / defineSite(...); pure data only.
4
+ */
5
+ // @ts-nocheck
6
+ import crypto from 'node:crypto';
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ export const SITE_DELIVERY_CONTRACT_SCHEMA = 'vmz.site.delivery_contract.v0';
10
+ export const SITE_DELIVERY_RESOLUTION_SCHEMA = 'vmz.site.delivery_resolution.v0';
11
+ /**
12
+ * Pure-data helper for `defineConfig({ delivery: defineSite(...) })`.
13
+ * Not a second config entry — CLI never auto-discovers `vmz.site.ts`.
14
+ * @param {Record<string, unknown>} delivery
15
+ */
16
+ export function defineSite(delivery) {
17
+ return delivery;
18
+ }
19
+ /**
20
+ * Normalize authoring delivery → frozen SiteDeliveryContract.
21
+ * @param {unknown} raw
22
+ * @param {{ siteId?: string, projectRoot?: string }} [opts]
23
+ * @returns {{ ok: true, contract: Record<string, any> } | { ok: false, diagnostics: Array<{ code: string, message: string }> }}
24
+ */
25
+ export function normalizeSiteDelivery(raw, opts = {}) {
26
+ /** @type {Array<{ code: string, message: string }>} */
27
+ const diagnostics = [];
28
+ if (raw == null || typeof raw !== 'object' || Array.isArray(raw)) {
29
+ return {
30
+ ok: false,
31
+ diagnostics: [{ code: 'site.delivery.invalid', message: 'delivery must be a plain object' }],
32
+ };
33
+ }
34
+ const d = /** @type {Record<string, any>} */ (raw);
35
+ if (typeof d.artifact !== 'string' || !d.artifact.trim()) {
36
+ diagnostics.push({ code: 'site.delivery.artifact', message: 'delivery.artifact is required' });
37
+ }
38
+ if (!Array.isArray(d.sources) || d.sources.length < 1) {
39
+ diagnostics.push({ code: 'site.delivery.sources', message: 'delivery.sources must be a non-empty array' });
40
+ }
41
+ const sources = [];
42
+ const seen = new Set();
43
+ for (const [i, s] of (d.sources || []).entries()) {
44
+ if (!s || typeof s !== 'object') {
45
+ diagnostics.push({ code: 'site.delivery.source', message: `sources[${i}] must be an object` });
46
+ continue;
47
+ }
48
+ const id = String(s.id || '').trim();
49
+ const kind = String(s.kind || '').trim();
50
+ if (!id) {
51
+ diagnostics.push({ code: 'site.delivery.sourceId', message: `sources[${i}].id is required` });
52
+ continue;
53
+ }
54
+ if (seen.has(id)) {
55
+ diagnostics.push({ code: 'site.delivery.sourceId.dup', message: `duplicate source id ${id}` });
56
+ continue;
57
+ }
58
+ seen.add(id);
59
+ if (!['embedded', 'filesystem', 'remote'].includes(kind)) {
60
+ diagnostics.push({
61
+ code: 'site.delivery.kind',
62
+ message: `sources[${i}].kind must be embedded|filesystem|remote`,
63
+ });
64
+ continue;
65
+ }
66
+ if (typeof s.directory === 'function' || typeof s.baseUrl === 'function') {
67
+ diagnostics.push({
68
+ code: 'site.delivery.executable',
69
+ message: `sources[${i}] must be pure data (no functions)`,
70
+ });
71
+ continue;
72
+ }
73
+ if (kind === 'filesystem' && typeof s.directory !== 'string') {
74
+ diagnostics.push({
75
+ code: 'site.delivery.filesystem',
76
+ message: `sources[${i}] filesystem requires directory string`,
77
+ });
78
+ }
79
+ if (kind === 'remote' && typeof s.baseUrl !== 'string') {
80
+ diagnostics.push({
81
+ code: 'site.delivery.remote',
82
+ message: `sources[${i}] remote requires baseUrl string`,
83
+ });
84
+ }
85
+ if (kind === 'embedded' && s.artifact == null && d.artifact == null) {
86
+ diagnostics.push({
87
+ code: 'site.delivery.embedded',
88
+ message: `sources[${i}] embedded requires artifact id`,
89
+ });
90
+ }
91
+ sources.push({
92
+ sourceId: id,
93
+ kind,
94
+ priority: typeof s.priority === 'number' ? s.priority : i,
95
+ trust: s.trust || 'signed-release',
96
+ directory: kind === 'filesystem' ? String(s.directory) : null,
97
+ baseUrl: kind === 'remote' ? String(s.baseUrl).replace(/\/$/, '') : null,
98
+ timeoutMs: kind === 'remote' ? Number(s.timeoutMs || 1500) : null,
99
+ artifact: s.artifact != null ? String(s.artifact) : kind === 'embedded' ? String(d.artifact) : null,
100
+ integrity: s.integrity || null,
101
+ signaturePolicy: s.signaturePolicy || s.trust || 'signed-release',
102
+ });
103
+ }
104
+ const resolution = d.resolution && typeof d.resolution === 'object' ? d.resolution : {};
105
+ const mode = String(resolution.mode || 'release');
106
+ if (mode !== 'release') {
107
+ diagnostics.push({
108
+ code: 'site.delivery.resolution.mode',
109
+ message: `resolution.mode must be 'release' (got ${mode})`,
110
+ });
111
+ }
112
+ let fallback = Array.isArray(resolution.fallback) ? resolution.fallback.map(String) : sources.map((s) => s.sourceId);
113
+ for (const id of fallback) {
114
+ if (!seen.has(id)) {
115
+ diagnostics.push({
116
+ code: 'site.delivery.fallback.unknown',
117
+ message: `resolution.fallback references unknown source ${id}`,
118
+ });
119
+ }
120
+ }
121
+ // Preserve author order — do not reorder by discovery/speed.
122
+ const activation = String(d.activation || 'atomic');
123
+ if (activation !== 'atomic') {
124
+ diagnostics.push({
125
+ code: 'site.delivery.activation',
126
+ message: `activation must be 'atomic' in v0`,
127
+ });
128
+ }
129
+ if (diagnostics.length)
130
+ return { ok: false, diagnostics };
131
+ const contractBody = {
132
+ schema: SITE_DELIVERY_CONTRACT_SCHEMA,
133
+ schemaVersion: '0',
134
+ siteId: opts.siteId || d.siteId || d.artifact,
135
+ artifact: String(d.artifact),
136
+ expectedCompatibility: d.expectedCompatibility || {
137
+ runtime: 'vmz',
138
+ deliveryProfiles: ['web-static', 'filesystem'],
139
+ },
140
+ sources,
141
+ resolutionPolicy: {
142
+ mode: 'release',
143
+ fallback,
144
+ fileLevelMix: false,
145
+ },
146
+ failurePolicy: d.failure || d.failurePolicy || { onAllSourcesFailed: 'embedded-or-fail' },
147
+ updatePolicy: d.update || d.updatePolicy || { backgroundRemote: true },
148
+ rollbackPolicy: d.rollback || d.rollbackPolicy || { retainPrevious: true },
149
+ securityPolicy: d.security || d.securityPolicy || { requireSignature: true, forbidSecretsInContract: true },
150
+ activation: 'atomic',
151
+ };
152
+ contractBody.contractDigest = sha256Hex(canonicalJson(contractBody));
153
+ return { ok: true, contract: contractBody };
154
+ }
155
+ /**
156
+ * Probe one physical source for release-level readiness (not per-URL).
157
+ * @param {{
158
+ * available?: boolean,
159
+ * artifactDigest?: string | null,
160
+ * integrityOk?: boolean,
161
+ * signatureOk?: boolean,
162
+ * objectClosureOk?: boolean,
163
+ * mixedDigestObjects?: boolean,
164
+ * error?: string | null,
165
+ * }} [probe]
166
+ */
167
+ export function normalizeSourceProbe(probe = {}) {
168
+ return {
169
+ available: probe.available !== false,
170
+ artifactDigest: probe.artifactDigest ?? null,
171
+ integrityOk: probe.integrityOk !== false,
172
+ signatureOk: probe.signatureOk !== false,
173
+ objectClosureOk: probe.objectClosureOk !== false,
174
+ mixedDigestObjects: Boolean(probe.mixedDigestObjects),
175
+ error: probe.error || null,
176
+ };
177
+ }
178
+ /**
179
+ * Resolve one complete release per SiteDeliveryContract (no file-level mix).
180
+ * @param {Record<string, any>} contract
181
+ * @param {Record<string, ReturnType<typeof normalizeSourceProbe>>} probes by sourceId
182
+ */
183
+ export function resolveSiteRelease(contract, probes = {}) {
184
+ const attempted = [];
185
+ const fallback = contract.resolutionPolicy?.fallback || contract.sources.map((s) => s.sourceId);
186
+ for (const sourceId of fallback) {
187
+ const source = (contract.sources || []).find((s) => s.sourceId === sourceId);
188
+ if (!source) {
189
+ attempted.push({ sourceId, result: 'skip', reason: 'unknown-source' });
190
+ continue;
191
+ }
192
+ const probe = normalizeSourceProbe(probes[sourceId]);
193
+ if (!probe.available) {
194
+ attempted.push({ sourceId, result: 'reject', reason: probe.error || 'unavailable' });
195
+ continue;
196
+ }
197
+ if (probe.mixedDigestObjects) {
198
+ attempted.push({
199
+ sourceId,
200
+ result: 'reject',
201
+ reason: 'file-level-mix-forbidden',
202
+ });
203
+ continue;
204
+ }
205
+ if (!probe.objectClosureOk) {
206
+ attempted.push({ sourceId, result: 'reject', reason: 'object-closure-incomplete' });
207
+ continue;
208
+ }
209
+ if (contract.securityPolicy?.requireSignature !== false && !probe.signatureOk) {
210
+ attempted.push({ sourceId, result: 'reject', reason: 'signature-failed' });
211
+ continue;
212
+ }
213
+ if (!probe.integrityOk) {
214
+ attempted.push({ sourceId, result: 'reject', reason: 'integrity-failed' });
215
+ continue;
216
+ }
217
+ if (!probe.artifactDigest) {
218
+ attempted.push({ sourceId, result: 'reject', reason: 'missing-artifact-digest' });
219
+ continue;
220
+ }
221
+ attempted.push({ sourceId, result: 'accept', reason: null });
222
+ const resolution = {
223
+ schema: SITE_DELIVERY_RESOLUTION_SCHEMA,
224
+ status: 'activated',
225
+ selectedSourceId: sourceId,
226
+ selectedKind: source.kind,
227
+ selectedDigest: probe.artifactDigest,
228
+ fallbackReason: attempted.some((a) => a.result === 'reject')
229
+ ? attempted
230
+ .filter((a) => a.result === 'reject')
231
+ .map((a) => `${a.sourceId}:${a.reason}`)
232
+ .join(';')
233
+ : null,
234
+ attempted,
235
+ fileLevelMix: false,
236
+ activation: 'atomic',
237
+ contractDigest: contract.contractDigest,
238
+ };
239
+ resolution.resolutionDigest = sha256Hex(canonicalJson(resolution));
240
+ return resolution;
241
+ }
242
+ return {
243
+ schema: SITE_DELIVERY_RESOLUTION_SCHEMA,
244
+ status: 'failed',
245
+ selectedSourceId: null,
246
+ selectedKind: null,
247
+ selectedDigest: null,
248
+ fallbackReason: 'all-sources-rejected',
249
+ attempted,
250
+ fileLevelMix: false,
251
+ activation: 'atomic',
252
+ contractDigest: contract.contractDigest,
253
+ resolutionDigest: null,
254
+ };
255
+ }
256
+ /**
257
+ * Read a packed release directory probe (expects _vmz/release-envelope.json + object closure).
258
+ * @param {string} releaseDir absolute path to a release snapshot (contains dist/ or is dist/)
259
+ */
260
+ export function probeReleaseDirectory(releaseDir) {
261
+ const root = path.resolve(releaseDir);
262
+ const dist = fs.existsSync(path.join(root, 'dist')) ? path.join(root, 'dist') : root;
263
+ const envelopePath = path.join(dist, '_vmz', 'release-envelope.json');
264
+ if (!fs.existsSync(envelopePath)) {
265
+ return normalizeSourceProbe({
266
+ available: false,
267
+ error: 'missing-release-envelope',
268
+ objectClosureOk: false,
269
+ signatureOk: false,
270
+ integrityOk: false,
271
+ });
272
+ }
273
+ let envelope;
274
+ try {
275
+ envelope = JSON.parse(fs.readFileSync(envelopePath, 'utf8'));
276
+ }
277
+ catch {
278
+ return normalizeSourceProbe({
279
+ available: false,
280
+ error: 'invalid-release-envelope',
281
+ objectClosureOk: false,
282
+ signatureOk: false,
283
+ integrityOk: false,
284
+ });
285
+ }
286
+ const digest = envelope.artifactDigest || null;
287
+ const required = ['index.html', '_vmz/release-envelope.json'];
288
+ let closureOk = true;
289
+ for (const rel of required) {
290
+ if (!fs.existsSync(path.join(dist, rel))) {
291
+ closureOk = false;
292
+ break;
293
+ }
294
+ }
295
+ // Detect naive file-level mix: CURRENT HTML digest stamp mismatch with envelope (test hook).
296
+ const mixMarker = path.join(dist, '_vmz', 'MIXED_DIGEST');
297
+ const mixed = fs.existsSync(mixMarker);
298
+ return normalizeSourceProbe({
299
+ available: true,
300
+ artifactDigest: digest,
301
+ integrityOk: Boolean(digest) && !mixed,
302
+ signatureOk: envelope.signatureOk !== false,
303
+ objectClosureOk: closureOk && !mixed,
304
+ mixedDigestObjects: mixed,
305
+ error: mixed ? 'mixed-digest' : null,
306
+ });
307
+ }
308
+ /**
309
+ * Emit normalized contract (+ optional resolution) under dist/_vmz.
310
+ * @param {string} outDir
311
+ * @param {unknown} deliveryRaw
312
+ * @param {{ siteId?: string, probes?: Record<string, any> }} [opts]
313
+ */
314
+ export function emitSiteDelivery(outDir, deliveryRaw, opts = {}) {
315
+ const norm = normalizeSiteDelivery(deliveryRaw, { siteId: opts.siteId });
316
+ if (!norm.ok) {
317
+ throw new Error(`emitSiteDelivery: ${norm.diagnostics.map((d) => d.message).join('; ')}`);
318
+ }
319
+ const vmzDir = path.join(outDir, '_vmz');
320
+ fs.mkdirSync(vmzDir, { recursive: true });
321
+ fs.writeFileSync(path.join(vmzDir, 'site-delivery-contract.json'), `${JSON.stringify(norm.contract, null, 2)}\n`, 'utf8');
322
+ let resolution = null;
323
+ if (opts.probes) {
324
+ resolution = resolveSiteRelease(norm.contract, opts.probes);
325
+ fs.writeFileSync(path.join(vmzDir, 'site-delivery-resolution.json'), `${JSON.stringify(resolution, null, 2)}\n`, 'utf8');
326
+ }
327
+ return { contract: norm.contract, resolution };
328
+ }
329
+ function sha256Hex(data) {
330
+ return crypto.createHash('sha256').update(data).digest('hex');
331
+ }
332
+ function canonicalJson(value) {
333
+ return JSON.stringify(sortKeys(value));
334
+ }
335
+ function sortKeys(value) {
336
+ if (Array.isArray(value))
337
+ return value.map(sortKeys);
338
+ if (value && typeof value === 'object') {
339
+ const out = {};
340
+ for (const k of Object.keys(value).sort())
341
+ out[k] = sortKeys(value[k]);
342
+ return out;
343
+ }
344
+ return value;
345
+ }