@plurnk/plurnk-service 3.0.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 (280) hide show
  1. package/.env.defaults +38 -21
  2. package/INSTALL.md +38 -1
  3. package/README.md +3 -0
  4. package/SPEC.md +681 -268
  5. package/dist/build-info.json +1 -1
  6. package/dist/completions/_plurnk-service +13 -0
  7. package/dist/completions/plurnk-service.bash +19 -0
  8. package/dist/completions/plurnk-service.fish +94 -0
  9. package/dist/content/path-mimetype.d.ts.map +1 -1
  10. package/dist/content/path-mimetype.js +2 -8
  11. package/dist/content/path-mimetype.js.map +1 -1
  12. package/dist/core/AdmittedTurnExecutor.d.ts +2 -2
  13. package/dist/core/AdmittedTurnExecutor.d.ts.map +1 -1
  14. package/dist/core/AdmittedTurnExecutor.js +36 -32
  15. package/dist/core/AdmittedTurnExecutor.js.map +1 -1
  16. package/dist/core/BudgetReadout.d.ts +7 -2
  17. package/dist/core/BudgetReadout.d.ts.map +1 -1
  18. package/dist/core/BudgetReadout.js +36 -5
  19. package/dist/core/BudgetReadout.js.map +1 -1
  20. package/dist/{schemes/_derivation-use.d.ts → core/ContentUse.d.ts} +3 -3
  21. package/dist/core/ContentUse.d.ts.map +1 -0
  22. package/dist/{schemes/_derivation-use.js → core/ContentUse.js} +8 -7
  23. package/dist/core/ContentUse.js.map +1 -0
  24. package/dist/core/DataStatementRunner.d.ts.map +1 -1
  25. package/dist/core/DataStatementRunner.js +8 -20
  26. package/dist/core/DataStatementRunner.js.map +1 -1
  27. package/dist/core/Dispatcher.d.ts +11 -3
  28. package/dist/core/Dispatcher.d.ts.map +1 -1
  29. package/dist/core/Dispatcher.js +186 -109
  30. package/dist/core/Dispatcher.js.map +1 -1
  31. package/dist/core/EditMutations.js +1 -1
  32. package/dist/core/EditMutations.js.map +1 -1
  33. package/dist/core/Engine.d.ts +1 -1
  34. package/dist/core/Engine.d.ts.map +1 -1
  35. package/dist/core/Engine.js +4 -7
  36. package/dist/core/Engine.js.map +1 -1
  37. package/dist/core/EnvFlags.js +1 -1
  38. package/dist/core/EnvFlags.js.map +1 -1
  39. package/dist/core/FabricatedLog.d.ts +1 -0
  40. package/dist/core/FabricatedLog.d.ts.map +1 -1
  41. package/dist/core/FabricatedLog.js +8 -0
  42. package/dist/core/FabricatedLog.js.map +1 -1
  43. package/dist/core/FileAddress.d.ts +12 -0
  44. package/dist/core/FileAddress.d.ts.map +1 -0
  45. package/dist/core/FileAddress.js +73 -0
  46. package/dist/core/FileAddress.js.map +1 -0
  47. package/dist/core/FilePublication.d.ts +15 -0
  48. package/dist/core/FilePublication.d.ts.map +1 -0
  49. package/dist/core/FilePublication.js +143 -0
  50. package/dist/core/FilePublication.js.map +1 -0
  51. package/dist/core/FileSnapshot.d.ts +26 -0
  52. package/dist/core/FileSnapshot.d.ts.map +1 -0
  53. package/dist/core/FileSnapshot.js +179 -0
  54. package/dist/core/FileSnapshot.js.map +1 -0
  55. package/dist/core/KillHandler.d.ts.map +1 -1
  56. package/dist/core/KillHandler.js +4 -1
  57. package/dist/core/KillHandler.js.map +1 -1
  58. package/dist/core/LiveSubscription.d.ts.map +1 -1
  59. package/dist/core/LiveSubscription.js +14 -2
  60. package/dist/core/LiveSubscription.js.map +1 -1
  61. package/dist/core/LiveSubscriptions.d.ts +3 -1
  62. package/dist/core/LiveSubscriptions.d.ts.map +1 -1
  63. package/dist/core/LiveSubscriptions.js +16 -1
  64. package/dist/core/LiveSubscriptions.js.map +1 -1
  65. package/dist/core/LogBody.d.ts.map +1 -1
  66. package/dist/core/LogBody.js +3 -1
  67. package/dist/core/LogBody.js.map +1 -1
  68. package/dist/core/LoopDriver.d.ts.map +1 -1
  69. package/dist/core/LoopDriver.js +3 -0
  70. package/dist/core/LoopDriver.js.map +1 -1
  71. package/dist/core/LoopLifecycle.sql +1 -0
  72. package/dist/core/MembershipMaterialization.d.ts.map +1 -1
  73. package/dist/core/MembershipMaterialization.js +87 -143
  74. package/dist/core/MembershipMaterialization.js.map +1 -1
  75. package/dist/core/MessageResources.sql +31 -0
  76. package/dist/core/MutationEffects.d.ts +2 -1
  77. package/dist/core/MutationEffects.d.ts.map +1 -1
  78. package/dist/core/MutationEffects.js +14 -1
  79. package/dist/core/MutationEffects.js.map +1 -1
  80. package/dist/core/PacketBuilder.d.ts +2 -2
  81. package/dist/core/PacketBuilder.d.ts.map +1 -1
  82. package/dist/core/PacketBuilder.js +123 -51
  83. package/dist/core/PacketBuilder.js.map +1 -1
  84. package/dist/core/PacketBuilder.sql +21 -14
  85. package/dist/core/PathGlob.d.ts +6 -0
  86. package/dist/core/PathGlob.d.ts.map +1 -0
  87. package/dist/core/PathGlob.js +24 -0
  88. package/dist/core/PathGlob.js.map +1 -0
  89. package/dist/core/ProposalLifecycle.d.ts +2 -1
  90. package/dist/core/ProposalLifecycle.d.ts.map +1 -1
  91. package/dist/core/ProposalLifecycle.js +45 -33
  92. package/dist/core/ProposalLifecycle.js.map +1 -1
  93. package/dist/core/ProposalLifecycle.sql +4 -0
  94. package/dist/core/ReplyMetadata.d.ts +10 -0
  95. package/dist/core/ReplyMetadata.d.ts.map +1 -0
  96. package/dist/core/ReplyMetadata.js +20 -0
  97. package/dist/core/ReplyMetadata.js.map +1 -0
  98. package/dist/core/ResourceBindings.d.ts.map +1 -1
  99. package/dist/core/ResourceBindings.js +4 -3
  100. package/dist/core/ResourceBindings.js.map +1 -1
  101. package/dist/core/ResourceSelector.d.ts +1 -2
  102. package/dist/core/ResourceSelector.d.ts.map +1 -1
  103. package/dist/core/ResourceSelector.js +30 -9
  104. package/dist/core/ResourceSelector.js.map +1 -1
  105. package/dist/core/ResourceTransfers.d.ts +1 -1
  106. package/dist/core/ResourceTransfers.d.ts.map +1 -1
  107. package/dist/core/ResourceTransfers.js +26 -11
  108. package/dist/core/ResourceTransfers.js.map +1 -1
  109. package/dist/core/ServiceHelp.d.ts +6 -0
  110. package/dist/core/ServiceHelp.d.ts.map +1 -0
  111. package/dist/core/ServiceHelp.js +35 -0
  112. package/dist/core/ServiceHelp.js.map +1 -0
  113. package/dist/core/StoredPacket.d.ts +1 -0
  114. package/dist/core/StoredPacket.d.ts.map +1 -1
  115. package/dist/core/StoredPacket.js +4 -1
  116. package/dist/core/StoredPacket.js.map +1 -1
  117. package/dist/core/StrikeRail.d.ts.map +1 -1
  118. package/dist/core/StrikeRail.js +2 -2
  119. package/dist/core/StrikeRail.js.map +1 -1
  120. package/dist/core/TokenCalibration.d.ts +2 -2
  121. package/dist/core/TokenCalibration.d.ts.map +1 -1
  122. package/dist/core/TokenCalibration.js +12 -16
  123. package/dist/core/TokenCalibration.js.map +1 -1
  124. package/dist/core/TokenCalibration.sql +29 -6
  125. package/dist/core/TurnDispositionHandler.d.ts +1 -2
  126. package/dist/core/TurnDispositionHandler.d.ts.map +1 -1
  127. package/dist/core/TurnDispositionHandler.js +30 -26
  128. package/dist/core/TurnDispositionHandler.js.map +1 -1
  129. package/dist/core/TurnMaterialization.d.ts.map +1 -1
  130. package/dist/core/TurnMaterialization.js +11 -1
  131. package/dist/core/TurnMaterialization.js.map +1 -1
  132. package/dist/core/TurnOps.d.ts +5 -1
  133. package/dist/core/TurnOps.d.ts.map +1 -1
  134. package/dist/core/TurnOps.js +29 -2
  135. package/dist/core/TurnOps.js.map +1 -1
  136. package/dist/core/TurnRunner.d.ts.map +1 -1
  137. package/dist/core/TurnRunner.js +122 -56
  138. package/dist/core/TurnRunner.js.map +1 -1
  139. package/dist/core/TurnSources.sql +2 -2
  140. package/dist/core/caps/SchemeCtxImpl.d.ts +1 -0
  141. package/dist/core/caps/SchemeCtxImpl.d.ts.map +1 -1
  142. package/dist/core/caps/SchemeCtxImpl.js +5 -0
  143. package/dist/core/caps/SchemeCtxImpl.js.map +1 -1
  144. package/dist/core/git-membership.d.ts +1 -0
  145. package/dist/core/git-membership.d.ts.map +1 -1
  146. package/dist/core/git-membership.js +9 -11
  147. package/dist/core/git-membership.js.map +1 -1
  148. package/dist/core/glob_match.js +3 -3
  149. package/dist/core/glob_match.js.map +1 -1
  150. package/dist/core/mutation-types.d.ts +3 -0
  151. package/dist/core/mutation-types.d.ts.map +1 -1
  152. package/dist/core/namespace.d.ts +4 -1
  153. package/dist/core/namespace.d.ts.map +1 -1
  154. package/dist/core/namespace.js +14 -3
  155. package/dist/core/namespace.js.map +1 -1
  156. package/dist/core/packet-wire.d.ts +7 -1
  157. package/dist/core/packet-wire.d.ts.map +1 -1
  158. package/dist/core/packet-wire.js +98 -36
  159. package/dist/core/packet-wire.js.map +1 -1
  160. package/dist/core/plurnk-uri.d.ts +3 -2
  161. package/dist/core/plurnk-uri.d.ts.map +1 -1
  162. package/dist/core/plurnk-uri.js +23 -11
  163. package/dist/core/plurnk-uri.js.map +1 -1
  164. package/dist/core/recap-lines.d.ts +4 -0
  165. package/dist/core/recap-lines.d.ts.map +1 -0
  166. package/dist/core/recap-lines.js +11 -0
  167. package/dist/core/recap-lines.js.map +1 -0
  168. package/dist/core/scheme-types.d.ts +1 -0
  169. package/dist/core/scheme-types.d.ts.map +1 -1
  170. package/dist/core/sql-functions.d.ts +2 -0
  171. package/dist/core/sql-functions.d.ts.map +1 -0
  172. package/dist/core/sql-functions.js +5 -0
  173. package/dist/core/sql-functions.js.map +1 -0
  174. package/dist/core/sqlite-synchronous.d.ts +2 -0
  175. package/dist/core/sqlite-synchronous.d.ts.map +1 -0
  176. package/dist/core/sqlite-synchronous.js +4 -0
  177. package/dist/core/sqlite-synchronous.js.map +1 -0
  178. package/dist/core/turn-scheduler.d.ts.map +1 -1
  179. package/dist/core/turn-scheduler.js +1 -1
  180. package/dist/core/turn-scheduler.js.map +1 -1
  181. package/dist/evidence/EvidenceReader.d.ts.map +1 -1
  182. package/dist/evidence/EvidenceReader.js +23 -3
  183. package/dist/evidence/EvidenceReader.js.map +1 -1
  184. package/dist/evidence/digest.sql +7 -1
  185. package/dist/man/plurnk-service.1 +499 -0
  186. package/dist/schemes/Env.d.ts +26 -0
  187. package/dist/schemes/Env.d.ts.map +1 -0
  188. package/dist/schemes/Env.js +133 -0
  189. package/dist/schemes/Env.js.map +1 -0
  190. package/dist/schemes/Exec.d.ts.map +1 -1
  191. package/dist/schemes/Exec.js +15 -15
  192. package/dist/schemes/Exec.js.map +1 -1
  193. package/dist/schemes/ExecOutputScheme.d.ts +3 -2
  194. package/dist/schemes/ExecOutputScheme.d.ts.map +1 -1
  195. package/dist/schemes/ExecOutputScheme.js +9 -0
  196. package/dist/schemes/ExecOutputScheme.js.map +1 -1
  197. package/dist/schemes/File.d.ts +1 -0
  198. package/dist/schemes/File.d.ts.map +1 -1
  199. package/dist/schemes/File.js +233 -268
  200. package/dist/schemes/File.js.map +1 -1
  201. package/dist/schemes/Log.d.ts.map +1 -1
  202. package/dist/schemes/Log.js +97 -17
  203. package/dist/schemes/Log.js.map +1 -1
  204. package/dist/schemes/Log.sql +9 -0
  205. package/dist/schemes/Worker.js +1 -1
  206. package/dist/schemes/Worker.js.map +1 -1
  207. package/dist/schemes/_entry-crud.d.ts +2 -0
  208. package/dist/schemes/_entry-crud.d.ts.map +1 -1
  209. package/dist/schemes/_entry-crud.js +8 -5
  210. package/dist/schemes/_entry-crud.js.map +1 -1
  211. package/dist/schemes/_entry-crud.sql +15 -15
  212. package/dist/schemes/_entry-find.d.ts +2 -1
  213. package/dist/schemes/_entry-find.d.ts.map +1 -1
  214. package/dist/schemes/_entry-find.js +31 -19
  215. package/dist/schemes/_entry-find.js.map +1 -1
  216. package/dist/schemes/_entry-manifest.d.ts +2 -2
  217. package/dist/schemes/_entry-manifest.d.ts.map +1 -1
  218. package/dist/schemes/_entry-manifest.js +10 -8
  219. package/dist/schemes/_entry-manifest.js.map +1 -1
  220. package/dist/schemes/_entry-ops.d.ts.map +1 -1
  221. package/dist/schemes/_entry-ops.js +9 -7
  222. package/dist/schemes/_entry-ops.js.map +1 -1
  223. package/dist/schemes/_path-scope.d.ts.map +1 -1
  224. package/dist/schemes/_path-scope.js +12 -4
  225. package/dist/schemes/_path-scope.js.map +1 -1
  226. package/dist/schemes/_search-index.js +3 -3
  227. package/dist/schemes/_search-index.js.map +1 -1
  228. package/dist/schemes/exec-abort.js +2 -2
  229. package/dist/schemes/exec-abort.js.map +1 -1
  230. package/dist/schemes/exec-env.d.ts.map +1 -1
  231. package/dist/schemes/exec-env.js +7 -22
  232. package/dist/schemes/exec-env.js.map +1 -1
  233. package/dist/server/Daemon.d.ts +1 -0
  234. package/dist/server/Daemon.d.ts.map +1 -1
  235. package/dist/server/Daemon.js +45 -68
  236. package/dist/server/Daemon.js.map +1 -1
  237. package/dist/server/DrainSupervisor.d.ts +2 -3
  238. package/dist/server/DrainSupervisor.d.ts.map +1 -1
  239. package/dist/server/DrainSupervisor.js +34 -6
  240. package/dist/server/DrainSupervisor.js.map +1 -1
  241. package/dist/server/EnvFunctionality.d.ts +4 -2
  242. package/dist/server/EnvFunctionality.d.ts.map +1 -1
  243. package/dist/server/EnvFunctionality.js +23 -12
  244. package/dist/server/EnvFunctionality.js.map +1 -1
  245. package/dist/server/Functionality.d.ts.map +1 -1
  246. package/dist/server/Functionality.js +15 -8
  247. package/dist/server/Functionality.js.map +1 -1
  248. package/dist/server/FunctionalityManager.d.ts +1 -1
  249. package/dist/server/FunctionalityManager.d.ts.map +1 -1
  250. package/dist/server/FunctionalityManager.js +5 -1
  251. package/dist/server/FunctionalityManager.js.map +1 -1
  252. package/dist/server/MembersFunctionality.d.ts.map +1 -1
  253. package/dist/server/MembersFunctionality.js +12 -12
  254. package/dist/server/MembersFunctionality.js.map +1 -1
  255. package/dist/server/Retention.d.ts.map +1 -1
  256. package/dist/server/Retention.js +5 -5
  257. package/dist/server/Retention.js.map +1 -1
  258. package/dist/server/Retention.sql +49 -23
  259. package/dist/server/client-input.d.ts.map +1 -1
  260. package/dist/server/client-input.js +12 -3
  261. package/dist/server/client-input.js.map +1 -1
  262. package/dist/server/drain.sql +10 -9
  263. package/dist/server/lifecycle-recovery.sql +4 -1
  264. package/dist/service.d.ts.map +1 -1
  265. package/dist/service.js +9 -31
  266. package/dist/service.js.map +1 -1
  267. package/docs/copy-move.md +3 -0
  268. package/docs/env.md +32 -4
  269. package/docs/members.md +16 -1
  270. package/migrations/018_worker_owners.sql +1 -0
  271. package/migrations/019_interactive_owners.sql +1 -0
  272. package/migrations/020_log_projection.sql +1 -0
  273. package/migrations/021_packet_roles.sql +86 -0
  274. package/migrations/022_message_completion.sql +24 -0
  275. package/migrations/023_file_revisions.sql +54 -0
  276. package/migrations/024_proposal_decisions.sql +34 -0
  277. package/migrations/025_response_retention.sql +30 -0
  278. package/package.json +38 -34
  279. package/dist/schemes/_derivation-use.d.ts.map +0 -1
  280. 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,13 +23,14 @@ 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";
