@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
@@ -1,20 +1,20 @@
1
- import { PathSyntax, } from "@plurnk/plurnk-contracts";
2
- import { LineMarkerOps } from "../content/index.js";
3
- import { ReadResolve } from "../content/index.js";
1
+ import { InvalidTagSignalError, PathSyntax, TagSignal, } from "@plurnk/plurnk-contracts";
4
2
  import Matcher from "../content/matcher.js";
5
- import { renderFindContent } from "./_entry-find.js";
3
+ import { emptyFindFields, projectFindResult } from "./_entry-find.js";
6
4
  import { CoreSchemeAdapterBase } from "../core/CoreSchemeServices.js";
7
5
  import Results from "../core/results.js";
8
6
  import LogBody from "../core/LogBody.js";
7
+ import LogEntryProjection from "../core/LogEntryProjection.js";
9
8
  import EntrySemantic from "./_entry-semantic.js";
10
9
  import EntryGraph from "./_entry-graph.js";
11
10
  import { resolveSearchCandidates } from "./_search-candidate.js";
12
11
  import { pathFolderSummaries, pathScope, pathScopeMatches } from "./_path-scope.js";
13
12
  // log:///<loop_seq>/<turn_seq>/<sequence>[/<op>] — the trailing /op segment
14
- // is wire-rendering self-documentation derived from the row's `op` field;
15
- // parsing accepts it (or omits it) and identifies the row by coordinate. The op
16
- // suffix is case-INSENSITIVE: model ops render UPPERCASE (READ/EDIT/FIND) but the
17
- // engine-minted rows are lowercase (`error`, `model`), and both are curatable.
13
+ // is canonical model-facing identity derived from the row's projected `op`;
14
+ // parsing accepts it (or omits it), but a supplied suffix must agree. The op
15
+ // suffix is case-INSENSITIVE: model ops render UPPERCASE (READ/EDIT/FIND) while
16
+ // engine-minted selectors such as `error` are lowercase. An actionless model
17
+ // emission has no suffix at all.
18
18
  const COORDINATE = /^(\d+)\/(\d+)\/(\d+)(?:\/([A-Za-z]+))?$/;
19
19
  // {§log-coordinate-hierarchy} — a log coordinate is a HIERARCHICAL PREFIX: `1` selects loop 1's rows,
20
20
  // `1/2` turn 1/2's rows, `1/2/3` the one row. A full coordinate is always 3 parts, so a 1- or 2-part
@@ -42,18 +42,25 @@ const parseCoordinate = (pathname) => {
42
42
  loopSeq: Number(match[1]),
43
43
  turnSeq: Number(match[2]),
44
44
  sequence: Number(match[3]),
45
+ op: match[4] ?? null,
45
46
  };
46
47
  };
47
- const canonicalCoordinate = (coordinate) => coordinate.replace(/\/[A-Za-z]+$/, "");
48
48
  // A rendered log path appends `/OP` to the canonical loop/turn/item coordinate
49
- // as self-documentation. Selection honors both views: ordinary path globs map
49
+ // as identity. Selection honors both views: ordinary path globs map
50
50
  // the three-level resource tree, while an explicit OP segment can still filter
51
51
  // rows (`log:///**/READ`).
