@plurnk/plurnk-service 0.7.0 → 0.9.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 (270) hide show
  1. package/SPEC.md +116 -83
  2. package/bin/plurnk-service.ts +132 -0
  3. package/dist/Paths.d.ts +8 -0
  4. package/dist/Paths.d.ts.map +1 -0
  5. package/dist/Paths.js +47 -0
  6. package/dist/Paths.js.map +1 -0
  7. package/dist/content/index.d.ts +9 -0
  8. package/dist/content/index.d.ts.map +1 -0
  9. package/dist/content/index.js +10 -0
  10. package/dist/content/index.js.map +1 -0
  11. package/dist/content/line-marker.d.ts +26 -0
  12. package/dist/content/line-marker.d.ts.map +1 -0
  13. package/dist/content/line-marker.js +323 -0
  14. package/dist/content/line-marker.js.map +1 -0
  15. package/dist/content/matcher.d.ts +15 -0
  16. package/dist/content/matcher.d.ts.map +1 -0
  17. package/dist/content/matcher.js +112 -0
  18. package/dist/content/matcher.js.map +1 -0
  19. package/dist/content/mimetype-binary.d.ts +9 -0
  20. package/dist/content/mimetype-binary.d.ts.map +1 -0
  21. package/dist/content/mimetype-binary.js +86 -0
  22. package/dist/content/mimetype-binary.js.map +1 -0
  23. package/dist/content/path-mimetype.d.ts +6 -0
  24. package/dist/content/path-mimetype.d.ts.map +1 -0
  25. package/dist/content/path-mimetype.js +49 -0
  26. package/dist/content/path-mimetype.js.map +1 -0
  27. package/dist/content/read-resolve.d.ts +20 -0
  28. package/dist/content/read-resolve.d.ts.map +1 -0
  29. package/dist/content/read-resolve.js +60 -0
  30. package/dist/content/read-resolve.js.map +1 -0
  31. package/dist/core/ChannelWrite.d.ts +35 -30
  32. package/dist/core/ChannelWrite.d.ts.map +1 -1
  33. package/dist/core/ChannelWrite.js +49 -41
  34. package/dist/core/ChannelWrite.js.map +1 -1
  35. package/dist/core/Engine.d.ts +16 -10
  36. package/dist/core/Engine.d.ts.map +1 -1
  37. package/dist/core/Engine.js +309 -115
  38. package/dist/core/Engine.js.map +1 -1
  39. package/dist/core/EnvFlags.d.ts +6 -3
  40. package/dist/core/EnvFlags.d.ts.map +1 -1
  41. package/dist/core/EnvFlags.js +62 -60
  42. package/dist/core/EnvFlags.js.map +1 -1
  43. package/dist/core/ExecutorRegistry.d.ts +26 -0
  44. package/dist/core/ExecutorRegistry.d.ts.map +1 -0
  45. package/dist/core/ExecutorRegistry.js +99 -0
  46. package/dist/core/ExecutorRegistry.js.map +1 -0
  47. package/dist/core/PluginLoader.d.ts +6 -3
  48. package/dist/core/PluginLoader.d.ts.map +1 -1
  49. package/dist/core/PluginLoader.js +77 -73
  50. package/dist/core/PluginLoader.js.map +1 -1
  51. package/dist/core/ProviderInstantiate.d.ts +4 -2
  52. package/dist/core/ProviderInstantiate.d.ts.map +1 -1
  53. package/dist/core/ProviderInstantiate.js +23 -22
  54. package/dist/core/ProviderInstantiate.js.map +1 -1
  55. package/dist/core/SchemeRegistry.d.ts +1 -1
  56. package/dist/core/SchemeRegistry.d.ts.map +1 -1
  57. package/dist/core/SchemeRegistry.js +3 -3
  58. package/dist/core/SchemeRegistry.js.map +1 -1
  59. package/dist/core/git-membership.d.ts +8 -0
  60. package/dist/core/git-membership.d.ts.map +1 -0
  61. package/dist/core/git-membership.js +125 -0
  62. package/dist/core/git-membership.js.map +1 -0
  63. package/dist/core/packet-wire.d.ts +47 -6
  64. package/dist/core/packet-wire.d.ts.map +1 -1
  65. package/dist/core/packet-wire.js +376 -312
  66. package/dist/core/packet-wire.js.map +1 -1
  67. package/dist/core/resolveForLoop.d.ts +16 -0
  68. package/dist/core/resolveForLoop.d.ts.map +1 -0
  69. package/dist/core/resolveForLoop.js +34 -0
  70. package/dist/core/resolveForLoop.js.map +1 -0
  71. package/dist/core/results.d.ts +40 -0
  72. package/dist/core/results.d.ts.map +1 -0
  73. package/dist/core/results.js +52 -0
  74. package/dist/core/results.js.map +1 -0
  75. package/dist/core/scheme-types.d.ts +7 -4
  76. package/dist/core/scheme-types.d.ts.map +1 -1
  77. package/dist/core/scheme-types.js +4 -4
  78. package/dist/core/scheme-types.js.map +1 -1
  79. package/dist/core/types.d.ts +26 -0
  80. package/dist/core/types.d.ts.map +1 -0
  81. package/dist/core/types.js +17 -0
  82. package/dist/core/types.js.map +1 -0
  83. package/dist/index.d.ts +1 -6
  84. package/dist/index.d.ts.map +1 -1
  85. package/dist/index.js +4 -43
  86. package/dist/index.js.map +1 -1
  87. package/dist/schemes/EffectPolicy.d.ts +6 -0
  88. package/dist/schemes/EffectPolicy.d.ts.map +1 -0
  89. package/dist/schemes/EffectPolicy.js +18 -0
  90. package/dist/schemes/EffectPolicy.js.map +1 -0
  91. package/dist/schemes/Exec.d.ts +1 -0
  92. package/dist/schemes/Exec.d.ts.map +1 -1
  93. package/dist/schemes/Exec.js +86 -45
  94. package/dist/schemes/Exec.js.map +1 -1
  95. package/dist/schemes/File.d.ts +0 -1
  96. package/dist/schemes/File.d.ts.map +1 -1
  97. package/dist/schemes/File.js +32 -69
  98. package/dist/schemes/File.js.map +1 -1
  99. package/dist/schemes/Known.d.ts.map +1 -1
  100. package/dist/schemes/Known.js +13 -13
  101. package/dist/schemes/Known.js.map +1 -1
  102. package/dist/schemes/Log.d.ts.map +1 -1
  103. package/dist/schemes/Log.js +8 -52
  104. package/dist/schemes/Log.js.map +1 -1
  105. package/dist/schemes/Plurnk.d.ts.map +1 -1
  106. package/dist/schemes/Plurnk.js +13 -13
  107. package/dist/schemes/Plurnk.js.map +1 -1
  108. package/dist/schemes/Skill.d.ts.map +1 -1
  109. package/dist/schemes/Skill.js +13 -13
  110. package/dist/schemes/Skill.js.map +1 -1
  111. package/dist/schemes/Unknown.d.ts.map +1 -1
  112. package/dist/schemes/Unknown.js +13 -13
  113. package/dist/schemes/Unknown.js.map +1 -1
  114. package/dist/schemes/_entry-crud.d.ts +5 -3
  115. package/dist/schemes/_entry-crud.d.ts.map +1 -1
  116. package/dist/schemes/_entry-crud.js +55 -50
  117. package/dist/schemes/_entry-crud.js.map +1 -1
  118. package/dist/schemes/_entry-find.d.ts +10 -3
  119. package/dist/schemes/_entry-find.d.ts.map +1 -1
  120. package/dist/schemes/_entry-find.js +99 -77
  121. package/dist/schemes/_entry-find.js.map +1 -1
  122. package/dist/schemes/_entry-manifest.d.ts +6 -0
  123. package/dist/schemes/_entry-manifest.d.ts.map +1 -0
  124. package/dist/schemes/_entry-manifest.js +45 -0
  125. package/dist/schemes/_entry-manifest.js.map +1 -0
  126. package/dist/schemes/_entry-ops.d.ts +7 -4
  127. package/dist/schemes/_entry-ops.d.ts.map +1 -1
  128. package/dist/schemes/_entry-ops.js +198 -316
  129. package/dist/schemes/_entry-ops.js.map +1 -1
  130. package/dist/schemes/_entry-send.d.ts +4 -1
  131. package/dist/schemes/_entry-send.d.ts.map +1 -1
  132. package/dist/schemes/_entry-send.js +57 -55
  133. package/dist/schemes/_entry-send.js.map +1 -1
  134. package/dist/server/ClientConnection.js +3 -3
  135. package/dist/server/ClientConnection.js.map +1 -1
  136. package/dist/server/Daemon.d.ts +5 -5
  137. package/dist/server/Daemon.d.ts.map +1 -1
  138. package/dist/server/Daemon.js +234 -176
  139. package/dist/server/Daemon.js.map +1 -1
  140. package/dist/server/clientTurn.d.ts +4 -1
  141. package/dist/server/clientTurn.d.ts.map +1 -1
  142. package/dist/server/clientTurn.js +19 -17
  143. package/dist/server/clientTurn.js.map +1 -1
  144. package/dist/server/dsl.d.ts +19 -16
  145. package/dist/server/dsl.d.ts.map +1 -1
  146. package/dist/server/dsl.js +127 -105
  147. package/dist/server/dsl.js.map +1 -1
  148. package/dist/server/envelope.d.ts +22 -19
  149. package/dist/server/envelope.d.ts.map +1 -1
  150. package/dist/server/envelope.js +116 -102
  151. package/dist/server/envelope.js.map +1 -1
  152. package/dist/server/logEntry.d.ts +4 -1
  153. package/dist/server/logEntry.d.ts.map +1 -1
  154. package/dist/server/logEntry.js +41 -39
  155. package/dist/server/logEntry.js.map +1 -1
  156. package/dist/server/methods/_dispatchAsClient.d.ts +3 -1
  157. package/dist/server/methods/_dispatchAsClient.d.ts.map +1 -1
  158. package/dist/server/methods/_dispatchAsClient.js +31 -29
  159. package/dist/server/methods/_dispatchAsClient.js.map +1 -1
  160. package/dist/server/methods/discover.d.ts +3 -1
  161. package/dist/server/methods/discover.d.ts.map +1 -1
  162. package/dist/server/methods/discover.js +8 -6
  163. package/dist/server/methods/discover.js.map +1 -1
  164. package/dist/server/methods/entry_read.d.ts +4 -1
  165. package/dist/server/methods/entry_read.d.ts.map +1 -1
  166. package/dist/server/methods/entry_read.js +54 -52
  167. package/dist/server/methods/entry_read.js.map +1 -1
  168. package/dist/server/methods/log_read.d.ts +4 -1
  169. package/dist/server/methods/log_read.d.ts.map +1 -1
  170. package/dist/server/methods/log_read.js +38 -36
  171. package/dist/server/methods/log_read.js.map +1 -1
  172. package/dist/server/methods/loop_cancel.d.ts +3 -1
  173. package/dist/server/methods/loop_cancel.d.ts.map +1 -1
  174. package/dist/server/methods/loop_cancel.js +19 -17
  175. package/dist/server/methods/loop_cancel.js.map +1 -1
  176. package/dist/server/methods/loop_resolve.d.ts +3 -1
  177. package/dist/server/methods/loop_resolve.d.ts.map +1 -1
  178. package/dist/server/methods/loop_resolve.js +42 -40
  179. package/dist/server/methods/loop_resolve.js.map +1 -1
  180. package/dist/server/methods/loop_run.d.ts +3 -1
  181. package/dist/server/methods/loop_run.d.ts.map +1 -1
  182. package/dist/server/methods/loop_run.js +104 -102
  183. package/dist/server/methods/loop_run.js.map +1 -1
  184. package/dist/server/methods/op_copy.d.ts +3 -1
  185. package/dist/server/methods/op_copy.d.ts.map +1 -1
  186. package/dist/server/methods/op_copy.js +25 -23
  187. package/dist/server/methods/op_copy.js.map +1 -1
  188. package/dist/server/methods/op_dispatch.d.ts +3 -1
  189. package/dist/server/methods/op_dispatch.d.ts.map +1 -1
  190. package/dist/server/methods/op_dispatch.js +18 -16
  191. package/dist/server/methods/op_dispatch.js.map +1 -1
  192. package/dist/server/methods/op_edit.d.ts +3 -1
  193. package/dist/server/methods/op_edit.d.ts.map +1 -1
  194. package/dist/server/methods/op_edit.js +23 -21
  195. package/dist/server/methods/op_edit.js.map +1 -1
  196. package/dist/server/methods/op_exec.d.ts +3 -1
  197. package/dist/server/methods/op_exec.d.ts.map +1 -1
  198. package/dist/server/methods/op_exec.js +20 -18
  199. package/dist/server/methods/op_exec.js.map +1 -1
  200. package/dist/server/methods/op_find.d.ts +3 -1
  201. package/dist/server/methods/op_find.d.ts.map +1 -1
  202. package/dist/server/methods/op_find.js +23 -21
  203. package/dist/server/methods/op_find.js.map +1 -1
  204. package/dist/server/methods/op_hide.d.ts +3 -1
  205. package/dist/server/methods/op_hide.d.ts.map +1 -1
  206. package/dist/server/methods/op_hide.js +23 -21
  207. package/dist/server/methods/op_hide.js.map +1 -1
  208. package/dist/server/methods/op_move.d.ts +3 -1
  209. package/dist/server/methods/op_move.d.ts.map +1 -1
  210. package/dist/server/methods/op_move.js +23 -21
  211. package/dist/server/methods/op_move.js.map +1 -1
  212. package/dist/server/methods/op_parse.d.ts +3 -1
  213. package/dist/server/methods/op_parse.d.ts.map +1 -1
  214. package/dist/server/methods/op_parse.js +24 -22
  215. package/dist/server/methods/op_parse.js.map +1 -1
  216. package/dist/server/methods/op_read.d.ts +3 -1
  217. package/dist/server/methods/op_read.d.ts.map +1 -1
  218. package/dist/server/methods/op_read.js +23 -21
  219. package/dist/server/methods/op_read.js.map +1 -1
  220. package/dist/server/methods/op_send.d.ts +3 -1
  221. package/dist/server/methods/op_send.d.ts.map +1 -1
  222. package/dist/server/methods/op_send.js +22 -20
  223. package/dist/server/methods/op_send.js.map +1 -1
  224. package/dist/server/methods/op_show.d.ts +3 -1
  225. package/dist/server/methods/op_show.d.ts.map +1 -1
  226. package/dist/server/methods/op_show.js +23 -21
  227. package/dist/server/methods/op_show.js.map +1 -1
  228. package/dist/server/methods/ping.d.ts +3 -1
  229. package/dist/server/methods/ping.d.ts.map +1 -1
  230. package/dist/server/methods/ping.js +8 -6
  231. package/dist/server/methods/ping.js.map +1 -1
  232. package/dist/server/methods/providers_list.d.ts +3 -1
  233. package/dist/server/methods/providers_list.d.ts.map +1 -1
  234. package/dist/server/methods/providers_list.js +19 -17
  235. package/dist/server/methods/providers_list.js.map +1 -1
  236. package/dist/server/methods/session_attach.d.ts +3 -1
  237. package/dist/server/methods/session_attach.d.ts.map +1 -1
  238. package/dist/server/methods/session_attach.js +43 -41
  239. package/dist/server/methods/session_attach.js.map +1 -1
  240. package/dist/server/methods/session_create.d.ts +3 -1
  241. package/dist/server/methods/session_create.d.ts.map +1 -1
  242. package/dist/server/methods/session_create.js +51 -49
  243. package/dist/server/methods/session_create.js.map +1 -1
  244. package/dist/server/methods/session_list.d.ts +3 -1
  245. package/dist/server/methods/session_list.d.ts.map +1 -1
  246. package/dist/server/methods/session_list.js +9 -7
  247. package/dist/server/methods/session_list.js.map +1 -1
  248. package/dist/server/methods/session_runs.d.ts +3 -1
  249. package/dist/server/methods/session_runs.d.ts.map +1 -1
  250. package/dist/server/methods/session_runs.js +19 -17
  251. package/dist/server/methods/session_runs.js.map +1 -1
  252. package/dist/server/methods/session_set_persona.d.ts +3 -1
  253. package/dist/server/methods/session_set_persona.d.ts.map +1 -1
  254. package/dist/server/methods/session_set_persona.js +28 -26
  255. package/dist/server/methods/session_set_persona.js.map +1 -1
  256. package/dist/server/methods/session_set_root.d.ts +3 -1
  257. package/dist/server/methods/session_set_root.d.ts.map +1 -1
  258. package/dist/server/methods/session_set_root.js +31 -29
  259. package/dist/server/methods/session_set_root.js.map +1 -1
  260. package/dist/server/noProposals.d.ts +6 -0
  261. package/dist/server/noProposals.d.ts.map +1 -0
  262. package/dist/server/noProposals.js +37 -0
  263. package/dist/server/noProposals.js.map +1 -0
  264. package/dist/server/yolo.d.ts +3 -1
  265. package/dist/server/yolo.d.ts.map +1 -1
  266. package/dist/server/yolo.js +15 -13
  267. package/dist/server/yolo.js.map +1 -1
  268. package/package.json +71 -32
  269. package/bin/plurnk-service.js +0 -112
  270. /package/migrations/{001_schema.sql → 0000-00-00.01_schema.sql} +0 -0