29
30
  import WorkerControlAddress from "./WorkerControlAddress.js";
30
31
  import KillHandler from "./KillHandler.js";
31
32
  import TurnDispositionHandler from "./TurnDispositionHandler.js";
33
+ import ReplyMetadata from "./ReplyMetadata.js";
32
34
  import LogWriter from "./LogWriter.js";
33
35
  import LogEntryProjection from "./LogEntryProjection.js";
34
36
  import DurableStatement from "./DurableStatement.js";
@@ -90,7 +92,6 @@ class Dispatcher {
90
92
  this.#resourceSelector = new ResourceSelector({
91
93
  schemes,
92
94
  admitRead: (statement, ctx) => this.#checkCapabilities(statement, ctx),
93
- canonicalFilePath: (pathname, workspaceId) => this.#canonicalFilePath(pathname, workspaceId),
94
95
  prepareDataRepresentation: (args) => this.#prepareDataRepresentation({
95
96
  ...args,
96
97
  handler: args.handler,
@@ -246,13 +247,10 @@ class Dispatcher {
246
247
  if (key !== null)
247
248
  target.pathname = key;
248
249
  }
249
- async #canonicalFilePath(pathname, workspaceId) {
250
- return Namespace.canonicalizeSpelling(pathname, await this.#workspaceRoot(workspaceId));
251
- }
252
250
  async #editTargetIdentity(statement, workspaceId, _workerId) {
253
251
  const target = this.#extractTarget(statement.target, workspaceId);
254
252
  await this.#canonColumns(target, workspaceId);
255
- return renderTarget(target);
253
+ return renderTarget(target, await this.#workspaceRoot(workspaceId));
256
254
  }
257
255
  // {§kill-scope-entry} — a scoped KILL on an entry-bearing scheme is one EDIT with an empty
258
256
  // body over the same marker: prepared, gated, and merged as an EDIT while its log row
@@ -294,9 +292,9 @@ class Dispatcher {
294
292
  async dispatch(context) {
295
293
  await this.#operationEvent(context, "started");
296
294
  await this.#noticeFilePaths(context);
297
- const result = await this.#dispatch(context);
298
- await this.#operationEvent(context, "settled", result);
299
- return result;
295
+ const execution = await this.#dispatch(context);
296
+ await this.#operationEvent(context, "settled", execution.result);
297
+ return execution;
300
298
  }
301
299
  // {§file-path-normalization} Authored operands, once, before internal fan-out.
302
300
  async #noticeFilePaths({ statement, origin, workspaceId, workerId, loopId }) {
@@ -305,21 +303,29 @@ class Dispatcher {
305
303
  const targets = statement.op === "COPY" || statement.op === "MOVE"
306
304
  ? [statement.source.target, statement.destination.target]
307
305
  : [statement.target];
308
- const absolute = targets.flatMap((target) => target === null || schemeNameOf(target) !== "file" ? []
309
- : [PathSyntax.decodeParens(target.kind === "url" ? target.pathname : target.raw)])
310
- .filter((pathname) => pathname.startsWith("/"));
311
- 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)
312
314
  return;
313
315
  const root = await this.#workspaceRoot(workspaceId);
314
316
  if (root === null || root === "/")
315
317
  return;
316
- for (const pathname of absolute) {
318
+ for (const { target, pathname } of normalized) {
317
319
  const key = Namespace.canonicalizeSpelling(pathname, root);
318
320
  if (key === null)
319
321
  continue;
322
+ const address = renderTarget({ scheme: null, pathname: key }, root, target);
323
+ if (address?.startsWith("file://"))
324
+ continue;
320
325
  this.#notices.push(workspaceId, workerId, loopId, {
321
326
  source: "scheme:file", kind: "path_normalized", level: "warn",
322
- message: `Path resolved to '${key || "."}'.`,
327
+ directive: "YOU SHOULD address the resolved path.",
328
+ message: `Path resolved to '${address || "."}'.`,
323
329
  });
324
330
  }
325
331
  }
