@plurnk/plurnk-service 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/.env.defaults +22 -17
  2. package/INSTALL.md +20 -1
  3. package/SPEC.md +333 -88
  4. package/dist/build-info.json +1 -1
  5. package/dist/completions/_plurnk-service +1 -1
  6. package/dist/completions/plurnk-service.bash +1 -1
  7. package/dist/completions/plurnk-service.fish +1 -0
  8. package/dist/content/path-mimetype.d.ts.map +1 -1
  9. package/dist/content/path-mimetype.js +2 -8
  10. package/dist/content/path-mimetype.js.map +1 -1
  11. package/dist/core/AdmittedTurnExecutor.d.ts.map +1 -1
  12. package/dist/core/AdmittedTurnExecutor.js +22 -10
  13. package/dist/core/AdmittedTurnExecutor.js.map +1 -1
  14. package/dist/{schemes/_derivation-use.d.ts → core/ContentUse.d.ts} +3 -3
  15. package/dist/core/ContentUse.d.ts.map +1 -0
  16. package/dist/{schemes/_derivation-use.js → core/ContentUse.js} +8 -7
  17. package/dist/core/ContentUse.js.map +1 -0
  18. package/dist/core/DataStatementRunner.d.ts.map +1 -1
  19. package/dist/core/DataStatementRunner.js +8 -20
  20. package/dist/core/DataStatementRunner.js.map +1 -1
  21. package/dist/core/Dispatcher.d.ts +9 -1
  22. package/dist/core/Dispatcher.d.ts.map +1 -1
  23. package/dist/core/Dispatcher.js +144 -71
  24. package/dist/core/Dispatcher.js.map +1 -1
  25. package/dist/core/EditMutations.js +1 -1
  26. package/dist/core/EditMutations.js.map +1 -1
  27. package/dist/core/Engine.d.ts +1 -1
  28. package/dist/core/Engine.d.ts.map +1 -1
  29. package/dist/core/Engine.js +3 -3
  30. package/dist/core/Engine.js.map +1 -1
  31. package/dist/core/FileAddress.d.ts +12 -0
  32. package/dist/core/FileAddress.d.ts.map +1 -0
  33. package/dist/core/FileAddress.js +73 -0
  34. package/dist/core/FileAddress.js.map +1 -0
  35. package/dist/core/FilePublication.d.ts +15 -0
  36. package/dist/core/FilePublication.d.ts.map +1 -0
  37. package/dist/core/FilePublication.js +143 -0
  38. package/dist/core/FilePublication.js.map +1 -0
  39. package/dist/core/FileSnapshot.d.ts +26 -0
  40. package/dist/core/FileSnapshot.d.ts.map +1 -0
  41. package/dist/core/FileSnapshot.js +179 -0
  42. package/dist/core/FileSnapshot.js.map +1 -0
  43. package/dist/core/KillHandler.d.ts.map +1 -1
  44. package/dist/core/KillHandler.js +2 -1
  45. package/dist/core/KillHandler.js.map +1 -1
  46. package/dist/core/MembershipMaterialization.d.ts.map +1 -1
  47. package/dist/core/MembershipMaterialization.js +87 -143
  48. package/dist/core/MembershipMaterialization.js.map +1 -1
  49. package/dist/core/MutationEffects.d.ts +2 -1
  50. package/dist/core/MutationEffects.d.ts.map +1 -1
  51. package/dist/core/MutationEffects.js +14 -1
  52. package/dist/core/MutationEffects.js.map +1 -1
  53. package/dist/core/PacketBuilder.sql +8 -0
  54. package/dist/core/PathGlob.d.ts +6 -0
  55. package/dist/core/PathGlob.d.ts.map +1 -0
  56. package/dist/core/PathGlob.js +24 -0
  57. package/dist/core/PathGlob.js.map +1 -0
  58. package/dist/core/ProposalLifecycle.d.ts +2 -1
  59. package/dist/core/ProposalLifecycle.d.ts.map +1 -1
  60. package/dist/core/ProposalLifecycle.js +45 -33
  61. package/dist/core/ProposalLifecycle.js.map +1 -1
  62. package/dist/core/ProposalLifecycle.sql +4 -0
  63. package/dist/core/ResourceBindings.d.ts.map +1 -1
  64. package/dist/core/ResourceBindings.js +4 -3
  65. package/dist/core/ResourceBindings.js.map +1 -1
  66. package/dist/core/ResourceSelector.d.ts +1 -2
  67. package/dist/core/ResourceSelector.d.ts.map +1 -1
  68. package/dist/core/ResourceSelector.js +30 -9
  69. package/dist/core/ResourceSelector.js.map +1 -1
  70. package/dist/core/ResourceTransfers.d.ts +1 -1
  71. package/dist/core/ResourceTransfers.d.ts.map +1 -1
  72. package/dist/core/ResourceTransfers.js +26 -11
  73. package/dist/core/ResourceTransfers.js.map +1 -1
  74. package/dist/core/TurnRunner.d.ts.map +1 -1
  75. package/dist/core/TurnRunner.js +6 -4
  76. package/dist/core/TurnRunner.js.map +1 -1
  77. package/dist/core/caps/SchemeCtxImpl.d.ts +1 -0
  78. package/dist/core/caps/SchemeCtxImpl.d.ts.map +1 -1
  79. package/dist/core/caps/SchemeCtxImpl.js +5 -0
  80. package/dist/core/caps/SchemeCtxImpl.js.map +1 -1
  81. package/dist/core/git-membership.d.ts +1 -0
  82. package/dist/core/git-membership.d.ts.map +1 -1
  83. package/dist/core/git-membership.js +9 -11
  84. package/dist/core/git-membership.js.map +1 -1
  85. package/dist/core/glob_match.js +3 -3
  86. package/dist/core/glob_match.js.map +1 -1
  87. package/dist/core/mutation-types.d.ts +3 -0
  88. package/dist/core/mutation-types.d.ts.map +1 -1
  89. package/dist/core/namespace.d.ts +4 -1
  90. package/dist/core/namespace.d.ts.map +1 -1
  91. package/dist/core/namespace.js +14 -3
  92. package/dist/core/namespace.js.map +1 -1
  93. package/dist/core/packet-wire.d.ts.map +1 -1
  94. package/dist/core/packet-wire.js +11 -9
  95. package/dist/core/packet-wire.js.map +1 -1
  96. package/dist/core/plurnk-uri.d.ts +3 -2
  97. package/dist/core/plurnk-uri.d.ts.map +1 -1
  98. package/dist/core/plurnk-uri.js +23 -11
  99. package/dist/core/plurnk-uri.js.map +1 -1
  100. package/dist/core/scheme-types.d.ts +1 -0
  101. package/dist/core/scheme-types.d.ts.map +1 -1
  102. package/dist/core/sqlite-synchronous.d.ts +2 -0
  103. package/dist/core/sqlite-synchronous.d.ts.map +1 -0
  104. package/dist/core/sqlite-synchronous.js +4 -0
  105. package/dist/core/sqlite-synchronous.js.map +1 -0
  106. package/dist/evidence/EvidenceReader.d.ts.map +1 -1
  107. package/dist/evidence/EvidenceReader.js +8 -1
  108. package/dist/evidence/EvidenceReader.js.map +1 -1
  109. package/dist/evidence/digest.sql +6 -1
  110. package/dist/man/plurnk-service.1 +32 -21
  111. package/dist/schemes/Env.d.ts +1 -1
  112. package/dist/schemes/Env.d.ts.map +1 -1
  113. package/dist/schemes/Env.js +2 -2
  114. package/dist/schemes/Env.js.map +1 -1
  115. package/dist/schemes/File.d.ts +1 -0
  116. package/dist/schemes/File.d.ts.map +1 -1
  117. package/dist/schemes/File.js +233 -268
  118. package/dist/schemes/File.js.map +1 -1
  119. package/dist/schemes/Log.d.ts.map +1 -1
  120. package/dist/schemes/Log.js +64 -7
  121. package/dist/schemes/Log.js.map +1 -1
  122. package/dist/schemes/Worker.js +1 -1
  123. package/dist/schemes/Worker.js.map +1 -1
  124. package/dist/schemes/_entry-crud.d.ts +2 -0
  125. package/dist/schemes/_entry-crud.d.ts.map +1 -1
  126. package/dist/schemes/_entry-crud.js +8 -5
  127. package/dist/schemes/_entry-crud.js.map +1 -1
  128. package/dist/schemes/_entry-crud.sql +15 -15
  129. package/dist/schemes/_entry-find.d.ts +1 -0
  130. package/dist/schemes/_entry-find.d.ts.map +1 -1
  131. package/dist/schemes/_entry-find.js +15 -10
  132. package/dist/schemes/_entry-find.js.map +1 -1
  133. package/dist/schemes/_entry-manifest.d.ts +2 -2
  134. package/dist/schemes/_entry-manifest.d.ts.map +1 -1
  135. package/dist/schemes/_entry-manifest.js +7 -5
  136. package/dist/schemes/_entry-manifest.js.map +1 -1
  137. package/dist/schemes/_entry-ops.d.ts.map +1 -1
  138. package/dist/schemes/_entry-ops.js +9 -7
  139. package/dist/schemes/_entry-ops.js.map +1 -1
  140. package/dist/schemes/_path-scope.d.ts.map +1 -1
  141. package/dist/schemes/_path-scope.js +12 -4
  142. package/dist/schemes/_path-scope.js.map +1 -1
  143. package/dist/schemes/_search-index.js +3 -3
  144. package/dist/schemes/_search-index.js.map +1 -1
  145. package/dist/schemes/exec-env.d.ts.map +1 -1
  146. package/dist/schemes/exec-env.js +7 -22
  147. package/dist/schemes/exec-env.js.map +1 -1
  148. package/dist/server/Daemon.d.ts.map +1 -1
  149. package/dist/server/Daemon.js +40 -68
  150. package/dist/server/Daemon.js.map +1 -1
  151. package/dist/server/DrainSupervisor.d.ts +1 -3
  152. package/dist/server/DrainSupervisor.d.ts.map +1 -1
  153. package/dist/server/DrainSupervisor.js +30 -5
  154. package/dist/server/DrainSupervisor.js.map +1 -1
  155. package/dist/server/EnvFunctionality.d.ts.map +1 -1
  156. package/dist/server/EnvFunctionality.js +19 -11
  157. package/dist/server/EnvFunctionality.js.map +1 -1
  158. package/dist/server/Functionality.d.ts.map +1 -1
  159. package/dist/server/Functionality.js +15 -8
  160. package/dist/server/Functionality.js.map +1 -1
  161. package/dist/server/FunctionalityManager.d.ts +1 -1
  162. package/dist/server/FunctionalityManager.d.ts.map +1 -1
  163. package/dist/server/FunctionalityManager.js +5 -1
  164. package/dist/server/FunctionalityManager.js.map +1 -1
  165. package/dist/server/MembersFunctionality.d.ts.map +1 -1
  166. package/dist/server/MembersFunctionality.js +12 -12
  167. package/dist/server/MembersFunctionality.js.map +1 -1
  168. package/dist/server/Retention.d.ts.map +1 -1
  169. package/dist/server/Retention.js +5 -5
  170. package/dist/server/Retention.js.map +1 -1
  171. package/dist/server/Retention.sql +49 -23
  172. package/dist/server/lifecycle-recovery.sql +4 -1
  173. package/dist/service.d.ts.map +1 -1
  174. package/dist/service.js +4 -0
  175. package/dist/service.js.map +1 -1
  176. package/docs/copy-move.md +3 -0
  177. package/docs/env.md +12 -6
  178. package/docs/members.md +16 -1
  179. package/migrations/023_file_revisions.sql +54 -0
  180. package/migrations/024_proposal_decisions.sql +34 -0
  181. package/migrations/025_response_retention.sql +30 -0
  182. package/package.json +33 -31
  183. package/dist/schemes/_derivation-use.d.ts.map +0 -1
  184. package/dist/schemes/_derivation-use.js.map +0 -1
