textus 0.22.0 → 0.29.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 (186) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +195 -48
  3. data/CHANGELOG.md +178 -0
  4. data/README.md +55 -13
  5. data/SPEC.md +79 -42
  6. data/docs/conventions.md +10 -0
  7. data/lib/textus/boot.rb +31 -29
  8. data/lib/textus/builder/pipeline.rb +13 -12
  9. data/lib/textus/call.rb +28 -0
  10. data/lib/textus/cli/group/mcp.rb +9 -0
  11. data/lib/textus/cli/group/zone.rb +9 -0
  12. data/lib/textus/cli/verb/accept.rb +1 -1
  13. data/lib/textus/cli/verb/audit.rb +2 -2
  14. data/lib/textus/cli/verb/blame.rb +1 -1
  15. data/lib/textus/cli/verb/boot.rb +1 -1
  16. data/lib/textus/cli/verb/build.rb +3 -3
  17. data/lib/textus/cli/verb/delete.rb +1 -1
  18. data/lib/textus/cli/verb/deps.rb +1 -1
  19. data/lib/textus/cli/verb/doctor.rb +1 -1
  20. data/lib/textus/cli/verb/freshness.rb +1 -1
  21. data/lib/textus/cli/verb/get.rb +1 -1
  22. data/lib/textus/cli/verb/hook_run.rb +3 -4
  23. data/lib/textus/cli/verb/hooks.rb +11 -14
  24. data/lib/textus/cli/verb/key_delete.rb +24 -0
  25. data/lib/textus/cli/verb/list.rb +1 -1
  26. data/lib/textus/cli/verb/mcp_serve.rb +17 -0
  27. data/lib/textus/cli/verb/migrate.rb +18 -0
  28. data/lib/textus/cli/verb/mv.rb +11 -3
  29. data/lib/textus/cli/verb/published.rb +1 -1
  30. data/lib/textus/cli/verb/pulse.rb +1 -1
  31. data/lib/textus/cli/verb/put.rb +8 -6
  32. data/lib/textus/cli/verb/rdeps.rb +1 -1
  33. data/lib/textus/cli/verb/refresh.rb +1 -1
  34. data/lib/textus/cli/verb/refresh_stale.rb +1 -1
  35. data/lib/textus/cli/verb/reject.rb +1 -1
  36. data/lib/textus/cli/verb/rule_explain.rb +1 -1
  37. data/lib/textus/cli/verb/rule_lint.rb +18 -0
  38. data/lib/textus/cli/verb/schema.rb +1 -1
  39. data/lib/textus/cli/verb/uid.rb +1 -1
  40. data/lib/textus/cli/verb/where.rb +1 -1
  41. data/lib/textus/cli/verb/zone_mv.rb +19 -0
  42. data/lib/textus/cli/verb.rb +7 -7
  43. data/lib/textus/cli.rb +0 -7
  44. data/lib/textus/container.rb +23 -0
  45. data/lib/textus/dispatcher.rb +49 -0
  46. data/lib/textus/doctor/check/audit_log.rb +2 -2
  47. data/lib/textus/doctor/check/handler_allowlist.rb +2 -2
  48. data/lib/textus/doctor/check/hooks.rb +4 -3
  49. data/lib/textus/doctor/check/illegal_keys.rb +2 -2
  50. data/lib/textus/doctor/check/intake_registration.rb +2 -2
  51. data/lib/textus/doctor/check/manifest_files.rb +2 -2
  52. data/lib/textus/doctor/check/protocol_version.rb +2 -2
  53. data/lib/textus/doctor/check/refresh_locks.rb +2 -2
  54. data/lib/textus/doctor/check/rule_ambiguity.rb +2 -2
  55. data/lib/textus/doctor/check/schema_parse_error.rb +1 -1
  56. data/lib/textus/doctor/check/schema_violations.rb +1 -1
  57. data/lib/textus/doctor/check/schemas.rb +2 -2
  58. data/lib/textus/doctor/check/sentinels.rb +11 -9
  59. data/lib/textus/doctor/check/templates.rb +2 -2
  60. data/lib/textus/doctor/check/unowned_schema_fields.rb +1 -1
  61. data/lib/textus/doctor/check.rb +12 -3
  62. data/lib/textus/doctor.rb +24 -27
  63. data/lib/textus/domain/authorizer.rb +6 -6
  64. data/lib/textus/{application → domain}/policy/predicates/accept_authority_signed.rb +2 -2
  65. data/lib/textus/{application → domain}/policy/predicates/schema_valid.rb +1 -1
  66. data/lib/textus/{application → domain}/policy/promotion.rb +1 -1
  67. data/lib/textus/domain/sentinel.rb +9 -65
  68. data/lib/textus/domain/staleness/generator_check.rb +46 -26
  69. data/lib/textus/domain/staleness/intake_check.rb +20 -12
  70. data/lib/textus/domain/staleness.rb +4 -4
  71. data/lib/textus/envelope/io/reader.rb +44 -0
  72. data/lib/textus/{application/writes/envelope_io.rb → envelope/io/writer.rb} +32 -58
  73. data/lib/textus/hooks/builtin.rb +14 -14
  74. data/lib/textus/hooks/context.rb +30 -13
  75. data/lib/textus/hooks/error_log.rb +32 -0
  76. data/lib/textus/hooks/{bus.rb → event_bus.rb} +41 -53
  77. data/lib/textus/hooks/loader.rb +29 -3
  78. data/lib/textus/hooks/rpc_registry.rb +77 -0
  79. data/lib/textus/key/path.rb +7 -3
  80. data/lib/textus/maintenance/key_delete_prefix.rb +36 -0
  81. data/lib/textus/maintenance/key_mv_prefix.rb +46 -0
  82. data/lib/textus/maintenance/migrate.rb +51 -0
  83. data/lib/textus/maintenance/rule_lint.rb +56 -0
  84. data/lib/textus/maintenance/zone_mv.rb +51 -0
  85. data/lib/textus/maintenance.rb +15 -0
  86. data/lib/textus/manifest/data.rb +79 -0
  87. data/lib/textus/manifest/entry/base.rb +38 -18
  88. data/lib/textus/manifest/entry/derived.rb +8 -9
  89. data/lib/textus/manifest/entry/nested.rb +7 -9
  90. data/lib/textus/manifest/entry/parser.rb +2 -2
  91. data/lib/textus/manifest/entry/validators/events.rb +2 -2
  92. data/lib/textus/manifest/entry/validators/format_matrix.rb +2 -2
  93. data/lib/textus/manifest/entry/validators/index_filename.rb +1 -1
  94. data/lib/textus/manifest/entry/validators/inject_boot.rb +4 -2
  95. data/lib/textus/manifest/entry/validators/publish_each.rb +1 -1
  96. data/lib/textus/manifest/entry/validators.rb +2 -2
  97. data/lib/textus/manifest/entry.rb +0 -5
  98. data/lib/textus/manifest/policy.rb +48 -0
  99. data/lib/textus/manifest/resolver.rb +14 -14
  100. data/lib/textus/manifest/rules.rb +1 -1
  101. data/lib/textus/manifest.rb +47 -110
  102. data/lib/textus/mcp/errors.rb +32 -0
  103. data/lib/textus/mcp/server.rb +126 -0
  104. data/lib/textus/mcp/session.rb +40 -0
  105. data/lib/textus/mcp/tool_schemas.rb +71 -0
  106. data/lib/textus/mcp/tools.rb +129 -0
  107. data/lib/textus/mcp.rb +6 -0
  108. data/lib/textus/{infra → ports}/audit_log.rb +1 -1
  109. data/lib/textus/{infra → ports}/audit_subscriber.rb +7 -8
  110. data/lib/textus/{infra → ports}/build_lock.rb +1 -1
  111. data/lib/textus/{infra → ports}/clock.rb +1 -1
  112. data/lib/textus/{infra → ports}/publisher.rb +6 -6
  113. data/lib/textus/{infra → ports}/refresh/detached.rb +3 -3
  114. data/lib/textus/{infra → ports}/refresh/lock.rb +1 -1
  115. data/lib/textus/ports/sentinel_store.rb +67 -0
  116. data/lib/textus/ports/storage/file_stat.rb +19 -0
  117. data/lib/textus/{infra → ports}/storage/file_store.rb +1 -1
  118. data/lib/textus/projection.rb +91 -0
  119. data/lib/textus/read/audit.rb +111 -0
  120. data/lib/textus/read/blame.rb +81 -0
  121. data/lib/textus/read/boot.rb +18 -0
  122. data/lib/textus/read/deps.rb +24 -0
  123. data/lib/textus/read/doctor.rb +19 -0
  124. data/lib/textus/read/freshness.rb +101 -0
  125. data/lib/textus/read/get.rb +66 -0
  126. data/lib/textus/read/get_or_refresh.rb +69 -0
  127. data/lib/textus/read/list.rb +15 -0
  128. data/lib/textus/read/policy_explain.rb +37 -0
  129. data/lib/textus/read/published.rb +15 -0
  130. data/lib/textus/read/pulse.rb +89 -0
  131. data/lib/textus/read/rdeps.rb +25 -0
  132. data/lib/textus/read/schema_envelope.rb +16 -0
  133. data/lib/textus/read/stale.rb +17 -0
  134. data/lib/textus/read/uid.rb +20 -0
  135. data/lib/textus/read/validate_all.rb +22 -0
  136. data/lib/textus/read/validator.rb +84 -0
  137. data/lib/textus/read/where.rb +16 -0
  138. data/lib/textus/role_scope.rb +49 -0
  139. data/lib/textus/schema/tools.rb +14 -10
  140. data/lib/textus/store.rb +25 -11
  141. data/lib/textus/version.rb +1 -1
  142. data/lib/textus/write/accept.rb +86 -0
  143. data/lib/textus/write/authority_gate.rb +24 -0
  144. data/lib/textus/write/delete.rb +54 -0
  145. data/lib/textus/write/materializer.rb +48 -0
  146. data/lib/textus/write/mv.rb +123 -0
  147. data/lib/textus/write/publish.rb +66 -0
  148. data/lib/textus/write/put.rb +59 -0
  149. data/lib/textus/write/refresh_all.rb +44 -0
  150. data/lib/textus/write/refresh_orchestrator.rb +102 -0
  151. data/lib/textus/write/refresh_worker.rb +138 -0
  152. data/lib/textus/write/reject.rb +54 -0
  153. data/lib/textus.rb +7 -1
  154. metadata +75 -46
  155. data/lib/textus/application/context.rb +0 -34
  156. data/lib/textus/application/projection.rb +0 -91
  157. data/lib/textus/application/reads/audit.rb +0 -94
  158. data/lib/textus/application/reads/blame.rb +0 -82
  159. data/lib/textus/application/reads/deps.rb +0 -26
  160. data/lib/textus/application/reads/freshness.rb +0 -88
  161. data/lib/textus/application/reads/get.rb +0 -67
  162. data/lib/textus/application/reads/get_or_refresh.rb +0 -51
  163. data/lib/textus/application/reads/list.rb +0 -17
  164. data/lib/textus/application/reads/policy_explain.rb +0 -39
  165. data/lib/textus/application/reads/published.rb +0 -17
  166. data/lib/textus/application/reads/pulse.rb +0 -63
  167. data/lib/textus/application/reads/rdeps.rb +0 -27
  168. data/lib/textus/application/reads/schema_envelope.rb +0 -18
  169. data/lib/textus/application/reads/stale.rb +0 -15
  170. data/lib/textus/application/reads/uid.rb +0 -23
  171. data/lib/textus/application/reads/validate_all.rb +0 -24
  172. data/lib/textus/application/reads/validator.rb +0 -86
  173. data/lib/textus/application/reads/where.rb +0 -18
  174. data/lib/textus/application/refresh/all.rb +0 -52
  175. data/lib/textus/application/refresh/orchestrator.rb +0 -78
  176. data/lib/textus/application/refresh/worker.rb +0 -116
  177. data/lib/textus/application/writes/accept.rb +0 -89
  178. data/lib/textus/application/writes/authority_gate.rb +0 -26
  179. data/lib/textus/application/writes/delete.rb +0 -33
  180. data/lib/textus/application/writes/materializer.rb +0 -50
  181. data/lib/textus/application/writes/mv.rb +0 -105
  182. data/lib/textus/application/writes/publish.rb +0 -81
  183. data/lib/textus/application/writes/put.rb +0 -37
  184. data/lib/textus/application/writes/reject.rb +0 -50
  185. data/lib/textus/infra/event_bus.rb +0 -27
  186. data/lib/textus/operations.rb +0 -176
