textus 0.10.5 → 0.14.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 (120) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +60 -40
  3. data/CHANGELOG.md +318 -3
  4. data/README.md +34 -27
  5. data/SPEC.md +226 -145
  6. data/docs/conventions.md +8 -8
  7. data/lib/textus/application/context.rb +4 -0
  8. data/lib/textus/application/reads/blame.rb +1 -1
  9. data/lib/textus/application/reads/deps.rb +15 -0
  10. data/lib/textus/application/reads/freshness.rb +4 -4
  11. data/lib/textus/application/reads/get.rb +9 -12
  12. data/lib/textus/application/reads/list.rb +15 -0
  13. data/lib/textus/application/reads/policy_explain.rb +2 -2
  14. data/lib/textus/application/reads/published.rb +15 -0
  15. data/lib/textus/application/reads/rdeps.rb +15 -0
  16. data/lib/textus/application/reads/schema_envelope.rb +15 -0
  17. data/lib/textus/application/reads/stale.rb +15 -0
  18. data/lib/textus/application/reads/uid.rb +15 -0
  19. data/lib/textus/application/reads/validate_all.rb +15 -0
  20. data/lib/textus/application/reads/where.rb +15 -0
  21. data/lib/textus/application/refresh/all.rb +2 -2
  22. data/lib/textus/application/refresh/orchestrator.rb +1 -1
  23. data/lib/textus/application/refresh/worker.rb +8 -8
  24. data/lib/textus/application/writes/accept.rb +26 -8
  25. data/lib/textus/application/writes/build.rb +12 -49
  26. data/lib/textus/application/writes/delete.rb +1 -1
  27. data/lib/textus/application/writes/mv.rb +144 -0
  28. data/lib/textus/application/writes/publish.rb +42 -10
  29. data/lib/textus/application/writes/put.rb +1 -1
  30. data/lib/textus/application/writes/reject.rb +37 -0
  31. data/lib/textus/builder/pipeline.rb +1 -1
  32. data/lib/textus/builder/renderer/json.rb +1 -1
  33. data/lib/textus/builder/renderer/yaml.rb +1 -1
  34. data/lib/textus/cli/group/key.rb +1 -1
  35. data/lib/textus/cli/group/refresh.rb +21 -0
  36. data/lib/textus/cli/group/rule.rb +11 -0
  37. data/lib/textus/cli/verb/accept.rb +1 -2
  38. data/lib/textus/cli/verb/audit.rb +3 -3
  39. data/lib/textus/cli/verb/blame.rb +1 -2
  40. data/lib/textus/cli/verb/build.rb +6 -2
  41. data/lib/textus/cli/verb/delete.rb +1 -2
  42. data/lib/textus/cli/verb/deps.rb +1 -1
  43. data/lib/textus/cli/verb/freshness.rb +1 -2
  44. data/lib/textus/cli/verb/get.rb +2 -3
  45. data/lib/textus/cli/verb/hook_run.rb +3 -2
  46. data/lib/textus/cli/verb/hooks.rb +1 -1
  47. data/lib/textus/cli/verb/{migrate_keys.rb → key_normalize.rb} +1 -1
  48. data/lib/textus/cli/verb/list.rb +1 -1
  49. data/lib/textus/cli/verb/mv.rb +1 -1
  50. data/lib/textus/cli/verb/published.rb +1 -1
  51. data/lib/textus/cli/verb/put.rb +3 -3
  52. data/lib/textus/cli/verb/rdeps.rb +1 -1
  53. data/lib/textus/cli/verb/refresh.rb +1 -2
  54. data/lib/textus/cli/verb/reject.rb +1 -1
  55. data/lib/textus/cli/verb/{policy_explain.rb → rule_explain.rb} +2 -3
  56. data/lib/textus/cli/verb/{policy_list.rb → rule_list.rb} +3 -3
  57. data/lib/textus/cli/verb/schema.rb +1 -1
  58. data/lib/textus/cli/verb/uid.rb +1 -1
  59. data/lib/textus/cli/verb/where.rb +1 -1
  60. data/lib/textus/cli/verb.rb +9 -3
  61. data/lib/textus/cli.rb +6 -6
  62. data/lib/textus/doctor/check/handler_allowlist.rb +1 -1
  63. data/lib/textus/doctor/check/illegal_keys.rb +39 -16
  64. data/lib/textus/doctor/check/intake_registration.rb +4 -4
  65. data/lib/textus/doctor/check/protocol_version.rb +47 -0
  66. data/lib/textus/doctor/check/{policy_ambiguity.rb → rule_ambiguity.rb} +6 -6
  67. data/lib/textus/doctor/check/schema_violations.rb +1 -1
  68. data/lib/textus/doctor.rb +6 -5
  69. data/lib/textus/domain/freshness/evaluator.rb +1 -1
  70. data/lib/textus/domain/permission.rb +4 -4
  71. data/lib/textus/domain/policy/predicates/human_accept.rb +31 -0
  72. data/lib/textus/domain/policy/predicates/schema_valid.rb +50 -0
  73. data/lib/textus/domain/policy/promotion.rb +45 -0
  74. data/lib/textus/entry/base.rb +28 -0
  75. data/lib/textus/entry/json.rb +59 -0
  76. data/lib/textus/entry/markdown.rb +46 -0
  77. data/lib/textus/entry/text.rb +35 -0
  78. data/lib/textus/entry/yaml.rb +59 -0
  79. data/lib/textus/entry.rb +16 -0
  80. data/lib/textus/envelope.rb +44 -14
  81. data/lib/textus/errors.rb +24 -5
  82. data/lib/textus/hooks/builtin.rb +5 -5
  83. data/lib/textus/hooks/dispatcher.rb +1 -1
  84. data/lib/textus/hooks/dsl.rb +3 -10
  85. data/lib/textus/hooks/loader.rb +1 -2
  86. data/lib/textus/hooks/registry.rb +22 -21
  87. data/lib/textus/infra/refresh/detached.rb +1 -1
  88. data/lib/textus/init.rb +25 -34
  89. data/lib/textus/intro.rb +65 -9
  90. data/lib/textus/manifest/entry/parser.rb +84 -0
  91. data/lib/textus/manifest/entry/validators/events.rb +21 -0
  92. data/lib/textus/manifest/entry/validators/format_matrix.rb +26 -0
  93. data/lib/textus/manifest/entry/validators/index_filename.rb +45 -0
  94. data/lib/textus/manifest/entry/validators/inject_intro.rb +18 -0
  95. data/lib/textus/manifest/entry/validators/publish_each.rb +37 -0
  96. data/lib/textus/manifest/entry/validators.rb +20 -0
  97. data/lib/textus/manifest/entry.rb +38 -189
  98. data/lib/textus/manifest/{policies.rb → rules.rb} +12 -10
  99. data/lib/textus/manifest/schema.rb +49 -0
  100. data/lib/textus/manifest.rb +50 -24
  101. data/lib/textus/migrate_keys.rb +1 -1
  102. data/lib/textus/operations/reads.rb +39 -0
  103. data/lib/textus/operations/refresh.rb +27 -0
  104. data/lib/textus/operations/writes.rb +21 -0
  105. data/lib/textus/operations.rb +44 -0
  106. data/lib/textus/projection.rb +9 -8
  107. data/lib/textus/refresh.rb +4 -5
  108. data/lib/textus/schema/tools.rb +8 -7
  109. data/lib/textus/store/reader.rb +1 -1
  110. data/lib/textus/store/staleness/intake_check.rb +1 -1
  111. data/lib/textus/store/validator.rb +3 -3
  112. data/lib/textus/store/writer.rb +5 -74
  113. data/lib/textus/store.rb +1 -55
  114. data/lib/textus/version.rb +2 -2
  115. data/lib/textus.rb +1 -0
  116. metadata +35 -10
  117. data/lib/textus/cli/group/policy.rb +0 -11
  118. data/lib/textus/composition.rb +0 -72
  119. data/lib/textus/proposal.rb +0 -10
  120. data/lib/textus/store/mover.rb +0 -167
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 79987029ce43500b025495ef26cefc285f7a748cdcd43388aff2fefafdf4c0ca
4
- data.tar.gz: 44a9d40720a84e4e942a1c4fde9e0ed9e7773126f443b247f6d8cb27d630c204
3
+ metadata.gz: 24eda2a2cfec1884eb3c675189493827b6c0f43d5765922dbee06b70fbdc9685
4
+ data.tar.gz: 84e3850666541900f4c3b99f71c59b73dd142c7001f80fa9e8233d38f984719e
5
5
  SHA512:
6
- metadata.gz: a539030b1d406226bbe3d3fad03b3daee59094d3d8db351d97c9c5adc445ca8c188a7d13a16cdfaaae0c933091e6a8858c14a91362f461f25a1dce52aa04a60f
7
- data.tar.gz: 83667dab7c91d4c2e9def3c2a2d605cf45d84f3fdf89bf0e6d8d25ff2b8fe668d6b71ceae0488e7ffe24970bf80e0f5eeeb13c308b2d16c9d11fc41633160dd6
6
+ metadata.gz: d68ad56bc7891aefda10e47234f1bbd1efbfd75b576f7981c3374641efbd306dc66947b382b74c70da24bc58122090c3e0789c33b1a8291aa1005b9e6c952e2e
7
+ data.tar.gz: fd1c1cefad648636b4fb5aeba100d81c66472f8e1b50d07ba2239607c19179842902abf0b76925786c66868049acfd507a9a76bd07e39026dd2aa0d261ebfdad
data/ARCHITECTURE.md CHANGED
@@ -2,73 +2,93 @@
2
2
 
3
3
  ```
4
4
  ┌─ Interface ────────────────────────────────────────────────┐
5
- │ CLI verbs: ctx = Composition.context(store, role:)
6
- Composition.<use_case>(ctx).call(...)
5
+ │ CLI verbs: ops = Operations.for(store, role:)
6
+ ops.reads.<name>.call(...)
7
+ │ ops.writes.<name>.call(...) │
8
+ │ ops.refresh.<name>.call(...) │
7
9
  └──────────────────────┬─────────────────────────────────────┘
8
10
 
9
11
  ┌─ Application ────────▼─────────────────────────────────────┐
10
12
  │ Context (per-request: store, role, correlation, │
11
- │ clock, dry_run; can_read?/can_write?)
12
- Composition (factory module)
13
+ │ clock, dry_run; can_read?/can_write?;
14
+ Context.system(store) for infra path)
15
+ │ Operations (facade with .reads/.writes/.refresh) │
13
16
  │ │
14
- │ reads/get.rb writes/put.rb
15
- refresh/worker.rb writes/delete.rb
16
- refresh/orchestrator.rb writes/build.rb
17
- refresh/all.rb writes/accept.rb
18
- writes/publish.rb
17
+ │ reads/{get,list,where,uid,schema_envelope,deps,rdeps,
18
+ published,stale,validate_all,freshness,audit,
19
+ blame,policy_explain}.rb
20
+ │ writes/{put,delete,mv,accept,reject,build,publish}.rb
21
+ refresh/{worker,orchestrator,all}.rb
19
22
  └──────────┬───────────────────────────────┬─────────────────┘
20
23
  │ uses domain │ uses ports
21
24
  ┌─ Domain ─▼─────────────────────────────────────────────────┐
22
- │ Permission NEW: predicate, not Action
23
- │ Freshness::Policy Freshness::Verdict
24
- Freshness::Evaluator Action Outcome
25
+ │ Permission (write/read predicate per zone)
26
+ │ Freshness::{Policy,Verdict,Evaluator}
27
+ │ Action Outcome
28
+ │ Policy::Promotion (proposal accept gates) │
25
29
  └──────────────────────────────────────────┬─────────────────┘
26
30
  │ implements
27
31
  ┌─ Infrastructure ─────────────────────────▼─────────────────┐
28
- │ Store (pure adapter — exposes ports only) │
29
- │ Reader#read_envelope Writer#write_envelope_…
30
- AuditLog, Staleness, Validator, Mover
31
- Manifest (incl. permission_for)
32
- Hooks::Registry EventBus Clock
33
- Refresh::Lock Refresh::Detached
34
- Infra::Publisher (file copy + sentinel)
32
+ │ Store (pure adapter — filesystem + hooks) │
33
+ │ Reader#{get,list,where,uid,deps,rdeps,published,
34
+ stale,validate_all,read_raw_envelope,
35
+ schema_envelope}
36
+ Writer#{write_envelope_to_disk,delete_envelope_from_
37
+ disk,existing_uid_for,ensure_uid,
38
+ enforce_name_match!,serialize_for_put}
39
+ │ AuditLog, Staleness, Validator, Sentinel │
40
+ │ Manifest (Entry, Rules, Schema, permission_for) │
41
+ │ Hooks::{Registry,Dispatcher,Loader,Dsl,Builtin} │
42
+ │ Infra::{Publisher,EventBus,Clock,Refresh::Lock, │
43
+ │ Refresh::Detached} │
44
+ │ Entry::{Markdown,Json,Yaml,Text} (format strategies) │
35
45
  └────────────────────────────────────────────────────────────┘
36
46
 
37
47
  Dependency rule: arrows point DOWN. Domain has zero outbound
38
48
  imports. Application imports Domain + Infra (via ports).
39
49
  ```