@@ -3,10 +3,11 @@ import { parsePath } from "@plurnk/plurnk-parser";
3
3
  import { ConfigurationError, Knob } from "@plurnk/plurnk-meta";
4
4
  import { TurnDisposition, UNKNOWN_POSITION } from "@plurnk/plurnk-contracts";
5
5
  import Turn from "./Turn.js";
6
- import { foldAuthorityIntoPath, renderAddress, renderTarget, schemeNameOf } from "./plurnk-uri.js";
6
+ import { entryAddress, foldAuthorityIntoPath, renderAddress, renderAuthoredTarget, renderTarget, schemeNameOf } from "./plurnk-uri.js";
7
7
  import WorkerName from "./WorkerName.js";
8
8
  import { PathSyntax } from "@plurnk/plurnk-contracts";
9
9
  import Namespace from "./namespace.js";
10
+ import FileAddress from "./FileAddress.js";
10
11
  import CapabilityResolver from "./CapabilityResolver.js";
11
12
  import SchemeCtxImpl from "./caps/SchemeCtxImpl.js";
12
13
  import Results from "./results.js";
@@ -22,7 +23,7 @@ import BodyPreview from "../content/body-preview.js";
22
23
  import ReadResolve from "../content/read-resolve.js";
23
24
  import LogVisibility from "./LogVisibility.js";
