@plurnk/plurnk-service 1.6.0 → 1.6.1

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 (124) hide show
  1. package/.env.defaults +7 -21
  2. package/INSTALL.md +18 -17
  3. package/README.md +1 -1
  4. package/SPEC.md +125 -115
  5. package/dist/build-info.json +1 -1
  6. package/dist/core/ChannelWrite.sql +3 -3
  7. package/dist/core/CoreSchemeServices.d.ts +1 -1
  8. package/dist/core/CoreSchemeServices.d.ts.map +1 -1
  9. package/dist/core/CoreSchemeServices.js +1 -1
  10. package/dist/core/CoreSchemeServices.js.map +1 -1
  11. package/dist/core/Dispatcher.d.ts +2 -2
  12. package/dist/core/Dispatcher.d.ts.map +1 -1
  13. package/dist/core/Dispatcher.js +79 -1115
  14. package/dist/core/Dispatcher.js.map +1 -1
  15. package/dist/core/Engine.d.ts +9 -33
  16. package/dist/core/Engine.d.ts.map +1 -1
  17. package/dist/core/Engine.js +58 -1656
  18. package/dist/core/Engine.js.map +1 -1
  19. package/dist/core/Engine.sql +46 -32
  20. package/dist/core/LogBody.d.ts +1 -0
  21. package/dist/core/LogBody.d.ts.map +1 -1
  22. package/dist/core/LogBody.js +4 -0
  23. package/dist/core/LogBody.js.map +1 -1
  24. package/dist/core/ModelCall.d.ts +2 -2
  25. package/dist/core/ModelCall.d.ts.map +1 -1
  26. package/dist/core/ModelCall.js +4 -2
  27. package/dist/core/ModelCall.js.map +1 -1
  28. package/dist/core/PacketBuilder.d.ts +11 -22
  29. package/dist/core/PacketBuilder.d.ts.map +1 -1
  30. package/dist/core/PacketBuilder.js +66 -168
  31. package/dist/core/PacketBuilder.js.map +1 -1
  32. package/dist/core/ProblemLog.d.ts +1 -1
  33. package/dist/core/ProblemLog.d.ts.map +1 -1
  34. package/dist/core/ProblemLog.js +14 -3
  35. package/dist/core/ProblemLog.js.map +1 -1
  36. package/dist/core/ProposalLifecycle.d.ts +2 -2
  37. package/dist/core/ProposalLifecycle.d.ts.map +1 -1
  38. package/dist/core/ProposalLifecycle.js +14 -5
  39. package/dist/core/ProposalLifecycle.js.map +1 -1
  40. package/dist/core/ProviderInstantiate.js +2 -2
  41. package/dist/core/ProviderInstantiate.js.map +1 -1
  42. package/dist/core/ResourceMutations.d.ts +73 -0
  43. package/dist/core/ResourceMutations.d.ts.map +1 -0
  44. package/dist/core/ResourceMutations.js +1142 -0
  45. package/dist/core/ResourceMutations.js.map +1 -0
  46. package/dist/core/SchemeRegistry.js +3 -3
  47. package/dist/core/SchemeRegistry.js.map +1 -1
  48. package/dist/core/StoredPacket.d.ts +2 -2
  49. package/dist/core/StoredPacket.js +7 -7
  50. package/dist/core/TurnRunner.d.ts +72 -0
  51. package/dist/core/TurnRunner.d.ts.map +1 -0
  52. package/dist/core/TurnRunner.js +1730 -0
  53. package/dist/core/TurnRunner.js.map +1 -0
  54. package/dist/core/caps/DbChannelCaps.js +4 -4
  55. package/dist/core/caps/DbChannelCaps.js.map +1 -1
  56. package/dist/core/content-hash.js +2 -2
  57. package/dist/core/content-hash.js.map +1 -1
  58. package/dist/core/content-weight.d.ts +2 -0
  59. package/dist/core/content-weight.d.ts.map +1 -0
  60. package/dist/core/content-weight.js +4 -0
  61. package/dist/core/content-weight.js.map +1 -0
  62. package/dist/core/content_weight.d.ts +4 -0
  63. package/dist/core/content_weight.d.ts.map +1 -0
  64. package/dist/core/content_weight.js +6 -0
  65. package/dist/core/content_weight.js.map +1 -0
  66. package/dist/core/fork.sql +5 -5
  67. package/dist/core/packet-wire.d.ts +4 -4
  68. package/dist/core/packet-wire.d.ts.map +1 -1
  69. package/dist/core/packet-wire.js +21 -21
  70. package/dist/core/packet-wire.js.map +1 -1
  71. package/dist/core/scheme-types.d.ts +1 -1
  72. package/dist/core/scheme-types.d.ts.map +1 -1
  73. package/dist/digest/Digest.js +1 -1
  74. package/dist/digest/Digest.js.map +1 -1
  75. package/dist/schemes/Exec.d.ts.map +1 -1
  76. package/dist/schemes/Exec.js +26 -10
  77. package/dist/schemes/Exec.js.map +1 -1
  78. package/dist/schemes/ExecOutputScheme.js +1 -1
  79. package/dist/schemes/ExecOutputScheme.js.map +1 -1
  80. package/dist/schemes/File.d.ts.map +1 -1
  81. package/dist/schemes/File.js +1 -1
  82. package/dist/schemes/File.js.map +1 -1
  83. package/dist/schemes/Log.d.ts.map +1 -1
  84. package/dist/schemes/Log.js +2 -2
  85. package/dist/schemes/Log.sql +2 -2
  86. package/dist/schemes/Worker.js +2 -2
  87. package/dist/schemes/Worker.js.map +1 -1
  88. package/dist/schemes/_entry-crud.js +4 -4
  89. package/dist/schemes/_entry-crud.js.map +1 -1
  90. package/dist/schemes/_entry-crud.sql +2 -2
  91. package/dist/schemes/_entry-find.d.ts +5 -5
  92. package/dist/schemes/_entry-find.d.ts.map +1 -1
  93. package/dist/schemes/_entry-find.js +29 -28
  94. package/dist/schemes/_entry-find.js.map +1 -1
  95. package/dist/schemes/_entry-manifest.js +4 -4
  96. package/dist/schemes/_entry-manifest.js.map +1 -1
  97. package/dist/schemes/_entry-ops.d.ts.map +1 -1
  98. package/dist/schemes/_entry-ops.js +3 -3
  99. package/dist/schemes/_entry-ops.js.map +1 -1
  100. package/dist/schemes/_entry-ops.sql +3 -3
  101. package/dist/server/Daemon.d.ts +7 -42
  102. package/dist/server/Daemon.d.ts.map +1 -1
  103. package/dist/server/Daemon.js +124 -887
  104. package/dist/server/Daemon.js.map +1 -1
  105. package/dist/server/DrainSupervisor.d.ts +117 -0
  106. package/dist/server/DrainSupervisor.d.ts.map +1 -0
  107. package/dist/server/DrainSupervisor.js +802 -0
  108. package/dist/server/DrainSupervisor.js.map +1 -0
  109. package/dist/server/lifecycle-recovery.sql +3 -0
  110. package/dist/server/logEntry.d.ts +1 -1
  111. package/dist/server/logEntry.js +1 -1
  112. package/dist/server/seam-entry-read.sql +2 -2
  113. package/dist/service.js +1 -1
  114. package/dist/service.js.map +1 -1
  115. package/migrations/001_schema.sql +20 -10
  116. package/package.json +33 -33
  117. package/dist/core/ruler_count.d.ts +0 -4
  118. package/dist/core/ruler_count.d.ts.map +0 -1
  119. package/dist/core/ruler_count.js +0 -7
  120. package/dist/core/ruler_count.js.map +0 -1
  121. package/dist/core/token-ruler.d.ts +0 -2
  122. package/dist/core/token-ruler.d.ts.map +0 -1
  123. package/dist/core/token-ruler.js +0 -5
  124. package/dist/core/token-ruler.js.map +0 -1
@@ -9,8 +9,7 @@ import { PathSyntax, TagSignal } from "@plurnk/plurnk-contracts";
9
9
  import Namespace from "./namespace.js";
10
10
  import LoopFlagsReader from "./LoopFlagsReader.js";
11
11
  import ChannelWrite from "./ChannelWrite.js";
12
- import { assertEditBatchReceipt, assertEditReceipt, assertResourceEffects, EditCollision, editReceipt, LineAnchors, LineMarkerOps, MimetypeBinary, PathMimetype, ReadProjector, projectEditReceipt, } from "../content/index.js";
13
- import DbProjectionCaps from "./caps/DbProjectionCaps.js";
12
+ import { ReadProjector } from "../content/index.js";
14
13
  import SchemeCtxImpl from "./caps/SchemeCtxImpl.js";
15
14
  import EntryCrud from "../schemes/_entry-crud.js";
16
15
  import EntryOps from "../schemes/_entry-ops.js";
@@ -24,6 +23,8 @@ import EffectPolicy from "../schemes/EffectPolicy.js";
24
23
  import { CoreSchemeAdapterBase, } from "./CoreSchemeServices.js";
25
24
  import { InvalidOperationResultError, NetworkAddress, } from "@plurnk/plurnk-schemes";
