@plurnk/plurnk-service 1.4.0 → 1.6.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 (302) hide show
  1. package/.env.defaults +37 -31
  2. package/INSTALL.md +16 -11
  3. package/README.md +1 -0
  4. package/SPEC.md +631 -397
  5. package/dist/Paths.d.ts.map +1 -1
  6. package/dist/Paths.js +2 -3
  7. package/dist/Paths.js.map +1 -1
  8. package/dist/build-info.json +1 -1
  9. package/dist/content/edit-collision.d.ts +5 -0
  10. package/dist/content/edit-collision.d.ts.map +1 -0
  11. package/dist/content/edit-collision.js +14 -0
  12. package/dist/content/edit-collision.js.map +1 -0
  13. package/dist/content/edit-receipt.d.ts +3 -2
  14. package/dist/content/edit-receipt.d.ts.map +1 -1
  15. package/dist/content/edit-receipt.js +58 -13
  16. package/dist/content/edit-receipt.js.map +1 -1
  17. package/dist/content/index.d.ts +5 -1
  18. package/dist/content/index.d.ts.map +1 -1
  19. package/dist/content/index.js +4 -1
  20. package/dist/content/index.js.map +1 -1
  21. package/dist/content/line-anchors.d.ts +41 -0
  22. package/dist/content/line-anchors.d.ts.map +1 -0
  23. package/dist/content/line-anchors.js +191 -0
  24. package/dist/content/line-anchors.js.map +1 -0
  25. package/dist/content/line-marker.d.ts +7 -3
  26. package/dist/content/line-marker.d.ts.map +1 -1
  27. package/dist/content/line-marker.js +2 -1
  28. package/dist/content/line-marker.js.map +1 -1
  29. package/dist/content/matcher.d.ts +1 -1
  30. package/dist/content/matcher.d.ts.map +1 -1
  31. package/dist/content/matcher.js +2 -2
  32. package/dist/content/matcher.js.map +1 -1
  33. package/dist/content/read-projector.d.ts +20 -0
  34. package/dist/content/read-projector.d.ts.map +1 -0
  35. package/dist/content/read-projector.js +122 -0
  36. package/dist/content/read-projector.js.map +1 -0
  37. package/dist/content/read-resolve.d.ts +2 -7
  38. package/dist/content/read-resolve.d.ts.map +1 -1
  39. package/dist/content/read-resolve.js +32 -107
  40. package/dist/content/read-resolve.js.map +1 -1
  41. package/dist/core/BranchReceipt.d.ts +0 -1
  42. package/dist/core/BranchReceipt.d.ts.map +1 -1
  43. package/dist/core/BranchReceipt.js +0 -3
  44. package/dist/core/BranchReceipt.js.map +1 -1
  45. package/dist/core/BudgetReadout.d.ts.map +1 -1
  46. package/dist/core/BudgetReadout.js +9 -2
  47. package/dist/core/BudgetReadout.js.map +1 -1
  48. package/dist/core/ChannelWrite.d.ts +3 -0
  49. package/dist/core/ChannelWrite.d.ts.map +1 -1
  50. package/dist/core/ChannelWrite.js +3 -3
  51. package/dist/core/ChannelWrite.js.map +1 -1
  52. package/dist/core/ChannelWrite.sql +4 -3
  53. package/dist/core/CoreSchemeServices.d.ts +10 -2
  54. package/dist/core/CoreSchemeServices.d.ts.map +1 -1
  55. package/dist/core/CoreSchemeServices.js.map +1 -1
  56. package/dist/core/Dispatcher.d.ts +14 -8
  57. package/dist/core/Dispatcher.d.ts.map +1 -1
  58. package/dist/core/Dispatcher.js +656 -312
  59. package/dist/core/Dispatcher.js.map +1 -1
  60. package/dist/core/DurableStatement.d.ts.map +1 -1
  61. package/dist/core/DurableStatement.js +2 -0
  62. package/dist/core/DurableStatement.js.map +1 -1
  63. package/dist/core/Engine.d.ts +24 -18
  64. package/dist/core/Engine.d.ts.map +1 -1
  65. package/dist/core/Engine.js +620 -392
  66. package/dist/core/Engine.js.map +1 -1
  67. package/dist/core/Engine.sql +188 -119
  68. package/dist/core/ExecutableTools.d.ts +10 -0
  69. package/dist/core/ExecutableTools.d.ts.map +1 -0
  70. package/dist/core/ExecutableTools.js +41 -0
  71. package/dist/core/ExecutableTools.js.map +1 -0
  72. package/dist/core/ExecutorRegistry.d.ts +3 -3
  73. package/dist/core/ExecutorRegistry.d.ts.map +1 -1
  74. package/dist/core/ExecutorRegistry.js +2 -2
  75. package/dist/core/ExecutorRegistry.js.map +1 -1
  76. package/dist/core/LogBody.d.ts +4 -1
  77. package/dist/core/LogBody.d.ts.map +1 -1
  78. package/dist/core/LogBody.js +46 -2
  79. package/dist/core/LogBody.js.map +1 -1
  80. package/dist/core/LogEntryProjection.d.ts +14 -0
  81. package/dist/core/LogEntryProjection.d.ts.map +1 -0
  82. package/dist/core/LogEntryProjection.js +38 -0
  83. package/dist/core/LogEntryProjection.js.map +1 -0
  84. package/dist/core/LoopLifecycle.d.ts +1 -2
  85. package/dist/core/LoopLifecycle.d.ts.map +1 -1
  86. package/dist/core/LoopLifecycle.js +1 -4
  87. package/dist/core/LoopLifecycle.js.map +1 -1
  88. package/dist/core/LoopLifecycle.sql +1 -4
  89. package/dist/core/ModelCall.d.ts +27 -0
  90. package/dist/core/ModelCall.d.ts.map +1 -0
  91. package/dist/core/ModelCall.js +125 -0
  92. package/dist/core/ModelCall.js.map +1 -0
  93. package/dist/core/PacketBuilder.d.ts +13 -12
  94. package/dist/core/PacketBuilder.d.ts.map +1 -1
  95. package/dist/core/PacketBuilder.js +146 -118
  96. package/dist/core/PacketBuilder.js.map +1 -1
  97. package/dist/core/ProblemLog.d.ts.map +1 -1
  98. package/dist/core/ProblemLog.js +1 -0
  99. package/dist/core/ProblemLog.js.map +1 -1
  100. package/dist/core/ProposalLifecycle.d.ts.map +1 -1
  101. package/dist/core/ProposalLifecycle.js +3 -4
  102. package/dist/core/ProposalLifecycle.js.map +1 -1
  103. package/dist/core/ProviderInstantiate.d.ts.map +1 -1
  104. package/dist/core/ProviderInstantiate.js +23 -6
  105. package/dist/core/ProviderInstantiate.js.map +1 -1
  106. package/dist/core/SchemeRegistry.js +7 -7
  107. package/dist/core/SchemeRegistry.js.map +1 -1
  108. package/dist/core/StrikeRail.d.ts +5 -2
  109. package/dist/core/StrikeRail.d.ts.map +1 -1
  110. package/dist/core/StrikeRail.js +10 -7
  111. package/dist/core/StrikeRail.js.map +1 -1
  112. package/dist/core/TerminalResult.d.ts +16 -0
  113. package/dist/core/TerminalResult.d.ts.map +1 -0
  114. package/dist/core/TerminalResult.js +49 -0
  115. package/dist/core/TerminalResult.js.map +1 -0
  116. package/dist/core/caps/CapsResolve.d.ts +5 -1
  117. package/dist/core/caps/CapsResolve.d.ts.map +1 -1
  118. package/dist/core/caps/CapsResolve.js +9 -5
  119. package/dist/core/caps/CapsResolve.js.map +1 -1
  120. package/dist/core/caps/DbChannelCaps.d.ts +1 -1
  121. package/dist/core/caps/DbChannelCaps.d.ts.map +1 -1
  122. package/dist/core/caps/DbChannelCaps.js +6 -4
  123. package/dist/core/caps/DbChannelCaps.js.map +1 -1
  124. package/dist/core/caps/DbEntryCaps.d.ts +2 -1
  125. package/dist/core/caps/DbEntryCaps.d.ts.map +1 -1
  126. package/dist/core/caps/DbEntryCaps.js +21 -8
  127. package/dist/core/caps/DbEntryCaps.js.map +1 -1
  128. package/dist/core/caps/DbNotifyCaps.d.ts +1 -1
  129. package/dist/core/caps/DbNotifyCaps.d.ts.map +1 -1
  130. package/dist/core/caps/DbNotifyCaps.js +6 -4
  131. package/dist/core/caps/DbNotifyCaps.js.map +1 -1
  132. package/dist/core/caps/DbProjectionCaps.d.ts +1 -0
  133. package/dist/core/caps/DbProjectionCaps.d.ts.map +1 -1
  134. package/dist/core/caps/DbProjectionCaps.js +29 -0
  135. package/dist/core/caps/DbProjectionCaps.js.map +1 -1
  136. package/dist/core/caps/DbSubscriptionCaps.d.ts +4 -6
  137. package/dist/core/caps/DbSubscriptionCaps.d.ts.map +1 -1
  138. package/dist/core/caps/DbSubscriptionCaps.js +27 -13
  139. package/dist/core/caps/DbSubscriptionCaps.js.map +1 -1
  140. package/dist/core/caps/SchemeCtxImpl.d.ts +11 -3
  141. package/dist/core/caps/SchemeCtxImpl.d.ts.map +1 -1
  142. package/dist/core/caps/SchemeCtxImpl.js +10 -8
  143. package/dist/core/caps/SchemeCtxImpl.js.map +1 -1
  144. package/dist/core/fork.d.ts.map +1 -1
  145. package/dist/core/fork.js +23 -7
  146. package/dist/core/fork.js.map +1 -1
  147. package/dist/core/fork.sql +35 -14
  148. package/dist/core/git-iso.d.ts +3 -2
  149. package/dist/core/git-iso.d.ts.map +1 -1
  150. package/dist/core/git-iso.js +27 -12
  151. package/dist/core/git-iso.js.map +1 -1
  152. package/dist/core/git-membership.d.ts +1 -0
  153. package/dist/core/git-membership.d.ts.map +1 -1
  154. package/dist/core/git-membership.js +87 -26
  155. package/dist/core/git-membership.js.map +1 -1
  156. package/dist/core/git-state.d.ts +8 -1
  157. package/dist/core/git-state.d.ts.map +1 -1
  158. package/dist/core/git-state.js +36 -17
  159. package/dist/core/git-state.js.map +1 -1
  160. package/dist/core/namespace.d.ts +1 -0
  161. package/dist/core/namespace.d.ts.map +1 -1
  162. package/dist/core/namespace.js +20 -0
  163. package/dist/core/namespace.js.map +1 -1
  164. package/dist/core/optimistic-settlement.d.ts +2 -0
  165. package/dist/core/optimistic-settlement.d.ts.map +1 -0
  166. package/dist/core/optimistic-settlement.js +14 -0
  167. package/dist/core/optimistic-settlement.js.map +1 -0
  168. package/dist/core/packet-wire.d.ts +5 -2
  169. package/dist/core/packet-wire.d.ts.map +1 -1
  170. package/dist/core/packet-wire.js +318 -114
  171. package/dist/core/packet-wire.js.map +1 -1
  172. package/dist/core/provider-accounting.d.ts +24 -0
  173. package/dist/core/provider-accounting.d.ts.map +1 -0
  174. package/dist/core/provider-accounting.js +76 -0
  175. package/dist/core/provider-accounting.js.map +1 -0
  176. package/dist/core/results.d.ts +4 -2
  177. package/dist/core/results.d.ts.map +1 -1
  178. package/dist/core/results.js +6 -0
  179. package/dist/core/results.js.map +1 -1
  180. package/dist/core/scheme-types.d.ts +0 -12
  181. package/dist/core/scheme-types.d.ts.map +1 -1
  182. package/dist/core/scheme-types.js.map +1 -1
  183. package/dist/core/search-gate.d.ts +2 -2
  184. package/dist/core/search-gate.d.ts.map +1 -1
  185. package/dist/core/search-gate.js +8 -8
  186. package/dist/core/search-gate.js.map +1 -1
  187. package/dist/core/token-ruler.js +1 -1
  188. package/dist/core/token-ruler.js.map +1 -1
  189. package/dist/core/turn-scheduler.js +1 -1
  190. package/dist/core/turn-scheduler.js.map +1 -1
  191. package/dist/core/worker-ops.sql +4 -4
  192. package/dist/core/workspace-settings.js +1 -1
  193. package/dist/core/workspace-settings.js.map +1 -1
  194. package/dist/digest/Digest.d.ts.map +1 -1
  195. package/dist/digest/Digest.js +349 -76
  196. package/dist/digest/Digest.js.map +1 -1
  197. package/dist/digest/digest.sql +42 -16
  198. package/dist/schemes/Exec.d.ts +4 -4
  199. package/dist/schemes/Exec.d.ts.map +1 -1
  200. package/dist/schemes/Exec.js +167 -134
  201. package/dist/schemes/Exec.js.map +1 -1
  202. package/dist/schemes/ExecOutputScheme.d.ts +4 -5
  203. package/dist/schemes/ExecOutputScheme.d.ts.map +1 -1
  204. package/dist/schemes/ExecOutputScheme.js +12 -20
  205. package/dist/schemes/ExecOutputScheme.js.map +1 -1
  206. package/dist/schemes/File.d.ts +5 -6
  207. package/dist/schemes/File.d.ts.map +1 -1
  208. package/dist/schemes/File.js +35 -41
  209. package/dist/schemes/File.js.map +1 -1
  210. package/dist/schemes/Log.d.ts +16 -5
  211. package/dist/schemes/Log.d.ts.map +1 -1
  212. package/dist/schemes/Log.js +229 -262
  213. package/dist/schemes/Log.js.map +1 -1
  214. package/dist/schemes/Log.sql +50 -27
  215. package/dist/schemes/Prompt.d.ts +3 -4
  216. package/dist/schemes/Prompt.d.ts.map +1 -1
  217. package/dist/schemes/Prompt.js +5 -5
  218. package/dist/schemes/Prompt.js.map +1 -1
  219. package/dist/schemes/Skill.d.ts +4 -5
  220. package/dist/schemes/Skill.d.ts.map +1 -1
  221. package/dist/schemes/Skill.js +3 -3
  222. package/dist/schemes/Skill.js.map +1 -1
  223. package/dist/schemes/Worker.d.ts +7 -7
  224. package/dist/schemes/Worker.d.ts.map +1 -1
  225. package/dist/schemes/Worker.js +99 -84
  226. package/dist/schemes/Worker.js.map +1 -1
  227. package/dist/schemes/_entry-crud.d.ts +2 -2
  228. package/dist/schemes/_entry-crud.d.ts.map +1 -1
  229. package/dist/schemes/_entry-crud.js +29 -10
  230. package/dist/schemes/_entry-crud.js.map +1 -1
  231. package/dist/schemes/_entry-crud.sql +11 -17
  232. package/dist/schemes/_entry-find.d.ts +41 -14
  233. package/dist/schemes/_entry-find.d.ts.map +1 -1
  234. package/dist/schemes/_entry-find.js +159 -166
  235. package/dist/schemes/_entry-find.js.map +1 -1
  236. package/dist/schemes/_entry-find.sql +2 -21
  237. package/dist/schemes/_entry-graph.sql +1 -1
  238. package/dist/schemes/_entry-manifest.d.ts +5 -17
  239. package/dist/schemes/_entry-manifest.d.ts.map +1 -1
  240. package/dist/schemes/_entry-manifest.js +28 -24
  241. package/dist/schemes/_entry-manifest.js.map +1 -1
  242. package/dist/schemes/_entry-ops.d.ts +13 -8
  243. package/dist/schemes/_entry-ops.d.ts.map +1 -1
  244. package/dist/schemes/_entry-ops.js +98 -119
  245. package/dist/schemes/_entry-ops.js.map +1 -1
  246. package/dist/schemes/_entry-ops.sql +31 -6
  247. package/dist/schemes/_entry-semantic.d.ts +5 -7
  248. package/dist/schemes/_entry-semantic.d.ts.map +1 -1
  249. package/dist/schemes/_entry-semantic.js +10 -31
  250. package/dist/schemes/_entry-semantic.js.map +1 -1
  251. package/dist/schemes/_entry-semantic.sql +2 -2
  252. package/dist/schemes/_entry-send.js +4 -4
  253. package/dist/schemes/_entry-send.js.map +1 -1
  254. package/dist/schemes/_search-index.d.ts.map +1 -1
  255. package/dist/schemes/_search-index.js +16 -6
  256. package/dist/schemes/_search-index.js.map +1 -1
  257. package/dist/schemes/exec-abort.js +2 -2
  258. package/dist/schemes/exec-abort.js.map +1 -1
  259. package/dist/server/BranchBatches.d.ts +1 -0
  260. package/dist/server/BranchBatches.d.ts.map +1 -1
  261. package/dist/server/BranchBatches.js +1 -0
  262. package/dist/server/BranchBatches.js.map +1 -1
  263. package/dist/server/Daemon.d.ts +6 -8
  264. package/dist/server/Daemon.d.ts.map +1 -1
  265. package/dist/server/Daemon.js +238 -68
  266. package/dist/server/Daemon.js.map +1 -1
  267. package/dist/server/DaemonModule.d.ts +3 -3
  268. package/dist/server/DaemonModule.d.ts.map +1 -1
  269. package/dist/server/branch-batch.sql +3 -1
  270. package/dist/server/client-input.d.ts +2 -1
  271. package/dist/server/client-input.d.ts.map +1 -1
  272. package/dist/server/client-input.js +7 -1
  273. package/dist/server/client-input.js.map +1 -1
  274. package/dist/server/drain.sql +8 -7
  275. package/dist/server/envelope.d.ts +0 -2
  276. package/dist/server/envelope.d.ts.map +1 -1
  277. package/dist/server/envelope.js +0 -1
  278. package/dist/server/envelope.js.map +1 -1
  279. package/dist/server/envelope.sql +3 -4
  280. package/dist/server/lifecycle-recovery.sql +44 -1
  281. package/dist/server/logEntry.d.ts +2 -1
  282. package/dist/server/logEntry.d.ts.map +1 -1
  283. package/dist/server/logEntry.js +15 -2
  284. package/dist/server/logEntry.js.map +1 -1
  285. package/dist/server/logEntry.sql +5 -1
  286. package/dist/service.d.ts.map +1 -1
  287. package/dist/service.js +13 -1
  288. package/dist/service.js.map +1 -1
  289. package/migrations/001_schema.sql +730 -221
  290. package/package.json +53 -46
  291. package/dist/core/BudgetOverflow.d.ts +0 -23
  292. package/dist/core/BudgetOverflow.d.ts.map +0 -1
  293. package/dist/core/BudgetOverflow.js +0 -61
  294. package/dist/core/BudgetOverflow.js.map +0 -1
  295. package/dist/core/caps/DbTagCaps.d.ts +0 -10
  296. package/dist/core/caps/DbTagCaps.d.ts.map +0 -1
  297. package/dist/core/caps/DbTagCaps.js +0 -44
  298. package/dist/core/caps/DbTagCaps.js.map +0 -1
  299. package/dist/matrix/live-specimen.d.ts +0 -9
  300. package/dist/matrix/live-specimen.d.ts.map +0 -1
  301. package/dist/matrix/live-specimen.js +0 -37
  302. package/dist/matrix/live-specimen.js.map +0 -1
