textus 0.49.0 → 0.51.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 (118) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -0
  3. data/README.md +41 -43
  4. data/SPEC.md +176 -197
  5. data/docs/architecture/README.md +46 -42
  6. data/docs/reference/conventions.md +33 -28
  7. data/lib/textus/boot.rb +58 -47
  8. data/lib/textus/call.rb +1 -1
  9. data/lib/textus/cli/runner.rb +15 -10
  10. data/lib/textus/cli/verb/get.rb +1 -3
  11. data/lib/textus/cli/verb/hook_run.rb +1 -1
  12. data/lib/textus/cli/verb/put.rb +4 -20
  13. data/lib/textus/cli.rb +1 -4
  14. data/lib/textus/dispatcher.rb +1 -3
  15. data/lib/textus/doctor/check/generator_drift.rb +4 -3
  16. data/lib/textus/doctor/check/handler_allowlist.rb +1 -1
  17. data/lib/textus/doctor/check/intake_registration.rb +5 -5
  18. data/lib/textus/doctor/check/rule_ambiguity.rb +3 -3
  19. data/lib/textus/doctor/check/sentinels.rb +2 -2
  20. data/lib/textus/doctor/check/templates.rb +13 -11
  21. data/lib/textus/doctor.rb +0 -2
  22. data/lib/textus/domain/freshness/evaluator.rb +150 -14
  23. data/lib/textus/domain/freshness/verdict.rb +28 -6
  24. data/lib/textus/domain/freshness.rb +4 -33
  25. data/lib/textus/domain/policy/base_guards.rb +1 -1
  26. data/lib/textus/domain/policy/predicates/fresh_within.rb +1 -1
  27. data/lib/textus/domain/policy/publish_target.rb +34 -0
  28. data/lib/textus/domain/policy/retention.rb +29 -0
  29. data/lib/textus/domain/policy/source.rb +79 -0
  30. data/lib/textus/domain/retention/sweep.rb +57 -0
  31. data/lib/textus/domain/retention.rb +11 -0
  32. data/lib/textus/errors.rb +4 -4
  33. data/lib/textus/hooks/builtin.rb +5 -5
  34. data/lib/textus/hooks/catalog.rb +8 -7
  35. data/lib/textus/hooks/context.rb +5 -10
  36. data/lib/textus/init/templates/machine_intake.rb +4 -4
  37. data/lib/textus/init.rb +47 -47
  38. data/lib/textus/key/matching.rb +24 -0
  39. data/lib/textus/maintenance/reconcile.rb +160 -0
  40. data/lib/textus/manifest/capabilities.rb +1 -1
  41. data/lib/textus/manifest/data.rb +2 -2
  42. data/lib/textus/manifest/entry/base.rb +28 -8
  43. data/lib/textus/manifest/entry/nested.rb +3 -4
  44. data/lib/textus/manifest/entry/parser.rb +25 -21
  45. data/lib/textus/manifest/entry/produced.rb +56 -0
  46. data/lib/textus/manifest/entry/publish/subtree_mirror.rb +7 -6
  47. data/lib/textus/manifest/entry/publish/to_paths.rb +62 -11
  48. data/lib/textus/manifest/entry/validators/format_matrix.rb +3 -11
  49. data/lib/textus/manifest/entry/validators/publish.rb +3 -1
  50. data/lib/textus/manifest/entry/validators.rb +0 -1
  51. data/lib/textus/manifest/policy.rb +16 -4
  52. data/lib/textus/manifest/resolver.rb +10 -4
  53. data/lib/textus/manifest/rules.rb +37 -36
  54. data/lib/textus/manifest/schema/keys.rb +98 -0
  55. data/lib/textus/manifest/schema/validator.rb +324 -0
  56. data/lib/textus/manifest/schema/vocabulary.rb +24 -0
  57. data/lib/textus/manifest/schema.rb +27 -247
  58. data/lib/textus/manifest.rb +5 -3
  59. data/lib/textus/mcp/server.rb +9 -2
  60. data/lib/textus/ports/audit_log.rb +6 -0
  61. data/lib/textus/ports/build_lock.rb +6 -0
  62. data/lib/textus/ports/clock.rb +4 -3
  63. data/lib/textus/ports/produce_on_write_subscriber.rb +69 -0
  64. data/lib/textus/ports/publisher.rb +11 -7
  65. data/lib/textus/produce/acquire/handler.rb +29 -0
  66. data/lib/textus/produce/acquire/intake.rb +130 -0
  67. data/lib/textus/produce/acquire/projection.rb +127 -0
  68. data/lib/textus/produce/acquire/serializer/json.rb +31 -0
  69. data/lib/textus/produce/acquire/serializer/text.rb +16 -0
  70. data/lib/textus/produce/acquire/serializer/yaml.rb +31 -0
  71. data/lib/textus/produce/acquire/serializer.rb +17 -0
  72. data/lib/textus/produce/engine.rb +143 -0
  73. data/lib/textus/produce/events.rb +36 -0
  74. data/lib/textus/produce/render.rb +23 -0
  75. data/lib/textus/projection.rb +17 -6
  76. data/lib/textus/read/boot.rb +4 -2
  77. data/lib/textus/read/deps.rb +3 -3
  78. data/lib/textus/read/freshness.rb +63 -29
  79. data/lib/textus/read/get.rb +20 -102
  80. data/lib/textus/read/rdeps.rb +3 -3
  81. data/lib/textus/read/rule_explain.rb +41 -23
  82. data/lib/textus/read/rule_list.rb +25 -8
  83. data/lib/textus/read/validate_all.rb +14 -0
  84. data/lib/textus/role.rb +2 -1
  85. data/lib/textus/schemas.rb +8 -0
  86. data/lib/textus/store.rb +1 -0
  87. data/lib/textus/version.rb +1 -1
  88. data/lib/textus/write/put.rb +1 -1
  89. metadata +23 -30
  90. data/lib/textus/builder/pipeline.rb +0 -88
  91. data/lib/textus/builder/renderer/json.rb +0 -45
  92. data/lib/textus/builder/renderer/markdown.rb +0 -24
  93. data/lib/textus/builder/renderer/text.rb +0 -14
  94. data/lib/textus/builder/renderer/yaml.rb +0 -45
  95. data/lib/textus/builder/renderer.rb +0 -17
  96. data/lib/textus/cli/verb/boot.rb +0 -13
  97. data/lib/textus/cli/verb/build.rb +0 -15
  98. data/lib/textus/doctor/check/fetch_locks.rb +0 -49
  99. data/lib/textus/doctor/check/lifecycle_action_invalid.rb +0 -39
  100. data/lib/textus/domain/freshness/policy.rb +0 -18
  101. data/lib/textus/domain/lifecycle.rb +0 -83
  102. data/lib/textus/domain/outcome.rb +0 -10
  103. data/lib/textus/domain/policy/lifecycle.rb +0 -35
  104. data/lib/textus/domain/staleness/generator_check.rb +0 -109
  105. data/lib/textus/domain/staleness.rb +0 -29
  106. data/lib/textus/maintenance/tend.rb +0 -110
  107. data/lib/textus/manifest/entry/derived.rb +0 -65
  108. data/lib/textus/manifest/entry/intake.rb +0 -31
  109. data/lib/textus/manifest/entry/validators/inject_boot.rb +0 -21
  110. data/lib/textus/mcp/tools.rb +0 -14
  111. data/lib/textus/ports/fetch/detached.rb +0 -52
  112. data/lib/textus/ports/fetch/lock.rb +0 -44
  113. data/lib/textus/write/build.rb +0 -90
  114. data/lib/textus/write/fetch_events.rb +0 -42
  115. data/lib/textus/write/fetch_orchestrator.rb +0 -101
  116. data/lib/textus/write/fetch_worker.rb +0 -127
  117. data/lib/textus/write/intake_fetch.rb +0 -25
  118. data/lib/textus/write/materializer.rb +0 -51