26
25
  import DurableStatement from "./DurableStatement.js";
26
+ import ResourceMutations from "./ResourceMutations.js";
27
+ import LogBody from "./LogBody.js";
27
28
  // SPEC {§scheme-surface}: writer must be in target scheme's manifest.writableBy.
28
29
  // OPEN/FOLD/READ/FIND are not gated — they curate the log or read, never mutating an entry.
29
30
  const MUTATING_OPS = new Set(["EDIT", "SEND", "COPY", "MOVE", "EXEC", "KILL", "FORK", "WORK"]);
@@ -37,9 +38,8 @@ const assertClassifyingSignal = (statement) => {
37
38
  TagSignal.applied(statement.signal);
38
39
  }
39
40
  };
40
- // Op dispatch ({§op-methods-op-dispatch}): gates (writableBy, loop flags), the
41
- // engine-owned op orchestrations (COPY/MOVE/KILL/SEND), scheme
42
- // routing, the durable log write, and the proposal pause.
41
+ // Op dispatch ({§op-methods-op-dispatch}): admission, operation-owner routing,
42
+ // durable log writing, and proposal lifecycle.
43
43
  class Dispatcher {
44
44
  static #failure(code, status, detail, fields = {}, extensions = {}) {
45
45
  return Results.failure("engine:dispatcher", code, status, detail, fields, extensions);
@@ -52,7 +52,7 @@ class Dispatcher {
52
52
  #db;
53
53
  #schemes;
54
54
  #mimetypes;
55
- #tokenize;
55
+ #weighContent;
56
56
  #notices;
57
57
  #proposals;
58
58
  // Boot-discovered runtime executors, late-injected on Engine — thunked.
@@ -74,12 +74,12 @@ class Dispatcher {
74
74
  #joinTargets;
75
75
  #liveSubscriptions;
76
76
  #lifecycle;
77
- #preparedEdits = new WeakMap();
78
- constructor({ db, schemes, mimetypes, tokenize, notices, proposals, executors, loopSignal, streamEventNotify, wakeWorkerNotify, injectWorker, branchWorker, branchCompletionGate, cancelWorker, cancelDescendants, searchGate, parkDeadlines, joinTargets, liveSubscriptions }) {
77
+ #resourceMutations;
78
+ constructor({ db, schemes, mimetypes, weigh, notices, proposals, executors, loopSignal, streamEventNotify, wakeWorkerNotify, injectWorker, branchWorker, branchCompletionGate, cancelWorker, cancelDescendants, searchGate, parkDeadlines, joinTargets, liveSubscriptions }) {
79
79
  this.#db = db;
80
80
  this.#schemes = schemes;
81
81
  this.#mimetypes = mimetypes;
82
- this.#tokenize = tokenize;
82
+ this.#weighContent = weigh;
83
83
  this.#notices = notices;
84
84
  this.#proposals = proposals;
85
85
  this.#executors = executors;
@@ -96,6 +96,23 @@ class Dispatcher {
96
96
  this.#joinTargets = joinTargets ?? new Set();
97
97
  this.#liveSubscriptions = liveSubscriptions;
98
98
  this.#lifecycle = new LoopLifecycle(db);
99
+ this.#resourceMutations = new ResourceMutations({
100
+ schemes,
101
+ liveSubscriptions,
102
+ run: (schemeName, statement, ctx) => this.#run(schemeName, statement, ctx),
103
+ checkWritable: (statement, origin) => this.#checkWritable(statement, origin),
104
+ checkFlagsGate: (statement, loopId) => this.#checkFlagsGate(statement, loopId),
105
+ editTargetIdentity: (statement, workspaceId) => this.#editTargetIdentity(statement, workspaceId),
106
+ canonicalFilePath: (pathname, workspaceId) => this.#canonicalFilePath(pathname, workspaceId),
107
+ prepareDataRepresentation: (args) => this.#prepareDataRepresentation({
108
+ ...args,
109
+ handler: args.handler,
110
+ }),
111
+ readEntry: (scheme, pathname, ctx) => this.#readEntry(scheme, pathname, ctx),
112
+ writeEntry: (scheme, pathname, entry, ctx) => this.#writeEntry(scheme, pathname, entry, ctx),
113
+ deleteChannel: (scheme, pathname, channel, ctx) => this.#deleteChannel(scheme, pathname, channel, ctx),
114
+ applyProposal: (statement, result, resolution, ids) => this.#proposals.workerApply(statement, result, resolution, ids),
115
+ });
99
116
  }
100
117
  // workspace → project_root, memoized: {§fs-namespace} fixes the root immutably at
101
118
  // workspace creation, so a process-lifetime cache can never go stale.
@@ -213,230 +230,35 @@ class Dispatcher {
213
230
  if (key !== null)
214
231
  target.pathname = key;
215
232
  }
216
- async #resolveEditAnchors(statements, identity, schemeName, manifest, ctx) {
217
- const anchored = statements.filter(({ lineMarker }) => LineAnchors.hasAnchor(lineMarker));
218
- if (anchored.length === 0) {
219
- return { statements: statements, precondition: null };
220
- }
221
- if (manifest.textEditScopes !== true || !manifest.writableBy.includes("model")) {
222
- return {
223
- result: _a.#failure("line-anchor-unsupported", 400, `Scheme '${schemeName}' does not support textual EDIT scopes.`, {}, {
224
- scheme: schemeName,
225
- operation: "EDIT",
226
- recovery: "Remove the scope and submit the scheme's complete editable unit.",
227
- retryable: false,
228
- }),
229
- };
230
- }
231
- if (identity === null) {
232
- return {
233
- result: _a.#failure("edit-target-required", 400, "A line-anchored EDIT requires a target resource.", {}, { recovery: "Provide the target that rendered the line anchor.", retryable: false }),
234
- };
235
- }
236
- const first = statements[0];
237
- if (first === undefined)
238
- return { statements: [], precondition: null };
239
- const current = await this.#run(schemeName, {
240
- op: "READ",
241
- suffix: first.suffix,
242
- signal: null,
243
- target: first.target,
244
- lineMarker: { marks: [1, -1] },
245
- body: null,
246
- position: first.position,
247
- }, ctx);
248
- if (current.status === 204 || current.status === 404) {
249
- return { result: EditCollision.result(identity) };
250
- }
251
- if (current.status >= 300) {
252
- return {
253
- result: _a.#failure("line-anchor-validation-failed", current.status, `EDIT could not validate its line anchor at ${identity}: ${current.problem?.detail ?? `READ returned ${current.status}`}`, {}, {
254
- target: identity,
255
- upstreamStatus: current.status,
256
- stage: "mutation-precondition",
257
- retryable: false,
258
- }),
259
- };
260
- }
261
- if (current.status !== 200) {
262
- return { result: EditCollision.result(identity) };
263
- }
264
- const content = current.content;
265
- if (typeof content !== "string") {
266
- throw new InvalidOperationResultError(`Scheme '${schemeName}' returned READ ${current.status} without textual content while validating an EDIT anchor.`);
267
- }
268
- const lineAnchors = current.lineAnchors;
269
- const lineAnchorIdentity = current.lineAnchorIdentity;
270
- try {
271
- LineAnchors.assertProjection(content, lineAnchors);
272
- if (typeof lineAnchorIdentity !== "string" || lineAnchorIdentity.length === 0) {
273
- throw new TypeError("READ line anchors require their canonical derivation identity.");
274
- }
275
- }
276
- catch (cause) {
277
- throw new InvalidOperationResultError(`Scheme '${schemeName}' returned READ 200 without its core-owned line-anchor projection.`, { cause });
278
- }
279
- const resolved = [];
280
- const checks = [];
281
- for (const statement of statements) {
282
- if (statement.lineMarker === null || !LineAnchors.hasAnchor(statement.lineMarker)) {
283
- resolved.push(statement);
284
- continue;
285
- }
286
- const resolution = LineAnchors.resolve(lineAnchors, statement.lineMarker);
287
- if (!resolution.ok) {
288
- const { anchor, kind } = resolution.failure;
289
- const invalid = kind === "invalid";
290
- if (!invalid)
291
- return { result: EditCollision.result(lineAnchorIdentity) };
292
- return {
293
- result: _a.#failure("line-anchor-invalid", 400, `${anchor} is not valid in that EDIT line-coordinate position.`, {}, {
294
- anchor,
295
- target: identity,
296
- recovery: "Use anchors only where the EDIT scope denotes a line.",
297
- retryable: false,
298
- }),
299
- };
300
- }
301
- for (const [index, anchor] of statement.lineMarker.marks.entries()) {
302
- if (typeof anchor !== "string")
303
- continue;
304
- const line = resolution.marker.marks[index];
305
- if (typeof line !== "number") {
306
- throw new InvalidOperationResultError("An EDIT line anchor did not lower to a numeric line.");
307
- }
308
- checks.push({ anchor, line });
309
- }
310
- resolved.push({ ...statement, lineMarker: resolution.marker });
311
- }
312
- const uniqueChecks = [...new Map(checks.map((check) => [`${check.anchor}:${check.line}`, check])).values()];
313
- return {
314
- statements: resolved,
315
- precondition: { identity: lineAnchorIdentity, checks: uniqueChecks },
316
- };
233
+ async #canonicalFilePath(pathname, workspaceId) {
234
+ return Namespace.canonicalizeSpelling(pathname, await this.#workspaceRoot(workspaceId));
317
235
  }
