@plurnk/plurnk-service 1.3.3 → 1.3.4

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 (136) hide show
  1. package/.env.defaults +16 -3
  2. package/INSTALL.md +7 -3
  3. package/SPEC.md +35 -19
  4. package/dist/build-info.json +1 -1
  5. package/dist/content/edit-receipt.d.ts +35 -0
  6. package/dist/content/edit-receipt.d.ts.map +1 -0
  7. package/dist/content/edit-receipt.js +158 -0
  8. package/dist/content/edit-receipt.js.map +1 -0
  9. package/dist/content/index.d.ts +2 -0
  10. package/dist/content/index.d.ts.map +1 -1
  11. package/dist/content/index.js +1 -0
  12. package/dist/content/index.js.map +1 -1
  13. package/dist/content/line-marker.d.ts +3 -1
  14. package/dist/content/line-marker.d.ts.map +1 -1
  15. package/dist/content/line-marker.js +2 -0
  16. package/dist/content/line-marker.js.map +1 -1
  17. package/dist/core/ChannelWrite.d.ts.map +1 -1
  18. package/dist/core/ChannelWrite.js.map +1 -1
  19. package/dist/core/Db.d.ts +2 -13
  20. package/dist/core/Db.d.ts.map +1 -1
  21. package/dist/core/Db.js +0 -5
  22. package/dist/core/Db.js.map +1 -1
  23. package/dist/core/Dispatcher.d.ts +2 -1
  24. package/dist/core/Dispatcher.d.ts.map +1 -1
  25. package/dist/core/Dispatcher.js +151 -26
  26. package/dist/core/Dispatcher.js.map +1 -1
  27. package/dist/core/Engine.d.ts.map +1 -1
  28. package/dist/core/Engine.js +26 -27
  29. package/dist/core/Engine.js.map +1 -1
  30. package/dist/core/Engine.sql +25 -0
  31. package/dist/core/LoopLifecycle.d.ts.map +1 -1
  32. package/dist/core/LoopLifecycle.js.map +1 -1
  33. package/dist/core/Owner.d.ts.map +1 -1
  34. package/dist/core/Owner.js.map +1 -1
  35. package/dist/core/PacketBuilder.d.ts.map +1 -1
  36. package/dist/core/PacketBuilder.js.map +1 -1
  37. package/dist/core/ProposalLifecycle.d.ts +1 -0
  38. package/dist/core/ProposalLifecycle.d.ts.map +1 -1
  39. package/dist/core/ProposalLifecycle.js +8 -4
  40. package/dist/core/ProposalLifecycle.js.map +1 -1
  41. package/dist/core/ProviderInstantiate.d.ts.map +1 -1
  42. package/dist/core/ProviderInstantiate.js +10 -22
  43. package/dist/core/ProviderInstantiate.js.map +1 -1
  44. package/dist/core/TelemetryChannel.d.ts.map +1 -1
  45. package/dist/core/TelemetryChannel.js.map +1 -1
  46. package/dist/core/caps/CapsResolve.d.ts.map +1 -1
  47. package/dist/core/caps/CapsResolve.js.map +1 -1
  48. package/dist/core/caps/DbChannelCaps.d.ts.map +1 -1
  49. package/dist/core/caps/DbChannelCaps.js.map +1 -1
  50. package/dist/core/caps/DbEntryCaps.d.ts.map +1 -1
  51. package/dist/core/caps/DbEntryCaps.js +8 -4
  52. package/dist/core/caps/DbEntryCaps.js.map +1 -1
  53. package/dist/core/caps/DbSubscriptionCaps.d.ts.map +1 -1
  54. package/dist/core/caps/DbSubscriptionCaps.js.map +1 -1
  55. package/dist/core/caps/DbTagCaps.d.ts.map +1 -1
  56. package/dist/core/caps/DbTagCaps.js.map +1 -1
  57. package/dist/core/fork.d.ts.map +1 -1
  58. package/dist/core/fork.js.map +1 -1
  59. package/dist/core/git-iso.d.ts +3 -0
  60. package/dist/core/git-iso.d.ts.map +1 -1
  61. package/dist/core/git-iso.js +35 -4
  62. package/dist/core/git-iso.js.map +1 -1
  63. package/dist/core/git-membership.d.ts.map +1 -1
  64. package/dist/core/git-membership.js.map +1 -1
  65. package/dist/core/git-state.d.ts.map +1 -1
  66. package/dist/core/git-state.js +3 -6
  67. package/dist/core/git-state.js.map +1 -1
  68. package/dist/core/packet-wire.d.ts.map +1 -1
  69. package/dist/core/packet-wire.js +37 -11
  70. package/dist/core/packet-wire.js.map +1 -1
  71. package/dist/core/turn-scheduler.d.ts +3 -0
  72. package/dist/core/turn-scheduler.d.ts.map +1 -0
  73. package/dist/core/turn-scheduler.js +20 -0
  74. package/dist/core/turn-scheduler.js.map +1 -0
  75. package/dist/core/worker-cap.d.ts.map +1 -1
  76. package/dist/core/worker-cap.js.map +1 -1
  77. package/dist/core/workspace-settings.d.ts.map +1 -1
  78. package/dist/core/workspace-settings.js.map +1 -1
  79. package/dist/core/world-state.d.ts.map +1 -1
  80. package/dist/core/world-state.js.map +1 -1
  81. package/dist/digest/Digest.d.ts.map +1 -1
  82. package/dist/digest/Digest.js +4 -6
  83. package/dist/digest/Digest.js.map +1 -1
  84. package/dist/schemes/Exec.d.ts.map +1 -1
  85. package/dist/schemes/Exec.js +5 -1
  86. package/dist/schemes/Exec.js.map +1 -1
  87. package/dist/schemes/File.d.ts +5 -0
  88. package/dist/schemes/File.d.ts.map +1 -1
  89. package/dist/schemes/File.js +51 -10
  90. package/dist/schemes/File.js.map +1 -1
  91. package/dist/schemes/Log.d.ts.map +1 -1
  92. package/dist/schemes/Log.js +9 -1
  93. package/dist/schemes/Log.js.map +1 -1
  94. package/dist/schemes/Prompt.d.ts +1 -1
  95. package/dist/schemes/Prompt.d.ts.map +1 -1
  96. package/dist/schemes/Prompt.js +2 -2
  97. package/dist/schemes/Prompt.js.map +1 -1
  98. package/dist/schemes/Skill.d.ts +1 -0
  99. package/dist/schemes/Skill.d.ts.map +1 -1
  100. package/dist/schemes/Skill.js +4 -1
  101. package/dist/schemes/Skill.js.map +1 -1
  102. package/dist/schemes/Worker.d.ts +3 -0
  103. package/dist/schemes/Worker.d.ts.map +1 -1
  104. package/dist/schemes/Worker.js +11 -2
  105. package/dist/schemes/Worker.js.map +1 -1
  106. package/dist/schemes/_entry-crud.d.ts.map +1 -1
  107. package/dist/schemes/_entry-crud.js.map +1 -1
  108. package/dist/schemes/_entry-find.d.ts +8 -8
  109. package/dist/schemes/_entry-find.d.ts.map +1 -1
  110. package/dist/schemes/_entry-find.js +76 -23
  111. package/dist/schemes/_entry-find.js.map +1 -1
  112. package/dist/schemes/_entry-find.sql +20 -0
  113. package/dist/schemes/_entry-graph.d.ts.map +1 -1
  114. package/dist/schemes/_entry-graph.js.map +1 -1
  115. package/dist/schemes/_entry-manifest.d.ts.map +1 -1
  116. package/dist/schemes/_entry-manifest.js.map +1 -1
  117. package/dist/schemes/_entry-ops.d.ts +3 -2
  118. package/dist/schemes/_entry-ops.d.ts.map +1 -1
  119. package/dist/schemes/_entry-ops.js +41 -13
  120. package/dist/schemes/_entry-ops.js.map +1 -1
  121. package/dist/schemes/_entry-semantic.d.ts +2 -1
  122. package/dist/schemes/_entry-semantic.d.ts.map +1 -1
  123. package/dist/schemes/_entry-semantic.js +15 -4
  124. package/dist/schemes/_entry-semantic.js.map +1 -1
  125. package/dist/schemes/_entry-semantic.sql +3 -0
  126. package/dist/schemes/_entry-send.d.ts.map +1 -1
  127. package/dist/schemes/_entry-send.js.map +1 -1
  128. package/dist/server/Daemon.d.ts.map +1 -1
  129. package/dist/server/Daemon.js.map +1 -1
  130. package/dist/server/clientTurn.d.ts.map +1 -1
  131. package/dist/server/clientTurn.js.map +1 -1
  132. package/dist/server/envelope.d.ts.map +1 -1
  133. package/dist/server/envelope.js.map +1 -1
  134. package/dist/server/logEntry.d.ts.map +1 -1
  135. package/dist/server/logEntry.js.map +1 -1
  136. package/package.json +20 -18
package/.env.defaults CHANGED
@@ -110,11 +110,12 @@ PLURNK_SERVICE_GIT_AUTO=1
110
110
  # PLURNK_SERVICE_GIT_NATIVE — git READ backend. Default (0) = in-process isomorphic-git:
111
111
  # portable, sandbox-safe, no subprocess, hermetic by construction. =1 routes membership +
112
112
  # status to the system git binary — in-process membership measures ~8x native (~130ms at
113
- # 20k files) and the status read ~55x, so a large-repo host with git buys the hot path back.
113
+ # 20k files) and the status read ~55x. Native is also the explicit remedy when
114
+ # isomorphic-git cannot read a repository shape; it requires the git binary.
114
115
  PLURNK_SERVICE_GIT_NATIVE=0
115
116
 
116
117
  # PLURNK_SERVICE_ARRIVAL_PREVIEW_LINES — the arrival law (§arrival-law, #499): content another
117
- # actor pushes at a worker (a prompt, a child's deliverable) rides OPEN only up to this many
118
+ # actor pushes at a worker (a prompt, a child's deliverable, terminal stream output) rides OPEN only up to this many
118
119
  # lines AND 80×N chars (the char cap guards single-line bombs); over, a bounded head + the
119
120
  # address + the full price ride instead. Self-invited content (a worker's own READ/FIND) is
120
121
  # budget-bounded, never previewed. 16 = generous but not dangerous.
