textus 0.30.0 → 0.35.1

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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +2 -241
  3. data/CHANGELOG.md +113 -0
  4. data/README.md +83 -62
  5. data/SPEC.md +352 -211
  6. data/docs/conventions.md +42 -37
  7. data/lib/textus/boot.rb +89 -74
  8. data/lib/textus/cli/group/{refresh.rb → fetch.rb} +4 -4
  9. data/lib/textus/cli/verb/build.rb +1 -1
  10. data/lib/textus/cli/verb/fetch.rb +14 -0
  11. data/lib/textus/cli/verb/{refresh_stale.rb → fetch_stale.rb} +3 -3
  12. data/lib/textus/cli/verb/get.rb +1 -1
  13. data/lib/textus/cli/verb/hooks.rb +1 -1
  14. data/lib/textus/cli/verb/put.rb +1 -1
  15. data/lib/textus/cli/verb/rule_list.rb +7 -7
  16. data/lib/textus/cli.rb +2 -2
  17. data/lib/textus/container.rb +1 -2
  18. data/lib/textus/dispatcher.rb +3 -3
  19. data/lib/textus/doctor/check/{refresh_locks.rb → fetch_locks.rb} +7 -7
  20. data/lib/textus/doctor/check/proposal_targets.rb +45 -0
  21. data/lib/textus/doctor/check/rule_ambiguity.rb +3 -3
  22. data/lib/textus/doctor.rb +2 -1
  23. data/lib/textus/domain/action.rb +3 -3
  24. data/lib/textus/domain/freshness/evaluator.rb +3 -3
  25. data/lib/textus/domain/freshness/policy.rb +2 -2
  26. data/lib/textus/domain/freshness.rb +7 -7
  27. data/lib/textus/domain/outcome.rb +2 -2
  28. data/lib/textus/domain/permission.rb +2 -10
  29. data/lib/textus/domain/policy/base_guards.rb +25 -0
  30. data/lib/textus/domain/policy/evaluation.rb +18 -0
  31. data/lib/textus/domain/policy/{refresh.rb → fetch.rb} +1 -1
  32. data/lib/textus/domain/policy/guard.rb +35 -0
  33. data/lib/textus/domain/policy/guard_factory.rb +40 -0
  34. data/lib/textus/domain/policy/predicates/author_held.rb +33 -0
  35. data/lib/textus/domain/policy/predicates/etag_match.rb +32 -0
  36. data/lib/textus/domain/policy/predicates/fresh_within.rb +58 -0
  37. data/lib/textus/domain/policy/predicates/registry.rb +39 -0
  38. data/lib/textus/domain/policy/predicates/schema_valid.rb +30 -19
  39. data/lib/textus/domain/policy/predicates/target_is_canon.rb +33 -0
  40. data/lib/textus/domain/policy/predicates/zone_writable_by.rb +39 -0
  41. data/lib/textus/domain/staleness/intake_check.rb +6 -6
  42. data/lib/textus/envelope.rb +2 -2
  43. data/lib/textus/errors.rb +25 -28
  44. data/lib/textus/hooks/event_bus.rb +4 -4
  45. data/lib/textus/init.rb +23 -18
  46. data/lib/textus/maintenance/zone_mv.rb +1 -1
  47. data/lib/textus/manifest/capabilities.rb +29 -0
  48. data/lib/textus/manifest/data.rb +14 -10
  49. data/lib/textus/manifest/policy.rb +37 -21
  50. data/lib/textus/manifest/rules.rb +16 -14
  51. data/lib/textus/manifest/schema.rb +48 -58
  52. data/lib/textus/manifest.rb +3 -3
  53. data/lib/textus/mcp/server.rb +1 -1
  54. data/lib/textus/mcp/tool_schemas.rb +3 -3
  55. data/lib/textus/mcp/tools.rb +7 -7
  56. data/lib/textus/ports/audit_subscriber.rb +1 -1
  57. data/lib/textus/ports/{refresh → fetch}/detached.rb +4 -4
  58. data/lib/textus/ports/{refresh → fetch}/lock.rb +1 -1
  59. data/lib/textus/projection.rb +1 -1
  60. data/lib/textus/read/freshness.rb +9 -9
  61. data/lib/textus/read/get.rb +8 -8
  62. data/lib/textus/read/{get_or_refresh.rb → get_or_fetch.rb} +11 -11
  63. data/lib/textus/read/policy_explain.rb +14 -10
  64. data/lib/textus/read/pulse.rb +5 -4
  65. data/lib/textus/read/validator.rb +1 -1
  66. data/lib/textus/schema/tools.rb +5 -5
  67. data/lib/textus/version.rb +1 -1
  68. data/lib/textus/write/accept.rb +19 -55
  69. data/lib/textus/write/delete.rb +14 -2
  70. data/lib/textus/write/{refresh_all.rb → fetch_all.rb} +6 -6
  71. data/lib/textus/write/{refresh_orchestrator.rb → fetch_orchestrator.rb} +14 -14
  72. data/lib/textus/write/{refresh_worker.rb → fetch_worker.rb} +21 -14
  73. data/lib/textus/write/mv.rb +15 -3
  74. data/lib/textus/write/put.rb +14 -2
  75. data/lib/textus/write/reject.rb +11 -5
  76. metadata +24 -18
  77. data/lib/textus/cli/verb/refresh.rb +0 -14
  78. data/lib/textus/domain/authorizer.rb +0 -37
  79. data/lib/textus/domain/policy/predicates/accept_authority_signed.rb +0 -33
  80. data/lib/textus/domain/policy/promote.rb +0 -26
  81. data/lib/textus/domain/policy/promotion.rb +0 -57
  82. data/lib/textus/manifest/role_kinds.rb +0 -21
  83. data/lib/textus/write/authority_gate.rb +0 -24
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d84508fac499044df50d5fa793000fbd2249732c6b867f1b0f88535bfab4f083
4
- data.tar.gz: 5069218d7c3c1a8360f4507bb99f94951e92a7d345ba4fb44ee72819fb4739e4
3
+ metadata.gz: e3e6ac596658c63369ce977d194a2c43d0a789cdfa97b04d100b70e572165e63
4
+ data.tar.gz: c3ed7085fa38777fdc6e8a38e8c4c8f4a51ce113065f24b42f596aa220acb9f3
5
5
  SHA512:
6
- metadata.gz: abf8e5e74b77227f34bbc95dd38c1e9b8d716f56f1cda11664f3da284f417d8581a51e4c27accdcb88173c274ff1aa929443bb027b851a95cbced029fdf34852
7
- data.tar.gz: bd63c95ae46643c063c1b035fc859957418f185795fc32b54f798b44779d825101d9f71667a85f518dacab6ffbcebe70bc5676c9ae24e80cc9de4fa44e38aa38
6
+ metadata.gz: a95c23aaf19216505f272f0262b66e1e7a3e01b3e78a2b8062ab827a3c8109a6d085348b3e1b1e664a41f0bbdeaf46cf736b47fe211495905445c9ae283c88fa
7
+ data.tar.gz: 453327b6cea08e0102e76dd22723d6f77a69d10f8d6810427e23934956d5a332b5f9d33b81d568bf7529cf8ab83bdfb6b323b447d2d9dead6d06c2b703e66ac4
data/ARCHITECTURE.md CHANGED
@@ -1,242 +1,3 @@
1
- # Textus architecture
1
+ # Architecture
2
2
 
3
- ```
4
- ┌─ Interface ────────────────────────────────────────────────┐
5
- │ CLI verbs: store.<verb>(..., role:) │
6
- │ store.as(role).<verb>(...) │
7
- │ # (put/get/refresh/…) │
8
- │ │
9
- │ MCP gate: textus mcp serve — same use cases, JSON-RPC. │
10
- └──────────────────────┬─────────────────────────────────────┘
11
-
12
- ┌─ Application ────────▼─────────────────────────────────────┐
13
- │ Call (slim Data: role, correlation_id, now, │
14
- │ dry_run — request state only) │
15
- │ Container (single record — wired ports + manifest) │
16
- │ Dispatcher (static VERBS table: verb → use-case) │
17
- │ RoleScope (Store#as(role) — forwards verb calls) │
18
- │ │
19
- │ read/{get,get_or_refresh,list,where,uid,schema_envelope, │
20
- │ deps,rdeps,published,stale,validate_all,boot,doctor,│
21
- │ freshness,audit,blame,policy_explain,pulse}.rb │
22
- │ write/{put,delete,mv,accept,reject,publish, │
23
- │ materializer,authority_gate, │
24
- │ refresh_worker,refresh_orchestrator,refresh_all} │
25
- │ maintenance/{migrate,key_mv_prefix,key_delete_prefix, │
26
- │ zone_mv,rule_lint}.rb │
27
- │ envelope/io/{reader,writer}.rb (split: parse vs persist) │
28
- │ projection.rb │
29
- └──────────┬───────────────────────────────┬─────────────────┘
30
- │ uses domain │ uses ports
31
- ┌─ Domain ─▼─────────────────────────────────────────────────┐
32
- │ Authorizer (manifest + role → allow / deny) │
33
- │ Permission (write/read predicate per zone) │
34
- │ Freshness::{Policy,Verdict,Evaluator} │
35
- │ Staleness (Generator/Intake checks) │
36
- │ Action Outcome Sentinel │
37
- │ Policy::{Promote,Refresh,Matcher,HandlerAllowlist, │
38
- │ Predicates::{SchemaValid,AcceptAuthoritySigned}} │
39
- └──────────────────────────────────────────┬─────────────────┘
40
- │ implements
41
- ┌─ Infrastructure ─────────────────────────▼─────────────────┐
42
- │ Store (composition root — wires ports, │
43
- │ vends a Container + dispatches verbs) │
44
- │ Storage::FileStore (bytes-only port: read/write/delete/ │
45
- │ exists?/etag) │
46
- │ Manifest (Data, Resolver, Policy, Rules) │
47
- │ Schemas (eager-load cache) │
48
- │ Ports::{AuditLog,AuditSubscriber,Publisher,Clock, │
49
- │ Refresh::Lock,Refresh::Detached,BuildLock} │
50
- │ Hooks::{EventBus,RpcRegistry,Loader,Context,FireReport, │
51
- │ Signature,Builtin,ErrorLog} │
52
- │ Entry::{Markdown,Json,Yaml,Text} (format strategies) │
53
- └────────────────────────────────────────────────────────────┘
54
-
55
- Dependency rule: arrows point DOWN. Domain performs no direct
56
- File/Dir/Time.now I/O — all disk and clock access is routed through
57
- injected ports (FileStat, Clock). Pure path math (File.join/dirname/
58
- absolute_path?/expand_path/basename), Digest hashing of injected
59
- bytes, and Time.parse of stored strings are NOT I/O and are allowed.
60
- Application imports Domain + Ports.
61
- Use cases are plain classes on (container:, call:).
62
- Verbs are looked up in the static Dispatcher::VERBS table.
63
- ```
64
-
65
- ## How a verb becomes a method
66
-
67
- Each application use case is a plain class under `lib/textus/{read,write,maintenance}/`. The shape is uniform:
68
-
69
- ```ruby
70
- module Textus
71
- module Read
72
- class Get
73
- def initialize(container:, call:)
74
- @container = container
75
- @call = call
76
- end
77
-
78
- def call(key)
79
- ...
80
- end
81
- end
82
- end
83
- end
84
- ```
85
-
86
- Verbs are looked up in a static frozen table (`Textus::Dispatcher::VERBS`) that maps `:get → Textus::Read::Get`, `:put → Textus::Write::Put`, etc. `Store#put` / `Store#get` / `Store#as(role).<verb>(...)` instantiate the use case on `(container:, call:)` and invoke `#call`. Adding a new verb is **one entry in `Dispatcher::VERBS`** plus the class — no metaprogramming.
87
-
88
- The instantiate-and-call step itself has one home: `Dispatcher.invoke(verb, container:, call:, args:, kwargs:)` (ADR 0026). `RoleScope` builds the `Call` (request state) and delegates the dispatch to `Dispatcher.invoke`; the convention for invoking a uniform-shape use case lives next to the table that maps the verbs, not re-spelled in the caller. `Store`'s own verb loop is separate — it extracts the `role:` keyword and forwards to `as(role)`, a role-selection job distinct from invocation.
89
-
90
- `boot` and `doctor` are read verbs like any other: `Read::Boot` / `Read::Doctor`
91
- are thin `(container:, call:)` use cases that delegate to the `Textus::Boot` /
92
- `Textus::Doctor` report-builder libraries (`build(container:, ...)`). They are
93
- reached through `Dispatcher::VERBS`, not a special method on `RoleScope`.
94
-
95
- Two collaborators live outside the dispatcher because they're composed by other use cases, not invoked as verbs:
96
-
97
- - `Write::RefreshOrchestrator` — composes `RefreshWorker` with the freshness `Action` returned by `Domain::Freshness`.
98
- - `Envelope::IO::{Reader,Writer}` — own the parse and persist halves of the write pipeline; the audit-append-as-final-step invariant lives in `Writer`.
99
-
100
- ## Container
101
-
102
- Use cases never see the raw `Store`. `Textus::Container` is a single record holding the wired collaborators:
103
-
104
- ```ruby
105
- Container = Data.define(
106
- :manifest, :file_store, :schemas, :root,
107
- :audit_log, :events, :rpc, :authorizer
108
- )
109
- ```
110
-
111
- 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.
112
-
113
- ## Ports
114
-
115
- Ports are infrastructure adapters with an interface defined by the domain. Each port is independently replaceable — swap the implementation for tests or alternative runtimes without touching application or domain code.
116
-
117
- | Class | Role |
118
- |---|---|
119
- | `Ports::Storage::FileStore` | Bytes-only FS I/O — `read`, `write`, `delete`, `exists?`, `etag`. No knowledge of envelopes or schemas. |
120
- | `Ports::AuditLog` | Append-only structured log (`audit.log`). Owns seq numbering, file-locking, and rotation. |
121
- | `Ports::Clock` | Supplies `Time.now` — a module-function so tests can swap it without dependency injection boilerplate. |
122
- | `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. |
123
- | `Ports::Refresh::Lock` | Non-blocking `flock`-backed lock per key — prevents concurrent refresh workers from racing on the same entry. |
124
- | `Ports::Refresh::Detached` | Spawns a background thread for async refresh; the caller receives a `refresh_backgrounded` event instead of blocking. |
125
- | `Ports::BuildLock` | Process-exclusive `flock` guard over the materializer build pipeline. Raises `BuildInProgress` if a build is already running. |
126
-
127
- Application use cases access ports only through `Container` fields — never through the raw `Store`.
128
-
129
- ### EnvelopeIO
130
-
131
- `Envelope::IO::Reader` and `Envelope::IO::Writer` split the envelope pipeline into read-only parse and write-with-audit halves.
132
-
133
- **Reader** (`lib/textus/envelope/io/reader.rb`) — resolves a key through `manifest.resolver`, reads bytes via `FileStore`, delegates parsing to the format strategy (`Entry.for_format`), and returns an `Envelope`. No audit, no events, no permission checks. Also used by `Writer` for the existing-uid lookup on `put`.
134
-
135
- **Writer** (`lib/textus/envelope/io/writer.rb`) — owns the full write pipeline: serialize → schema-validate → etag-check → `FileStore#write` → `AuditLog#append`. The class comment states the invariant directly: every public method's final action is `@audit_log.append(...)`. If the audit append fails, the caller sees the underlying error — the byte write already happened, but the pipeline contract treats audit as the commit step. No permission check, no event firing — those stay in the calling use case (`Write::Put`, `Write::Delete`, `Write::Mv`).
136
-
137
- The three public methods are `put`, `delete`, and `move`; all follow the same validate → write → audit sequence.
138
-
139
- Both are built from a `Container` via named constructors — `Writer.from(container:, call:)` (which builds its own `Reader.from`) and `Reader.from(container:)` (ADR 0026). Write use cases call `Writer.from` rather than reconstructing the object graph by hand, so a change to the Writer's dependencies is a one-line edit in one place.
140
-
141
- ## Manifest carving
142
-
143
- Manifest carving means slicing the parsed manifest YAML into four purpose-specific sub-objects. Each consumer sees only the fields it needs; none reach into the full raw document.
144
-
145
- `Manifest` itself is a `Data.define` struct — a composition record with four named members:
146
-
147
- | Member | Class | Responsibility |
148
- |---|---|---|
149
- | `data` | `Manifest::Data` | Frozen value: `raw`, `root`, `zones`, `entries`, `audit_config`, `role_mapping`. Structural data only — no behaviour beyond accessors and key validation. |
150
- | `resolver` | `Manifest::Resolver` | Key → `Resolution(entry, path, remaining)`. Handles nested entry enumeration and fuzzy-match suggestions. |
151
- | `policy` | `Manifest::Policy` | Zone/role authority — `zone_writers`, `zone_kinds`, `permission_for`, `role_kind`, `roles_with_kind`, `propose_zone_for(role)`. Derived from a `Data` snapshot; no filesystem I/O. `propose_zone_for` owns the "first writable zone whose name contains `review`" convention used by `MCP::Server` (ADR 0027). |
152
- | `rules` | `Manifest::Rules` | Pattern-matched rule engine. `rules.for(key)` returns a `RuleSet(refresh, handler_allowlist, promote, retention)` by evaluating all `match:` blocks against the key. |
153
-
154
- Rationale: cleaner test seams — a use case that only needs key resolution constructs a `Manifest::Resolver` from a stub `Data`; one that only needs rule lookup constructs a `Manifest::Rules` directly. No consumer is forced to build the full manifest to exercise one sub-view.
155
-
156
- The four members are wired in `Manifest.build` (`lib/textus/manifest.rb`). `Manifest::Data` constructs `Policy` internally during `initialize`; the others are assembled by the loader and handed in as named arguments.
157
-
158
- ## Read path (`store.get(key)`)
159
-
160
- 1. CLI verb (or MCP tool) calls `store.get(key, role:)` (or `store.as(role).get(key)`).
161
- 2. `Store#get` looks up `Dispatcher::VERBS[:get] → Read::Get`, builds a `Call`, instantiates `Read::Get.new(container:, call:).call(key)`.
162
- 3. `Read::Get#call` resolves the path through `container.manifest`, reads bytes via `container.file_store`, parses the envelope.
163
- 4. Looks up the refresh policy via `container.manifest.rules.for(key)`. If absent, returns the envelope annotated fresh.
164
- 5. Otherwise `Domain::Freshness::Evaluator.call(policy, envelope, now:)` returns a `Verdict`; the envelope is annotated with `stale`, `reason`, `refreshing: false`.
165
-
166
- `store.get_or_refresh(key)` composes `Read::Get` with `Write::RefreshOrchestrator` to optionally refresh on stale.
167
-
168
- ## Write path (`store.put(key, ...)`)
169
-
170
- 1. CLI verb calls `store.put(key, meta:, body:, content:, if_etag:, role:)`.
171
- 2. `Write::Put#call` validates the key, resolves the manifest entry, and calls `container.authorizer.authorize_write!(mentry, role: call.role)` — raises `WriteForbidden` if denied.
172
- 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.
173
- 4. Publishes `:entry_put` via `container.events` with `ctx: <Hooks::Context>`, `key:`, `envelope:`.
174
-
175
- `Write::{Delete,Mv,Accept,Reject,Publish}` follow the same shape: explicit container, `Authorizer` for authz, `Envelope::IO::Writer` for persistence (where applicable), event published with the `Hooks::Context` handle.
176
-
177
- `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.
178
-
179
- ## Refresh path (`store.refresh(key)`)
180
-
181
- 1. CLI `Verb::Refresh` calls `store.refresh(key, role: "runner")`.
182
- 2. `Write::RefreshWorker#run(key)`:
183
- - Resolves the manifest entry, looks up the intake handler via `container.rpc.callable(:resolve_intake, mentry.handler)`.
184
- - Publishes `:refresh_started` with the hook context.
185
- - Invokes the handler under a 30s thread-join deadline.
186
- - On any error: publishes `:refresh_failed`, then re-raises.
187
- - On success: applies `container.authorizer.authorize_write!` and persists via `Envelope::IO::Writer#write` directly (no `Put` round-trip); publishes `:entry_refreshed` unless etag is unchanged.
188
- 3. `store.refresh_all(prefix:, zone:)` lists stale entries via `Read::Stale` and runs `Worker#run` per entry; returns `{ refreshed:, failed:, skipped: }`.
189
-
190
- ## Hook payload contract
191
-
192
- Pub-sub hooks (`:entry_put`, `:entry_refreshed`, …) 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.
193
-
194
- RPC hooks (`:resolve_intake`, `:transform_rows`, `:validate`) receive `caps:` — a `Textus::Container`. They are gem-internal: the framework calls them, not user pub-sub.
195
-
196
- ## Agent surface (boot + pulse + MCP)
197
-
198
- Agents and plugins talk to a textus store through three layers:
199
-
200
- ```
201
- soul (skill/agent) ──▶ gate (CLI | MCP) ──▶ Store ──▶ memory (.textus/)
202
- ```
203
-
204
- Two transports, one façade:
205
-
206
- - **CLI** — human/script surface. `textus boot`, `textus pulse --since=N`, `textus get/put/...`.
207
- - **MCP** — agent surface. `textus mcp serve` runs a stdio JSON-RPC 2.0 server speaking MCP draft 2024-11-05. Tools are auto-derived from the manifest. Session state (cursor, role, manifest_etag) is server-side.
208
-
209
- Both transports call `store.<verb>(..., role:)` (or `store.as(role).<verb>(...)`). No duplicate logic.
210
-
211
- The agent loop (cadence guide in `docs/agent-integration.md`):
212
-
213
- 1. **Session start:** `boot()` → contract envelope (zones, entries, schemas, write_flows, agent_quickstart with `latest_seq`).
214
- 2. **Per turn:** `pulse(since=cursor)` → `{cursor, changed, stale, pending_review, doctor}`.
215
- 3. **On demand:** `get`, `put`, `propose`, `refresh`, `schema`, `rules`.
216
-
217
- Manifest drift surfaces as `ContractDrift` (manifest_etag mismatch); audit cursor falls off the keep window as `CursorExpired`. Both signal "call `boot` again."
218
-
219
- ## Hooks::EventBus event catalog
220
-
221
- `Hooks::Signature` is the single home of callable keyword-introspection — both `EventBus` (pub-sub dispatch) and `RpcRegistry` (RPC dispatch) delegate to it for `accepts_keyrest?`, `declared_keys`, `missing`, and `filter` rather than each maintaining a hand-rolled copy (ADR 0027).
222
-
223
- RPC (single handler, declares `caps:`):
224
- - `resolve_intake(caps:, config:, args:)` — intake fetch handler.
225
- - `transform_rows(caps:, rows:, config:)` — row transform for intakes.
226
- - `validate(caps:)` — custom doctor validator.
227
-
228
- Pub-sub (0..N handlers, declare `ctx:`):
229
- - `entry_put(ctx:, key:, envelope:)`
230
- - `entry_deleted(ctx:, key:)`
231
- - `entry_refreshed(ctx:, key:, envelope:, change:)`
232
- - `entry_renamed(ctx:, key:, from_key:, to_key:, envelope:)`
233
- - `build_completed(ctx:, key:, envelope:, sources:)`
234
- - `proposal_accepted(ctx:, key:, target_key:)`
235
- - `proposal_rejected(ctx:, key:, target_key:)`
236
- - `file_published(ctx:, key:, envelope:, source:, target:)`
237
- - `store_loaded(ctx:)`
238
- - `refresh_started(ctx:, key:, mode:)`
239
- - `refresh_failed(ctx:, key:, error_class:, error_message:)`
240
- - `refresh_backgrounded(ctx:, key:, started_at:, budget_ms:)`
241
-
242
- Authoritative source: `lib/textus/hooks/event_bus.rb` `EVENTS`.
3
+ Moved to [`docs/architecture/README.md`](docs/architecture/README.md).
data/CHANGELOG.md CHANGED
@@ -9,6 +9,119 @@ The **gem version** (`0.x.y`) is distinct from the **protocol version**
9
9
  bump is a breaking change that requires a store migration; the gem version