24
25
  import PacketWire from "./packet-wire.js";
25
- import { resultPrefix, resultSize, unfitResult } from "./ContextFit.js";
26
+ import { isContextReceipt, reserved, resultPrefix, resultSize, unfitResult } from "./ContextFit.js";
26
27
  import { TextCoordinates } from "@plurnk/plurnk-mimetypes";
27
28
  import EntryAddressBinding from "./EntryAddressBinding.js";
28
29
  import WorkerControlHandler from "./WorkerControlHandler.js";
@@ -91,7 +92,6 @@ class Dispatcher {
91
92
  this.#resourceSelector = new ResourceSelector({
92
93
  schemes,
93
94
  admitRead: (statement, ctx) => this.#checkCapabilities(statement, ctx),
94
- canonicalFilePath: (pathname, workspaceId) => this.#canonicalFilePath(pathname, workspaceId),
95
95
  prepareDataRepresentation: (args) => this.#prepareDataRepresentation({
96
96
  ...args,
97
97
  handler: args.handler,
@@ -247,13 +247,10 @@ class Dispatcher {
247
247
  if (key !== null)
248
248
  target.pathname = key;
249
249
  }
250
- async #canonicalFilePath(pathname, workspaceId) {
251
- return Namespace.canonicalizeSpelling(pathname, await this.#workspaceRoot(workspaceId));
252
- }
253
250
  async #editTargetIdentity(statement, workspaceId, _workerId) {
254
251
  const target = this.#extractTarget(statement.target, workspaceId);
255
252
  await this.#canonColumns(target, workspaceId);
256
- return renderTarget(target);
253
+ return renderTarget(target, await this.#workspaceRoot(workspaceId));
257
254
  }
258
255
  // {§kill-scope-entry} — a scoped KILL on an entry-bearing scheme is one EDIT with an empty
259
256
  // body over the same marker: prepared, gated, and merged as an EDIT while its log row
@@ -295,9 +292,9 @@ class Dispatcher {
295
292
  async dispatch(context) {
296
293
  await this.#operationEvent(context, "started");
297
294
  await this.#noticeFilePaths(context);
298
- const result = await this.#dispatch(context);
299
- await this.#operationEvent(context, "settled", result);
300
- return result;
295
+ const execution = await this.#dispatch(context);
296
+ await this.#operationEvent(context, "settled", execution.result);
297
+ return execution;
301
298
  }
302
299
  // {§file-path-normalization} Authored operands, once, before internal fan-out.
303
300
  async #noticeFilePaths({ statement, origin, workspaceId, workerId, loopId }) {
@@ -306,22 +303,29 @@ class Dispatcher {
306
303
  const targets = statement.op === "COPY" || statement.op === "MOVE"
307
304
  ? [statement.source.target, statement.destination.target]
308
305
  : [statement.target];
309
- const absolute = targets.flatMap((target) => target === null || schemeNameOf(target) !== "file" ? []
310
- : [PathSyntax.decodeParens(target.kind === "url" ? target.pathname : target.raw)])
311
- .filter((pathname) => pathname.startsWith("/"));
312
- if (absolute.length === 0)
306
+ const normalized = targets.flatMap((target) => {
307
+ if (target === null || schemeNameOf(target) !== "file")
308
+ return [];
309
+ const pathname = FileAddress.pathname(target);
310
+ return typeof pathname === "string" && (pathname.startsWith("/") || Namespace.isHomeRelative(pathname))
311
+ ? [{ target, pathname }] : [];
312
+ });
313
+ if (normalized.length === 0)
313
314
  return;
314
315
  const root = await this.#workspaceRoot(workspaceId);
315
316
  if (root === null || root === "/")
316
317
  return;
317
- for (const pathname of absolute) {
318
+ for (const { target, pathname } of normalized) {
318
319
  const key = Namespace.canonicalizeSpelling(pathname, root);
319
320
  if (key === null)
320
321
  continue;
322
+ const address = renderTarget({ scheme: null, pathname: key }, root, target);
323
+ if (address?.startsWith("file://"))
324
+ continue;
321
325
  this.#notices.push(workspaceId, workerId, loopId, {
322
326
  source: "scheme:file", kind: "path_normalized", level: "warn",
323
327
  directive: "YOU SHOULD address the resolved path.",
324
- message: `Path resolved to '${key || "."}'.`,
328
+ message: `Path resolved to '${address || "."}'.`,
325
329
  });
326
330
  }
327
331
  }
@@ -336,11 +340,9 @@ class Dispatcher {
336
340
  ...(result === undefined ? {} : { result }),
337
341
  });