data/SPEC.md CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  ## 1. What textus is
12
12
 
13
- A storage convention and JSON wire protocol that lets humans, agents, and runners read and write structured project memory **deterministically**, with addressable dotted keys, schema validation, role-based write gates, declarative compute, and copy-based publish targets.
13
+ A storage convention and JSON wire protocol for humans, agents, and runners to read and write structured project memory **deterministically**. It provides addressable dotted keys, schema validation, role-based write gates, declarative compute, and copy-based publish targets.
14
14
 
15
15
  The storage lives in a `.textus/` directory at the project root. Each entry is a Markdown file with YAML frontmatter. A manifest binds dotted keys to subtrees and declares which roles may write to each zone. Schemas (also YAML) define what frontmatter shape each entry must have. Derived entries are computed from other entries via pure projections and a vendored Mustache template engine, then optionally published to repo-relative paths as byte-for-byte file copies. The CLI surface (`textus get/put/list/where/schema/build/...` `--output=json`) returns a versioned envelope any caller can parse without knowing Markdown.
16
16
 
@@ -68,10 +68,10 @@ The root is `.textus/` at the project working directory. A typical tree:
68
68
  .textus/
69
69
  manifest.yaml # internal: key → subtree mapping + zones declarations
70
70
  audit.log # internal, append-only NDJSON log of every successful write