40
50
 
41
- ## Read path (`store.get`)
51
+ ## Read path (`ops.reads.get.call(key)`)
42
52
 
43
- 1. CLI verb (or any caller) invokes `store.get(key, as:)`.
44
- 2. `Store#get` constructs `Reads::Get(store, orchestrator)` and calls `.call(key, as:)`.
45
- 3. `Reads::Get#call` reads the envelope from disk via `store.reader.read_raw_envelope(key)`.
46
- 4. Resolves `Manifest::Entry#policy` a `Domain::Freshness::Policy` value.
53
+ 1. CLI verb (or any external caller) builds `ops = Textus::Operations.for(store, role:)` then `ops.reads.get.call(key)`.
54
+ 2. `Operations::Reads#get` returns an `Application::Reads::Get.new(ctx:, orchestrator:)` instance bound to the request context.
55
+ 3. `Reads::Get#call(key)` reads the bare envelope from disk via `@ctx.store.reader.read_raw_envelope(key)`.
56
+ 4. Resolves the manifest rules for the key via `@ctx.store.manifest.rules_for(key)` and extracts the `refresh` policy.
47
57
  5. `Domain::Freshness::Evaluator.call(policy, envelope, now:)` returns a `Verdict`.
48
58
  6. If fresh → annotate envelope (`stale: false`, `refreshing: false`) and return.
49
59
  7. Otherwise `policy.decide(verdict) → Action` (data, not behavior).
50
- 8. `Orchestrator.execute(action, key, as)` interprets the Action:
60
+ 8. `Refresh::Orchestrator#execute(action, key:)` interprets the `Action`:
51
61
  - `Action::Return` → `Outcome::Skipped`
52
- - `Action::RefreshSync` → run Worker inline → `Refreshed | Failed`
53
- - `Action::RefreshTimed(budget_ms:)` → race Worker thread vs budget; on timeout, kill thread, fire `:refresh_detached`, fork+detach child, return `Outcome::Detached`
62
+ - `Action::RefreshSync` → run `Refresh::Worker` inline → `Refreshed | Failed`
63
+ - `Action::RefreshTimed(budget_ms:)` → race Worker thread vs budget; on timeout, kill thread, fire `:refresh_backgrounded`, fork+detach child, return `Outcome::Detached`
54
64
  9. Map outcome → envelope annotations (`stale`, `refreshing`, `refresh_error`) and return.
55
65
 
56
- ## Write path (`store.put`)
66
+ ## Write path (`ops.writes.put.call(key, ...)`)
57
67
 
58
- 1. CLI verb calls `ctx = Composition.context(store, role:)` then `Composition.writes_put(ctx).call(key, ...)`.
59
- 2. `Writes::Put#call` checks `ctx.can_write?(zone)` — raises `write_forbidden` if denied.
60
- 3. Delegates pure I/O to `Store::Writer#write_envelope_to_disk(key, ...)`.
61
- 4. On success, fires `:put` event via the injected `Infra::EventBus`, including `correlation_id` from the Context.
68
+ 1. CLI verb calls `ops = Operations.for(store, role:)` then `ops.writes.put.call(key, meta:, body:, content:, if_etag:, suppress_events:)`.
69
+ 2. `Writes::Put#call` validates the key, resolves the manifest entry, and checks `@ctx.can_write?(mentry.zone)` — raises `WriteForbidden` if denied.
70
+ 3. Delegates raw I/O to `Store::Writer#write_envelope_to_disk(key, mentry:, payload:, ctx:, if_etag:)`, which:
71
+ - Resolves the path via `Manifest#resolve`
72
+ - Serializes via `Entry.for_format(...).serialize(...)`
73
+ - Validates against schema if declared
74
+ - Etag-checks if `if_etag:` provided (raises `EtagMismatch` on conflict)
75
+ - Writes to disk via `File.binwrite`
76
+ - Appends the audit row
77
+ 4. On success, publishes `:entry_put` via the bus, with `store: @ctx.with_role(@ctx.role)`, `key:`, `envelope:`, `correlation_id:`.
62
78
 
63
- The same pattern applies to `Writes::Delete`, `Writes::Build`, `Writes::Accept`, and `Writes::Publish`: each takes a `Context`, checks permissions at the use-case layer, then delegates raw I/O to the corresponding `Store::Writer` or `Infra::Publisher` primitive.
79
+ The same pattern applies to `Writes::{Delete,Mv,Accept,Reject,Build,Publish}`: each takes a `Context`, checks permissions at the use-case layer, delegates raw I/O to `Store::Writer` or `Infra::Publisher`, and fires the matching event.
64
80
 
65
- ## Refresh path (`textus refresh KEY`)
81
+ ## Refresh path (`ops.refresh.worker.run(key)`)
66
82
 
67
- 1. CLI `Verb::Refresh` calls `Textus::Refresh.call(store, key, as:)`.
68
- 2. That shim instantiates `Application::Refresh::Worker` and runs it.
69
- 3. `Worker#run`:
70
- - Resolves the manifest entry, looks up the `:intake` handler.
71
- - Publishes `:refresh_began` via the injected `Infra::EventBus`.
83
+ 1. CLI `Verb::Refresh` builds `ops = Operations.for(store, role: "runner")` then calls `ops.refresh.worker.run(key)`.
84
+ 2. `Refresh::Worker#run(key)`:
85
+ - Resolves the manifest entry, looks up the intake handler via `store.registry.rpc_callable(:resolve_intake, mentry.intake_handler)`.
86
+ - Publishes `:refresh_started` via the bus.
72
87
  - Invokes the handler under a 30s `Timeout.timeout` budget.
73
88
  - On any error: publishes `:refresh_failed`, then re-raises (or wraps in `UsageError`).
74
- - On success: normalizes the return shape, persists via `store.put`, publishes `:refreshed` (unless the etag is unchanged).
89
+ - On success: normalizes the return shape, persists via `Application::Writes::Put` with `suppress_events: true`, publishes `:entry_refreshed` (unless the etag is unchanged).
90
+ 3. The batch entry point is `Application::Refresh::All.call(ctx, prefix:, zone:)` which lists stale entries via `Application::Reads::Stale`, then runs `Worker#run` per entry, returning a summary envelope `{ refreshed: [...], failed: [...], skipped: [...] }`.
91
+
92
+ ## Infrastructure-side hook dispatch
93
+
94
+ The hook bus needs an `Application::Context` even when fired from inside `Store#initialize` or other infrastructure-side code paths. `Application::Context.system(store)` returns a Context with `role: "human"` and a fresh correlation_id, designed for exactly this case — see `lib/textus/store.rb` (the `:store_loaded` publish), `lib/textus/doctor.rb` (the doctor-check view), and `lib/textus/projection.rb` (the transform_rows view).
data/CHANGELOG.md CHANGED
@@ -5,10 +5,325 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  The **gem version** (`0.x.y`) is distinct from the **protocol version**
8
- (currently `textus/2`, embedded in every envelope as `protocol`). The protocol
9
- is additive within a major; a new major would change the wire string.
8
+ (currently `textus/3`, embedded in every envelope as `protocol`). A protocol
9
+ bump is a breaking change that requires a store migration; the gem version
10
+ tracks both additive improvements and breaking protocol bumps independently.
10
11
 
