@volter/world-core 3.0.155 → 3.0.157

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 (150) hide show
  1. package/attach.cjs +2 -0
  2. package/capability-decision.cjs +6 -4
  3. package/dist/attach.cjs +2 -0
  4. package/dist/capability-decision.cjs +6 -4
  5. package/dist/generated/pack-facts.json +204 -4
  6. package/dist/inject.cjs +150 -86
  7. package/dist/network-policy.cjs +11 -1
  8. package/dist/network-policy.d.cts +1 -0
  9. package/dist/pack-facts.d.cts +3 -0
  10. package/dist/required-credential-transport.cjs +29 -0
  11. package/dist/required-credential-transport.d.cts +3 -0
  12. package/dist/src/actions.d.ts +15 -0
  13. package/dist/src/actions.js +82 -21
  14. package/dist/src/ancestry.d.ts +5 -4
  15. package/dist/src/ancestry.js +53 -28
  16. package/dist/src/blob-store.d.ts +16 -0
  17. package/dist/src/blob-store.js +76 -1
  18. package/dist/src/changeset.js +1 -1
  19. package/dist/src/clickhouse/compression.d.ts +28 -0
  20. package/dist/src/clickhouse/compression.js +281 -0
  21. package/dist/src/clickhouse/index.d.ts +1 -0
  22. package/dist/src/clickhouse/index.js +1 -0
  23. package/dist/src/derived-core.d.ts +84 -7
  24. package/dist/src/derived-core.js +336 -61
  25. package/dist/src/derived-real.js +97 -15
  26. package/dist/src/derived.d.ts +5 -0
  27. package/dist/src/derived.js +3 -0
  28. package/dist/src/executor.d.ts +7 -0
  29. package/dist/src/executor.js +15 -1
  30. package/dist/src/fork.js +1 -1
  31. package/dist/src/git/lfs.d.ts +13 -3
  32. package/dist/src/git/lfs.js +41 -9
  33. package/dist/src/grpc-wire.js +2 -1
  34. package/dist/src/h2.js +63 -10
  35. package/dist/src/head.d.ts +5 -0
  36. package/dist/src/head.js +15 -0
  37. package/dist/src/history.d.ts +1 -1
  38. package/dist/src/history.js +7 -7
  39. package/dist/src/index.d.ts +5 -1
  40. package/dist/src/index.js +6 -1
  41. package/dist/src/live/ingest.js +9 -13
  42. package/dist/src/live/rtmp.js +7 -5
  43. package/dist/src/live/segmenter.js +7 -1
  44. package/dist/src/log.d.ts +23 -3
  45. package/dist/src/log.js +160 -37
  46. package/dist/src/machines.js +5 -1
  47. package/dist/src/managed-go-programs.d.ts +55 -0
  48. package/dist/src/managed-go-programs.js +133 -0
  49. package/dist/src/managed-original-inputs.d.ts +101 -0
  50. package/dist/src/managed-original-inputs.js +125 -0
  51. package/dist/src/mongodb/client.d.ts +8 -0
  52. package/dist/src/mongodb/client.js +27 -0
  53. package/dist/src/mongodb/index.d.ts +2 -0
  54. package/dist/src/mongodb/index.js +5 -0
  55. package/dist/src/mongodb/stream.d.ts +97 -0
  56. package/dist/src/mongodb/stream.js +206 -0
  57. package/dist/src/observe.js +1 -1
  58. package/dist/src/pack-fetch.d.ts +6 -1
  59. package/dist/src/pack-fetch.js +69 -14
  60. package/dist/src/pack-paths.d.ts +2 -1
  61. package/dist/src/pack-paths.js +6 -1
  62. package/dist/src/packRegistry.d.ts +10 -14
  63. package/dist/src/packRegistry.js +2 -13
  64. package/dist/src/protocol.d.ts +13 -0
  65. package/dist/src/protocol.js +13 -0
  66. package/dist/src/redis/engine.d.ts +10 -4
  67. package/dist/src/redis/engine.js +103 -32
  68. package/dist/src/redis/member-storage.js +20 -8
  69. package/dist/src/redis/service-times.d.ts +25 -0
  70. package/dist/src/redis/service-times.js +87 -0
  71. package/dist/src/redis/stream.d.ts +2 -1
  72. package/dist/src/redis/stream.js +109 -20
  73. package/dist/src/resource-blob.d.ts +4 -0
  74. package/dist/src/resource-blob.js +8 -1
  75. package/dist/src/runtime-state.d.ts +11 -0
  76. package/dist/src/runtime-state.js +13 -0
  77. package/dist/src/runtime.d.ts +7 -5
  78. package/dist/src/runtime.js +7 -4
  79. package/dist/src/serve-http.d.ts +2 -0
  80. package/dist/src/serve-http.js +73 -6
  81. package/dist/src/serve.d.ts +10 -0
  82. package/dist/src/serve.js +43 -3
  83. package/dist/src/sockets.d.ts +4 -1
  84. package/dist/src/sockets.js +59 -6
  85. package/dist/src/storage.d.ts +2 -1
  86. package/dist/src/storage.js +6 -3
  87. package/dist/src/vendor-call.js +58 -0
  88. package/dist/src/world-boot.d.ts +1 -0
  89. package/dist/src/world-boot.js +6 -0
  90. package/dist/src/world-clock.js +6 -2
  91. package/dist/src/world-store.d.ts +122 -7
  92. package/dist/src/world-store.js +959 -103
  93. package/dist/src/xml.d.ts +30 -0
  94. package/dist/src/xml.js +74 -0
  95. package/generated/pack-facts.json +204 -4
  96. package/inject.cjs +150 -86
  97. package/network-policy.cjs +11 -1
  98. package/network-policy.d.cts +1 -0
  99. package/pack-facts.d.cts +3 -0
  100. package/package.json +27 -1
  101. package/required-credential-transport.cjs +29 -0
  102. package/required-credential-transport.d.cts +3 -0
  103. package/src/actions.ts +78 -21
  104. package/src/ancestry.ts +46 -24
  105. package/src/blob-store.ts +59 -1
  106. package/src/changeset.ts +1 -1
  107. package/src/clickhouse/compression.ts +172 -0
  108. package/src/clickhouse/index.ts +2 -0
  109. package/src/derived-core.ts +356 -61
  110. package/src/derived-real.ts +80 -14
  111. package/src/derived.ts +9 -1
  112. package/src/executor.ts +17 -3
  113. package/src/fork.ts +1 -1
  114. package/src/git/lfs.ts +40 -9
  115. package/src/grpc-wire.ts +2 -1
  116. package/src/h2.ts +46 -5
  117. package/src/head.ts +15 -0
  118. package/src/history.ts +7 -7
  119. package/src/index.ts +7 -1
  120. package/src/live/ingest.ts +7 -3
  121. package/src/live/rtmp.ts +7 -5
  122. package/src/live/segmenter.ts +7 -1
  123. package/src/log.ts +147 -32
  124. package/src/machines.ts +5 -1
  125. package/src/managed-go-programs.ts +146 -0
  126. package/src/managed-original-inputs.ts +163 -0
  127. package/src/mongodb/client.ts +27 -0
  128. package/src/mongodb/index.ts +5 -0
  129. package/src/mongodb/stream.ts +223 -0
  130. package/src/observe.ts +1 -1
  131. package/src/pack-fetch.ts +66 -16
  132. package/src/pack-paths.ts +5 -1
  133. package/src/packRegistry.ts +3 -15
  134. package/src/protocol.ts +13 -0
  135. package/src/redis/engine.ts +100 -57
  136. package/src/redis/member-storage.ts +17 -9
  137. package/src/redis/service-times.ts +81 -0
  138. package/src/redis/stream.ts +66 -14
  139. package/src/resource-blob.ts +9 -1
  140. package/src/runtime-state.ts +14 -0
  141. package/src/runtime.ts +10 -3
  142. package/src/serve-http.ts +65 -6
  143. package/src/serve.ts +45 -3
  144. package/src/sockets.ts +39 -8
  145. package/src/storage.ts +5 -3
  146. package/src/vendor-call.ts +54 -0
  147. package/src/world-boot.ts +6 -0
  148. package/src/world-clock.ts +6 -2
  149. package/src/world-store.ts +768 -91
  150. package/src/xml.ts +71 -0