@@ -7,10 +7,11 @@
7
7
  // Format and omission rules are owned by {§packet-markdown}. Section producers
8
8
  // supply names and typed content; this projection preserves their ordered evidence.
9
9
  import { Validator } from "@plurnk/plurnk-contracts";
10
- import { Results as SchemeResults } from "@plurnk/plurnk-schemes";
10
+ import { TextCoordinates } from "@plurnk/plurnk-mimetypes";
11
11
  import { renderTarget } from "./plurnk-uri.js";
12
12
  import LogBody from "./LogBody.js";
13
- import { assertEditReceipt, assertResourceEffects, } from "../content/index.js";
13
+ import LogEntryProjection from "./LogEntryProjection.js";
14
+ import { assertEditReceipt, assertResourceEffects, LineAnchors, } from "../content/index.js";
14
15
  const editReceiptRevisionChars = () => {
15
16
  const raw = process.env.PLURNK_SERVICE_EDIT_RECEIPT_REVISION_CHARS;
16
17
  const value = Number(raw);
@@ -91,23 +92,24 @@ export default class PacketWire {
91
92
  return items.map((r) => `* ${String(r.status)} ${String(r.path)}`).join("\n");
92
93
  }
93
94
  // The git section content: the working-tree summary. "" when absent.
94
- static renderGit(git) {
95
- return git === null || git === undefined ? "" : PacketWire.#renderGitState(git);
95
+ static renderGit(git, assignedBranch = null) {
96
+ const status = git === null || git === undefined ? "" : PacketWire.#renderGitState(git);
97
+ const assignment = assignedBranch === null
98
+ ? ""
99
+ : `assigned branch \`${assignedBranch}\` — commit any project changes and leave the checkout clean before concluding`;
100
+ return [status, assignment].filter((line) => line.length > 0).join("\n");
96
101
  }
97
102
  // The log section's content: the model's curated rows as a fenced `jsonplurnk` array ({§jsonplurnk}).
98
103
  // Data only — no prose leads the fence (the log carries rules for no one). Empty log → ""
99
104
  // (the section is omitted).
100
- static renderLog(entries, countTokens) {
105
+ static renderLog(entries, countTokens, options = {}) {
101
106
  const log = Array.isArray(entries) ? entries : [];
102
107
  if (log.length === 0)
103
108
  return "";
104
- const items = PacketWire.#renderLogEntries(log, countTokens);
105
- // The opening fence is DYNAMIC — one backtick longer than the longest run in the rendered
106
- // entries — so a code sample inside a body can never close the block early (CommonMark closes
107
- // a fence only on a line of ≥ its own length). {§jsonplurnk-dynamic-fence}
108
- const longestTicks = Math.max(0, ...[...items.matchAll(/`+/g)].map((m) => m[0].length));
109
- const fence = "`".repeat(Math.max(3, longestTicks + 1));
110
- return `${fence}jsonplurnk\n[\n${items}\n]\n${fence}`;
109
+ const items = PacketWire.#renderLogEntries(log, countTokens, options);
110
+ // Every source line is coordinate-prefixed, so source backticks never occupy the
111
+ // CommonMark closing-fence position. The fixed opener keeps the packet prefix cache-stable.
112
+ return `\`\`\`jsonplurnk\n[\n${items}\n]\n\`\`\``;
111
113
  }
112
114
  // Read one section's content by name off a packet (Engine's or re-parsed).
113
115
  // The legible accessor — consumers name the section they want instead of
@@ -126,31 +128,33 @@ export default class PacketWire {
126
128
  { role: "user", content: PacketWire.renderSlot(sections, "user") },
127
129
  ];
128
130
  }
129
- // Number each line of body as `<N>:<line>` — a bare `N:` prefix, NO separator whitespace
131
+ // Number a non-READ body line as `<N>:<line>` — a bare `N:` prefix, NO separator whitespace
130
132
  // ({§render-rule-line-navigable-prefix}): the leading digit prevents column-zero fence collisions and gives
131
- // the model line refs for free (`READ<42-46>`), while the absence of any separator means a
133
+ // the model line refs for free (`## READ0 (...) <42-46>`), while the absence of any separator means a
132
134
  // reproduced line has nothing between `N:` and the content to copy — the hard-tab separator used
133
135
  // to leak into edit bodies and corrupt indentation. The content's OWN leading whitespace is
134
136
  // content, preserved verbatim.
135
137
  // Used for READ@200 content; index-preview numbering is the framework's
136
- // job now (baked into the preview string — see renderHeredoc).
138
+ // job now (baked into the preview string).
137
139
  static #numberLines(body, start = 1) {
138
140
  let line = start;
139
- return body.replace(/(^|\r\n|\r|\n)(?=[\s\S])/g, (separator) => `${separator}${line++}:`);
141
+ return `${line++}:${body.replace(/(\r\n|\r(?!\n)|\n)(?=[\s\S])/g, (separator) => `${separator}${line++}:`)}`;
140
142
  }
141
- // The single content-body renderer EVERY output-emitting op routes through, so the line-number
142
- // convention the model orients on can't drift. Every textual body receives
143
- // the `N:` prefix from `startLine`; matchers consume canonical content before
144
- // this presentation projection. Empty content produces no body.
145
- static #renderContentBody(fence, content, startLine = 1) {
143
+ // The single content-body renderer EVERY output-emitting op routes through.
144
+ // Exact READ content receives source-width-aligned `@hash N:`; other textual bodies receive `N:`.
145
+ // Matchers consume canonical content before this presentation projection.
146
+ // Empty content produces no body.
147
+ static #renderContentBody(content, startLine = 1, lineAnchors = null, lineNumberWidth = null) {
146
148
  if (content.length === 0)
147
149
  return "";
148
150
  // `startLine === null` means the producer already supplied numbered
149
151
  // content; re-numbering would duplicate its coordinates.
150
152
  const rendered = startLine !== null
151
- ? PacketWire.#numberLines(content, startLine)
153
+ ? lineAnchors === null
154
+ ? PacketWire.#numberLines(content, startLine)
155
+ : LineAnchors.render(content, startLine, lineAnchors, lineNumberWidth ?? 0)
152
156
  : content;
153
- return PacketWire.#wrapHeredocBody(fence, rendered);
157
+ return PacketWire.#quoteBody(rendered);
154
158
  }
155
159
  // Tolerant JSON parser for log entries' persisted rx/tx strings. The engine
156
160
  // pre-parses application/json mimetypes; malformed stored text is not JSON.
@@ -176,6 +180,7 @@ export default class PacketWire {
176
180
  const head = {
177
181
  rev: receipt.revision.slice(0, editReceiptRevisionChars()),
178
182
  extent: `${receipt.unit} ${receipt.before}->${receipt.after}`,
183
+ ...(receipt.parseIssues === undefined ? {} : { parseIssues: receipt.parseIssues }),
179
184
  };
180
185
  if ("effect" in receipt) {
181
186
  return {
@@ -196,18 +201,19 @@ export default class PacketWire {
196
201
  }),
197
202
  };
198
203
  }
199
- // Wrap a body in heredoc fences. The fence is FIXED (`<<BODY`/`BODY`), never the
200
- // target: every body line is `N:`-prefixed, so a bare fence line can never occur
201
- // inside a body, and a fixed fence gives the model nothing address-shaped to
202
- // mimic back as op syntax — identity lives in the adjacent meta JSON. Leading
203
- // `\n` always (separates the opening fence from the first body character).
204
- // Trailing `\n` only when the body doesn't already end with one — otherwise you
205
- // get a doubled newline that renders as a blank line before the closing fence,
206
- // which reads as "the content has a trailing blank line" when actually it
207
- // doesn't. The body's own whitespace decides the shape.
208
- static #wrapHeredocBody(fence, body) {
209
- const sep = body.endsWith("\n") ? "" : "\n";
210
- return `<<BODY\n${body}${sep}BODY`;
204
+ // {§jsonplurnk} One deliberately raw multiline JSON string. The physical
205
+ // newline after the opening quote and every positive numeric or anchored coordinate prefix
206
+ // make the closing quote at column zero unambiguous without an invented
207
+ // delimiter for source text to imitate. Already-numbered producer output is
208
+ // checked here too: malformed bodies fail at the one projection boundary.
209
+ static #quoteBody(body) {
210
+ const endsWithLineBreak = /(?:\r\n|\r|\n)$/.test(body);
211
+ const lines = body.split(/\r\n|\r|\n/);
212
+ const contentLines = endsWithLineBreak ? lines.slice(0, -1) : lines;
213
+ if (contentLines.length === 0 || contentLines.some((line) => !/^[1-9]\d*:/.test(line) && !LineAnchors.isAnchoredLine(line))) {
214
+ throw new TypeError("A raw jsonplurnk body requires a positive coordinate prefix on every physical line.");
215
+ }
216
+ return `"\n${body}${endsWithLineBreak ? "" : "\n"}"`;
211
217
  }
212
218
  // Render one Log entry → a single bullet line carrying the meta JSON.
213
219
  // No body, no fence — every meaningful field is in the JSON. Naming
@@ -229,38 +235,180 @@ export default class PacketWire {
229
235
  return null;
230
236
  return op !== null ? `log:///${coordinate}/${op}` : `log:///${coordinate}`;
231
237
  }
238
+ static #offsetAfterCharacters(text, count) {
239
+ let offset = 0;
240
+ let consumed = 0;
241
+ while (offset < text.length && consumed < count) {
242
+ if (text.startsWith("\r\n", offset)) {
243
+ offset += 2;
244
+ }
245
+ else {
246
+ const codePoint = text.codePointAt(offset);
247
+ if (codePoint === undefined)
248
+ break;
249
+ offset += String.fromCodePoint(codePoint).length;
250
+ }
251
+ consumed++;
252
+ }
253
+ return offset;
254
+ }
232
255
  // One preview function for every bounded model-facing projection. Lines
233
- // protect ordinary documents and chars protect a single-line bomb.
256
+ // protect ordinary documents and Unicode characters protect a single-line
257
+ // bomb. Once a physical line is complete, a character cut retreats to that
258
+ // line boundary rather than exposing a partial coordinate prefix.
234
259
  static #preview(text) {
235
260
  const { lines: maxLines, chars: maxChars } = previewBounds();
236
- let lineEnd = text.length;
237
- let newline = -1;
238
- for (let line = 0; line < maxLines; line++) {
239
- newline = text.indexOf("\n", newline + 1);
240
- if (newline === -1)
261
+ const coordinates = new TextCoordinates(text);
262
+ const physicalLines = coordinates.logicalLines();
263
+ const lineEnd = physicalLines.length > maxLines
264
+ ? physicalLines[maxLines - 1].end
265
+ : text.length;
266
+ const characterEnd = PacketWire.#offsetAfterCharacters(text, maxChars);
267
+ let end = Math.min(lineEnd, characterEnd);
268
+ if (characterEnd < text.length && characterEnd <= lineEnd) {
269
+ const completeLine = physicalLines.findLast((line) => line.separator.length > 0 && line.end <= characterEnd);
270
+ if (completeLine !== undefined)
271
+ end = completeLine.end;
272
+ }
273
+ const cut = end < text.length;
274
+ return {
275
+ text: text.slice(0, end),
276
+ cut,
277
+ chunk: cut ? PacketWire.#chunk(coordinates, physicalLines, end, text.length) : null,
278
+ };
279
+ }
280
+ static #formatRegion(region) {
281
+ return `<${region.startLine},${region.startColumn},${region.endLine},${region.endColumn}>`;
282
+ }
283
+ static #chunk(coordinates, lines, end, completeEnd) {
284
+ const finalCompleteLine = lines.findIndex((line) => line.separator.length > 0 && line.end === end);
285
+ if (finalCompleteLine !== -1) {
286
+ const selected = `<1,${finalCompleteLine + 1}>`;
287
+ const complete = `<1,${lines.length}>`;
288
+ if (selected === complete) {
289
+ throw new Error("a bounded body chunk must differ from its complete line extent");
290
+ }
291
+ return `showing ${selected} of ${complete}`;
292
+ }
293
+ const selectedRegion = coordinates.regionFromOffsets(0, end);
294
+ const completeRegion = coordinates.regionFromOffsets(0, completeEnd);
295
+ if (selectedRegion === null || completeRegion === null) {
296
+ throw new Error("a character-bound body chunk must resolve to exact text coordinates");
297
+ }
298
+ const selected = PacketWire.#formatRegion(selectedRegion);
299
+ const complete = PacketWire.#formatRegion(completeRegion);
300
+ if (selected === complete) {
301
+ throw new Error("a bounded body chunk must differ from its complete text extent");
302
+ }
303
+ return `showing ${selected} of ${complete}`;
304
+ }
305
+ static #promptProjection(body, budget, countTokens) {
306
+ const weightAt = (end) => countTokens(PacketWire.#renderContentBody(body.content.slice(0, end), body.startLine, null));
307
+ if (weightAt(body.content.length) <= budget) {
308
+ return { text: body.content, cut: false, chunk: null };
309
+ }
310
+ if (budget <= 0)
311
+ return { text: "", cut: true, chunk: null };
312
+ const coordinates = new TextCoordinates(body.content);
313
+ const lines = coordinates.logicalLines();
314
+ const completeLineEnds = lines
315
+ .filter((line) => line.separator.length > 0 && line.end < body.content.length)
316
+ .map((line) => line.end);
317
+ let low = 0;
318
+ let high = completeLineEnds.length;
319
+ while (low < high) {
320
+ const middle = Math.floor((low + high) / 2);
321
+ if (weightAt(completeLineEnds[middle]) <= budget)
322
+ low = middle + 1;
323
+ else
324
+ high = middle;
325
+ }
326
+ const completeLineEnd = low === 0 ? 0 : completeLineEnds[low - 1];
327
+ if (completeLineEnd > 0) {
328
+ return {
329
+ text: body.content.slice(0, completeLineEnd),
330
+ cut: true,
331
+ chunk: PacketWire.#chunk(coordinates, lines, completeLineEnd, body.content.length),
332
+ };
333
+ }
334
+ const firstLineEnd = lines[0]?.contentEnd ?? body.content.length;
335
+ const offsets = [0];
336
+ let offset = 0;
337
+ for (const codePoint of body.content.slice(0, firstLineEnd)) {
338
+ offset += codePoint.length;
339
+ offsets.push(offset);
340
+ }
341
+ low = 0;
342
+ high = offsets.length;
343
+ while (low < high) {
344
+ const middle = Math.floor((low + high) / 2);
345
+ if (weightAt(offsets[middle]) <= budget)
346
+ low = middle + 1;
347
+ else
348
+ high = middle;
349
+ }
350
+ const characterEnd = low === 0 ? 0 : offsets[low - 1];
351
+ if (characterEnd === 0)
352
+ return { text: "", cut: true, chunk: null };
353
+ return {
354
+ text: body.content.slice(0, characterEnd),
355
+ cut: true,
356
+ chunk: PacketWire.#chunk(coordinates, lines, characterEnd, body.content.length),
357
+ };
358
+ }
359
+ static #promptBudgets(entries, bodies, countTokens, budget) {
360
+ if (budget === undefined)
361
+ return new Map();
362
+ if (!Number.isSafeInteger(budget) || budget < 0) {
363
+ throw new RangeError(`promptProjectionBudget must be a non-negative safe integer, got ${JSON.stringify(budget)}`);
364
+ }
365
+ const costs = entries.flatMap((entry, index) => {
366
+ if (entry.op !== "prompt" || entry.folded === true || bodies[index].content.length === 0)
367
+ return [];
368
+ const rendered = PacketWire.#renderContentBody(bodies[index].content, bodies[index].startLine, null);
369
+ return [{ index, cost: countTokens(rendered) }];
370
+ });
371
+ const allocations = new Map();
372
+ let remaining = budget;
373
+ let active = costs;
374
+ while (active.length > 0) {
375
+ const share = Math.floor(remaining / active.length);
376
+ const complete = active.filter(({ cost }) => cost <= share);
377
+ if (complete.length === 0) {
378
+ const extra = remaining % active.length;
379
+ active.forEach(({ index }, position) => allocations.set(index, share + (position < extra ? 1 : 0)));
241
380
  break;
242
- if (line === maxLines - 1)
243
- lineEnd = newline + 1;
381
+ }
382
+ for (const { index, cost } of complete) {
383
+ allocations.set(index, cost);
384
+ remaining -= cost;
385
+ }
386
+ const completed = new Set(complete.map(({ index }) => index));
387
+ active = active.filter(({ index }) => !completed.has(index));
244
388
  }