318
- async prepareEditBatches(statements, context) {
319
- const { workspaceId, workerId, loopId, turnId, origin } = context;
320
- const schemeCtx = this.#buildSchemeCtx({ workspaceId, workerId, loopId, turnId, origin });
321
- const groups = new Map();
322
- for (const statement of statements) {
323
- assertClassifyingSignal(statement); // {§log-tag-signal}
324
- const target = this.#extractTarget(statement.target);
325
- await this.#canonColumns(target, workspaceId);
326
- const key = JSON.stringify([
236
+ async #editTargetIdentity(statement, workspaceId) {
237
+ const target = this.#extractTarget(statement.target);
238
+ await this.#canonColumns(target, workspaceId);
239
+ return {
240
+ key: JSON.stringify([
327
241
  schemeNameOf(statement.target),
328
242
  target.scheme,
329
243
  target.hostname,
330
244
  target.pathname,
331
245
  target.fragment,
332
- ]);
333
- const group = groups.get(key);
334
- if (group === undefined)
335
- groups.set(key, {
336
- identity: renderTarget(target),
337
- statements: [statement],
338
- });
339
- else
340
- group.statements.push(statement);
341
- }
342
- for (const preparedGroup of groups.values()) {
343
- const group = preparedGroup.statements;
344
- const first = group[0];
345
- const schemeName = schemeNameOf(first.target);
346
- let initial;
347
- let denial = group.map((statement) => this.#checkWritable(statement, origin)).find((result) => result !== null) ?? null;
348
- if (denial === null) {
349
- for (const statement of group) {
350
- denial = await this.#checkFlagsGate(statement, loopId);
351
- if (denial !== null)
352
- break;
353
- }
354
- }
355
- if (denial !== null) {
356
- initial = denial;
357
- }
358
- else if (schemeName === null) {
359
- initial = _a.#failure("target-required", 400, "EDIT requires a target scheme.", {}, { retryable: false });
360
- }
361
- else {
362
- const handler = this.#schemes.get(schemeName);
363
- const method = handler?.editBatch;
364
- const manifest = this.#schemes.manifestFor(schemeName);
365
- if (typeof method !== "function" || manifest === undefined) {
366
- initial = _a.#failure("operation-not-implemented", 501, `Scheme '${schemeName}' does not implement EDIT batches.`, {}, {
367
- scheme: schemeName,
368
- operation: "EDIT",
369
- retryable: false,
370
- });
371
- }
372
- else {
373
- try {
374
- const resolved = await this.#resolveEditAnchors(group, preparedGroup.identity, schemeName, manifest, schemeCtx);
375
- if ("result" in resolved) {
376
- initial = resolved.result;
377
- }
378
- else {
379
- const addressedScheme = first.target?.kind === "url" ? first.target.scheme : schemeName;
380
- const publishedChannel = first.target?.kind === "url"
381
- ? first.target.fragment ?? manifest.defaultChannel
382
- : manifest.defaultChannel;
383
- initial = Results.assert(await method.call(handler, resolved.statements, new SchemeCtxImpl(schemeCtx, addressedScheme ?? schemeName, manifest, this.#liveSubscriptions, {
384
- publishedChannel,
385
- editPrecondition: resolved.precondition,
386
- })));
387
- }
388
- }
389
- catch (err) {
390
- if (err instanceof InvalidOperationResultError)
391
- throw err;
392
- console.error(`Scheme '${schemeName}' EDIT batch threw outside its operation result contract:`, err);
393
- initial = _a.#failure("scheme-handler-threw", 500, `The '${schemeName}' scheme did not produce an EDIT result.`, {}, {
394
- stage: "scheme-dispatch",
395
- scheme: schemeName,
396
- operation: "EDIT",
397
- });
398
- }
399
- }
400
- }
401
- let resolveSettled;
402
- const settled = new Promise((resolve) => { resolveSettled = resolve; });
403
- const candidate = initial.editReceipt;
404
- const expectedNormalizations = group.filter(({ lineMarker }) => lineMarker?.marks.length === 3).length;
405
- if (initial.status < 400
406
- && (initial.scopeNormalizations?.length ?? 0) !== expectedNormalizations) {
407
- throw new InvalidOperationResultError(`EDIT batch normalized ${initial.scopeNormalizations?.length ?? 0} scope(s), expected ${expectedNormalizations}.`);
408
- }
409
- const batch = {
410
- initial,
411
- settled,
412
- aggregate: candidate === undefined || candidate === null
413
- ? undefined
414
- : assertEditBatchReceipt(candidate),
415
- settle: resolveSettled,
416
- };
417
- let normalizationIndex = 0;
418
- for (const [index, statement] of group.entries()) {
419
- const ownsNormalization = statement.lineMarker?.marks.length === 3 && initial.status < 400;
420
- this.#preparedEdits.set(statement, {
421
- first: index === 0,
422
- index,
423
- normalizationIndex: ownsNormalization ? normalizationIndex++ : null,
424
- batch,
425
- });
426
- }
427
- }
246
+ ]),
247
+ identity: renderTarget(target),
248
+ };
249
+ }
250
+ async prepareEditBatches(statements, context) {
251
+ for (const statement of statements)
252
+ assertClassifyingSignal(statement); // {§log-tag-signal}
253
+ const { workspaceId, workerId, loopId, turnId, origin } = context;
254
+ const schemeCtx = this.#buildSchemeCtx({ workspaceId, workerId, loopId, turnId, origin });
255
+ await this.#resourceMutations.prepareEditBatches(statements, context, schemeCtx);
428
256
  }
429
257
  async dispatch(context) {
430
258
  assertClassifyingSignal(context.statement); // {§log-tag-signal}
431
259
  const result = await this.#dispatchOne(context);
432
260
  if (context.statement.op === "EDIT") {
433
- const prepared = this.#preparedEdits.get(context.statement);
434
- if (prepared?.first === true) {
435
- const normalizations = prepared.batch.initial.scopeNormalizations;
436
- prepared.batch.settle(normalizations === undefined
437
- ? result
438
- : Results.assert({ ...result, scopeNormalizations: normalizations }));
439
- }
261
+ this.#resourceMutations.settleEdit(context.statement, result);
440
262
  }
441
263
  return result;
442
264
  }
@@ -459,34 +281,7 @@ class Dispatcher {
459
281
  // those are system failures.
460
282
  try {
461
283
  if (statement.op === "EDIT") {
462
- const prepared = this.#preparedEdits.get(statement);
463
- if (prepared === undefined) {
464
- throw new InvalidOperationResultError("EDIT reached dispatch without a prepared resource batch.");
465
- }
466
- else {
467
- const settled = prepared.first
468
- ? prepared.batch.initial
469
- : await prepared.batch.settled;
470
- const normalization = prepared.normalizationIndex === null
471
- ? undefined
472
- : settled.scopeNormalizations?.[prepared.normalizationIndex];
473
- const { scopeNormalizations: _scopeNormalizations, ...projectedSettlement } = settled;
474
- const statementResult = normalization === undefined
475
- ? projectedSettlement
476
- : { ...projectedSettlement, scopeNormalizations: [normalization] };
477
- const aggregate = prepared.batch.aggregate;
478
- if (aggregate !== undefined) {
479
- const receipt = assertEditBatchReceipt(aggregate);
480
- const { editReceipt: _editReceipt, ...withoutAggregate } = statementResult;
481
- result = {
482
- ...withoutAggregate,
483
- receipt: projectEditReceipt(receipt, prepared.index),
484
- };
485
- }
486
- else {
487
- result = statementResult;
488
- }
489
- }
284
+ result = await this.#resourceMutations.preparedEditResult(statement);
490
285
  }
491
286
  else if (statement.op === "SEND" && statement.target === null) {
492
287
  result = await this.#handleSendBroadcast(statement, { workspaceId, workerId, loopId, turnId, sequence });
@@ -500,10 +295,10 @@ class Dispatcher {
500
295
  result = await this.#handleWorkerControl(statement, schemeCtx);
501
296
  }
502
297
  else if (statement.op === "COPY") {
503
- result = await this.#handleCopy(statement, schemeCtx);
298
+ result = await this.#resourceMutations.handleCopy(statement, schemeCtx);
504
299
  }
505
300
  else if (statement.op === "MOVE") {
506
- result = await this.#handleMove(statement, schemeCtx);
301
+ result = await this.#resourceMutations.handleMove(statement, schemeCtx);
507
302
  }
508
303
  else if (statement.op === "KILL") {
509
304
  result = await this.#handleKill(statement, schemeCtx);
@@ -583,15 +378,13 @@ class Dispatcher {
583
378
  }
584
379
  if (autoAccept) {
585
380
  const initialSettlement = await this.#proposals.workerApply(statement, result, { decision: "accept" }, { workspaceId, workerId, loopId, turnId });
586
- const settlement = _a.#settleProposalEffects(result, initialSettlement);
587
- const effective = await this.#settleMoveProposal({
381
+ const effective = await this.#resourceMutations.settleProposal({
588
382
  statement,
589
383
  result,
590
- settlement,
384
+ settlement: initialSettlement,
591
385
  ctx: schemeCtx,
592
386
  ids: { workspaceId, workerId, loopId, turnId },
593
387
  });
594
- this.#recordEditSettlement(statement, effective);
595
388
  return this.#proposals.applyResolution(logEntryId, effective);
596
389
  }
597
390
  // Register the resolution waiter SYNCHRONOUSLY before any await
@@ -620,15 +413,13 @@ class Dispatcher {
620
413
  // preserved: an apply failure keeps its original status and
621
414
  // Problem Details instead of masquerading as a client rejection.
622
415
  const initialSettlement = await this.#proposals.workerApply(statement, result, resolution, { workspaceId, workerId, loopId, turnId });
623
- const settlement = _a.#settleProposalEffects(result, initialSettlement);
624
- const effective = await this.#settleMoveProposal({
416
+ const effective = await this.#resourceMutations.settleProposal({
625
417
  statement,
626
418
  result,
627
- settlement,
419
+ settlement: initialSettlement,
628
420
  ctx: schemeCtx,
629
421
  ids: { workspaceId, workerId, loopId, turnId },
630
422
  });
631
- this.#recordEditSettlement(statement, effective);
632
423
  const post = await this.#proposals.applyResolution(logEntryId, effective);
633
424
  return post;
634
425
  }