10
10
  tracks both additive improvements and breaking protocol bumps independently.
11
11
 
12
+ ## 0.35.1 — 2026-05-31 — RSpec foundation consolidation (test-only)
13
+
14
+ No `textus/3` wire-format change; no manifest-schema change; no library behavior change.
15
+ Test-suite maintenance only.
16
+
17
+ ### Changed
18
+
19
+ - Removed redundant per-file `include TextusSpecHelpers` lines (the module is
20
+ globally included via `spec/support/fixtures.rb`).
21
+ - Envelope-writer specs now assert audit side-effects through the shared
22
+ `have_audit_verb` / `last_audit_row` helpers instead of raw `audit.log`
23
+ JSON substring matching.
24
+
25
+ ### Fixed
26
+
27
+ - `pulse_queue_zone` spec no longer leaks a temp directory per run (its second
28
+ store now builds under the `textus_store_fixture` tmp tree, which is cleaned up).
29
+
30
+ ## 0.35.0 — 2026-05-31 — Proposal target-zone constraint + `author_held` ([ADR 0035](docs/architecture/decisions/0035-proposal-target-zone-constraint.md))
31
+
32
+ No `textus/3` wire-format change; no manifest-schema change.
33
+
34
+ ### Changed (BREAKING)
35
+
36
+ - **`accept` now refuses a proposal whose `target_key` is not a `canon` zone** (new floor
37
+ predicate `target_is_canon`). Previously such a proposal failed confusingly downstream
38
+ (accept-into-derived) or incoherently "succeeded" (accept-into-workspace). Refusals surface
39
+ as `guard_failed` naming `target_is_canon`.
40
+ - **Predicate `author_signed` renamed to `author_held`** — it checks possession of the `author`
41
+ capability, not a signing gesture. `guard_failed` output and any `rules[].guard` referencing
42
+ the old name change accordingly.
43
+
44
+ ### Added
45
+
46
+ - **`doctor` check `proposal_targets`** — warns on queued proposals whose `target_key` is
47
+ non-canon (`proposal.target_not_canon`) or unresolvable (`proposal.target_unresolved`).
48
+
49
+ ## 0.34.0 — 2026-05-31 — Unify the Lane vocabulary + finish boot's kind-derived zone naming ([ADR 0034](docs/architecture/decisions/0034-unify-lane-vocabulary.md))
50
+
51
+ No `textus/3` wire-format change; no manifest-schema change. The five zone-kinds and five
52
+ capabilities, their names, and their mapping are identical to 0.33.0.
53
+
54
+ ### Changed
55
+
56
+ - **One `Schema::LANES` table is now the source of truth** for the closed coordination
57
+ vocabulary; `ZONE_KINDS`, `CAPABILITIES`, and `KIND_REQUIRES_VERB` are derived from it, so a
58
+ zone-kind and its required capability can no longer drift. (`CAPABILITIES` array ordering
59
+ now follows `LANES.values`; no behaviour depends on it.)
60
+ - **`boot` names zones by kind, not by hardcoded instance.** `write_flows`, the
61
+ `agent_protocol` recipes, and the CLI verb catalog now reflect the live store
62
+ (`knowledge`/`notebook`/`feeds`/`proposals`/`artifacts`) and survive zone renames —
63
+ completing the rename-fragility fix [ADR 0033](docs/architecture/decisions/0033-complete-primitives-and-vocabulary.md) §6 began for `ZONE_PURPOSES`.
64
+ - **`keep`-holders now get a `notebook` write-flow in `boot`.** 0.33 added the `keep`
65
+ capability but no boot guidance for it; the agent's durable-lane flow was silently omitted.
66
+
67
+ ### Fixed
68
+
69
+ - **`pulse` `pending_review` was silently empty on default stores since 0.33.** It hardcoded
70
+ the pre-0.33 zone name `review`; it now derives the queue zone from the manifest, so it
71
+ surfaces pending proposals from the (default-named) `proposals` zone again.
72
+
73
+ ### Removed (BREAKING, internal)
74
+
75
+ - **`Manifest::Data#zones`** (the unused `name => []` map) is removed; the four internal
76
+ readers now use `Manifest::Data#declared_zone_kinds`. No manifest-schema or wire change.
77
+
78
+ ## 0.33.0 — Complete primitives + vocabulary (ADR 0033) — 2026-05-31
79
+
80
+ **BREAKING (manifest schema + default scaffold + predicate/error names; `textus/3` wire format UNCHANGED):**
81
+ - New coordination primitive: `workspace` zone-kind + `keep` capability — agents get a durable self-owned lane (`notebook` in the default scaffold). Closes the agent-memory gap.
82
+ - Renamed capability `accept` → `author` (the `accept` *transition* / CLI verb is unchanged); predicate `accept_signed` → `author_signed`; zone-kind `origin` → `canon`.
83
+ - Default scaffold renamed: `identity` + `working` → `knowledge` (identity is now the `knowledge.identity.*` key prefix), `intake` → `feeds`, `review` → `proposals`, `output` → `artifacts`; new `notebook` workspace zone.
84
+ - Zones may declare optional `owner:` (informational) and `desc:` (surfaced as the boot zone purpose).
85
+ - Manifests using `origin` / `accept` (capability) / `accept_signed` get an unknown-value rejection at load — no aliasing.
86
+ - The `textus/3` envelope, audit-log, and key-grammar wire formats are unchanged.
87
+
88
+ ## 0.32.1 — 2026-05-30
89
+
90
+ ### Internal
91
+
92
+ - Test-suite cleanup for the unified-Guard specs (no `lib/` change): the new Guard/predicate/write specs now use the shared `textus_store_fixture` context plus a single `store_from_manifest` helper (replacing 9 per-file `build_*_store` methods and hand-rolled `Dir.mktmpdir` nesting), a `fail_guard_with` matcher for `GuardFailed` assertions, and uniformly mockist predicate unit specs (`zone_writable_by_spec` joins `accept_signed_spec`/`schema_valid_spec`).
93
+
94
+ ## 0.32.0 — 2026-05-30
95
+
96
+ Unified Guard engine ([ADR 0031](docs/architecture/decisions/0031-unified-guard.md), moves 2 & 3 of [ADR 0028](docs/architecture/decisions/0028-coordination-planes.md)), plus dropping the never-enforced read gate ([ADR 0032](docs/architecture/decisions/0032-drop-read-policy.md)). Every write transition now authorizes through **one Guard** — an ordered list of pure predicates over a single `Evaluation` context. No wire format (`textus/3`) change; the manifest schema and error envelopes change (breaking).
97
+
98
+ ### Changed (BREAKING)
99
+
100
+ - **Manifest `rules[].promotion: { requires: [...] }` is removed; use `rules[].guard: { accept: [...] }`.** A `guard:` block is a map of transition (`put`/`delete`/`mv`/`accept`/`reject`/`fetch`) → predicate list, composed (AND) onto each transition's built-in base guard. A stale `promotion:` key is now rejected at load (unknown key).
101
+ - **Authorization is unified into one Guard** (ADR 0031). Promotion / accept-authority / schema failures now surface as `guard_failed` naming the unmet predicate(s); the topology refusal keeps the `write_forbidden` code and `--as=<role>` hint. Custom/vendored predicates must use the `#call(Evaluation)` signature.
102
+ - **`read_policy` is removed from the manifest** (ADR 0032): textus gates writes, not reads. `Domain::Authorizer` and `ReadForbidden` are gone. Reads are unrestricted at the protocol layer (the `.textus/` files are on disk); per-role read-scoping, if needed, is an agent-surface projection, not a manifest field.
103
+
104
+ ### Internal
105
+
106
+ - New `Domain::Policy::{Evaluation, Guard, GuardFactory, BaseGuards}` and `Predicates::{Registry, ZoneWritableBy, EtagMatch, FreshWithin}`; `Predicates::{AcceptSigned, SchemaValid}` reshaped to `#call(Evaluation)`. `Domain::Policy::{Promotion, Promote}` and `Write::AuthorityGate` are deleted (folded into the Guard + single `Predicates::REGISTRY`). `Manifest::Rules` `RuleSet` gains `guard`, loses `promote`. `Permission` collapses to `(zone, writers)`.
107
+
108
+ ## 0.31.0 — 2026-05-30
109
+
110
+ Capability-based roles and the `refresh`→`fetch` transition rename ([ADR 0030](docs/architecture/decisions/0030-capability-based-roles.md)). No wire format (`textus/3`) change; the manifest schema changes (breaking) and the data-in transition is renamed.
111
+
112
+ ### Changed (BREAKING)
113
+
114
+ - **Roles are now capability-based.** A role declares `can: [verbs]` over a closed 4-verb set — `propose`, `accept`, `fetch`, `build` — replacing the 1:1 `role → kind` model. The role-kinds `accept_authority`/`proposer`/`generator`/`runner` are gone, as are the role names `runner` and `builder` (the umbrella automated role is now `automation`). Default mapping (when no `roles:` block is declared): `human=[accept, propose]`, `agent=[propose]`, `automation=[fetch, build]`.
115
+ - **Per-zone `write_policy:` is removed.** Write authority is **derived** from the role's capabilities × the zone's kind: a role may write a zone iff it holds the verb that kind requires — `queue→propose`, `origin→accept`, `quarantine→fetch`, `derived→build`. Zones now declare only `kind:` (and optional `read_policy:`). A manifest carrying `write_policy:` is rejected at load (unknown key).
116
+ - **`accept` is the single trust anchor:** at most one role may hold the `accept` capability. The `accept`/`reject` gate and the `maintained_by` override key on the `accept` capability rather than a hard-coded role kind. The promotion predicate `:accept_authority_signed` (and the older `:human_accept`) is renamed `:accept_signed`.
117
+ - **The `refresh` transition is renamed `fetch`.** CLI `textus fetch` / `textus fetch stale` (was `refresh` / `refresh stale`); the rule block `refresh:` is now `fetch:`; events `:refresh_started`/`:refresh_failed`/`:refresh_backgrounded`/`:entry_refreshed` are `:fetch_started`/`:fetch_failed`/`:fetch_backgrounded`/`:entry_fetched`; the freshness meta field `last_refreshed_at` is `last_fetched_at`, the freshness verdict `never_refreshed` is `never_fetched`, and the envelope's `refreshing?`/`fetching` field is `fetching`. The MCP tools `refresh`/`refresh_stale` are `fetch`/`fetch_stale`.
118
+ - `WriteForbidden` now reports the missing capability: `writing '<key>' (zone '<zone>') needs capability '<verb>'`, with a hint naming the roles that hold it (or `no declared role`).
119
+
120
+ ### Internal
121
+
122
+ - `Manifest::Capabilities` (was `RoleKinds`) resolves `roles:` to `{ name => [verbs] }`; `Manifest::Data#role_caps` replaces `#role_mapping`. `Manifest::Policy` gains `verb_for_zone`, `roles_with_capability`, `proposer_role`, and derives `zone_writers` from capabilities × zone-kind; `role_kind`/`roles_with_kind`/`role_mapping` are removed. Schema validates the capability vocabulary, the ≤1-`accept` invariant, and that every declared zone-kind's required verb is held by some role.
123
+ - The `refresh`→`fetch` rename is mechanical across the engine: `Write::{FetchWorker,FetchAll,FetchOrchestrator}`, `Read::GetOrFetch`, `Domain::Policy::Fetch`, `Ports::Fetch::{Lock,Detached}`, `Domain::Policy::Predicates::AcceptSigned`, `Outcome::Fetched`, and the dispatcher verb keys.
124
+
12
125
  ## 0.30.0 — 2026-05-29
13
126
 
14
127
  Explicit zone kind (strict) and entry retention (ADR 0028, moves 1 & 4). No wire format (`textus/3`) change.