@@ -140,6 +141,15 @@ PLURNK_SERVICE_ARRIVAL_PREVIEW_LINES=16
140
141
  # settings.filesItems (#231).
141
142
  PLURNK_SERVICE_FILES_ITEMS=-1
142
143
 
144
+ # Number of full SHA-256 revision characters rendered on each model-facing
145
+ # EDIT receipt. The durable operation result retains all 64 characters; this
146
+ # abbreviation is display correlation only, never identity or comparison.
147
+ PLURNK_SERVICE_EDIT_RECEIPT_REVISION_CHARS=8
148
+
149
+ # Number of neighboring lines (or top-level JSON items) rendered on each side
150
+ # of an EDIT's resulting range. The model can READ for arbitrary final content.
151
+ PLURNK_SERVICE_EDIT_RECEIPT_CONTEXT_LINES=2
152
+
143
153
  # Workspace-tier ceiling on CONCURRENT active runs (a run with a non-terminal loop)
144
154
  # — the fork-bomb / destabilization brake. -1 = no cap (default); only concurrency
145
155
  # is bounded, never lifetime, since workspaces persist for months. A spawn/fork past
@@ -213,8 +223,11 @@ PLURNK_SERVICE_DOCS_EXCLUDE="plurnk,file,exec"
213
223
  # --- Semantic search (~query chunking) ---
214
224
  # Project Semantics tiles each entry into <=window chunks so a large body is fully
215
225
  # searchable, not truncated. ACTIVE only when the installed embedder reports its
216
- # tokenizer; otherwise one whole-entry chunk (today's behavior). Both knobs are read
226
+ # tokenizer; otherwise one whole-entry chunk (today's behavior). The chunk knobs are read
217
227
  # only when chunking is active.
228
+ # Ranked results returned by a markerless `FIND(...):~query:FIND`. A positive
229
+ # integer `<K>` overrides this count; a decimal marker selects by similarity.
230
+ PLURNK_SERVICE_SEMANTIC_TOP_K=10
218
231
  # Chunk budget in TOKENS. EMPTY (the default) = the installed embedder's reported
219
232
  # window — NO model-specific number is assumed, so it scales to whatever embedder you
220
233
  # install. Set a positive value ONLY to cap below the window (e.g. to sweep
package/INSTALL.md CHANGED
@@ -39,13 +39,17 @@ These are relationships *between* flags. Set them as a unit.
39
39
 
40
40
  - **The window partition is exact.** `promptBudget = min(PLURNK_SERVICE_CONTEXT_WINDOW, real window) − REASONING − COMPLETION − SAFETY`; `REASONING + COMPLETION` is the per-call `max_tokens`. Shipped invariant: any window ≥ 77Ki partitions to **exactly 65536** prompt tokens (`78848 − 4096 − 8192 − 1024`). Reserves exceeding the window fail the boot. *(Pinned: `Engine.budget` / `shipped-defaults`.)*
41
41
  - **Reasoning capacity is one number in three places.** `PLURNK_SERVICE_REASONING` (the partition's reserve) **must equal** `PLURNK_PROVIDERS_THINKING_CAPACITY` (the provider's thinking cap) **must equal** the serving box's `--reasoning-budget` launch flag. llama-server ignores per-request numeric budgets, so only the launch flag clamps it; a mismatch makes the reserve fiction. The daemon warns at boot when thinking is on. *(Pinned: `shipped-defaults` asserts the first equality.)*
42
- - **Grammar rails ship on, gated on the provider's claim (#336).** A backend that doesn't enforce grammars drops it cleanly — the daemon boots with a notice, unconstrained on that alias. A backend that CLAIMS enforcement (`constrainsOutput`) is **verified end-to-end at boot** and fails hard if it returns unconstrained output. Daemon-global — not alias-scoped (one backend per daemon). For a known llama-server alias, pin `PLURNK_PROVIDERS_LLAMA_SERVER_<alias>=1` — it transports the grammar deterministically instead of probing `/v1/models` (a probe race once silently disabled the rails). *(Pinned: `grammar-enforcement-verify`.)*
42
+ - **Local GBNF is optional.** The PLURNK language is always parsed normally.
43
+ Local llama-server users may set `PLURNK_PROVIDERS_GBNF_<alias>`; transport
44
+ and enforcement are verified at boot. Cloud and endpoint-managed aliases
45
+ leave it unset. Pin `PLURNK_PROVIDERS_LLAMA_SERVER_<alias>=1` only when a
46
+ llama-server cannot be fingerprinted reliably.
43
47
  - **A think-trained model must think somewhere.** `PLURNK_PROVIDERS_THINKING=off` reroutes a reasoning model's thought into the grammar's legal free zone as prose. Keep it `on` with a capacity; providers auto-clamp thinking on in-band grammar backends, so one setting is right everywhere.
44
48
 
45
49
  ## Profiles (examples, not a decision tree — adapt to the real box)
46
50
 
47
51
  - **Local GPU (llama-server).** `PLURNK_MODEL_local="openai/<name>"`, `OPENAI_BASE_URL=http://127.0.0.1:<port>`, `PLURNK_MODEL=local`, `PLURNK_PROVIDERS_LLAMA_SERVER_local=1`, thinking `on`/`4096` **with the box launched `--reasoning-budget 4096`**. Full rails, exact tokenization.
48
- - **Cloud, bring-your-own-key.** `PLURNK_MODEL_cloud="openrouter/<model>"`, `OPENROUTER_API_KEY=…`, `PLURNK_MODEL=cloud`. No `LLAMA_SERVER` pin (not llama-server); a `response_format`-grammar backend auto-clamps thinking to none.
52
+ - **Cloud, bring-your-own-key.** `PLURNK_MODEL_cloud="openrouter/<model>"`, `OPENROUTER_API_KEY=…`, `PLURNK_MODEL=cloud`. No local GBNF or `LLAMA_SERVER` pin.
49
53
  - **plurnk.ai endpoint.** `PLURNK_MODEL_plurnk="plurnk/plurnk"`, `PLURNK_API_KEY=…`, `PLURNK_MODEL=plurnk`.
50
54
  - **Headless / CI / constrained container.** A CPU-only box should NOT disable semantic search — it should point derivation at a real embedder: `PLURNK_MIMETYPES_EMBED_BASE_URL` (any OpenAI-compatible `/v1/embeddings` — a host GPU turns a CPU-hours corpus grind into seconds). Weak hardware is the target workload, not a reason to shed capability; `PLURNK_SERVICE_EMBED_DISABLE=1` exists for test lanes that deterministically assert non-semantic behavior, nothing else. Consider `PLURNK_SERVICE_MAX_TURNS=<n>` as a cost cap, `PLURNK_SERVICE_GIT_ALLOWED=0` to lock out git in a sandbox.
51
55
 
@@ -62,7 +66,7 @@ Each mirrors a `# --- section ---` in the floor; consult the floor for exact def
62
66
  - **Providers** — `PLURNK_PROVIDERS_THINKING`/`_THINKING_CAPACITY`, `_TEMPERATURE`, `_REPEAT_PENALTY`, `_FETCH_TIMEOUT`, `_RETRY_ATTEMPTS`/`_RETRY_DELAY`, `_PROBE_ATTEMPTS`/`_PROBE_DELAY`, `_GBNF` (grammar variant), `_LLAMA_SERVER_<alias>`, `_CONTEXT_SIZE`, `_GBNF_DEBUG`. Alias-scopable: any knob takes a `_<alias>` suffix that wins over the bare fallback.
63
67
  - **The window partition** — `PLURNK_SERVICE_CONTEXT_WINDOW`/`_REASONING`/`_ASSISTANT`/`_SAFETY` (see Couplings).
64
68
  - **Plugins** — bare `PLURNK_PLUGINS_TRUSTED_ONLY` (0/unset = load all installed; a value = `@plurnk/*` plus an allowlist).
