textus 0.8.0 → 0.9.2
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +245 -0
- data/README.md +54 -26
- data/SPEC.md +194 -63
- data/docs/architecture.md +22 -4
- data/docs/conventions.md +24 -17
- data/lib/textus/application/context.rb +68 -0
- data/lib/textus/application/reads/audit.rb +69 -0
- data/lib/textus/application/reads/blame.rb +79 -0
- data/lib/textus/application/reads/freshness.rb +77 -0
- data/lib/textus/application/reads/get.rb +62 -0
- data/lib/textus/application/reads/policy_explain.rb +39 -0
- data/lib/textus/application/refresh/all.rb +41 -0
- data/lib/textus/application/refresh/orchestrator.rb +68 -0
- data/lib/textus/application/refresh/worker.rb +79 -0
- data/lib/textus/application/writes/accept.rb +43 -0
- data/lib/textus/application/writes/build.rb +24 -0
- data/lib/textus/application/writes/delete.rb +37 -0
- data/lib/textus/application/writes/publish.rb +25 -0
- data/lib/textus/application/writes/put.rb +44 -0
- data/lib/textus/builder.rb +27 -14
- data/lib/textus/cli/group/policy.rb +11 -0
- data/lib/textus/cli/verb/accept.rb +2 -1
- data/lib/textus/cli/verb/audit.rb +31 -0
- data/lib/textus/cli/verb/blame.rb +17 -0
- data/lib/textus/cli/verb/build.rb +2 -1
- data/lib/textus/cli/verb/delete.rb +2 -1
- data/lib/textus/cli/verb/freshness.rb +17 -0
- data/lib/textus/cli/verb/get.rb +8 -1
- data/lib/textus/cli/verb/hook_run.rb +3 -3
- data/lib/textus/cli/verb/policy_explain.rb +15 -0
- data/lib/textus/cli/verb/policy_list.rb +25 -0
- data/lib/textus/cli/verb/put.rb +5 -4
- data/lib/textus/cli/verb/refresh.rb +2 -1
- data/lib/textus/cli/verb/refresh_stale.rb +19 -0
- data/lib/textus/cli/verb/reject.rb +15 -0
- data/lib/textus/cli.rb +16 -2
- data/lib/textus/composition.rb +71 -0
- data/lib/textus/doctor/check/handler_allowlist.rb +33 -0
- data/lib/textus/doctor/check/intake_registration.rb +46 -0
- data/lib/textus/doctor/check/legacy_intake_fields.rb +57 -0
- data/lib/textus/doctor/check/policy_ambiguity.rb +49 -0
- data/lib/textus/doctor.rb +5 -1
- data/lib/textus/domain/action.rb +9 -0
- data/lib/textus/domain/freshness/evaluator.rb +30 -0
- data/lib/textus/domain/freshness/policy.rb +18 -0
- data/lib/textus/domain/freshness/verdict.rb +12 -0
- data/lib/textus/domain/outcome.rb +10 -0
- data/lib/textus/domain/permission.rb +15 -0
- data/lib/textus/domain/policy/handler_allowlist.rb +17 -0
- data/lib/textus/domain/policy/matcher.rb +51 -0
- data/lib/textus/domain/policy/promote.rb +24 -0
- data/lib/textus/domain/policy/refresh.rb +48 -0
- data/lib/textus/domain/policy.rb +7 -0
- data/lib/textus/hooks/builtin.rb +5 -5
- data/lib/textus/hooks/dispatcher.rb +15 -1
- data/lib/textus/hooks/dsl.rb +18 -0
- data/lib/textus/hooks/registry.rb +12 -5
- data/lib/textus/infra/clock.rb +9 -0
- data/lib/textus/infra/event_bus.rb +27 -0
- data/lib/textus/infra/publisher.rb +73 -0
- data/lib/textus/infra/refresh/detached.rb +38 -0
- data/lib/textus/infra/refresh/lock.rb +44 -0
- data/lib/textus/init.rb +71 -28
- data/lib/textus/intro.rb +22 -14
- data/lib/textus/manifest/entry.rb +18 -9
- data/lib/textus/manifest/policies.rb +83 -0
- data/lib/textus/manifest.rb +30 -0
- data/lib/textus/proposal.rb +4 -21
- data/lib/textus/publisher.rb +4 -69
- data/lib/textus/refresh.rb +9 -44
- data/lib/textus/store/mover.rb +14 -9
- data/lib/textus/store/reader.rb +10 -8
- data/lib/textus/store/staleness.rb +4 -16
- data/lib/textus/store/validator.rb +46 -20
- data/lib/textus/store/view.rb +8 -19
- data/lib/textus/store/writer.rb +51 -14
- data/lib/textus/store.rb +32 -12
- data/lib/textus/version.rb +1 -1
- data/lib/textus.rb +1 -0
- metadata +46 -2
- data/lib/textus/cli/verb/stale.rb +0 -14
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ca479a6c2f4282b97184aee5c65bc94c3607d18ae0a608756737c70ef53407b5
|
|
4
|
+
data.tar.gz: 2294eaa31b51276d48f1a7bc850464c3351a2c93b5d273100192fec4d89561d9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6a6c5a434cf90e8e417faed9034e6ea653f1f94b3e373ea00b47045297e73b9e0f58d4619a84f497b45cb3078930da9b4327d7e179540369af7bf7297db2bd05
|
|
7
|
+
data.tar.gz: 4cbcd09254c93d94d1fd06c766ceefb5125990851422db6f6b3af4ad716b7754d71e432ebc88161d938aa5177ec26d6e1f43b7577daa80210e7a493e0266f6ea
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,251 @@ The **gem version** (`0.x.y`) is distinct from the **protocol version**
|
|
|
8
8
|
(currently `textus/2`, embedded in every envelope as `protocol`). The protocol
|
|
9
9
|
is additive within a major; a new major would change the wire string.
|
|
10
10
|
|
|
11
|
+
## 0.9.2 — Policies, audit verbs, zone rename (2026-05-22)
|
|
12
|
+
|
|
13
|
+
### Breaking — manifest YAML
|
|
14
|
+
|
|
15
|
+
- **Top-level `policies:` block added.** Replaces entry-level `intake.ttl` and
|
|
16
|
+
`intake.on_stale`. Hand-edit existing manifests (see migration recipe below);
|
|
17
|
+
no migrator ships with 0.9.2 because the gem is pre-1.0 with no known
|
|
18
|
+
outside upgraders.
|
|
19
|
+
- **Default zone names renamed.** `canon → identity`, `intake → inbox`,
|
|
20
|
+
`pending → review`, `derived → output`. `working` unchanged. Hand-edit
|
|
21
|
+
the manifest + `mv` the zone directories (see recipe below).
|
|
22
|
+
- Custom-named zones are unaffected.
|
|
23
|
+
|
|
24
|
+
### Breaking — CLI
|
|
25
|
+
|
|
26
|
+
- `textus stale` removed. Use `textus freshness`.
|
|
27
|
+
|
|
28
|
+
### Added — verbs
|
|
29
|
+
|
|
30
|
+
- `textus freshness [--prefix=K] [--zone=Z]` — per-entry status (ttl, age,
|
|
31
|
+
next_due_at, status: fresh|stale|never_refreshed|no_policy).
|
|
32
|
+
- `textus audit [--key=K] [--zone=Z] [--role=R] [--verb=V] [--since=X]
|
|
33
|
+
[--correlation-id=ID] [--limit=N]` — query `.textus/audit.log`.
|
|
34
|
+
- `textus blame KEY` — audit rows joined with git commit metadata.
|
|
35
|
+
- `textus policy list` — dump effective policies.
|
|
36
|
+
- `textus policy explain KEY` — show per-slot winners and matching blocks.
|
|
37
|
+
|
|
38
|
+
### Added — domain
|
|
39
|
+
|
|
40
|
+
- `Textus::Domain::Policy::Refresh` — ttl + on_stale value, exports to
|
|
41
|
+
`Domain::Freshness::Policy`. `on_stale` vocab is `warn | sync | timed_sync`
|
|
42
|
+
(unchanged from 0.9.0).
|
|
43
|
+
- `Textus::Domain::Policy::Promote` — promote_requires predicate.
|
|
44
|
+
- `Textus::Domain::Policy::HandlerAllowlist` — allowed intake handlers.
|
|
45
|
+
- `Textus::Domain::Policy::Matcher` — glob match + specificity ranking.
|
|
46
|
+
- `Textus::Manifest::Policies` — collection over policy blocks with
|
|
47
|
+
most-specific-wins resolution.
|
|
48
|
+
|
|
49
|
+
### Added — doctor checks
|
|
50
|
+
|
|
51
|
+
- `policy_ambiguity` — two blocks of the same specificity matching one key.
|
|
52
|
+
- `handler_allowlist` — intake handler outside its policy's allowlist.
|
|
53
|
+
- `legacy_intake_fields` — `intake.ttl`/`intake.on_stale` still present in
|
|
54
|
+
raw YAML.
|
|
55
|
+
|
|
56
|
+
### Unchanged
|
|
57
|
+
|
|
58
|
+
- Wire protocol stays `textus/2`. Envelope shape unchanged.
|
|
59
|
+
- Hook DSL, event names, role gate semantics, schema validation unchanged.
|
|
60
|
+
- `on_stale:` vocabulary (`warn | sync | timed_sync`) and its semantics
|
|
61
|
+
(return-stale / block-and-refresh / try-with-deadline) are unchanged —
|
|
62
|
+
policies merely change where the value lives.
|
|
63
|
+
- `:publish` hook (shipped 0.8.2) remains the extension point for custom
|
|
64
|
+
publish targets.
|
|
65
|
+
|
|
66
|
+
### Migration recipe (hand-edit, no migrator ships)
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
# In your existing .textus/manifest.yaml:
|
|
70
|
+
# 1. Rename zones[].name fields: canon→identity, intake→inbox,
|
|
71
|
+
# pending→review, derived→output.
|
|
72
|
+
# 2. Rewrite every entries[].zone and entries[].path prefix accordingly.
|
|
73
|
+
# 3. Move each entries[].intake.ttl / on_stale / sync_budget_ms into
|
|
74
|
+
# a new top-level policies:[] block keyed by the entry's exact key:
|
|
75
|
+
#
|
|
76
|
+
# policies:
|
|
77
|
+
# - match: "inbox.news.hn"
|
|
78
|
+
# refresh: { ttl: 6h, on_stale: sync }
|
|
79
|
+
|
|
80
|
+
# On disk:
|
|
81
|
+
mv .textus/zones/canon .textus/zones/identity
|
|
82
|
+
mv .textus/zones/intake .textus/zones/inbox
|
|
83
|
+
mv .textus/zones/pending .textus/zones/review
|
|
84
|
+
mv .textus/zones/derived .textus/zones/output
|
|
85
|
+
|
|
86
|
+
# Verify:
|
|
87
|
+
textus doctor
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Find-and-replace tips for ad-hoc references in your own files:
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
# README snippets, CI yaml, shell scripts
|
|
94
|
+
sed -i.bak \
|
|
95
|
+
-e 's/\bcanon\b/identity/g' \
|
|
96
|
+
-e 's/\bintake\b/inbox/g' \
|
|
97
|
+
-e 's/\bpending\b/review/g' \
|
|
98
|
+
-e 's/\bderived\b/output/g' \
|
|
99
|
+
-e 's/textus stale/textus freshness/g' \
|
|
100
|
+
README.md CONTRIBUTING.md
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## 0.9.1 — write-path layering + request Context (2026-05-22)
|
|
104
|
+
|
|
105
|
+
### Changed — internal architecture (no plugin-visible impact)
|
|
106
|
+
|
|
107
|
+
- Promoted `Store::View` to `Application::Context`. The new Context carries `store`, `role`, `correlation_id`, `clock`, and `dry_run`. It answers `can_read?(zone)` / `can_write?(zone)` via the new `Domain::Permission` value. `Store::View` remains as a deprecated alias for one release; slated for removal in 0.10.0.
|
|
108
|
+
- Extracted `Domain::Permission` from `Manifest#zone_writers`. Pure predicate value — `allows_read?(role)` / `allows_write?(role)`. Manifest gains `#permission_for(zone_name)` returning a `Permission`.
|
|
109
|
+
- Extracted write-path use cases under `Application::Writes::*`:
|
|
110
|
+
- `Writes::Put` (was `Store::Writer#put` orchestration)
|
|
111
|
+
- `Writes::Delete` (was `Store::Writer#delete` orchestration)
|
|
112
|
+
- `Writes::Build` (was `Builder#build` orchestration)
|
|
113
|
+
- `Writes::Accept` (was `Proposal.accept`)
|
|
114
|
+
- `Writes::Publish` (was direct calls to `Publisher.publish`)
|
|
115
|
+
- `Store::Writer#put` and `#delete` reduced to pure I/O (`#write_envelope_to_disk`, `#delete_envelope_from_disk`). The original methods remain as backward-compat shims that delegate to the use cases.
|
|
116
|
+
- `Builder`, `Proposal`, `Publisher` become thin shims. `Publisher` also moved to `Textus::Infra::Publisher` (its prior location remains as an alias).
|
|
117
|
+
- `Store#get`, `#put`, `#delete` reduced to 2-line shims through the new `Composition` module. `Store` itself no longer imports from `Application::*`.
|
|
118
|
+
- New `Composition` factory module wires Contexts and use cases. CLI verbs construct via `Composition.context(store, role:)` then `Composition.<use_case>(ctx).call(...)`.
|
|
119
|
+
- `Refresh::Worker` and `Refresh::Orchestrator` migrated to take `Context` instead of `store:` + `as:`.
|
|
120
|
+
|
|
121
|
+
### Added
|
|
122
|
+
|
|
123
|
+
- Every event payload now includes `correlation_id` — a UUID generated once per Context. Hook authors can use this to correlate events within a single request (e.g., a `:refreshed` event and a downstream `:built` event share an ID).
|
|
124
|
+
|
|
125
|
+
### Deprecated
|
|
126
|
+
|
|
127
|
+
- `Textus::Store::View` — use `Textus::Application::Context`. Removed in 0.10.0.
|
|
128
|
+
- `Textus::Publisher` — use `Textus::Infra::Publisher` or `Textus::Application::Writes::Publish`. Removed in 0.10.0.
|
|
129
|
+
|
|
130
|
+
### Unchanged
|
|
131
|
+
|
|
132
|
+
- Plugin DSL, manifest YAML schema, CLI verb JSON output, envelope fields, event names, wire protocol — all identical to 0.9.0.
|
|
133
|
+
- No migration needed for plugin authors.
|
|
134
|
+
|
|
135
|
+
## 0.9.0 — intake, event standardization, read-time freshness, layered architecture (2026-05-22)
|
|
136
|
+
|
|
137
|
+
### Breaking — manifest schema
|
|
138
|
+
- The `source:` block is renamed to `intake:`. Its inner `fetch:` is renamed to `handler:`. Other inner fields (`config:`, `ttl:`) keep their names.
|
|
139
|
+
- Loading a manifest that still uses `source:` raises a clear migration error.
|
|
140
|
+
|
|
141
|
+
### Breaking — event names (pub-sub bus)
|
|
142
|
+
- `:fetch` → `:intake` (RPC)
|
|
143
|
+
- `:delete` → `:deleted`
|
|
144
|
+
- `:refresh` → `:refreshed`
|
|
145
|
+
- `:build` → `:built`
|
|
146
|
+
- `:publish` → `:published`
|
|
147
|
+
- `:accept` → `:accepted`
|
|
148
|
+
- `:put`, `:mv`, `:reject`, `:loaded` are unchanged (already past tense).
|
|
149
|
+
|
|
150
|
+
### Breaking — DSL sugar
|
|
151
|
+
- `Textus.fetch(:name)` → `Textus.intake(:name)`
|
|
152
|
+
- `Textus.refresh`, `.build`, `.publish`, `.delete`, `.accept` rename to past-tense equivalents.
|
|
153
|
+
- The primitive `Textus.hook(event, name)` is unchanged — event symbols update per above.
|
|
154
|
+
|
|
155
|
+
### Added — read-time freshness
|
|
156
|
+
- Every entry's manifest may declare `intake.on_stale: warn | sync | timed_sync` (default `warn`).
|
|
157
|
+
- `warn` — return stale envelope with `stale: true`, `stale_reason: "…"`; no refresh.
|
|
158
|
+
- `sync` — refresh inline, return fresh envelope.
|
|
159
|
+
- `timed_sync` — attempt sync up to `sync_budget_ms` (default 500ms); if exceeded, fork+detach a child to complete the refresh and return stale + `refreshing: true` to the caller. Unix only; on Windows falls back to `warn`.
|
|
160
|
+
- New envelope fields on `textus get`: `stale`, `stale_reason`, `refreshing`.
|
|
161
|
+
|
|
162
|
+
### Added — refresh lifecycle events
|
|
163
|
+
- `:refresh_began { key, mode }` fires when refresh begins.
|
|
164
|
+
- `:refresh_failed { key, error_class, error_message }` fires on intake errors.
|
|
165
|
+
- `:refresh_detached { key, started_at, budget_ms }` fires when timed_sync gives up waiting and forks.
|
|
166
|
+
|
|
167
|
+
### Added — actuator
|
|
168
|
+
- `textus refresh-stale [--prefix=KEY] [--zone=Z]` — refreshes every entry whose TTL has expired. Returns `{ refreshed, failed, skipped }` JSON. Exits non-zero on any failure. Intended for cron / CI.
|
|
169
|
+
|
|
170
|
+
### Added — doctor check
|
|
171
|
+
- `textus doctor` now verifies every manifest `intake.handler:` resolves to a registered `Textus.intake(:name)`, reports missing handlers as errors, and orphan registrations as warnings.
|
|
172
|
+
|
|
173
|
+
### Changed — internal architecture (no plugin-visible impact)
|
|
174
|
+
- Internals reorganized into four layers: `domain/` (pure values), `application/` (use cases), `infra/` (adapters), and `cli/` (interface). Plugin DSL, manifest schema, CLI verbs, and envelope shape are unchanged.
|
|
175
|
+
- `Freshness` is split into `Domain::Freshness::Evaluator` (pure) + `Domain::Freshness::Policy#decide` (data-driven) + `Application::Refresh::Orchestrator` (effects).
|
|
176
|
+
- `Refresh.call` is now a one-line shim over `Application::Refresh::Worker.run`. `Refresh::Lock` and `Refresh::Detached` moved to `Infra::Refresh`.
|
|
177
|
+
- `Store#get` now routes through `Application::Reads::Get`. `Store::Reader#get` was reduced to pure I/O (`#read_raw_envelope`) — third-party code calling it directly should switch to `Store#get` for freshness annotations.
|
|
178
|
+
|
|
179
|
+
### Unchanged
|
|
180
|
+
- Wire protocol stays `textus/2`.
|
|
181
|
+
- `:reduce`, `:check`, `:put` unchanged.
|
|
182
|
+
- The recursive `hooks/**/*.rb` loader from 0.8.2.
|
|
183
|
+
|
|
184
|
+
### Migration
|
|
185
|
+
1. In `.textus/manifest.yaml`, replace every `source:` with `intake:` and every `fetch:` inside it with `handler:`. No other inner-field renames needed.
|
|
186
|
+
2. In hook files, replace `Textus.fetch(:name)` with `Textus.intake(:name)` and the other five pub-sub sugar names with their past-tense equivalents.
|
|
187
|
+
3. (Optional) Add `on_stale: timed_sync` to entries where you want self-healing reads.
|
|
188
|
+
4. Wire `textus refresh-stale` into cron / GH Actions for scheduled freshness.
|
|
189
|
+
5. If you subscribed to the `:refresh_started` event during 0.9.0 betas, rename your handler to `Textus.refresh_began(:name)`.
|
|
190
|
+
6. If you called `Textus::Refresh::Lock` or `Textus::Refresh::Detached` directly (you probably did not), update to `Textus::Infra::Refresh::Lock` / `Textus::Infra::Refresh::Detached`.
|
|
191
|
+
|
|
192
|
+
## 0.8.3 — :mv, :reject, :loaded events (2026-05-22)
|
|
193
|
+
|
|
194
|
+
### Added
|
|
195
|
+
- New `:mv` event — fires after a successful `store.mv`. Payload:
|
|
196
|
+
`{ key:, from_key:, to_key:, envelope: }` where `key:` equals `to_key:`
|
|
197
|
+
so `keys:` glob filters route against the entry's post-move home.
|
|
198
|
+
`:put` and `:delete` remain suppressed for renames; `:mv` is the sole signal.
|
|
199
|
+
- New `:reject` event + `store.reject(pending_key, as: "human")` +
|
|
200
|
+
`textus reject KEY --as=human` CLI verb. Counterpart to `:accept` —
|
|
201
|
+
explicitly discards a proposal. Fires `:delete` then `:reject`.
|
|
202
|
+
- New `:loaded` event — fires exactly once at the tail of `Store#initialize`,
|
|
203
|
+
after all hooks are registered and reader/writer are built. Use for cache
|
|
204
|
+
warmups and one-shot setup. Payload: `store:` only.
|
|
205
|
+
|
|
206
|
+
## 0.8.2 — Hook DSL sugar + :publish event (2026-05-22)
|
|
207
|
+
|
|
208
|
+
### Added
|
|
209
|
+
- Per-event hook sugar: `Textus.fetch`, `.reduce`, `.check`, `.put`,
|
|
210
|
+
`.delete`, `.refresh`, `.build`, `.accept`, `.publish`. Each takes
|
|
211
|
+
`(name, **opts, &blk)` and delegates to the existing registry. Block
|
|
212
|
+
signatures are per-event (use `**` to absorb unused kwargs).
|
|
213
|
+
- New `:publish` pub-sub event. Fires once per file written to a repo
|
|
214
|
+
path (both for the fixed-list `publish_to:` case and the `publish_each:`
|
|
215
|
+
per-leaf case). Payload: `{ key:, envelope:, source:, target: }`.
|
|
216
|
+
Listeners can react per-file — e.g. `git add` each published file,
|
|
217
|
+
notify on writes, compute checksums.
|
|
218
|
+
- `.textus/hooks/**/*.rb` — hook files in subdirectories are now loaded.
|
|
219
|
+
Subdirectory names are organizational; the registered event and name
|
|
220
|
+
come from the DSL call, not the file path. Files load in alphabetical
|
|
221
|
+
order by full path.
|
|
222
|
+
|
|
223
|
+
### Unchanged
|
|
224
|
+
- `Textus.hook(:event, :name, &blk)` primitive — still works, still the
|
|
225
|
+
authoritative entry point.
|
|
226
|
+
- `:build` event semantics — still fires once per derived entry.
|
|
227
|
+
- Registry shape, dispatcher behavior, audit log, wire protocol
|
|
228
|
+
(`textus/2`), envelope shape.
|
|
229
|
+
|
|
230
|
+
### Example migrations
|
|
231
|
+
The bundled `examples/claude-plugin` was migrated to the new DSL
|
|
232
|
+
(snake_case names, sugar methods). No behavioral change; serves as the
|
|
233
|
+
canonical example.
|
|
234
|
+
|
|
235
|
+
## 0.8.1 — Terminology cleanup (2026-05-21)
|
|
236
|
+
|
|
237
|
+
### Breaking — intro output
|
|
238
|
+
- `textus intro` JSON: the `"extensions"` key is renamed to `"hooks"`. Consumers
|
|
239
|
+
reading `env["extensions"]` must switch to `env["hooks"]`. Wire protocol
|
|
240
|
+
remains `textus/2`; envelope shape on read/write is unchanged.
|
|
241
|
+
|
|
242
|
+
### Internal Ruby renames
|
|
243
|
+
- `Textus::Store#load_extensions` → `Textus::Store#load_hooks`.
|
|
244
|
+
- `Textus::Intro.extensions_for` → `Textus::Intro.hooks_for`.
|
|
245
|
+
- Error string `"failed loading extension <file>"` → `"failed loading hook <file>"`.
|
|
246
|
+
|
|
247
|
+
### Fixed
|
|
248
|
+
- `textus doctor` `:check`-hook failure hint pointed to `.textus/extensions/`,
|
|
249
|
+
which has never existed in 0.6+. Now correctly points to `.textus/hooks/`.
|
|
250
|
+
|
|
251
|
+
### Docs
|
|
252
|
+
- SPEC.md §5.10: "single extension verb" → "single hook verb".
|
|
253
|
+
- Scaffolded `.textus/hooks/README.md` no longer mixes "hook" and "extension"
|
|
254
|
+
terminology.
|
|
255
|
+
|
|
11
256
|
## 0.8.0 — Folder restructure & Zeitwerk autoload (2026-05-21)
|
|
12
257
|
|
|
13
258
|
### Breaking — internal Ruby renames
|
data/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Reference implementation in Ruby. Wire format `textus/2`. SPEC: [`SPEC.md`](SPEC
|
|
|
14
14
|
Two versions, deliberately independent:
|
|
15
15
|
|
|
16
16
|
- **Protocol wire string:** `textus/2`. Stable; breaking changes require `textus/3`.
|
|
17
|
-
- **Gem version:** semver, currently `0.
|
|
17
|
+
- **Gem version:** semver, currently `0.9.2`. The gem version is decoupled from the protocol string — internal refactors bump the gem; only wire-format changes bump the protocol.
|
|
18
18
|
|
|
19
19
|
Envelope payloads carry the `protocol` field. The gem version is irrelevant to the wire format.
|
|
20
20
|
|
|
@@ -45,18 +45,20 @@ You get `.textus/` with all five zone directories, baseline schemas, an empty au
|
|
|
45
45
|
audit.log # append-only NDJSON, every write
|
|
46
46
|
schemas/ # YAML field shapes per entry family
|
|
47
47
|
templates/ # mustache templates for derived entries
|
|
48
|
-
|
|
48
|
+
hooks/ # one .rb per hook
|
|
49
49
|
sentinels/ # publish bookkeeping
|
|
50
50
|
zones/
|
|
51
|
-
|
|
51
|
+
identity/ # human-only — identity, voice, decisions
|
|
52
52
|
working/ # human / ai / script — day-to-day catalog
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
inbox/ # script — declared external inputs (actions)
|
|
54
|
+
review/ # ai + human — proposals awaiting accept
|
|
55
|
+
output/ # build only — computed outputs
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
Manifest `path:` fields are relative to `.textus/zones/`. So `working.network.org.jane` lives at `.textus/zones/working/network/org/jane.md`.
|
|
59
59
|
|
|
60
|
+
> **Renamed in 0.9.2.** Pre-0.9.2 defaults were `canon`, `intake`, `pending`, `derived`. `working` is unchanged. Upgrading a 0.9.1 store is a hand-edit (see CHANGELOG migration recipe).
|
|
61
|
+
|
|
60
62
|
Read and write:
|
|
61
63
|
|
|
62
64
|
```sh
|
|
@@ -64,7 +66,9 @@ textus get working.network.org.jane
|
|
|
64
66
|
textus list --zone=working
|
|
65
67
|
echo '{"_meta":{"name":"bob","org":"acme"},"body":"hi\n"}' \
|
|
66
68
|
| textus put working.network.org.bob --as=human --stdin
|
|
67
|
-
textus
|
|
69
|
+
textus freshness --zone=output # per-entry fresh/stale/never_refreshed/no_policy
|
|
70
|
+
textus policy list # show every policy block
|
|
71
|
+
textus audit --limit=20 # query the audit log
|
|
68
72
|
```
|
|
69
73
|
|
|
70
74
|
(All verbs return JSON envelopes by default; pass `--format=json` explicitly if you prefer.)
|
|
@@ -77,8 +81,8 @@ For the full shape — Claude plugin with agents, skills, commands, pending walk
|
|
|
77
81
|
- **Per-leaf publishing.** Nested entries declare `publish_each: "skills/{basename}/SKILL.md"`. Every leaf byte-copies to its consumer location on `textus build`. No more hand-mirrored `agents/` / `skills/` / `commands/` directories.
|
|
78
82
|
- **Stable identity (`uid:`).** 16-char hex, auto-minted on first `put`, preserved across writes and moves. `textus key mv old.key new.key` renames in place — uid survives, audit row records `from_key`, `to_key`, `uid`. Reorganising a tree no longer breaks references.
|
|
79
83
|
- **Strict key grammar.** `/^[a-z0-9][a-z0-9-]*$/`, max 8 segments × 64 chars. `textus key migrate --dry-run|--write` rewrites existing stores with illegal segments deterministically.
|
|
80
|
-
- **`textus intro`.** One-shot store orientation: zones with writers + purposes, entry families with schemas and publish targets, loaded
|
|
81
|
-
- **`textus doctor`.** Health check across 9 categories: missing schemas/templates, broken
|
|
84
|
+
- **`textus intro`.** One-shot store orientation: zones with writers + purposes, entry families with schemas and publish targets, loaded hooks, write flows per role, the full CLI verb table. The boot signal for any agent — one tool call and it knows your store.
|
|
85
|
+
- **`textus doctor`.** Health check across 9 categories: missing schemas/templates, broken hooks, illegal nested keys, sentinel drift, audit log readability, unowned schema fields, schema violations, and missing manifest files. Returns `ok: true` only when nothing is wrong; warnings and info don't flip the bit.
|
|
82
86
|
- **Actionable hints on every error.** `UnknownKey` carries ranked "did you mean" suggestions. `WriteForbidden` names the role that *would* be allowed. `BadFrontmatter` tells you exactly what to rename. Printed to stderr alongside the JSON envelope on stdout.
|
|
83
87
|
|
|
84
88
|
Symlink-mode publish was removed; publish is `FileUtils.cp` + sentinel. Sentinels for published files live under `.textus/sentinels/<target_rel>.textus-managed.json` so consumer directories stay clean. Legacy sibling sentinels auto-migrate on next publish.
|
|
@@ -91,34 +95,38 @@ All verbs accept `--format=json` and return the envelope defined in SPEC §8. Wr
|
|
|
91
95
|
|
|
92
96
|
| Verb | Purpose |
|
|
93
97
|
|---|---|
|
|
94
|
-
| `intro` | Store orientation: zones, entries,
|
|
98
|
+
| `intro` | Store orientation: zones, entries, hooks, write flows, CLI map |
|
|
95
99
|
| `list [--prefix=K] [--zone=Z]` | Enumerate keys |
|
|
96
100
|
| `where K` | Resolve a key to its filesystem path |
|
|
97
101
|
| `get K` | Full envelope (frontmatter, body, uid, etag, format) |
|
|
98
102
|
| `schema show K` | Schema bound to an entry |
|
|
99
|
-
| `
|
|
103
|
+
| `freshness [--prefix=K] [--zone=Z]` | Per-entry status (fresh / stale / never_refreshed / no_policy) against `policies:` |
|
|
104
|
+
| `audit [--key=K] [--zone=Z] [--role=R] [--verb=V] [--since=X] [--correlation-id=ID] [--limit=N]` | Query `.textus/audit.log` |
|
|
105
|
+
| `blame KEY` | Audit rows joined with git commit metadata |
|
|
106
|
+
| `policy list` / `policy explain KEY` | Dump effective policies / per-slot winners for one key |
|
|
100
107
|
| `deps K` / `rdeps K` | Forward / reverse projection dependencies |
|
|
101
108
|
| `published` | List `publish_to:` targets and their backing keys |
|
|
102
109
|
| `doctor --check=schema_violations` | Validate every entry against its schema |
|
|
103
|
-
| `hook list [--event=E]` | Registered hooks grouped by event (
|
|
110
|
+
| `hook list [--event=E]` | Registered hooks grouped by event (intake, reduce, check, put, deleted, refreshed, built, accepted, published, mv, reject, loaded, refresh_began, refresh_failed, refresh_detached) |
|
|
104
111
|
|
|
105
112
|
**Write:**
|
|
106
113
|
|
|
107
114
|
| Verb | Role |
|
|
108
115
|
|---|---|
|
|
109
116
|
| `put K --stdin --as=R [--action=NAME]` | per zone |
|
|
110
|
-
| `hook run NAME [--key=val] [--as=R]` | per zone written (invoke a registered
|
|
117
|
+
| `hook run NAME [--key=val] [--as=R]` | per zone written (invoke a registered intake hook) |
|
|
111
118
|
| `delete K --if-etag=E --as=R` | per zone |
|
|
112
119
|
| `refresh K --as=script` | per zone (typically `script`) |
|
|
113
120
|
| `key mv old new --as=R [--dry-run]` | per zone (same-zone moves; uid preserved) |
|
|
114
121
|
| `build [--prefix=K] [--dry-run]` | `build` |
|
|
115
122
|
| `accept K --as=human` | `human` only |
|
|
123
|
+
| `reject K --as=human` | `human` only (discards a pending proposal; fires `:reject`) |
|
|
116
124
|
|
|
117
125
|
**Health & maintenance:**
|
|
118
126
|
|
|
119
127
|
| Verb | Purpose |
|
|
120
128
|
|---|---|
|
|
121
|
-
| `doctor` |
|
|
129
|
+
| `doctor` | Health checks (manifest, schemas, templates, hooks, illegal keys, sentinels, audit log, policy ambiguity, handler allowlist, legacy intake fields); `ok: true` when clean |
|
|
122
130
|
| `key migrate [--dry-run]` | Rename files whose basenames violate the strict key grammar |
|
|
123
131
|
|
|
124
132
|
**Scaffolding (human-only):**
|
|
@@ -134,11 +142,11 @@ All verbs accept `--format=json` and return the envelope defined in SPEC §8. Wr
|
|
|
134
142
|
|
|
135
143
|
| Zone | `writable_by` | Purpose |
|
|
136
144
|
|---|---|---|
|
|
137
|
-
| `
|
|
145
|
+
| `identity` | `[human]` | Identity, voice, decisions — slow-changing |
|
|
138
146
|
| `working` | `[human, ai, script]` | Active project state |
|
|
139
|
-
| `
|
|
140
|
-
| `
|
|
141
|
-
| `
|
|
147
|
+
| `inbox` | `[script]` | Declared external inputs (actions) |
|
|
148
|
+
| `review` | `[ai, human]` | AI proposals; humans run `textus accept` to apply |
|
|
149
|
+
| `output` | `[build]` | Computed outputs from `textus build` |
|
|
142
150
|
|
|
143
151
|
Mismatches return `write_forbidden` with a hint naming the role that *would* be allowed. Every write records the resolved role in `.textus/audit.log`.
|
|
144
152
|
|
|
@@ -150,20 +158,40 @@ Derived entries declare a `projection:` (`select`, `pluck`, `sort_by`, `limit`,
|
|
|
150
158
|
|
|
151
159
|
## Extension points
|
|
152
160
|
|
|
153
|
-
textus exposes
|
|
161
|
+
textus exposes a hook DSL. Drop `.rb` files into `.textus/hooks/` (subdirectories are fine; files load alphabetically by full path). Events:
|
|
162
|
+
|
|
163
|
+
- `:intake` — bring bytes in from elsewhere (returns `{_meta:, body:}`)
|
|
164
|
+
- `:reduce` — transform rows during projection (returns rows)
|
|
165
|
+
- `:check` — custom doctor check (returns issues)
|
|
166
|
+
- `:put`, `:deleted`, `:refreshed`, `:built`, `:accepted`, `:published`, `:mv`, `:reject`, `:loaded` — react to lifecycle events
|
|
167
|
+
- `:refresh_began`, `:refresh_failed`, `:refresh_detached` — background-refresh lifecycle (0.9.0+)
|
|
154
168
|
|
|
155
169
|
```ruby
|
|
156
|
-
|
|
170
|
+
# Inside .textus/hooks/local_file.rb
|
|
171
|
+
Textus.intake(:local_file) do |config:, args:, **|
|
|
172
|
+
path = config["path"] or raise "local-file requires intake.config.path"
|
|
173
|
+
{
|
|
174
|
+
_meta: { "last_refreshed_at" => Time.now.utc.iso8601, "source_path" => path },
|
|
175
|
+
body: File.read(File.expand_path(path)),
|
|
176
|
+
}
|
|
177
|
+
end
|
|
157
178
|
```
|
|
158
179
|
|
|
159
|
-
|
|
180
|
+
```ruby
|
|
181
|
+
Textus.reduce(:rank_by_recency) do |rows:, **|
|
|
182
|
+
rows.sort_by { |r| r["updated_at"].to_s }.reverse
|
|
183
|
+
end
|
|
184
|
+
```
|
|
160
185
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
186
|
+
To keep a batch of stale intake entries current in one shot:
|
|
187
|
+
|
|
188
|
+
```sh
|
|
189
|
+
textus refresh-stale --prefix=working --zone=intake --as=script
|
|
190
|
+
# or just refresh everything stale in the intake zone:
|
|
191
|
+
textus refresh-stale --zone=intake --as=script
|
|
192
|
+
```
|
|
165
193
|
|
|
166
|
-
See SPEC.md §5.10 for the full contract.
|
|
194
|
+
The primitive `Textus.hook(event, name, **opts) { ... }` is also supported. See SPEC.md §5.10 for the full contract.
|
|
167
195
|
|
|
168
196
|
Schemas (`.textus/schemas/<name>.yaml`) declare field shapes, per-field `maintained_by:` ownership, and an `evolution:` block (`added_in`, `deprecated_at`, `migrate_from`). Full contract in SPEC §5.8.
|
|
169
197
|
|