11
- ## [Unreleased]
12
+ ## 0.14.0 — 2026-05-26
13
+
14
+ ### Breaking (Ruby API only — CLI JSON output unchanged)
15
+
16
+ - `Operations.reads.get.call(...)` and every other use case that returned
17
+ an envelope Hash now returns `Textus::Envelope` (a `Data.define`
18
+ instance). Call `envelope.to_h_for_wire` to recover the previous Hash
19
+ shape for JSON serialization. The Hash shape itself is byte-identical.
20
+ - `Operations.writes.build.call(...)` return shape no longer includes
21
+ `published_leaves`. Call `Operations.writes.publish.call(...)`
22
+ separately for that. The CLI verb `textus build` runs both
23
+ automatically, so CLI users see no change.
24
+
25
+ ### Added
26
+
27
+ - `Textus::Application::Writes::Publish` — new use case that copies
28
+ nested-leaf files to their `publish_each` targets. Fires
29
+ `:file_published`.
30
+ - `Operations.writes.publish` — factory exposing the new use case.
31
+ - `Textus::Envelope` (now a class, was a module) — typed accessors for
32
+ `protocol`, `key`, `zone`, `owner`, `path`, `format`, `uid`, `etag`,
33
+ `schema_ref`, `meta`, `body`, `content`, `freshness`. Methods:
34
+ `to_h_for_wire`, `stale?`, `refreshing?`.
35
+
36
+ ### Internal
37
+
38
+ - `Application::Writes::Build` trimmed from 116 LOC to ~50 LOC; now
39
+ only materializes generator-zone entries.
40
+ - All ~17 internal `env["..."]` call sites migrated to typed access.
41
+ - `Envelope.build` no longer carries `# rubocop:disable
42
+ Metrics/ParameterLists` (the `Data.define` member list serves the
43
+ same role more clearly).
44
+
45
+ ### Reference
46
+
47
+ - See [ADR 0007](docs/architecture/decisions/0007-envelope-data-class.md).
48
+
49
+ ## 0.13.1 — 2026-05-26
50
+
51
+ ### Internal
52
+
53
+ - `Manifest::Entry` (260 LOC, 11 responsibilities) decomposed into:
54
+ - `Manifest::Entry::Parser` — raw hash → Entry value object.
55
+ - `Manifest::Entry::Validators::*` — one file per validation rule
56
+ (events, publish_each, inject_intro, index_filename, format_matrix).
57
+ - `Manifest::Entry` (~50 LOC) — value object with attr readers,
58
+ zone-kind predicates, and `publish_target_for`.
59
+ - Each validation rule is now independently testable. Adding a new
60
+ rule is one new file under `lib/textus/manifest/entry/validators/`
61
+ plus one line in `Validators::REGISTERED`.
62
+ - Pattern matches the existing `doctor/check/*` (~15 files, same
63
+ shape).
64
+
65
+ ### Compatibility
66
+
67
+ - `Manifest::Entry` keeps all public attribute readers, predicates,
68
+ and `publish_target_for`. External callers consuming `Entry`
69
+ instances see no change. Embedders who subclassed `Entry` may
70
+ need adjustment.
71
+
72
+ ## 0.13.0 — 2026-05-26
73
+
74
+ ### Added
75
+
76
+ - `Textus::Entry::Base` grows 7 abstract class methods that concrete
77
+ strategies must implement: `nested_glob`, `inject_uid`,
78
+ `enforce_name_match!`, `rewrite_name`, `serialize_for_put`,
79
+ `validate_path_extension`.
80
+ - `Textus::Entry.infer_from_extension(ext)` — registry method
81
+ replacing the deleted `Manifest::EXT_TO_FORMAT` constant.
82
+
83
+ ### Changed
84
+
85
+ - Format-specific branches in `Manifest#nested_glob`,
86
+ `Manifest::Entry#{validate_format_matrix!,resolve_format!,
87
+ validate_index_filename!}`, `Store::Writer#{ensure_uid,
88
+ enforce_name_match!,serialize_for_put}`, and
89
+ `Application::Writes::Mv#rewrite_name_for_mv!` all collapse to
90
+ single-line delegations to `Entry.for_format(fmt).<method>(...)`.
91
+ - The 3 rubocop `Metrics/*` disable comments in
92
+ `Manifest::Entry#validate_format_matrix!` are removed; the method
93
+ is now 3 lines.
94
+
95
+ ### Removed
96
+
97
+ - `Textus::Manifest::EXT_TO_FORMAT` constant. Use
98
+ `Textus::Entry.infer_from_extension(ext)` instead.
99
+
100
+ ### Reference
101
+
102
+ - See [ADR 0006](docs/architecture/decisions/0006-format-strategy-extraction.md).
103
+
104
+ ## 0.12.6 — 2026-05-26
105
+
106
+ ### Examples
107
+
108
+ - New `examples/project/` — demonstrates textus as the context store
109
+ for your own project (identity + runbooks + ADR proposal flow,
110
+ projecting `CLAUDE.md` and `AGENTS.md` at the repo root). Staged as
111
+ a fictional Rails service (`ledger`) so the entries read like a real
112
+ codebase, and the pre-staged ADR proposal is `accept`-runnable
113
+ end-to-end (carries a valid `frontmatter:` payload for its target).
114
+ - Refined `examples/claude-plugin/` — reduced to one entry of each kind
115
+ (one agent, one skill, one command, one identity entry, one output,
116
+ one intake recipe). Removed `bin/`, `Rakefile`, `lefthook.yml`, the
117
+ duplicate `github_folder.rb` recipe, and the per-recipe README.
118
+ - Fixed `examples/claude-plugin/recipes/skill_fanout.rb` — the recipe
119
+ routed inner writes through `store.list/put/delete`, which were
120
+ removed in v0.12.2. Now uses `Operations.writes.{put,delete}` against
121
+ the `Application::Context` that hooks actually receive.
122
+ - Updated `spec/examples/skill_fanout_hook_spec.rb` to test the recipe
123
+ against a Context-like duck type, matching the runtime contract.
124
+
125
+ ## 0.12.5 — 2026-05-26
126
+
127
+ ### Documentation
128
+
129
+ - Rewrote `docs/events.md` to use the textus/3 event names from
130
+ `Hooks::Registry::EVENTS` (`:entry_put`, `:entry_deleted`,
131
+ `:build_completed`, `:entry_renamed`, `:proposal_accepted`,
132
+ `:proposal_rejected`, `:file_published`, `:store_loaded`,
133
+ `:entry_refreshed`, `:refresh_started`, `:refresh_failed`,
134
+ `:refresh_backgrounded`). All hook examples now register against
135
+ events that actually exist.
136
+ - Rewrote `docs/zones.md` to use textus/3 vocabulary (`intake` not
137
+ `inbox`, `write_policy:` not `writable_by:`, `rules:` not
138
+ `policies:`, `runner` role not `script`). Manifest fixtures bump
139
+ to `version: textus/3`.
140
+ - Rewrote `ARCHITECTURE.md` to match the v0.12.4 layering:
141
+ `Operations` facade replaces `Composition`, `Application::Reads/Writes`
142
+ use cases replace direct `Store#get/#put` calls, `Store` is pure
143
+ infrastructure.
144
+ - `SPEC.md` Composition → Operations sweep; removed references to
145
+ deleted Store delegators.
146
+ - `docs/recipe-github-skill-bundle.md` updated to use the
147
+ `Operations.writes.{put,delete}` inner-write surface.
148
+
149
+ ## [0.12.4] — 2026-05-26
150
+
151
+ ### Breaking
152
+
153
+ - Removed `Textus::Store#list`, `#where`, `#deps`, `#rdeps`, `#published`,
154
+ `#stale`, `#validate_all`, `#uid`, `#schema_envelope`, and `#fire_event`.
155
+ Use `Textus::Operations.for(store).reads.<name>.call(...)` instead.
156
+ - Removed `Textus::Store::Writer#delete`, `#accept`, `#reject`. Use
157
+ `Textus::Operations.for(store, role:).writes.<verb>.call(...)`.
158
+ - Removed `Textus::Store::Mover`. The mv use case lives entirely in
159
+ `Textus::Application::Writes::Mv` now.
160
+
161
+ ### Added
162
+
163
+ - `Textus::Application::Reads::{List,Where,Uid,SchemaEnvelope,Deps,Rdeps,Published,Stale,ValidateAll}`.
164
+ - `Textus::Application::Writes::Reject`.
165
+ - `Textus::Application::Context.system(store)` for infrastructure-side
166
+ hook dispatch.
167
+
168
+ ### Internal
169
+
170
+ - Internal call sites (`Projection`, `Schema::Tools`, `Application::Refresh::All`,
171
+ `Doctor::Check::SchemaViolations`) now route reads through `Operations`.
172
+
173
+ See [ADR 0005](docs/architecture/decisions/0005-store-facade-final-removal.md).
174
+
175
+ ## [0.12.3] — 2026-05-26
176
+
177
+ ### Added
178
+ - `textus intro` output now includes an `agent_protocol` block: envelope shape, role-resolution rules, and four canonical recipes (`read`, `write`, `propose`, `refresh`). One `textus intro` call is sufficient orientation for a fresh AI agent to operate the store without consulting `SPEC.md`.
179
+
180
+ ### Compatibility
181
+ - Fully additive. All pre-0.12.3 fields on `Textus::Intro.run` retain their existing keys, types, and shapes. The wire `"protocol"` field continues to hold the string `"textus/3"`.
182
+
183
+ ### Examples
184
+ - `examples/claude-plugin/.textus/templates/claude-root.mustache` now projects the new `agent_protocol` recipes into the rendered `CLAUDE.md` via the existing `inject_intro:` mechanism. A plugin's CLAUDE.md auto-projects the four recipes alongside zone authority — agents reading CLAUDE.md get full orientation without a separate `textus intro` call. This is the canonical pattern for plugin authors who want recipes inline.
185
+ - `examples/claude-plugin/` trimmed to one of each surface (one agent, one skill, one command, one identity entry, one JSON output). Removed: the duplicate `fact-checker` variants, the `marketplace.json` projection (the JSON-output lesson is already shown by `plugin.json`), the degenerate `local_file` intake demo (pulled a file from the same store), and unused demo hooks (`rank_by_recency`, `build-stamp`). File count drops from 51 to 33; manifest from 142 to 99 lines.
186
+
187
+ ## [0.12.2] — 2026-05-26
188
+
189
+ ### Breaking changes
190
+
191
+ - **Removed `Textus::Composition`.** All call sites now go through
192
+ `Textus::Operations.for(store, role:)`. The new facade groups use-cases
193
+ by kind: `ops.writes.put`, `ops.reads.get`, `ops.refresh.worker`, etc.
194
+ No alias, no deprecation warning — internal callers update on upgrade.
195
+ - **Removed `Store#put / #get / #delete / #accept / #reject / #mv`.** These
196
+ were thin shims over `Composition`. Use `Operations` directly.
197
+ - **Removed `Writer#put`** (the explicit "Backward-compat shim").
198
+
199
+ ### Internal
200
+
201
+ - Added `Application::Writes::Mv` use-case wrapping `Store::Mover`.
202
+ - Internal Application callers (Accept, Build, Refresh::Worker, Refresh::All,
203
+ Reads::Blame) no longer re-enter via the top-level facade.
204
+ - Audited `spec/` and removed redundant examples (~-48 LOC; most redundancy
205
+ was already absorbed by call-site migration in earlier tasks).
206
+
207
+ See [ADR 0004](docs/architecture/decisions/0004-operations-rename-and-store-facade-removal.md).
208
+
209
+ ### Migration
210
+
211
+ There is no migration path for `Composition` and the Store facade methods —
212
+ they were internal. External consumers (hooks, custom verbs, gem embedders)
213
+ that referenced these symbols must update:
214
+
215
+ ```ruby
216
+ # Before
217
+ ctx = Textus::Composition.context(store, role: "agent")
218
+ Textus::Composition.writes_put(ctx).call(key, body: "...")
219
+ # or:
220
+ store.put(key, body: "...", as: "agent")
221
+
222
+ # After
223
+ Textus::Operations.for(store, role: "agent").writes.put.call(key, body: "...")
224
+ ```
225
+
226
+ ## 0.12.1 — textus/2 hint fix (2026-05-26)
227
+
228
+ ### Fixed
229
+ - Manifest parser now points textus/2 stores at the 0.11.x stepping-stone
230
+ migrator instead of the misleading "check YAML frontmatter for syntax errors"
231
+ hint. The protocol_version doctor check carried the correct hint already, but
232
+ was unreachable on textus/2 stores because `Store.discover` → `Manifest.load`
233
+ raises before doctor checks run. Surfaced by v0.12.0 release smoke testing.
234
+
235
+ ## 0.12.0 — legacy sweep (2026-05-25)
236
+
237
+ ### Removed (breaking)
238
+ - `Role::LEGACY_RENAMES` (`ai`/`script`/`build` → friendly error). Legacy role
239
+ names now fail with the generic `InvalidRole` error.
240
+ - `Manifest::LEGACY_ZONE_RENAMES` (`inbox` → friendly error).
241
+ - `Hooks::Registry::LEGACY_EVENT_RENAMES` (14 legacy event names → friendly
242
+ error). Legacy events now fail with `unknown event: <name>`.
243
+ - `CLI::LEGACY_VERB_RENAMES` / `CLI::LEGACY_GROUP_RENAMES` and the
244
+ `CommandRenamed` error class.
245
+ - `textus migrate --to=textus/3` verb and `lib/textus/migration/**` (eight
246
+ files, ~924 lines).
247
+ - Eight ad-hoc legacy-key guards in `manifest.rb` / `manifest/entry.rb` /
248
+ `manifest/rules.rb`.
249
+
250
+ ### Added
251
+ - `Manifest::Schema.validate!` — strict-unknown-keys parser. Manifests with
252
+ any unrecognized key fail uniformly with `unknown key 'X' at '<jsonpath>'`.
253
+ - ADR 0003 documenting the sweep and the 0.11.x stepping-stone path.
254
+
255
+ ### Changed
256
+ - `Doctor::Check::ProtocolVersion` hint no longer suggests `textus migrate`
257
+ (the verb is gone); points at 0.11.x docs instead.
258
+ - Test suite consolidated: five batches of disciplined deletions/merges
259
+ (−4 files, −134 LOC from the post-P6 peak). Net effect across the release:
260
+ test suite grew +8.2% LOC to cover new behavior (schema walker, permissive
261
+ audit-log tolerance).
262
+
263
+ ### Migration
264
+ - **From textus/2 (gem ≤0.10.x):** install textus 0.11.x first; run
265
+ `textus migrate --to=textus/3`; then upgrade to 0.12.0.
266
+ - **From 0.11.x:** drop-in upgrade.
267
+
268
+ ## 0.11.0 — textus/3 vocabulary redesign (2026-05-25)
269
+
270
+ **BREAKING:** Protocol bumps to `textus/3`. Stores authored on 0.10.x must run `textus migrate --to=textus/3` before installing 0.11.0. `textus doctor` refuses to operate on un-migrated stores.
271
+
272
+ ### Renamed — actors
273
+
274
+ - `ai` → `agent`, `script` → `runner`, `build` → `builder`. `Role.resolve` rejects legacy names with a one-line migration hint pointing at `--as=<new>`.
275
+
276
+ ### Renamed — zone
277
+
278
+ - `inbox` → `intake`. Directory rename + key prefix update + manifest field handled by the migrator.
279
+
280
+ ### Renamed — manifest schema
281
+
282
+ - `writable_by:` → `write_policy:`; new explicit `read_policy:` on zones (default `[all]`).
283
+ - `policies:` (top-level) → `rules:`. Class rename: `Manifest::Policies` → `Manifest::Rules`.
284
+ - `projection:` and `generator:` unified under `compute: { kind: projection|external, ... }`.
285
+ - `reduce:` (inside compute/projection) → `transform:`.
286
+ - `handler_allowlist:` → `intake_handler_allowlist:`.
287
+ - `promote_requires:` (reserved in textus/2) → `promotion: { requires: [...] }` and is now **enforced** during `textus accept`.
288
+
289
+ ### Renamed — hook events
290
+
291
+ - RPC: `:intake` → `:resolve_intake`, `:reduce` → `:transform_rows`, `:check` → `:validate`.
292
+ - Pub-sub (object_pasttense): `:put` → `:entry_put`, `:deleted` → `:entry_deleted`, `:built` → `:build_completed`, `:mv` → `:entry_renamed`, `:accepted` → `:proposal_accepted`, `:reject` → `:proposal_rejected`, `:published` → `:file_published`, `:loaded` → `:store_loaded`, `:refreshed` → `:entry_refreshed`, `:refresh_began` → `:refresh_started`, `:refresh_detached` → `:refresh_backgrounded`. `:refresh_failed` kept.
293
+ - DSL: single `Textus.on(event, name, **opts) { ... }`. Sugar methods (`Textus.intake`, `Textus.reduce`, `Textus.check`, etc.) and the generic `Textus.hook(...)` form removed.
294
+
295
+ ### Renamed — CLI
296
+
297
+ - Namespaced: `textus key mv`, `textus key normalize` (was `key migrate`), `textus rule list` (was `policy list`), `textus rule explain` (was `policy explain`), `textus refresh stale` (was `refresh-stale`).
298
+ - Top-level mutator `textus mv` removed (use `textus key mv`).
299
+ - Envelope-render flag `--format=json` → `--output=json`. Entry-level `format:` in the manifest is unchanged.
300
+ - Legacy spellings emit a `CommandRenamed` envelope (`code: "command_renamed"`); legacy flags emit `FlagRenamed`.
301
+
302
+ ### Added
303
+
304
+ - `textus migrate --to=textus/3`: idempotent one-shot migrator (manifest YAML rewrite, zone directory rename `inbox` → `intake`, frontmatter owner sweep across `.md`/`.json`/`.yaml`, audit-log marker, hook DSL scanner that reports old call sites).
305
+ - `textus doctor` check `protocol_version`: refuses textus/2 stores.
306
+ - `promotion.requires` predicates: `schema_valid`, `human_accept`. Enforced by `textus accept` for matching rules.
307
+
308
+ ### Internal
309
+
310
+ - `Manifest::Policies` → `Manifest::Rules` (class + file + accessor + doctor check).
311
+ - New errors: `Textus::BadManifest`, `Textus::CommandRenamed`, `Textus::FlagRenamed`.
312
+ - Two new domain classes under `Textus::Domain::Policy::Predicates::` for promotion gating.
313
+ - Migration toolkit under `Textus::Migration::V3::`.
314
+
315
+ ### Migration notes for 0.10.x users
316
+
317
+ 1. Update `Gemfile`: `gem "textus", "~> 0.11"`.
318
+ 2. `bundle update textus`.
319
+ 3. `cd` to each textus store and run `textus migrate --to=textus/3`.
320
+ 4. Review the hook-scanner findings printed at the end of the migrate output. For each call site, replace `Textus.X(:name) { ... }` with the canonical `Textus.on(:Y, :name) { ... }` per the event rename table above.
321
+ 5. Run `textus doctor` — should report `ok: true`.
322
+ 6. Commit the rewritten `.textus/` directory (manifest, audit marker, possibly renamed zone dir).
323
+
324
+ ### Fixed
325
+
326
+ - **`Doctor::Check::IllegalKeys` now honors `index_filename:`.** Previously the doctor walked every file and directory under a nested entry and flagged any whose basename failed the `[a-z0-9][a-z0-9-]*` segment regex — including `SKILL.md` itself and unrelated siblings like `references/foo.md`. With this fix, when an entry declares `index_filename:`, only the parent-directory segments leading to each matching index file are validated; sibling files and unrelated subtrees are not enumerated and are not flagged. `manifest.enumerate` already filtered correctly via the new glob; this brings the doctor check into parity. Two new specs in `spec/doctor_spec.rb` cover (a) `SKILL.md` is not flagged, (b) sibling `references/` files are not flagged. The pre-existing illegal-parent-segment case (e.g. `Bad_Name/SKILL.md`) still reports `key.illegal`.
12
327
 