@@ -57,7 +57,6 @@ export function liveIngest<C>(options: LiveIngestOptions<C>): TwinStream {
57
57
  let priorInit: number | undefined;
58
58
  let started = false;
59
59
  let metadata: Record<string, unknown> | undefined;
60
- let refusedCodec: string | undefined;
61
60
 
62
61
  const recording = (): LiveRecording => {
63
62
  const row = store.rows(LIVE).find((r) => r.id === stream)!;
@@ -135,7 +134,12 @@ export function liveIngest<C>(options: LiveIngestOptions<C>): TwinStream {
135
134
  app: ask!.app, key: ask!.key, state: 'publishing', startedAt: typeof prior?.startedAt === 'string' ? prior.startedAt : now, endedAt: null,
136
135
  publishes: Number(prior?.publishes ?? 0) + 1, window: ruling.window ?? 6, segmentSeconds: ruling.segmentSeconds ?? 2, ...(ruling.retain !== undefined ? { retain: ruling.retain } : {}),
137
136
  }, 'live.publish'));
137
+ // Every admitted publish has its own encoder and hook state. Continuation comes from the recording above,
138
+ // including on a connection reused after FCUnpublish, never from the prior publish's in-memory state.
139
+ initIndex = -1;
138
140
  priorInit = last?.init;
141
+ started = false;
142
+ metadata = undefined;
139
143
  segmenter = new Segmenter({ targetMs: Math.max(0.5, ruling.segmentSeconds ?? 2) * 1000, offsetMs, firstSequence: last ? last.seq + 1 : 0 });
140
144
  stream = named;
141
145
  admitted = ruling;