71
- role # internal, role token (one line, e.g. "human")
72
71
  schemas/ # internal: YAML schema files
73
72
  templates/ # internal: Mustache templates referenced by derived entries
74
- parsers/ # internal: project-local parser extensions
73
+ hooks/ # internal: one Ruby file per hook
74
+ sentinels/ # internal: bookkeeping for byte-copied publish targets (see §5.3)
75
75
  zones/ # ALL user content lives here
76
76
  identity/ # zone: identity (human-only)
77
77
  working/ # zone: working (human, agent, runner)
@@ -80,7 +80,7 @@ The root is `.textus/` at the project working directory. A typical tree:
80
80
  output/ # zone: output (builder only — computed outputs)
81
81
  ```
82
82
 
83
- Textus internals (`manifest.yaml`, `audit.log`, `role`, `schemas/`, `templates/`, `parsers/`) live directly under `.textus/`. **All user content lives under `.textus/zones/`.** Manifest `path:` fields are relative to `.textus/zones/` — they do **not** include the `zones/` prefix. Implementations MUST prepend `zones/` to every `path:` when resolving a key to a filesystem location.
83
+ Textus internals (`manifest.yaml`, `audit.log`, `schemas/`, `templates/`, `hooks/`, `sentinels/`) live directly under `.textus/`. **All user content lives under `.textus/zones/`.** Manifest `path:` fields are relative to `.textus/zones/` — they do **not** include the `zones/` prefix. Implementations MUST prepend `zones/` to every `path:` when resolving a key to a filesystem location.
84
84
 
85
85
  Zone directories under `zones/` are conventional; their write semantics are declared in the manifest, not the directory name.
86
86
 
@@ -138,6 +138,10 @@ entries:
138
138
  rules:
139
139
  - match: intake.**
140
140
  refresh: { ttl: 6h, on_stale: warn }
141
+
142
+ audit:
143
+ max_size: 10485760 # bytes before rotating (default: 10 485 760 = 10 MiB)
144
+ keep: 5 # rotated files to retain (default: 5)
141
145
  ```
142
146
 
143
147
  Zone names are conventional — the manifest is the source of truth for write permissions; rename freely.
@@ -397,7 +401,7 @@ In intake mode the handler MUST return one of three shapes, all normalized by th
397
401
 
398
402
  **Refresh paths.** Two are supported:
399
403
 
400
- 1. **In-process** — `textus refresh KEY --as=runner` resolves the entry's `intake.handler`, invokes the registered `:resolve_intake` hook with `(config:, store:, args: {})`, and writes the result under role `runner`.
404
+ 1. **In-process** — `textus refresh KEY --as=runner` resolves the entry's `intake.handler`, invokes the registered `:resolve_intake` hook with `(caps:, config:, args: {})`, and writes the result under role `runner`.
401
405
  2. **External runner** — a cron job or agent harness reads `textus list --zone=intake --stale --output=json`, fetches the source out of band, and pipes bytes back through `textus put KEY --as=runner --stdin`. The CLI verb `textus refresh stale [--prefix=K] [--zone=Z]` drives this loop in one shot.
402
406
 
403
407
  Both paths share the same role gate, audit-log entry, and `:entry_refreshed` event. User-supplied hooks live in `.textus/hooks/**/*.rb` and auto-load at `Store#initialize` — see §5.10 for the full hook contract.
@@ -439,6 +443,35 @@ Schema (one JSON object per line, no interior whitespace):
439
443
 
440
444
  For `mv`, the structural fields `from_key`, `to_key`, and `uid` appear at the top level of the JSON object. Remaining verb-specific data (e.g. `from_path`, `to_path`) is nested under an `extras` key. The `extras` key is omitted entirely when empty.
441
445
 