13
328
  ## 0.10.5 — tech-debt cleanup + `index_filename:` + docs polish (2026-05-25)
14
329
 
data/README.md CHANGED
@@ -7,17 +7,19 @@
7
7
 
8
8
  A context store for codebases that humans and AI agents both have to read and write. Dotted keys, schema-validated entries, role-gated writes, byte-copy publish, an audit log of every change. Built so an agent landing in your repo can run one command (`textus intro`) and know what to read, what to write, and what's off-limits.
9
9
 
10
- Reference implementation in Ruby. Wire format `textus/2`. SPEC: [`SPEC.md`](SPEC.md). Implementation notes: [`docs/`](docs/).
10
+ Reference implementation in Ruby. Wire format `textus/3`. SPEC: [`SPEC.md`](SPEC.md). Implementation notes: [`docs/`](docs/).
11
11
 
12
12
  ## Versioning
13
13
 
14
14
  Two versions, deliberately independent:
15
15
 
16
- - **Protocol wire string:** `textus/2`. Stable; breaking changes require `textus/3`.
17
- - **Gem version:** semver, currently `0.10.3`. The gem version is decoupled from the protocol string — internal refactors bump the gem; only wire-format changes bump the protocol.
16
+ - **Protocol wire string:** `textus/3`. Breaking changes require `textus/4`.
17
+ - **Gem version:** semver, currently `0.14.0`. 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
 