@@ -154,7 +158,7 @@ export function liveIngest<C>(options: LiveIngestOptions<C>): TwinStream {
154
158
  async video(timestamp, payload) {
155
159
  if (!segmenter) return;
156
160
  const tag = readVideoTag(payload);
157
- if (tag.kind === 'unsupported') { if (refusedCodec !== tag.codec) log(`${stream}: ${tag.codec} video is not recorded (H.264 is)`); refusedCodec = tag.codec; return; }
161
+ if (tag.kind === 'unsupported') throw new RtmpRefusal('NetStream.Failed', `${tag.codec} video is not supported; publish H.264.`);
158
162
  const s = segmenter;
159
163
  if (tag.kind === 'config') await keepAll(s.videoConfig(tag.config));
160
164
  else if (tag.kind === 'frame') await keepAll(overrun(() => s.videoFrame(timestamp, tag.key, tag.cts, tag.data)));
@@ -162,7 +166,7 @@ export function liveIngest<C>(options: LiveIngestOptions<C>): TwinStream {
162
166
  async audio(timestamp, payload) {
163
167
  if (!segmenter) return;
164
168
  const tag = readAudioTag(payload);
165
- if (tag.kind === 'unsupported') { if (refusedCodec !== tag.codec) log(`${stream}: ${tag.codec} audio is not recorded (AAC is)`); refusedCodec = tag.codec; return; }
169
+ if (tag.kind === 'unsupported') throw new RtmpRefusal('NetStream.Failed', `${tag.codec} audio is not supported; publish AAC.`);
166
170
  const s = segmenter;
167
171
  if (tag.kind === 'config') await keepAll(s.audioConfig(tag.config));
168
172
  else await keepAll(overrun(() => s.audioFrame(timestamp, tag.data)));
package/src/live/rtmp.ts CHANGED
@@ -124,6 +124,9 @@ export function rtmpSession(sink: TwinStreamSink, publisher: RtmpPublisher, log:
124
124
  chunks.clear();
125
125
  current = undefined;
126
126
  held = 0;
127
+ // A refused/ended transport owns the publish's cleanup too. An admission already awaiting the dialect settles
128
+ // first; messages queued behind the refusal cannot admit a new publish on the closed connection.
129
+ chain = chain.then(() => stop()).catch((error) => log(`the publish's end failed: ${String((error as Error)?.message ?? error)}`));
127
130
  try { sink.end(); } catch { /* the peer is gone */ }
128
131
  };
129
132
  /** A connection past a ceiling: said once, and closed. */
@@ -175,6 +178,7 @@ export function rtmpSession(sink: TwinStreamSink, publisher: RtmpPublisher, log:
175
178
  try { refusal = await publisher.publish({ app, name: named, ...(type !== undefined ? { type } : {}), ...(tcUrl !== undefined ? { tcUrl } : {}), ...(flashVer !== undefined ? { flashVer } : {}) }); }
176
179
  catch (error) { log(`publish failed: ${String((error as Error)?.message ?? error)}`); refusal = { code: 'NetStream.Publish.Denied', description: 'The server could not take the stream.' }; }
177
180
  if (refusal) { status(stream, 'error', refusal.code, refusal.description, named); close(); return; }
181
+ ended = false;
178
182
  publishing = true;
179
183
  publishStream = stream;
180
184
  // user control StreamBegin (event 0) for the publish's stream, then its start
@@ -257,7 +261,7 @@ export function rtmpSession(sink: TwinStreamSink, publisher: RtmpPublisher, log:
257
261
  const message: Message = { type: st.type, streamId: st.streamId, timestamp: st.timestamp, payload };
258
262
  queued += payload.length;
259
263
  if (queued > MAX_QUEUED) { overrun(`more than ${MAX_QUEUED} bytes of messages wait to be recorded`); return; }
260
- chain = chain.then(() => (phase === 'closed' && message.type !== 20 ? undefined : onMessage(message))).catch((error) => {
264
+ chain = chain.then(() => (phase === 'closed' ? undefined : onMessage(message))).catch((error) => {
261
265
  log(`a message failed: ${String((error as Error)?.message ?? error)}`);
262
266
  close();
263
267
  }).finally(() => { queued -= payload.length; });
@@ -366,10 +370,8 @@ export function rtmpSession(sink: TwinStreamSink, publisher: RtmpPublisher, log:
366
370
  // message's bytes are not in it: only more bytes finish it, and what it may hold is bounded (MAX_HELD)
367
371
  backlog: () => queued + pending.length,
368
372
  close() {
369
- clearTimeout(deadline);
370
- // the connection ended: a publish still open ends with it, after every message already read
371
- chain = chain.then(() => stop()).catch((error) => log(`the publish's end failed: ${String((error as Error)?.message ?? error)}`));
372
- phase = 'closed';
373
+ // Peer close and server refusal share one end, after any admission or write already under way settles.
374
+ close();
373
375
  },
374
376
  };
375
377
  }
@@ -64,14 +64,20 @@ export class Segmenter {
64
64
  this.avc = config;
65
65
  this.freshInit = true;
66
66
  this.keyed = false;
67
+ this.unkeyedSince = undefined;
67
68
  return out;
68
69
  }
69
70
 
70
71
  audioConfig(config: AacConfig): Cut[] {
71
72
  if (this.aac && same(this.aac.asc, config.asc)) return [];
72
- const out = this.aac ? this.flush() : [];
73
+ const out = this.flush();
73
74
  this.aac = config;
74
75
  this.freshInit = true;
76
+ // A configuration boundary has no running video segment. Its first frame must be a new keyframe, and the
77
+ // audio sample clock belongs to the new configuration (which may have a different sample rate).
78
+ this.keyed = false;
79
+ this.unkeyedSince = undefined;
80
+ this.audioNext = undefined;
75
81
  return out;
76
82
  }
77
83
 
package/src/log.ts CHANGED
@@ -30,7 +30,7 @@ import { resolveReferences } from './references.ts';
30
30
  import { hashFieldValue } from './hash.ts';
31
31
  import type { SubjectFields } from './hash.ts';
32
32
  import { worldPaths } from './storage.ts';
33
- import { getActiveWorldStore } from './world-store.ts';
33
+ import { getActiveWorldStore, TREE_FILES } from './world-store.ts';
34
34
  import type { WorldServiceEvent } from './types.ts';
35
35
  import type { TwinResource } from './serve.ts';
36
36
 
@@ -97,8 +97,8 @@ const TREE_PROJECTION_VERSION = 2;
97
97
  export const DELTA_SUFFIX = '.delta';
98
98
 
99
99
  export function parentLogPath(service: string, root?: string): string { return worldPaths(service, root).events; }
100
- export function branchLogPath(service: string, root?: string): string { return join(dirname(worldPaths(service, root).events), 'actions.jsonl'); }
101
- export function branchMetaPath(service: string, root?: string): string { return join(dirname(worldPaths(service, root).events), 'branch.json'); }
100
+ export function branchLogPath(service: string, root?: string): string { return join(dirname(worldPaths(service, root).events), TREE_FILES.branch); }
101
+ export function branchMetaPath(service: string, root?: string): string { return join(dirname(worldPaths(service, root).events), TREE_FILES.pointer); }
102
102
  /** The fetched cache of a URL parent's whole log — what `fetch` extends and a read folds up to the position. */
103
103
  export function originLogPath(service: string, root?: string): string { return join(dirname(worldPaths(service, root).events), 'origin.jsonl'); }
104
104
  /** A parent is at a URL when it is not a path on this machine. */
@@ -173,8 +173,9 @@ function readRows<T>(path: string): T[] {
173
173
  return rows.slice();
174
174
  }
175
175
 
176
- export function readBranchMeta(service: string, root?: string): BranchMeta | null {
177
- const raw = getActiveWorldStore().read(branchMetaPath(service, root));
176
+ export function readBranchMeta(service: string, root?: string, read?: string | null): BranchMeta | null {
177
+ // `read`: branch.json's text where the caller has just read it (a tree's stamp reads it once for both uses).
178
+ const raw = read !== undefined ? read : getActiveWorldStore().read(branchMetaPath(service, root));
178
179
  if (raw === null) return null;
179
180
  const meta = parseExactJson(raw) as BranchMeta & { parent?: { root?: string; parentCount?: number; branchCount?: number } };
180
181
  // a pointer written before positions were one number: the parent's whole log is parent then branch
@@ -228,8 +229,8 @@ export function writeBranchMeta(service: string, meta: BranchMeta, root?: string
228
229
  getActiveWorldStore().mkdir(dirname(path));
229
230
  getActiveWorldStore().writeAtomic(path, `${JSON.stringify(meta, null, 2)}\n`);
230
231
  if (meta.parent?.directory && !isUrlParent(meta.parent.at)) commitParentPin(meta.parent.directory, path);
231
- });
232
- });
232
+ }, { operation: 'pointer publication' });
233
+ }, { operation: 'pointer publication' });
233
234
  }
234
235
 
235
236
  /** A v1 observed row as an entry: a delta event folds its `changed.*.after`; anything else folds
@@ -304,7 +305,7 @@ export function appendOriginEntry(entry: Entry, root?: string): { appended: bool
304
305
  if (readRows<{ id?: string }>(path).some((r) => r.id === entry.id)) return { appended: false };
305
306
  store.append(path, `${JSON.stringify(entry)}\n`);
306
307
  return { appended: true };
307
- }));
308
+ }, { operation: 'origin append' }), { operation: 'origin append' });
308
309
  }
309
310
 
310
311
  /** Where the parent stands now: the length of its whole log (live at a path, the cache for a URL). */
@@ -338,7 +339,7 @@ export function aliasesFrom(parent: Entry[]): Map<string, string> {
338
339
  }
339
340
 
340
341
  const META = new Set(['id', 'type', 'updatedAt']);
341
- type Subject = { type: string; id: string; updatedAt: string; fields: SubjectFields };
342
+ export type Subject = { type: string; id: string; updatedAt: string; fields: SubjectFields };
342
343
  export type Tree = Map<string, Subject>;
343
344
 
344
345
  /** Last write per field. `deleted: true` is the tombstone a delete carries; it holds until the subject
@@ -351,6 +352,24 @@ function overlay(tree: Tree, type: string, id: string, fields: SubjectFields, at
351
352
  tree.set(key, { ...existing, updatedAt: at, fields: merged });
352
353
  }
353
354
 
355
+ /** One subject read by the same guarded key as a write's answer; restore the vendor's own metadata fields. */
356
+ export function heldSubject(service: string, type: string, id: string, root?: string): Subject | undefined {
357
+ const row = readSubject(service, type, id, root);
358
+ return row ? { type: row.type, id: row.id, updatedAt: row.updatedAt, fields: ownFields(row) } : undefined;
359
+ }
360
+
361
+ /**
362
+ * One local write folded onto its subject as the tree folds its entry (`overlay`): the subject as it was (undefined
363
+ * for one new to the tree) is copied, never changed. A command's writes staged in `ctx.batch` are read back by
364
+ * folding them this way, so a read inside the batch sees what the tree will hold once they land (derived-core.ts).
365
+ */
366
+ export function foldLocalWrite(previous: Subject | undefined, type: string, id: string, fields: SubjectFields, at: string): Subject {
367
+ const key = `${type}:${id}`;
368
+ const tree: Tree = previous ? new Map([[key, { ...previous, fields: { ...previous.fields } }]]) : new Map();
369
+ overlay(tree, type, id, fields, at);
370
+ return tree.get(key)!;
371
+ }
372
+
354
373
  /** Fold entries onto a tree, in order: `set` overlays its fields and applies its projection; a
355
374
  * reverted entry folds nothing; a `confirm` row (v1) folds nothing. `skip` names entries the
356
375
  * parent already holds. */
@@ -447,6 +466,22 @@ export function foldHistory(entries: Entry[], layout: HistoryLayout, service?: s
447
466
  foldAliases(tree, aliasesFrom(parent), service);
448
467
  return tree;
449
468
  }
469
+ /**
470
+ * One subject of the current tree as a resource, by its type and id: the resource `projectResources(...).find` gives
471
+ * for the same type and id, read by the subject's key without cloning or projecting the rest of the tree. Every subject is
472
+ * held under `${type}:${id}` (overlay is the only setter, and an alias re-keys through it), so the key is the
473
+ * subject. A write's answer read back the whole tree this way (serve.ts, applyTwinWrite's `resource`), once per
474
+ * written subject: a derived pack writing each document, index and collection of a database's startup paid a
475
+ * projection of every subject the World holds for each (S2 R17).
476
+ */
477
+ export function readSubject(service: string, type: string, id: string, root?: string): TwinResource | undefined {
478
+ const key = `${type}:${id}`;
479
+ const held = currentTree(service, root).tree.get(key);
480
+ // a type or id holding ':' can share a key with another (type, id): only the subject itself is it
481
+ if (!held || held.type !== type || held.id !== id) return undefined;
482
+ return treeResources(new Map([[key, { ...held, fields: { ...held.fields } }]]))[0];
483
+ }
484
+
450
485
  export function readTree(service: string, root?: string, opts: { until?: string; at?: number; view?: string } = {}): TwinResource[] {
451
486
  return treeResources(readTreeMap(service, root, opts));
452
487
  }
@@ -460,7 +495,7 @@ export function readParentTreeMap(service: string, root?: string): Tree {
460
495
  return foldHistory([...(inherited ? historyEntries(inherited) : []), ...own], {
461
496
  ...(inherited ? { base: inherited.layout } : {}), parent: own.length, branch: 0,
462
497
  }, service);
463
- });
498
+ }, { operation: 'tree fold' });
464
499
  }
