textus 0.8.1 → 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 +224 -0
- data/README.md +50 -22
- 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 +4 -0
- 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 +19 -11
- 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 +29 -9
- 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,230 @@ 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
|
+
|
|
11
235
|
## 0.8.1 — Terminology cleanup (2026-05-21)
|
|
12
236
|
|
|
13
237
|
### Breaking — intro output
|
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
|
|
|
@@ -48,15 +48,17 @@ You get `.textus/` with all five zone directories, baseline schemas, an empty au
|
|
|
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.)
|
|
@@ -96,29 +100,33 @@ All verbs accept `--format=json` and return the envelope defined in SPEC §8. Wr
|
|
|
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
|
|