21
+ See [CHANGELOG.md](CHANGELOG.md) for per-release notes.
22
+
21
23
  ## Install
22
24
 
23
25
  ```sh
@@ -49,10 +51,10 @@ You get `.textus/` with all five zone directories, baseline schemas, an empty au
49
51
  sentinels/ # publish bookkeeping
50
52
  zones/
51
53
  identity/ # human-only — identity, voice, decisions
52
- working/ # human / ai / script — day-to-day catalog
53
- inbox/ # script — declared external inputs (actions)
54
- review/ # ai + human — proposals awaiting accept
55
- output/ # build only — computed outputs
54
+ working/ # human / agent / runner — day-to-day catalog
55
+ intake/ # runner — declared external inputs (actions)
56
+ review/ # agent + human — proposals awaiting accept
57
+ output/ # builder only — computed outputs
56
58
  ```
57
59
 
58
60
  Manifest `path:` fields are relative to `.textus/zones/`. So `working.network.org.jane` lives at `.textus/zones/working/network/org/jane.md`.
@@ -65,38 +67,43 @@ 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
69
  textus freshness --zone=output # per-entry fresh/stale/never_refreshed/no_policy
68
- textus policy list # show every policy block
70
+ textus rule list # show every rule block
69
71
  textus audit --limit=20 # query the audit log