@@ -334,11 +340,9 @@ class Dispatcher {
334
340
  ...(result === undefined ? {} : { result }),
335
341
  });
336
342
  }
337
- async #dispatch(context) {
338
- if (context.statement.op === "READ" && _a.#globTarget(context.statement.target)) {
339
- return this.#fanOutRead(context, context.statement);
340
- }
341
- 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;
342
346
  const edit = context.statement.op === "EDIT" ? context.statement : this.#scopedEntryEdits.get(context.statement);
343
347
  if (edit !== undefined) {
344
348
  // {§edit-batch-merges} — a proposal's apply result replaces the projected one; the
@@ -359,7 +363,7 @@ class Dispatcher {
359
363
  await ResourceBindings.using(this.#schemes, this.#buildSchemeCtx(noteContext), (ctx) => this.#dispatchOne(noteContext, ctx));
360
364
  result = Results.assert({ ...result, rowsWritten: (result.rowsWritten ?? 1) + 1 });
361
365
  }
362
- return result;
366
+ return { ...execution, result };
363
367
  }
364
368
  // {§matcher-refusal} — a matcher the parser could not read refuses its one operation with the parser's
365
369
  // diagnostic and working form: the operation's own row, soft on the rail, never a contract strike.
@@ -378,56 +382,109 @@ class Dispatcher {
378
382
  return statement.body.trim() === "" ? null : statement.body;
379
383
  }
380
384
  static #globTarget(target) {
381
- 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 ?? "");
382
393
  }
