@world-engines/scenario-review-snapshot 0.0.0 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import type { JsonValue } from "./types.js";
2
- import { type ArtifactV5, type FrozenPublishInput, type FrozenArtifactBundle, type LegacyArtifactV5Package2Read, type PublishedArtifactHead, type PublishedArtifactHeadCore } from "./artifact.js";
2
+ import { type ArtifactV5, type FrozenPublishInput, type FrozenArtifactBundle, type FrozenRuntimeIdentity, type LegacyArtifactV5Package2Read, type PublishedArtifactHead, type PublishedArtifactHeadCore } from "./artifact.js";
3
3
  export declare function canonicalBytes(value: JsonValue): Uint8Array;
4
4
  export declare function canonicalSha256(value: JsonValue): string;
5
5
  export declare function buildArtifactV5(input: FrozenPublishInput): ArtifactV5;
@@ -13,3 +13,4 @@ export declare function buildPublishedArtifactHead(core: PublishedArtifactHeadCo
13
13
  export declare function canonicalPublishedArtifactHeadCoreBytes(core: PublishedArtifactHeadCore): Uint8Array;
14
14
  export declare function digestPublishedArtifactHeadCore(core: PublishedArtifactHeadCore): string;
15
15
  export declare function parsePublishedArtifactHead(value: unknown): PublishedArtifactHead;
16
+ export declare function parseFrozenRuntimeIdentity(value: unknown): FrozenRuntimeIdentity;
@@ -1,7 +1,8 @@
1
1
  import { canonicalJson, compareUtf8, sha256Hex, toJsonValue } from "./canonical.js";
2
+ import { parsePublishedDeviceViewReceipt, assertDeviceViewsMatchArtifact } from "./device-view-receipt.js";
2
3
  import { parseViewInteractionManifestV3, } from "@world-engines/tmw/view-interaction-manifest-v3";
3
4
  import { parseMusicCandidateSourceV3, } from "@world-engines/tmw/music-candidate-set";
4
- import { ARTIFACT_COMPILER_WITNESS_OBJECTS, ARTIFACT_COMPILER_WITNESS_PATHS, ARTIFACT_COMPILER_WITNESS_RESERVED_PREFIX, ARTIFACT_MANIFEST_VERSION_V5, ARTIFACT_PACKAGE_FORMAT_VERSION_V5, CAPTURED_REVISION_SCHEMA_VERSION, FROZEN_PUBLISH_INPUT_SCHEMA_VERSION, FROZEN_ARTIFACT_OBJECT_COUNT, FROZEN_DATABASE_DELIVERY_CODEC, FROZEN_DATABASE_DELIVERY_MEDIA_TYPE, FROZEN_DATABASE_DELIVERY_MAGIC, FROZEN_DATABASE_DELIVERY_RELATIVE_PATH, LEGACY_ARTIFACT_PACKAGE_FORMAT_VERSION_V5, PUBLISHED_ARTIFACT_HEAD_SCHEMA_VERSION, } from "./artifact.js";
5
+ import { ARTIFACT_COMPILER_WITNESS_OBJECTS, ARTIFACT_COMPILER_WITNESS_PATHS, ARTIFACT_COMPILER_WITNESS_RESERVED_PREFIX, ARTIFACT_MANIFEST_VERSION_V5, ARTIFACT_PACKAGE_FORMAT_VERSION_V5, CAPTURED_REVISION_SCHEMA_VERSION, FROZEN_PUBLISH_INPUT_SCHEMA_VERSION, FROZEN_ARTIFACT_OBJECT_COUNT, FROZEN_DATABASE_DELIVERY_CODEC, FROZEN_DATABASE_DELIVERY_MEDIA_TYPE, FROZEN_DATABASE_DELIVERY_MAGIC, FROZEN_DATABASE_DELIVERY_RELATIVE_PATH, EARLIER_LEGACY_ARTIFACT_PACKAGE_FORMAT_VERSION_V5, LEGACY_ARTIFACT_PACKAGE_FORMAT_VERSION_V5, PUBLISHED_ARTIFACT_HEAD_SCHEMA_VERSION, } from "./artifact.js";
5
6
  const encoder = new TextEncoder();
6
7
  const strictDecoder = new TextDecoder("utf-8", { fatal: true });
7
8
  const SHA256_PATTERN = /^[0-9a-f]{64}$/u;
@@ -60,11 +61,13 @@ export function buildArtifactV5(input) {
60
61
  retrieval,
61
62
  resources: { manifest: resourceManifest, entries: resourceEntries },
62
63
  runtime: { identity: normalized.runtime_identity, canonical: runtimeCanonical },
64
+ published_view: normalized.published_view ?? null,
65
+ ...(normalized.published_device_views ? { published_device_views: normalized.published_device_views } : {}),
63
66
  archive,
64
67
  });
65
68
  const core = {
66
69
  manifest_version: ARTIFACT_MANIFEST_VERSION_V5,
67
- package_format_version: ARTIFACT_PACKAGE_FORMAT_VERSION_V5,
70
+ package_format_version: normalized.published_device_views ? 5 : ARTIFACT_PACKAGE_FORMAT_VERSION_V5,
68
71
  scenario_id: normalized.captured_revision.scenario_id,
69
72
  epoch: normalized.captured_revision.epoch,
70
73
  runtime_contract_hash: normalized.runtime_contract_hash,
@@ -83,8 +86,12 @@ export function buildArtifactV5(input) {
83
86
  retrieval,
84
87
  resources: { manifest: resourceManifest, entries: resourceEntries },
85
88
  runtime: { identity: normalized.runtime_identity, canonical: runtimeCanonical },
89
+ published_view: normalized.published_view ?? null,
90
+ ...(normalized.published_device_views ? { published_device_views: normalized.published_device_views } : {}),
86
91
  archive,
87
92
  };
93
+ if (core.published_device_views)
94
+ assertDeviceViewsMatchArtifact(core.published_device_views, core);
88
95
  return deepFreeze({ ...core, canonical_digest: digestArtifactV5Core(core) });
89
96
  }