338
342
  }
339
- async #dispatch(context) {
340
- if (context.statement.op === "READ" && _a.#globTarget(context.statement.target)) {
341
- return this.#fanOutRead(context, context.statement);
342
- }
343
- let result = await ResourceBindings.using(this.#schemes, this.#buildSchemeCtx(context), (ctx) => this.#dispatchOne(context, ctx));
343
+ async #dispatch(context, targetsResolved = false) {
344
+ const execution = await ResourceBindings.using(this.#schemes, this.#buildSchemeCtx(context), (ctx) => this.#dispatchOne(context, ctx, targetsResolved));
345
+ let { result } = execution;
344
346
  const edit = context.statement.op === "EDIT" ? context.statement : this.#scopedEntryEdits.get(context.statement);
345
347
  if (edit !== undefined) {
346
348
  // {§edit-batch-merges} — a proposal's apply result replaces the projected one; the
@@ -361,7 +363,7 @@ class Dispatcher {
361
363
  await ResourceBindings.using(this.#schemes, this.#buildSchemeCtx(noteContext), (ctx) => this.#dispatchOne(noteContext, ctx));
362
364
  result = Results.assert({ ...result, rowsWritten: (result.rowsWritten ?? 1) + 1 });
363
365
  }
364
- return result;
366
+ return { ...execution, result };
365
367
  }
366
368
  // {§matcher-refusal} — a matcher the parser could not read refuses its one operation with the parser's
367
369
  // diagnostic and working form: the operation's own row, soft on the rail, never a contract strike.
@@ -380,57 +382,109 @@ class Dispatcher {
380
382
  return statement.body.trim() === "" ? null : statement.body;
381
383
  }
382
384
  static #globTarget(target) {
383
- return target !== null && PathSyntax.hasGlob(target.kind === "url" ? target.pathname : target.raw);
385
+ if (target === null)
386
+ return false;
387
+ if (target.kind === "local")
388
+ return PathSyntax.hasGlob(target.raw);
389
+ // Authority-only worker addresses name actors, not their resource trees.
390
+ if (target.scheme === "worker" && (target.pathname === "" || target.pathname === "/"))
391
+ return false;
392
+ return PathSyntax.hasGlob(target.pathname) || PathSyntax.hasGlob(target.hostname ?? "");
384
393
  }
385
- // {§read-fan-out} — a READ over a glob reads every matching path: the paths come from the
386
- // ordinary FIND over the same target (unlogged; a matcher FIND when there is one, the catalog
387
- // otherwise; its resource page bounds the fan-out), and each is read as an ordinary exact READ
388
- // with the authored scope and matcher and its own receipt row, so every rendered line keeps its
389
- // path, ordinal and anchor. No path: one 204 receipt on the authored glob.
390
- async #fanOutRead(context, statement) {
391
- const survey = {
392
- op: "FIND", aside: statement.aside, target: statement.target, metadata: statement.metadata,
393
- matcher: statement.matcher, lineMarker: null, body: null, position: statement.position,
394
- };
395
- const found = await ResourceBindings.using(this.#schemes, this.#buildSchemeCtx(context), (ctx) => this.#dataRun.run(schemeNameOf(statement.target), survey, ctx));
394
+ // {§bulk-resource-targeting} Selection and exact execution share the ordinary owners.
395
+ async #findTargets(survey, ctx) {
396
+ const found = await this.#dataRun.run(schemeNameOf(survey.target), survey, ctx);
397
+ if (found.status >= 400)
398
+ return { result: found };
396
399
  const paths = found.status < 300
397
400
  ? (found.results ?? []).flatMap((item) => Array.isArray(item) && !("items" in item[0]) ? [item[0].path] : [])
398
401
  : [];
399
- if (paths.length === 0) {
400
- const result = found.status >= 400 ? found : Results.assert(statement.matcher === null
401
- ? { status: 204, detail: `No path matched ${statement.target.raw}.` }
402
- : { status: 204, matched: 0, matcher: statement.matcher.raw, detail: `No line under ${statement.target.raw} matched the pattern.` });
403
- const logEntryId = await this.#logWriter.writeLog({
404
- statement, result, workspaceId: context.workspaceId, workerId: context.workerId, loopId: context.loopId,
405
- turnId: context.turnId, sequence: context.sequence, origin: context.origin, curationPlan: null, modelCallId: null,
406
- });
407
- context.onDispatch?.(logEntryId);
408
- await this.#notifySettled(context, logEntryId);
409
- return result;
402
+ const matched = typeof found.matchingPathCount === "number" ? found.matchingPathCount : paths.length;
403
+ if (survey.lineMarker !== null && matched !== paths.length) {
404
+ throw new InvalidOperationResultError("Unbounded FIND did not return its complete resource selection.");
410
405
  }
411
- const matchingPathCount = typeof found.matchingPathCount === "number" ? found.matchingPathCount : paths.length;
412
- const results = [];
413
- for (const [index, path] of paths.entries()) {
414
- const target = parsePath(path);
406
+ const targets = paths.map((path) => {
407
+ const selected = survey.target?.fragment == null ? path : `${path.split("#")[0]}#${survey.target.fragment}`;
408
+ const target = parsePath(selected);
415
409
  if (target === null)
416
- throw new InvalidOperationResultError(`FIND named an unparseable path: ${path}`);
417
- const fanout = { target: statement.target.raw, matched: matchingPathCount, index, count: paths.length };
418
- results.push(await this.#dispatch({ ...context, statement: { ...statement, target }, sequence: context.sequence + index, fanout }));
410
+ throw new InvalidOperationResultError(`FIND named an unparseable path: ${selected}`);
411
+ return target;
412
+ });
413
+ return { targets, matched };
414
+ }
415
+ async #selectTargets(statement, ctx) {
416
+ if (statement.op === "COPY" || statement.op === "MOVE") {
417
+ if (![statement.source, statement.destination].some(({ target }) => _a.#globTarget(target)))
418
+ return null;
419
+ let resolved = statement;
420
+ for (const operand of ["source", "destination"]) {
421
+ const selection = statement[operand];
422
+ if (!_a.#globTarget(selection.target))
423
+ continue;
424
+ const found = await this.#findTargets({
425
+ ...selection, op: "FIND", matcher: null, lineMarker: { marks: [1, -1] },
426
+ aside: statement.aside, position: statement.position, body: null,
427
+ }, ctx);
428
+ if ("result" in found)
429
+ return found;
430
+ if (found.targets.length !== 1)
431
+ return { result: _a.#failure("target-cardinality", 400, `${statement.op} ${operand} matched ${found.matched} resources; expected one.`, {}, { operand, matchingPathCount: found.matched, recovery: "Name one source and one destination per pair.", retryable: false }) };
432
+ resolved = { ...resolved, [operand]: { ...selection, target: found.targets[0] } };
433
+ }
434
+ return { statements: [resolved] };
419
435
  }
420
- if (matchingPathCount > paths.length) {
421
- this.#notices.push(context.workspaceId, context.workerId, context.loopId, {
422
- source: "engine:dispatcher",
423
- kind: "read_fanout_bounded",
424
- level: "warn",
436
+ if (!(statement.op === "READ" || statement.op === "EDIT" || statement.op === "KILL"))
437
+ return null;
438
+ if (!_a.#globTarget(statement.target) || (statement.op === "KILL" && schemeNameOf(statement.target) === "log"))
439
+ return null;
440
+ const found = await this.#findTargets({
441
+ op: "FIND", aside: statement.aside, target: statement.target, metadata: statement.metadata,
442
+ matcher: statement.op === "READ" ? statement.matcher : null,
443
+ lineMarker: statement.op === "READ" ? null : { marks: [1, -1] }, body: null, position: statement.position,
444
+ }, ctx);
445
+ if ("result" in found)
446
+ return found;
447
+ if (found.targets.length === 0)
448
+ return { result: Results.assert(statement.op === "READ" && statement.matcher !== null
449
+ ? { status: 204, matched: 0, matcher: statement.matcher.raw, detail: `No line under ${statement.target.raw} matched the pattern.` }
450
+ : { status: 204, detail: `No path matched ${statement.target.raw}.` }) };
451
+ if (statement.op === "READ" && found.matched > found.targets.length) {
452
+ this.#notices.push(ctx.workspaceId, ctx.workerId, ctx.loopId, {
453
+ source: "engine:dispatcher", kind: "read_fanout_bounded", level: "warn",
425
454
  directive: "YOU SHOULD narrow the glob, or FIND first.",
426
- message: `READ ${statement.target.raw} matched ${matchingPathCount} paths; the first ${paths.length} were read.`,
455
+ message: `READ ${statement.target.raw} matched ${found.matched} paths; the first ${found.targets.length} were selected.`,
427
456
  });
428
457
  }
429
- return Results.assert({ ...(results.find(({ status }) => status >= 400) ?? results[results.length - 1]), rowsWritten: results.length });
458
+ return { statements: found.targets.map((target) => ({ ...statement, target })), fanout: { target: statement.target.raw, matched: found.matched } };
430
459
  }
431
- async #dispatchOne(context, schemeCtx) {
460
+ async #dispatchSelection(context, selection) {
461
+ const admitted = selection.statements.slice(0, context.remainingCommands);
462
+ const results = [];
463
+ let rowsWritten = 0;
464
+ for (const [index, statement] of admitted.entries()) {
465
+ const { result } = await this.#dispatch({
466
+ ...context, statement, sequence: context.sequence + rowsWritten,
467
+ ...(selection.fanout === undefined ? {} : { fanout: { ...selection.fanout, index, count: admitted.length } }),
468
+ ...(context.fit === undefined ? {} : { fit: reserved(context.fit, admitted.length - index - 1) }),
469
+ }, true);
470
+ results.push(result);
471
+ rowsWritten += result.rowsWritten ?? 1;
472
+ if (context.failOnOperationError && result.status >= 400 && !isContextReceipt(result))
473
+ break;
474
+ }
475
+ if (results.length === 0)
476
+ throw new Error("Dispatch requires an admitted operation.");
477
+ return { result: { ...(results.find(({ status }) => status >= 400) ?? results.at(-1)), rowsWritten },
478
+ commandsExecuted: results.length, commandsOmitted: selection.statements.length - admitted.length };
479
+ }
480
+ // {§bulk-resource-targeting} Dispatch accounting is core-owned, never a scheme's extension data.
481
+ static #singleExecution(result) {
482
+ return { result, commandsExecuted: 1, commandsOmitted: 0 };
483
+ }
484
+ async #dispatchOne(context, schemeCtx, targetsResolved = false) {
432
485
  const { statement, workspaceId, workerId, loopId, turnId, sequence, origin, onDispatch, } = context;