@@ -805,7 +596,7 @@ class Dispatcher {
805
596
  wakeWorkerNotify: this.#wakeWorkerNotify,
806
597
  injectWorker: this.#injectWorker,
807
598
  mimetypes: this.#mimetypes,
808
- tokenize: this.#tokenize,
599
+ weigh: this.#weighContent,
809
600
  pushNotice: (notice) => this.#notices.push(workspaceId, loopId, notice),
810
601
  executors: this.#executors(),
811
602
  };
@@ -979,7 +770,7 @@ class Dispatcher {
979
770
  return check(statement.target);
980
771
  }
981
772
  // Worker control is FORK/WORK (grammar 0.74.55), not COPY — its body
982
- // is the new worker's seed prompt, not a destination path. The COPY gates and #handleCopy
773
+ // is the new worker's seed prompt, not a destination path. The COPY gates and ResourceMutations.handleCopy
983
774
  // branch on this so they never parse the prompt as a dst path.
984
775
  #isWorkerControl(statement) {
985
776
  return statement.op === "FORK" || statement.op === "WORK"; // worker control targets worker://<name> (grammar 0.74.55)
@@ -1056,53 +847,6 @@ class Dispatcher {
1056
847
  await ctx.injectWorker({ workspaceId: ctx.workspaceId, workerId: row.id, prompt, flags, parentLoopId: ctx.loopId });
1057
848
  return { status: 200, body: name };
1058
849
  }
1059
- async #handleCopy(statement, ctx) {
1060
- if (statement.target === null) {
1061
- return _a.#failure("copy-source-required", 400, "COPY requires a source path.", {}, { retryable: false });
1062
- }
1063
- if (statement.body === null) {
1064
- return _a.#failure("copy-destination-required", 400, "COPY requires a destination.", {}, { retryable: false });
1065
- }
1066
- return this.#copyOrchestration({
1067
- statement,
1068
- source: {
1069
- target: statement.target,
1070
- lineMarker: statement.lineMarker,
1071
- },
1072
- destination: statement.body,
1073
- ctx,
1074
- });
1075
- }
1076
- async #handleMove(statement, ctx) {
1077
- if (statement.target === null) {
1078
- return _a.#failure("move-source-required", 400, "MOVE requires a source path.", {}, { retryable: false });
1079
- }
1080
- // MOVE is relocation only - deletion is KILL's job ({§move}, {§move-dev-null-not-special}). The /dev/null
1081
- // and null-body delete-by-MOVE has no alternate meaning.
1082
- if (statement.body === null) {
1083
- return _a.#failure("move-destination-required", 400, "MOVE requires a destination.", {}, {
1084
- recovery: "Use KILL when the intended operation is deletion.",
1085
- retryable: false,
1086
- });
1087
- }
1088
- const sourceMarks = statement.lineMarker?.marks;
1089
- const sourceLineMarker = sourceMarks?.length === 2
1090
- && sourceMarks[0] === 1
1091
- && sourceMarks[1] === -1
1092
- ? null
1093
- : statement.lineMarker;
1094
- return this.#moveOrchestration({
1095
- statement,
1096
- source: {
1097
- target: statement.target,
1098
- // Canonicalize only the execution selection. #writeLog retains
1099
- // the authored marker as operation evidence. {§move-canonical-whole-source}
1100
- lineMarker: sourceLineMarker,
1101
- },
1102
- destination: statement.body,
1103
- ctx,
1104
- });
1105
- }
1106
850
  // KILL is target-polymorphic. Scheme handlers own the optional numeric code's
1107
851
  // meaning; core retains worker and entry dispatch. {§operation-code-polymorphism}
1108
852
  async #handleKill(statement, ctx) {
@@ -1173,6 +917,8 @@ class Dispatcher {
1173
917
  return this.#deleteEntry(schemeName, entryPathnameOf(path), ctx);
1174
918
  }
1175
919
  async #writeActionlessEntry({ verbatim, workerId, loopId, turnId, sequence, origin, kind, folded, modelCallId = null, attrs = {} }) {
920
+ const durableAttrs = { ...attrs, kind };
921
+ const rx = JSON.stringify({ content: verbatim, mimetype: "text/vnd.plurnk" });
1176
922
  const row = await this.#db.engine_insert_log_entry.get({
1177
923
  worker_id: workerId, loop_id: loopId, turn_id: turnId, sequence,
1178
924
  origin, source: null, model_call_id: modelCallId,
@@ -1180,10 +926,19 @@ class Dispatcher {
1180
926
  scheme: null, username: null, password: null, hostname: null, port: null,
1181
927
  pathname: null, query: null, fragment: null, lineMarker: null,
1182
928
  tx: "", mimetype_tx: "text/vnd.plurnk",
1183
- rx: JSON.stringify({ content: verbatim, mimetype: "text/vnd.plurnk" }),
929
+ rx,
1184
930
  mimetype_rx: "application/json",
1185
- status_rx: 200, tokens: this.#tokenize(verbatim), state: "resolved", outcome: null,
1186
- attrs: JSON.stringify({ ...attrs, kind }),
931
+ status_rx: 200,
932
+ weight: LogBody.weight({
933
+ op: null,
934
+ attrs: durableAttrs,
935
+ tx: "",
936
+ rx,
937
+ mimetypeTx: "text/vnd.plurnk",
938
+ mimetypeRx: "application/json",
939
+ }, this.#weighContent),
940
+ state: "resolved", outcome: null,
941
+ attrs: JSON.stringify(durableAttrs),
1187
942
  });
1188
943
  if (row === undefined)
1189
944
  throw new Error("Dispatcher.#writeActionlessEntry: insert returned no row");
@@ -1236,804 +991,6 @@ class Dispatcher {
1236
991
  context.onDispatch?.(logEntryId);
1237
992
  return result;
1238
993
  }