70
72
  ```
71
73
 
72
- (All verbs return JSON envelopes by default; pass `--format=json` explicitly if you prefer.)
74
+ (All verbs return JSON envelopes by default; pass `--output=json` explicitly if you prefer.)
73
75
 
74
76
  For the full shape — Claude plugin with agents, skills, commands, pending walkthrough, intake action — see [`examples/claude-plugin/`](examples/claude-plugin/).
75
77
 
76
78
  ## What ships today
77
79
 
78
- - **Per-entry formats.** `format: markdown | json | yaml | text` on a manifest entry. `cat .textus/zones/output/marketplace.json | jq .` works without going through textus — the in-store file *is* the consumer-shaped artifact. Structured outputs carry `_meta` at the top level (`generated_at`, `from`, `template`, `reducer`).
80
+ - **Per-entry formats.** `format: markdown | json | yaml | text` on a manifest entry. `cat .textus/zones/output/marketplace.json | jq .` works without going through textus — the in-store file *is* the consumer-shaped artifact. Structured outputs carry `_meta` at the top level (`generated_at`, `from`, `template`, `transform`).
79
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.
82
+ - **Split build/publish (v0.14.0).** `Application::Writes::Build` materializes generator-zone entries; `Application::Writes::Publish` copies nested leaves to `publish_each` targets. The `textus build` CLI verb calls both and merges results, so wire output is unchanged.
83
+ - **Typed envelopes (v0.14.0).** `Textus::Envelope` is a `Data.define` value object with typed accessors (`.meta`, `.body`, `.etag`, `.uid`, `.freshness`, …). Ruby API callers get IDE help and `NoMethodError` on typos. The CLI JSON wire format is preserved byte-for-byte via `envelope.to_h_for_wire`.
80
84
  - **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.
81
- - **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.
85
+ - **Strict key grammar.** `/^[a-z0-9][a-z0-9-]*$/`, max 8 segments × 64 chars. `textus key normalize --dry-run|--write` rewrites existing stores with illegal segments deterministically.
82
86
  - **`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.