433
486
  let result;
487
+ let resolved = null;
434
488
  let curationPlan = null;
435
489
  const denial = this.#checkWritable(statement, origin, workspaceId)
436
490
  ?? await this.#checkCapabilities(statement, schemeCtx)
@@ -445,7 +499,14 @@ class Dispatcher {
445
499
  // skips it. Logging failures (#writeLog throws) are NOT caught —
446
500
  // those are system failures.
447
501
  try {
448
- if (statement.op === "EDIT") {
502
+ const selection = targetsResolved ? null : await this.#selectTargets(statement, schemeCtx);
503
+ if (selection !== null && !("result" in selection)) {
504
+ resolved = selection;
505
+ }
506
+ else if (selection !== null) {
507
+ result = selection.result;
508
+ }
509
+ else if (statement.op === "EDIT") {
449
510
  result = await this.#resourceMutations.edit(statement, schemeCtx, context.editSequence);
450
511
  }
451
512
  else if (statement.op === "SEND" && statement.target === null) {
@@ -516,6 +577,12 @@ class Dispatcher {
516
577
  }
517
578
  }
518
579
  }
580
+ // Child dispatch includes persistence and proposal settlement; their failures are
581
+ // internal, not exceptions from a scheme handler.
582
+ if (resolved !== null)
583
+ return this.#dispatchSelection(context, resolved);
584
+ if (result === undefined)
585
+ throw new Error("Operation dispatch produced neither targets nor a result.");
519
586
  result = await this.#fitResult(context, statement, result);
520
587
  // {§configuration-repair-path}: decide admission before persisting a proposed row.
521
588
  let autoAccept = false;
@@ -538,7 +605,7 @@ class Dispatcher {
538
605
  }
539
606
  }
540
607
  }