1239
- static #isDispatchResult(value) {
1240
- return "status" in value;
1241
- }
1242
- async #resolveResourceSelection(selection, ctx) {
1243
- const { target, lineMarker } = selection;
1244
- const scheme = schemeNameOf(target);
1245
- if (scheme === null) {
1246
- return _a.#failure("resource-scheme-required", 400, "COPY and MOVE resources require a scheme.", {}, { retryable: false });
1247
- }
1248
- if (target.kind === "url" && target.scheme === "worker" && (target.hostname ?? "") !== "") {
1249
- return _a.#failure("worker-copy-address-invalid", 400, "COPY and MOVE do not address named worker spaces.", {}, {
1250
- recovery: "Move worker-space content with READ and EDIT.",
1251
- retryable: false,
1252
- });
1253
- }
1254
- const handler = this.#schemes.get(scheme);
1255
- const manifest = this.#schemes.manifestFor(scheme);
1256
- if (handler === undefined || manifest === undefined) {
1257
- return _a.#failure("scheme-not-found", 501, `COPY or MOVE addressed the unregistered scheme '${scheme}'.`, {}, {
1258
- scheme,
1259
- retryable: false,
1260
- });
1261
- }
1262
- if (manifest.category !== "data") {
1263
- return _a.#failure("entry-operation-unsupported", 400, `COPY and MOVE require entry-bearing resources; '${scheme}' is a ${manifest.category} scheme.`, {}, {
1264
- scheme,
1265
- category: manifest.category,
1266
- retryable: false,
1267
- });
1268
- }
1269
- const fragment = target.kind === "url" ? target.fragment : null;
1270
- const channel = fragment ?? manifest.defaultChannel;
1271
- if (channel.length === 0) {
1272
- return _a.#failure("channel-required", 400, `The '${scheme}' scheme has no default channel.`, {}, {
1273
- scheme,
1274
- recovery: "Address a named channel with a URI fragment.",
1275
- retryable: false,
1276
- });
1277
- }
1278
- if (fragment !== null
1279
- && fragment !== manifest.defaultChannel
1280
- && !Object.hasOwn(manifest.channels, fragment)) {
1281
- const availableChannels = [
1282
- ...new Set([manifest.defaultChannel, ...Object.keys(manifest.channels)]),
1283
- ].filter((candidate) => candidate.length > 0);
1284
- return _a.#failure("channel-not-found", 400, `Channel #${fragment} is not declared by the '${scheme}' scheme.`, {}, {
1285
- requestedChannel: fragment,
1286
- availableChannels,
1287
- retryable: false,
1288
- });
1289
- }
1290
- const pathname = entryPathnameOf(target);
1291
- const canonicalFilePath = scheme === "file"
1292
- ? Namespace.canonicalizeSpelling(pathname, await this.#workspaceRoot(ctx.workspaceId))
1293
- : pathname;
1294
- return {
1295
- target,
1296
- lineMarker,
1297
- scheme,
1298
- pathname,
1299
- identityPathname: canonicalFilePath ?? pathname,
1300
- channel,
1301
- manifest,
1302
- };
1303
- }
1304
- #resolveResourceLineMarker(selection, content, operation) {
1305
- if (!LineAnchors.hasAnchor(selection.lineMarker)) {
1306
- return {
1307
- selection: {
1308
- ...selection,
1309
- lineMarker: selection.lineMarker,
1310
- },
1311
- precondition: null,
1312
- };
1313
- }
1314
- const target = this.#resourceAddress(selection);
1315
- if (selection.manifest.textEditScopes !== true || !selection.manifest.writableBy.includes("model")) {
1316
- return {
1317
- result: _a.#failure("line-anchor-unsupported", 400, `The representation at ${target} does not publish line anchors.`, {}, {
1318
- operation,
1319
- target,
1320
- recovery: "Use numeric text coordinates.",
1321
- retryable: false,
1322
- }),
1323
- };
1324
- }
1325
- const resolution = LineAnchors.resolve(LineAnchors.tokens(target, content), selection.lineMarker);
1326
- if (!resolution.ok) {
1327
- if (resolution.failure.kind === "invalid") {
1328
- return {
1329
- result: _a.#failure("line-anchor-invalid", 400, "A line anchor appeared outside a line-coordinate position.", {}, {
1330
- operation,
1331
- target,
1332
- recovery: "Use anchors only for L, SL, or EL; columns remain numeric.",
1333
- retryable: false,
1334
- }),
1335
- };
1336
- }
1337
- return {
1338
- result: _a.#failure("line-anchor-collision", 409, `${operation} coordinates collided with current content at ${target}.`, {}, {
1339
- operation,
1340
- target,
1341
- recovery: `READ ${target} again and retry against its current coordinates.`,
1342
- retryable: true,
1343
- }),
1344
- };
1345
- }
1346
- return {
1347
- selection: { ...selection, lineMarker: resolution.marker },
1348
- precondition: {
1349
- identity: target,
1350
- checks: LineAnchors.checks(selection.lineMarker, resolution.marker),
1351
- },
1352
- };
1353
- }
1354
- async #selectSource(selection, ctx, operation) {
1355
- const handler = this.#schemes.get(selection.scheme);
1356
- if (handler === undefined) {
1357
- throw new InvalidOperationResultError(`Resolved COPY/MOVE source scheme '${selection.scheme}' is no longer registered.`);
1358
- }
1359
- const addressedScheme = selection.target.kind === "url"
1360
- ? selection.target.scheme
1361
- : selection.scheme;
1362
- const schemeCtx = new SchemeCtxImpl(ctx, addressedScheme, selection.manifest, this.#liveSubscriptions, { publishedChannel: selection.channel });
1363
- const prepared = await this.#prepareDataRepresentation({
1364
- target: selection.target,
1365
- routedScheme: selection.scheme,
1366
- handler,
1367
- manifest: selection.manifest,
1368
- ctx,
1369
- schemeCtx,
1370
- publishedChannel: selection.channel,
1371
- });
1372
- if (prepared.result !== null)
1373
- return prepared.result;
1374
- if (prepared.address === null) {
1375
- return _a.#failure("entry-not-found", 404, `No entry exists at ${this.#resourceAddress(selection)}.`, {}, { target: this.#resourceAddress(selection) });
1376
- }
1377
- const storageAddress = prepared.address;
1378
- const read = await EntryCrud.readEntry(storageAddress.pathname, ctx, storageAddress.scheme, storageAddress.ownerId);
1379
- if (read.status >= 400)
1380
- return read;
1381
- if (read.status !== 200 || read.entry === null) {
1382
- throw new InvalidOperationResultError(`The '${selection.scheme}' scheme returned status ${read.status} without a COPY/MOVE source entry.`);
1383
- }
1384
- const selected = read.entry.channels[selection.channel];
1385
- if (selected === undefined) {
1386
- return _a.#failure("channel-not-found", 404, `No channel named #${selection.channel} exists at ${renderAddress(storageAddress.scheme, storageAddress.pathname)}.`, {}, {
1387
- target: renderAddress(storageAddress.scheme, storageAddress.pathname),
1388
- requestedChannel: selection.channel,
1389
- availableChannels: Object.keys(read.entry.channels),
1390
- retryable: false,
1391
- });
1392
- }
1393
- const resolvedMarker = this.#resolveResourceLineMarker(selection, selected.content, operation);
1394
- if ("result" in resolvedMarker)
1395
- return resolvedMarker.result;
1396
- let content = selected.content;
1397
- let scopeNormalizations;
1398
- if (await MimetypeBinary.isBinaryMimetype(selected.mimetype, ctx.mimetypes)) {
1399
- return _a.#failure("binary-source-unsupported", 415, `Channel #${selection.channel} is a binary marker and cannot be copied or moved.`, {}, {
1400
- channel: selection.channel,
1401
- mimetype: selected.mimetype,
1402
- recovery: "Use a source with a readable text projection.",
1403
- retryable: false,
1404
- });
1405
- }
1406
- if (resolvedMarker.selection.lineMarker !== null) {
1407
- const sliced = LineMarkerOps.sliceLinesRaw(content, resolvedMarker.selection.lineMarker);
1408
- if (sliced.status !== 200)
1409
- return Results.assert(sliced);
1410
- content = sliced.text ?? "";
1411
- scopeNormalizations = sliced.scopeNormalizations;
1412
- }
1413
- if (selected.producerResult !== undefined && selected.producerResult.status >= 400) {
1414
- return Results.assert(selected.producerResult);
1415
- }
1416
- return {
1417
- ...resolvedMarker.selection,
1418
- storageAddress,
1419
- content,
1420
- completeContent: selected.content,
1421
- mimetype: selected.mimetype,
1422
- lineAnchorPrecondition: resolvedMarker.precondition,
1423
- ...(scopeNormalizations === undefined ? {} : { scopeNormalizations }),
1424
- };
1425
- }
1426
- static #prependScopeNormalizations(result, scopeNormalizations) {
1427
- if (scopeNormalizations === undefined)
1428
- return result;
1429
- return Results.assert({
1430
- ...result,
1431
- scopeNormalizations: [
1432
- ...scopeNormalizations,
1433
- ...(result.scopeNormalizations ?? []),
1434
- ],
1435
- });
1436
- }
1437
- static #mergeLineAnchorPreconditions(...values) {
1438
- const present = values.filter((value) => value !== null);
1439
- if (present.length === 0)
1440
- return null;
1441
- const identity = present[0].identity;
1442
- if (present.some((value) => value.identity !== identity)) {
1443
- throw new TypeError("Line-anchor preconditions for one edit batch must share a resource identity.");
1444
- }
1445
- const checks = [...new Map(present.flatMap(({ checks: valueChecks }) => valueChecks)
1446
- .map((check) => [`${check.anchor}:${check.line}`, check])).values()];
1447
- return { identity, checks };
1448
- }
1449
- #resourceAddress(selection) {
1450
- const address = renderTarget({
1451
- scheme: selection.scheme === "file" ? null : selection.scheme,
1452
- pathname: selection.scheme === "file"
1453
- ? selection.identityPathname.replace(/^\//, "")
1454
- : selection.identityPathname,
1455
- fragment: selection.channel === selection.manifest.defaultChannel
1456
- ? null
1457
- : selection.channel,
1458
- });
1459
- if (address === null)
1460
- throw new Error("resolved resource selection has no renderable address");
1461
- return address;
1462
- }
1463
- #pendingEffect(selection, action) {
1464
- return {
1465
- target: this.#resourceAddress(selection),
1466
- action,
1467
- };
1468
- }
1469
- static #appliedEffects(result, pending) {
1470
- // {§edit-result-copy-move-effects} — only an applied mutation earns
1471
- // engine-composed effects; native scheme receipts remain internal input.
1472
- const exact = Results.assert(result);
1473
- if (exact.status === 304 || exact.status === 202 || exact.status >= 300)
1474
- return exact;
1475
- if (exact.effects !== undefined) {
1476
- throw new InvalidOperationResultError("A COPY/MOVE mutation result supplied effects before engine composition.");
1477
- }
1478
- const batch = exact.editReceipt === undefined
1479
- ? undefined
1480
- : assertEditBatchReceipt(exact.editReceipt);
1481
- const single = batch === undefined && exact.receipt !== undefined
1482
- ? assertEditReceipt(exact.receipt)
1483
- : undefined;
1484
- const batchSize = batch === undefined
1485
- ? undefined
1486
- : "disposition" in batch
1487
- ? batch.superseded.length
1488
- : batch.effects.length;
1489
- if (batchSize !== undefined && batchSize !== pending.length) {
1490
- throw new InvalidOperationResultError(`COPY/MOVE expected ${pending.length} receipt projections, got ${batchSize}.`);
1491
- }
1492
- if (single !== undefined && pending.length !== 1) {
1493
- throw new InvalidOperationResultError(`COPY/MOVE received one receipt for ${pending.length} resource effects.`);
1494
- }
1495
- let effects;
1496
- if (batch !== undefined && "disposition" in batch) {
1497
- const replacement = pending[0];
1498
- if (replacement === undefined) {
1499
- throw new InvalidOperationResultError("A reviewer-replaced COPY/MOVE batch has no resource effect.");
1500
- }
1501
- if (pending.some(({ target, action }) => target !== replacement.target || action !== replacement.action)) {
1502
- throw new InvalidOperationResultError("A reviewer-replaced COPY/MOVE batch spans incompatible resource effects.");
1503
- }
1504
- effects = [{
1505
- ...replacement,
1506
- receipt: projectEditReceipt(batch, 0),
1507
- }];
1508
- }
1509
- else {
1510
- effects = pending.map((effect, index) => ({
1511
- ...effect,
1512
- ...(batch !== undefined
1513
- ? { receipt: projectEditReceipt(batch, index) }
1514
- : single !== undefined
1515
- ? { receipt: single }
1516
- : {}),
1517
- }));
1518
- }
1519
- assertResourceEffects(effects);
1520
- const { editReceipt: _editReceipt, receipt: _receipt, ...withoutInternalReceipts } = exact;
1521
- return {
1522
- ...withoutInternalReceipts,
1523
- effects,
1524
- };
1525
- }
1526
- // {§copy-move-observation} {§edit-result-copy-move-effects} — scoped
1527
- // COPY/MOVE into an unscoped channel is a text materialization even when
1528
- // CRUD creates the channel wholesale. Carry the receipt through synchronous
1529
- // writes and proposals; the destination scheme owns reviewed output.
1530
- static #withEditMaterialization(result, receipt) {
1531
- const exact = Results.assert(result);
1532
- if (exact.status !== 200 && exact.status !== 201 && exact.status !== 202)
1533
- return exact;
1534
- const materialized = exact.editReceipt === undefined
1535
- ? assertEditBatchReceipt(receipt)
1536
- : assertEditBatchReceipt(exact.editReceipt);
1537
- return {
1538
- ...exact,
1539
- editReceipt: materialized,
1540
- ...(exact.status === 202
1541
- ? {
1542
- attrs: {
1543
- ...exact.attrs,
1544
- editReceipt: materialized,
1545
- },
1546
- }
1547
- : {}),
1548
- };
1549
- }
1550
- #finalizeEffects(result, selection, pending) {
1551
- const routed = this.#withProposalRoute(result, selection);
1552
- if (routed.status !== 202)
1553
- return _a.#appliedEffects(routed, pending);
1554
- return {
1555
- ...routed,
1556
- attrs: {
1557
- ...routed.attrs,
1558
- proposalEffects: pending,
1559
- },
1560
- };
1561
- }
1562
- static #effectsOf(result) {
1563
- return result.effects === undefined
1564
- ? []
1565
- : assertResourceEffects(result.effects);
1566
- }
1567
- static #withCombinedEffects(result, ...additional) {
1568
- const existing = _a.#effectsOf(result);
1569
- const effects = [...existing, ...additional.flat()];
1570
- const { effects: _effects, ...withoutEffects } = result;
1571
- return effects.length === 0
1572
- ? withoutEffects
1573
- : { ...withoutEffects, effects: assertResourceEffects(effects) };
1574
- }
1575
- static #settleProposalEffects(original, settlement) {
1576
- const pending = original.attrs
1577
- ?.proposalEffects;
1578
- if (pending === undefined
1579
- || settlement.resolution.decision !== "accept"
1580
- || settlement.applied === undefined
1581
- || settlement.applied.status >= 300) {
1582
- return settlement;
1583
- }
1584
- const projected = settlement.resolution.result ?? {};
1585
- const aggregate = settlement.applied.editReceipt;
1586
- const applied = _a.#appliedEffects({
1587
- ...projected,
1588
- status: settlement.applied.status,
1589
- ...(aggregate === undefined ? {} : { editReceipt: aggregate }),
1590
- }, pending);
1591
- const { status: _status, body: _body, ...result } = applied;
1592
- const { body: _resolutionBody, ...resolution } = settlement.resolution;
1593
- return {
1594
- ...settlement,
1595
- resolution: {
1596
- ...resolution,
1597
- result,
1598
- },
1599
- };
1600
- }
1601
- #recordEditSettlement(statement, settlement) {
1602
- if (statement.op !== "EDIT")
1603
- return;
1604
- const prepared = this.#preparedEdits.get(statement);
1605
- if (prepared === undefined || !prepared.first) {
1606
- throw new InvalidOperationResultError("An EDIT proposal settled without its prepared batch owner.");
1607
- }
1608
- const { resolution, applied } = settlement;
1609
- if (resolution.decision !== "accept"
1610
- || applied === undefined
1611
- || applied.status >= 300) {
1612
- prepared.batch.aggregate = undefined;
1613
- return;
1614
- }
1615
- if (applied.editReceipt === null) {
1616
- prepared.batch.aggregate = undefined;
1617
- return;
1618
- }
1619
- if (applied.editReceipt !== undefined) {
1620
- prepared.batch.aggregate = assertEditBatchReceipt(applied.editReceipt);
1621
- return;
1622
- }
1623
- if (resolution.body !== undefined)
1624
- prepared.batch.aggregate = undefined;
1625
- }
1626
- static #settlementEffects(settlement) {
1627
- const effects = settlement.resolution.result
1628
- ?.effects;
1629
- return effects === undefined ? [] : assertResourceEffects(effects);
1630
- }
1631
- static #withSettlementEffects(settlement, effects) {
1632
- const projected = (settlement.resolution.result ?? {});
1633
- const { effects: _effects, ...withoutEffects } = projected;
1634
- const result = effects.length === 0
1635
- ? withoutEffects
1636
- : {
1637
- ...withoutEffects,
1638
- effects: assertResourceEffects(effects),
1639
- };
1640
- return {
1641
- ...settlement,
1642
- resolution: {
1643
- ...settlement.resolution,
1644
- result,
1645
- },
1646
- };
1647
- }
1648
- #sameChannel(left, right) {
1649
- return left.scheme === right.scheme
1650
- && left.identityPathname === right.identityPathname
1651
- && left.channel === right.channel;
1652
- }
1653
- #withProposalRoute(result, selection) {
1654
- if (result.status !== 202)
1655
- return result;
1656
- return {
1657
- ...result,
1658
- attrs: {
1659
- ...result.attrs,
1660
- proposalScheme: selection.scheme,
1661
- proposalTarget: {
1662
- scheme: selection.scheme,
1663
- pathname: selection.identityPathname,
1664
- },
1665
- },
1666
- };
1667
- }
1668
- async #invokeEditBatch(selection, edits, ctx, precondition = null) {
1669
- const handler = this.#schemes.get(selection.scheme);
1670
- if (typeof handler?.editBatch !== "function") {
1671
- return _a.#failure("operation-not-implemented", 501, `Scheme '${selection.scheme}' does not implement EDIT batches.`, {}, {
1672
- scheme: selection.scheme,
1673
- operation: "EDIT",
1674
- retryable: false,
1675
- });
1676
- }
1677
- const statements = edits.map(({ marker, body, position }) => ({
1678
- op: "EDIT",
1679
- suffix: "",
1680
- signal: null,
1681
- target: selection.target,
1682
- lineMarker: marker,
1683
- body,
1684
- position,
1685
- }));
1686
- const addressedScheme = selection.target.kind === "url"
1687
- ? selection.target.scheme
1688
- : selection.scheme;
1689
- try {
1690
- const result = Results.assert(await handler.editBatch(statements, new SchemeCtxImpl(ctx, addressedScheme, selection.manifest, this.#liveSubscriptions, {
1691
- publishedChannel: selection.channel,
1692
- editPrecondition: precondition,
1693
- })));
1694
- return this.#withProposalRoute(result, selection);
1695
- }
1696
- catch (err) {
1697
- if (err instanceof InvalidOperationResultError)
1698
- throw err;
1699
- console.error(`Scheme '${selection.scheme}' COPY/MOVE edit threw outside its operation result contract:`, err);
1700
- return _a.#failure("scheme-handler-threw", 500, `The '${selection.scheme}' scheme did not produce a COPY/MOVE edit result.`, {}, {
1701
- stage: "scheme-dispatch",
1702
- scheme: selection.scheme,
1703
- operation: "EDIT",
1704
- });
1705
- }
1706
- }
1707
- async #writeDestination(statement, source, destination, ctx) {
1708
- const existingResult = await this.#readEntry(destination.scheme, destination.pathname, ctx);
1709
- if (existingResult.status >= 400 && existingResult.status !== 404) {
1710
- return existingResult;
1711
- }
1712
- const existing = existingResult.status === 200
1713
- ? existingResult.entry
1714
- : null;
1715
- if (existingResult.status === 200 && existing === null) {
1716
- throw new InvalidOperationResultError(`The '${destination.scheme}' scheme returned 200 without a destination entry.`);
1717
- }
1718
- const destinationChannel = existing?.channels[destination.channel];
1719
- const expectedMimetype = destinationChannel?.mimetype
1720
- ?? await PathMimetype.resolveEntryMimetype(destination.pathname, destination.manifest.channels[destination.channel] ?? source.mimetype, ctx.mimetypes);
1721
- if (source.mimetype !== expectedMimetype) {
1722
- return _a.#failure("mimetype-mismatch", 415, `COPY or MOVE cannot write '${source.mimetype}' into a '${expectedMimetype}' channel.`, {}, {
1723
- channel: destination.channel,
1724
- sourceMimetype: source.mimetype,
1725
- destinationMimetype: expectedMimetype,
1726
- retryable: false,
1727
- });
1728
- }
1729
- const destinationEffect = this.#pendingEffect(destination, destinationChannel === undefined ? "create" : "update");
1730
- if (destination.lineMarker !== null) {
1731
- if (destinationChannel === undefined) {
1732
- return _a.#failure("destination-region-not-found", 404, `A destination region requires an existing #${destination.channel} channel.`, {}, {
1733
- destination: this.#resourceAddress(destination),
1734
- retryable: false,
1735
- });
1736
- }
1737
- if (await MimetypeBinary.isBinaryMimetype(destinationChannel.mimetype, ctx.mimetypes)) {
1738
- return _a.#failure("binary-region-unsupported", 415, `Channel #${destination.channel} is binary and cannot receive a textual region.`, {}, {
1739
- channel: destination.channel,
1740
- mimetype: destinationChannel.mimetype,
1741
- retryable: false,
1742
- });
1743
- }
1744
- const resolvedMarker = this.#resolveResourceLineMarker(destination, destinationChannel.content, statement.op);
1745
- if ("result" in resolvedMarker)
1746
- return resolvedMarker.result;
1747
- const edited = await this.#invokeEditBatch(resolvedMarker.selection, [{
1748
- marker: resolvedMarker.selection.lineMarker,
1749
- body: source.content,
1750
- position: statement.position,
1751
- }], ctx, resolvedMarker.precondition);
1752
- return this.#finalizeEffects(edited, resolvedMarker.selection, [destinationEffect]);
1753
- }
1754
- if (destinationChannel !== undefined
1755
- && destinationChannel.content !== source.content) {
1756
- return _a.#failure("copy-destination-exists", 409, `COPY or MOVE destination ${this.#resourceAddress(destination)} already contains different content.`, {}, {
1757
- destination: this.#resourceAddress(destination),
1758
- retryable: false,
1759
- });
1760
- }
1761
- if (destinationChannel !== undefined)
1762
- return { status: 304 };
1763
- const channels = {
1764
- ...(existing?.channels ?? {}),
1765
- [destination.channel]: {
1766
- content: source.content,
1767
- mimetype: source.mimetype,
1768
- },
1769
- };
1770
- const written = await this.#writeEntry(destination.scheme, destination.pathname, { channels }, ctx);
1771
- const exactWritten = Results.assert(written);
1772
- const parseIssues = exactWritten.status === 200 || exactWritten.status === 201
1773
- ? await new DbProjectionCaps(ctx).parseIssues(source.content, source.mimetype)
1774
- : undefined;
1775
- const materialized = source.lineMarker === null
1776
- || (exactWritten.status !== 200 && exactWritten.status !== 201 && exactWritten.status !== 202)
1777
- ? exactWritten
1778
- : _a.#withEditMaterialization(exactWritten, editReceipt("", source.content, [{
1779
- marker: { marks: [1, -1] },
1780
- body: source.content,
1781
- }], parseIssues));
1782
- return this.#finalizeEffects(materialized, destination, [destinationEffect]);
1783
- }
1784
- async #copyOrchestration({ statement, source, destination, ctx, }) {
1785
- const resolvedSource = await this.#resolveResourceSelection(source, ctx);
1786
- if (_a.#isDispatchResult(resolvedSource))
1787
- return resolvedSource;
1788
- const resolvedDestination = await this.#resolveResourceSelection(destination, ctx);
1789
- if (_a.#isDispatchResult(resolvedDestination))
1790
- return resolvedDestination;
1791
- const selected = await this.#selectSource(resolvedSource, ctx, "COPY");
1792
- if (_a.#isDispatchResult(selected))
1793
- return selected;
1794
- const result = await this.#writeDestination(statement, selected, resolvedDestination, ctx);
1795
- return _a.#prependScopeNormalizations(result, selected.scopeNormalizations);
1796
- }
1797
- #deferredMoveSource(source, destination, lineAnchorPrecondition) {
1798
- return {
1799
- target: source.target,
1800
- lineMarker: source.lineMarker,
1801
- scheme: source.scheme,
1802
- pathname: source.pathname,
1803
- channel: source.channel,
1804
- destination: this.#resourceAddress(destination),
1805
- lineAnchorPrecondition,
1806
- };
1807
- }
1808
- #moveFailureAfterDestination(result, destination, destinationEffects) {
1809
- const exact = Results.assert(result);
1810
- if (exact.status < 400) {
1811
- throw new InvalidOperationResultError("A successful MOVE source result was classified as a partial failure.");
1812
- }
1813
- if (exact.problem === undefined) {
1814
- throw new InvalidOperationResultError("A failed MOVE source mutation has no Problem Details.");
1815
- }
1816
- const failed = Results.assert({
1817
- ...exact,
1818
- problem: {
1819
- ...exact.problem,
1820
- operation: "MOVE",
1821
- destinationWritten: true,
1822
- destination,
1823
- },
1824
- });
1825
- return _a.#withCombinedEffects(failed, destinationEffects);
1826
- }
1827
- async #removeMoveSource(statement, source, ctx, lineAnchorPrecondition = null) {
1828
- const effect = this.#pendingEffect(source, source.lineMarker === null ? "delete" : "update");
1829
- if (source.lineMarker === null) {
1830
- const deleted = await this.#deleteChannel(source.scheme, source.pathname, source.channel, ctx);
1831
- return this.#finalizeEffects(Results.assert(deleted), source, [effect]);
1832
- }
1833
- const edited = await this.#invokeEditBatch(source, [{
1834
- marker: source.lineMarker,
1835
- body: "",
1836
- position: statement.position,
1837
- }], ctx, lineAnchorPrecondition);
1838
- return this.#finalizeEffects(edited, source, [effect]);
1839
- }
1840
- async #moveWithinChannel(statement, source, destination, ctx) {
1841
- if (source.lineMarker === null) {
1842
- if (destination.lineMarker !== null) {
1843
- return _a.#failure("move-region-overlap", 409, "MOVE cannot insert a whole channel into itself and then remove that channel.", {}, {
1844
- source: this.#resourceAddress(source),
1845
- destination: this.#resourceAddress(destination),
1846
- retryable: false,
1847
- });
1848
- }
1849
- return this.#writeDestination(statement, source, destination, ctx);
1850
- }
1851
- if (destination.lineMarker === null) {
1852
- return this.#writeDestination(statement, source, destination, ctx);
1853
- }
1854
- const resolvedDestination = this.#resolveResourceLineMarker(destination, source.completeContent, "MOVE");
1855
- if ("result" in resolvedDestination)
1856
- return resolvedDestination.result;
1857
- const precondition = _a.#mergeLineAnchorPreconditions(source.lineAnchorPrecondition, resolvedDestination.precondition);
1858
- const moved = await this.#invokeEditBatch(resolvedDestination.selection, [
1859
- {
1860
- marker: resolvedDestination.selection.lineMarker,
1861
- body: source.content,
1862
- position: statement.position,
1863
- },
1864
- {
1865
- marker: source.lineMarker,
1866
- body: "",
1867
- position: statement.position,
1868
- },
1869
- ], ctx, precondition);
1870
- const effect = this.#pendingEffect(resolvedDestination.selection, "update");
1871
- return this.#finalizeEffects(moved, resolvedDestination.selection, [effect, effect]);
1872
- }
1873
- async #moveOrchestration({ statement, source, destination, ctx, }) {
1874
- const resolvedSource = await this.#resolveResourceSelection(source, ctx);
1875
- if (_a.#isDispatchResult(resolvedSource))
1876
- return resolvedSource;
1877
- const resolvedDestination = await this.#resolveResourceSelection(destination, ctx);
1878
- if (_a.#isDispatchResult(resolvedDestination))
1879
- return resolvedDestination;
1880
- const selected = await this.#selectSource(resolvedSource, ctx, "MOVE");
1881
- if (_a.#isDispatchResult(selected))
1882
- return selected;
1883
- if (this.#sameChannel(resolvedSource, resolvedDestination)) {
1884
- const result = await this.#moveWithinChannel(statement, selected, resolvedDestination, ctx);
1885
- return resolvedDestination.lineMarker === null
1886
- ? _a.#prependScopeNormalizations(result, selected.scopeNormalizations)
1887
- : result;
1888
- }
1889
- const destinationResult = _a.#prependScopeNormalizations(await this.#writeDestination(statement, selected, resolvedDestination, ctx), selected.scopeNormalizations);
1890
- if (destinationResult.status >= 400)
1891
- return destinationResult;
1892
- const destinationAddress = this.#resourceAddress(resolvedDestination);
1893
- const destinationEffects = _a.#effectsOf(destinationResult);
1894
- if (destinationResult.status === 202) {
1895
- return {
1896
- ...destinationResult,
1897
- attrs: {
1898
- ...destinationResult.attrs,
1899
- moveSource: this.#deferredMoveSource(selected, resolvedDestination, selected.lineAnchorPrecondition),
1900
- },
1901
- };
1902
- }
1903
- const sourceResult = await this.#removeMoveSource(statement, selected, ctx, selected.lineAnchorPrecondition);
1904
- if (sourceResult.status === 202) {
1905
- return {
1906
- ...sourceResult,
1907
- ...(destinationResult.scopeNormalizations === undefined
1908
- ? {}
1909
- : { scopeNormalizations: destinationResult.scopeNormalizations }),
1910
- attrs: {
1911
- ...sourceResult.attrs,
1912
- moveDestinationWritten: destinationAddress,
1913
- moveDestinationEffects: destinationEffects,
1914
- },
1915
- };
1916
- }
1917
- if (sourceResult.status >= 400) {
1918
- return this.#moveFailureAfterDestination(destinationResult.scopeNormalizations === undefined
1919
- ? sourceResult
1920
- : Results.assert({
1921
- ...sourceResult,
1922
- scopeNormalizations: destinationResult.scopeNormalizations,
1923
- }), destinationAddress, destinationEffects);
1924
- }
1925
- const base = destinationResult.status === 304
1926
- ? { ...destinationResult, status: 200 }
1927
- : destinationResult;
1928
- return _a.#withCombinedEffects(base, _a.#effectsOf(sourceResult));
1929
- }
1930
- async #settleMoveProposal({ statement, result, settlement, ctx, ids, }) {
1931
- if (statement.op !== "MOVE")
1932
- return settlement;
1933
- const attrs = result.attrs;
1934
- const destinationWritten = attrs?.moveDestinationWritten;
1935
- if (destinationWritten !== undefined) {
1936
- const destinationEffects = attrs?.moveDestinationEffects === undefined
1937
- ? []
1938
- : assertResourceEffects(attrs.moveDestinationEffects);
1939
- if (settlement.resolution.decision !== "accept") {
1940
- const decision = settlement.resolution.decision;
1941
- return {
1942
- resolution: settlement.resolution,
1943
- applied: this.#moveFailureAfterDestination(_a.#failure("move-source-not-applied", decision === "cancel" ? 499 : 409, `The MOVE destination was written, but source removal was ${decision === "cancel" ? "cancelled" : "rejected"}.`, {}, {
1944
- retryable: false,
1945
- }), destinationWritten, destinationEffects),
1946
- };
1947
- }
1948
- if (settlement.applied === undefined) {
1949
- return {
1950
- resolution: settlement.resolution,
1951
- applied: this.#moveFailureAfterDestination(_a.#failure("proposal-apply-missing", 500, "The source scheme accepted its MOVE proposal without applying the source mutation.", {}, {
1952
- stage: "proposal-application",
1953
- retryable: false,
1954
- }), destinationWritten, destinationEffects),
1955
- };
1956
- }
1957
- if (settlement.applied.status >= 400) {
1958
- return {
1959
- resolution: settlement.resolution,
1960
- applied: this.#moveFailureAfterDestination(settlement.applied, destinationWritten, destinationEffects),
1961
- };
1962
- }
1963
- return _a.#withSettlementEffects(settlement, [
1964
- ...destinationEffects,
1965
- ..._a.#settlementEffects(settlement),
1966
- ]);
1967
- }
1968
- const deferred = attrs?.moveSource;
1969
- if (deferred === undefined
1970
- || settlement.resolution.decision !== "accept"
1971
- || (settlement.applied?.status ?? 200) >= 400) {
1972
- return settlement;
1973
- }
1974
- if (settlement.applied === undefined) {
1975
- return {
1976
- resolution: settlement.resolution,
1977
- applied: _a.#failure("proposal-apply-missing", 500, "The destination scheme accepted its MOVE proposal without applying the destination mutation.", {}, {
1978
- stage: "proposal-application",
1979
- retryable: false,
1980
- }),
1981
- };
1982
- }
1983
- const destinationEffects = _a.#settlementEffects(settlement);
1984
- const resolvedSource = await this.#resolveResourceSelection({
1985
- target: deferred.target,
1986
- lineMarker: deferred.lineMarker,
1987
- }, ctx);
1988
- if (_a.#isDispatchResult(resolvedSource)) {
1989
- return {
1990
- resolution: settlement.resolution,
1991
- applied: this.#moveFailureAfterDestination(resolvedSource, deferred.destination, destinationEffects),
1992
- };
1993
- }
1994
- if (resolvedSource.scheme !== deferred.scheme
1995
- || resolvedSource.pathname !== deferred.pathname
1996
- || resolvedSource.channel !== deferred.channel) {
1997
- throw new InvalidOperationResultError("A deferred MOVE source no longer resolves to its recorded identity.");
1998
- }
1999
- const removed = await this.#removeMoveSource(statement, {
2000
- ...resolvedSource,
2001
- lineMarker: resolvedSource.lineMarker,
2002
- }, ctx, deferred.lineAnchorPrecondition);
2003
- if (removed.status >= 400) {
2004
- return {
2005
- resolution: settlement.resolution,
2006
- applied: this.#moveFailureAfterDestination(removed, deferred.destination, destinationEffects),
2007
- };
2008
- }
2009
- if (removed.status !== 202) {
2010
- return _a.#withSettlementEffects(settlement, [
2011
- ...destinationEffects,
2012
- ..._a.#effectsOf(removed),
2013
- ]);
2014
- }
2015
- const initialSourceSettlement = await this.#proposals.workerApply(statement, removed, { decision: "accept" }, ids);
2016
- const sourceSettlement = _a.#settleProposalEffects(removed, initialSourceSettlement);
2017
- if (sourceSettlement.applied === undefined) {
2018
- return {
2019
- resolution: settlement.resolution,
2020
- applied: this.#moveFailureAfterDestination(_a.#failure("proposal-apply-missing", 500, "The source scheme accepted its MOVE proposal without applying the source mutation.", {}, {
2021
- stage: "proposal-application",
2022
- retryable: false,
2023
- }), deferred.destination, destinationEffects),
2024
- };
2025
- }
2026
- if (sourceSettlement.applied.status >= 400) {
2027
- return {
2028
- resolution: settlement.resolution,
2029
- applied: this.#moveFailureAfterDestination(sourceSettlement.applied, deferred.destination, destinationEffects),
2030
- };
2031
- }
2032
- return _a.#withSettlementEffects(settlement, [
2033
- ...destinationEffects,
2034
- ..._a.#settlementEffects(sourceSettlement),
2035
- ]);
2036
- }
2037
994
  // {§send-premature-terminate} — the unified PENDING SET, judged at the terminal's OWN dispatch