465
500
 
466
501
  // A READ'S TREE, MEMOIZED per store and per process or isolate. What a read folds is named by cheap
@@ -483,11 +518,44 @@ const TREE_STEPS_KEPT = 64;
483
518
  function statKey(path: string): string { const s = getActiveWorldStore().stat(path); return s ? `${s.size}:${s.mtimeMs}` : '-'; }
484
519
  /** What a service's PARENT side at `root` is folded from (its parent log, branch.json and inherited history), as one
485
520
  * string: while it stands, its aliases and landed ids are the same, whatever the branch's own log does. */
486
- export function parentStamp(service: string, root?: string): string { return parentKeyOf(service, root); }
487
- function parentKeyOf(service: string, root?: string): string { return `${TREE_PROJECTION_VERSION}|${statKey(parentLogPath(service, root))}|${getActiveWorldStore().read(branchMetaPath(service, root)) ?? ''}|${inheritedHistoryKey(service, root)}`; }
521
+ export function parentStamp(service: string, root?: string): string { return treeFacts(service, root).parentKey; }
522
+ function parentKeyOf(service: string, root?: string): string { return parentFacts(service, root).key; }
523
+ /** The parent side's key, and whether it names a file outside the tree's own directory (an inherited view). */
524
+ function parentFacts(service: string, root?: string): { key: string; inherits: boolean } {
525
+ // branch.json is small and every stamp needs it: kept while its own facts stand (world-store.ts readKept), so an
526
+ // unchanged pointer costs one check. Its text is still in the key, so a changed pointer still moves the stamp.
527
+ const store = getActiveWorldStore(), pointer = branchMetaPath(service, root);
528
+ const meta = store.readKept ? store.readKept(pointer) : store.read(pointer);
529
+ const inherited = inheritedHistoryKey(service, root, meta);
530
+ return { key: `${TREE_PROJECTION_VERSION}|${statKey(parentLogPath(service, root))}|${meta ?? ''}|${inherited}`, inherits: inherited !== '' };
531
+ }
532
+ // ONE QUESTION PER STAMP, WHERE THE STORE KEEPS A GENERATION (world-store.ts FsWorldStore.generation, which states
533
+ // what makes it sound and where that stops). A stamp was three questions of the file system, on every operation of
534
+ // every twin: the parent log, the pointer and the branch log, the first two of files a World with no parent does not
535
+ // have. The facts are kept under the generation read BEFORE them, and served while the generation still reads the
536
+ // same. Never kept: where the directory has no generation yet (nothing has been written there by a store that keeps
537
+ // one), and where the parent side names files of other directories (an inherited view: its ancestors' writes move no
538
+ // generation here). Both are asked in full, as before.
539
+ type TreeFacts = { parentKey: string; branchKey: string };
540
+ const keptFacts = new WeakMap<object, Map<string, TreeFacts & { generation: string }>>();
541
+ function treeFacts(service: string, root?: string): TreeFacts {
542
+ const store = getActiveWorldStore();
543
+ const branchLog = branchLogPath(service, root);
544
+ if (!store.generation) return { parentKey: parentKeyOf(service, root), branchKey: statKey(branchLog) };
545
+ const directory = dirname(branchLog);
546
+ const generation = store.generation(directory);
547
+ let kept = keptFacts.get(store);
548
+ if (!kept) { kept = new Map(); keptFacts.set(store, kept); }
549
+ const held = kept.get(directory);
550
+ if (generation !== null && held?.generation === generation) return held;
551
+ const parent = parentFacts(service, root);
552
+ const facts = { parentKey: parent.key, branchKey: statKey(branchLog) };
553
+ if (generation !== null && !parent.inherits) kept.set(directory, { ...facts, generation }); else kept.delete(directory);
554
+ return facts;
555
+ }
488
556
  /** What a service's tree (and every subject's history) at `root` is folded from, as one string: while it
489
557
  * stands, nothing was written. A pack that projects the tree into its own state memoizes on it. */