446
+ **Rotation.** After every successful append the implementation checks whether `audit.log` exceeds `max_size` bytes (checked inside the held `flock`, so the check sees the post-write size). If it does, the active log is rotated:
447
+
448
+ 1. The seq range (`min_seq`, `max_seq`) of the active log is scanned, and a JSON sidecar (`audit.log.1.meta.json`) is written with those values plus a `rotated_at` ISO 8601 timestamp.
449
+ 2. Existing rotated files are shifted: `audit.log.(N)` → `audit.log.(N+1)` for N = `keep-1` down to 1 (with their `.meta.json` sidecars).
450
+ 3. `audit.log` is renamed to `audit.log.1`.
451
+ 4. The file that would be shifted to `audit.log.(keep+1)` — i.e., `audit.log.keep` and its sidecar — is deleted before the shift.
452
+ 5. The next append creates a fresh `audit.log` via `O_CREAT`. Seq numbering continues from the previous maximum; there is no reset.
453
+
454
+ Rotation is triggered by **byte size only** — there is no row-count or time-based trigger.
455
+
456
+ **Rotation knobs** (configured via the optional `audit:` block in `manifest.yaml`):
457
+
458
+ | Key | Default | Meaning |
459
+ |------------|--------------|---------|
460
+ | `max_size` | `10485760` | Maximum size of `audit.log` in bytes (10 MiB) before rotation is triggered. |
461
+ | `keep` | `5` | Number of rotated files retained on disk. When this limit is exceeded the oldest rotated file and its sidecar are deleted. |
462
+
463
+ Both keys are optional. Omitting `audit:` entirely uses the defaults above.
464
+
465
+ **`CursorExpired`.** When `audit --seq-since=N` or `pulse --since=N` is called with a cursor `N`, the implementation checks whether `N` is below the oldest sequence number still available on disk (`min_available_seq`, derived from the oldest retained rotated file's sidecar). The condition that raises `CursorExpired` is:
466
+
467
+ ```
468
+ N < min_available_seq - 1
469
+ ```
470
+
471
+ The error includes `requested` (the supplied cursor value) and `min_available` (the oldest seq still on disk).
472
+
473
+ **Recommended caller behavior on `CursorExpired`.** Call `textus boot` (without `--since`) to obtain a fresh `latest_seq` from the current audit log state, then resume `pulse` calls using that new cursor. Do not attempt to replay from an expired cursor — the intervening rows are gone.
474
+
442
475
  ### 5.7 Security bounds
443
476
 
444
477
  textus enforces fixed bounds to keep behavior predictable under hostile or buggy input:
@@ -478,7 +511,7 @@ evolution:
478
511
 
479
512
  **Defaults:** when `fields:` and `evolution:` are absent, `schema.maintained_by(field)` returns `nil` for every field and `schema.evolution` returns `{}`.
480
513
 
481
- **Override rule:** the role `human` is permitted to write any `maintained_by` field, regardless of declared owner. This preserves human authority over agent/runner-managed data humans curating canon over agent-written embeddings is a feature, not a bug. All other role mismatches are reported by `doctor --check=schema_violations` with code `role_authority`, including fields `key`, `field`, `expected`, and `last_writer`.
514
+ **Override rule:** the role `human` is permitted to write any `maintained_by` field, regardless of declared owner. Humans override agent-maintained fields by design: schema field ownership (`maintained_by:`) makes the boundary explicit, not implicit. All other role mismatches are reported by `doctor --check=schema_violations` with code `role_authority`, including fields `key`, `field`, `expected`, and `last_writer`.
482
515
 
483
516
  ### 5.9 Row transforms
484
517
 
@@ -495,11 +528,11 @@ The subdirectory layout under `hooks/` is organizational only; the registered ev
495
528
  ```ruby
496
529
  # Canonical form — works for every event:
497
530
  Textus.hook do |reg|
498
- reg.on(:resolve_intake, :my_source) { |config:, args:, **| … }
499
- reg.on(:transform_rows, :rank_by_recency) { |rows:, **| … }
500
- reg.on(:validate, :storage_writable) { |store:| … }
501
- reg.on(:entry_put, :audit, keys: ["working.*"]) { |key:, envelope:, **| … }
502
- reg.on(:file_published, :git_add, keys: ["derived.*"]) { |target:, **| `git add #{target.shellescape}` }
531
+ reg.on(:resolve_intake, :my_source) { |caps:, config:, args:, **| … }
532
+ reg.on(:transform_rows, :rank_by_recency) { |caps:, rows:, **| … }
533
+ reg.on(:validate, :storage_writable) { |caps:| … }
534
+ reg.on(:entry_put, :audit, keys: ["working.*"]) { |ctx:, key:, envelope:, **| … }
535
+ reg.on(:file_published, :git_add, keys: ["derived.*"]) { |ctx:, target:, **| `git add #{target.shellescape}` }
503
536
  end
504
537
  ```
505
538
 
@@ -509,21 +542,21 @@ end
509
542
 
510
543
  | Event | Mode | Args | Return | Failure |
511
544
  |-------------------------|---------|-----------------------------------------------------------|-----------------------|---------------|
512
- | `:resolve_intake` | rpc | store:, config:, args: | {_meta:, body:} | aborts op |
513
- | `:transform_rows` | rpc | store:, rows:, config: | rows array | aborts op |
514
- | `:validate` | rpc | store: | issues array | aborts doctor |
515
- | `:entry_put` | pubsub | store:, key:, envelope: | (discarded) | logged |
516
- | `:entry_deleted` | pubsub | store:, key: | (discarded) | logged |
517
- | `:entry_refreshed` | pubsub | store:, key:, envelope:, change: | (discarded) | logged |
518
- | `:build_completed` | pubsub | store:, key:, envelope:, sources: | (discarded) | logged |
519
- | `:proposal_accepted` | pubsub | store:, key:, target_key: | (discarded) | logged |
520
- | `:file_published` | pubsub | store:, key:, envelope:, source:, target: | (discarded) | logged |
521
- | `:entry_renamed` | pubsub | store:, key:, from_key:, to_key:, envelope: | (discarded) | logged |
522
- | `:proposal_rejected` | pubsub | store:, key:, target_key: | (discarded) | logged |
523
- | `:store_loaded` | pubsub | store: | (discarded) | logged |
524
- | `:refresh_started` | pubsub | store:, key:, mode: | (discarded) | logged |
525
- | `:refresh_failed` | pubsub | store:, key:, error_class:, error_message: | (discarded) | logged |
526
- | `:refresh_backgrounded` | pubsub | store:, key:, started_at:, budget_ms: | (discarded) | logged |
545
+ | `:resolve_intake` | rpc | caps:, config:, args: | {_meta:, body:} | aborts op |
546
+ | `:transform_rows` | rpc | caps:, rows:, config: | rows array | aborts op |
547
+ | `:validate` | rpc | caps: | issues array | aborts doctor |
548
+ | `:entry_put` | pubsub | ctx:, key:, envelope: | (discarded) | logged |
549
+ | `:entry_deleted` | pubsub | ctx:, key: | (discarded) | logged |
550
+ | `:entry_refreshed` | pubsub | ctx:, key:, envelope:, change: | (discarded) | logged |
551
+ | `:build_completed` | pubsub | ctx:, key:, envelope:, sources: | (discarded) | logged |
552
+ | `:proposal_accepted` | pubsub | ctx:, key:, target_key: | (discarded) | logged |
553
+ | `:file_published` | pubsub | ctx:, key:, envelope:, source:, target: | (discarded) | logged |
554
+ | `:entry_renamed` | pubsub | ctx:, key:, from_key:, to_key:, envelope: | (discarded) | logged |
555
+ | `:proposal_rejected` | pubsub | ctx:, key:, target_key: | (discarded) | logged |
556
+ | `:store_loaded` | pubsub | ctx: | (discarded) | logged |
557
+ | `:refresh_started` | pubsub | ctx:, key:, mode: | (discarded) | logged |
558
+ | `:refresh_failed` | pubsub | ctx:, key:, error_class:, error_message: | (discarded) | logged |
559
+ | `:refresh_backgrounded` | pubsub | ctx:, key:, started_at:, budget_ms: | (discarded) | logged |
527
560
 
528
561
  The three `:refresh_*` lifecycle events report the progress and failures of background (timed_sync) refreshes.
529
562
 
@@ -533,14 +566,19 @@ The three `:refresh_*` lifecycle events report the progress and failures of back
533
566
 
534
567
  **`:refresh_backgrounded`** fires when a `timed_sync` refresh exceeds its budget and is handed off to a background thread. `started_at:` is an ISO-8601 UTC string; `budget_ms:` is the configured deadline as an integer.
535
568
 
536
- **Signature invariant** — every hook receives `store:` as its first keyword argument. Event-specific kwargs follow in stable left-to-right order. The primary entity is always `key:` (for `:proposal_accepted`, `key:` is the pending key being accepted and `target_key:` is the destination). For `:entry_renamed`, `key:` is present and equals `to_key:` — it is the entry's post-move home, present so `keys:` glob filters route correctly; `from_key:` is the prior key. For `:proposal_rejected`, `key:` is the pending key being rejected. For `:store_loaded`, no key — the event observes store readiness, not an entry.
569
+ **Signature invariant** — hooks receive a capability handle as their first keyword argument; the name depends on the mode:
570
+
571
+ - **RPC hooks** (`rpc` mode) receive `caps:` — a `Textus::Container`. Event-specific kwargs (`config:`, `args:`, `rows:`) follow in the stable order shown in the table above.
572
+ - **Pub-sub hooks** (`pubsub` mode) receive `ctx:` — a `Textus::Hooks::Context` that exposes a narrow surface: `get`, `list`, `deps`, `freshness` (reads), `put`, `delete`, `audit` (authorized writes), `publish_followup`, plus `role` and `correlation_id`. The raw `Store` is not handed out.
573
+
574
+ Declaring `store:` instead of `caps:` in an RPC callable will pass registration but raise `UsageError` at call time (`Hooks::RpcRegistry#invoke` rejects `store:` — there is no shim).
575
+
576
+ The primary entity is always `key:` (for `:proposal_accepted`, `key:` is the pending key being accepted and `target_key:` is the destination). For `:entry_renamed`, `key:` is present and equals `to_key:` — it is the entry's post-move home, present so `keys:` glob filters route correctly; `from_key:` is the prior key. For `:proposal_rejected`, `key:` is the pending key being rejected. For `:store_loaded`, no key — the event observes store readiness, not an entry.
537
577
 
538
578
  **RPC mode** — exactly one handler per (event, name). The manifest references the handler by name (`intake.handler: NAME`, `compute.transform: NAME`). Failure or timeout aborts the calling operation.
539
579
 
540
580
  **Pub-sub mode** — zero or more handlers per event. All matching handlers fire. The `keys:` option restricts a handler to keys matching one of the given globs (`File.fnmatch?` rules). Absence of `keys:` fires on every event of that type. Handler failures and 2s timeouts are logged to `audit.log` as `event_error` rows; they NEVER abort the triggering operation.
541
581
 
542
- The `store:` argument is always a read-only store proxy. Write attempts raise `UsageError`.
543
-
544
582
  Each handler runs under `Timeout.timeout(2)`.
545
583
 
546
584
  ### 5.11 Rules
@@ -888,10 +926,10 @@ Given a review entry `review.identity.self.patch` proposing a change to `identit
888
926
 
889
927
  Textus internals are organized into four layers. The dependency rule is one-way — each layer may only import from the layer beneath it.
890
928
 
891
- - **Interface** (`lib/textus/cli/`) — CLI verbs. Parses flags, calls a use case, formats JSON.
892
- - **Application** (`lib/textus/application/`) — Use cases: `Reads::Get`, `Refresh::Worker`, `Refresh::Orchestrator`, `Refresh::All`. Orchestrate domain + infra; no business rules.
893
- - **Domain** (`lib/textus/domain/`) — Pure values: `Freshness::Policy`, `Action`, `Outcome`, `Freshness::Verdict`, `Freshness::Evaluator`. No I/O, no globals, testable without disk.
894
- - **Infrastructure** (`lib/textus/infra/`) — Adapters: `EventBus`, `Clock`, `Refresh::Lock`, `Refresh::Detached`. Wrap OS / library primitives.
929
+ - **Interface** (`lib/textus/cli/`, `lib/textus/mcp/`) — CLI verbs and the MCP gate. Parses flags / RPC, calls a use case, formats JSON.
930
+ - **Application** (`lib/textus/application/`) — Use cases: `Read::Get`, `Write::Put`, `Write::RefreshWorker`, `Write::RefreshOrchestrator`, `Write::RefreshAll`, `Maintenance::Migrate`, etc. Orchestrate domain + infra; no business rules.
931
+ - **Domain** (`lib/textus/domain/`) — Pure values: `Authorizer`, `Permission`, `Freshness::{Policy,Verdict,Evaluator}`, `Action`, `Outcome`, `Sentinel`, `Staleness`. No I/O, no globals, testable without disk.
932
+ - **Infrastructure** (`lib/textus/infra/`) — Adapters: `Storage::FileStore`, `AuditLog`, `AuditSubscriber`, `Publisher`, `Clock`, `Refresh::Lock`, `Refresh::Detached`, `BuildLock`. Wrap OS / library primitives.
895
933
 
896
934
  The `lib/textus/store/`, `lib/textus/manifest/`, `lib/textus/hooks/` namespaces are infrastructure adapters that predate this split and remain at their existing paths for backward-compat with the plugin DSL.
897
935
 
@@ -899,14 +937,13 @@ Plugin authors interact only with the Hook DSL (`Textus.hook { |reg| reg.on(:res
899
937
 
900
938
  Both read and write paths flow through the application layer:
901
939
 
902
- - **Reads** flow through `Application::Reads::Get`, which takes a `Context` and dispatches refresh via `Application::Refresh::Orchestrator`.
903
- - **Writes** flow through `Application::Writes::{Put,Delete,Build,Accept,Publish}`, each taking a `Context`. Permission checks happen at the use-case layer (via `Context#can_write?`); I/O happens at `Store::Writer#write_envelope_to_disk` (pure).
904
- - `Application::Context` is the universal request object: it carries `store`, `role`, `correlation_id`, `clock`, and `dry_run`. Use cases never thread these as separate kwargs.
905
- - `Textus::Operations` is the factory CLI verbs (and future MCP server / HTTP shim)
906
- use to construct Contexts and use cases. `Operations.for(store, role:)` returns
907
- a flat facade exposing one method per use case (`#put`, `#get`, `#refresh`, …);
908
- internal use-case instances are memoized via `||=` and live under
909
- `lib/textus/application/{reads,writes,refresh}/`.
940
+ - **Reads** flow through `Textus::Read::Get` (pure read + freshness annotation) or `Read::GetOrRefresh` (composes Get with `Write::RefreshOrchestrator`). Each takes a `container:` and a `call:`.
941
+ - **Writes** flow through `Textus::Write::{Put,Delete,Mv,Accept,Reject,Publish,RefreshWorker}`. Permission checks happen at the use-case layer (via `Domain::Authorizer#authorize_write!`); the audit-append invariant lives in `Textus::Envelope::IO::Writer`.
942
+ - `Textus::Call` is the slim per-invocation record: `role`, `correlation_id`, `now`, `dry_run`. Ports come from `Textus::Container`, not from the Call.
943
+ - `Textus::Store` is the composition root and verb dispatcher. CLI verbs and the
944
+ MCP gate call `store.<verb>(..., role:)` (or `store.as(role).<verb>(...)`).
945
+ Verbs are looked up in the static `Textus::Dispatcher::VERBS` table; adding a
946
+ use case is a single entry in `VERBS` plus the class.
910
947
 
911
948
  See `ARCHITECTURE.md` for an ASCII diagram and the full read-path walkthrough.
912
949
 
@@ -914,7 +951,7 @@ See `ARCHITECTURE.md` for an ASCII diagram and the full read-path walkthrough.
914
951
 
915
952
  - **Locking on `put`:** the reference impl uses sha256 etags. Should the spec also define a file-lock fallback for systems where read-before-write is racy?
916
953
  - **Schema imports:** can one schema reference another (`type: $ref: person`)?
917
- - **Internationalization:** non-ASCII in keys? Spec currently restricts segments to `[a-z0-9_-]`. Revisit if community wants Unicode.
954
+ - **Internationalization:** non-ASCII in keys? Spec currently restricts segments to `[a-z0-9][a-z0-9-]*`. Revisit if community wants Unicode.
918
955
  - **Generated content in `derived/`:** the spec says `schema: null` is allowed, but should there be a separate marker (`generated: true`) for clarity?
919
956
 
920
957
  ## 15. Implementation checklist
data/docs/conventions.md CHANGED
@@ -126,6 +126,16 @@ Build always uses the pure path; injecting refresh into materialization caused t
126
126
 
127
127
  For multi-writer environments, **always pass `if_etag`** on `put`. The gem treats etag-less writes as last-writer-wins on purpose (single-writer scripts, fresh-file creation), but anything resembling a daemon or a long-running agent should round-trip the etag.
128
128
 
129
+ ## Application layering
130
+
131
+ The application layer is organised around three shapes — `Manifest` as a composition record, a single `Container` capability record handed to every use case, and a split envelope reader/writer. See [ADR 0018](architecture/decisions/0018-manifest-carving.md), [ADR 0017](architecture/decisions/0017-envelope-io-split.md), [ADR 0022](architecture/decisions/0022-container-call-dispatcher.md), and [ADR 0023](architecture/decisions/0023-uniform-use-case-shape.md).
132
+
133
+ - **`Manifest` is a composition record** (`Data.define(:data, :resolver, :policy, :rules)`). Reach individual concerns through the field accessors: `manifest.data.entries`, `manifest.policy.permission_for(zone)`, `manifest.resolver.resolve(key)`, `manifest.rules.for(key)`.
134
+ - **Use cases are plain `(container:, call:)` classes.** Each is a single class under `lib/textus/{read,write,maintenance}/` with `def initialize(container:, call:)` and a `#call(...)` method; verbs are looked up in the static `Textus::Dispatcher::VERBS` table. `Container` is a `Data.define` record bundling the wired ports + manifest (`manifest`, `file_store`, `schemas`, `root`, `audit_log`, `events`, `rpc`, `authorizer`); `Call` is the immutable per-invocation value (`role`, `correlation_id`, `now`, `dry_run`). A use case that emits events derives its `Hooks::Context` from `(container, call)` — nothing is injected. Use cases pull only what they need into ivars; nobody passes the raw `Store` around the application layer. `store.as(role)` returns a `RoleScope` that forwards verbs to the dispatcher.
135
+ - **Write path is split**: `Envelope::IO::Reader` owns read/parse (existing-uid lookup, raw read, parse), and `Envelope::IO::Writer` owns put/delete/move + the audit-append invariant (every public method's final action is `@audit_log.append(...)`).
136
+
137
+ The user-facing CLI surface, the wire envelope shape, and the protocol version (`textus/3`) are unchanged.
138
+
129
139
  ## Pairing with other tools
130
140
 
131
141
  - **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.
data/lib/textus/boot.rb CHANGED
@@ -26,7 +26,7 @@ module Textus
26
26
  "edit files in identity/working zones, then 'textus put KEY --as=#{name}'"
27
27
  end,
28
28
  proposer: lambda do |name, manifest|
29
- authority = manifest.roles_with_kind(:accept_authority).first || "accept_authority"
29
+ authority = manifest.policy.roles_with_kind(:accept_authority).first || "accept_authority"
30
30
  "propose changes by writing review.* entries with --as=#{name} and a 'proposal:' frontmatter block; " \
31
31
  "the #{authority} role runs 'textus accept' to apply"
32
32
  end,
@@ -39,7 +39,7 @@ module Textus
39
39
  }.freeze
40
40
 
41
41
  def self.write_flows_for(manifest)
42
- manifest.role_mapping.each_with_object({}) do |(name, kind), acc|
42
+ manifest.policy.role_mapping.each_with_object({}) do |(name, kind), acc|
43
43
  tmpl = WRITE_FLOW_TEMPLATES[kind]
44
44
  acc[name] = tmpl.call(name, manifest) if tmpl
45
45
  end
@@ -120,11 +120,11 @@ module Textus
120
120
  "summary" => "delta since cursor — changed entries, stale, pending review, doctor summary" },
121
121
  ].freeze
122
122
 
123
- def self.agent_quickstart(manifest, store)
124
- proposer_roles = manifest.roles_with_kind(:proposer)
123
+ def self.agent_quickstart(manifest, audit_log)
124
+ proposer_roles = manifest.policy.roles_with_kind(:proposer)
125
125
  agent_role = proposer_roles.first
126
126
 
127
- writable_zones = manifest.zones.each_with_object([]) do |(zname, writers), acc|
127
+ writable_zones = manifest.data.zones.each_with_object([]) do |(zname, writers), acc|
128
128
  acc << zname if agent_role && writers.include?(agent_role)
129
129
  end
130
130
 
@@ -135,7 +135,7 @@ module Textus
135
135
  "write_verbs" => agent_role ? ["put KEY --as=#{agent_role} --stdin"] : [],
136
136
  "writable_zones" => writable_zones,
137
137
  "propose_zone" => propose_zone,
138
- "latest_seq" => store.audit_log.latest_seq,
138
+ "latest_seq" => audit_log.latest_seq,
139
139
  }