52
- const coordinateScopeMatches = (scope, coordinate) => pathScopeMatches(scope, coordinate) || pathScopeMatches(scope, canonicalCoordinate(coordinate));
52
+ const coordinateScopeMatches = (scope, coordinate) => pathScopeMatches(scope, coordinate) || pathScopeMatches(scope, LogEntryProjection.base(coordinate));
53
+ const projectedCoordinateRows = (rows) => rows.map((row) => ({
54
+ ...row,
55
+ coordinate: LogEntryProjection.coordinate(row.coordinate, row),
56
+ }));
57
+ const coordinateCandidatePrefix = (prefix) => prefix === null
58
+ ? null
59
+ : LogEntryProjection.base(prefix);
53
60
  export default class Log extends CoreSchemeAdapterBase {
54
61
  static manifest = {
55
62
  name: "log",
56
- channels: {}, // logs render through read(), not channel storage
63
+ channels: {},
57
64
  defaultChannel: "",
58
65
  category: "logging",
59
66
  // Log KILL is model-authorized curation. Other mutations remain unavailable
@@ -62,27 +69,21 @@ export default class Log extends CoreSchemeAdapterBase {
62
69
  volatile: false,
63
70
  modelVisible: true,
64
71
  folderScopes: true,
65
- example: "<<READ(log:///1/2/3)::READ",
72
+ example: "## READ0 (log:///1/2/3)",
66
73
  };
67
- async read(statement, ctx) {
74
+ async resolveCoreRepresentation(target, ctx) {
68
75
  const core = this.coreContext(ctx);
69
76
  const { db, workerId } = core;
70
- const failure = (code, status, detail, extensions = {}) => Results.failure("scheme:log", code, status, detail, { content: null, mimetype: null }, extensions);
71
- if (statement.target === null) {
77
+ const failure = (code, status, detail, extensions = {}) => ({
78
+ result: Results.failure("scheme:log", code, status, detail, { content: null, mimetype: null, channel: null }, extensions),
79
+ });
80
+ if (target === null) {
72
81
  return failure("read-target-required", 400, "READ requires a log coordinate.", {
73
82
  recovery: "Provide one exact log coordinate.",
74
83
  retryable: false,
75
84
  });
76
85
  }
77
- // READ is exact — one coordinate, one row. Tag recall is OPEN[tag]/FIND[tag]'s job ({§log-region-tagging}),
78
- // not a filter on a single-row read.
79
- if (Array.isArray(statement.signal) && statement.signal.length > 0) {
80
- return failure("tagged-coordinate-not-found", 404, "The exact log row does not carry every requested tag.", {
81
- recovery: "Remove the tag filter or use a log row carrying those tags.",
82
- retryable: false,
83
- });
84
- }
85
- const pathname = (statement.target.kind === "url" ? statement.target.pathname : statement.target.raw).replace(/^\//, "");
86
+ const pathname = (target.kind === "url" ? target.pathname : target.raw).replace(/^\//, "");
86
87
  const coord = parseCoordinate(pathname);
87
88
  if (coord === null) {
88
89
  return failure("coordinate-malformed", 400, `The log coordinate '${pathname}' is malformed.`, {
@@ -94,67 +95,45 @@ export default class Log extends CoreSchemeAdapterBase {
94
95
  const row = await db.log_read_by_coordinate.get({ worker_id: workerId, loop_seq: coord.loopSeq, turn_seq: coord.turnSeq, sequence: coord.sequence });
95
96
  if (row === undefined)
96
97
  return failure("entry-not-found", 404, `No log entry exists at log:///${pathname}.`);
98
+ if (!LogEntryProjection.accepts(coord.op, row)) {
99
+ return failure("entry-not-found", 404, `No log entry exists at log:///${pathname}.`);
100
+ }
97
101
  const { content: underlyingContent, mimetype: underlyingMimetype } = LogBody.resolve({
98
102
  op: row.op,
103
+ attrs: row.attrs,
99
104
  tx: row.tx,
100
105
  rx: row.rx,
101
106
  mimetypeTx: row.mimetype_tx,
102
107
  mimetypeRx: row.mimetype_rx,
103
108
  });
104
- const resolved = await ReadResolve.resolve({
105
- content: underlyingContent,
106
- mimetype: underlyingMimetype,
107
- lineMarker: statement.lineMarker,
108
- body: statement.body,
109
- mimetypes: core.mimetypes,
110
- });
111
- if (resolved.status >= 400) {
112
- if (resolved.problem !== undefined) {
113
- return Results.assert({
114
- ...resolved,
115
- content: null,
116
- mimetype: null,
117
- });
118
- }
119
- if (resolved.reason === undefined) {
120
- throw new Error(`Log.read: ReadResolve returned status ${resolved.status} without Problem Details or a diagnostic`);
121
- }
122
- return failure(resolved.status === 416
123
- ? "range-not-satisfiable"
124
- : resolved.status === 501
125
- ? "matcher-unavailable"
126
- : "read-resolution-failed", resolved.status, resolved.reason, {
127
- ...(resolved.range === undefined ? {} : { range: resolved.range, stage: "projection" }),
128
- ...(statement.body === null || resolved.status === 416
129
- ? {}
130
- : {
131
- stage: "matcher",
132
- dialect: statement.body.dialect,
133
- recovery: resolved.status === 501
134
- ? "Retry the READ without a content matcher."
135
- : "Correct or remove the matcher.",
136
- retryable: false,
137
- }),
138
- });
139
- }
140
- return { ...resolved };
109
+ return {
110
+ representation: {
111
+ channels: {
112
+ "": {
113
+ content: underlyingContent,
114
+ mimetype: underlyingMimetype,
115
+ state: "static",
116
+ },
117
+ },
118
+ },
119
+ };
141
120
  }
142
121
  // {§log-uniform-query} — FIND over the worker's log rows, on the SAME source-agnostic primitive
143
122
  // every entry scheme runs (Matcher.matchCandidates, {§find-source-agnostic}): candidates are the
144
123
  // coordinate-scoped rows resolved by LogBody exactly as READ shows them, so every content
145
- // dialect works on log BY CONSTRUCTION and FIND(log)->READ(coordinate) composes like any scheme.
124
+ // dialect works on log BY CONSTRUCTION and log FIND -> coordinate READ composes like any scheme.
146
125
  async find(statement, ctx) {
147
- return this.#find(statement, this.coreContext(ctx), true);
126
+ return this.#find(statement, this.coreContext(ctx));
148
127
  }
149
- async #find(statement, core, enforceRenderBudget, allowTargetless = false) {
128
+ async #find(statement, core, allowTargetless = false, filterTags = []) {
150
129
  const { db, workerId, mimetypes } = core;
151
130
  const empty = (status, detail, extensions = {}) => {
152
- const fields = { content: null, mimetype: null, results: [], itemsTokenTotal: 0, pathnames: [], matches: [] };
131
+ const fields = emptyFindFields();
153
132
  if (status >= 400 && detail === undefined) {
154
133
  throw new Error(`Log.find: selection returned status ${status} without Problem Details or a diagnostic`);
155
134
  }
156
135
  return status >= 400
157
- ? Results.failure("scheme:log", status === 416 ? "range-not-satisfiable" : status === 501 ? "matcher-not-implemented" : "find-failed", status, detail, fields, extensions)
136
+ ? Results.failure("scheme:log", status === 404 ? "entry-not-found" : status === 416 ? "range-not-satisfiable" : status === 501 ? "matcher-not-implemented" : "find-failed", status, detail, fields, extensions)
158
137
  : { status, ...fields };
159
138
  };
160
139
  if (statement.target === null && !allowTargetless) {
@@ -175,12 +154,18 @@ export default class Log extends CoreSchemeAdapterBase {
175
154
  });
176
155
  }
177
156
  const scope = pathScope(glob, false);
178
- // {§log-region-tagging} — a tag signal AND-filters the candidates ({§find-tag-filter-and-semantics}):
179
- // a row survives only if it carries EVERY listed tag. No signal → the plain coordinate scope.
180
- const tags = Array.isArray(statement.signal) ? statement.signal : [];
181
- const candidateRows = tags.length > 0
182
- ? await db.log_find_candidates_tagged.all({ worker_id: workerId, scope_prefix: scope.candidatePrefix, tags: JSON.stringify(tags) })
183
- : await db.log_find_candidates.all({ worker_id: workerId, scope_prefix: scope.candidatePrefix });
157
+ if (scope.kind === "exact") {
158
+ const coordinate = parseCoordinate(scope.pathname);
159
+ if (coordinate === null)
160
+ throw new Error(`Exact log scope has a malformed coordinate ${JSON.stringify(scope.pathname)}`);
161
+ if (await this.#resolveExactId(coordinate, core) === null) {
162
+ return empty(404, `No log entry exists at ${statement.target?.raw ?? `log:///${scope.pathname}`}.`, { target: statement.target?.raw ?? `log:///${scope.pathname}` });
163
+ }
164
+ }
165
+ const storedCandidateRows = filterTags.length > 0
166
+ ? await db.log_curation_find_candidates_tagged.all({ worker_id: workerId, scope_prefix: coordinateCandidatePrefix(scope.candidatePrefix), tags: JSON.stringify(filterTags) })
167
+ : await db.log_find_candidates.all({ worker_id: workerId, scope_prefix: coordinateCandidatePrefix(scope.candidatePrefix) });
168
+ const candidateRows = projectedCoordinateRows(storedCandidateRows);
184
169
  let rows;
185
170
  try {
186
171
  rows = candidateRows.filter((row) => coordinateScopeMatches(scope, row.coordinate));
@@ -192,12 +177,13 @@ export default class Log extends CoreSchemeAdapterBase {
192
177
  retryable: false,
193
178
  });
194
179
  }
195
- const candidateByCanonicalCoord = new Map(candidateRows.map((row) => [canonicalCoordinate(row.coordinate), row]));
180
+ const candidateByCanonicalCoord = new Map(candidateRows.map((row) => [LogEntryProjection.base(row.coordinate), row]));
196
181
  const byCoord = new Map(rows.map((r) => [r.coordinate, r]));
197
182
  const projected = rows.map((r) => ({
198
183
  key: r.coordinate,
199
184
  ...LogBody.resolve({
200
185
  op: r.op,
186
+ attrs: r.attrs,
201
187
  tx: r.tx,
202
188
  rx: r.rx,
203
189
  mimetypeTx: r.mimetype_tx,
@@ -234,26 +220,7 @@ export default class Log extends CoreSchemeAdapterBase {
234
220
  retryable: false,
235
221
  });
236
222
  }
237
- let selected = ranked.results;
238
- if (selection.page !== null) {
239
- const page = LineMarkerOps.page(selected, selection.page);
240
- if (page.status !== 200) {
241
- if (page.problem === undefined)
242
- throw new Error("Log semantic FIND pagination failed without Problem Details");
243
- return Results.assert({
244
- status: page.status,
245
- problem: page.problem,
246
- content: null,
247
- mimetype: null,
248
- results: [],
249
- itemsTokenTotal: 0,
250
- pathnames: [],
251
- matches: [],
252
- });
253
- }
254
- selected = page.items ?? [];
255
- }
256
- const sourceMatches = selected.map(({ key, lineStart, lineEnd }) => ({
223
+ const sourceMatches = ranked.results.map(({ key, lineStart, lineEnd }) => ({
257
224
  key,
258
225
  span: { lineStart, lineEnd },
259
226
  }));
@@ -269,7 +236,7 @@ export default class Log extends CoreSchemeAdapterBase {
269
236
  recovery: "Wait for search indexing to complete before repeating the search.",
270
237
  retryable: false,
271
238
  });
272
- const universeRows = await db.log_find_candidates.all({ worker_id: workerId, scope_prefix: null });
239
+ const universeRows = projectedCoordinateRows(await db.log_find_candidates.all({ worker_id: workerId, scope_prefix: null }));
273
240
  const universe = resolveSearchCandidates(universeRows.map(({ coordinate, deep_hash }) => ({ key: coordinate, deepHash: deep_hash })));
274
241
  if (universe.state === "incomplete")
275
242
  return empty(503, `The persistent search index covers ${universe.indexed} of ${universe.total} log results in the relationship universe.`, {
@@ -313,43 +280,15 @@ export default class Log extends CoreSchemeAdapterBase {
313
280
  return Results.assert({
314
281
  status: r.status,
315
282
  problem: r.problem,
316
- content: null,
317
- mimetype: null,
318
- results: [],
319
- itemsTokenTotal: 0,
320
- pathnames: [],
321
- matches: [],
283
+ ...emptyFindFields(),
322
284
  });
323
285
  }
324
286
  matches = r.matches.map(({ key, matches: ranges }) => ({ pathname: key, matches: ranges }));
325
287
  }
326
288
  const folderSummaries = statement.body === null
327
- ? pathFolderSummaries(scope, candidateRows.map((row) => canonicalCoordinate(row.coordinate)))
289
+ ? pathFolderSummaries(scope, candidateRows.map((row) => LogEntryProjection.base(row.coordinate)))
328
290
  : [];
329
- if (statement.lineMarker !== null && statement.body?.dialect !== "semantic" && folderSummaries.length === 0) {
330
- const page = LineMarkerOps.page(matches, statement.lineMarker);
331
- if (page.status !== 200) {
332
- if (page.problem === undefined)
333
- throw new Error("Log FIND pagination failed without Problem Details");
334
- return Results.assert({
335
- status: page.status,
336
- problem: page.problem,
337
- content: null,
338
- mimetype: null,
339
- results: [],
340
- itemsTokenTotal: 0,
341
- pathnames: [],
342
- matches: [],
343
- });
344
- }
345
- matches = page.items ?? [];
346
- }
347
- // The result rows mirror the catalog-row shape: one item per selected
348
- // resource, with optional match coordinates for a surgical READ.
349
- let results = [];
350
- const seenPath = [];
351
- const seen = new Set();
352
- let itemsTokenTotal = 0;
291
+ const resources = [];
353
292
  for (const m of matches) {
354
293
  const row = byCoord.get(m.pathname);
355
294
  if (row === undefined)
@@ -357,22 +296,28 @@ export default class Log extends CoreSchemeAdapterBase {
357
296
  const path = `log:///${m.pathname}`;
358
297
  const proj = LogBody.resolve({
359
298
  op: row.op,
299
+ attrs: row.attrs,
360
300
  tx: row.tx,
361
301
  rx: row.rx,
362
302
  mimetypeTx: row.mimetype_tx,
363
303
  mimetypeRx: row.mimetype_rx,
364
304
  });
365
- const item = {
305
+ const channel = {
366
306
  path,
367
- channels: { [path]: { mimetype: proj.mimetype, tokens: row.tokens, lines: proj.content.length === 0 ? 0 : proj.content.split("\n").length } },
307
+ mimetype: proj.mimetype,
308
+ tokens: row.tokens,
309
+ lines: proj.content.length === 0 ? 0 : proj.content.split("\n").length,
368
310
  };
369
- results.push(m.matches.length > 0 ? { ...item, matches: m.matches } : item);
370
- if (!seen.has(m.pathname)) {
371
- seen.add(m.pathname);
372
- seenPath.push(m.pathname);
373
- itemsTokenTotal += row.tokens;
311
+ const storedTags = JSON.parse(row.tags);
312
+ if (!Array.isArray(storedTags) || !storedTags.every((tag) => typeof tag === "string" && tag.length > 0)) {
313
+ throw new TypeError(`Log row ${m.pathname} contains invalid tag storage.`);
374
314
  }
315
+ if (storedTags.length > 0)
316
+ channel.tags = storedTags;
317
+ const item = [channel];
318
+ resources.push({ item, match: m });
375
319
  }
320
+ const scopes = [];
376
321
  for (const folder of folderSummaries) {
377
322
  const members = folder.pathnames.map((coordinate) => candidateByCanonicalCoord.get(coordinate)).filter((row) => row !== undefined);
378
323
  const item = {
@@ -380,83 +325,56 @@ export default class Log extends CoreSchemeAdapterBase {
380
325
  items: members.length,
381
326
  tokens: members.reduce((sum, row) => sum + row.tokens, 0),
382
327
  };
383
- results.push(item);
384
- itemsTokenTotal += item.tokens;
328
+ scopes.push(item);
385
329
  }
386
- if (folderSummaries.length > 0) {
387
- results.sort((a, b) => a.path.localeCompare(b.path));
388
- if (statement.lineMarker !== null) {
389
- const page = LineMarkerOps.page(results, statement.lineMarker);
390
- if (page.status !== 200) {
391
- if (page.problem === undefined)
392
- throw new Error("Log FIND pagination failed without Problem Details");
393
- return Results.assert({
394
- status: page.status,
395
- problem: page.problem,
396
- content: null,
397
- mimetype: null,
398
- results: [],
399
- itemsTokenTotal: 0,
400
- pathnames: [],
401
- matches: [],
402
- });
403
- }
404
- results = page.items ?? [];
405
- const retained = new Set(results.filter((item) => item.items === undefined).map((item) => item.path.replace(/^log:\/\/\//, "").replace(/^\//, "")));
406
- matches = matches.filter((match) => retained.has(match.pathname));
407
- seenPath.splice(0, seenPath.length, ...matches.map((match) => match.pathname).filter((coordinate, i, all) => all.indexOf(coordinate) === i));
408
- itemsTokenTotal = results.reduce((sum, item) => sum + (item.items === undefined
409
- ? Object.values(item.channels).reduce((channelSum, channel) => channelSum + channel.tokens, 0)
410
- : item.tokens ?? 0), 0);
411
- }
412
- }
413
- if (results.length === 0)
414
- return empty(204);
415
- // {§find-count-not-contents} is source-agnostic. Count the rendered
416
- // catalog items; a shallow map's folder summaries replace, rather than
417
- // conceal, their descendants.
418
- const budget = Number.parseInt(process.env.PLURNK_SERVICE_FIND_MAX_MATCHES ?? "0", 10);
419
- if (enforceRenderBudget && budget > 0 && results.length > budget) {
420
- const noun = results.some((item) => item.items !== undefined) ? "catalog items" : "entries";
421
- return {
422
- status: 200,
423
- content: `${results.length} ${noun} match, exceeding the render budget (${budget}) — not enumerated.`,
424
- mimetype: "text/markdown",
425
- results: [],
426
- itemsTokenTotal,
427
- pathnames: [],
428
- matches: [],
429
- omittedItems: results.length,
430
- maximumItems: budget,
431
- };
432
- }
433
- // matches[].pathname is the fan-out's retarget key — `/loop/turn/seq/OP` re-parses as a
434
- // coordinate (the /OP suffix is accepted), so READ(log://)<matcher> fan-out delivers rows.
435
- const fanMatches = matches.map((m) => ({ ...m, pathname: `/${m.pathname}` }));
436
- return { status: 200, content: renderFindContent(results), mimetype: "application/json", results, itemsTokenTotal, pathnames: seenPath.map((p) => `/${p}`), matches: fanMatches };
330
+ return projectFindResult(statement, scope, resources, scopes);
437
331
  }
438
332
  async open(statement, ctx) {
439
- return this.#setExpanded(statement, this.coreContext(ctx), 1);
333
+ const core = this.coreContext(ctx);
334
+ const outcome = await this.#planCuration(statement, core, 1);
335
+ if (outcome.plan !== null)
336
+ await this.#applyDirect(outcome.plan, core);
337
+ return outcome.result;
440
338
  }
441
339
  // FOLD toggles the expanded bit only — an active subscription stays alive. {§subscriptions-fold-keeps-subscription}
442
340
  async fold(statement, ctx) {
443
- return this.#setExpanded(statement, this.coreContext(ctx), 0);
341
+ const core = this.coreContext(ctx);
342
+ const outcome = await this.#planCuration(statement, core, 0);
343
+ if (outcome.plan !== null)
344
+ await this.#applyDirect(outcome.plan, core);
345
+ return outcome.result;
346
+ }
347
+ // Core dispatch retains the exact landed effect beside the suppressed
348
+ // OPEN/FOLD event. Direct scheme callers receive the ordinary result above.
349
+ async curate(statement, ctx) {
350
+ return this.#planCuration(statement, this.coreContext(ctx), statement.op === "OPEN" ? 1 : 0);
444
351
  }
445
352
  // Resolve a log:/// target — a concrete coordinate or path-glob — to the matched row ids.
446
353
  // OPEN/FOLD/KILL share this one path selection; log curation has no positional pagination.
354
+ async #resolveExactId(coordinate, ctx) {
355
+ const row = await ctx.db.log_id_by_coordinate.get({
356
+ worker_id: ctx.workerId,
357
+ loop_seq: coordinate.loopSeq,
358
+ turn_seq: coordinate.turnSeq,
359
+ sequence: coordinate.sequence,
360
+ });
361
+ return row !== undefined && LogEntryProjection.accepts(coordinate.op, row) ? row.id : null;
362
+ }
447
363
  async #resolveIds(pathname, ctx) {
448
364
  const { db, workerId } = ctx;
449
365
  const coord = parseCoordinate(pathname);
450
366
  if (coord !== null) {
451
- const row = await db.log_id_by_coordinate.get({ worker_id: workerId, loop_seq: coord.loopSeq, turn_seq: coord.turnSeq, sequence: coord.sequence });
452
- return row === undefined ? { status: 404, ids: [] } : { status: 200, ids: [row.id] };
367
+ const id = await this.#resolveExactId(coord, ctx);
368
+ return id === null
369
+ ? { status: 404, ids: [] }
370
+ : { status: 200, ids: [id] };
453
371
  }
454
372
  // {§log-coordinate-hierarchy} — one resolution for every consumer (curation here, find below).
455
373
  const glob = coordinateGlob(pathname);
456
374
  if (glob === null)
457
375
  return { status: 400, ids: [], error: `The log target '${pathname}' is malformed.` };
458
376
  const scope = pathScope(glob, false);
459
- const candidates = await db.log_match_coordinates.all({ worker_id: workerId, scope_prefix: scope.candidatePrefix });
377
+ const candidates = projectedCoordinateRows(await db.log_match_coordinates.all({ worker_id: workerId, scope_prefix: coordinateCandidatePrefix(scope.candidatePrefix) }));
460
378
  let matched;
461
379
  try {
462
380
  matched = candidates.filter((row) => coordinateScopeMatches(scope, row.coordinate));
@@ -470,8 +388,8 @@ export default class Log extends CoreSchemeAdapterBase {
470
388
  return { status: 204, ids: [] };
471
389
  return { status: 200, ids: matched.map((row) => row.id) };
472
390
  }
473
- // {§log-region-tagging} — resolve OPEN[tag] to ids: candidates are the target's glob scope (the
474
- // whole worker log when targetless — a bare OPEN[tag] recalls the entire tagged working-set),
391
+ // {§log-item-tags} — resolve tagged OPEN/FOLD to ids: candidates are the target's glob scope (the
392
+ // whole worker log when targetless),
475
393
  // AND-filtered to rows carrying EVERY listed tag. Zero matches is a no-op success (204), mirroring
476
394
  // #resolveIds — recalling a name that tags nothing steers nothing.
477
395
  async #resolveByTags(statement, tags, ctx) {
@@ -480,12 +398,16 @@ export default class Log extends CoreSchemeAdapterBase {
480
398
  const glob = coordinateGlob(pathname);
481
399
  if (glob === null)
482
400
  return { status: 400, ids: [], error: `The log target '${pathname}' is malformed.` };
401
+ const coordinate = parseCoordinate(pathname);
402
+ if (coordinate !== null && await this.#resolveExactId(coordinate, ctx) === null) {
403
+ return { status: 404, ids: [] };
404
+ }
483
405
  const scope = pathScope(glob, false);
484
- const candidates = await db.log_match_coordinates_tagged.all({
406
+ const candidates = projectedCoordinateRows(await db.log_match_coordinates_tagged.all({
485
407
  worker_id: workerId,
486
- scope_prefix: scope.candidatePrefix,
408
+ scope_prefix: coordinateCandidatePrefix(scope.candidatePrefix),
487
409
  tags: JSON.stringify(tags),
488
- });
410
+ }));
489
411
  let matched;
490
412
  try {
491
413
  matched = candidates.filter((row) => coordinateScopeMatches(scope, row.coordinate));
@@ -498,100 +420,145 @@ export default class Log extends CoreSchemeAdapterBase {
498
420
  return { status: 200, ids: matched.map((row) => row.id) };
499
421
  }
500
422
  // A matcher-bearing curation op reuses FIND's complete source-agnostic selector with
501
- // rendering disabled. OPEN's tags filter candidates; FOLD's tags are withheld until
502
- // after selection because they apply to the rows being folded.
423
+ // explicit all-results pagination. Curation tags are an independent selector;
424
+ // an authored FIND signal classifies its result row and never enters candidate selection.
503
425
  async #resolveByMatcher(statement, filterTags, ctx) {
504
426
  const selected = await this.#find({
505
427
  ...statement,
506
428
  op: "FIND",
507
- signal: filterTags ? statement.signal : null,
508
- lineMarker: null,
509
- }, ctx, false, filterTags && Array.isArray(statement.signal) && statement.signal.length > 0 && statement.target === null);
429
+ signal: null,
430
+ lineMarker: { marks: [1, -1] },
431
+ }, ctx, true, filterTags);
510
432
  if (selected.status !== 200)
511
433
  return { status: selected.status, ids: [], problem: selected.problem };
434
+ let selectedPaths = selected.results.flatMap((item) => Array.isArray(item) && typeof item[0]?.path === "string"
435
+ ? [item[0].path]
436
+ : []);
437
+ if (selectedPaths.length === 0) {
438
+ if (statement.target === null) {
439
+ throw new Error("A successful broad log matcher selection returned no resource paths");
440
+ }
441
+ const pathname = (statement.target.kind === "url" ? statement.target.pathname : statement.target.raw).replace(/^\//, "");
442
+ const glob = coordinateGlob(pathname);
443
+ if (glob === null)
444
+ throw new Error(`Log matcher selected malformed target ${JSON.stringify(pathname)}`);
445
+ const scope = pathScope(glob, false);
446
+ if (scope.kind !== "exact") {
447
+ throw new Error("A successful broad log matcher selection returned no resource paths");
448
+ }
449
+ selectedPaths = [scope.pathname];
450
+ }
512
451
  const ids = [];
513
- for (const { pathname } of selected.matches) {
514
- const coordinate = parseCoordinate(pathname.replace(/^\//, ""));
452
+ for (const pathname of selectedPaths) {
453
+ const coordinate = parseCoordinate(pathname.replace(/^log:\/\/\//, "").replace(/^\//, ""));
515
454
  if (coordinate === null)
516
455
  throw new Error(`Log matcher selected malformed coordinate '${pathname}'`);
517
- const row = await ctx.db.log_id_by_coordinate.get({
518
- worker_id: ctx.workerId,
519
- loop_seq: coordinate.loopSeq,
520
- turn_seq: coordinate.turnSeq,
521
- sequence: coordinate.sequence,
522
- });
523
- if (row === undefined)
456
+ const id = await this.#resolveExactId(coordinate, ctx);
457
+ if (id === null)
524
458
  return { status: 404, ids: [] };
525
- ids.push(row.id);
459
+ ids.push(id);
526
460
  }
527
461
  return ids.length === 0 ? { status: 204, ids: [] } : { status: 200, ids };
528
462
  }
529
- async #applyExpanded(ids, expanded, tags, ctx) {
530
- for (const id of ids)
531
- await ctx.db.log_set_expanded_by_id.run({ id, expanded });
532
- if (expanded === 0) {
533
- for (const id of ids)
534
- for (const tag of tags)
535
- await ctx.db.log_write_tag.run({ log_entry_id: id, tag });
463
+ async #applyDirect(plan, ctx) {
464
+ for (const id of plan.targetLogEntryIds) {
465
+ await ctx.db.log_set_expanded_by_id.run({ id, expanded: plan.expanded });
466
+ for (const tag of plan.remove)
467
+ await ctx.db.log_remove_tag.run({ log_entry_id: id, tag });
468
+ for (const tag of plan.add)
469
+ await ctx.db.log_write_tag.run({ log_entry_id: id, tag });
536
470
  }
537
- return { status: 200, matched: ids.length };
538
471
  }
539
- async #setExpanded(statement, ctx, expanded) {
540
- const signal = Array.isArray(statement.signal) ? statement.signal : [];
472
+ async #planCuration(statement, ctx, expanded) {
473
+ let tags;
474
+ try {
475
+ tags = TagSignal.curation(statement.signal);
476
+ }
477
+ catch (cause) {
478
+ if (!(cause instanceof InvalidTagSignalError))
479
+ throw cause;
480
+ return {
481
+ result: Results.failure("scheme:log", "tag-signal-invalid", 400, cause.message, {}, { retryable: false }),
482
+ plan: null,
483
+ };
484
+ }
485
+ const planned = (ids) => ({
486
+ result: { status: 200, matched: ids.length },
487
+ plan: {
488
+ targetLogEntryIds: ids,
489
+ expanded,
490
+ add: tags.add,
491
+ remove: tags.remove,
492
+ },
493
+ });
541
494
  if (statement.body !== null) {
542
- const matched = await this.#resolveByMatcher(statement, expanded === 1, ctx);
495
+ const matched = await this.#resolveByMatcher(statement, tags.filter, ctx);
543
496
  if (matched.status === 204)
544
- return { status: 204, matched: 0 };
497
+ return { result: { status: 204, matched: 0 }, plan: null };
545
498
  if (matched.status !== 200) {
546
499
  if (matched.problem !== undefined) {
547
- return Results.assert({ status: matched.status, problem: matched.problem });
500
+ return {
501
+ result: Results.assert({ status: matched.status, problem: matched.problem }),
502
+ plan: null,
503
+ };
548
504
  }
549
- return Results.failure("scheme:log", matched.status === 404 ? "entry-not-found" : "curation-failed", matched.status, "No log entry matches the requested selection.");
505
+ return {
506
+ result: Results.failure("scheme:log", matched.status === 404 ? "entry-not-found" : "curation-failed", matched.status, "No log entry matches the requested selection."),
507
+ plan: null,
508
+ };
550
509
  }
551
- return this.#applyExpanded(matched.ids, expanded, signal, ctx);
510
+ return planned(matched.ids);
552
511
  }
553
- // {§log-region-tagging} — OPEN[tag] is the READ side: recall rows by tag, target optional (a bare
554
- // OPEN[tag] recalls the whole tagged working-set). FOLD never resolves by tag — it is the WRITE
555
- // side (it stamps the tag below), always scoped to the target region it folds.
556
- if (expanded === 1 && signal.length > 0) {
557
- const rt = await this.#resolveByTags(statement, signal, ctx);
558
- if (rt.status === 204)
559
- return { status: 204, matched: 0 };
560
- if (rt.status !== 200) {
561
- return Results.failure("scheme:log", "open-failed", rt.status, rt.error ?? "No log entry matches the requested selection.", {}, {
562
- target: statement.target?.raw ?? null,
563
- ...(rt.status === 400
564
- ? {
565
- recovery: "Use a log coordinate, prefix, or glob.",
566
- retryable: false,
567
- }
568
- : {}),
569
- });
512
+ // {§log-item-tags} — unsigned terms are the symmetric ALL-tags selector;
513
+ // signed terms are changes applied only after the exact set is resolved.
514
+ if (tags.filter.length > 0) {
515
+ const selected = await this.#resolveByTags(statement, [...tags.filter], ctx);
516
+ if (selected.status === 204)
517
+ return { result: { status: 204, matched: 0 }, plan: null };
518
+ if (selected.status !== 200) {
519
+ return {
520
+ result: Results.failure("scheme:log", "curation-failed", selected.status, selected.error ?? "No log entry matches the requested selection.", {}, {
521
+ target: statement.target?.raw ?? null,
522
+ ...(selected.status === 400
523
+ ? {
524
+ recovery: "Use a log coordinate, prefix, or glob.",
525
+ retryable: false,
526
+ }
527
+ : {}),
528
+ }),
529
+ plan: null,
530
+ };
570
531
  }
571
- return this.#applyExpanded(rt.ids, 1, signal, ctx);
532
+ return planned(selected.ids);
572
533
  }
573
534
  if (statement.target === null) {
574
- return Results.failure("scheme:log", "target-required", 400, `${expanded === 1 ? "OPEN" : "FOLD"} requires a log target.`, {}, {
575
- recovery: "Provide a log coordinate, prefix, or glob.",
576
- retryable: false,
577
- });
535
+ return {
536
+ result: Results.failure("scheme:log", "target-required", 400, `${expanded === 1 ? "OPEN" : "FOLD"} requires a path, body pattern, or unsigned tag; signed tags do not select log items.`, {}, {
537
+ recovery: "Provide a log coordinate, prefix, glob, body pattern, or unsigned tag.",
538
+ retryable: false,
539
+ }),
540
+ plan: null,
541
+ };
578
542
  }
579
543
  const pathname = (statement.target.kind === "url" ? statement.target.pathname : statement.target.raw).replace(/^\//, "");
580
- const r = await this.#resolveIds(pathname, ctx);
581
- if (r.status === 204)
582
- return { status: 204, matched: 0 };
583
- if (r.status !== 200) {
584
- return Results.failure("scheme:log", r.status === 404 ? "entry-not-found" : "curation-failed", r.status, r.error ?? `No log entry matches '${pathname}'.`, {}, {
585
- target: pathname,
586
- ...(r.status === 400
587
- ? {
588
- recovery: "Use a log coordinate, prefix, or glob.",
589
- retryable: false,
590
- }
591
- : {}),
592
- });
544
+ const selected = await this.#resolveIds(pathname, ctx);
545
+ if (selected.status === 204)
546
+ return { result: { status: 204, matched: 0 }, plan: null };
547
+ if (selected.status !== 200) {
548
+ return {
549
+ result: Results.failure("scheme:log", selected.status === 404 ? "entry-not-found" : "curation-failed", selected.status, selected.error ?? `No log entry matches '${pathname}'.`, {}, {
550
+ target: pathname,
551
+ ...(selected.status === 400
552
+ ? {
553
+ recovery: "Use a log coordinate, prefix, or glob.",
554
+ retryable: false,
555
+ }
556
+ : {}),
557
+ }),
558
+ plan: null,
559
+ };
593
560
  }
594
- return this.#applyExpanded(r.ids, expanded, signal, ctx);
561
+ return planned(selected.ids);
595
562
  }
596
563
  // KILL shares OPEN/FOLD's address resolution, deletes instead of flipping visibility,
597
564
  // and carries no positional scope. {§model-entry-log-curation} {§log-curation-set-selection}