@@ -1,13 +1,13 @@
1
1
  # Textus architecture
2
2
 
3
3
  > **Explanation** · for contributors · **read this first** for orientation before SPEC
4
- > **SSoT for** the Ruby implementation layout (layers, container, ports, read/write/fetch paths) · **reviewed** 2026-06 (v0.43)
4
+ > **SSoT for** the Ruby implementation layout (layers, container, ports, read/write/produce paths) · **reviewed** 2026-06 (v0.45)
5
5
 
6
6
  ```mermaid
7
7
  flowchart TD
8
8
  interface["Interface — CLI verbs · MCP gate (JSON-RPC)"]
9
- application["Application — Call · Container · Dispatcher · RoleScope<br/>read/ · write/ · maintenance/ use cases · envelope IO"]
10
- domain["Domain — Permission · Freshness · Staleness<br/>Policy (Guard · GuardFactory · BaseGuards · Evaluation · Fetch · Matcher · Predicates)"]
9
+ application["Application — Call · Container · Dispatcher · RoleScope<br/>read/ · write/ · maintenance/ · produce/ use cases · envelope IO"]
10
+ domain["Domain — Permission · Freshness<br/>Policy (Guard · GuardFactory · BaseGuards · Evaluation · Fetch · Matcher · Predicates)"]
11
11
  infra["Infrastructure — Store · FileStore · Manifest · Schemas<br/>Ports · Hooks · Entry format strategies"]
12
12
  interface --> application
13
13
  application --> domain
@@ -38,12 +38,11 @@ MCP/Ruby, `view(:cli)` for the operator envelope); declarative `source:`/
38
38
  wrap the single dispatch site (`RoleScope#dispatch_bound`) for stateful verbs;
39
39
  and `cli_default:` declares a CLI default that diverges from the agent default.
40
40
  `CLI::Runner` generates a command per `:cli` contract, dispatching `contract.verb`
41
- by construction. Only verbs with genuine *behavior* — `put` (fetch
42
- orchestration), `get` (UnknownKey + resolver suggestions, CLI-only), `build`
43
- (actor-role resolution + BuildLock), the `fetch`/`fetch_all` workers, and the
44
- `boot`/`doctor` composite reports stay hand-authored, plus commands with no
45
- dispatcher verb (`init`, `hook`, `mcp serve`, `schema diff/init`). Total
46
- reconciliation specs make name/dispatch/facet drift unrepresentable.
41
+ by construction. Only verbs with genuine *behavior* — `put` (entry persistence),
42
+ `get` (UnknownKey + resolver suggestions, CLI-only), and `doctor` (not yet
43
+ generatable) stay hand-authored, plus commands with no dispatcher verb (`init`,
44
+ `hook`, `mcp serve`, `schema diff/init`). `boot` is auto-generated from its
45
+ contract. Total reconciliation specs make name/dispatch/facet drift unrepresentable.
47
46
 
48
47
  **Application**
49
48
 