383
- // {§read-fan-out} — a READ over a glob reads every matching path: the paths come from the
384
- // ordinary FIND over the same target (unlogged; a matcher FIND when there is one, the catalog
385
- // otherwise; its resource page bounds the fan-out), and each is read as an ordinary exact READ
386
- // with the authored scope and matcher and its own receipt row, so every rendered line keeps its
387
- // path, ordinal and anchor. No path: one 204 receipt on the authored glob.
388
- async #fanOutRead(context, statement) {
389
- const survey = {
390
- op: "FIND", aside: statement.aside, target: statement.target, metadata: statement.metadata,
391
- matcher: statement.matcher, lineMarker: null, body: null, position: statement.position,
392
- };
393
- 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 };
394
399
  const paths = found.status < 300
395
400
  ? (found.results ?? []).flatMap((item) => Array.isArray(item) && !("items" in item[0]) ? [item[0].path] : [])
396
401
  : [];
397
- if (paths.length === 0) {
398
- const result = found.status >= 400 ? found : Results.assert(statement.matcher === null
399
- ? { status: 204, detail: `No path matched ${statement.target.raw}.` }
400
- : { status: 204, matched: 0, matcher: statement.matcher.raw, detail: `No line under ${statement.target.raw} matched the pattern.` });
401
- const logEntryId = await this.#logWriter.writeLog({
402
- statement, result, workspaceId: context.workspaceId, workerId: context.workerId, loopId: context.loopId,
403
- turnId: context.turnId, sequence: context.sequence, origin: context.origin, curationPlan: null, modelCallId: null,
404
- });
405
- context.onDispatch?.(logEntryId);
406
- await this.#notifySettled(context, logEntryId);
407
- 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.");
408
405
  }