90
97
  export function buildFrozenArtifactBundle(input) {
@@ -119,6 +126,10 @@ export function parseFrozenArtifactBundle(value) {
119
126
  return Object.freeze({ artifact, objects: Object.freeze(objects) });
120
127
  }
121
128
  function canonicalArtifactV5CoreBytes(core) {
129
+ if (core.package_format_version === LEGACY_ARTIFACT_PACKAGE_FORMAT_VERSION_V5) {
130
+ const { published_view: _publishedView, ...legacyCore } = core;
131
+ return canonicalBytes(toJsonValue(legacyCore));
132
+ }
122
133
  return canonicalBytes(toJsonValue(core));
123
134
  }
124
135
  export function canonicalArtifactV5Bytes(artifact) {
@@ -137,10 +148,12 @@ export function parseArtifactV5(value) {
137
148
  if (manifestVersion !== ARTIFACT_MANIFEST_VERSION_V5) {
138
149
  throw new Error("artifact_v5_required");
139
150
  }
140
- if (root.package_format_version === LEGACY_ARTIFACT_PACKAGE_FORMAT_VERSION_V5) {
141
- throw new Error("artifact_v5_legacy_package_reader_required");
151
+ const packageFormat = root.package_format_version;
152
+ if (packageFormat !== LEGACY_ARTIFACT_PACKAGE_FORMAT_VERSION_V5
153
+ && packageFormat !== ARTIFACT_PACKAGE_FORMAT_VERSION_V5 && packageFormat !== 5) {
154
+ throw new Error("artifact_v5_package_format_version_invalid");
142
155
  }
143
- exactKeys(root, [
156
+ const commonKeys = [
144
157
  "manifest_version", "package_format_version", "scenario_id", "captured_revision",
145
158
  "frozen_input_sha256", "metadata", "protagonist", "system_prompts", "database",
146
159
  "database_delivery",
@@ -148,7 +161,10 @@ export function parseArtifactV5(value) {
148
161
  "retrieval", "resources", "runtime", "archive",
149
162
  "epoch", "runtime_contract_hash",
150
163
  "canonical_digest",
151
- ], "artifact_v5");
164
+ ];
165
+ exactKeys(root, packageFormat === 5 ? [...commonKeys, "published_view", "published_device_views"] : packageFormat === ARTIFACT_PACKAGE_FORMAT_VERSION_V5
166
+ ? [...commonKeys, "published_view"]
167
+ : commonKeys, "artifact_v5");
152
168
  const capturedRevision = parseCapturedRevision(root.captured_revision);
153
169
  const resources = plainObject(root.resources, "artifact_v5_resources_object_required");
154
170
  exactKeys(resources, ["manifest", "entries"], "artifact_v5.resources");
@@ -164,7 +180,7 @@ export function parseArtifactV5(value) {
164
180
  exactKeys(runtime, ["identity", "canonical"], "artifact_v5.runtime");
165
181
  const artifact = {
166
182
  manifest_version: ARTIFACT_MANIFEST_VERSION_V5,
167
- package_format_version: requireExactInteger(root.package_format_version, ARTIFACT_PACKAGE_FORMAT_VERSION_V5, "artifact_v5_package_format_version_invalid"),
183
+ package_format_version: packageFormat,
168
184
  scenario_id: requireIdentifier(root.scenario_id, "artifact_v5_scenario_id_invalid"),
169
185
  epoch: requirePositiveInteger(root.epoch, "artifact_v5_epoch_invalid"),
170
186
  runtime_contract_hash: requireSha256(root.runtime_contract_hash, "artifact_v5_runtime_contract_hash_invalid"),
@@ -196,6 +212,10 @@ export function parseArtifactV5(value) {
196
212
  identity: parseRuntimeIdentity(runtime.identity),
197
213
  canonical: parseJsonIdentity(runtime.canonical),
198
214
  },
215
+ published_view: packageFormat >= ARTIFACT_PACKAGE_FORMAT_VERSION_V5
216
+ ? parsePublishedViewBuildReceipt(root.published_view)
217
+ : null,
218
+ ...(packageFormat === 5 ? { published_device_views: parsePublishedDeviceViewReceipt(root.published_device_views) } : {}),
199
219
  archive: parseBinaryIdentity(root.archive),
200
220
  canonical_digest: requireSha256(root.canonical_digest, "artifact_v5_canonical_digest_invalid"),
201
221
  };
@@ -225,12 +245,18 @@ export function parseArtifactV5(value) {
225
245
  || runtimeBytes.byteLength !== artifact.runtime.canonical.size_bytes) {
226
246
  throw new Error("artifact_v5_runtime_identity_mismatch");
227
247
  }
248
+ if (artifact.package_format_version === ARTIFACT_PACKAGE_FORMAT_VERSION_V5
249
+ && artifact.published_view !== null) {
250
+ assertPublishedViewBuildReceiptMatchesArtifact(artifact.published_view, artifact);
251
+ }
252
+ if (artifact.published_device_views)
253
+ assertDeviceViewsMatchArtifact(artifact.published_device_views, artifact);
228
254
  return deepFreeze(artifact);
229
255
  }
230
256
  export function parseLegacyArtifactV5Package2(value) {
231
257
  const root = plainObject(value, "artifact_v5_legacy_object_required");
232
258
  if (root.manifest_version !== ARTIFACT_MANIFEST_VERSION_V5
233
- || root.package_format_version !== LEGACY_ARTIFACT_PACKAGE_FORMAT_VERSION_V5) {
259
+ || root.package_format_version !== EARLIER_LEGACY_ARTIFACT_PACKAGE_FORMAT_VERSION_V5) {
234
260
  throw new Error("artifact_v5_legacy_package2_required");
235
261
  }
236
262
  exactKeys(root, [
@@ -251,7 +277,7 @@ export function parseLegacyArtifactV5Package2(value) {
251
277
  return deepFreeze({
252
278
  verification: "legacy_unverified",
253
279
  manifest_version: ARTIFACT_MANIFEST_VERSION_V5,
254
- package_format_version: LEGACY_ARTIFACT_PACKAGE_FORMAT_VERSION_V5,
280
+ package_format_version: EARLIER_LEGACY_ARTIFACT_PACKAGE_FORMAT_VERSION_V5,
255
281
  scenario_id: scenarioId,
256
282
  canonical_digest: canonicalDigest,
257
283
  raw_manifest: rawManifest,
@@ -341,6 +367,25 @@ function normalizeFrozenPublishInput(input) {
341
367
  || databaseDeliveryBytes[0] !== FROZEN_DATABASE_DELIVERY_MAGIC) {
342
368
  throw new Error("frozen_publish_database_delivery_bytes_invalid");
343
369
  }
370
+ const publishedView = input.published_view == null
371
+ ? null
372
+ : parsePublishedViewBuildReceipt(input.published_view);
373
+ if (publishedView !== null) {
374
+ if (publishedView.epoch !== captured_revision.epoch
375
+ || publishedView.runtime_contract_hash !== runtimeContractHash
376
+ || publishedView.source_revision !== captured_revision.revision_number
377
+ || publishedView.source_digest !== captured_revision.source_sha256
378
+ || publishedView.source_revision_digest !== captured_revision.revision_digest
379
+ || publishedView.source_head_digest !== captured_revision.revision_id) {
380
+ throw new Error("frozen_publish_published_view_capture_mismatch");
381
+ }
382
+ if ((publishedView.mobile?.html_sha256 ?? sha256Hex(new Uint8Array())) !== sha256Hex(viewPublishMobileBytes)
383
+ || (publishedView.mobile?.html_size_bytes ?? 0) !== viewPublishMobileBytes.byteLength
384
+ || (publishedView.pc?.html_sha256 ?? sha256Hex(new Uint8Array())) !== sha256Hex(viewPublishPcBytes)
385
+ || (publishedView.pc?.html_size_bytes ?? 0) !== viewPublishPcBytes.byteLength) {
386
+ throw new Error("frozen_publish_published_view_html_identity_mismatch");
387
+ }
388
+ }
344
389
  return {
345
390
  input_schema_version: FROZEN_PUBLISH_INPUT_SCHEMA_VERSION,
346
391
  captured_revision,
@@ -364,9 +409,75 @@ function normalizeFrozenPublishInput(input) {
364
409
  retrieval: toJsonValue(input.retrieval),
365
410
  resources,
366
411
  runtime_identity: runtimeIdentity,
412
+ published_view: publishedView,
413
+ ...(input.published_device_views ? { published_device_views: parsePublishedDeviceViewReceipt(input.published_device_views) } : {}),
367
414
  archive_bytes: requireBytes(input.archive_bytes, "frozen_publish_archive_bytes_invalid"),
368
415
  };
369
416
  }
417
+ function parsePublishedViewBuildReceipt(value) {
418
+ if (value === null)
419
+ return null;
420
+ const root = plainObject(value, "published_view_build_receipt_object_required");
421
+ exactKeys(root, [
422
+ "target_view_id", "target_version", "binding_revision", "copy_operation_id",
423
+ "copy_receipt_sha256", "target_svp_sha256", "source_head_digest", "source_revision",
424
+ "source_digest", "source_revision_digest", "epoch", "runtime_contract_hash", "mobile", "pc",
425
+ ], "published_view_build_receipt");
426
+ const parseHtml = (entry, target) => {
427
+ if (entry === null)
428
+ return null;
429
+ const html = plainObject(entry, `published_view_build_${target}_object_required`);
430
+ exactKeys(html, ["build_id", "encoding", "size_bytes", "sha256", "html_sha256", "html_size_bytes"], `published_view_build_${target}`);
431
+ const identity = parseBinaryIdentity({ encoding: html.encoding, size_bytes: html.size_bytes, sha256: html.sha256 });
432
+ const htmlSha = requireSha256(html.html_sha256, `published_view_build_${target}_sha_invalid`);
433
+ const htmlSize = requirePositiveInteger(html.html_size_bytes, `published_view_build_${target}_size_invalid`);
434
+ if (identity.sha256 !== htmlSha || identity.size_bytes !== htmlSize) {
435
+ throw new Error(`published_view_build_${target}_identity_mismatch`);
436
+ }
437
+ return Object.freeze({
438
+ ...identity,
439
+ build_id: requireIdentifier(html.build_id, `published_view_build_${target}_id_invalid`),
440
+ html_sha256: htmlSha,
441
+ html_size_bytes: htmlSize,
442
+ });
443
+ };
444
+ const mobile = parseHtml(root.mobile, "mobile");
445
+ const pc = parseHtml(root.pc, "pc");
446
+ if (mobile === null && pc === null)
447
+ throw new Error("published_view_build_device_required");
448
+ if (mobile !== null && pc !== null && mobile.build_id === pc.build_id)
449
+ throw new Error("published_view_build_id_duplicate");
450
+ return Object.freeze({
451
+ target_view_id: requireIdentifier(root.target_view_id, "published_view_target_id_invalid"),
452
+ target_version: requirePositiveInteger(root.target_version, "published_view_target_version_invalid"),
453
+ binding_revision: requirePositiveInteger(root.binding_revision, "published_view_binding_revision_invalid"),
454
+ copy_operation_id: requireIdentifier(root.copy_operation_id, "published_view_copy_operation_invalid"),
455
+ copy_receipt_sha256: requireSha256(root.copy_receipt_sha256, "published_view_copy_receipt_invalid"),
456
+ target_svp_sha256: requireSha256(root.target_svp_sha256, "published_view_target_svp_invalid"),
457
+ source_head_digest: requireSha256(root.source_head_digest, "published_view_source_head_invalid"),
458
+ source_revision: requirePositiveInteger(root.source_revision, "published_view_source_revision_invalid"),
459
+ source_digest: requireSha256(root.source_digest, "published_view_source_digest_invalid"),
460
+ source_revision_digest: requireSha256(root.source_revision_digest, "published_view_source_revision_digest_invalid"),
461
+ epoch: requirePositiveInteger(root.epoch, "published_view_epoch_invalid"),
462
+ runtime_contract_hash: requireSha256(root.runtime_contract_hash, "published_view_runtime_contract_invalid"),
463
+ mobile,
464
+ pc,
465
+ });
466
+ }
467
+ function assertPublishedViewBuildReceiptMatchesArtifact(receipt, artifact) {
468
+ if (receipt.epoch !== artifact.epoch
469
+ || receipt.runtime_contract_hash !== artifact.runtime_contract_hash
470
+ || receipt.source_head_digest !== artifact.captured_revision.revision_id
471
+ || receipt.source_revision !== artifact.captured_revision.revision_number
472
+ || receipt.source_digest !== artifact.captured_revision.source_sha256
473
+ || receipt.source_revision_digest !== artifact.captured_revision.revision_digest
474
+ || (receipt.mobile?.html_sha256 ?? sha256Hex(new Uint8Array())) !== artifact.views.publish_mobile.sha256
475
+ || (receipt.mobile?.html_size_bytes ?? 0) !== artifact.views.publish_mobile.size_bytes
476
+ || (receipt.pc?.html_sha256 ?? sha256Hex(new Uint8Array())) !== artifact.views.publish_pc.sha256
477
+ || (receipt.pc?.html_size_bytes ?? 0) !== artifact.views.publish_pc.size_bytes) {
478
+ throw new Error("artifact_v5_published_view_binding_mismatch");
479
+ }
480
+ }
370
481
  function normalizeViewInteractionManifest(value, expectedEpoch, expectedRuntimeContractHash, expectedViewArtifactHash, target) {
371
482
  if (value === null)
372
483
  return null;
@@ -423,6 +534,9 @@ function parseRuntimeIdentity(value) {
423
534
  sdk_schema_version: requirePositiveInteger(object.sdk_schema_version, "frozen_runtime_sdk_schema_version_invalid"),
424
535
  });
425
536
  }
537
+ export function parseFrozenRuntimeIdentity(value) {
538
+ return parseRuntimeIdentity(value);
539
+ }
426
540
  function parseJsonIdentity(value) {
427
541
  const object = plainObject(value, "artifact_v5_json_identity_invalid");
428
542
  exactKeys(object, ["encoding", "size_bytes", "sha256"], "artifact_v5_json_identity");
@@ -521,7 +635,7 @@ function jsonIdentity(value) {
521
635
  });
522
636
  }
523
637
  function digestFrozenInputIdentity(artifact) {
524
- return canonicalSha256(toJsonValue({
638
+ const identity = {
525
639
  input_schema_version: FROZEN_PUBLISH_INPUT_SCHEMA_VERSION,
526
640
  captured_revision: artifact.captured_revision,
527
641
  runtime_contract_hash: artifact.runtime_contract_hash,
@@ -539,7 +653,10 @@ function digestFrozenInputIdentity(artifact) {
539
653
  resources: artifact.resources,
540
654
  runtime: artifact.runtime,
541
655
  archive: artifact.archive,
542
- }));
656
+ };
657
+ return canonicalSha256(toJsonValue(artifact.package_format_version >= ARTIFACT_PACKAGE_FORMAT_VERSION_V5
658
+ ? { ...identity, published_view: artifact.published_view, ...(artifact.published_device_views ? { published_device_views: artifact.published_device_views } : {}) }
659
+ : identity));
543
660
  }
544
661
  function binaryIdentity(value) {
545
662
  return Object.freeze({ encoding: "bytes", size_bytes: value.byteLength, sha256: sha256Hex(value) });
@@ -1,11 +1,15 @@
1
1
  import type { JsonValue } from "./types.js";
2
2
  import type { ViewInteractionManifestV3 } from "@world-engines/tmw/view-interaction-manifest-v3";
3
3
  import type { MusicCandidateSourceV3 } from "@world-engines/tmw/music-candidate-set";
4
+ import type { PublishedDeviceViewReceipt } from "./device-view-receipt.js";
4
5
  export declare const FROZEN_PUBLISH_INPUT_SCHEMA_VERSION: 1;
5
6
  export declare const CAPTURED_REVISION_SCHEMA_VERSION: 3;
6
7
  export declare const ARTIFACT_MANIFEST_VERSION_V5: 5;
7
- export declare const ARTIFACT_PACKAGE_FORMAT_VERSION_V5: 3;
8
- export declare const LEGACY_ARTIFACT_PACKAGE_FORMAT_VERSION_V5: 2;
8
+ export declare const ARTIFACT_PACKAGE_FORMAT_VERSION_V5: 4;
9
+ export declare const DEVICE_VIEW_ARTIFACT_PACKAGE_FORMAT_VERSION: 5;
10
+ /** V3 使用既有完整 ArtifactV5 合同;仅缺少 V4 的 Published View Build receipt。 */
11
+ export declare const LEGACY_ARTIFACT_PACKAGE_FORMAT_VERSION_V5: 3;
12
+ export declare const EARLIER_LEGACY_ARTIFACT_PACKAGE_FORMAT_VERSION_V5: 2;
9
13
  export declare const PUBLISHED_ARTIFACT_HEAD_SCHEMA_VERSION: 1;
10
14
  export declare const FROZEN_ARTIFACT_OBJECT_COUNT: 9;
11
15
  export declare const FROZEN_DATABASE_DELIVERY_CODEC: "scnb-v2";
@@ -72,6 +76,32 @@ export interface FrozenRuntimeIdentity {
72
76
  readonly protocol_version: number;
73
77
  readonly sdk_schema_version: number;
74
78
  }
79
+ /**
80
+ * `bundle_only` target 的服务端 build 证明。`target_svp_sha256` 只标识私有
81
+ * SVP 输入,绝不能替代或推导任何 HTML identity;HTML 仅由各自 exact bytes
82
+ * 的 `html_sha256`/`html_size_bytes` 证明。
83
+ */
84
+ export interface PublishedViewBuildReceipt {
85
+ readonly target_view_id: string;
86
+ readonly target_version: number;
87
+ readonly binding_revision: number;
88
+ readonly copy_operation_id: string;
89
+ readonly copy_receipt_sha256: Sha256Hex;
90
+ readonly target_svp_sha256: Sha256Hex;
91
+ readonly source_head_digest: Sha256Hex;
92
+ readonly source_revision: number;
93
+ readonly source_digest: Sha256Hex;
94
+ readonly source_revision_digest: Sha256Hex;
95
+ readonly epoch: number;
96
+ readonly runtime_contract_hash: Sha256Hex;
97
+ readonly mobile: PublishedViewBuildHtmlReceipt | null;
98
+ readonly pc: PublishedViewBuildHtmlReceipt | null;
99
+ }
100
+ export interface PublishedViewBuildHtmlReceipt extends BinaryIdentity {
101
+ readonly build_id: string;
102
+ readonly html_sha256: Sha256Hex;
103
+ readonly html_size_bytes: number;
104
+ }
75
105
  /**
76
106
  * 一次性 capture 的完整输入。生产 assembler 只能消费该值,不得再次读取 live store。
77
107
  */
@@ -96,6 +126,9 @@ export interface FrozenPublishInput {
96
126
  readonly retrieval: JsonValue;
97
127
  readonly resources: readonly FrozenPublishResource[];
98
128
  readonly runtime_identity: FrozenRuntimeIdentity;
129
+ /** 仅 server-bound bundle-only pipeline 可提供;普通自有 View 明确为 null。 */
130
+ readonly published_view?: PublishedViewBuildReceipt | null;
131
+ readonly published_device_views?: PublishedDeviceViewReceipt;
99
132
  readonly archive_bytes: Uint8Array;
100
133
  }
101
134
  export interface CanonicalJsonIdentity {
@@ -122,7 +155,7 @@ export interface FrozenResourceIdentity extends BinaryIdentity {
122
155
  }
123
156
  export interface ArtifactV5Core {
124
157
  readonly manifest_version: typeof ARTIFACT_MANIFEST_VERSION_V5;
125
- readonly package_format_version: typeof ARTIFACT_PACKAGE_FORMAT_VERSION_V5;
158
+ readonly package_format_version: typeof LEGACY_ARTIFACT_PACKAGE_FORMAT_VERSION_V5 | typeof ARTIFACT_PACKAGE_FORMAT_VERSION_V5 | typeof DEVICE_VIEW_ARTIFACT_PACKAGE_FORMAT_VERSION;
126
159
  readonly scenario_id: string;
127
160
  readonly epoch: number;
128
161
  readonly runtime_contract_hash: Sha256Hex;
@@ -154,6 +187,8 @@ export interface ArtifactV5Core {
154
187
  readonly identity: FrozenRuntimeIdentity;
155
188
  readonly canonical: CanonicalJsonIdentity;
156
189
  };
190
+ readonly published_view: PublishedViewBuildReceipt | null;
191
+ readonly published_device_views?: PublishedDeviceViewReceipt;
157
192
  readonly archive: BinaryIdentity;
158
193
  }
159
194
  export interface ArtifactV5 extends ArtifactV5Core {
@@ -166,7 +201,7 @@ export interface ArtifactV5 extends ArtifactV5Core {
166
201
  export interface LegacyArtifactV5Package2Read {
167
202
  readonly verification: "legacy_unverified";
168
203
  readonly manifest_version: typeof ARTIFACT_MANIFEST_VERSION_V5;
169
- readonly package_format_version: typeof LEGACY_ARTIFACT_PACKAGE_FORMAT_VERSION_V5;
204
+ readonly package_format_version: typeof EARLIER_LEGACY_ARTIFACT_PACKAGE_FORMAT_VERSION_V5;
170
205
  readonly scenario_id: string;
171
206
  readonly canonical_digest: Sha256Hex;
172
207
  readonly raw_manifest: JsonValue;
package/dist/artifact.js CHANGED
@@ -1,8 +1,11 @@
1
1
  export const FROZEN_PUBLISH_INPUT_SCHEMA_VERSION = 1;
2
2
  export const CAPTURED_REVISION_SCHEMA_VERSION = 3;
3
3
  export const ARTIFACT_MANIFEST_VERSION_V5 = 5;
4
- export const ARTIFACT_PACKAGE_FORMAT_VERSION_V5 = 3;
5
- export const LEGACY_ARTIFACT_PACKAGE_FORMAT_VERSION_V5 = 2;
4
+ export const ARTIFACT_PACKAGE_FORMAT_VERSION_V5 = 4;
5
+ export const DEVICE_VIEW_ARTIFACT_PACKAGE_FORMAT_VERSION = 5;
6
+ /** V3 使用既有完整 ArtifactV5 合同;仅缺少 V4 的 Published View Build receipt。 */
7
+ export const LEGACY_ARTIFACT_PACKAGE_FORMAT_VERSION_V5 = 3;
8
+ export const EARLIER_LEGACY_ARTIFACT_PACKAGE_FORMAT_VERSION_V5 = 2;
6
9
  export const PUBLISHED_ARTIFACT_HEAD_SCHEMA_VERSION = 1;
7
10
  export const FROZEN_ARTIFACT_OBJECT_COUNT = 9;
8
11
  export const FROZEN_DATABASE_DELIVERY_CODEC = "scnb-v2";
@@ -1,5 +1,7 @@
1
+ import { type PreparedBoundViewSource } from "./bound-view-source-core.js";
1
2
  import { type NeutralSourceCaptureWitness, type NeutralSourceCompileInput } from "@world-engines/scenario-author-source";
2
- import type { FrozenArtifactBundle, FrozenRuntimeIdentity } from "./artifact.js";
3
+ import type { FrozenArtifactBundle, FrozenRuntimeIdentity, PublishedViewBuildReceipt } from "./artifact.js";
4
+ import type { FrozenPublishInput } from "./artifact.js";
3
5
  import { type NeutralArtifactCompilation, type NeutralArtifactEmbeddingMaterialization } from "./neutral-artifact-assembler.js";
4
6
  /**
5
7
  * Bound-copy authority 的 server readback。`bound_source` 必须是替换目标 SVP
@@ -7,29 +9,32 @@ import { type NeutralArtifactCompilation, type NeutralArtifactEmbeddingMateriali
7
9
  */
8
10
  export interface BoundCopyPublishReceipt {
9
11
  readonly copy_operation_id: string;
12
+ readonly target_view_id: string;
13
+ readonly target_version: number;
14
+ readonly binding_revision: number;
15
+ readonly copy_receipt_sha256: string;
10
16
  readonly base_source_revision_digest: string;
11
17
  readonly target_svp_sha256: string;
12
18
  readonly bound_source: NeutralSourceCaptureWitness;
13
19
  }
14
- /** server-only codec seam;生产实现使用 SVP decoder 与 canonical gzip-tar packer。 */
15
- export interface ServerBoundViewProjectCodec<TViewProject> {
16
- decode_svp(bytes: Uint8Array): TViewProject;
17
- pack_gzip_tar(project: TViewProject): Promise<Uint8Array>;
18
- }
19
20
  /** compiler 只消费本模块重建并 capture 的 source,不能接收调用方拼出的 model。 */
20
21
  export interface ServerBoundViewCompiler<TViewProject> {
21
22
  compile(source: NeutralSourceCompileInput): Promise<NeutralArtifactCompilation<TViewProject>>;
22
23
  }
23
24
  export interface BoundViewFreezeInput {
24
25
  readonly bound_copy_receipt: BoundCopyPublishReceipt;
25
- /** bound-copy 前的冻结 scenario input;adapter 只替换 view_project section。 */
26
+ /** bound-copy 前的冻结 scenario input;仅用于证明 receipt base identity。 */
26
27
  readonly current_compile_input: NeutralSourceCompileInput;
27
- /** server-only store 读出的 SVP1 bytes;任何 digest 都不能替代它。 */
28
- readonly target_svp_bytes: Uint8Array;
28
+ /** 私有 writer 持久化前由 server-bound-view-source 生成的 exact bytes。 */
29
+ readonly prepared_source: PreparedBoundViewSource;
30
+ /** 同一 prepare response 的 SVP input digest;不允许用 gzip-tar digest 冒充。 */
31
+ readonly prepared_target_svp_sha256: string;
29
32
  readonly platform_html: Readonly<{
30
33
  readonly mobile: Uint8Array;
31
34
  readonly pc: Uint8Array;
32
35
  }>;
36
+ /** 仅服务端完成 build 的 receipt;禁止把 SVP hash 当 HTML hash 传入。 */
37
+ readonly published_view: PublishedViewBuildReceipt;
33
38
  readonly content_epoch: number;
34
39
  readonly embedding: NeutralArtifactEmbeddingMaterialization;
35
40
  readonly database_bytes: Uint8Array;
@@ -40,4 +45,5 @@ export interface BoundViewFreezeInput {
40
45
  export interface BoundViewFreezeAdapter {
41
46
  freeze(input: BoundViewFreezeInput): Promise<FrozenArtifactBundle>;
42
47
  }
43
- export declare function createBoundViewFreezeAdapter<TViewProject>(codec: ServerBoundViewProjectCodec<TViewProject>, compiler: ServerBoundViewCompiler<TViewProject>): BoundViewFreezeAdapter;
48
+ export declare function createBoundViewFreezeAdapter<TViewProject>(compiler: ServerBoundViewCompiler<TViewProject>): BoundViewFreezeAdapter;
49
+ export declare function assembleBoundViewFrozenPublishInput<TViewProject>(input: BoundViewFreezeInput, compiler: ServerBoundViewCompiler<TViewProject>): Promise<FrozenPublishInput>;
@@ -1,98 +1,87 @@
1
- import { canonicalScenarioSourceManifestDigestInputBytes, captureNeutralSourceCompileInput, createNeutralScenarioSource, encodeNeutralScenarioSource, } from "@world-engines/scenario-author-source";
2
- import { sha256Hex } from "./canonical.js";
1
+ import { capturePreparedBoundViewSource, } from "./bound-view-source-core.js";
3
2
  import { buildFrozenArtifactBundle } from "./artifact-digest.js";
3
+ import { sha256Hex } from "./canonical.js";
4
4
  import { assembleNeutralFrozenPublishInput, } from "./neutral-artifact-assembler.js";
5
5
  const SHA256 = /^[0-9a-f]{64}$/u;
6
- export function createBoundViewFreezeAdapter(codec, compiler) {
6
+ export function createBoundViewFreezeAdapter(compiler) {
7
7
  return Object.freeze({
8
8
  async freeze(input) {
9
- assertInputReceiptBinding(input);
10
- const targetProject = codec.decode_svp(input.target_svp_bytes.slice());
11
- const targetGzipTar = await codec.pack_gzip_tar(targetProject);
12
- if (!(targetGzipTar instanceof Uint8Array) || targetGzipTar.byteLength === 0) {
13
- throw new Error("bound_view_freeze_target_project_bytes_invalid");
14
- }
15
- const source = await rebuildBoundCompileInput(input, targetGzipTar);
16
- const compilation = await compiler.compile(source);
17
- assertTrustedCompilationMatchesBoundSource(input, source, targetGzipTar, compilation);
18
- const frozen = await assembleNeutralFrozenPublishInput({
19
- source,
20
- content_epoch: input.content_epoch,
21
- compilation,
22
- embedding: input.embedding,
23
- database_bytes: input.database_bytes,
24
- database_delivery_bytes: input.database_delivery_bytes,
25
- view_publish_mobile_bytes: input.platform_html.mobile,
26
- view_publish_pc_bytes: input.platform_html.pc,
27
- runtime_identity: input.runtime_identity,
28
- });
29
- return buildFrozenArtifactBundle(frozen);
9
+ return buildFrozenArtifactBundle(await assembleBoundViewFrozenPublishInput(input, compiler));
30
10
  },
31
11
  });
32
12
  }
33
- async function rebuildBoundCompileInput(input, targetGzipTar) {
34
- let replaced = false;
35
- const sections = input.current_compile_input.sections.map((section) => {
36
- if (section.section_key !== "view_project")
37
- return cloneSection(section);
38
- replaced = true;
39
- return {
40
- section_key: "view_project",
41
- media_type: "application/vnd.chat.view-project+gzip",
42
- canonical_encoding: "gzip-tar-v1",
43
- payload: targetGzipTar.slice(),
44
- };
13
+ export async function assembleBoundViewFrozenPublishInput(input, compiler) {
14
+ assertInputReceiptBinding(input);
15
+ const source = await capturePreparedBoundViewSource(input.prepared_source, input.bound_copy_receipt.bound_source);
16
+ const compilation = await compiler.compile(source);
17
+ assertTrustedCompilationMatchesBoundSource(input, source, input.prepared_source.target_view_gzip_tar_bytes, compilation);
18
+ return await assembleNeutralFrozenPublishInput({
19
+ source,
20
+ content_epoch: input.content_epoch,
21
+ compilation,
22
+ embedding: input.embedding,
23
+ database_bytes: input.database_bytes,
24
+ database_delivery_bytes: input.database_delivery_bytes,
25
+ view_publish_mobile_bytes: input.platform_html.mobile,
26
+ view_publish_pc_bytes: input.platform_html.pc,
27
+ runtime_identity: input.runtime_identity,
28
+ published_view: input.published_view,
45
29
  });
46
- if (!replaced)
47
- throw new Error("bound_view_freeze_current_view_section_missing");
48
- const source = await createNeutralScenarioSource(input.current_compile_input.witness.scenario_id, sections);
49
- const sourceBytes = await encodeNeutralScenarioSource(source);
50
- const witness = input.bound_copy_receipt.bound_source;
51
- const manifest = source.manifest;
52
- if (manifest === undefined
53
- || sha256Hex(sourceBytes) !== witness.source_digest
54
- || sha256Hex(canonicalScenarioSourceManifestDigestInputBytes(manifest)) !== witness.manifest_digest) {
55
- throw new Error("bound_view_freeze_rebuilt_source_receipt_mismatch");
56
- }
57
- return await captureNeutralSourceCompileInput({ witness, source_bytes: sourceBytes });
58
30
  }
59
31
  function assertInputReceiptBinding(input) {
60
32
  const receipt = input.bound_copy_receipt;
61
33
  const current = input.current_compile_input.witness;
62
34
  if (!nonEmptyText(receipt.copy_operation_id)
63
35
  || !SHA256.test(receipt.base_source_revision_digest)
36
+ || !nonEmptyText(receipt.target_view_id)
37
+ || !Number.isSafeInteger(receipt.target_version) || receipt.target_version <= 0
38
+ || !Number.isSafeInteger(receipt.binding_revision) || receipt.binding_revision <= 0
39
+ || !SHA256.test(receipt.copy_receipt_sha256)
64
40
  || !SHA256.test(receipt.target_svp_sha256)
65
41
  || !validWitness(receipt.bound_source)) {
66
42
  throw new Error("bound_view_freeze_receipt_invalid");
67
43
  }
68
- if (!(input.target_svp_bytes instanceof Uint8Array)
69
- || !(input.platform_html.mobile instanceof Uint8Array)
44
+ if (!(input.platform_html.mobile instanceof Uint8Array)
70
45
  || !(input.platform_html.pc instanceof Uint8Array)) {
71
46
  throw new Error("bound_view_freeze_actual_bytes_required");
72
47
  }
73
48
  if (receipt.base_source_revision_digest !== current.revision_digest
74
49
  || receipt.bound_source.scenario_id !== current.scenario_id
75
50
  || receipt.bound_source.owner_id !== current.owner_id
76
- || sha256Hex(input.target_svp_bytes) !== receipt.target_svp_sha256) {
51
+ || input.prepared_source.base.source_revision_digest !== current.revision_digest
52
+ || input.prepared_source.base.source_sha256 !== current.source_digest
53
+ || input.prepared_source.base.manifest_sha256 !== current.manifest_digest
54
+ || input.prepared_target_svp_sha256 !== receipt.target_svp_sha256) {
77
55
  throw new Error("bound_view_freeze_receipt_current_source_mismatch");
78
56
  }
57
+ const published = input.published_view;
58
+ if (published.target_view_id !== receipt.target_view_id
59
+ || published.target_version !== receipt.target_version
60
+ || published.binding_revision !== receipt.binding_revision
61
+ || published.copy_operation_id !== receipt.copy_operation_id
62
+ || published.copy_receipt_sha256 !== receipt.copy_receipt_sha256
63
+ || published.target_svp_sha256 !== receipt.target_svp_sha256
64
+ || published.source_head_digest !== receipt.bound_source.head_digest
65
+ || published.source_revision !== receipt.bound_source.source_revision
66
+ || published.source_digest !== receipt.bound_source.source_digest
67
+ || published.source_revision_digest !== receipt.bound_source.revision_digest
68
+ || published.epoch !== input.content_epoch
69
+ || published.runtime_contract_hash !== input.runtime_identity.runtime_contract_hash
70
+ || (published.mobile?.html_sha256 ?? sha256Hex(new Uint8Array())) !== sha256Hex(input.platform_html.mobile)
71
+ || (published.mobile?.html_size_bytes ?? 0) !== input.platform_html.mobile.byteLength
72
+ || (published.pc?.html_sha256 ?? sha256Hex(new Uint8Array())) !== sha256Hex(input.platform_html.pc)
73
+ || (published.pc?.html_size_bytes ?? 0) !== input.platform_html.pc.byteLength) {
74
+ throw new Error("bound_view_freeze_published_view_proof_mismatch");
75
+ }
79
76
  }
80
77
  function assertTrustedCompilationMatchesBoundSource(input, source, targetGzipTar, compilation) {
81
78
  if (compilation.receipt.scenario_id !== source.witness.scenario_id
82
79
  || compilation.receipt.source_manifest_sha256 !== source.witness.manifest_digest
83
80
  || !sameBytes(compilation.model.view_source_bytes, targetGzipTar)
84
- || compilation.model.trigger_document.runtime_contract_hash !== input.runtime_identity.runtime_contract_hash) {
81
+ || compilation.model.trigger_runtime_contract_hash !== input.runtime_identity.runtime_contract_hash) {
85
82
  throw new Error("bound_view_freeze_trusted_compilation_mismatch");
86
83
  }
87
84
  }
88
- function cloneSection(section) {
89
- return {
90
- section_key: section.section_key,
91
- media_type: section.media_type,
92
- canonical_encoding: section.canonical_encoding,
93
- payload: section.payload.slice(),
94
- };
95
- }
96
85
  function validWitness(witness) {
97
86
  return nonEmptyText(witness.owner_id) && nonEmptyText(witness.scenario_id)
98
87
  && Number.isSafeInteger(witness.source_revision) && witness.source_revision > 0
@@ -0,0 +1,46 @@
1
+ import { type NeutralSourceCaptureWitness, type NeutralSourceCompileInput } from "@world-engines/scenario-author-source";
2
+ export interface V2MaterializedSourceSection {
3
+ readonly section_key: string;
4
+ readonly media_type: string;
5
+ readonly canonical_encoding: string;
6
+ /** Exact canonical ScenarioSourceSection protobuf wire, including unknown fields. */
7
+ readonly section_bytes: Uint8Array;
8
+ }
9
+ export interface V2MaterializedBoundSourceInput {
10
+ readonly witness: NeutralSourceCaptureWitness;
11
+ /** Exact decrypted ScenarioSourceManifest protobuf bytes, including unknown fields. */
12
+ readonly manifest_bytes: Uint8Array;
13
+ readonly sections: readonly V2MaterializedSourceSection[];
14
+ }
15
+ /**
16
+ * Restore V2 materializer bytes using the same manifest/section protobuf
17
+ * decode and canonical verification as host source capture. No digest is ever
18
+ * used to invent a source. Missing outer-source unknown wire fails final hash.
19
+ */
20
+ export declare function restoreV2BoundViewCompileInput(input: V2MaterializedBoundSourceInput): Promise<NeutralSourceCompileInput>;
21
+ export interface PreparedBoundViewSource {
22
+ readonly schema_version: 1;
23
+ readonly base: Readonly<{
24
+ readonly owner_id: string;
25
+ readonly scenario_id: string;
26
+ readonly source_revision: number;
27
+ readonly source_revision_digest: string;
28
+ readonly source_sha256: string;
29
+ readonly manifest_sha256: string;
30
+ }>;
31
+ readonly target_view_gzip_tar_bytes: Uint8Array;
32
+ readonly target_view_gzip_tar_sha256: string;
33
+ readonly derived_source_bytes: Uint8Array;
34
+ readonly derived_source_sha256: string;
35
+ /** Canonical manifest digest-input bytes; private writer persists these with derived source bytes. */
36
+ readonly derived_manifest_digest_input_bytes: Uint8Array;
37
+ readonly derived_manifest_sha256: string;
38
+ }
39
+ /**
40
+ * Pure preparation: validates the captured base source, replaces exactly one
41
+ * view_project payload, and preserves protobuf unknown fields. It does not
42
+ * create a witness or write any object.
43
+ */
44
+ export declare function prepareBoundViewSource(current: NeutralSourceCompileInput, targetViewGzipTarBytes: Uint8Array): Promise<PreparedBoundViewSource>;
45
+ /** After private persistence, bind the authority-issued witness to prepared bytes. */
46
+ export declare function capturePreparedBoundViewSource(prepared: PreparedBoundViewSource, witness: NeutralSourceCaptureWitness): Promise<NeutralSourceCompileInput>;