@@ -55,13 +54,13 @@ Dispatcher (static VERBS table: verb → use-case)
55
54
  RoleScope (Store#as(role) — forwards verb calls)
56
55
 
57
56
  read/{get,list,where,uid,schema_envelope,
58
- deps,rdeps,published,stale,validate_all,boot,doctor,
57
+ deps,rdeps,published,validate_all,boot,doctor,
59
58
  freshness,audit,blame,rule_explain,rule_list,pulse}.rb
60
- write/{put,delete,mv,accept,reject,build,
61
- materializer,intake_fetch,retention_sweep,
62
- fetch_worker,fetch_orchestrator,fetch_all}
63
- maintenance/{migrate,key_mv_prefix,key_delete_prefix,
59
+ write/{put,key_delete,key_mv,accept,reject,propose}.rb
60
+ maintenance/{reconcile,key_mv_prefix,key_delete_prefix,
64
61
  zone_mv,rule_lint}.rb
62
+ produce/{engine,events,render,
63
+ acquire/{intake,handler,projection,serializer}}.rb
65
64
  envelope/io/{reader,writer}.rb (split: parse vs persist)
66
65
  projection.rb
67
66
  ```
@@ -70,8 +69,7 @@ projection.rb
70
69
 
71
70
  ```
72
71
  Permission (write predicate per zone)
73
- Freshness::{Policy,Verdict,Evaluator}
74
- Staleness (Generator/Intake checks)
72
+ Freshness::{Verdict,Evaluator}
75
73
  Action Outcome Sentinel
76
74
  Policy::{Guard,GuardFactory,BaseGuards,Evaluation,Fetch,Matcher,HandlerAllowlist,
77
75
  Predicates::{ZoneWritableBy,SchemaValid,AuthorHeld,TargetIsCanon,EtagMatch,FreshWithin}}
@@ -87,7 +85,7 @@ Storage::FileStore (bytes-only port: read/write/delete/
87
85
  Manifest (Data, Resolver, Policy, Rules)
88
86
  Schemas (eager-load cache)
89
87
  Ports::{AuditLog,AuditSubscriber,Publisher,Clock,
90
- Fetch::Lock,Fetch::Detached,BuildLock}
88
+ BuildLock,ProduceOnWriteSubscriber,SentinelStore}
91
89
  Hooks::{EventBus,RpcRegistry,Loader,Context,FireReport,
92
90
  Signature,Builtin,ErrorLog}
93
91
  Entry::{Markdown,Json,Yaml,Text} (format strategies)
@@ -125,7 +123,7 @@ reached through `Dispatcher::VERBS`, not a special method on `RoleScope`.
125
123
 
126
124
  Two collaborators live outside the dispatcher because they're composed by other use cases, not invoked as verbs:
127
125
 
128
- - `Write::FetchOrchestrator` — composes `FetchWorker` with the freshness `Action` returned by `Domain::Freshness`.
126
+ - `Produce::Engine` — runs `reconcile`'s produce pipeline; composes `Acquire::Intake` (external pull via handler) with `Produce::Render` (template-driven publish) per entry. `AsyncRunner` (nested in `Engine`) enqueues reactive re-produce on `entry_written` events and drains before the process exits.
129
127
  - `Envelope::IO::{Reader,Writer}` — own the parse and persist halves of the write pipeline; the audit-append-as-final-step invariant lives in `Writer`.
130
128
 
131
129
  ## Container
@@ -139,7 +137,7 @@ Container = Data.define(
139
137
  )
140
138
  ```
141
139
 
142
- The `Store` builds one `Container` at boot; every use case receives it via `(container:, call:)`. RPC hook callables (`:resolve_intake`, `:transform_rows`, `:validate`) receive `caps: <Container>` — field names match what the prior `WriteCaps` exposed, so handlers reading `caps.manifest`, `caps.events`, etc. continue to work.
140
+ The `Store` builds one `Container` at boot; every use case receives it via `(container:, call:)`. RPC hook callables (`:resolve_handler`, `:transform_rows`, `:validate`) receive `caps: <Container>` — field names match what the prior `WriteCaps` exposed, so handlers reading `caps.manifest`, `caps.events`, etc. continue to work.
143
141
 
144
142
  ## Ports
145
143
 
@@ -151,9 +149,9 @@ Ports are infrastructure adapters with an interface defined by the domain. Each
151
149
  | `Ports::AuditLog` | Append-only structured log (`audit.log`). Owns seq numbering, file-locking, and rotation. |
152
150
  | `Ports::Clock` | Supplies `Time.now` — a module-function so tests can swap it without dependency injection boilerplate. |
153
151
  | `Ports::Publisher` | Copies a built artifact to a repo-relative consumer path and writes a sentinel so the next publish can confirm the target is managed. |
154
- | `Ports::Fetch::Lock` | Non-blocking `flock`-backed lock per key prevents concurrent fetch workers from racing on the same entry. |
155
- | `Ports::Fetch::Detached` | Spawns a background thread for async fetch; the caller receives a `fetch_backgrounded` event instead of blocking. |
156
- | `Ports::BuildLock` | Process-exclusive `flock` guard over the materializer build pipeline. Raises `BuildInProgress` if a build is already running. |
152
+ | `Ports::BuildLock` | Process-exclusive `flock` guard over the produce pipeline. Raises `BuildInProgress` if a build is already running. |
153
+ | `Ports::ProduceOnWriteSubscriber` | Pub-sub listener on `entry_written`; enqueues keys into `Produce::Engine::AsyncRunner` for reactive re-produce after any write. |
154
+ | `Ports::SentinelStore` | Reads and writes the per-target sentinel file that `Publisher` uses to detect unmanaged overwrites. |
157
155
 
158
156
  Application use cases access ports only through `Container` fields — never through the raw `Store`.
159
157
 
@@ -188,43 +186,49 @@ The four members are wired in `Manifest.build` (`lib/textus/manifest.rb`). `Mani
188
186
 
189
187
  ## Read path (`store.get(key)`)
190
188
 
191
- `Read::Get` is the single public read verb (ADR 0062). It is read-through by default: it returns the freshest obtainable envelope, fetching on a stale verdict per the entry's fetch rule, and degrading to a pure on-disk result when the key has no fetch rule. An optional `fetch: false` flag (CLI `--no-fetch`, MCP `{fetch:false}`) forces a pure on-disk read.
189
+ `Read::Get` is the single public read verb. It is a **pure read** (ADR 0089): it resolves the path, reads bytes, parses the envelope, and annotates a freshness verdict it NEVER ingests and NEVER mutates. The read-through that once refreshed a stale entry in-process (ADR 0062) is removed; quarantine freshness is system-pushed via `reconcile` (scheduled sweep) and `hook run` (event push).
192
190
 
193
191
  1. CLI verb (or MCP tool) calls `store.get(key, role:)` (or `store.as(role).get(key)`).
194
- 2. `Store#get` looks up `Dispatcher::VERBS[:get] → Read::Get`, builds a `Call`, instantiates `Read::Get.new(container:, call:).call(key)`. The contract declares `arg :fetch, default: true`, injected by `Contract::Binder.bind` at the single verb-dispatch chokepoint (`RoleScope#dispatch_bound`) for every surface — so the public verb is always read-through unless the caller explicitly passes `fetch: false`.
195
- 3. `Read::Get#call(key, fetch: false)` runs the pure read sub-step inline: resolves the path through `container.manifest`, reads bytes via `container.file_store`, parses the envelope, and annotates a freshness verdict (`stale`, `reason`, `fetching: false`). When the key has no fetch rule, the envelope is annotated fresh and returned immediatelyno orchestrator is involved.
196
- 4. If `fetch: true` and the verdict is stale and the entry's fetch rule demands action, `Read::Get` hands off to `Write::FetchOrchestrator` (built lazily — a pure `fetch: false` call never touches the orchestrator). The orchestrator executes the fetch policy's `Action` (`sync`, `timed_sync`, `detached`, …) and returns an `Outcome`.
197
- 5. The outcome is mapped back to an envelope: `Fetched` → fresh envelope from the write; `Detached` → original envelope with `fetching: true`; `Failed` → original envelope with `fetch_error` set; `Skipped` → original envelope unchanged.
192
+ 2. `Store#get` looks up `Dispatcher::VERBS[:get] → Read::Get`, builds a `Call`, instantiates `Read::Get.new(container:, call:).call(key)`. The verb takes only `key` there is no `fetch` flag on any surface.
193
+ 3. `Read::Get#call(key)` resolves the path through `container.manifest`, reads bytes via `container.file_store`, parses the envelope, and annotates a freshness verdict (`stale`, `reason`, `fetching: false`). When the key has no `upkeep` rule, the envelope is annotated fresh. A stale entry with `upkeep: { ttl:, action: refresh }` is returned **stale**the read does not refresh it; the next `reconcile` does.
198
194
 
199
- The pure read is `Read::Get#call(key, fetch: false)`it is the safe default for direct in-process callers (accept/reject/publish, materializer, uid, validate_all/validator, schema/tools, hooks/context) that must never trigger a fetch. They construct `Read::Get` directly, bypassing the dispatch injection that sets `fetch: true`. The prior separate read-through path `get_or_fetch` and the separate pure class `Read::GetEntry` were both unified into the one `Read::Get` class (ADR 0062 amendment).
195
+ Because the read is always pure, every callerinteractive reads, dashboards, and the direct in-process callers (accept/reject/publish, materializer, uid, validate_all/validator, schema/tools, hooks/context) gets the same orchestrator-free, side-effect-free read. The prior read-through path (`get_or_fetch`, then the `fetch:`-flagged `Read::Get`, ADR 0062) and its `Write::FetchOrchestrator` are gone (ADR 0089).
200
196
 
201
197
  ## Write path (`store.put(key, ...)`)
202
198
 
203
199
  1. CLI verb calls `store.put(key, meta:, body:, content:, if_etag:, role:)`.
204
200
  2. `Write::Put#call` validates the key, resolves the manifest entry, builds `GuardFactory.for(:put, key)` and calls `Guard#check!(eval)` (topology is predicate #0, `zone_writable_by`) — raises `WriteForbidden` if the topology gate denies, `GuardFailed` if any other predicate fails.
205
201
  3. Delegates persistence to `Envelope::IO::Writer#put`, which serializes, schema-validates, etag-checks (raises `EtagMismatch` on conflict), writes via the `FileStore` port, and appends the audit row.
206
- 4. Publishes `:entry_put` via `container.events` with `ctx: <Hooks::Context>`, `key:`, `envelope:`.
202
+ 4. Publishes `:entry_written` via `container.events` with `ctx: <Hooks::Context>`, `key:`, `envelope:`.
207
203
 
208
- `Write::{Delete,Mv,Accept,Reject,Build}` follow the same shape: explicit container, the unified `Guard` for authz (built per transition via `GuardFactory`), `Envelope::IO::Writer` for persistence (where applicable), event published with the `Hooks::Context` handle.
204
+ `Write::{KeyDelete,KeyMv,Accept,Reject,Propose}` follow the same shape: explicit container, the unified `Guard` for authz (built per transition via `GuardFactory`), `Envelope::IO::Writer` for persistence (where applicable), event published with the `Hooks::Context` handle.
209
205
 
210
- `Write::Mv` delegates the file-move + audit to `Envelope::IO::Writer#move`, then publishes `:entry_renamed` itself. UID injection (when the source lacks one) goes through `Envelope::IO::Writer#write` directly — no `Put` bypass.
206
+ `Write::KeyMv` delegates the file-move + audit to `Envelope::IO::Writer#move`, then publishes `:entry_renamed` itself. UID injection (when the source lacks one) goes through `Envelope::IO::Writer#write` directly — no `Put` bypass.
211
207
 
212
- ## Fetch path (`store.fetch(key)`)
208
+ ## Produce path (`reconcile` + reactive `entry_written`)
213
209
 
214
- 1. CLI `Verb::Fetch` calls `store.fetch(key, role: "automation")`.
215
- 2. `Write::FetchWorker#run(key)`:
216
- - Resolves the manifest entry, looks up the intake handler via `container.rpc.callable(:resolve_intake, mentry.handler)`.
217
- - Publishes `:fetch_started` with the hook context.
218
- - Invokes the handler under a 30s thread-join deadline.
219
- - On any error: publishes `:fetch_failed`, then re-raises.
220
- - On success: builds `GuardFactory.for(:fetch, key)` and calls `Guard#check!`, then persists via `Envelope::IO::Writer#write` directly (no `Put` round-trip); publishes `:entry_fetched` unless etag is unchanged.
221
- 3. `store.fetch_all(prefix:, zone:)` lists stale entries via `Read::Stale` and runs `FetchWorker#run` per entry; returns `{ fetched:, failed:, skipped: }`.
210
+ The produce pipeline handles two concerns — **acquire** (pull live data via an intake handler) and **render** (template-driven artifact publish) — unified under `Produce::Engine`.
211
+
212
+ `Produce::Engine.converge(container:, call:, keys:)` is the entry point for both `reconcile` (scheduled batch) and the reactive `AsyncRunner` (triggered on `entry_written` by `Ports::ProduceOnWriteSubscriber`).
213
+
214
+ For each key, `Engine#produce_one`:
215
+
216
+ 1. **Acquire phase** `Produce::Acquire::Intake#run(key)`:
217
+ - Resolves the manifest entry; looks up the intake handler via `container.rpc.callable(:resolve_handler, mentry.handler)`.
218
+ - Publishes `:entry_fetch_started` via `Produce::Events`.
219
+ - Invokes the handler under a timeout deadline.
220
+ - On error: publishes `:entry_fetch_failed`, re-raises.
221
+ - On success: normalises the handler result via its own `normalize_action_result` (keyed on the entry's format), checks guard, persists via `Envelope::IO::Writer`, publishes `:entry_fetched` unless the etag is unchanged.
222
+ - `Acquire::Handler` resolves and invokes the RPC callable under the timeout deadline. (The sibling **projection** sub-path — `from: project` entries — instead runs `Acquire::Projection`, which renders data files through `Acquire::Serializer::{Json,Yaml,Text}` before persisting.)
223
+ 2. **Render phase** — `entry.publish_via(context)` calls `Produce::Render#bytes_for(target:, data:, boot:)` to expand the Mustache template and copy the result to the publish target via `Ports::Publisher`. Returns `nil` if no publish is configured (skipped).
224
+
225
+ `AsyncRunner` (nested in `Engine`) enqueues reactive produce when `entry_written` fires, then drains before the process exits via an `at_exit` hook. A held `BuildLock` is a soft miss — the in-flight build already produces fresh output.
222
226
 
223
227
  ## Hook payload contract
224
228
 
225
- Pub-sub hooks (`:entry_put`, `:entry_fetched`, …) receive `ctx:` — a `Textus::Hooks::Context` that exposes a narrow surface (`get`, `list`, `put`, `delete`, `audit`, `publish_followup`, plus `role` and `correlation_id`). The raw `Store` is not handed out.
229
+ Pub-sub hooks (`:entry_written`, `:entry_fetched`, …) receive `ctx:` — a `Textus::Hooks::Context` that exposes a narrow surface (`get`, `list`, `put`, `delete`, `audit`, `publish_followup`, plus `role` and `correlation_id`). The raw `Store` is not handed out.
226
230
 
227
- RPC hooks (`:resolve_intake`, `:transform_rows`, `:validate`) receive `caps:` — a `Textus::Container`. They are gem-internal: the framework calls them, not user pub-sub.
231
+ RPC hooks (`:resolve_handler`, `:transform_rows`, `:validate`) receive `caps:` — a `Textus::Container`. They are gem-internal: the framework calls them, not user pub-sub.
228
232
 
229
233
  ## Agent surface (boot + pulse + MCP)
230
234
 
@@ -49,78 +49,83 @@ Tooling around `git blame` or audit logs may filter on owner; the gem itself onl
49
49
 
50
50
  ## Derived entries
51
51
 
52
- A derived entry declares a `compute:` block with a `kind:` discriminator. Two kinds:
52
+ A derived entry declares a `source:` block with a `from:` discriminator (ADR 0093/0094). A `source:` acquires **data** — it never renders; rendering is a publish concern (below). Two `from:` values for derived entries:
53
53
 
54
- **`compute: { kind: projection }`** — textus computes the entry on `textus build` from other store entries. Declarative; nothing shells out.
54
+ **`source: { from: project }`** — textus computes the entry's data on `textus reconcile` from other store entries. Declarative; nothing shells out. Projection fields are flat under `source:`.
55
55
 
56
56
  ```yaml
57
- - key: artifacts.catalogs.people
58
- path: artifacts/catalogs/people.md
57
+ - key: artifacts.derived.people
58
+ path: artifacts/derived/people.md
59
59
  zone: artifacts
60
60
  schema: null
61
61
  owner: automation:catalog-people
62
- compute:
63
- kind: projection
62
+ source:
63
+ from: project
64
64
  select: knowledge.network.org # prefix or list of prefixes
65
65
  pluck: [name, relationship, org]
66
66
  sort_by: name
67
- template: people.mustache # under .textus/templates/
67
+ format: json # the stored form is data
68
68
  publish:
69
- to: [docs/people.md] # optional repo-relative byte-copy targets
69
+ - { to: docs/people.md, template: people.mustache } # render the data through a template
70
70
  ```
71
71
 
72
- **`compute: { kind: external }`** — an external build tool (rake, just, a shell script) produces the file. textus never executes the `command:`; it only tracks `sources:` so `textus freshness` can compare source mtimes against the file's `_meta.generated.at`. The role running the build must hold `build` (default: `automation`).
72
+ **`source: { from: command }`** — an external build tool (rake, just, a shell script) produces the file. textus never executes the `command:`; it only tracks `sources:` so `doctor`'s `generator_drift` check can compare source mtimes against the file's `_meta.generated.at`. The role running the build must hold `reconcile` (default: `automation`).
73
73
 
74
74
  ```yaml
75
- - key: artifacts.catalogs.skills
76
- path: artifacts/catalogs/skills.md
75
+ - key: artifacts.derived.skills
76
+ path: artifacts/derived/skills.md
77
77
  zone: artifacts
78
78
  owner: automation:catalog-skills
79
- compute:
80
- kind: external
79
+ source:
80
+ from: command
81
81
  command: "rake catalog:skills" # informational; the automation invokes it
82
82
  sources: [knowledge.projects, knowledge.network]
83
83
  ```
84
84
 
85
- The build automation is responsible for writing the `generated:` frontmatter block (`by`, `at`, `from`) when it produces the file. `generated.from` SHOULD match `compute.sources` — same list, recorded twice so a diff proves what was consumed.
85
+ The build automation is responsible for writing the `generated:` frontmatter block (`by`, `at`, `from`) when it produces the file. `generated.from` SHOULD match `source.sources` — same list, recorded twice so a diff proves what was consumed.
86
86
 
87
- Full contract for both shapes is in [`../../SPEC.md` §5.2.1 and §5.2.2](../../SPEC.md). Transforms (`compute.transform:`) and subtree publishing (`publish: { tree: }`) are also covered there.
87
+ Full contract for both shapes is in [`../../SPEC.md` §5.2.1 and §5.2.2](../../SPEC.md). Data transforms (`source.transform:`) and publishing (a `publish:` list of `{ to, template? }` / `{ tree }` targets — render or copy the data) are covered in §5.2–§5.3.
88
88
 
89
89
  ## Intake and freshness
90
90
 
91
- External inputs land via `:resolve_intake` hooks, not shell commands. Each intake entry names a registered handler; refresh is on demand via a read-through `get`:
91
+ External inputs land via `:resolve_handler` hooks, not shell commands. Each intake entry declares `source: { from: handler, handler: <name>, ttl: <dur> }`; re-pull is system-pushed via `reconcile` (scheduled sweep) and `hook run` (event push) — a `get` never refreshes (ADR 0089):
92
92
 
93
93
  ```sh
94
- textus get feeds.notion.roadmap --as=automation # refreshes if stale
95
- textus freshness --zone=feeds --output=json # which entries are expired
94
+ textus pulse --output=json # `stale` lists expired entries; `next_due_at` is the soonest deadline
95
+ textus reconcile --as=automation # re-pulls every intake entry past its source.ttl
96
96
  ```
97
97
 
98
- Lifecycle budgets live in the top-level `rules:` block, matched by glob:
98
+ The re-pull cadence is the entry's own `source.ttl`. Age-based garbage collection is the orthogonal `retention:` rule slot in the top-level `rules:` block, matched by glob (`{ ttl, action: drop | archive }`); the two compose — re-pull hourly *and* archive at 90 days:
99
99
 
100
100
  ```yaml
101
+ - key: artifacts.feeds.notion.events
102
+ kind: produced # produce-method (intake) read from source.from: handler
103
+ zone: artifacts
104
+ source: { from: handler, handler: notion, config: { ... }, ttl: 6h }
105
+
101
106
  rules:
102
- - match: feeds.notion.**
103
- lifecycle: { ttl: 6h, on_expire: refresh } # refresh | warn | drop | archive
107
+ - match: artifacts.feeds.notion.**
108
+ retention: { ttl: 90d, action: archive }
104
109
  ```
105
110
 
106
- A typical scheduled integration reads each expired feed (a read-through `get`
107
- refreshes it in-process):
111
+ A typical scheduled integration runs `reconcile` on a cron to re-pull every
112
+ expired feed:
108
113
 
109
114
  ```sh
110
- textus get feeds.notion.roadmap --as=automation # in cron / CI
115
+ textus reconcile --as=automation # in cron / CI — re-pulls all stale feeds
111
116
  ```
112
117
 
113
118
  See [`./zones.md` §6](zones.md) for the full intake contract and [`../how-to/writing-hooks.md`](../how-to/writing-hooks.md) for writing custom handlers.
114
119
 
115
120
  ### Read vs. refresh
116
121
 
117
- There is one public read operation (ADR 0062):
122
+ There is one public read operation, and it is pure (ADR 0089):
118
123
 
119
124
  | Operation | Behaviour | Use for |
120
125
  |-----------|-----------|---------|
121
- | `ops.get` | Read-through by defaultrefreshes on stale per the entry's `lifecycle` rule when `on_expire: refresh`; degrades to a pure on-disk read when the key has no lifecycle rule. Pass `fetch: false` (CLI `--no-fetch`, MCP `{fetch:false}`) for an explicit pure read | all callers, including interactive reads, dashboards, and scripts that want the freshest obtainable envelope |
126
+ | `ops.get` | A pure on-disk read annotated with a freshness verdict it NEVER ingests, regardless of the entry's `action`. A stale `refresh` entry reads back stale until the next `reconcile`. | every caller interactive reads, dashboards, scripts, and internal pipelines (materializer, projection, schema tooling, accept/reject/publish, uid, validator) |
122
127
 
123
- Build pipelines and other internal callers that must never trigger a refresh (materializer, projection, schema tooling, accept/reject/publish, uid, validator) construct `Read::Get` directly with the method default `fetch: false` a pure, orchestrator-free read. They bypass the verb-dispatch injection that sets `fetch: true`, so they always get pure reads without any extra argument.
128
+ Refreshing a stale entry is `reconcile`'s job (or a `hook run` event), never a read's so no caller can accidentally trigger network I/O by reading.
124
129
 
125
130
  ## Body content
126
131
 
@@ -146,4 +151,4 @@ The user-facing CLI surface, the wire envelope shape, and the protocol version (
146
151
 
147
152
  - **MCP servers**: a thin server that exposes `textus get` and `textus put` as tools is the recommended way to give Claude/agents access. Don't bake MCP into this gem.
148
153
  - **Vector stores**: index `body` content into a vector store if you want fuzzy retrieval. `frontmatter` stays in textus as the source of truth for deterministic facts.
149
- - **CI**: run `textus freshness` (or `textus list` + schema validation) in CI to catch drift between derived entries and their sources.
154
+ - **CI**: run `textus doctor` (the `generator_drift` check) or `textus pulse` (the `stale` list) in CI to catch drift between derived entries and their sources.
data/lib/textus/boot.rb CHANGED
@@ -26,14 +26,11 @@ module Textus
26
26
  "propose changes by writing #{manifest.policy.queue_zone}.* entries with --as=#{name} " \
27
27
  "and a 'proposal:' frontmatter block; the #{authority} role runs 'textus accept' to apply"
28
28
  end,
29
- fetch: lambda do |name, manifest|
30
- "fetch #{zone_label(manifest, :quarantine, "quarantine")} entries with " \
31
- "'textus fetch KEY --as=#{name}' (uses the entry's declared action)"
32
- end,
33
- build: lambda do |_name, manifest|
34
- derived = zone_label(manifest, :derived, "derived")
35
- "'textus build' computes #{derived} entries from projections; " \
36
- "#{derived} files are never hand-edited"
29
+ reconcile: lambda do |_name, manifest|
30
+ machine = zone_label(manifest, :machine, "machine")
31
+ "'textus reconcile' materializes derived #{machine} entries from their sources and " \
32
+ "refreshes stale intake #{machine} entries from their declared source; " \
33
+ "derived files are never hand-edited (reactive on canon writes, or a full pass on demand)"
37
34
  end,
38
35
  }.freeze
39
36
 
@@ -71,49 +68,49 @@ module Textus
71
68
  },
72
69
  }.freeze
73
70
 
74
- # Curated agent-facing verb catalog. For verbs that have a Dispatcher contract,
75
- # the summary is derived from `contract.summary` at load time (ADR 0039). The
76
- # editorial strings below are the fallback for CLI-only verbs without contracts.
77
- # CLI_VERBS itself is assigned in textus.rb after Zeitwerk eager_load so that
78
- # all contract-declaring files are loaded before derivation runs.
71
+ # Curated agent-facing verb catalog. This declares which verbs the operator
72
+ # CLI surfaces and in what order the editorial presentation. The summary of
73
+ # each verb is a fact, not presentation: it is derived from `contract.summary`
74
+ # at load time (ADR 0039). A literal "summary" survives here only for grouped
75
+ # CLI tokens (schema/key/rule/hook) that aggregate several sub-contracts and so
76
+ # have no single contract to derive from. CLI_VERBS itself is assigned in
77
+ # textus.rb after Zeitwerk eager_load so all contract files are present.
79
78
  CURATED_CLI_VERBS = [
80
79
  { "name" => "boot" },
81
80
  { "name" => "list" },
82
81
  { "name" => "get" },
83
- { "name" => "where", "summary" => "resolve a key to its zone and path without reading" },
82
+ { "name" => "where" },
84
83
  { "name" => "schema", "summary" => "schema operations: 'schema show KEY', 'schema diff', 'schema init', 'schema migrate'" },
85
84
  { "name" => "put" },
86
85
  { "name" => "propose" },
87
- { "name" => "accept", "summary" => "apply a queued proposal to its target zone; requires the author capability" },
88
- { "name" => "key", "summary" => "key operations: 'key delete', 'key mv', 'key uid'" },
89
- { "name" => "build", "summary" => "materialize derived entries; publish_to and publish_tree fan out copies" },
90
- { "name" => "tend" },
91
- { "name" => "freshness", "summary" => "per-entry lifecycle report (status, age, ttl, on_expire action)" },
92
- { "name" => "audit", "summary" => "query .textus/audit.log with filters (key, role, since, correlation-id, ...)" },
93
- { "name" => "blame", "summary" => "audit rows for one key joined with git commit metadata" },
94
- { "name" => "rule", "summary" => "inspect effective rules: 'rule list', 'rule explain KEY'" },
95
- { "name" => "doctor", "summary" => "health-check the store (missing schemas, illegal keys, sentinel drift, etc.)" },
96
- { "name" => "hook", "summary" => "list and run registered hooks: 'hook list', 'hook run NAME'" },
86
+ { "name" => "accept" },
87
+ { "name" => "key", "summary" => "key operations: 'key delete', 'key mv', 'key uid'" },
88
+ { "name" => "reconcile" },
89
+ { "name" => "audit" },
90
+ { "name" => "blame" },
91
+ { "name" => "rule", "summary" => "inspect effective rules: 'rule list', 'rule explain KEY'" },
92
+ { "name" => "doctor" },
93
+ { "name" => "hook", "summary" => "list and run registered hooks: 'hook list', 'hook run NAME'" },
97
94
  { "name" => "pulse" },
98
95
  { "name" => "capabilities" },
99
96
  ].freeze
100
97
 
101
- # Build the CLI verb catalog by deriving each summary from the corresponding
102
- # Dispatcher contract when one exists, falling back to the editorial string for
103
- # CLI-only verbs without a contract (e.g. accept, build, where). Called once
104
- # from textus.rb after eager_load so all contract files are present.
105
- def self.build_cli_verbs
106
- by_contract = Dispatcher::VERBS.values
107
- .select { |k| k.respond_to?(:contract?) && k.contract? }
108
- .to_h { |k| [k.contract.verb.to_s, k.contract.summary] }
98
+ # verb token => contract.summary, for every Dispatcher verb that carries a
99
+ # contract. The single source for a verb's one-line summary (ADR 0039).
100
+ def self.contract_summaries
101
+ Dispatcher::VERBS.values
102
+ .select { |k| k.respond_to?(:contract?) && k.contract? }
103
+ .to_h { |k| [k.contract.verb.to_s, k.contract.summary] }
104
+ end
109
105
 
106
+ # Build the CLI verb catalog: each summary is derived from its contract when
107
+ # one exists, falling back to the curated editorial string for grouped tokens
108
+ # (schema/key/rule/hook). Called once from textus.rb after eager_load.
109
+ def self.build_cli_verbs
110
+ summaries = contract_summaries
110
111
  CURATED_CLI_VERBS.map do |entry|
111
- derived = by_contract[entry["name"]]
112
- if derived
113
- entry.merge("summary" => derived)
114
- else
115
- entry
116
- end
112
+ derived = summaries[entry["name"]]
113
+ derived ? entry.merge("summary" => derived) : entry
117
114
  end
118
115
  end
119
116
 
@@ -130,7 +127,8 @@ module Textus
130
127
  # Both verb lists derive from the MCP catalog (ADR 0056, ADR 0057): the
131
128
  # agent's real read and write surface, named as verbs the agent calls —
132
129
  # not CLI strings. read_verbs can neither advertise a verb the agent
133
- # cannot call (audit/freshness/doctor are CLI-only) nor omit one it can
130
+ # cannot call (audit/doctor are CLI-only; freshness is a Ruby-only
131
+ # internal scan, ADR 0085) nor omit one it can
134
132
  # (schema_show/rules); write_verbs drops the old `put KEY --as=… --stdin` CLI
135
133
  # framing (role is connection-resolved over MCP; there is no stdin).
136
134
  # writable_zones / propose_zone below carry the agent's write authority.
@@ -144,11 +142,11 @@ module Textus
144
142
 
145
143
  # Recipes reference verbs, not a transport's CLI strings (ADR 0056): every
146
144
  # step names a verb the agent can call (each transport frames it — CLI as
147
- # `textus get KEY`, MCP as the `get` tool) or is a plain build step. This
145
+ # `textus get KEY`, MCP as the `get` tool) or is a plain materialize step. This
148
146
  # keeps shell lines out of the surface an MCP agent reads.
149
147
  def self.recipes(manifest)
150
148
  queue = manifest.policy.queue_zone
151
- feeds = zone_label(manifest, :quarantine, "the quarantine zone")
149
+ feeds = zone_label(manifest, :machine, "the machine zone")
152
150
  {
153
151
  "read" => {
154
152
  "purpose" => "find and read an entry",
@@ -174,11 +172,11 @@ module Textus
174
172
  "accept #{queue}.KEY — promotes the proposal into its target zone",
175
173
  ],
176
174
  },
177
- "fetch" => {
178
- "purpose" => "refresh stale quarantine-zone caches from their declared intake",
175
+ "reconcile" => {
176
+ "purpose" => "keep the machine-maintained lanes fresh — re-pull stale intake entries from their declared source",
179
177
  "steps" => [
180
178
  "pulse — its `stale` list names entries past their ttl",
181
- "fetch_all (zone: #{feeds}) — re-pull the stale entries",
179
+ "reconcile (zone: #{feeds}) — re-pull the stale entries",
182
180
  ],
183
181
  },
184
182
  }
@@ -196,8 +194,20 @@ module Textus
196
194
  )
197
195
  end
198
196
 
199
- def self.build(container:)
197
+ def self.build(container:, lean: false)
200
198
  manifest = container.manifest
199
+ etag = Textus::Etag.for_contract(container.root)
200
+
201
+ if lean
202
+ return {
203
+ "protocol" => PROTOCOL_ID,
204
+ "store_root" => container.root,
205
+ "zones" => zones_for(manifest),
206
+ "agent_quickstart" => agent_quickstart(manifest, container.audit_log),
207
+ "contract_etag" => etag,
208
+ }
209
+ end
210
+
201
211
  {
202
212
  "protocol" => PROTOCOL_ID,
203
213
  "store_root" => container.root,
@@ -208,6 +218,7 @@ module Textus
208
218
  "cli_verbs" => CLI_VERBS.map(&:dup),
209
219
  "agent_protocol" => agent_protocol(manifest),
210
220
  "agent_quickstart" => agent_quickstart(manifest, container.audit_log),
221
+ "contract_etag" => etag,
211
222
  "docs" => { "spec" => "SPEC.md", "example" => "examples/project/" },
212
223
  }
213
224
  end
@@ -225,7 +236,7 @@ module Textus
225
236
 
226
237
  def self.entries_for(manifest)
227
238
  manifest.data.entries.map do |e|
228
- derived = manifest.policy.derived_zone?(e.zone)
239
+ derived = e.derived?
229
240
  {
230
241
  "key" => e.key,
231
242
  "zone" => e.zone,
@@ -234,7 +245,7 @@ module Textus
234
245
  "owner" => e.owner,
235
246
  "format" => e.format,
236
247
  "derived" => derived,
237
- "intake" => e.is_a?(Textus::Manifest::Entry::Intake),
248
+ "intake" => e.intake?,
238
249
  "publish_to" => Array(e.publish_to),
239
250
  }
240
251
  end
data/lib/textus/call.rb CHANGED
@@ -9,7 +9,7 @@ module Textus
9
9
  new(
10
10
  role: role.to_s,
11
11
  correlation_id: correlation_id || SecureRandom.uuid,
12
- now: now || Textus::Ports::Clock.now,
12
+ now: now || Textus::Ports::Clock.new.now,
13
13
  dry_run: dry_run,
14
14
  )
15
15
  end
@@ -130,18 +130,23 @@ module Textus
130
130
  # — behavior the generic projection cannot express (ADR 0068/0069):
131
131
  # get — raises UnknownKey with resolver suggestions (a CLI-only
132
132
  # affordance; the agent surface deliberately returns nil)
133
- # put — IntakeFetch read-through orchestration on --fetch
134
- # build auto-resolves the build-capability actor role (not --as) and
135
- # serializes under BuildLock; the role resolution is policy, not
136
- # a projection (around: covers only the lock)
137
- BEHAVIORAL_HATCHES = %i[get put build].freeze
133
+ # put — reads the entry JSON from --stdin (ADR 0089: just stores bytes,
134
+ # no --fetch transform)
135
+ # (build removed in ADR 0087: materialization is system-pushed via reconcile)
136
+ BEHAVIORAL_HATCHES = %i[get put].freeze
138
137
 
139
138
  # Contract verbs whose CLI is a plain `< Verb` command, not a projection at
140
- # all — composite reports assembled outside the contract:
141
- # boot, doctor composite reports
142
- # (fetch/fetch_all were removed in ADR 0079: FetchWorker is now internal,
143
- # driven by get's read-through orchestrator and the tend sweep.)
144
- NON_PROJECTED_CLI = %i[boot doctor].freeze
139
+ # all — composite reports assembled outside the contract.
140
+ # (boot removed: its contract carries surfaces :cli + the :lean arg, so the
141
+ # generic projection now generates it; the hand-authored CLI::Verb::Boot is
142
+ # deleted in ADR 0101.)
143
+ # (doctor retained: hand-authored to preserve --check=NAME flag spelling and
144
+ # the exit_code: res["ok"] ? 0 : 1 behavior — two things the generic
145
+ # projection cannot yet express; kept in ADR 0101 pending a future pass.)
146
+ # (fetch/fetch_all were removed in ADR 0079: Produce::Acquire::Intake is now internal,
147
+ # driven by the reconcile sweep and hook run — ADR 0089 removed the
148
+ # read-through that once also drove it.)
149
+ NON_PROJECTED_CLI = %i[doctor].freeze
145
150
 
146
151
  # The installer skips generation for either category.
147
152
  HAND_AUTHORED_VERBS = (BEHAVIORAL_HATCHES + NON_PROJECTED_CLI).freeze
@@ -4,12 +4,10 @@ module Textus
4
4
  class Get < Runner::Base
5
5
  self.spec = Textus::Read::Get.contract
6
6
  option :as_flag, "--as=ROLE"
7
- option :no_fetch, "--no-fetch"
8
7
 
9
8
  def invoke(store)
10
9
  key = positional.shift or raise UsageError.new("get requires a key")
11
- kw = no_fetch.nil? ? {} : { fetch: false }
12
- result = session_for(store).get(key, **kw)
10
+ result = session_for(store).get(key)
13
11
  raise Textus::UnknownKey.new(key, suggestions: store.manifest.resolver.suggestions_for(key)) if result.nil?
14
12
 
15
13
  emit(result.to_h_for_wire)
@@ -31,7 +31,7 @@ module Textus
31
31
  Role.resolve(flag: as_flag, env: ENV, root: store.root)
32
32
 
33
33
  begin
34
- Textus::Write::IntakeFetch.invoke(
34
+ Textus::Produce::Acquire::Handler.invoke(
35
35
  caps: store.container, handler: name, config: {}, args: args, label: "hook run",
36
36
  )
37
37
  rescue Textus::Error
@@ -6,7 +6,6 @@ module Textus
6
6
 
7
7
  option :as_flag, "--as=ROLE"
8
8
  option :use_stdin, "--stdin"
9
- option :fetch_name, "--fetch=NAME"
10
9
 
11
10
  def invoke(store)
12
11
  key = positional.shift or raise UsageError.new("put requires a key")
@@ -14,25 +13,10 @@ module Textus
14
13
 
15
14
  role = resolved_role(store)
16
15
 
17
- raw = @stdin.read
18
- payload =
19
- if fetch_name
20
- result = Textus::Write::IntakeFetch.invoke(
21
- caps: store.container, handler: fetch_name,
22
- config: { "bytes" => raw }, args: {}, label: "fetch"
23
- )
24
- basename = key.split(".").last
25
- {
26
- "_meta" => {
27
- "name" => basename,
28
- "last_fetched_at" => Time.now.utc.iso8601,
29
- "fetched_with" => fetch_name,
30
- }.merge(result[:_meta] || result["_meta"] || {}),
31
- "body" => result[:body] || result["body"] || "",
32
- }
33
- else
34
- JSON.parse(raw)
35
- end
16
+ # put only stores the stdin JSON (ADR 0089): no transform-on-write.
17
+ # Ingest (running a handler over bytes) is system-pushed via reconcile
18
+ # and hook run, never a put flag.
19
+ payload = JSON.parse(@stdin.read)
36
20
 
37
21
  meta = payload["_meta"] || {}
38
22
  body = payload["body"] || ""
data/lib/textus/cli.rb CHANGED
@@ -122,10 +122,7 @@ module Textus
122
122
  textus list [--prefix=KEY] [--zone=Z]
123
123
  textus where KEY
124
124
  textus get KEY
125
- textus put KEY --stdin [--fetch=NAME] --as=ROLE
126
- textus freshness [--prefix=KEY] [--zone=Z]
127
- textus fetch KEY
128
- textus fetch all [--prefix=KEY] [--zone=Z]
125
+ textus put KEY --stdin --as=ROLE
129
126
  textus audit [--key=K] [--zone=Z] [--role=R] [--verb=V] [--since=X] [--correlation-id=ID] [--limit=N]
130
127
  textus blame KEY [--limit=N]
131
128
  textus doctor