409
- const matchingPathCount = typeof found.matchingPathCount === "number" ? found.matchingPathCount : paths.length;
410
- const results = [];
411
- for (const [index, path] of paths.entries()) {
412
- 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);
413
409
  if (target === null)
414
- throw new InvalidOperationResultError(`FIND named an unparseable path: ${path}`);
415
- const fanout = { target: statement.target.raw, matched: matchingPathCount, index, count: paths.length };
416
- 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] };
417
435
  }
418
- if (matchingPathCount > paths.length) {
419
- this.#notices.push(context.workspaceId, context.workerId, context.loopId, {
420
- source: "engine:dispatcher",
421
- kind: "read_fanout_bounded",
422
- level: "warn",
423
- message: `READ ${statement.target.raw} matched ${matchingPathCount} paths; the first ${paths.length} were read. Narrow the glob, or FIND first.`,
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",
454
+ directive: "YOU SHOULD narrow the glob, or FIND first.",
455
+ message: `READ ${statement.target.raw} matched ${found.matched} paths; the first ${found.targets.length} were selected.`,
424
456
  });
425
457
  }
426
- 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 } };
459
+ }
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 };
427
479
  }
428
- async #dispatchOne(context, schemeCtx) {
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) {
429
485
  const { statement, workspaceId, workerId, loopId, turnId, sequence, origin, onDispatch, } = context;
430
486
  let result;
487
+ let resolved = null;
431
488
  let curationPlan = null;
432
489
  const denial = this.#checkWritable(statement, origin, workspaceId)
433
490
  ?? await this.#checkCapabilities(statement, schemeCtx)
@@ -442,17 +499,19 @@ class Dispatcher {
442
499
  // skips it. Logging failures (#writeLog throws) are NOT caught —
443
500
  // those are system failures.
444
501
  try {
445
- 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") {
446
510
  result = await this.#resourceMutations.edit(statement, schemeCtx, context.editSequence);
447
511
  }
448
512
  else if (statement.op === "SEND" && statement.target === null) {
449
513
  result = await this.#respond(statement, schemeCtx, loopId);
450
514
  }
451
- else if (TurnDisposition.isCompletion(statement)) {
452
- result = await this.#disposition.completion({ workerId, loopId, turnId, origin }, context.finalResponse === true);
453
- if (result.status === 200)
454
- result = await this.#respond(statement, schemeCtx, loopId);
455
- }
456
515
  else if (statement.op === "NOTE") {
457
516
  await Turn.recordSource(this.#db, turnId, "note", statement.body ?? "", { sequence });
458
517
  const coordinate = await this.#db.engine_loop_turn_seqs.get({ loop_id: loopId, turn_id: turnId });
@@ -518,6 +577,12 @@ class Dispatcher {
518
577
  }
519
578
  }
520
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.");
521
586
  result = await this.#fitResult(context, statement, result);
522
587
  // {§configuration-repair-path}: decide admission before persisting a proposed row.
523
588
  let autoAccept = false;
@@ -540,7 +605,7 @@ class Dispatcher {
540
605
  }
541
606
  }
542
607
  }
543
- // {§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.
544
609
  if (context.fanout !== undefined) {
545
610
  const attrs = typeof result.attrs === "object" && result.attrs !== null ? result.attrs : {};
546
611
  result = { ...result, attrs: { ...attrs, fanout: context.fanout } };
@@ -571,6 +636,7 @@ class Dispatcher {
571
636
  // result the caller sees, so runTurn never branches on a pending state.
572
637
  if (_a.#isProposal(statement, result)) {
573
638
  if (autoAccept) {
639
+ await this.#proposals.recordDecision(logEntryId, { decision: "accept" });
574
640
  const initialSettlement = await this.#proposals.workerApply(statement, result, { decision: "accept" }, { workspaceId, workerId, loopId, turnId, resources: schemeCtx.resources });
575
641
  const effective = await this.#resourceMutations.settleProposal({
576
642
  statement,
@@ -581,7 +647,7 @@ class Dispatcher {
581
647
  });
582
648
  const post = await this.#proposals.applyResolution(logEntryId, effective);
583
649
  await this.#notifySettled(context, logEntryId);
584
- return post;
650
+ return _a.#singleExecution(post);
585
651
  }
586
652
  // Register the resolution waiter SYNCHRONOUSLY before any await
587
653
  // yields. A same-tick resolveProposal() (e.g. from a test that
@@ -621,10 +687,10 @@ class Dispatcher {
621
687
  });
622
688
  const post = await this.#proposals.applyResolution(logEntryId, effective);
623
689
  await this.#notifySettled(context, logEntryId);
624
- return post;
690
+ return _a.#singleExecution(post);
625
691
  }
626
692
  await this.#notifySettled(context, logEntryId);
627
- return result;
693
+ return _a.#singleExecution(result);
628
694
  }