541
- // {§read-fan-out} — a row read on behalf of a glob names the statement it came from.
608
+ // {§bulk-resource-targeting} — a row on behalf of a glob names the statement it came from.
542
609
  if (context.fanout !== undefined) {
543
610
  const attrs = typeof result.attrs === "object" && result.attrs !== null ? result.attrs : {};
544
611
  result = { ...result, attrs: { ...attrs, fanout: context.fanout } };
@@ -569,6 +636,7 @@ class Dispatcher {
569
636
  // result the caller sees, so runTurn never branches on a pending state.
570
637
  if (_a.#isProposal(statement, result)) {
571
638
  if (autoAccept) {
639
+ await this.#proposals.recordDecision(logEntryId, { decision: "accept" });
572
640
  const initialSettlement = await this.#proposals.workerApply(statement, result, { decision: "accept" }, { workspaceId, workerId, loopId, turnId, resources: schemeCtx.resources });
573
641
  const effective = await this.#resourceMutations.settleProposal({
574
642
  statement,
@@ -579,7 +647,7 @@ class Dispatcher {
579
647
  });
580
648
  const post = await this.#proposals.applyResolution(logEntryId, effective);
581
649
  await this.#notifySettled(context, logEntryId);
582
- return post;
650
+ return _a.#singleExecution(post);
583
651
  }
584
652
  // Register the resolution waiter SYNCHRONOUSLY before any await
585
653
  // yields. A same-tick resolveProposal() (e.g. from a test that
@@ -619,10 +687,10 @@ class Dispatcher {
619
687
  });
620
688
  const post = await this.#proposals.applyResolution(logEntryId, effective);
621
689
  await this.#notifySettled(context, logEntryId);
622
- return post;
690
+ return _a.#singleExecution(post);
623
691
  }