490
- export function treeStamp(service: string, root?: string): string { return `${parentKeyOf(service, root)}#${statKey(branchLogPath(service, root))}`; }
558
+ export function treeStamp(service: string, root?: string): string { const facts = treeFacts(service, root); return `${facts.parentKey}#${facts.branchKey}`; }
491
559
  const cloneTree = (tree: Tree): Tree => new Map([...tree].map(([k, s]) => [k, { ...s, fields: { ...s.fields } }]));
492
560
  const memoStamp = (m: TreeMemo): string => `${m.parentKey}#${m.branchKey}`;
493
561
 
@@ -529,7 +597,7 @@ function currentTree(service: string, root?: string): { tree: Tree; memo?: TreeM
529
597
  // A hit needs no lock: a memo is only ever stored from a fold made under the history lock, and while the
530
598
  // facts still match that fold, nothing was written since. A writer mid-batch has moved them.
531
599
  const unlocked = memos.get(slot);
532
- if (unlocked && unlocked.parentKey === parentKeyOf(service, root) && unlocked.branchKey === statKey(branchLogPath(service, root))) return { tree: unlocked.tree, memo: unlocked };
600
+ if (unlocked) { const facts = treeFacts(service, root); if (unlocked.parentKey === facts.parentKey && unlocked.branchKey === facts.branchKey) return { tree: unlocked.tree, memo: unlocked }; }
533
601
  return withHistoryLock(worldPaths(service, root).dir, () => {
534
602
  const parentKey = parentKeyOf(service, root); const branchKey = statKey(branchLogPath(service, root));
535
603
  const held = memos.get(slot);
@@ -539,19 +607,7 @@ function currentTree(service: string, root?: string): { tree: Tree; memo?: TreeM
539
607
  if (held && held.parentKey === parentKey && branch.length >= held.branchCount
540
608
  && (held.branchCount === 0 ? held.lastBranchId === null : branch[held.branchCount - 1]?.id === held.lastBranchId)
541
609
  && !branch.slice(held.branchCount).some((e) => e.op === 'revert')) {
542
- const fresh = branch.slice(held.branchCount);
543
- const keys = touchedKeys(fresh, held.aliases);
544
- const from = memoStamp(held);
545
- let appended: string[];
546
- try {
547
- appended = insertionsDuring(held.tree, () => {
548
- foldEntries(held.tree, fresh, held.landed);
549
- foldAliases(held.tree, held.aliases, service, keys);
550
- });
551
- } catch (error) { memos.delete(slot); throw error; } // a half-folded memo is never read again
552
- Object.assign(held, { branchKey, branchCount: branch.length, lastBranchId: last(branch) });
553
- held.steps.push({ from, to: memoStamp(held), keys, appended });
554
- if (held.steps.length > TREE_STEPS_KEPT) held.steps.splice(0, held.steps.length - TREE_STEPS_KEPT);
610
+ extendMemo(memos, slot, held, branch.slice(held.branchCount), branchKey, service);
555
611
  return { tree: held.tree, memo: held };
556
612
  }
557
613
  const tree = readTreeMapFull(service, root);
@@ -563,7 +619,66 @@ function currentTree(service: string, root?: string): { tree: Tree; memo?: TreeM
563
619
  return { tree, memo };
564
620
  }
565
621
  return { tree };
566
- });
622
+ }, { operation: 'tree fold' });
623
+ }
624
+
625
+ /** The memo extended in place by `fresh`, the branch log's entries after its own, which leave the log at `branchKey`,
626
+ * with the step that records the subjects they touched. */
627
+ function extendMemo(memos: Map<string, TreeMemo>, slot: string, held: TreeMemo, fresh: Entry[], branchKey: string, service: string): void {
628
+ const keys = touchedKeys(fresh, held.aliases);
629
+ const from = memoStamp(held);
630
+ let appended: string[];
631
+ try {
632
+ appended = insertionsDuring(held.tree, () => {
633
+ foldEntries(held.tree, fresh, held.landed);
634
+ foldAliases(held.tree, held.aliases, service, keys);
635
+ });
636
+ } catch (error) { memos.delete(slot); throw error; } // a half-folded memo is never read again
637
+ Object.assign(held, { branchKey, branchCount: held.branchCount + fresh.length, lastBranchId: fresh.length ? fresh[fresh.length - 1]!.id : held.lastBranchId });
638
+ held.steps.push({ from, to: memoStamp(held), keys, appended });
639
+ if (held.steps.length > TREE_STEPS_KEPT) held.steps.splice(0, held.steps.length - TREE_STEPS_KEPT);
640
+ }
641
+
642
+ // AN OWN APPEND EXTENDS THE READS' MEMO (S2 R17c). A write appended its entries and the next read found the facts
643
+ // moved, so it took the history lock, read the facts again and the log's new bytes, and extended the memo with the
644
+ // entries this process had just written: in a tab, where each of those is a call into the kernel, about twenty calls
645
+ // per command that writes, taken again by the command after it. The writer, still holding the history lock, extends
646
+ // the memo with its own entries and keeps the tree's facts under the generation its own write left, so the next read
647
+ // asks one question. It does so only where the store can say its write was the one write since the generation it
648
+ // read just before (`generationAfterOwnWrite`), the facts were kept under that generation and the memo stood at them,
649
+ // the parent side's facts read after the append are still the kept ones, and the log grew by exactly its bytes;
650
+ // otherwise it extends nothing, and the next read checks as before.
651
+ /** Append `data` (whole JSONL lines of a service's branch log, `append` writing them) and, when the reads' memo held
652
+ * the tree as it stood, extend it by them. Called holding the directory's history lock. Answers the log's facts
653
+ * after the append (`size:mtimeMs`) when it read them, for a caller keyed on the same. */
654
+ export function appendToBranchLog(service: string, root: string | undefined, data: string, append: () => void): string | undefined {
655
+ const store = getActiveWorldStore();
656
+ const memos = treeMemos.get(store);
657
+ const slot = `${service}\u0000${root ?? ''}`;
658
+ const held = memos?.get(slot);
659
+ const branchLog = branchLogPath(service, root);
660
+ const directory = dirname(branchLog);
661
+ const kept = keptFacts.get(store)?.get(directory);
662
+ const standing = held !== undefined && kept !== undefined && kept.parentKey === held.parentKey && kept.branchKey === held.branchKey;
663
+ const before = standing && store.generation && store.generationAfterOwnWrite ? store.generation(directory) : null;
664
+ append();
665
+ if (!memos || !held || !kept || before === null || kept.generation !== before) return undefined;
666
+ const after = store.generationAfterOwnWrite!(directory, before);
667
+ if (after === null) return undefined;
668
+ // read after the generation, as treeFacts reads its facts. The parent side is read again too: a writer the generation
669
+ // does not see (an older version's, or one that died before its second byte) may have moved it under the kept
670
+ // generation, and carried forward its old key would outlive every append this process makes (S2 R17c review).
671
+ const branchKey = statKey(branchLog);
672
+ if (parentKeyOf(service, root) !== kept.parentKey) return branchKey;
673
+ const grown = (key: string): number => (key === '-' ? 0 : Number(key.slice(0, key.indexOf(':'))));
674
+ const fresh: Entry[] = [];
675
+ // line numbers in a refusal count from this append's first line
676
+ parseLines(`${branchLog} (appended)`, data.split('\n'), 0, fresh);
677
+ if (grown(branchKey) !== grown(held.branchKey) + Buffer.byteLength(data) || fresh.some((e) => e.op === 'revert')) return branchKey;
678
+ // the write has landed: a fold that fails here drops the memo (extendMemo) and the next read folds in full, never failing the write
679
+ try { extendMemo(memos, slot, held, fresh, branchKey, service); } catch { return branchKey; }
680
+ keptFacts.get(store)!.set(directory, { parentKey: kept.parentKey, branchKey, generation: after });
681
+ return branchKey;
567
682
  }