629
695
  // {§context-fit} — a result the model asked for arrives whole when it fits the remaining budget;
630
696
  // otherwise the row is its size and address — a READ of lines above the longest prefix of them that
@@ -726,11 +792,9 @@ class Dispatcher {
726
792
  return null;
727
793
  if (resolved.address === null)
728
794
  return null;
729
- const rendered = target.kind === "url"
730
- ? renderTarget({ ...target, fragment: null })
731
- : renderTarget({ scheme: null, pathname: target.raw, fragment: null });
732
- if (rendered === null)
733
- 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 });
734
798
  return {
735
799
  scheme: resolved.address.scheme,
736
800
  authority: resolved.address.authority,
@@ -873,7 +937,7 @@ class Dispatcher {
873
937
  #checkWritable(statement, origin, workspaceId) {
874
938
  if (!isExecution(statement) && !MUTATING_OPS.has(statement.op))
875
939
  return null;
876
- if (TurnDisposition.is(statement) || TurnDisposition.isCompletion(statement) || statement.op === "SEND" && statement.target === null)
940
+ if (TurnDisposition.is(statement) || statement.op === "SEND" && statement.target === null)
877
941
  return null;
878
942
  // An execution's operation authority always belongs to the exec scheme;
879
943
  // runtime-specific resource authority is gated separately below.
@@ -891,17 +955,17 @@ class Dispatcher {
891
955
  // body is a seed prompt, not a dst path, so the entry-COPY dst-parse below doesn't apply.
892
956
  // {§machine-processes}
893
957
  if (this.#isWorkerControl(statement))
894
- return this.#denyIfDisallowed("worker", origin, workspaceId);
958
+ return this.#denyIfDisallowed("worker", origin, workspaceId, statement.target);
895
959
  if (statement.op === "COPY" || statement.op === "MOVE") {
896
960
  const dst = statement.destination.target;
897
961
  const dstScheme = schemeNameOf(dst);
898
- const dstDenial = this.#denyIfDisallowed(dstScheme, origin, workspaceId);
962
+ const dstDenial = this.#denyIfDisallowed(dstScheme, origin, workspaceId, dst);
899
963
  if (dstDenial !== null)
900
964
  return dstDenial;
901
965
  if (statement.op === "MOVE") {
902
966
  const srcScheme = schemeNameOf(statement.source.target);
903
967
  if (srcScheme !== dstScheme) {
904
- const srcDenial = this.#denyIfDisallowed(srcScheme, origin, workspaceId);
968
+ const srcDenial = this.#denyIfDisallowed(srcScheme, origin, workspaceId, statement.source.target);
905
969
  if (srcDenial !== null)
906
970
  return srcDenial;
907
971
  }
@@ -909,7 +973,7 @@ class Dispatcher {
909
973
  return null;
910
974
  }
911
975
  const target = schemeNameOf(statement.target);
912
- const denial = this.#denyIfDisallowed(target, origin, workspaceId);
976
+ const denial = this.#denyIfDisallowed(target, origin, workspaceId, statement.target);
913
977
  // {§send-target-recipient} — SEND addresses recipients, not otherwise
914
978
  // read-only resources. State that boundary without guessing whether the
915
979
  // model intended a reply, deletion, or directed message.
@@ -917,19 +981,21 @@ class Dispatcher {
917
981
  return _a.#failure("send-target-not-a-recipient", 400, "The addressed scheme is not a SEND recipient.", {}, {
918
982
  target: statement.target?.raw ?? String(target),
919
983
  stage: "dispatch",
920
- recovery: "A targetless SEND answers the open messages; a directed SEND requires a recipient that implements SEND.",
984
+ recovery: "Name a message address or a recipient that implements SEND.",
921
985
  retryable: false,
922
986
  });
923
987
  }
924
988
  return denial;
925
989
  }
926
- #denyIfDisallowed(schemeName, origin, workspaceId) {
990
+ // {§runtime-resource-binding} The manifest that governs the coordinate decides: a runtime
991
+ // scheme's facet may make its claimed resources writable where the stored output is not.
992
+ #denyIfDisallowed(schemeName, origin, workspaceId, target = null) {
927
993
  if (schemeName === null)
928
994
  return null;
929
995
  const handler = this.#schemes.get(schemeName, workspaceId);
930
996
  if (handler === undefined)
931
997
  return null;
932
- const manifest = this.#schemes.manifestFor(schemeName, workspaceId);
998
+ const manifest = this.#schemes.manifestAt(schemeName, target, workspaceId);
933
999
  if (manifest === undefined)
934
1000
  throw new Error(`registered scheme '${schemeName}' has no manifest`);
935
1001
  if (manifest.writableBy.includes(origin))
@@ -1055,11 +1121,11 @@ class Dispatcher {
1055
1121
  if (reasoning.length === 0)
1056
1122
  return null;
1057
1123
  const pathname = `/${loopSeq}/${turnSeq}`;
1058
- // The row's page: its own length when the operator names one, the shared page otherwise; the
1059
- // character bound is always the shared one.
1060
- const pageLines = Knob.optionalInteger("PLURNK_SERVICE_REASONING_TRAILING_LINES", 1) ?? Knob.integer("PLURNK_SERVICE_PREVIEW_LINES", 1);
1061
- const tail = BodyPreview.selectTail(reasoning, pageLines);
1062
- const lineMarker = tail.whole ? null : tail.marker;
1124
+ const pageLines = Knob.optionalInteger("PLURNK_SERVICE_REASONING_TRAILING_LINES", -1) ?? Knob.integer("PLURNK_SERVICE_PREVIEW_LINES", 1);
1125
+ if (pageLines === 0)
1126
+ throw new ConfigurationError("PLURNK_SERVICE_REASONING_TRAILING_LINES", "PLURNK_SERVICE_REASONING_TRAILING_LINES must be -1 or a positive integer.");
1127
+ const tail = pageLines === -1 ? null : BodyPreview.selectTail(reasoning, pageLines);
1128
+ const lineMarker = tail === null || tail.whole ? null : tail.marker;
1063
1129
  const projected = lineMarker === null ? null : await ReadResolve.resolve({ content: reasoning, mimetype: "text/markdown", lineMarker });
1064
1130
  const statement = {
1065
1131
  op: "READ", aside: null, metadata: null, lineMarker, matcher: null, body: null, position: UNKNOWN_POSITION,
@@ -1184,21 +1250,21 @@ class Dispatcher {
1184
1250
  }
1185
1251
  // {§send-response-receipt}: message content is literal; only its header selects a destination.
1186
1252
  async #respond(statement, schemeCtx, loopId) {
1187
- const captured = await MessageAttachments.capture(statement.metadata, schemeCtx.resources, "message:reply");
1188
- if ("failure" in captured)
1189
- return captured.failure;
1253
+ const options = ReplyMetadata.read(statement.metadata);
1254
+ if ("failure" in options)
1255
+ return options.failure;
1190
1256
  const target = statement.target;
1191
1257
  let answers;
1258
+ let speechOnly = false;
1192
1259
  if (target === null) {
1193
- const content = typeof statement.body === "string" ? statement.body : statement.body?.raw ?? "";
1194
- if (content.trim() === "" && captured.attachments.length === 0)
1195
- return { status: 200 };
1196
- answers = await this.#openMessages(loopId);
1197
- if (answers.length === 0) {
1198
- const original = await this.#db.engine_original_message.get({ loop_id: loopId });
1199
- if (original !== undefined)
1200
- answers = [original.path];
1201
- }
1260
+ const open = await this.#db.engine_open_messages.get({ loop_id: loopId });
1261
+ // {§message-completion} — with none open, the reply is speech to the loop's latest message:
1262
+ // delivered to its originating client, its outcome unchanged.
1263
+ const message = open ?? await this.#db.message_latest_published.get({ loop_id: loopId });
1264
+ if (message === undefined)
1265
+ return _a.#failure("send-target-required", 400, "SEND has no recipient and its loop has no published message.", {}, { recovery: "Name a recipient address.", retryable: false });
1266
+ answers = [message.path];
1267
+ speechOnly = open === undefined;
1202
1268
  }
1203
1269
  else {
1204
1270
  const message = await this.#db.message_source_by_address.get({
@@ -1208,16 +1274,20 @@ class Dispatcher {
1208
1274
  return _a.#failure("message-not-found", 404, `No accepted message exists at ${target.raw}.`, {}, { retryable: false });
1209
1275
  answers = [message.path];
1210
1276
  }
1277
+ const captured = await MessageAttachments.capture(options.metadata, schemeCtx.resources, "message:reply");
1278
+ if ("failure" in captured)
1279
+ return captured.failure;
1211
1280
  return { status: 200, answers,
1281
+ ...(options.completion === null || speechOnly ? {} : { completion: options.completion }),
1212
1282
  ...(captured.attachments.length === 0 ? {} : { attachments: MessageAttachments.receipts(captured.attachments) }) };
1213
1283
  }
1214
- // {§send-response-receipt}: published unanswered source addresses, oldest first.
1215
- async #openMessages(loopId) {
1216
- const rows = await this.#db.engine_open_messages.all({ loop_id: loopId });
1217
- return rows.map((row) => row.path);
1218
- }
1219
- async settleProgram(ctx, waits, finalResponse) {
1220
- return this.#disposition.settle(ctx, waits, finalResponse);
1284
+ async settleProgram(ctx, waits, completionAllowed) {
1285
+ const result = await this.#disposition.settle(ctx, waits, completionAllowed);
1286
+ if (typeof result.detail === "string")
1287
+ this.#notices.push(ctx.workspaceId, ctx.workerId, ctx.loopId, {
1288
+ source: "engine:turn", kind: "results_pending", level: "info", message: result.detail,
1289
+ });
1290
+ return result.status;
1221
1291
  }
1222
1292
  // {§send-premature-terminate}: judge observation boundaries after the whole program settles.
1223
1293
  async #pendingSet(workerId, turnId, loopId) {
@@ -1301,7 +1371,7 @@ class Dispatcher {
1301
1371
  static #isProposal(statement, result) {
1302
1372
  if (result.status !== 202)
1303
1373
  return false;
1304
- return !TurnDisposition.is(statement) && !TurnDisposition.isCompletion(statement);
1374
+ return !TurnDisposition.is(statement);
1305
1375
  }
1306
1376
  // Normalize a parsed target for log storage. Bare paths and `file:///...`
1307
1377
  // inputs collapse to scheme=null in log target metadata because both render
@@ -1313,7 +1383,14 @@ class Dispatcher {
1313
1383
  // `local` (bare path) carries no URL parts — store the raw text as the pathname for the log record, scheme=null.
1314
1384
  if (path.kind === "local")
1315
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}
1316
- 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;
1317
1394
  // The registered scheme owns authority disposition. Namespace authority
1318
1395
  // is path syntax; resource and owner authorities remain explicit in the
1319
1396
  // durable operation evidence.