65
- - **Semantic search** — `PLURNK_SERVICE_SEMANTIC_CHUNK_TOKENS`/`_CHUNK_OVERLAP` (service-side chunking), `PLURNK_SERVICE_EMBED_DISABLE` (FTS-only), `PLURNK_MIMETYPES_EMBED_WORKERS` (the embedder's pool — mimetypes-owned).
69
+ - **Semantic search** — `PLURNK_SERVICE_SEMANTIC_TOP_K` (markerless result count), `_SEMANTIC_CHUNK_TOKENS`/`_CHUNK_OVERLAP` (service-side chunking), `PLURNK_SERVICE_EMBED_DISABLE` (FTS-only), `PLURNK_MIMETYPES_EMBED_WORKERS` (the embedder's pool — mimetypes-owned).
66
70
  - **Schemes: http** — `PLURNK_SCHEMES_HTTP_FETCH_TIMEOUT`/`_SALVAGE_MIN_BODY_CHARS`/`_IDLE_TIMEOUT` (required on the HTML render path), optional Playwright/Chromium knobs.
67
71
  - **Execs** — `PLURNK_EXECS_<runtime>=0` disables a runtime; `PLURNK_EXECS_MCP_<server>` bridges an MCP server; `PLURNK_EXECS_SEARCH_SEARXNG_URL` enables web search (unset = search off).
68
72
 
package/SPEC.md CHANGED
@@ -223,7 +223,7 @@ The lifecycle store admits only the guarded transitions shown above: `100 → 10
223
223
  - **A cancelled run is not resurrected by its own torn-down work.** A stream conclusion delivered to a cancelled, idle worker starts no fresh drain: an aborted (499) conclusion is skipped, and a straggler that concluded cleanly surfaces its deliverable as an environment delta (§env-delta), never a revived loop. The cancel was deliberate; only an explicit `loop.run` resumes the worker. {§worker-lifecycle-no-resurrection}
224
224
  - **A stream conclusion always reaches its worker.** When a backgrounded stream concludes, the daemon routes it through the same inject seam as any loop source (§actor-boundary-passive-wake): an active worker folds the conclusion into its next turn; a worker **blocked on a 202 wait** for that stream (§wait-obligation-matrix) **awakens that loop in place** — the blocked loop *is* the continuation, so there is no fresh loop and no summary-as-prompt fiction. The result is never lost: a blocked loop sleeps rather than ending, and the stream's status-transition is the arrival (§actor-boundary-passive-wake) that wakes it; on resume it reads the concluded stream's own state, not a synthetic prompt. {§worker-lifecycle-wake-liveness}
225
225
  - **A child worker concluding wakes a parent blocked on it — the topology join.** `worker://` spawn/fork records `parent_worker_id` (§lifecycle-terms). When a worker's drain exits having **concluded** — no `202`-blocked loop, no open stream — the daemon resumes its parent **in place** if the parent is blocked on the join (`#onDrainExit` → the shared `#wakeParkedWorker`, the same 202→100 resume a stream conclusion uses). So a parent that spawns work and blocks (`SEND[202]`) is woken the moment its child finishes; on resume it reads the child's deliverable from the §worker-scheme-collect delta in its own log — a control edge, **never an injected prompt**. The wake recurses upward via the parent's own drain-exit. A child still running — or itself blocked at 202 — is not *concluded*, so it does not wake the parent (it's still a live thing the subtree holds). This is the structured-concurrency join: streams and child workers are the same kind of "live thing a worker holds," driving premature-terminate (§send-premature-terminate), the wake edge, and the collect delta identically. A worker conclusion is a **bounded, un-loseable** wake: if the conclusion fires while the parent is mid-turn (before its block commits), `#wakeParkedWorker` finds it not-yet-slept and records an **owed wake**, which the drain honors when the parent blocks — so a wait awaiting workers **always returns**, never dead-blocks on a conclude-before-block race. (Only a live exec stream, unbounded absent a timeout, may legitimately hold a wait open.) {§worker-lifecycle-child-wake}
226
- - **An idle run concludes; it does not park.** A loop with nothing in flight no live child, no open stream concludes through `SEND[200]` or an already-drained `SEND[202]` join (§wait-obligation-matrix). There is no held-open `202` loop and no `loop/quiesced` soft signal. A concluded worker is *already* reawakable — it is durable working history (§machine-processes-worker-is-its-log, engine_render_log spans the whole worker), and an addressed arrival (a sibling `irc`, an operator inject, a later `loop.run`) reawakens it as a **new loop** carrying that arrival's real prompt. Only a loop that holds a live obligation *blocks* (§wait-obligation-matrix, the `202+J` join); a subtree with any live thing under it is not idle, and that thing's conclusion reawakens the **same** loop with its prompt intact (§worker-lifecycle-child-wake). {§worker-lifecycle-idle-is-concluded}
226
+ - **An idle run concludes; it does not park.** A loop is idle only when it has neither live obligations nor completed results awaiting their first packet. A live child or stream blocks a `SEND[202]` join; a completed stream, child result, or same-turn retrieval continues directly to the next packet where it is observed. Only after those sets are drained does `SEND[202]` resolve like `SEND[200]`. There is no held-open idle loop and no `loop/quiesced` soft signal. A concluded worker is durable working history and an addressed arrival reawakens it as a new loop. {§worker-lifecycle-idle-is-concluded}
227
227
  - **A loop is never stranded by a drain's exit.** A drain relinquishes its registry slot only after a lock-held re-claim confirms the queue is empty; a loop enqueued during that teardown is either re-claimed by the exiting drain or claimed by a fresh drain that a later inject starts. The relinquish and the start are serialized, so neither the lost-loop hang nor a transient double-drain can occur. {§worker-lifecycle-no-lost-loop}
228
228
  - **Restart is owner-loss reconciliation, not replay.** {§worker-lifecycle-restart-recovery} Before opening client transports, the service holds an exclusive database-adjacent daemon lock; a second live owner fails before touching SQLite, while a dead-PID crash claim is replaced atomically without a timeout lease. Boot preserves accepted `100` loops and restores their drains. A `102` loop belonged to a vanished drain/provider call, so it settles `500` with the interruption on its durable row—never replayed across an unknown effect boundary. Every durable-open subscription likewise belonged to a vanished callable: active channels become errored and its row closes `500`. A `202` continuation survives only while a live child obligation remains; after reconciliation, an unblocked park requeues `202→100` and resumes in place. Child terminalization wakes its parked parent on every outcome, including provider exceptions, cancellation, and restart interruption, recursively through the durable parent edges. These operations are idempotent, so an interrupted recovery safely repeats.
229
229
 
@@ -275,9 +275,20 @@ Deferred (#249): grounding the attribution value in real per-turn value flow rat
275
275
 
276
276
  Model alias parsing (`parseAliasesFromEnv` / `resolveActiveAlias`) lives in [`@plurnk/plurnk-providers`](https://github.com/plurnk/plurnk-providers). {§provider-instantiation-alias-resolution} Dynamic provider instantiation (`instantiateProvider` / `loadActiveProvider`) lives in `src/core/ProviderInstantiate.ts` here — `import()` resolves package specifiers relative to the calling module, so the dynamic-import path stays in the consumer where the `@plurnk/plurnk-providers-<vendor>` packages are installed.
277
277
 
278
- **Grammar enforcement is verified at boot.** When the operator sets `PLURNK_PROVIDERS_GBNF`, `loadActiveProvider` forces a trivial grammar (`root ::= "PLURNK-RAILS-LIVE"`) and confirms the backend returned exactly that — a live end-to-end proof the rails engaged. Anything else **fails hard at boot**: the openai provider only transports the grammar when its probe detects llama-server (grammarStyle `llamacpp`), and any probe hiccup silently falls back to `none` — unconstrained generation with no signal, the whole grammar contract dark, model rambles that read as reasoning failure. The Provider interface exposes no capability to introspect this, so the contract is *verified* rather than trusted; a legible boot refusal beats silent garbage. No-op when GBNF is unset/`0` (unconstrained is then a deliberate mode). **Resolved PER ALIAS** gbnf-per-alias}: `PLURNK_PROVIDERS_GBNF_<alias>` over the bare fallback (scopeEnvToAlias, #353), because only sampling-constraining backends benefit — a cloud model that merely IGNORES an in-band grammar earns a filter-mode divergence event every turn for nothing. So the bare default ships OFF and the GBNF-capable aliases opt IN (in the operator's own .env, never the template); the boot verify still gates each enabled alias on its provider's `constrainsOutput` claim. {§grammar-enforcement-verified-at-boot}
279
-
280
- **Rail truth is the engine's verdict, on every path.** {§rail-truth-engine-verdict} On every completed model turn whose alias configures a grammar, the engine grades the raw emission against that grammar (`@plurnk/gbnf` `validateGbnf`) — delegated enforcement changes who *constrains*, never who *verifies*, and verification that shares a failure domain with the enforcer is not verification (#534: an enforcing relay's own telemetry attests green even when it enforces a stale grammar). The turn's `meta` carries `railsAttached` — `"client"` when the provider claims `constrainsOutput` (transported), else `"delegated"` — and `railsVerdict` (`accept`/`incomplete`/`reject`, or `unverifiable` when the grammar itself fails to parse in the validator, warned once), engine-stamped over the provider passthrough: the one service-authored carve-out in the #252 meta contract. A non-accept verdict mints the `grammar_unenforced` telemetry event engine-side when the provider did not already observe it, so the error channel is path-independent. On a hosted alias the GBNF knob thereby configures the *contract the engine verifies*, independent of transport (settles the #532 knob question: it stays).
278
+ **Optional local GBNF is verified at boot.** {§grammar-enforcement-verified-at-boot}
279
+ The ANTLR grammar always defines and validates the PLURNK language. Separately,
280
+ an operator may configure `PLURNK_PROVIDERS_GBNF_<alias>` for a local
281
+ llama-server. The provider must advertise GBNF transport and satisfy a forcing
282
+ probe (`root ::= "PLURNK-RAILS-LIVE"`) or boot fails. The setting is resolved
283
+ per alias and is unset by default. Configuring it on a cloud or endpoint-managed
284
+ provider is an error, not a request for best-effort filtering.
285
+
286
+ **Local constraint truth is independently observed.** {§rail-truth-engine-verdict}
287
+ For a configured local GBNF, the engine independently validates every completed
288
+ emission and stamps `railsAttached: "client"` plus `railsVerdict`. A non-accept
289
+ verdict emits `grammar_unenforced` telemetry if the provider did not already
290
+ report it. With no local GBNF, core adds no rail state and makes no claim about
291
+ endpoint-owned settings; any endpoint telemetry remains provider metadata.
281
292
 
282
293
  ```
283
294
  PLURNK_MODEL_gemma=openai/macher.gguf
@@ -302,7 +313,7 @@ Author-facing contract: [plurnk-schemes#1](https://github.com/plurnk/plurnk-sche
302
313
  Every op targets a URI; the entry key is `(workspace, owner, scheme, pathname)` ({§entry-identity-no-null}). The URI parses per RFC 3986 (`scheme://[authority]/path`). Handler routing and resource identity are separate:
303
314
 
304
315
  - A **registered** scheme is a plurnk namespace: its authority is a leading path segment, folded into the pathname (`Dispatcher.#extractTarget` → `foldAuthorityIntoPath`). So `known://x`, `known:///x`, and pathname `/x` are the same entry — the authority is never a host, and the two-slash and three-slash forms are not distinct resources. {§scheme-address-namespace-fold}
305
- - A **network resource** keeps its addressed protocol while folding the host into the storage pathname: `https://example.com/page` → `(https, /example.com/page)`. `https` may route through the registered `http` handler, just as `ws` routes through `wss`; that implementation alias never aliases identities. Absolute network URLs are single resources even when their path ends `/` — folder/glob expansion belongs to entry namespaces, never an HTTP origin.
316
+ - A **network resource** keeps its addressed protocol while folding the host into the storage pathname: `https://example.com/page` → `(https, /example.com/page)`. `https` may route through the registered `http` handler, just as `ws` routes through `wss`; that implementation alias never aliases identities. The `SchemeCtx.entries` direct CRUD and standard operation caps both bind to the addressed protocol, regardless of the handler manifest's canonical name. Absolute network URLs are single resources even when their path ends `/` — folder/glob expansion belongs to entry namespaces, never an HTTP origin.
306
317
  - The **`file` class is the workspace filesystem** — a mount namespace with its own resolution and naming law, specified below.
307
318
 
308
319
  **The workspace is a mount namespace; `project_root` is the model's `/`.** {§fs-namespace} Chroot semantics: host paths do not exist inside the jail, and no engine surface ever folds a host-absolute spelling onto a member (the convenience fold was run59's biography — #545). The root is **fixed immutably at workspace creation** (headless is forever); the namespace's mount table changes only through the declared membership overlay (§membership), never by re-rooting. At `project_root = /` the jail is the whole filesystem and every rule below degenerates to identity — the design's proof case, and the common benchmark topology.
@@ -331,9 +342,13 @@ Per author contract (`readEntry` / `writeEntry` / `deleteEntry`). Engine drives
331
342
 
332
343
  ### §op-methods Op methods
333
344
 
334
- Per author contract (`edit`/`read`/`open`/`fold`/`find`/`send`/`exec?`). Engine dispatches by `PlurnkStatement.op`. {§op-methods-op-dispatch} COPY and MOVE are NOT scheme methods — engine orchestrates over CRUD primitives (§copy/§move).
345
+ Per author contract (`editBatch`/`read`/`open`/`fold`/`find`/`send`/`exec?`). Engine dispatches EDIT resource batches through `editBatch`; every other op dispatches by its lowercased `PlurnkStatement.op`. {§op-methods-op-dispatch} COPY and MOVE are NOT scheme methods — engine orchestrates over CRUD primitives (§copy/§move).
346
+
347
+ - **A continuing turn executes in MODE phases.** {§op-mode-phases} A model turn describes intended effects and requested observations; it is not an imperative program whose later statements can consume invisible same-turn results. The engine therefore performs four stable phases: **Mutate** (`EDIT`, `COPY`, `MOVE`, `KILL`, `FOLD`), **Observe** (`FIND`, `READ`, `OPEN`), **Do** (all remaining non-terminal actions, including `EXEC`, `WORK`, `FORK`, and directed `SEND`), then **End** (the terminal `SEND`). `PLAN` remains the turn anchor and is recorded before those phases. Authored order is preserved within each phase. A result still lands in the next packet; phasing makes that result describe settled state instead of an accidental intermediate state.
348
+
349
+ - **Decisive operations settle before the next scheduled operation.** {§op-synchronous} The dispatcher `await`s every decisive operation. The only operations that return tracked work still in flight are the operations whose purpose is to create concurrency: `FORK`, `WORK`, and stream-producing `EXEC` handlers. MODE changes scheduling, not completion semantics. This is why a same-turn `KILL + SEND[200]` concludes (§send-premature-terminate): `KILL` synchronously flips the worker's live loops terminal (`engine_terminate_worker_live_loops`) before the End phase judges the pending set, while the physical scope reap rides `cancelWorker` asynchronously and invisibly.
335
350
 
336
- - **Every op completes before the next begins — except the three concurrency-creators.** {§op-synchronous} Within a turn ops run in sequence, and each is *decisive*: its effect has fully landed by the time the next op or the terminal gate reads it. The dispatch `await`s each, so `KILL`, `EDIT`, `COPY`, `MOVE`, `READ` (including the blocking worker-collect, §worker-scheme-collect), `FIND`, `OPEN`, `FOLD` never leave work "in flight" for a later op to trip over. The ONLY exceptions are the ops whose *job* is to create concurrency and hand it back tracked: **FORK** and **WORK** (spawn a child worker) and **stream-producers** (`EXEC` and friends) return the instant the concurrent thing is launched and registered (the §child-orientation sections), never blocking the turn on its conclusion. This is why a same-turn `KILL + SEND[200]` concludes (§send-premature-terminate): `KILL` synchronously flips the worker's live loops terminal (`engine_terminate_worker_live_loops`) so the gate sees it dead at once, while the physical scope reap rides `cancelWorker` asynchronously and invisibly. The obligation is the model's protection: it must never reason about whether its last decisive op has landed yet.
351
+ - **Same-resource EDITs are one mutation.** {§edit-batch} Every EDIT targeting the same canonical resource and channel in one turn applies to the resource's one pre-turn snapshot. The scheme validates the complete batch before writing, applies disjoint ranges from the highest original coordinate downward, and commits its one resulting revision atomically; reversing the statements cannot change that revision. A failing statement rejects that resource batch without a partial write; independent resource batches remain independent. Whole-resource replacement or creation cannot coexist with another EDIT in the same batch, selected ranges may not overlap, and a boundary insertion may occur at most once at each boundary. Prepend (`<0>`), append (`<-1>`), and fractional insertions name distinct snapshot boundaries and compose with non-overlapping ranges; two insertions naming the same boundary fail rather than acquiring an authored-order meaning. Proposal-gated schemes expose one proposal for the resource batch and accept all or none. The public scheme contract is batch-shaped: a scheme must never emulate this guarantee by applying individual EDITs sequentially.
337
352
 
338
353
  ### §orchestration Cross-scheme orchestration
339
354
 
@@ -364,7 +379,7 @@ Directed SEND (non-null path) routes to scheme's `send`. Status = intent:
364
379
 
365
380
  - **Log speaks the universal query contract** {§log-uniform-query} — `FIND(log://…)` works like every scheme's FIND (the jumbo model ran the canon-documented `FIND(log:///**):#…#` six times into a bare 501 — log was the one scheme outside the universal paradigm, by accident not decision). Candidates are the worker's rows scoped by the coordinate hierarchy ({§log-coordinate-hierarchy}); each is projected EXACTLY as READ shows it (the rx unwrap — FIND must match what the model can retrieve); the shared `Matcher.matchCandidates` ({§find-source-agnostic}) applies any content dialect; results are catalog-shaped items keyed `log:///loop/turn/seq/OP` with `{mimetype, tokens, lines}`, and the fan-out retargets per-row READs so `READ(log://…):matcher` composes identically to entries (#286). A tag signal filters candidates by the model's own region tags ({§log-region-tagging}); log rows carry no symbol channels (`@graph` 501, an honest absence). `~semantic` is an HONEST 501 until log rows receive the same exhaustive vector coverage as entries; storage stays the event stream (the Q3/Option-B decision: uniform QUERY contract, honest distinct storage).
366
381
  - **The content matcher is source-agnostic** {§find-source-agnostic} — `Matcher.matchCandidates(body, candidates, mimetypes)` applies a content matcher (regex/jsonpath/xpath/glob) to candidates from ANY source, keyed by the caller's own identity (a pathname for entries, a `loop/turn/seq` coordinate for log). The matcher never cares what table the content came from, so FIND/READ with every content dialect works uniformly across schemes BY CONSTRUCTION — `EntryFind` and `Log.find` run the ONE shared primitive rather than re-implementing per scheme. This is the query-layer half of the log-uniformity decision (Q3, Option B): log stays its own event stream, but its rows are candidates the shared matcher covers like any entry's content.
367
- - **Line-oriented matching is the tent pole; the selection signal is additive** {§matcher-selection-signal} — a pattern match returns the SOURCE LINE containing it, with its line number (`42:I bought Alice some flowers`, never `1:Alice`): the line is the universal coordinate the whole op-algebra composes on (FIND spans → READ delivers → EDIT mutates), and extraction dialects admit NO exception (owner ruling a bare value has no coordinate and silently breaks the composition). The degenerate case a MINIFIED single-line document, where "the line" is the whole file — is answered with SIGNAL on the FIND row, not a semantics change: a matcher READ's fan-out WRITES its internal FIND as a selection-summary row (sequence-first, exactly as if the model had FINDed then READ), whose rx carries one result item per HIT with `matchSpan` and `matchPath` (the hit's canonical dialect coordinate, e.g. `$['users'][0]['name']`, when the dialect provides one). Deliveries stay span-DEDUPED (#286 N hits on one source line deliver that line once, no identical-row noise); the summary row is what tells the model its query hit N times and WHERE (run30: two hits indistinguishable from failure; 17 retries, 508). The teaching half is grammar's canon (grammar#56).
382
+ - **Matching carries provenance and navigation coordinates** {§matcher-selection-signal} — a pattern match returns the SOURCE LINE containing it, with its line number (`42:I bought Alice some flowers`, never `1:Alice`), and its FIND item carries both that source footprint and the corresponding rows accepted by scoped READ. `matchSpan` is `{lineStart,lineEnd,rowStart,rowEnd}`; rows equal lines for line-navigable text and identify top-level readable items for structured content. `matchPath` remains the hit's canonical dialect coordinate (for example `$['users'][0]['name']`) when available. A matcher READ writes its internal FIND as a selection-summary row before its span-deduplicated deliveries, so minified or otherwise shared-line hits remain distinguishable without changing the returned source content. The teaching half is grammar's canon (grammar#56).
368
383
 
369
384
  `SEND[410](path[#fragment])` also deletes the target entry/channel — an implemented side-effect, NOT taught to the model and with no live/demo surface. The model-facing delete idiom is KILL (§move).
370
385
 
@@ -642,9 +657,10 @@ AST: `{ op: "FIND", target (scope), body: MatcherBody | null (predicate), signal
642
657
 
643
658
  - Filters entries within scope. A **bare** path is the exact entry; an explicit **glob** expands to a scope; `#regex#` filters by pathname. A trailing slash is a folder scope only for a scheme whose manifest declares `folderScopes: true`; otherwise it is ordinary resource syntax and READ dispatches it directly. This is an explicit plugin contract, never inferred from URL punctuation: new schemes cannot accidentally turn a root resource into unbounded fan-out. For declared folder schemes the same target contract governs FIND and READ — bare = the entry, folder/glob = a scope (#286). {§find-scope-prefix-filter}
644
659
  - `body` matcher operates on entry content (glob/regex/jsonpath/xpath), per grammar plurnk.md §"Body matcher dispatch"; the path-glob lives in the (target), not the body. {§find-glob-filter-on-content}
660
+ - Every matcher operates only over the candidate set selected by `(target)` and `[tags]`; relation matchers do not bypass that selection. Semantic ranking is exhaustive within that candidate set, then applies its result policy—never rank the wider corpus and discard out-of-scope hits afterward, which changes top-K meaning and leaks entries across an exact target. A semantic matcher with no `<scope>` returns the configured `PLURNK_SERVICE_SEMANTIC_TOP_K` highest-ranked results. An integer scope overrides that count; a leading decimal is a minimum cosine-similarity threshold, optionally followed by a result cap. The ordinary FIND render budget remains independent of semantic ranking. {§find-semantic-default-top-k}
645
661
  - `signal` is a tag filter; entries match if they have ALL listed tags. {§find-tag-filter-and-semantics}
646
662
  - Workspace + scheme scoped — no cross-workspace/cross-scheme leakage. {§find-scoped-isolation}
647
- - Returns `FindResult { status, content, mimetype, results: MatchItem[], matches, pathnames }`. The matcher sets the unit (#286). A **body-less** FIND is the **catalog**: one item per *entry* — `{ path, seconds?, tags?, channels: { <uri>: { mimetype, tokens, lines } } }` (the addressable path, per-channel `{mimetype, tokens, lines}` keyed by URI — default channel → the bare path, non-default → `path#channel` — plus `tags` and a live `seconds` stream age), the manifest's per-scheme slice. A **matcher** FIND resolves to one item per *match*: the entry's catalog row plus the `matchSpan` `{lineStart, lineEnd}` it hit. **A file with N matches yields N items** — the same row repeated, one span each; there is no `matchLines` array. The unit is uniform across every dialect — glob/regex/jsonpath/xpath select line spans, `~`semantic the ranked chunk's span, `@`graph the matched symbol's span — all `(file, span)`, all real content lines (the old "the extent of ~semantic/@graph is not a content line" carve-out was false: a chunk span and a symbol span are line ranges). Order is match order (rank for `~`semantic, source order otherwise); a miss contributes nothing; identical spans dedup. `content` is the items as a JSON array (`application/json`). {§find-result-catalog-rows} **Over the render budget, FIND returns a count, not contents** (#418, `PLURNK_SERVICE_FIND_MAX_MATCHES`): a repo-scale `FIND(**)` over a 19k-entry workspace can't enumerate — materializing every match overflows the window, and a clean grind must not be a crash-and-recover. When the match set exceeds the budget the result carries `overflow: N` and its `content` states the fact (`"N entries match, exceeding the render budget — not enumerated"`), `text/markdown` not the JSON array; its enumerated `results`, `matches`, and `pathnames` arrays are EMPTY so no caller can perform hidden work from content the model was denied, while `overflow` and `itemsTokenTotal` report the full count and aggregate weight. INDEPENDENT of window size — even a 256k window should not render a whole repo's catalog into one turn. `0`/unset = no gate (small workspaces enumerate as before). {§find-count-not-contents}
663
+ - Returns `FindResult { status, content, mimetype, results: MatchItem[], matches, pathnames }`. The matcher sets the unit (#286). A **body-less** FIND is the **catalog**: one item per *entry* — `{ path, seconds?, tags?, channels: { <uri>: { mimetype, tokens, lines } } }` (the addressable path, per-channel `{mimetype, tokens, lines}` keyed by URI — default channel → the bare path, non-default → `path#channel` — plus `tags` and a live `seconds` stream age), the manifest's per-scheme slice. A **matcher** FIND resolves to one item per *match*: the entry's catalog row plus the `matchSpan` `{lineStart,lineEnd,rowStart,rowEnd}` it hit. The line pair records source provenance; the row pair is directly reusable as scoped READ input. **A file with N matches yields N items** — the same row repeated, one span each; there is no `matchLines` array. The unit is uniform across every dialect — glob/regex/jsonpath/xpath select source spans, `~`semantic the ranked chunk's span, `@`graph the matched symbol's span — and the mimetype handler maps each to readable rows. Order is match order (rank for `~`semantic, source order otherwise); a miss contributes nothing; identical source spans dedup. `content` is the items as a JSON array (`application/json`). {§find-result-catalog-rows} **Over the render budget, FIND returns a count, not contents** (#418, `PLURNK_SERVICE_FIND_MAX_MATCHES`): a repo-scale `FIND(**)` over a 19k-entry workspace can't enumerate — materializing every match overflows the window, and a clean grind must not be a crash-and-recover. When the match set exceeds the budget the result carries `overflow: N` and its `content` states the fact (`"N entries match, exceeding the render budget — not enumerated"`), `text/markdown` not the JSON array; its enumerated `results`, `matches`, and `pathnames` arrays are EMPTY so no caller can perform hidden work from content the model was denied, while `overflow` and `itemsTokenTotal` report the full count and aggregate weight. INDEPENDENT of window size — even a 256k window should not render a whole repo's catalog into one turn. `0`/unset = no gate (small workspaces enumerate as before). {§find-count-not-contents}
648
664
 
649
665
  ### §send SEND
650
666
 
@@ -670,7 +686,7 @@ The engine's failure terminals — **500** (strike threshold) and **508** (cycle
670
686
  **Three engine error states verify the claim.** None is a status code the model learns; all are engine machinery (§engine-rails), pushed to the model as a steering hint on the next packet and **never** as the strike itself (the model sees errors that happened, never the engine's accounting — the gamification policy, §engine-rails). Each strikes (`turnErrors`) and lets the loop continue so the model can correct; a model that ignores the hint and keeps offending spins out to the engine's 500, seeing only the repeated hint, never the count. (All live at `Engine.runLoop`'s turn close.)
671
687
 
672
688
  - **Idle turn** {§send-idle-turn} — a continuing turn (102) whose ops are only PLAN/SEND — no work op. The model continued with nothing to do. The steer, verbatim: *"If your work is done, conclude with 200. If you're waiting on a child or stream you spawned, SEND[202] to block on it — a 202 with nothing to wait on simply concludes."*
673
- - **Premature terminate — the pending set** {§send-premature-terminate} — `SEND[200]` terminates, gated by ONE rule: *nothing pending may be silently discarded*. pending = open streams/spawns (§subscriptions) **live child workers** (a child is live by its **latest loop** — the same definition §child-orientation renders, so the gate and the section the model reads never disagree) ∪ **this turn's retrievals** (READ/FIND/OPEN rows, whose results fold back next packet — a [200] over them discards answers the model asked for) ∪ **children terminated during this turn's generation** (`terminated_at` past the current turn's timestamp no longer *live*, but their §worker-scheme collect deltas are queued for the NEXT build and unseen; the 1ms fan-out race where workers outrun the parent's own emission {§send-undelivered-child-term}). The set is judged **at the terminal's own dispatch, post-batch**: the emission's earlier ops have executed, so a same-turn `KILL + [200]` repairs in ONE turn, and a same-turn `WORK + [200]` is caught (the spawn is live by the time the SEND lands). A refusal is **409** with one steer naming the pending kinds — *"KILL what you no longer need; SEND[102] (or [102]<seconds>) to receive the rest; then conclude"* — the row records the attempt faithfully (never rewritten, never erased) and the loop stays a continue. The strike is **kind-specific** (owner ruling): a refusal holding open streams or live children strikes (§grinder-strike-coupling — discarding live work stays serious); a **retrievals-only** refusal teaches WITHOUT striking — atomic-turn-pretrained models pair fetch-and-answer by habit, each refusal is individually correct, and maxTurns bounds the walk; striking executed visibly-converging behavior (the jumbo/admins specimens: three correct refusals → 500 mid-adaptation). Its steer (*"Last turn both performed retrieval operations and attempted to terminate. Retrieval operations force an additional turn to receive results for review and reaction. To conclude, only use PLAN and SEND[200] operations."* — owner wording, #384/run48) carries three lessons the specimen requiems taught: it narrates the refusal as HISTORY (weak models read log rows in the third person and never take "you"-steers as self-addressed — run48's model called our 409 "strange… Anyway" and proceeded), it states the mechanism as law (retrievals FORCE an additional turn — physics, not blame), and it prescribes the concluding emission's legal SHAPE (PLAN + SEND[200] only), leaving no room for the groundedness-trained justify-READ that re-armed the gate four times around a correct answer. **Failures gate the terminal identically** {§send-200-failed-ops} — a turn that carried **failed operations** (the model's own op results at status ≥ 400, or this emission's parse errors) cannot conclude `200`: those errors land in the log **next** packet, so a same-turn `[200]` concludes blind past a failure the model never weighed — refused **409** (*"weigh them, then conclude"*), the row faithful, the loop a continue; next turn, the failures in-log and seen, `[200]` stands (the gate judges only the current turn, so it never re-arms on its own refusal). `SEND[499]` abandons regardless — discard by stated intent, the one legitimate discard; declaring failure IS weighing it. Waiting is its own signal — `SEND[202]<T,P>` (§wait-obligation-matrix): a **bounded** block on the loop's live obligations, `loops.status = 202` internally, woken by any arrival (stream/child conclusion, sibling irc, operator inject) or its `<T>` deadline. A `202` with **no** obligation resolves like `200` — a wait on nothing is already satisfied — and `<-1>+∅` (an unbounded wait on nothing) self-resolves rather than hang the agent. A child that **terminated during this turn's generation** is not ∅: its deliverable is on the doorstep (queued for the next build), so the wait CONTINUES like a retrieval (R semantics — parking would hang, the wake edges having already fired into an unparked worker; §send-undelivered-child-term). The groundless park is therefore never *refused*, it is *complete*: an idle worker with nothing in flight **concludes** (§worker-lifecycle-idle-is-concluded), reawakened later by a new prompt as a new loop. The park deadline is engine-memory consumed by the daemon's drain park-exit (a daemon restart drops pending deadlines; arrivals still wake).
689
+ - **Premature terminate — the pending set** {§send-premature-terminate} — `SEND[200]` is gated by one rule: *nothing pending may be silently discarded*. Pending work has two states: **live obligations** (open streams/spawns and live child workers) and **completed-but-unobserved results** (same-turn READ/FIND/OPEN results, terminal stream output without a terminal foisted READ, and child results queued for the next packet). Completion is not delivery; a result becomes observed only after crossing a packet boundary. The set is judged at the terminal's own dispatch, after earlier operations in the emission. `[200]` over any member is refused 409 and the loop continues; `[499]` deliberately abandons regardless. `SEND[202]` parks only on live obligations. If work has completed but is unobserved, it continues directly to the next packet because the wake edge has already fired; only a genuinely empty set resolves immediately like `[200]`. {§send-undelivered-child-term}
674
690
  - **SEND[300] is an operator question — a PROPOSAL, the stop-the-world system file edits and MCP auths ride** (owner ruling, #346). Enablement cascades: `PLURNK_QUESTIONS=0` is a servicewide ceiling; otherwise the client affirmatively requests per workspace (`settings.questions: true` at workspace.create), which ALSO injects the questions.md teaching — capability and teaching gate as one. Enabled: the `;`-delimited body parses leniently (first segment the question, the rest choices; zero choices = an open question — never malformed), and the ask raises a proposal: dispatch stops the world, `loop/proposal` carries `{question, choices}` in attrs, and the client's `loop.resolve {decision:"accept", body}` delivers the ANSWER — written into the ask's own model-facing rx (`{"status":200,"body":…}`), read next packet. Reject/timeout resolve through the standard §proposal semantics; the turn records a continue either way (never a 300 terminal), and the loop simply proceeds. Loop auto never auto-answers a question — it exists precisely to stop the world for a human, and the workspace opted in. Disabled: refused 409 with a self-decide steer, never a park into the void. {§send-300-choices}
675
691
 
676
692
  ### §exec EXEC
@@ -693,7 +709,7 @@ Engine routes unconditionally to `exec` scheme (the `(target)` slot is a local p
693
709
 
694
710
  A `read` runtime (observes external state, e.g. search) or `pure` runtime (no observable effect, e.g. `:memory:` sqlite) is side-effect-free → **auto-run**: no proposal, no human gate, no notification. It skips the gate a host command faces, but it does NOT resolve in-band — like every exec it backgrounds and streams, its output reaching the model through the environment-observation injector (a foisted READ of the stream's new bytes each turn, §exec-stream), never a same-turn receipt. {§exec-readpure-ungated}
695
711
 
696
- **Stream surfacing.** An exec's output is *observed, not fetched*. Each turn the environment-observation injector — the same machine §env-delta rides — reads each of the worker's open channels from a per-channel byte cursor and foists the new bytes as an `origin=plurnk` READ at `<runtime>:///<coord>#<channel>`, then advances the cursor each delta carries the `startLine` that cursor implies, so a stream spanning turns numbers into one continuous sequence (lines 1–k, then k+1–m), not a fresh `1:` each turn. The delta is **folded** while the channel streams and auto-**opened** on the terminal one (the channel closed): a model ignores a chatty background stream but always SEES a finished one. It never types these READs — it consumes them. The EXEC row itself renders the *command* it ran, `:::`-fenced and line-numbered per §render-rule so the model can line-reference its own code — the input, distinct from the stream above (the output). This is exec as an instance of one ambient machine, env-delta as another (sibling edits, timestamp cursor, always folded). {§exec-stream}
712
+ **Stream surfacing.** An exec's output is *observed, not fetched*. Each turn the environment-observation injector reads each owned channel from its byte cursor and foists the new bytes as an `origin=plurnk` READ at `<runtime>:///<coord>#<channel>`. Streaming deltas are folded; the terminal delta is born OPEN. Because this is pushed content, the OPEN body obeys `PLURNK_SERVICE_ARRIVAL_PREVIEW_LINES`; a cut names the full stream address for an explicit READ. A stream that closes before a same-turn wait remains pending until this terminal READ crosses the next packet boundary. The EXEC row separately records the command. {§exec-stream}
697
713
 
698
714
  `SEND[499](exec:///<loop>/<turn>/<seq>)` cancels the in-flight subprocess via the subscription registry's stored `AbortController` — the coordinate addresses the spawn (`exec://` is the process-control face); the `<runtime>://` output entry delegates the same KILL to the one `Exec` handler that owns the abort state (§stream-control).
699
715
 
@@ -903,7 +919,7 @@ Model selection: separate alias cascade in `ProviderRegistry` (§provider-instan
903
919
  | `PLURNK_SERVICE_MD_<ALIAS>` | (unset) | Operator reference doc: materializes `<path>` as `plurnk:///<ALIAS>.md`, auto-READ into every model worker's turn 0 (§actor-boundary). `~` expands to home. |
904
920
  | `PLURNK_SERVICE_FILES_ITEMS` | `-1` | Turn-0 catalog preview, one `FIND(scheme:///**)` per scheme. Memory/scratch/docs always full; the first-`N` cap applies **only** to the `file` list. `-1` = all full; positive `N` = file list first-N (memory still full); `0` / unset = off (§actor-boundary-catalog-preview). |
905
921
  | `PLURNK_SERVICE_PROPOSAL_TIMEOUT_MS` | (empty — waits indefinitely) | ms wait for a proposed entry (status=202) to be resolved before timing out. |
906
- | `PLURNK_PROVIDERS_REASONING` + `_BUDGET` | `adaptive` / (unset) | The activation/budget split (a numeric budget silently flipping template flags was secret flag-setting). `off | adaptive | on`; budget (tokens) REQUIRED when on. A reasoning-trained model MUST reason — off reroutes its thought into the grammar's legal free zone as prose. One setting is right everywhere: providers clamp reasoning to none on in-band (response_format) grammar calls themselves, so the channel-topology parallax needs no per-backend override. F7 coupling: llama-server honors only the box's `--reasoning-budget` launch flag (must equal the budget; boot-warned). |
922
+ | `PLURNK_PROVIDERS_REASONING` + `_BUDGET` | `adaptive` / (unset) | The activation/budget split (a numeric budget silently flipping template flags was secret flag-setting). `off | adaptive | on`; budget (tokens) REQUIRED when on. F7 coupling: llama-server honors only the box's `--reasoning-budget` launch flag (must equal the budget; boot-warned). |
907
923
  | `PLURNK_PROVIDERS_FETCH_TIMEOUT` | `600000` | Service-wide ms ceiling on any outbound request (providers, future http schemes). Module-specific overrides are allowed below the ceiling. |
908
924
 
909
925
  Every knob listed is enforced — the engine reads and acts on it; `.env.defaults` is the authoritative default (reader-declares, §operator-config-env-defaults).
@@ -912,7 +928,7 @@ Every knob listed is enforced — the engine reads and acts on it; `.env.default
912
928
  - **Ceiling** (most-restrictive-wins) — an operator-set hard bound nothing downstream may exceed: not a lower-precedence file, not a per-workspace constraint, not a per-call RPC arg. `PLURNK_SERVICE_GIT_ALLOWED` (`=0` flatly denies git service-wide, §membership), `PLURNK_SERVICE_MAX_COMMANDS`, `PLURNK_SERVICE_MAX_STRIKES`, `PLURNK_PROVIDERS_FETCH_TIMEOUT` (module overrides allowed only *below* it), and `PLURNK_SERVICE_MAX_TURNS` (`-1` ships it off; a positive value caps the per-call request). The sandbox/cost guarantee: the operator caps it; no client widens it.
913
929
  - **Default** (explicit-wins) — a fallback the most-specific setter replaces freely: `PLURNK_MODEL` (a `loop.run({alias})` overrides it), `PLURNK_SERVICE_REQUIREMENTS` (the per-call requirements default), and the config-time vars (`HOST` / `PORT` / `DB_PATH`).
914
930
 
915
- **The shipped `.env.defaults` is itself under test** (no active `PLURNK_SERVICE_MD_*` doc alias — the policy is a SECTION, a doc default double-injects it; no active `PLURNK_MODEL`; a per-alias `PLURNK_PROVIDERS_GBNF` (bare OFF, the three GBNF-capable aliases opting in); the policy renders in exactly one packet section): every other tier runs the test cascade, so shipped-default regressions are invisible to it by construction. {§operator-config-shipped-defaults} Its companion **flag-parity** check binds code and template both ways: every `PLURNK_SERVICE_*` the service reads has a `.env.defaults` line (a floor, a `--flag`, a legend entry) and every declared `PLURNK_SERVICE_*` is read — so a half-landed rename (a missed file, a script-glob gap) fails a test instead of a user's boot, and a dead knob can't ship. {§operator-config-flag-parity}
931
+ **The shipped `.env.defaults` is itself under test** (no active `PLURNK_SERVICE_MD_*` doc alias — the policy is a SECTION, a doc default double-injects it; no active `PLURNK_MODEL`; no active local GBNF constraint; the policy renders in exactly one packet section): every other tier runs the test cascade, so shipped-default regressions are invisible to it by construction. {§operator-config-shipped-defaults} Its companion **flag-parity** check binds code and template both ways: every `PLURNK_SERVICE_*` the service reads has a `.env.defaults` line (a floor, a `--flag`, a legend entry) and every declared `PLURNK_SERVICE_*` is read — so a half-landed rename (a missed file, a script-glob gap) fails a test instead of a user's boot, and a dead knob can't ship. {§operator-config-flag-parity}
916
932
 
917
933
  Enforcement is per-use-site — no central most-restrictive pass; each ceiling is checked where it bites. `PLURNK_SERVICE_MAX_TURNS` ships **off** (`-1` = no cap; the loop ends via SEND, budget, strikes, or cycle detection) and, when an operator sets a positive value, the per-call request is `min()`-capped against it. {§operator-config-max-turns-ceiling}
918
934
 
@@ -1172,7 +1188,7 @@ Search prefetch and direct HTTP READ materialize the same resource contract: pro
1172
1188
 
1173
1189
  Coverage protects the composition at distinct seams: HTTP unit tests pin fragmentless-body publication and explicit auxiliary selection; integration tests pin search→materialize→FIND/matcher READ and persistence/publication separation; deterministic model demos require a model-originated fragmentless HTTPS READ whose returned body contains facts absent from search snippets; a live positive-control demo requires a model-originated HTTPS READ and substantive answer from a real sanitized page. Live discovery demos remain diagnostic and may expose model judgment failures without weakening these assertions. **The search gates** (#406, owner ruling) are rail-family accounting — in-memory per-loop state cleaned at the same seam as strikes, restart-drop accepted (a post-restart duplicate re-fetches; the TTL makes it cheap): an IDENTICAL duplicate (same runtime + command in one loop) **strikes and serves** — status 409 (the strike rail counts the turn failure) carrying the prior ranked digest re-read live from the original exec entry, no re-fetch, no provenance prose; the per-turn CAP (`PLURNK_SERVICE_SEARCH_MAX_PER_TURN`) is flood control — 429 with a legible steer, nothing served. {§search-gate}
1174
1190
 
1175
- **git is the substrate.** {§membership-git-membership} git-tracked files (`git ls-files` semantics) are members with no explicit overlay — channel-less markers, disk is truth. git absent → no fs-walk (non-git/headless get no substrate membership); `pick` is then the sole source. **Every core git read is in-process by default** {§git-portable-default}: isomorphic-git (exact-pinned), an explicit-`dir` library read — no subprocess, no ambient `GIT_*`, no global/system config — so the read is hermetic **by construction** and identical on a sandboxed or git-less host; linked worktrees and submodule boundaries (gitlinks filtered) behave as native. The untracked scan is a pruning ignore-walk, **differential-gated**: it must reproduce native `ls-files --others --exclude-standard` across a gitignore edge-case corpus (negations, anchors, nested `.gitignore` precedence, `.git/info/exclude`, dir-vs-glob patterns, embedded-repo boundaries) or it does not ship. `PLURNK_SERVICE_GIT_NATIVE=1` routes membership + status to the system git binary {§git-native-flag} — in-process membership measures ~8× native (~130ms at 20k files) and the status read ~55× (statusMatrix hashes the workdir), so a large-repo host with git installed buys the hot path back. **That native arm scrubs ambient `GIT_*`** (#401): a process launched from a git hook inherits `GIT_DIR` — absolute in a worktree checkout — which retargets every child git at the enclosing repo regardless of cwd; project git state binds to the workspace's `project_root`, never to whoever spawned the daemon. One boundary (`git-env.hermeticGitEnv`), threaded through the native arm and every shell-git test fixture. {§membership-git-hermetic}
1191
+ **git is the substrate.** {§membership-git-membership} git-tracked files (`git ls-files` semantics) are members with no explicit overlay — channel-less markers, disk is truth. git absent → no fs-walk (non-git/headless get no substrate membership); `pick` is then the sole source. **Every core git read is in-process by default** {§git-portable-default}: isomorphic-git (exact-pinned), an explicit-`dir` library read — no subprocess, no ambient `GIT_*`, no global/system config — so the read is hermetic by construction and works on a sandboxed or git-less host. Covered repository shapes include ordinary and linked worktrees, submodule boundaries, and the differential gitignore corpus. When isomorphic-git cannot read an actual repository shape, membership and status fail with one core-owned error that preserves the upstream cause and names `PLURNK_SERVICE_GIT_NATIVE=1`; an explicit backend change is reproducible, while an automatic retry would depend on whether a system binary happens to exist. The untracked scan is a pruning ignore-walk, **differential-gated**: it must reproduce native `ls-files --others --exclude-standard` across a gitignore edge-case corpus (negations, anchors, nested `.gitignore` precedence, `.git/info/exclude`, dir-vs-glob patterns, embedded-repo boundaries) or it does not ship. `PLURNK_SERVICE_GIT_NATIVE=1` routes membership + status to the system git binary {§git-native-flag} — in-process membership measures ~8× native (~130ms at 20k files) and the status read ~55× (statusMatrix hashes the workdir), so a large or unsupported repo with git installed can select the native backend. **That native arm scrubs ambient `GIT_*`** (#401): a process launched from a git hook inherits `GIT_DIR` — absolute in a worktree checkout — which retargets every child git at the enclosing repo regardless of cwd; project git state binds to the workspace's `project_root`, never to whoever spawned the daemon. One boundary (`git-env.hermeticGitEnv`), threaded through the native arm and every shell-git test fixture. {§membership-git-hermetic}
1176
1192
 
1177
1193
  **Membership is a declared forest of repos.** {§membership-forest} A workspace is not one git repo but a **forest**: membership is the union, over a workspace-declared set of repos, of each repo's `ls-files` (gitlinks/mode-160000 filtered), each path-prefixed by the repo's path relative to `project_root`. The root need not itself be a repo — a non-git parent of ninety repos resolves to all ninety. A worktree, a submodule, a buried repo are not special cases: each is just another declared repo, resolved `rev-parse --show-toplevel` → `ls-files` in the tree it points at.
1178
1194
  - **Membership-gated edits.** {§membership-edit-membership-gate} EDIT is bounded by membership exactly as READ is. An existing **member**'s baseline is its entry snapshot — the body channel the model READ, not a fresh disk read — so the diff is naive against the view the model saw, never empty (the write-side CAS, §membership-edit-write-cas, prevents the silent overwrite of out-of-band drift). An existing **non-member** is refused (403) *before* any read or write: the model never reads a file it can't see (no leak into the proposal) and never overwrites one (no wiping a gitignored `.env` it never added). A **new path** stays open — proposal→accept adds it to the manifest. Reaching past membership is `EXEC[sh]`'s job, not the file scheme's.
@@ -1256,15 +1272,15 @@ The CAS is the **hard backstop**, at the moment of writing, on every accept path
1256
1272
 
1257
1273
  **Migration path.** Built. The per-worker world-snapshot the architecture forbade (§machine-processes) is **deleted**; its `[§machine-processes-worker-is-its-log]` conformance test is now green. The pull + the `plurnk`-run fs narration replace it.
1258
1274
 
1259
- ### §edit-result-render EDIT log rows render their result, not their input
1275
+ ### §edit-result-render EDIT log rows render a bounded effect receipt
1260
1276
 
1261
1277
  **Question.** An EDIT's log row exists so the model has a record of what it did. Re-emitting the model's *input* statement (the tx heredoc) records the *intent* but not the *outcome* — the model still has to READ the entry back to confirm "did it land, what does it look like now." And a system delta-EDIT (§env-delta) has no input statement at all. What should an EDIT row's body be?
1262
1278
 
1263
- **Decision — the edited area as it looks now.** An EDIT row renders the **resulting span**: the edited region of the entry *after* the write, line-numbered, with a couple of lines of context above and below. The model sees post-edit state inline no confirming READ and the same rendering serves the model's own EDITs and the system delta-EDITs (§env-delta) identically. The entry's object still carries op + target, so "I EDITed X" stays legible; the body says "and here's X now."
1279
+ **Decision — effect, revision, and bounded join context.** A model-authored EDIT row renders compact metadata (`rev`, `extent`, `change`, and `range`) plus bounded numbered context around that edit's resulting join. The durable result retains the full SHA-256 revision; `rev` abbreviates it to `PLURNK_SERVICE_EDIT_RECEIPT_REVISION_CHARS` for display correlation only and is never an identity, lookup key, or compare-and-swap token. Every row in one resource batch carries the same revision and extent but its own requested marker, normalized source/result ranges, removed/inserted counts, and context. The receipt proves what landed without copying an arbitrarily large changed region into the next packet. A deliberate READ in the same turn is scheduled after mutation (§op-mode-phases) and remains the universal way to request arbitrary current content.
1264
1280
 
1265
- **Scope.** The span is computed at edit time the write range and the result are both known then — and stored on the EDIT's `rx`; the render reads it. A large span is bounded like any rendered slice, and FOLD collapses it to the coordinate when the model doesn't need it.
1281
+ **Scope.** The receipt is computed from the one pre-turn snapshot and committed result and stored structurally on the EDIT's `rx`; reviewer-modified proposals recompute it from the content that actually lands. Text resources report lines; structural JSON resources report top-level items. `PLURNK_SERVICE_EDIT_RECEIPT_CONTEXT_LINES` bounds neighboring lines/items independently for each row. Environment-delta EDITs remain factual state-diff events and carry their resulting span (§env-delta); COPY/MOVE likewise retain their resulting span.
1266
1282
 
1267
- **Migration path.** Changes what EDIT rows *show* (input output); the op surface and EDIT's behaviour are unchanged. Tests asserting the input-heredoc render move to the resulting-span render.
1283
+ **Migration path.** Built with MODE scheduling and atomic resource batches; the former unbounded resulting-span confirmation is removed from model-authored EDITs.
1268
1284
 
1269
1285
  ### §proposal-ownership Loop auto and client YOLO
1270
1286
 
@@ -1507,7 +1523,7 @@ Same rule applies across Known, Unknown, Skill, Plurnk, File. Effective mimetype
1507
1523
 
1508
1524
  A log row renders its **result body** for the content-returning ops — `READ@200` (the content it pulled) and `FIND@200` (the catalog rows / matched entries it returned) — under the query's fence, mimetype-driven per the rules above; every other op re-emits its statement. FIND included: the model must see what a find *returned*, not just its echoed query, and the turn-0 foisted `FIND(scheme:///**)` reaches the packet through this branch — without it the catalog preview is invisible. {§render-rule-find-renders-result}
1509
1525
 
1510
- An `EDIT` log row renders its **resulting span** — the edited area as it looks now (`rx.span`), under the target's fence — not the input statement: the log reads "and here's X now," so the model sees its edit's effect. The object still carries op + target; the model's own EDITs and the system delta-EDITs (§env-delta) render identically; an emptied span the object's meta alone (no body). With no span stored, the row falls back to re-emitting the statement (the heredoc the model wrote). {§edit-result-render}
1526
+ An `EDIT` log row renders its bounded **effect receipt** (`rx.receipt`) as row metadata and join context, not its input statement. Proposal-gated file EDITs compute the accepted receipt from what actually lands. Environment-delta EDITs and COPY/MOVE rows render their resulting `rx.span`. {§edit-result-render}
1511
1527
 
1512
1528
  The `N:` prefix is presentation/reference per plurnk.md ("not part of the source"); stripped before any matcher operation on the log entry.
1513
1529
 
@@ -1 +1 @@
1
- {"package":"@plurnk/plurnk-service","version":"1.3.2","revision":"535b20fb8e1980ab2bdcc79ab7b7389942970c97","dirty":false}
1
+ {"package":"@plurnk/plurnk-service","version":"1.3.3","revision":"3352dea7a51575d4a1e7804428397e4d649f5c57","dirty":false}
@@ -0,0 +1,35 @@
1
+ import type { LineMarker } from "@plurnk/plurnk-grammar";
2
+ export interface ReceiptEdit {
3
+ readonly marker: LineMarker;
4
+ readonly body: string;
5
+ }
6
+ interface ReceiptOptions {
7
+ readonly unit?: "lines" | "items";
8
+ }
9
+ export interface EditEffectReceipt {
10
+ readonly requested: string;
11
+ readonly source: string;
12
+ readonly result: string;
13
+ readonly removed: number;
14
+ readonly inserted: number;
15
+ readonly context: string;
16
+ }
17
+ export interface EditBatchReceipt {
18
+ readonly revision: string;
19
+ readonly unit: "lines" | "items";
20
+ readonly before: number;
21
+ readonly after: number;
22
+ readonly effects: readonly EditEffectReceipt[];
23
+ }
24
+ export interface EditReceipt {
25
+ readonly revision: string;
26
+ readonly unit: "lines" | "items";
27
+ readonly before: number;
28
+ readonly after: number;
29
+ readonly effect: EditEffectReceipt;
30
+ }
31
+ export declare const editReceiptUnit: (structuralJson: boolean, original: string, updated: string) => "lines" | "items";
32
+ export declare const projectEditReceipt: (receipt: EditBatchReceipt, index: number) => EditReceipt;
33
+ export declare const editReceipt: (original: string, updated: string, edits: readonly ReceiptEdit[], options?: ReceiptOptions) => EditBatchReceipt;
34
+ export {};
35
+ //# sourceMappingURL=edit-receipt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edit-receipt.d.ts","sourceRoot":"","sources":["../../src/content/edit-receipt.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEzD,MAAM,WAAW,WAAW;IACxB,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACzB;AAED,UAAU,cAAc;IACpB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC;CACrC;AAED,MAAM,WAAW,iBAAiB;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,gBAAgB;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,SAAS,iBAAiB,EAAE,CAAC;CAClD;AAED,MAAM,WAAW,WAAW;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;CACtC;AAED,eAAO,MAAM,eAAe,mBACR,OAAO,YACb,MAAM,WACP,MAAM,KAChB,OAAO,GAAG,OASZ,CAAC;AAEF,eAAO,MAAM,kBAAkB,YAAa,gBAAgB,SAAS,MAAM,KAAG,WAU7E,CAAC;AA4BF,eAAO,MAAM,WAAW,aACV,MAAM,WACP,MAAM,SACR,SAAS,WAAW,EAAE,YACpB,cAAc,KACxB,gBAqGF,CAAC"}
@@ -0,0 +1,158 @@
1
+ import { createHash } from "node:crypto";
2
+ export const editReceiptUnit = (structuralJson, original, updated) => {
3
+ if (!structuralJson)
4
+ return "lines";
5
+ try {
6
+ if (original.length > 0)
7
+ JSON.parse(original);
8
+ if (updated.length > 0)
9
+ JSON.parse(updated);
10
+ return "items";
11
+ }
12
+ catch {
13
+ return "lines";
14
+ }
15
+ };
16
+ export const projectEditReceipt = (receipt, index) => {
17
+ const effect = receipt.effects[index];
18
+ if (effect === undefined)
19
+ throw new Error(`EDIT receipt has no effect at index ${index}`);
20
+ return {
21
+ revision: receipt.revision,
22
+ unit: receipt.unit,
23
+ before: receipt.before,
24
+ after: receipt.after,
25
+ effect,
26
+ };
27
+ };
28
+ const splitLines = (content) => {
29
+ if (content.length === 0)
30
+ return [];
31
+ const lines = content.split("\n");
32
+ if (content.endsWith("\n"))
33
+ lines.pop();
34
+ return lines;
35
+ };
36
+ const markerText = ({ marks }) => `<${marks.join(",")}>`;
37
+ const sourceRange = (marker, total) => {
38
+ const first = marker.marks[0];
39
+ const last = marker.marks[1];
40
+ if (last !== undefined) {
41
+ const start = first === 0 ? 1 : first;
42
+ const end = last === -1 ? total : last;
43
+ return { start, end, removed: Math.max(0, end - start + 1) };
44
+ }
45
+ if (first === 0)
46
+ return { start: 1, end: 0, removed: 0 };
47
+ if (first === -1)
48
+ return { start: total + 1, end: total, removed: 0 };
49
+ if (!Number.isInteger(first)) {
50
+ const start = Math.floor(first) + 1;
51
+ return { start, end: start - 1, removed: 0 };
52
+ }
53
+ return { start: first, end: first, removed: 1 };
54
+ };
55
+ export const editReceipt = (original, updated, edits, options = {}) => {
56
+ const { unit = "lines" } = options;
57
+ const before = splitLines(original);
58
+ const after = splitLines(updated);
59
+ let sourceShape = "scalar";
60
+ const itemCount = (content) => {
61
+ if (content.length === 0)
62
+ return 0;
63
+ const parsed = JSON.parse(content);
64
+ if (Array.isArray(parsed))
65
+ return parsed.length;
66
+ if (parsed !== null && typeof parsed === "object")
67
+ return Object.keys(parsed).length;
68
+ return 1;
69
+ };
70
+ if (unit === "items" && (original.length > 0 || updated.length > 0)) {
71
+ const parsed = JSON.parse(original.length > 0 ? original : updated);
72
+ sourceShape = Array.isArray(parsed) ? "array" : parsed !== null && typeof parsed === "object" ? "object" : "scalar";
73
+ }
74
+ const countBody = (body) => {
75
+ if (unit === "lines")
76
+ return splitLines(body).length;
77
+ if (body.length === 0)
78
+ return 0;
79
+ const parsed = JSON.parse(body);
80
+ if (sourceShape === "array")
81
+ return Array.isArray(parsed) ? parsed.length : 1;
82
+ if (sourceShape === "object") {
83
+ const values = Array.isArray(parsed) ? parsed : [parsed];
84
+ return values.reduce((count, value) => count + (value !== null && typeof value === "object" && !Array.isArray(value) ? Object.keys(value).length : 0), 0);
85
+ }
86
+ return Array.isArray(parsed) ? parsed.length : 1;
87
+ };
88
+ const beforeExtent = unit === "lines" ? before.length : itemCount(original);
89
+ const afterExtent = unit === "lines" ? after.length : itemCount(updated);
90
+ let offset = 0;
91
+ const effectsByIndex = new Array(edits.length);
92
+ edits
93
+ .map((edit, index) => ({ edit, index, source: sourceRange(edit.marker, beforeExtent) }))
94
+ .sort((a, b) => a.source.start - b.source.start)
95
+ .forEach(({ edit, index, source }) => {
96
+ const inserted = countBody(edit.body);
97
+ const resultStart = source.start + offset;
98
+ const resultEnd = inserted === 0 ? resultStart - 1 : resultStart + inserted - 1;
99
+ offset += inserted - source.removed;
100
+ effectsByIndex[index] = {
101
+ marker: markerText(edit.marker),
102
+ source,
103
+ inserted,
104
+ resultStart,
105
+ resultEnd,
106
+ };
107
+ });
108
+ const effects = effectsByIndex.map((effect, index) => {
109
+ if (effect === undefined)
110
+ throw new Error(`EDIT receipt calculation omitted effect ${index}`);
111
+ return effect;
112
+ });
113
+ const joinRadiusRaw = process.env.PLURNK_SERVICE_EDIT_RECEIPT_CONTEXT_LINES;
114
+ const joinRadius = Number(joinRadiusRaw);
115
+ if (!Number.isSafeInteger(joinRadius) || joinRadius < 0) {
116
+ throw new Error(`PLURNK_SERVICE_EDIT_RECEIPT_CONTEXT_LINES must be a non-negative safe integer, got ${JSON.stringify(joinRadiusRaw)}`);
117
+ }
118
+ const contextItems = unit === "lines"
119
+ ? after
120
+ : (() => {
121
+ if (updated.length === 0)
122
+ return [];
123
+ const parsed = JSON.parse(updated);
124
+ if (Array.isArray(parsed))
125
+ return parsed.map((item) => JSON.stringify(item));
126
+ if (parsed !== null && typeof parsed === "object")
127
+ return Object.entries(parsed).map(([key, value]) => JSON.stringify({ [key]: value }));
128
+ return [JSON.stringify(parsed)];
129
+ })();
130
+ const withContext = effects.map((effect) => {
131
+ const visible = new Set();
132
+ const join = Math.min(Math.max(effect.resultStart, 1), Math.max(contextItems.length, 1));
133
+ const end = Math.max(join, effect.resultEnd);
134
+ for (let line = Math.max(1, join - joinRadius); line <= Math.min(contextItems.length, end + joinRadius); line += 1) {
135
+ visible.add(line);
136
+ }
137
+ const contextRows = [...visible].sort((a, b) => a - b).map((line) => `${line}:${contextItems[line - 1]}`);
138
+ const sourceText = effect.source.removed === 0 ? `${effect.source.start}^` : effect.source.start === effect.source.end ? `${effect.source.start}` : `${effect.source.start}-${effect.source.end}`;
139
+ const resultText = effect.resultEnd < effect.resultStart ? `${effect.resultStart}^` : effect.resultStart === effect.resultEnd ? `${effect.resultStart}` : `${effect.resultStart}-${effect.resultEnd}`;
140
+ return {
141
+ requested: effect.marker,
142
+ source: sourceText,
143
+ result: resultText,
144
+ removed: effect.source.removed,
145
+ inserted: effect.inserted,
146
+ context: contextRows.join("\n"),
147
+ };
148
+ });
149
+ const revision = createHash("sha256").update(updated).digest("hex");
150
+ return {
151
+ revision,
152
+ unit,
153
+ before: beforeExtent,
154
+ after: afterExtent,
155
+ effects: withContext,
156
+ };
157
+ };
158
+ //# sourceMappingURL=edit-receipt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edit-receipt.js","sourceRoot":"","sources":["../../src/content/edit-receipt.ts"],"names":[],"mappings":"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"}
@@ -7,4 +7,6 @@ export type { MatchResult } from "./matcher.ts";
7
7
  export { default as ReadResolve } from "./read-resolve.ts";
8
8
  export type { ReadSliceResult } from "./read-resolve.ts";
9
9
  export { editedSpan } from "./edited-span.ts";
10
+ export { editReceipt, editReceiptUnit, projectEditReceipt } from "./edit-receipt.ts";
11
+ export type { EditBatchReceipt, EditEffectReceipt, EditReceipt, ReceiptEdit } from "./edit-receipt.ts";
10
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/content/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEjE,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAC5D,YAAY,EAAE,UAAU,IAAI,cAAc,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEnG,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAE7D,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,cAAc,CAAC;AAClD,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC3D,YAAY,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEzD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/content/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEjE,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAC5D,YAAY,EAAE,UAAU,IAAI,cAAc,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEnG,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAE7D,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,cAAc,CAAC;AAClD,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC3D,YAAY,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEzD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACrF,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC"}
@@ -8,4 +8,5 @@ export { default as PathMimetype } from "./path-mimetype.js";
8
8
  export { default as Matcher } from "./matcher.js";
9
9
  export { default as ReadResolve } from "./read-resolve.js";
10
10
  export { editedSpan } from "./edited-span.js";
11
+ export { editReceipt, editReceiptUnit, projectEditReceipt } from "./edit-receipt.js";
11
12
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/content/index.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,2EAA2E;AAC3E,qEAAqE;AACrE,wEAAwE;AAExE,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEjE,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAG5D,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAE7D,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,cAAc,CAAC;AAGlD,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAG3D,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/content/index.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,2EAA2E;AAC3E,qEAAqE;AACrE,wEAAwE;AAExE,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEjE,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAG5D,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAE7D,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,cAAc,CAAC;AAGlD,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAG3D,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC"}