624
692
  await this.#notifySettled(context, logEntryId);
625
- return result;
693
+ return _a.#singleExecution(result);
626
694
  }
627
695
  // {§context-fit} — a result the model asked for arrives whole when it fits the remaining budget;
628
696
  // otherwise the row is its size and address — a READ of lines above the longest prefix of them that
@@ -724,11 +792,9 @@ class Dispatcher {
724
792
  return null;
725
793
  if (resolved.address === null)
726
794
  return null;
727
- const rendered = target.kind === "url"
728
- ? renderTarget({ ...target, fragment: null })
729
- : renderTarget({ scheme: null, pathname: target.raw, fragment: null });
730
- if (rendered === null)
731
- throw new TypeError("Resolved entry target did not render.");
795
+ const rendered = resolved.address.scheme === "file"
796
+ ? entryAddress("file", "", resolved.address.pathname, await this.#workspaceRoot(workspaceId))
797
+ : renderAuthoredTarget({ ...target, fragment: null });
732
798
  return {
733
799
  scheme: resolved.address.scheme,
734
800
  authority: resolved.address.authority,
@@ -1317,7 +1383,14 @@ class Dispatcher {
1317
1383
  // `local` (bare path) carries no URL parts — store the raw text as the pathname for the log record, scheme=null.
1318
1384
  if (path.kind === "local")
1319
1385
  return { scheme: null, username: null, password: null, hostname: null, port: null, pathname: PathSyntax.decodeParens(path.raw), query: null, fragment: path.fragment ?? null }; // {§path-parentheses} {§local-path-fragment}
1320
- const scheme = path.scheme === "file" ? null : path.scheme;
1386
+ if (path.scheme === "file") {
1387
+ const pathname = FileAddress.pathname(path);
1388
+ // An invalid URL has no resolved identity; retain the refused input alongside its failure.
1389
+ if (typeof pathname !== "string")
1390
+ return { ...path };
1391
+ return { scheme: null, username: null, password: null, hostname: null, port: null, pathname, query: null, fragment: path.fragment };
1392
+ }
1393
+ const scheme = path.scheme;
1321
1394
  // The registered scheme owns authority disposition. Namespace authority
1322
1395
  // is path syntax; resource and owner authorities remain explicit in the
1323
1396
  // durable operation evidence.