245
- const end = Math.min(text.length, lineEnd, maxChars);
246
- return { text: text.slice(0, end), cut: end < text.length };
389
+ return allocations;
247
390
  }
248
- static #renderLogEntries(entries, countTokens) {
249
- return entries.map((e) => {
391
+ static #renderLogEntries(entries, countTokens, options) {
392
+ const bodies = entries.map((e) => {
393
+ const op = typeof e.op === "string" && e.op.length > 0 ? e.op : null;
394
+ return LogBody.resolve({
395
+ op,
396
+ attrs: e.attrs,
397
+ tx: e.tx,
398
+ rx: e.rx,
399
+ mimetypeTx: typeof e.mimetype_tx === "string" ? e.mimetype_tx : undefined,
400
+ mimetypeRx: typeof e.mimetype_rx === "string" ? e.mimetype_rx : undefined,
401
+ });
402
+ });
403
+ const promptBudgets = PacketWire.#promptBudgets(entries, bodies, countTokens, options.promptProjectionBudget);
404
+ return entries.map((e, index) => {
250
405
  const meta = {};
251
406
  const coordinate = typeof e.coordinate === "string" ? e.coordinate : null;
252
407
  const op = typeof e.op === "string" && e.op.length > 0 ? e.op : null;
253
- // An executor entry sink is durably journaled as the system EDIT that
254
- // created the entry. That storage fact is not the model-facing action:
255
- // the resulting resource is ordinary readable state pushed into its
256
- // environment. Project it as a folded system READ so the log advertises
257
- // the available operation instead of implying that the model or another
258
- // agent authored a mutation. The coordinate still resolves the same
259
- // underlying row; the typed attrs preserve exact replay/client semantics.
260
- const materializedEntry = e.origin === "plurnk" && op === "EDIT"
261
- && e.attrs !== null && typeof e.attrs === "object"
262
- && e.attrs.kind === "entry_materialized";
263
- const renderedOp = materializedEntry ? "READ" : op;
408
+ const renderedOp = LogEntryProjection.op(e);
409
+ const actionlessKind = op === null
410
+ ? LogBody.actionlessKind({ op, attrs: e.attrs })
411
+ : null;
264
412
  const path = PacketWire.#entryPath(coordinate, renderedOp);
265
413
  if (path !== null)
266
414
  meta.path = path;
@@ -270,10 +418,29 @@ export default class PacketWire {
270
418
  // subsystem token when present; absence means the owning worker.
271
419
  if (typeof e.source === "string" && e.source.length > 0)
272
420
  meta.source = e.source;
273
- if (renderedOp !== null)
274
- meta.op = renderedOp;
421
+ if (actionlessKind !== null)
422
+ meta.kind = actionlessKind;
423
+ if (e.source === "file" && e.attrs !== null && typeof e.attrs === "object" && "git" in e.attrs) {
424
+ const git = e.attrs.git;
425
+ if (typeof git !== "string" || git.length !== 2) {
426
+ throw new TypeError("A source=file log row carries malformed Git XY metadata.");
427
+ }
428
+ meta.git = git;
429
+ }
275
430
  if (typeof e.status === "number")
276
431
  meta.status = e.status;
432
+ if (e.tags !== undefined) {
433
+ const storedTags = e.tags;
434
+ if (!Array.isArray(storedTags) || !storedTags.every((tag) => typeof tag === "string" && tag.length > 0)) {
435
+ throw new TypeError("A log row carries malformed folksonomic tags.");
436
+ }
437
+ const tags = [...new Set(storedTags)].toSorted();
438
+ if (tags.length !== storedTags.length || tags.some((tag, index) => tag !== storedTags[index])) {
439
+ throw new TypeError("A log row's folksonomic tags must be unique and sorted.");
440
+ }
441
+ if (tags.length > 0)
442
+ meta.tags = tags;
443
+ }
277
444
  const tx = (typeof e.tx === "string" ? PacketWire.#safeParse(e.tx) : e.tx);
278
445
  const target = PacketWire.#renderActionTarget(e.target);
279
446
  if (op === "COPY" || op === "MOVE") {
@@ -297,7 +464,7 @@ export default class PacketWire {
297
464
  meta.target = target;
298
465
  }
299
466
  // EXEC's output is a separate stream entry ({§exec-stream}); its address rides in a
300
- // `stream` link, distinct from `target` (the cwd / executable path it ran in).
467
+ // `stream` link, distinct from the runtime-owned invocation target.
301
468
  if (op === "EXEC" && e.attrs !== null && typeof e.attrs === "object" && typeof e.attrs.stream === "string") {
302
469
  meta.stream = e.attrs.stream;
303
470
  }
@@ -310,107 +477,144 @@ export default class PacketWire {
310
477
  Validator.assertProblemDetails(problem);
311
478
  meta.problem = problem;
312
479
  }
313
- // READ + FIND enrichment: the matcher body, READ match coordinates,
314
- // and FIND resource count. Match coordinates let the model choose a
315
- // surgical follow-up READ without silently narrowing this result.
316
- let items = null;
480
+ // {§retrieval-packet-metadata}: one extent/coordinate owner plus
481
+ // only FIND aggregates that add information beyond that extent.
482
+ let findItems = null;
317
483
  if (op === "READ" || op === "FIND") {
318
- if (tx !== null && tx !== undefined && typeof tx === "object" && tx.body !== null && typeof tx.body === "object") {
319
- if (typeof tx.body.raw === "string")
320
- meta.matcher = tx.body.raw;
484
+ const findMatcher = op === "FIND"
485
+ && tx !== null
486
+ && tx !== undefined
487
+ && typeof tx === "object"
488
+ && tx.body !== null
489
+ && typeof tx.body === "object";
490
+ if (findMatcher) {
491
+ const body = tx?.body;
492
+ if (body !== null && typeof body === "object" && typeof body.raw === "string") {
493
+ meta.matcher = body.raw;
494
+ }
321
495
  }
322
- if (op === "FIND" && rx !== null && typeof rx === "object" && typeof rx.omittedItems === "number") {
323
- items = rx.omittedItems; // {§find-count-not-contents} - selected-resource count, though rows were not enumerated
324
- }
325
- else if (op === "FIND" && rx !== null && typeof rx === "object" && typeof rx.content === "string") {
496
+ if (op === "FIND" && rx !== null && typeof rx === "object" && typeof rx.content === "string") {
326
497
  const parsed = PacketWire.#safeParse(rx.content);
327
498
  if (Array.isArray(parsed))
328
- items = parsed.length;
499
+ findItems = parsed.length;
500
+ }
501
+ const range = rx !== null && typeof rx === "object" && rx.range !== undefined
502
+ ? rx.range
503
+ : undefined;
504
+ const problemOwnsRange = typeof e.status === "number"
505
+ && e.status >= 400
506
+ && meta.problem !== null
507
+ && typeof meta.problem === "object"
508
+ && Object.hasOwn(meta.problem, "range");
509
+ if (problemOwnsRange) {
510
+ Validator.assertRangeExtent(meta.problem.range);
329
511
  }
330
- if (op === "READ" && rx !== null && typeof rx === "object" && rx.matches !== undefined) {
331
- meta.matches = SchemeResults.assertMatchEvidenceList(rx.matches);
512
+ if (range !== undefined && !problemOwnsRange) {
513
+ meta.range = Validator.assertRangeExtent(range);
332
514
  }
333
- if (op === "READ" && rx !== null && typeof rx === "object" && rx.region !== undefined) {
515
+ else if (op === "READ" && rx !== null && typeof rx === "object" && rx.region !== undefined) {
334
516
  meta.region = Validator.assertTextRegion(rx.region);
335
517
  }
336
- // The matched set's content weight (sum of the entries' live channel tokens) — the
337
- // FIND self-describes its hits' READ-weight; carries the per-scheme roll-up in the foist.
338
- if (op === "FIND" && rx !== null && typeof rx === "object" && typeof rx.itemsTokenTotal === "number") {
518
+ // These are underlying selected-content weights, distinct from
519
+ // the emitted body's generic `tokens` measurement.
520
+ if (op === "FIND" && rx !== null && typeof rx === "object" && typeof rx.itemsTokenTotal === "number" && rx.itemsTokenTotal > 0) {
339
521
  meta.itemsTokenTotal = rx.itemsTokenTotal;
340
522
  }
523
+ if (op === "FIND"
524
+ && rx !== null
525
+ && typeof rx === "object"
526
+ && typeof rx.returnedItemsTokenTotal === "number"
527
+ && rx.returnedItemsTokenTotal > 0
528
+ && rx.returnedItemsTokenTotal !== rx.itemsTokenTotal) {
529
+ meta.returnedItemsTokenTotal = rx.returnedItemsTokenTotal;
530
+ }
531
+ if (findMatcher
532
+ && range !== null
533
+ && typeof range === "object"
534
+ && range.unit === "resource"
535
+ && rx !== null
536
+ && typeof rx === "object"
537
+ && typeof rx.matchLocationCount === "number"
538
+ && rx.matchLocationCount > 0) {
539
+ meta.matchLocationCount = rx.matchLocationCount;
540
+ }
341
541
  }
342
542
  // {§edit-result-receipt-projection} {§edit-result-copy-move-effects}
343
543
  // Mutations expose compact, validated outcome metadata. EDIT owns
344
544
  // one receipt; COPY/MOVE own ordered resource effects whose
345
545
  // optional receipts describe scoped textual materializations.
546
+ let structuredMutationReceipt = false;
346
547
  if (op === "EDIT" && rx !== null && typeof rx === "object" && Object.hasOwn(rx, "receipt")) {
347
548
  Object.assign(meta, PacketWire.#receiptMeta(rx.receipt));
549
+ structuredMutationReceipt = true;
348
550
  }
349
551
  if ((op === "COPY" || op === "MOVE")
350
552
  && rx !== null
351
553
  && typeof rx === "object"
352
554
  && Object.hasOwn(rx, "effects")) {
353
- meta.effects = assertResourceEffects(rx.effects).map((effect) => ({
555
+ const effects = assertResourceEffects(rx.effects);
556
+ meta.effects = effects.map((effect) => ({
354
557
  target: effect.target,
355
558
  action: effect.action,
356
559
  ...(effect.receipt === undefined
357
560
  ? {}
358
561
  : PacketWire.#receiptMeta(effect.receipt)),
359
562
  }));
563
+ structuredMutationReceipt = effects.some((effect) => effect.receipt !== undefined);
360
564
  }
361
- // The canonical full body is shared with READ(log://), FIND(log://),
362
- // and search derivation. Only a worker-issued READ/FIND result renders
363
- // complete; every pushed, authored, ambient, plugin, and engine body
364
- // uses this one preview projection.
365
- const fullBody = LogBody.resolve({
366
- op: op ?? "",
367
- tx: e.tx,
368
- rx: e.rx,
369
- mimetypeTx: typeof e.mimetype_tx === "string" ? e.mimetype_tx : undefined,
370
- mimetypeRx: typeof e.mimetype_rx === "string" ? e.mimetype_rx : undefined,
371
- });
372
- const emptyFind = op === "FIND" && e.status === 200 && items === 0;
373
- const previewExempt = e.origin === "model" && (op === "READ" || op === "FIND");
374
- const projection = previewExempt
375
- ? { text: fullBody.content, cut: false }
376
- : PacketWire.#preview(fullBody.content);
377
- const resourceBody = op === "READ" || op === "FIND" || op === "EDIT"
378
- || op === "prompt";
379
- const bodyFence = resourceBody ? (target ?? path) : path;
565
+ // The canonical full body is shared with log READ, log FIND,
566
+ // and search derivation. READ/FIND own selection bounds, PLAN is
567
+ // persistent working memory, and prompt rows share their packet
568
+ // allowance. Structured mutation receipts own their join bound;
569
+ // every remaining body uses the ordinary fixed preview.
570
+ const fullBody = bodies[index];
571
+ const emptyFind = op === "FIND" && e.status === 200 && findItems === 0;
572
+ const previewExempt = op === "READ"
573
+ || op === "FIND"
574
+ || op === "PLAN"
575
+ || structuredMutationReceipt;
576
+ const promptBudget = promptBudgets.get(index);
577
+ const projection = promptBudget !== undefined
578
+ ? PacketWire.#promptProjection(fullBody, promptBudget, countTokens)
579
+ : previewExempt
580
+ ? { text: fullBody.content, cut: false, chunk: null }
581
+ : PacketWire.#preview(fullBody.content);
380
582
  if (projection.cut && path === null) {
381
583
  throw new Error("a previewed log body requires an addressable log path");
382
584
  }
383
- if (projection.cut) {
384
- meta.overflow = `Body content truncated. Use READ ${path} to view the full body.`;
385
- }
585
+ const lineAnchors = op === "READ" ? e.lineAnchors ?? null : null;
586
+ const lineNumberWidth = op === "READ" ? e.lineNumberWidth ?? null : null;
386
587
  const body = emptyFind || projection.text.length === 0
387
588
  ? ""
388
- : PacketWire.#renderContentBody(bodyFence ?? `log:///${coordinate}`, projection.text, fullBody.startLine);
589
+ : PacketWire.#renderContentBody(projection.text, fullBody.startLine, lineAnchors, lineNumberWidth);
389
590
  // tokens on EVERY row (0 when there's genuinely no body) so the model can always weigh
390
- // it; for a folded row this is the room an OPEN would add. items present even at 0.
391
- if (items !== null)
392
- meta.items = items;
591
+ // it; for a folded row this is the room an OPEN would add.
393
592
  // 0 for a genuinely empty body — never call countTokens("") (some providers return
394
593
  // undefined for it, which JSON.stringify would drop, leaving the row with no tokens).
395
594
  meta.tokens = body.length > 0 ? countTokens(body) : 0;
396
- // lines beside tokens on any row with a navigable body — the count of `N:`-numbered
397
- // lines (fences and unnumbered prose don't count), so the model can plan a <start,end>
398
- // slice before paying for an OPEN. Omitted when the body isn't line-addressable.
399
- if (body.length > 0) {
595
+ // lines beside tokens on a non-retrieval row with a navigable body — the count of
596
+ // `N:`-numbered lines (fences and unnumbered prose don't count), so the model can plan
597
+ // a <start,end> slice before paying for an OPEN. READ/FIND own typed extents instead.
598
+ if (body.length > 0 && op !== "READ" && op !== "FIND") {
400
599
  const navigable = body.split("\n").filter((l) => /^\d+:/.test(l)).length;
401
600
  if (navigable > 0)
402
601
  meta.lines = navigable;
403
602
  }
404
603
  // {§jsonplurnk} — `display` describes the three body states: `none` carries an explicit
405
604
  // empty JSON string, `folded` withholds an existing body, and `open` appends that body as
406
- // the format's one non-JSON value (a raw, tagged heredoc). The explicit empty body keeps
605
+ // the format's one raw multiline string. The explicit empty body keeps
407
606
  // every state self-describing; OPEN/FOLD remain friendly no-ops on `none`.
408
607
  const display = body.length === 0 ? "none" : e.folded === true ? "folded" : "open";
409
608
  meta.display = display;
410
609
  if (display === "none")
411
610
  meta.body = "";
412
611
  const obj = PacketWire.#canonicalJson(meta);
413
- return display === "open" ? obj.replace(/\}$/, `,"body":\n${body}\n}`) : obj;
612
+ if (display !== "open")
613
+ return obj;
614
+ const chunk = projection.chunk !== null
615
+ ? `,"chunk":${JSON.stringify(projection.chunk)}`
616
+ : "";
617
+ return obj.replace(/\}$/, `,"body":${body}${chunk}}`);
414
618
  }).join(",\n");
415
619
  }
416
620
  static #renderActionTarget(target) {
@@ -437,9 +641,9 @@ export default class PacketWire {
437
641
  return null;
438
642
  if (marker === null || marker === undefined)
439
643
  return address;
440
- const validation = Validator.validateLineMarker(marker);
644
+ const validation = Validator.validateTextLineMarker(marker);
441
645
  if (!validation.valid) {
442
- throw new TypeError("A COPY/MOVE operand contains an invalid line marker.");
646
+ throw new TypeError("A COPY/MOVE operand contains an invalid text line marker.");
443
647
  }
444
648
  return `${address}<${marker.marks.join(",")}>`;
445
649
  }