83
87
  - **`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.
84
88
  - **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.
89
+ - **Compute.** Derived entries declare `compute: { kind: projection, ... }` (declarative rows + template) or `compute: { kind: external, ... }` (build runner produces the file; textus tracks sources for staleness). Inside projection computes, `transform:` names the row-shaping hook.
85
90
 
86
91
  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.
87
92
 
88
93
  ## CLI and zones
89
94
 
90
- All verbs accept `--format=json` and return the envelope defined in [SPEC §8](SPEC.md). Write verbs require `--as=<role>` (role resolution: `--as` → `TEXTUS_ROLE` env → `.textus/role` file → default `human`).
95
+ All verbs accept `--output=json` and return the envelope defined in [SPEC §8](SPEC.md). Write verbs require `--as=<role>` (role resolution: `--as` → `TEXTUS_ROLE` env → `.textus/role` file → default `human`). Recognized roles: `human`, `agent`, `runner`, `builder`.
91
96
 
92
97
  - Full verb table — read, write, health, scaffolding — is in [SPEC §9](SPEC.md).
93
- - Zone semantics and the role/`writable_by` mapping live in [SPEC §5](SPEC.md), with a tutorial expansion in [`docs/zones.md`](docs/zones.md).
98
+ - Zone semantics and the role/`write_policy` mapping live in [SPEC §5](SPEC.md), with a tutorial expansion in [`docs/zones.md`](docs/zones.md).
94
99
 
95
100
  `textus intro` prints the same information for the current store: zones, entry families with schemas, registered hooks, write flows, and the verb catalog. Run it inside a store and you get the live picture; reach for the SPEC when you want the contract.
96
101
 
97
102
  ## Compute and publish
98
103
 
99
- Derived entries declare a `projection:` (`select`, `pluck`, `sort_by`, `limit`, optional `reducer`) and either a template under `.textus/templates/` (markdown/text) or a templateless path that lets a reducer shape the output directly (json/yaml). Projections cap at 1000 rows; the vendored Mustache subset caps at depth 8. No partials, no lambdas, no HTML escaping.
104
+ Derived entries declare `compute: { kind: projection, select: ..., pluck: ..., sort_by: ..., limit: ..., transform: name }` and either a template under `.textus/templates/` (markdown/text) or a templateless path that lets a transform hook shape the output directly (json/yaml). Projections cap at 1000 rows; the vendored Mustache subset caps at depth 8. No partials, no lambdas, no HTML escaping.
105
+
106
+ For externally-generated entries, declare `compute: { kind: external, sources: [...] }` — textus tracks the declared sources for staleness; the build runner produces the file.
100
107
 
101
108
  `publish_to: [path]` byte-copies a single derived file to one target. `publish_each: "template/{basename}.md"` on a nested entry byte-copies every leaf to its templated target — substitutes `{leaf}`, `{basename}`, `{key}`, `{ext}`. Sentinels for every published file live under `.textus/sentinels/`. See SPEC §5.2, §5.3, §5.12.
102
109
 
@@ -104,15 +111,15 @@ Derived entries declare a `projection:` (`select`, `pluck`, `sort_by`, `limit`,
104
111
 
105
112
  textus exposes a hook DSL. Drop `.rb` files into `.textus/hooks/` (subdirectories are fine; files load alphabetically by full path). Events:
106
113
 
107
- - `:intake` — bring bytes in from elsewhere (returns `{_meta:, body:}`)
108
- - `:reduce` — transform rows during projection (returns rows)
109
- - `:check` — custom doctor check (returns issues)
110
- - `:put`, `:deleted`, `:refreshed`, `:built`, `:accepted`, `:published`, `:mv`, `:reject`, `:loaded` — react to lifecycle events
111
- - `:refresh_began`, `:refresh_failed`, `:refresh_detached` — background-refresh lifecycle
114
+ - `:resolve_intake` — bring bytes in from elsewhere (returns `{_meta:, body:}`)
115
+ - `:transform_rows` — transform rows during projection (returns rows)
116
+ - `:validate` — custom doctor check (returns issues)
117
+ - `:entry_put`, `:entry_deleted`, `:entry_refreshed`, `:build_completed`, `:proposal_accepted`, `:file_published`, `:entry_renamed`, `:proposal_rejected`, `:store_loaded` — react to lifecycle events
118
+ - `:refresh_started`, `:refresh_failed`, `:refresh_backgrounded` — background-refresh lifecycle
112
119
 
113
120
  ```ruby
114
121
  # Inside .textus/hooks/local_file.rb
115
- Textus.intake(:local_file) do |config:, args:, **|
122
+ Textus.on(:resolve_intake, :local_file) do |config:, args:, **|
116
123
  path = config["path"] or raise "local-file requires intake.config.path"
117
124
  {
118
125
  _meta: { "last_refreshed_at" => Time.now.utc.iso8601, "source_path" => path },
@@ -122,7 +129,7 @@ end
122
129
  ```
123
130
 
124
131
  ```ruby
125
- Textus.reduce(:rank_by_recency) do |rows:, **|
132
+ Textus.on(:transform_rows, :rank_by_recency) do |rows:, **|
126
133
  rows.sort_by { |r| r["updated_at"].to_s }.reverse
127
134
  end
128
135
  ```
@@ -130,18 +137,18 @@ end
130
137
  To keep a batch of stale intake entries current in one shot:
131
138
 
132
139
  ```sh
133
- textus refresh-stale --prefix=working --zone=inbox --as=script
134
- # or just refresh everything stale in the inbox zone:
135
- textus refresh-stale --zone=inbox --as=script
140
+ textus refresh stale --prefix=working --zone=intake --as=runner
141
+ # or just refresh everything stale in the intake zone:
142
+ textus refresh stale --zone=intake --as=runner
136
143
  ```
137
144
 
138
- The primitive `Textus.hook(event, name, **opts) { ... }` is also supported. See SPEC.md §5.10 for the full contract.
145
+ See SPEC.md §5.10 for the full hook contract.
139
146
 
140
147
  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.
141
148
 
142
149
  ## Examples
143
150
 
144
- [`examples/claude-plugin/`](examples/claude-plugin/) — a Claude Code plugin (`voice-tools`) whose entire content surface — agents, skills, commands, `CLAUDE.md`, `plugin.json`, `marketplace.json` — is textus-managed. Demonstrates per-entry formats, `publish_each`, intake actions, in-process reducers and hooks, the AI-propose / human-accept loop, and the `inject_intro:` flag that puts an orientation preamble at the top of `CLAUDE.md`.
151
+ [`examples/claude-plugin/`](examples/claude-plugin/) — a Claude Code plugin (`voice-tools`) whose entire content surface — agents, skills, commands, `CLAUDE.md`, `plugin.json`, `marketplace.json` — is textus-managed. Demonstrates per-entry formats, `publish_each`, intake actions, in-process transforms and hooks, the agent-propose / human-accept loop, and the `inject_intro:` flag that puts an orientation preamble at the top of `CLAUDE.md`.
145
152
 
146
153
  ## Tests
147
154
 
@@ -149,7 +156,7 @@ Schemas (`.textus/schemas/<name>.yaml`) declare field shapes, per-field `maintai
149
156
  bundle exec rspec
150
157
  ```
151
158
 
152
- ~490 examples; includes conformance fixtures A–I from SPEC §12.
159
+ ~637 examples; includes conformance fixtures A–I from SPEC §12.
153
160
 
154
161
  ## Code quality
155
162