@@ -8,334 +8,398 @@
8
8
  // 2026-05-22. Standard markdown idioms only — headers as section delimiters,
9
9
  // fenced code blocks for entry bodies, lists for arrays. No invented
10
10
  // separators. Models parse markdown natively.
11
+ //
11
12
  // Section headers follow the `# Plurnk System X` convention so the model
12
13
  // sees consistent framing across every section it might receive. Sections
13
14
  // with no content are omitted entirely (no empty headers in the wire).
14
- import { isLineNavigableMimetype } from "@plurnk/plurnk-schemes";
15
- // Render packet.system system message content (markdown string).
16
- // {system_definition verbatim}
17
- // # Plurnk System Instructions (persona)
18
- // # Plurnk System Index (entries — only when present)
19
- // # Plurnk System Log (log entries — only when present)
20
- export const renderSystemContent = (system) => {
21
- const parts = [system.system_definition];
22
- if (typeof system.persona === "string" && system.persona.length > 0) {
23
- parts.push(`# Plurnk System Instructions\n\n${system.persona}`);
24
- }
25
- if (Array.isArray(system.index) && system.index.length > 0) {
26
- parts.push(`# Plurnk System Index\n\n${renderIndexEntries(system.index)}`);
27
- }
28
- if (Array.isArray(system.log) && system.log.length > 0) {
29
- parts.push(`# Plurnk System Log\n\n${renderLogEntries(system.log)}`);
15
+ import { MimetypeBinary } from "../content/index.js";
16
+ export default class PacketWire {
17
+ // Render packet.system → system message content (markdown string).
18
+ // {system_definition verbatim}
19
+ // # Plurnk System Instructions (persona)
20
+ // # Plurnk System Index (entries — only when present)
21
+ // # Plurnk System Log (log entries — only when present)
22
+ static renderSystemContent(system) {
23
+ const parts = [system.system_definition];
24
+ if (typeof system.persona === "string" && system.persona.length > 0) {
25
+ parts.push(`# Plurnk System Instructions\n\n${system.persona}`);
26
+ }
27
+ if (Array.isArray(system.index) && system.index.length > 0) {
28
+ parts.push(`# Plurnk System Index\n\n${PacketWire.#renderIndexEntries(system.index)}`);
29
+ }
30
+ if (Array.isArray(system.log) && system.log.length > 0) {
31
+ parts.push(`# Plurnk System Log\n\n${PacketWire.#renderLogEntries(system.log)}`);
32
+ }
33
+ return parts.map((p) => p.replace(/\n+$/, "")).join("\n\n");
30
34
  }
31
- return parts.join("\n\n");
32
- };
33
- // Render packet.user user message content (markdown string).
34
- // # Plurnk System User Prompt
35
- // # Plurnk System Budget (token budget table — only when present)
36
- // # Plurnk System Errors (telemetry errors only when present)
37
- // # Plurnk System Requirements (static per-turn rulesonly when present)
38
- // Requirements renders LAST so the contract the model has to honor is the
39
- // most recent thing in the user message — closest to the assistant turn.
40
- export const renderUserContent = (user) => {
41
- const parts = [];
42
- if (typeof user.prompt === "string" && user.prompt.length > 0) {
43
- parts.push(`# Plurnk System User Prompt\n\n${user.prompt}`);
35
+ // Render packet.user → user message content (markdown string).
36
+ // # Plurnk System User Prompt
37
+ // # Plurnk System Budget (token budget table only when present)
38
+ // # Plurnk System Errors (telemetry errors — only when present)
39
+ // # Plurnk System Requirements (static per-turn rules — only when present)
40
+ // Requirements renders LAST so the contract the model has to honor is the
41
+ // most recent thing in the user message closest to the assistant turn.
42
+ static renderUserContent(user) {
43
+ const parts = [];
44
+ if (typeof user.prompt === "string" && user.prompt.length > 0) {
45
+ parts.push(`# Plurnk System User Prompt\n\n${user.prompt}`);
46
+ }
47
+ const telemetry = user.telemetry ?? { budget: "", errors: [] };
48
+ if (typeof telemetry.budget === "string" && telemetry.budget.length > 0) {
49
+ parts.push(`# Plurnk System Budget\n\n${telemetry.budget}`);
50
+ }
51
+ if (Array.isArray(telemetry.errors) && telemetry.errors.length > 0) {
52
+ parts.push(`# Plurnk System Errors\n\n${PacketWire.#renderTelemetryErrors(telemetry.errors)}`);
53
+ }
54
+ if (typeof user.system_requirements === "string" && user.system_requirements.length > 0) {
55
+ parts.push(`# Plurnk System Requirements\n\n${user.system_requirements}`);
56
+ }
57
+ return parts.map((p) => p.replace(/\n+$/, "")).join("\n\n");
44
58
  }
45
- const telemetry = user.telemetry ?? { budget: "", errors: [] };
46
- if (typeof telemetry.budget === "string" && telemetry.budget.length > 0) {
47
- parts.push(`# Plurnk System Budget\n\n${telemetry.budget}`);
59
+ // Project the full request half of a packet to ChatMessage[] for the wire.
60
+ // Engine calls this directly; the result is what provider.generate receives.
61
+ static packetToWireMessages(packet) {
62
+ return [
63
+ { role: "system", content: PacketWire.renderSystemContent(packet.system) },
64
+ { role: "user", content: PacketWire.renderUserContent(packet.user) },
65
+ ];
48
66
  }
49
- if (Array.isArray(telemetry.errors) && telemetry.errors.length > 0) {
50
- parts.push(`# Plurnk System Errors\n\n${renderTelemetryErrors(telemetry.errors)}`);
67
+ // Measure the wire-rendered token cost of the curatable sections (index,
68
+ // log) plus the assembled total, using the provider's tokenizer. The budget
69
+ // readout uses this so its subtotals match what actually ships — meta lines
70
+ // and fences included — not a serialized approximation. `total` is measured
71
+ // over whatever the packet currently holds, so the caller renders the budget
72
+ // with a `{{tokensFree}}` placeholder, measures, then substitutes (the
73
+ // placeholder/number length delta is negligible).
74
+ static measureBudgetSections(packet, countTokens) {
75
+ const system = packet.system;
76
+ const user = packet.user;
77
+ const indexEntries = Array.isArray(system.index) ? system.index : [];
78
+ const logEntries = Array.isArray(system.log) ? system.log : [];
79
+ const indexBody = indexEntries.length > 0 ? PacketWire.#renderIndexEntries(indexEntries) : "";
80
+ const logBody = logEntries.length > 0 ? PacketWire.#renderLogEntries(logEntries) : "";
81
+ // A channel renders iff its content is a non-empty string (renderIndexEntries).
82
+ const indexChannels = indexEntries.reduce((n, e) => n + Object.values(e.channels ?? {}).filter((ch) => typeof ch.content === "string" && ch.content.length > 0).length, 0);
83
+ // Per-scheme log breakdown (§14.2 {§14.2-per-scheme-balance}): each entry's
84
+ // render-weight grouped by the scheme it acted on, heaviest first — the
85
+ // model's "what's eating my window" signal and its HIDE target. Render-
86
+ // weight (not stored depth), consistent with the headline; tokenizing per
87
+ // entry is free.
88
+ const byScheme = new Map();
89
+ for (const e of logEntries) {
90
+ const scheme = e.target?.scheme ?? "—";
91
+ const acc = byScheme.get(scheme) ?? { scheme, entries: 0, tokens: 0 };
92
+ acc.entries += 1;
93
+ acc.tokens += countTokens(PacketWire.#renderLogEntries([e]));
94
+ byScheme.set(scheme, acc);
95
+ }
96
+ return {
97
+ index: {
98
+ channels: indexChannels,
99
+ tokens: indexBody ? countTokens(`# Plurnk System Index\n\n${indexBody}`) : 0,
100
+ },
101
+ log: {
102
+ entries: logEntries.length,
103
+ tokens: logBody ? countTokens(`# Plurnk System Log\n\n${logBody}`) : 0,
104
+ byScheme: [...byScheme.values()].toSorted((a, b) => b.tokens - a.tokens),
105
+ },
106
+ total: countTokens(PacketWire.renderSystemContent(system)) + countTokens(PacketWire.renderUserContent(user)),
107
+ };
51
108
  }
52
- if (typeof user.system_requirements === "string" && user.system_requirements.length > 0) {
53
- parts.push(`# Plurnk System Requirements\n\n${user.system_requirements}`);
109
+ // Number each line of body as `<N>:\t<line>` mirrors rummy
110
+ // plugins/helpers.js numberLines. The leading digit prevents column-zero
111
+ // fence collisions and gives the model line refs for free (`READ<42-46>`).
112
+ // Used for READ@200 content; index-preview numbering is the framework's
113
+ // job now (baked into the preview string — see renderHeredoc).
114
+ static #numberLines(body, start = 1) {
115
+ if (!body)
116
+ return "";
117
+ const trailingNewline = body.endsWith("\n");
118
+ const source = trailingNewline ? body.slice(0, -1) : body;
119
+ const numbered = source.split("\n").map((line, i) => `${start + i}:\t${line}`).join("\n");
120
+ return trailingNewline ? `${numbered}\n` : numbered;
54
121
  }
55
- return parts.join("\n\n");
56
- };
57
- // Project the full request half of a packet to ChatMessage[] for the wire.
58
- // Engine calls this directly; the result is what provider.generate receives.
59
- export const packetToWireMessages = (packet) => [
60
- { role: "system", content: renderSystemContent(packet.system) },
61
- { role: "user", content: renderUserContent(packet.user) },
62
- ];
63
- // Number each line of body as `<N>:\t<line>` — mirrors rummy
64
- // plugins/helpers.js numberLines. The leading digit prevents column-zero
65
- // fence collisions and gives the model line refs for free (`READ<42-46>`).
66
- const numberLines = (body, start = 1) => {
67
- if (!body)
68
- return "";
69
- const trailingNewline = body.endsWith("\n");
70
- const source = trailingNewline ? body.slice(0, -1) : body;
71
- const numbered = source.split("\n").map((line, i) => `${start + i}:\t${line}`).join("\n");
72
- return trailingNewline ? `${numbered}\n` : numbered;
73
- };
74
- // Tolerant JSON parser for log entries' rx/tx fields. The engine
75
- // pre-parses application/json mimetypes, but render may also receive
76
- // strings (legacy paths, manual tests). Returns null on parse failure.
77
- const safeParse = (s) => {
78
- try {
79
- return JSON.parse(s);
122
+ // Tolerant JSON parser for log entries' rx/tx fields. The engine
123
+ // pre-parses application/json mimetypes, but render may also receive
124
+ // strings (legacy paths, manual tests). Returns null on parse failure.
125
+ static #safeParse(s) {
126
+ try {
127
+ return JSON.parse(s);
128
+ }
129
+ catch {
130
+ return null;
131
+ }
80
132
  }
81
- catch {
82
- return null;
133
+ // Stable JSON: keys sorted alphabetically so the same meta produces the
134
+ // same string across turns — prefix-cache friendly. Mirrors rummy
135
+ // plugins/helpers.js canonicalJson.
136
+ static #canonicalJson(obj) {
137
+ const keys = Object.keys(obj).sort();
138
+ const sorted = {};
139
+ for (const k of keys)
140
+ sorted[k] = obj[k];
141
+ return JSON.stringify(sorted);
83
142
  }
84
- };
85
- // Stable JSON: keys sorted alphabetically so the same meta produces the
86
- // same string across turns prefix-cache friendly. Mirrors rummy
87
- // plugins/helpers.js canonicalJson.
88
- const canonicalJson = (obj) => {
89
- const keys = Object.keys(obj).sort();
90
- const sorted = {};
91
- for (const k of keys)
92
- sorted[k] = obj[k];
93
- return JSON.stringify(sorted);
94
- };
95
- // Wrap a body in heredoc fences. Leading `\n` always (separates the
96
- // opening fence from the first body character — necessary because
97
- // numbered bodies start with `1:\t…` which would otherwise collide
98
- // visually with the fence's closing `:`). Trailing `\n` only when the
99
- // body doesn't already end with one — otherwise you get a doubled
100
- // newline that renders as a blank line before the closing fence, which
101
- // reads as "the content has a trailing blank line" when actually it
102
- // doesn't. The body's own whitespace decides the shape.
103
- const wrapHeredocBody = (fence, body) => {
104
- const sep = body.endsWith("\n") ? "" : "\n";
105
- return `<<${fence}:\n${body}${sep}:${fence}`;
106
- };
107
- // Heredoc block for one channel of one entry. Fence is `URI#channel`
108
- // (plurnk-grammar-native form) so model emissions and entry projections
109
- // share one syntax. When `channel` is null/empty the fence is path-only —
110
- // this is the default-channel convention: the absence of `#channel` is
111
- // the addressing of the scheme's default channel, not a missing field.
112
- // Body is line-numbered.
113
- const renderHeredoc = (uri, channel, body) => {
114
- const fence = channel ? `${uri}#${channel}` : uri;
115
- return wrapHeredocBody(fence, numberLines(body));
116
- };
117
- // Re-render a plurnk statement (from log_entries.tx) as the heredoc form
118
- // the model would have emitted. Used by the log render so the model sees
119
- // its own ops in its own native syntax — what it wrote, mirrored back.
120
- //
121
- // Faithfulness over cleverness: render the parts as recorded. `target.raw`
122
- // preserves exactly what the model wrote (URL with fragment, bare path,
123
- // etc.) instead of round-tripping through scheme/pathname/fragment fields.
124
- // Returns null when tx isn't a parseable PlurnkStatement (callers fall
125
- // back to the meta line alone).
126
- //
127
- // Signal renders to `[…]`:
128
- // - array of strings (tags) → `[tag1,tag2]`
129
- // - number (status code, e.g. SEND[200]) → `[200]`
130
- // - string (runtime, e.g. EXEC[python]) → `[python]`
131
- // - null/missing → omitted
132
- // All plurnk statements share the same syntactic frame; signal type
133
- // varies by op but renders uniformly.
134
- const renderStatementHeredoc = (tx) => {
135
- if (tx === null || typeof tx !== "object" || typeof tx.op !== "string" || tx.op.length === 0)
136
- return null;
137
- const op = tx.op;
138
- const suffix = typeof tx.suffix === "string" ? tx.suffix : "";
139
- let signalStr = "";
140
- const signal = tx.signal;
141
- if (Array.isArray(signal)) {
142
- const tags = signal.filter((t) => typeof t === "string");
143
- if (tags.length > 0)
144
- signalStr = `[${tags.join(",")}]`;
143
+ // Wrap a body in heredoc fences. Leading `\n` always (separates the
144
+ // opening fence from the first body character necessary because
145
+ // numbered bodies start with `1:\t…` which would otherwise collide
146
+ // visually with the `:::FENCE` markers). Trailing `\n` only when the
147
+ // body doesn't already end with one — otherwise you get a doubled
148
+ // newline that renders as a blank line before the closing fence, which
149
+ // reads as "the content has a trailing blank line" when actually it
150
+ // doesn't. The body's own whitespace decides the shape.
151
+ static #wrapHeredocBody(fence, body) {
152
+ const sep = body.endsWith("\n") ? "" : "\n";
153
+ return `<<:::${fence}\n${body}${sep}:::${fence}`;
145
154
  }
146
- else if (typeof signal === "number") {
147
- signalStr = `[${signal}]`;
155
+ // Heredoc block for one channel of one entry. Fence is `URI#channel`
156
+ // (the `<<:::FENCE` packet-rendering marker per wrapHeredocBody — a
157
+ // projection is read-only context, NOT an emittable op, so it must not
158
+ // wear the DSL op-fence; that conflation was the demo.sh corruption bug).
159
+ // When `channel` is null/empty the fence is path-only —
160
+ // this is the default-channel convention: the absence of `#channel` is
161
+ // the addressing of the scheme's default channel, not a missing field.
162
+ // Body is a mimetypes preview, rendered VERBATIM — the framework owns its
163
+ // formatting (N:\t line numbers for text, source-annotated outline for
164
+ // symbols, correct start-line for tail slices) and bakes it into the
165
+ // preview string as of mimetypes 0.7.3, so the service must not re-number
166
+ // it (re-numbering would double-prefix text and mis-number symbol
167
+ // outlines — plurnk-mimetypes#8).
168
+ static #renderHeredoc(uri, channel, body) {
169
+ const fence = channel ? `${uri}#${channel}` : uri;
170
+ return PacketWire.#wrapHeredocBody(fence, body);
148
171
  }
149
- else if (typeof signal === "string" && signal.length > 0) {
150
- signalStr = `[${signal}]`;
172
+ // Re-render a plurnk statement (from log_entries.tx) as the heredoc form
173
+ // the model would have emitted. Used by the log render so the model sees
174
+ // its own ops in its own native syntax — what it wrote, mirrored back.
175
+ //
176
+ // Faithfulness over cleverness: render the parts as recorded. `target.raw`
177
+ // preserves exactly what the model wrote (URL with fragment, bare path,
178
+ // etc.) instead of round-tripping through scheme/pathname/fragment fields.
179
+ // Returns null when tx isn't a parseable PlurnkStatement (callers fall
180
+ // back to the meta line alone).
181
+ //
182
+ // Signal renders to `[…]`:
183
+ // - array of strings (tags) → `[tag1,tag2]`
184
+ // - number (status code, e.g. SEND[200]) → `[200]`
185
+ // - string (runtime, e.g. EXEC[python]) → `[python]`
186
+ // - null/missing → omitted
187
+ // All plurnk statements share the same syntactic frame; signal type
188
+ // varies by op but renders uniformly.
189
+ static #renderStatementHeredoc(tx) {
190
+ if (tx === null || typeof tx !== "object" || typeof tx.op !== "string" || tx.op.length === 0)
191
+ return null;
192
+ const op = tx.op;
193
+ const suffix = typeof tx.suffix === "string" ? tx.suffix : "";
194
+ let signalStr = "";
195
+ const signal = tx.signal;
196
+ if (Array.isArray(signal)) {
197
+ const tags = signal.filter((t) => typeof t === "string");
198
+ if (tags.length > 0)
199
+ signalStr = `[${tags.join(",")}]`;
200
+ }
201
+ else if (typeof signal === "number") {
202
+ signalStr = `[${signal}]`;
203
+ }
204
+ else if (typeof signal === "string" && signal.length > 0) {
205
+ signalStr = `[${signal}]`;
206
+ }
207
+ let targetStr = "";
208
+ const target = tx.target;
209
+ if (target !== null && target !== undefined && typeof target === "object" && typeof target.raw === "string") {
210
+ targetStr = `(${target.raw})`;
211
+ }
212
+ let markerStr = "";
213
+ const lm = tx.lineMarker;
214
+ if (lm !== null && lm !== undefined && typeof lm === "object" && typeof lm.first === "number") {
215
+ markerStr = typeof lm.last === "number" ? `<${lm.first},${lm.last}>` : `<${lm.first}>`;
216
+ }
217
+ let body;
218
+ if (typeof tx.body === "string")
219
+ body = tx.body;
220
+ else if (tx.body !== null && tx.body !== undefined && typeof tx.body === "object" && typeof tx.body.raw === "string")
221
+ body = tx.body.raw;
222
+ else
223
+ body = "";
224
+ // Character-perfect: no padding around body. The body string IS
225
+ // whatever the model wrote between the colons, including any leading
226
+ // or trailing whitespace it chose. Adding `\n` here would inflate
227
+ // single-line emissions into multi-line and nudge the model toward
228
+ // verbose forms — and it would violate the grammar's "body content
229
+ // is character-perfect" guarantee on the way back.
230
+ return `<<${op}${suffix}${signalStr}${targetStr}${markerStr}:${body}:${op}${suffix}`;
151
231
  }
152
- let targetStr = "";
153
- const target = tx.target;
154
- if (target !== null && typeof target === "object" && typeof target.raw === "string") {
155
- targetStr = `(${target.raw})`;
232
+ // Render a (scheme, pathname) tuple as the URI the model should SEE.
233
+ // Null scheme → bare pathname. The `file` scheme never reaches this
234
+ // function because Engine.#extractTarget normalizes it to null at the
235
+ // storage boundary; storage and wire output are uniform on this.
236
+ static #renderModelUri(scheme, pathname) {
237
+ const path = pathname ?? "";
238
+ if (scheme === null || scheme === undefined)
239
+ return path;
240
+ return `${scheme}://${path}`;
156
241
  }
157
- let markerStr = "";
158
- const lm = tx.lineMarker;
159
- if (lm !== null && typeof lm === "object" && typeof lm.first === "number") {
160
- markerStr = typeof lm.last === "number" ? `<${lm.first},${lm.last}>` : `<${lm.first}>`;
242
+ // Render one Index entry → `* {meta}` line followed by per-channel
243
+ // heredoc blocks. meta describes the entry; nested `channels` carries
244
+ // per-channel mimetype/tokens so the model doesn't have to READ to
245
+ // learn the shape of a channel's content.
246
+ //
247
+ // Empty channels are omitted entirely (no meta entry, no body block) —
248
+ // an empty stderr is an infohazard: the model has to read it and infer
249
+ // "this is intentionally blank." Absence carries the same information
250
+ // with zero tokens spent.
251
+ static #renderIndexEntries(entries) {
252
+ return entries.map((e) => {
253
+ const uri = PacketWire.#renderModelUri(e.scheme, e.pathname);
254
+ const defaultChannel = e.defaultChannel ?? "";
255
+ const meta = { path: uri };
256
+ if (Array.isArray(e.tags) && e.tags.length > 0)
257
+ meta.tags = e.tags;
258
+ const channelsMeta = {};
259
+ const blocks = [];
260
+ for (const [channelName, ch] of Object.entries(e.channels ?? {})) {
261
+ const content = ch.content;
262
+ if (typeof content !== "string" || content.length === 0)
263
+ continue;
264
+ const channelInfo = {};
265
+ if (typeof ch.mimetype === "string")
266
+ channelInfo.mimetype = ch.mimetype;
267
+ if (typeof ch.tokens === "number")
268
+ channelInfo.tokens = ch.tokens;
269
+ if (typeof ch.lines === "number")
270
+ channelInfo.lines = ch.lines;
271
+ channelsMeta[channelName] = channelInfo;
272
+ const fenceChannel = channelName === defaultChannel ? null : channelName;
273
+ blocks.push(PacketWire.#renderHeredoc(uri, fenceChannel, content));
274
+ }
275
+ if (Object.keys(channelsMeta).length > 0)
276
+ meta.channels = channelsMeta;
277
+ return blocks.length > 0
278
+ ? `* ${PacketWire.#canonicalJson(meta)}\n${blocks.join("\n")}`
279
+ : `* ${PacketWire.#canonicalJson(meta)}`;
280
+ }).join("\n\n");
161
281
  }
162
- let body;
163
- if (typeof tx.body === "string")
164
- body = tx.body;
165
- else if (tx.body !== null && typeof tx.body === "object" && typeof tx.body.raw === "string")
166
- body = tx.body.raw;
167
- else
168
- body = "";
169
- // Character-perfect: no padding around body. The body string IS
170
- // whatever the model wrote between the colons, including any leading
171
- // or trailing whitespace it chose. Adding `\n` here would inflate
172
- // single-line emissions into multi-line and nudge the model toward
173
- // verbose forms — and it would violate the grammar's "body content
174
- // is character-perfect" guarantee on the way back.
175
- return `<<${op}${suffix}${signalStr}${targetStr}${markerStr}:${body}:${op}${suffix}`;
176
- };
177
- // Render a (scheme, pathname) tuple as the URI the model should SEE.
178
- // Null scheme bare pathname. The `file` scheme never reaches this
179
- // function because Engine.#extractTarget normalizes it to null at the
180
- // storage boundary; storage and wire output are uniform on this.
181
- const renderModelUri = (scheme, pathname) => {
182
- const path = pathname ?? "";
183
- if (scheme === null || scheme === undefined)
184
- return path;
185
- return `${scheme}://${path}`;
186
- };
187
- // Render one Index entry `* {meta}` line followed by per-channel
188
- // heredoc blocks. meta describes the entry; nested `channels` carries
189
- // per-channel mimetype/tokens so the model doesn't have to READ to
190
- // learn the shape of a channel's content.
191
- //
192
- // Empty channels are omitted entirely (no meta entry, no body block) —
193
- // an empty stderr is an infohazard: the model has to read it and infer
194
- // "this is intentionally blank." Absence carries the same information
195
- // with zero tokens spent.
196
- const renderIndexEntries = (entries) => entries.map((e) => {
197
- const uri = renderModelUri(e.scheme, e.pathname);
198
- const defaultChannel = e.defaultChannel ?? "";
199
- const meta = { path: uri };
200
- if (Array.isArray(e.tags) && e.tags.length > 0)
201
- meta.tags = e.tags;
202
- const channelsMeta = {};
203
- const blocks = [];
204
- for (const [channelName, ch] of Object.entries(e.channels ?? {})) {
205
- const content = ch?.content;
206
- if (typeof content !== "string" || content.length === 0)
207
- continue;
208
- const channelInfo = {};
209
- if (typeof ch.mimetype === "string")
210
- channelInfo.mimetype = ch.mimetype;
211
- if (typeof ch.tokens === "number")
212
- channelInfo.tokens = ch.tokens;
213
- channelsMeta[channelName] = channelInfo;
214
- const fenceChannel = channelName === defaultChannel ? null : channelName;
215
- blocks.push(renderHeredoc(uri, fenceChannel, content));
282
+ // Render one Log entry → a single bullet line carrying the meta JSON.
283
+ // No body, no fence — every meaningful field is in the JSON. Naming
284
+ // follows the uniform principle: `path` is identity (this log row's
285
+ // own URI), `target` is the URI the action acted on. COPY/MOVE add
286
+ // `source`; currently the engine emits target only (source plumbing
287
+ // pending the COPY/MOVE-specific log shape pass).
288
+ //
289
+ // On error, status >= 400 signals the failure; the message lives in
290
+ // the next packet's user.telemetry.errors[] per SPEC §15.1. (Forward:
291
+ // meta will gain tokensBefore/After + linesBefore/After to convey
292
+ // change scope without carrying the body content.)
293
+ //
294
+ // Per-entry render: one meta JSON line plus a body block that the model
295
+ // can read to know what it did. Two body cases:
296
+ // 1. READ@200 with content → render rx.content under the target fence.
297
+ // The model asked for content; show it the content. (Matcher is in
298
+ // meta.matcher, count is in meta.matches.)
299
+ // 2. Every other op re-emit tx as a heredoc in the model's native
300
+ // syntax. The model wrote this; mirror it back so the log is a true
301
+ // record of its actions instead of a row of opaque status codes.
302
+ static #renderLogEntries(entries) {
303
+ return entries.map((e) => {
304
+ const meta = {};
305
+ const coordinate = typeof e.coordinate === "string" ? e.coordinate : null;
306
+ const op = typeof e.op === "string" && e.op.length > 0 ? e.op : null;
307
+ if (coordinate !== null && op !== null)
308
+ meta.path = `log://${coordinate}/${op}`;
309
+ else if (coordinate !== null)
310
+ meta.path = `log://${coordinate}`;
311
+ if (typeof e.origin === "string")
312
+ meta.origin = e.origin;
313
+ if (op !== null)
314
+ meta.op = op;
315
+ if (typeof e.status === "number")
316
+ meta.status = e.status;
317
+ const target = PacketWire.#renderActionTarget(e.target);
318
+ if (target !== null)
319
+ meta.target = target;
320
+ // Op-specific meta enrichment for READ: surface the matcher body
321
+ // and match count when a body matcher was used. Without these, the
322
+ // model can't distinguish "0 matches" from "empty content" — both
323
+ // would render as a status-204 line. The matcher comes from the
324
+ // stored statement (tx); the count from the result (rx).
325
+ if (op === "READ") {
326
+ const tx = e.tx;
327
+ if (tx !== null && tx !== undefined && typeof tx === "object" && tx.body !== null && typeof tx.body === "object") {
328
+ if (typeof tx.body.raw === "string")
329
+ meta.matcher = tx.body.raw;
330
+ }
331
+ const rx = (typeof e.rx === "string" ? PacketWire.#safeParse(e.rx) : e.rx);
332
+ if (rx !== null && typeof rx === "object" && typeof rx.matches === "number") {
333
+ meta.matches = rx.matches;
334
+ }
335
+ }
336
+ const metaLine = `* ${PacketWire.#canonicalJson(meta)}`;
337
+ // READ@200: expose the response body. READ@204 (successfully empty —
338
+ // 0 matcher hits, sentinel slice, or empty source) has no body to
339
+ // render; the meta line carries the signal via `matches` / status code.
340
+ if (op === "READ" && e.status === 200) {
341
+ const rx = (typeof e.rx === "string" ? PacketWire.#safeParse(e.rx) : e.rx);
342
+ if (rx !== null && typeof rx === "object" && typeof rx.content === "string" && rx.content.length > 0) {
343
+ const fence = target ?? `log://${coordinate}`;
344
+ // Line-navigable mimetypes (text/markdown, text/plain,
345
+ // source code, etc.) get N:\t prefix per plurnk.md. Tree-
346
+ // navigable (JSON, XML, HTML) render verbatim — line
347
+ // numbers in the wrapper would collide with structural
348
+ // navigation (jsonpath/xpath) used on these formats.
349
+ // Classifier is consumer-side in this repo (SPEC.md §16.6).
350
+ const mimetype = typeof rx.mimetype === "string" ? rx.mimetype : "text/plain";
351
+ if (MimetypeBinary.isLineNavigableMimetype(mimetype)) {
352
+ const start = typeof rx.startLine === "number" ? rx.startLine : 1;
353
+ return `${metaLine}\n${PacketWire.#wrapHeredocBody(fence, PacketWire.#numberLines(rx.content, start))}`;
354
+ }
355
+ return `${metaLine}\n${PacketWire.#wrapHeredocBody(fence, rx.content)}`;
356
+ }
357
+ }
358
+ // Every other op: re-emit the model's statement. EDIT, EXEC, SEND,
359
+ // COPY, MOVE, FIND, SHOW, HIDE — each gets its native heredoc form
360
+ // back. Without this the log row is a status code with no record
361
+ // of what the model actually wrote, and the model has to back into
362
+ // its own actions by inference (see reasoning.md trace from the
363
+ // pre-fix count-files run).
364
+ const heredoc = PacketWire.#renderStatementHeredoc(e.tx ?? null);
365
+ if (heredoc !== null)
366
+ return `${metaLine}\n${heredoc}`;
367
+ return metaLine;
368
+ }).join("\n");
216
369
  }
217
- if (Object.keys(channelsMeta).length > 0)
218
- meta.channels = channelsMeta;
219
- return blocks.length > 0
220
- ? `* ${canonicalJson(meta)}\n${blocks.join("\n")}`
221
- : `* ${canonicalJson(meta)}`;
222
- }).join("\n\n");
223
- // Render one Log entry → a single bullet line carrying the meta JSON.
224
- // No body, no fence — every meaningful field is in the JSON. Naming
225
- // follows the uniform principle: `path` is identity (this log row's
226
- // own URI), `target` is the URI the action acted on. COPY/MOVE add
227
- // `source`; currently the engine emits target only (source plumbing
228
- // pending the COPY/MOVE-specific log shape pass).
229
- //
230
- // On error, status >= 400 signals the failure; the message lives in
231
- // the next packet's user.telemetry.errors[] per SPEC §15.1. (Forward:
232
- // meta will gain tokensBefore/After + linesBefore/After to convey
233
- // change scope without carrying the body content.)
234
- //
235
- // Per-entry render: one meta JSON line plus a body block that the model
236
- // can read to know what it did. Two body cases:
237
- // 1. READ@200 with content → render rx.content under the target fence.
238
- // The model asked for content; show it the content. (Matcher is in
239
- // meta.matcher, count is in meta.matches.)
240
- // 2. Every other op → re-emit tx as a heredoc in the model's native
241
- // syntax. The model wrote this; mirror it back so the log is a true
242
- // record of its actions instead of a row of opaque status codes.
243
- const renderLogEntries = (entries) => entries.map((e) => {
244
- const meta = {};
245
- const coordinate = typeof e.coordinate === "string" ? e.coordinate : null;
246
- const op = typeof e.op === "string" && e.op.length > 0 ? e.op : null;
247
- if (coordinate !== null && op !== null)
248
- meta.path = `log://${coordinate}/${op}`;
249
- else if (coordinate !== null)
250
- meta.path = `log://${coordinate}`;
251
- if (typeof e.origin === "string")
252
- meta.origin = e.origin;
253
- if (op !== null)
254
- meta.op = op;
255
- if (typeof e.status === "number")
256
- meta.status = e.status;
257
- const target = renderActionTarget(e.target);
258
- if (target !== null)
259
- meta.target = target;
260
- // Op-specific meta enrichment for READ: surface the matcher body
261
- // and match count when a body matcher was used. Without these, the
262
- // model can't distinguish "0 matches" from "empty content" — both
263
- // would render as a status-204 line. The matcher comes from the
264
- // stored statement (tx); the count from the result (rx).
265
- if (op === "READ") {
266
- const tx = e.tx;
267
- if (tx !== null && typeof tx === "object" && tx.body !== null && typeof tx.body === "object") {
268
- if (typeof tx.body.raw === "string")
269
- meta.matcher = tx.body.raw;
270
- }
271
- const rx = typeof e.rx === "string" ? safeParse(e.rx) : e.rx;
272
- if (rx !== null && typeof rx === "object" && typeof rx.matches === "number") {
273
- meta.matches = rx.matches;
274
- }
370
+ static #renderActionTarget(target) {
371
+ if (target === null || target === undefined)
372
+ return null;
373
+ const rendered = PacketWire.#renderModelUri(target.scheme, target.pathname);
374
+ return rendered.length > 0 ? rendered : null;
275
375
  }
276
- const metaLine = `* ${canonicalJson(meta)}`;
277
- // READ@200: expose the response body. READ@204 (successfully empty —
278
- // 0 matcher hits, sentinel slice, or empty source) has no body to
279
- // render; the meta line carries the signal via `matches` / status code.
280
- if (op === "READ" && e.status === 200) {
281
- const rx = typeof e.rx === "string" ? safeParse(e.rx) : e.rx;
282
- if (rx !== null && typeof rx === "object" && typeof rx.content === "string" && rx.content.length > 0) {
283
- const fence = target ?? `log://${coordinate}`;
284
- // Line-navigable mimetypes (text/markdown, text/plain,
285
- // source code, etc.) get N:\t prefix per plurnk.md. Tree-
286
- // navigable (JSON, XML, HTML) render verbatim — line
287
- // numbers in the wrapper would collide with structural
288
- // navigation (jsonpath/xpath) used on these formats.
289
- // Classifier is consumer-side in this repo (SPEC.md §16.6).
290
- const mimetype = typeof rx.mimetype === "string" ? rx.mimetype : "text/plain";
291
- if (isLineNavigableMimetype(mimetype)) {
292
- const start = typeof rx.startLine === "number" ? rx.startLine : 1;
293
- return `${metaLine}\n${wrapHeredocBody(fence, numberLines(rx.content, start))}`;
294
- }
295
- return `${metaLine}\n${wrapHeredocBody(fence, rx.content)}`;
296
- }
376
+ // Render TelemetryEvent[] meta line per event, optionally followed by
377
+ // an N:\t-prefixed snippet block when the event carries `snippet` (the
378
+ // convention plurnk-service uses for content-offset positions model
379
+ // sees its own offending bytes alongside the error, not an abstract
380
+ // message it can't trace).
381
+ //
382
+ // Snippet renders verbatim already N:\t-prefixed at production time
383
+ // (Engine.#extractSnippet). The fence is `error://<line>` to give the
384
+ // model a stable URI shape it can ignore or reference; the `error://`
385
+ // scheme isn't writable, it just identifies "this block is locator
386
+ // context, not addressable content."
387
+ //
388
+ // `snippet` is stripped from the meta JSON so the snippet appears once,
389
+ // in the body block, not also as a quoted string in the meta.
390
+ static #renderTelemetryErrors(errors) {
391
+ return errors.map((e) => {
392
+ const snippet = typeof e.snippet === "string" ? e.snippet : null;
393
+ const meta = { ...e };
394
+ if (snippet !== null)
395
+ delete meta.snippet;
396
+ const metaLine = `* ${PacketWire.#canonicalJson(meta)}`;
397
+ if (snippet === null || snippet.length === 0)
398
+ return metaLine;
399
+ const line = typeof e.position?.line === "number" ? e.position.line : 0;
400
+ const fence = `error://${line}`;
401
+ return `${metaLine}\n${PacketWire.#wrapHeredocBody(fence, snippet)}`;
402
+ }).join("\n");
297
403
  }
298
- // Every other op: re-emit the model's statement. EDIT, EXEC, SEND,
299
- // COPY, MOVE, FIND, SHOW, HIDE — each gets its native heredoc form
300
- // back. Without this the log row is a status code with no record
301
- // of what the model actually wrote, and the model has to back into
302
- // its own actions by inference (see reasoning.md trace from the
303
- // pre-fix count-files run).
304
- const heredoc = renderStatementHeredoc(e.tx);
305
- if (heredoc !== null)
306
- return `${metaLine}\n${heredoc}`;
307
- return metaLine;
308
- }).join("\n");
309
- const renderActionTarget = (target) => {
310
- if (target === null || target === undefined)
311
- return null;
312
- const rendered = renderModelUri(target.scheme, target.pathname);
313
- return rendered.length > 0 ? rendered : null;
314
- };
315
- // Render TelemetryEvent[] → meta line per event, optionally followed by
316
- // an N:\t-prefixed snippet block when the event carries `snippet` (the
317
- // convention plurnk-service uses for content-offset positions — model
318
- // sees its own offending bytes alongside the error, not an abstract
319
- // message it can't trace).
320
- //
321
- // Snippet renders verbatim — already N:\t-prefixed at production time
322
- // (Engine.#extractSnippet). The fence is `error://<line>` to give the
323
- // model a stable URI shape it can ignore or reference; the `error://`
324
- // scheme isn't writable, it just identifies "this block is locator
325
- // context, not addressable content."
326
- //
327
- // `snippet` is stripped from the meta JSON so the snippet appears once,
328
- // in the body block, not also as a quoted string in the meta.
329
- const renderTelemetryErrors = (errors) => errors.map((e) => {
330
- const snippet = typeof e.snippet === "string" ? e.snippet : null;
331
- const meta = { ...e };
332
- if (snippet !== null)
333
- delete meta.snippet;
334
- const metaLine = `* ${canonicalJson(meta)}`;
335
- if (snippet === null || snippet.length === 0)
336
- return metaLine;
337
- const line = typeof e.position?.line === "number" ? e.position.line : 0;
338
- const fence = `error://${line}`;
339
- return `${metaLine}\n${wrapHeredocBody(fence, snippet)}`;
340
- }).join("\n");
404
+ }
341
405
  //# sourceMappingURL=packet-wire.js.map