140
140
  end
141
141
 
@@ -144,29 +144,30 @@ module Textus
144
144
  "role_resolution" => {
145
145
  "summary" => "write role is resolved in order: --as flag, TEXTUS_ROLE env var, .textus/role file, " \
146
146
  "default 'human'",
147
- "roles" => manifest.role_mapping.keys,
147
+ "roles" => manifest.policy.role_mapping.keys,
148
148
  "ref" => "SPEC.md §5",
149
149
  },
150
150
  )
151
151
  end
152
152
 
153
- def self.run(store)
153
+ def self.build(container:)
154
+ manifest = container.manifest
154
155
  {
155
156
  "protocol" => PROTOCOL_ID,
156
- "store_root" => store.root,
157
- "zones" => zones_for(store),
158
- "entries" => entries_for(store),
159
- "hooks" => hooks_for(store),
160
- "write_flows" => write_flows_for(store.manifest),
157
+ "store_root" => container.root,
158
+ "zones" => zones_for(manifest),
159
+ "entries" => entries_for(manifest),
160
+ "hooks" => hooks_for_container(container),
161
+ "write_flows" => write_flows_for(manifest),
161
162
  "cli_verbs" => CLI_VERBS.map(&:dup),
162
- "agent_protocol" => agent_protocol(store.manifest),
163
- "agent_quickstart" => agent_quickstart(store.manifest, store),
163
+ "agent_protocol" => agent_protocol(manifest),
164
+ "agent_quickstart" => agent_quickstart(manifest, container.audit_log),
164
165
  "docs" => { "spec" => "SPEC.md", "example" => "examples/claude-plugin/" },
165
166
  }