2038
995
  // (post-batch: the emission's earlier ops already executed, so a same-turn KILL+[200] repairs in
2039
996
  // ONE turn, and a same-turn WORK+[200] is caught — the spawn is live by the time the SEND lands).
@@ -2262,8 +1219,8 @@ class Dispatcher {
2262
1219
  content: null,
2263
1220
  mimetype: null,
2264
1221
  results: [],
2265
- itemsTokenTotal: 0,
2266
- returnedItemsTokenTotal: 0,
1222
+ itemsWeightTotal: 0,
1223
+ returnedItemsWeightTotal: 0,
2267
1224
  matchingPathCount: 0,
2268
1225
  matchLocationCount: 0,
2269
1226
  }
@@ -2406,8 +1363,8 @@ class Dispatcher {
2406
1363
  content: null,
2407
1364
  mimetype: null,
2408
1365
  results: [],
2409
- itemsTokenTotal: 0,
2410
- returnedItemsTokenTotal: 0,
1366
+ itemsWeightTotal: 0,
1367
+ returnedItemsWeightTotal: 0,
2411
1368
  matchingPathCount: 0,
2412
1369
  matchLocationCount: 0,
2413
1370
  }, { target });
@@ -2558,7 +1515,14 @@ class Dispatcher {
2558
1515
  rx: rxJson,
2559
1516
  mimetype_rx: "application/json",
2560
1517
  status_rx: result.status,
2561
- tokens: this.#tokenize(txJson) + this.#tokenize(rxJson),
1518
+ weight: LogBody.weight({
1519
+ op: durableStatement.op,
1520
+ attrs,
1521
+ tx: txJson,
1522
+ rx: rxJson,
1523
+ mimetypeTx: "application/json",
1524
+ mimetypeRx: "application/json",
1525
+ }, this.#weighContent),
2562
1526
  state: isProposed ? "proposed" : "resolved",
2563
1527
  outcome: null,
2564
1528
  attrs,