568
683
 
569
684
  /** What changed in a service's tree since `since` (a `treeStamp` an earlier read was made at): the subjects
@@ -649,7 +764,7 @@ function readTreeMapFull(service: string, root?: string, opts: { until?: string;
649
764
  getActiveWorldStore().writeAtomic(checkpointPath(service, root), JSON.stringify({ parentDigest, branchDigest, branchCount: branch.length, subjects: [...tree.values()], ...(now === facts ? { facts } : {}) }));
650
765
  }
651
766
  return tree;
652
- });
767
+ }, { operation: 'tree fold' });
653
768
  }
654
769
  export function cutCheckpoint(service: string, root?: string): void {
655
770
  return withHistoryLock(worldPaths(service, root).dir, () => {
@@ -657,7 +772,7 @@ export function cutCheckpoint(service: string, root?: string): void {
657
772
  const subjects = [...readTreeMap(service, root).values()];
658
773
  const inherited = inheritedHistory(service, root); const own = ownParentEntries(service, root);
659
774
  getActiveWorldStore().writeAtomic(checkpointPath(service, root), JSON.stringify({ parentDigest: hashFieldValue({ projectionVersion: TREE_PROJECTION_VERSION, inherited, own }), branchDigest: hashFieldValue(branchEntries(service, root)), branchCount: branchEntries(service, root).length, subjects }));
660
- });
775
+ }, { operation: 'checkpoint' });
661
776
  }
662
777
 
663
778
  /** The branch's entries the parent does not hold: `set` rows, not reverted, not landed. */
@@ -744,7 +859,7 @@ export function appendParentEntry(entry: Entry, root?: string): { appended: bool
744
859
  index.ids.add(entry.id);
745
860
  index.key = statKey(path);
746
861
  return { appended: true };
747
- }));
862
+ }, { operation: 'parent append' }), { operation: 'parent append' });
748
863
  }
749
864
 