166
167
  end
167
168
 
168
- def self.zones_for(store)
169
- store.manifest.zones.map do |name, writers|
169
+ def self.zones_for(manifest)
170
+ manifest.data.zones.map do |name, writers|
170
171
  row = { "name" => name, "writers" => Array(writers) }
171
172
  purpose = ZONE_PURPOSES[name]
172
173
  row["purpose"] = purpose if purpose
@@ -174,9 +175,9 @@ module Textus
174
175
  end
175
176
  end
176
177
 
177
- def self.entries_for(store)
178
- store.manifest.entries.map do |e|
179
- derived = store.manifest.zone_kinds(e.zone).include?(:generator)
178
+ def self.entries_for(manifest)
179
+ manifest.data.entries.map do |e|
180
+ derived = manifest.policy.zone_kinds(e.zone).include?(:generator)
180
181
  {
181
182
  "key" => e.key,
182
183
  "zone" => e.zone,
@@ -192,16 +193,17 @@ module Textus
192
193
  end
193
194
  end
194
195
 
195
- def self.hooks_for(store)
196
- bus = store.bus
196
+ def self.hooks_for_container(container)
197
+ hooks_for_container_internal(rpc: container.rpc, events: container.events)
198
+ end
199
+
200
+ def self.hooks_for_container_internal(rpc:, events:)
197
201
  sections = {}
198
- Hooks::Bus::EVENTS.each do |event, spec|
199
- case spec[:mode]
200
- when :rpc
201
- sections[event.to_s] = bus.rpc_names(event).map(&:to_s).sort
202
- when :pubsub
203
- sections[event.to_s] = bus.pubsub_handlers(event).map { |h| h[:name].to_s }.sort
204
- end
202
+ Hooks::RpcRegistry::EVENTS.each_key do |event|
203
+ sections[event.to_s] = rpc.names(event).map(&:to_s).sort
204
+ end
205
+ Hooks::EventBus::EVENTS.each_key do |event|
206
+ sections[event.to_s] = events.pubsub_handlers(event).map { |h| h[:name].to_s }.sort
205
207
  end
206
208
  sections
207
209
  end
@@ -53,6 +53,10 @@ module Textus
53
53
  end
54
54
 
55
55
  module Pipeline
56
+ Deps = Data.define(
57
+ :manifest, :reader, :lister, :rpc, :template_loader, :transform_context, :inject_boot
58
+ )
59
+
56
60
  def self.renderers
57
61
  @renderers ||= {
58
62
  "markdown" => Renderer::Markdown,
@@ -62,37 +66,34 @@ module Textus
62
66
  }
63
67
  end
64
68
 
65
- # rubocop:disable Metrics/ParameterLists
66
- def self.run(mentry:, manifest:, reader:, lister:, transform_resolver:, template_loader:,
67
- transform_context: nil, inject_boot: nil)
69
+ def self.run(mentry:, deps:)
68
70
  # 1. Load sources + project + reduce
69
71
  data =
70
72
  if mentry.is_a?(Textus::Manifest::Entry::Derived) && mentry.projection?
71
- Application::Projection.new(
72
- reader: reader,
73
+ Textus::Projection.new(
74
+ reader: deps.reader,
73
75
  spec: mentry.source.to_h.transform_keys(&:to_s),
74
- lister: lister,
75
- transform_resolver: transform_resolver,
76
- transform_context: transform_context,
76
+ lister: deps.lister,
77
+ rpc: deps.rpc,
78
+ transform_context: deps.transform_context,
77
79
  ).run
78
80
  else
79
81
  { "entries" => [], "count" => 0, "generated_at" => Time.now.utc.iso8601 }
80
82
  end
81
- data = data.merge("boot" => inject_boot.call) if mentry.inject_boot && inject_boot
83
+ data = data.merge("boot" => deps.inject_boot.call) if mentry.inject_boot && deps.inject_boot
82
84
 
83
85
  # 2. Render
84
86
  klass = renderers[mentry.format] or
85
87
  raise UsageError.new("builder: unsupported format #{mentry.format.inspect} for '#{mentry.key}'")
86
- bytes = klass.new(template_loader: template_loader).call(mentry: mentry, data: data)
88
+ bytes = klass.new(template_loader: deps.template_loader).call(mentry: mentry, data: data)
87
89
 
88
90
  # 3. Write (idempotent: skip if only generated_at would differ)
89
- target_path = Key::Path.resolve(manifest, mentry)
91
+ target_path = Key::Path.resolve(deps.manifest.data, mentry)
90
92
  FileUtils.mkdir_p(File.dirname(target_path))
91
93
  write_if_changed(target_path, bytes, mentry.format)
92
94
 
93
95
  target_path
94
96
  end
95
- # rubocop:enable Metrics/ParameterLists
96
97
 
97
98
  def self.write_if_changed(target_path, bytes, format)
98
99
  if File.exist?(target_path)
@@ -0,0 +1,28 @@
1
+ require "securerandom"
2
+
3
+ module Textus
4
+ # Immutable per-invocation value. Carries who is acting (role), the
5
+ # request correlation id, the wall clock, and the dry_run flag — the
6
+ # bits Use Cases need that are not part of the Container.
7
+ Call = Data.define(:role, :correlation_id, :now, :dry_run) do
8
+ def self.build(role:, correlation_id: nil, now: nil, dry_run: false)
9
+ new(
10
+ role: role.to_s,
11
+ correlation_id: correlation_id || SecureRandom.uuid,
12
+ now: now || Textus::Ports::Clock.now,
13
+ dry_run: dry_run,
14
+ )
15
+ end
16
+
17
+ def dry_run? = dry_run
18
+
19
+ def with_role(new_role)
20
+ self.class.new(
21
+ role: new_role.to_s,
22
+ correlation_id: correlation_id,
23
+ now: now,
24
+ dry_run: dry_run,
25
+ )
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,9 @@
1
+ module Textus
2
+ class CLI
3
+ class Group
4
+ class MCP < Group
5
+ command_name "mcp"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Textus
2
+ class CLI
3
+ class Group
4
+ class Zone < Group
5
+ command_name "zone"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -8,7 +8,7 @@ module Textus
8
8
 
9
9
  def call(store)
10
10
  key = positional.shift or raise UsageError.new("accept requires a key")
11
- emit(operations_for(store).accept(key))
11
+ emit(session_for(store).accept(key))
12
12
  end
13
13
  end
14
14
  end
@@ -14,8 +14,8 @@ module Textus
14
14
  option :limit, "--limit=N"
15
15
 
16
16
  def call(store)
17
- ops = operations_for(store)
18
- since_time = since && Textus::Application::Reads::Audit.parse_since(since, now: ops.ctx.now)
17
+ ops = session_for(store)
18
+ since_time = since && Textus::Read::Audit.parse_since(since, now: Time.now)
19
19
  rows = ops.audit(
20
20
  key: key_filter,
21
21
  zone: zone,
@@ -8,7 +8,7 @@ module Textus
8
8
 
9
9
  def call(store)
10
10
  key = positional.shift or raise UsageError.new("blame requires a key")
11
- rows = operations_for(store).blame(key: key, limit: limit&.to_i)
11
+ rows = session_for(store).blame(key: key, limit: limit&.to_i)
12
12
  emit({ "verb" => "blame", "key" => key, "rows" => rows })
13
13
  end
14
14
  end
@@ -5,7 +5,7 @@ module Textus
5
5
  command_name "boot"
6
6
 
7
7
  def call(store)
8
- emit(Textus::Boot.run(store))
8
+ emit(store.boot)
9
9
  end
10
10
  end
11
11
  end
@@ -7,9 +7,9 @@ module Textus
7
7
  option :prefix, "--prefix=K"
8
8
 
9
9
  def call(store)
10
- Textus::Infra::BuildLock.with(root: store.root) do
11
- role = store.manifest.roles_with_kind(:generator).first || "builder"
12
- ops = Textus::Operations.for(store, role: role)
10
+ Textus::Ports::BuildLock.with(root: store.root) do
11
+ role = store.manifest.policy.roles_with_kind(:generator).first || "builder"
12
+ ops = store.as(role)
13
13
  result = ops.publish(prefix: prefix)
14
14
  emit(result)
15
15
  end
@@ -9,7 +9,7 @@ module Textus
9
9
 
10
10
  def call(store)
11
11
  key = positional.shift or raise UsageError.new("delete requires a key")
12
- emit(operations_for(store).delete(key, if_etag: if_etag))
12
+ emit(session_for(store).delete(key, if_etag: if_etag))
13
13
  end
14
14
  end
15
15
  end
@@ -6,7 +6,7 @@ module Textus
6
6
 
7
7
  def call(store)
8
8
  key = positional.shift or raise UsageError.new("deps requires a key")
9
- emit({ "key" => key, "deps" => operations_for(store).deps(key) })
9
+ emit({ "key" => key, "deps" => session_for(store).deps(key) })
10
10
  end
11
11
  end
12
12
  end
@@ -8,7 +8,7 @@ module Textus
8
8
 
9
9
  def call(store)
10
10
  check_list = checks&.split(",")&.map(&:strip)
11
- res = Textus::Doctor.run(store, checks: check_list)
11
+ res = store.doctor(checks: check_list)
12
12
  emit(res, exit_code: res["ok"] ? 0 : 1)
13
13
  end
14
14
  end
@@ -8,7 +8,7 @@ module Textus
8
8
  option :zone, "--zone=Z"
9
9
 
10
10
  def call(store)
11
- rows = operations_for(store).freshness(prefix: prefix, zone: zone)
11
+ rows = session_for(store).freshness(prefix: prefix, zone: zone)
12
12
  emit({ "verb" => "freshness", "rows" => rows })
13
13
  end
14
14
  end
@@ -8,7 +8,7 @@ module Textus
8
8
 
9
9
  def call(store)
10
10
  key = positional.shift or raise UsageError.new("get requires a key")
11
- result = operations_for(store).get_or_refresh(key)
11
+ result = session_for(store).get_or_refresh(key)
12
12
  raise Textus::UnknownKey.new(key, suggestions: store.manifest.resolver.suggestions_for(key)) if result.nil?
13
13
 
14
14
  emit(result.to_h_for_wire)
@@ -27,15 +27,14 @@ module Textus
27
27
  end
28
28
 
29
29
  Role.resolve(flag: as_flag, env: ENV, root: store.root)
30
- callable = store.bus.rpc_callable(:resolve_intake, name)
31
30
 
32
31
  begin
33
- Timeout.timeout(Textus::Application::Refresh::Worker::FETCH_TIMEOUT_SECONDS) do
34
- callable.call(config: {}, store: store, args: args)
32
+ Timeout.timeout(Textus::Write::RefreshWorker::FETCH_TIMEOUT_SECONDS) do
33
+ store.rpc.invoke(:resolve_intake, name, caps: nil, config: {}, args: args)
35
34
  end
36
35
  rescue Timeout::Error
37
36
  raise UsageError.new(
38
- "hook run '#{name}' exceeded #{Textus::Application::Refresh::Worker::FETCH_TIMEOUT_SECONDS}s timeout",
37
+ "hook run '#{name}' exceeded #{Textus::Write::RefreshWorker::FETCH_TIMEOUT_SECONDS}s timeout",
39
38
  )
40
39
  rescue Textus::Error
41
40
  raise