750
865
  /** The whole log of a twin as a served world hands it out: the parent view, then the branch's own
@@ -815,7 +930,7 @@ export function rebaseBranch(service: string, root?: string, opts: { origin?: bo
815
930
  dropCheckpoint(service, root);
816
931
  }
817
932
  return { moved, from, to, conflicts };
818
- });
933
+ }, { operation: 'branch-pointer adjustment' });
819
934
  }
820
935
 
821
936
  /**
package/src/machines.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  // is reached only when a compute pool is asked to act. The light pool delegates to the host's Substrate adapter.
7
7
  import { refuseReadOnlyWrite } from './request-scope.ts';
8
8
  import { nodeBuiltin } from './serve-http.ts';
9
+ import { refuseRequiredMachineTransport } from '../required-credential-transport.cjs';
9
10
 
10
11
  export type MachineSpec = { name: string; image: string; env?: Record<string, string>; ports?: number[]; command?: string[]; checks?: Array<{ port: number; type: 'http' | 'tcp'; path?: string }>; resetOnStop?: boolean; mounts?: Array<{ name: string; path: string }>; signal?: AbortSignal; startTimeoutMs?: number };
11
12
  export type GuestImage = { context: string; dockerfile?: string; runtime?: 'node' | 'bun' | 'python' | 'deno'; command?: string[]; cwd?: string; env?: Record<string, string>; prepared?: boolean };
@@ -94,6 +95,7 @@ function dockerPool(world: string): MachinePool {
94
95
  });
95
96
  return {
96
97
  kind: 'docker',
98
+ async admitRun() { refuseRequiredMachineTransport(process.env, 'Docker machine application transport'); },
97
99
  async status(name) {
98
100
  const state = JSON.parse(await docker(['inspect', '--format', '{{json .State}}', containerName(world, name)]));
99
101
  return state.Running ? { state: 'started' } : { state: 'exited', exitCode: state.ExitCode, ...(state.Error ? { reason: state.Error } : {}) };
@@ -101,6 +103,7 @@ function dockerPool(world: string): MachinePool {
101
103
  async ports() { throw new Error('port discovery requires the light guest backing'); },
102
104
  async files() { throw new Error('file access requires the light guest backing'); },
103
105
  async run(spec) {
106
+ refuseRequiredMachineTransport(process.env, 'Docker machine application transport');
104
107
  const name = containerName(world, spec.name);
105
108
  await docker(['rm', '-f', name]).catch(() => undefined);
106
109
  const env = Object.entries(spec.env ?? {}).flatMap(([k, v]) => ['-e', `${k}=${v}`]);
@@ -114,6 +117,7 @@ function dockerPool(world: string): MachinePool {
114
117
  return { ports: published };
115
118
  },
116
119
  async exec(name, spec) {
120
+ refuseRequiredMachineTransport(process.env, 'Docker machine exec transport');
117
121
  const { spawn } = nodeBuiltin<typeof import('node:child_process')>('node:child_process');
118
122
  const env = Object.entries(spec.env ?? {}).flatMap(([k, v]) => ['-e', `${k}=${v}`]);
119
123
  // the shell names its own pid first, then becomes the process: its pid is the process's
@@ -176,7 +180,7 @@ function dockerPool(world: string): MachinePool {
176
180
  return fetch(`http://127.0.0.1:${host}${source.pathname}${source.search}`, { method: request.method, headers, body, redirect: 'manual', signal: request.signal });
177
181
  },
178
182
  async pause(name) { await docker(['pause', containerName(world, name)]); },
179
- async resume(name) { await docker(['unpause', containerName(world, name)]); },
183
+ async resume(name) { refuseRequiredMachineTransport(process.env, 'Docker machine resume transport'); await docker(['unpause', containerName(world, name)]); },
180
184
  async stop(name) { await docker(['stop', containerName(world, name)]).catch(() => undefined); },
181
185
  async remove(name) { await docker(['rm', '-f', containerName(world, name)]).catch(() => undefined); },
182
186
  };
@@ -0,0 +1,146 @@
1
+ /** Tagged managed Go operation metadata. This structural reader grants no
2
+ * source/image correspondence, filesystem custody, catalog admission or exec. */
3
+ import { posix } from 'node:path';
4
+
5
+ export interface NativeGoKitReference {
6
+ package: { name: string; version: string };
7
+ target: 'wasm32-wali-go-single-thread';
8
+ releaseSha256: string;
9
+ producerSha256: string;
10
+ sourceSha256: string;
11
+ selection: { path: string; sha256: string };
12
+ recipe: { path: string; sha256: string };
13
+ installedRoot: string;
14
+ }
15
+ export interface NativeGoOperation {
16
+ sourceService: string;
17
+ role: 'server' | 'prerequisite' | 'health';
18
+ kit: string;
19
+ program: { name: string; version: string; manifestId: string; manifestSha256: string; path: string };
20
+ /** Resolved arguments require separate comparison with the full source. */
21
+ args: string[];
22
+ /** Names of existing World values, never caller secrets in this record. */
23
+ environmentBindings: Record<string, string>;
24
+ requires: { service: string; condition: 'service_started' | 'service_healthy' | 'service_completed_successfully' }[];
25
+ }
26
+ export interface NativeGoReference {
27
+ version: 2;
28
+ implementation: string;
29
+ release: string;
30
+ directory: string;
31
+ snapshotSha256: string;
32
+ declarationSha256: string;
33
+ image: string;
34
+ kits: Record<string, NativeGoKitReference>;
35
+ server: NativeGoOperation;
36
+ prerequisites: NativeGoOperation[];
37
+ health: NativeGoOperation;
38
+ }
39
+
40
+ const fail = (field: string): never => { throw new Error('Native Go operation metadata is invalid: ' + field); };
41
+ function object(value: unknown, fields: readonly string[], label: string): Record<string, unknown> {
42
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return fail(label);
43
+ const row = value as Record<string, unknown>;
44
+ if (Object.keys(row).length !== fields.length || fields.some(key => !Object.hasOwn(row, key))) return fail(label);
45
+ return row;
46
+ }
47
+ function text(value: unknown, label: string): string {
48
+ if (typeof value !== 'string' || !value || value.includes('\0')) return fail(label);
49
+ return value;
50
+ }
51
+ function digest(value: unknown, label: string): string {
52
+ const out = text(value, label);
53
+ if (!/^[a-f0-9]{64}$/u.test(out)) return fail(label);
54
+ return out;
55
+ }
56
+ function relativePath(value: unknown, label: string): string {
57
+ const out = text(value, label);
58
+ if (out.startsWith('/') || out.includes('\\') || posix.normalize(out) !== out
59
+ || out.split('/').some(part => !part || part === '.' || part === '..')) return fail(label);
60
+ return out;
61
+ }
62
+ function installedPath(value: unknown, label: string): string {
63
+ const out = text(value, label);
64
+ if (!out.startsWith('/opt/programs/') || out.includes('\\') || posix.normalize(out) !== out
65
+ || out.split('/').slice(1).some(part => !part || part === '.' || part === '..')) return fail(label);
66
+ return out;
67
+ }
68
+ function pin(value: unknown, label: string): { path: string; sha256: string } {
69
+ const row = object(value, ['path', 'sha256'], label);
70
+ return { path: relativePath(row.path, label + '.path'), sha256: digest(row.sha256, label + '.sha256') };
71
+ }
72
+ function kit(value: unknown, label: string): NativeGoKitReference {
73
+ const row = object(value, ['package', 'target', 'releaseSha256', 'producerSha256', 'sourceSha256', 'selection', 'recipe', 'installedRoot'], label);
74
+ const pack = object(row.package, ['name', 'version'], label + '.package');
75
+ if (row.target !== 'wasm32-wali-go-single-thread') return fail(label + '.target');
76
+ return { package: { name: text(pack.name, label + '.package.name'), version: text(pack.version, label + '.package.version') },
77
+ target: row.target, releaseSha256: digest(row.releaseSha256, label + '.releaseSha256'),
78
+ producerSha256: digest(row.producerSha256, label + '.producerSha256'), sourceSha256: digest(row.sourceSha256, label + '.sourceSha256'),
79
+ selection: pin(row.selection, label + '.selection'), recipe: pin(row.recipe, label + '.recipe'),
80
+ installedRoot: installedPath(row.installedRoot, label + '.installedRoot') };
81
+ }
82
+ function operation(value: unknown, role: NativeGoOperation['role'], kits: Record<string, NativeGoKitReference>, label: string): NativeGoOperation {
83
+ const row = object(value, ['sourceService', 'role', 'kit', 'program', 'args', 'environmentBindings', 'requires'], label);
84
+ const selected = text(row.kit, label + '.kit');
85
+ if (row.role !== role || !Object.hasOwn(kits, selected)) return fail(label + '.role/kit');
86
+ const provider = kits[selected]!;
87
+ const command = object(row.program, ['name', 'version', 'manifestId', 'manifestSha256', 'path'], label + '.program');
88
+ const path = installedPath(command.path, label + '.program.path'), version = text(command.version, label + '.program.version');
89
+ if (version !== provider.package.version || !path.startsWith(provider.installedRoot + '/')) return fail(label + '.program placement/version');
90
+ if (!Array.isArray(row.args)) return fail(label + '.args');
91
+ const args: string[] = [];
92
+ for (const arg of row.args) {
93
+ if (typeof arg !== 'string' || arg.includes('\0')) return fail(label + '.args.' + args.length);
94
+ args.push(arg);
95
+ }
96
+ if (!row.environmentBindings || typeof row.environmentBindings !== 'object' || Array.isArray(row.environmentBindings)) return fail(label + '.environmentBindings');
97
+ const bindings: Record<string, string> = Object.create(null);
98
+ for (const [name, value] of Object.entries(row.environmentBindings)) {
99
+ const source = text(value, label + '.environmentBindings.' + name);
100
+ if (![name, source].every(item => /^[A-Za-z_][A-Za-z0-9_]*$/u.test(item))) return fail(label + '.environmentBindings name');
101
+ bindings[name] = source;
102
+ }
103
+ if (!Array.isArray(row.requires)) return fail(label + '.requires');
104
+ const dependencies: NativeGoOperation['requires'] = [];
105
+ for (const value of row.requires) {
106
+ const edge = object(value, ['service', 'condition'], label + '.requires');
107
+ if (edge.condition !== 'service_started' && edge.condition !== 'service_healthy' && edge.condition !== 'service_completed_successfully')
108
+ return fail(label + '.requires.condition');
109
+ const service = text(edge.service, label + '.requires.service');
110
+ if (dependencies.some(prior => prior.service === service)) return fail(label + '.requires duplicate service');
111
+ dependencies.push({ service, condition: edge.condition });
112
+ }
113
+ return { sourceService: text(row.sourceService, label + '.sourceService'), role, kit: selected,
114
+ program: { name: text(command.name, label + '.program.name'), version, manifestId: text(command.manifestId, label + '.program.manifestId'),
115
+ manifestSha256: digest(command.manifestSha256, label + '.program.manifestSha256'), path },
116
+ args, environmentBindings: bindings, requires: dependencies };
117
+ }
118
+
119
+ /** All fields are declared data. The later owned consumer must authenticate
120
+ * the complete source, kit, original image and admitted installed manifests. */
121
+ export function nativeGoReference(value: unknown): NativeGoReference {
122
+ const row = object(value, ['version', 'implementation', 'release', 'directory', 'snapshotSha256', 'declarationSha256', 'image', 'kits', 'server', 'prerequisites', 'health'], 'reference');
123
+ if (row.version !== 2) return fail('version');
124
+ const image = text(row.image, 'image');
125
+ if (!/^[^\s@]+@sha256:[a-f0-9]{64}$/u.test(image)) return fail('image');
126
+ if (!row.kits || typeof row.kits !== 'object' || Array.isArray(row.kits) || !Object.keys(row.kits).length) return fail('kits');
127
+ const kits: Record<string, NativeGoKitReference> = Object.create(null);
128
+ for (const [name, value] of Object.entries(row.kits)) kits[text(name, 'kit name')] = kit(value, 'kits.' + name);
129
+ const server = operation(row.server, 'server', kits, 'server'), health = operation(row.health, 'health', kits, 'health');
130
+ const implementation = text(row.implementation, 'implementation'), release = text(row.release, 'release');
131
+ if (kits[server.kit]!.package.name !== implementation || kits[server.kit]!.package.version !== release
132
+ || health.sourceService !== server.sourceService) return fail('server/health implementation');
133
+ if (!Array.isArray(row.prerequisites)) return fail('prerequisites');
134
+ const prerequisites: NativeGoOperation[] = [];
135
+ for (const value of row.prerequisites)
136
+ prerequisites.push(operation(value, 'prerequisite', kits, 'prerequisites.' + prerequisites.length));
137
+ if (new Set(prerequisites.map(item => item.sourceService)).size !== prerequisites.length
138
+ || prerequisites.some(item => item.sourceService === server.sourceService)) return fail('prerequisite service');
139
+ const usedKits = new Set([server.kit, health.kit, ...prerequisites.map(item => item.kit)]);
140
+ if (Object.keys(kits).some(name => !usedKits.has(name))) return fail('unconsumed kit');
141
+ for (const item of prerequisites) if (!server.requires.some(edge => edge.service === item.sourceService && edge.condition === 'service_completed_successfully'))
142
+ return fail('server prerequisite completion edge');
143
+ return { version: 2, implementation, release, directory: relativePath(row.directory, 'directory'),
144
+ snapshotSha256: digest(row.snapshotSha256, 'snapshotSha256'), declarationSha256: digest(row.declarationSha256, 'declarationSha256'),
145
+ image, kits, server, prerequisites, health };
146
+ }