textus 0.5.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +104 -1
  3. data/README.md +32 -24
  4. data/SPEC.md +75 -142
  5. data/docs/architecture.md +42 -23
  6. data/lib/textus/builder/pipeline.rb +56 -0
  7. data/lib/textus/builder/renderer/json.rb +42 -0
  8. data/lib/textus/builder/renderer/markdown.rb +22 -0
  9. data/lib/textus/builder/renderer/text.rb +14 -0
  10. data/lib/textus/builder/renderer/yaml.rb +42 -0
  11. data/lib/textus/builder/renderer.rb +17 -0
  12. data/lib/textus/builder.rb +9 -114
  13. data/lib/textus/cli/group/hook.rb +11 -0
  14. data/lib/textus/cli/group/key.rb +12 -0
  15. data/lib/textus/cli/group/schema.rb +13 -0
  16. data/lib/textus/cli/verb/accept.rb +15 -0
  17. data/lib/textus/cli/verb/build.rb +13 -0
  18. data/lib/textus/cli/verb/delete.rb +16 -0
  19. data/lib/textus/cli/verb/deps.rb +12 -0
  20. data/lib/textus/cli/verb/doctor.rb +15 -0
  21. data/lib/textus/cli/verb/get.rb +12 -0
  22. data/lib/textus/cli/verb/hook_run.rb +48 -0
  23. data/lib/textus/cli/verb/hooks.rb +50 -0
  24. data/lib/textus/cli/verb/init.rb +14 -0
  25. data/lib/textus/cli/verb/intro.rb +11 -0
  26. data/lib/textus/cli/verb/list.rb +14 -0
  27. data/lib/textus/cli/verb/migrate_keys.rb +16 -0
  28. data/lib/textus/cli/verb/mv.rb +17 -0
  29. data/lib/textus/cli/verb/published.rb +11 -0
  30. data/lib/textus/cli/verb/put.rb +50 -0
  31. data/lib/textus/cli/verb/rdeps.rb +12 -0
  32. data/lib/textus/cli/verb/refresh.rb +15 -0
  33. data/lib/textus/cli/verb/schema.rb +12 -0
  34. data/lib/textus/cli/verb/schema_diff.rb +12 -0
  35. data/lib/textus/cli/verb/schema_init.rb +16 -0
  36. data/lib/textus/cli/verb/schema_migrate.rb +16 -0
  37. data/lib/textus/cli/verb/stale.rb +14 -0
  38. data/lib/textus/cli/verb/uid.rb +12 -0
  39. data/lib/textus/cli/verb/where.rb +12 -0
  40. data/lib/textus/cli.rb +23 -42
  41. data/lib/textus/doctor/check/audit_log.rb +50 -0
  42. data/lib/textus/doctor/check/hooks.rb +29 -0
  43. data/lib/textus/doctor/check/illegal_keys.rb +49 -0
  44. data/lib/textus/doctor/check/manifest_files.rb +38 -0
  45. data/lib/textus/doctor/check/schema_violations.rb +22 -0
  46. data/lib/textus/doctor/check/schemas.rb +26 -0
  47. data/lib/textus/doctor/check/sentinels.rb +57 -0
  48. data/lib/textus/doctor/check/templates.rb +26 -0
  49. data/lib/textus/doctor/check/unowned_schema_fields.rb +34 -0
  50. data/lib/textus/doctor/check.rb +30 -0
  51. data/lib/textus/doctor.rb +23 -289
  52. data/lib/textus/entry/base.rb +30 -0
  53. data/lib/textus/entry/json.rb +5 -1
  54. data/lib/textus/entry/markdown.rb +1 -1
  55. data/lib/textus/entry/text.rb +1 -1
  56. data/lib/textus/entry/yaml.rb +5 -1
  57. data/lib/textus/entry.rb +0 -5
  58. data/lib/textus/envelope.rb +30 -0
  59. data/lib/textus/hooks/builtin.rb +70 -0
  60. data/lib/textus/hooks/dispatcher.rb +49 -0
  61. data/lib/textus/hooks/loader.rb +26 -0
  62. data/lib/textus/hooks/registry.rb +73 -0
  63. data/lib/textus/init.rb +13 -10
  64. data/lib/textus/intro.rb +17 -19
  65. data/lib/textus/key/distance.rb +55 -0
  66. data/lib/textus/key/grammar.rb +33 -0
  67. data/lib/textus/key/path.rb +17 -0
  68. data/lib/textus/manifest/entry.rb +199 -0
  69. data/lib/textus/manifest.rb +10 -34
  70. data/lib/textus/migrate_keys.rb +1 -1
  71. data/lib/textus/projection.rb +5 -4
  72. data/lib/textus/proposal.rb +1 -1
  73. data/lib/textus/refresh.rb +11 -11
  74. data/lib/textus/schema/tools.rb +89 -0
  75. data/lib/textus/store/audit_log.rb +71 -0
  76. data/lib/textus/store/mover.rb +19 -16
  77. data/lib/textus/store/reader.rb +67 -0
  78. data/lib/textus/store/staleness.rb +10 -19
  79. data/lib/textus/store/validator.rb +11 -8
  80. data/lib/textus/store/view.rb +29 -0
  81. data/lib/textus/store/writer.rb +132 -0
  82. data/lib/textus/store.rb +28 -224
  83. data/lib/textus/version.rb +1 -1
  84. data/lib/textus.rb +14 -67
  85. metadata +73 -40
  86. data/lib/textus/audit_log.rb +0 -67
  87. data/lib/textus/builtin_actions.rb +0 -68
  88. data/lib/textus/cli/accept.rb +0 -13
  89. data/lib/textus/cli/action.rb +0 -51
  90. data/lib/textus/cli/build.rb +0 -11
  91. data/lib/textus/cli/delete.rb +0 -14
  92. data/lib/textus/cli/deprecated_alias.rb +0 -31
  93. data/lib/textus/cli/deps.rb +0 -10
  94. data/lib/textus/cli/doctor.rb +0 -13
  95. data/lib/textus/cli/extension_group.rb +0 -9
  96. data/lib/textus/cli/extensions.rb +0 -49
  97. data/lib/textus/cli/get.rb +0 -10
  98. data/lib/textus/cli/init.rb +0 -12
  99. data/lib/textus/cli/intro.rb +0 -9
  100. data/lib/textus/cli/key_group.rb +0 -10
  101. data/lib/textus/cli/list.rb +0 -12
  102. data/lib/textus/cli/migrate.rb +0 -41
  103. data/lib/textus/cli/migrate_keys.rb +0 -19
  104. data/lib/textus/cli/mv.rb +0 -20
  105. data/lib/textus/cli/published.rb +0 -9
  106. data/lib/textus/cli/put.rb +0 -48
  107. data/lib/textus/cli/rdeps.rb +0 -10
  108. data/lib/textus/cli/refresh.rb +0 -13
  109. data/lib/textus/cli/schema.rb +0 -10
  110. data/lib/textus/cli/schema_diff.rb +0 -15
  111. data/lib/textus/cli/schema_group.rb +0 -33
  112. data/lib/textus/cli/schema_init.rb +0 -19
  113. data/lib/textus/cli/schema_migrate.rb +0 -19
  114. data/lib/textus/cli/stale.rb +0 -12
  115. data/lib/textus/cli/uid.rb +0 -15
  116. data/lib/textus/cli/where.rb +0 -10
  117. data/lib/textus/extension_registry.rb +0 -61
  118. data/lib/textus/extensions.rb +0 -33
  119. data/lib/textus/key_distance.rb +0 -53
  120. data/lib/textus/manifest_entry.rb +0 -185
  121. data/lib/textus/migrate_v2.rb +0 -27
  122. data/lib/textus/schema_tools.rb +0 -87
  123. data/lib/textus/store/events.rb +0 -31
  124. data/lib/textus/store_view.rb +0 -27
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 997772ea1cfaafc9f28bd57eda51cde19911ca527893c658aa6edb1329b1e2e6
4
- data.tar.gz: a134c101fedfb2cd84c6cc21ab0522597ee1252cdaeaa84bc1b25bf977676538
3
+ metadata.gz: e4b96d4cff83e901df4b2871b57f9c86d071b11126fd42e8fc5563abf45ce810
4
+ data.tar.gz: 6afa20a10f7ddee98b5b4adbf6eb87218725b5eb1c3f7b181b8bcb462d4fff74
5
5
  SHA512:
6
- metadata.gz: 666ac21f47159341408b2399f9051d0dad0fab406d08f73a83aa468ea245aa211f16b9749ccf80668dbdd304c5125fbd87644c7e16301c890538749c4a1c0b5e
7
- data.tar.gz: f15b77854967509b1c4918dd8452557949f5f42ce6e1ebcd928b3225596d36dff043bf47eca632c8f46b8ba29873cd26792a09109430903402a6b52e3ac51d66
6
+ metadata.gz: 83825a241b91ac10c2024ebdf04ec0f1bb753b8deeded27894fbbbff84f3f654ad6cc4c0faa2bbfcf940b989bc0393d832ae90af837039814fe51e5f7e4c4515
7
+ data.tar.gz: 57079b174111a5e89637140d53ca83f5c9b0c5f777c270be9833174c3dd11c7f4d8b8039ef30c69c6f85e1dfb9d78c89dab3eb99cd517b32e1cf7af2ce7ded7a
data/CHANGELOG.md CHANGED
@@ -8,7 +8,110 @@ 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
- ## [Unreleased]
11
+ ## 0.8.1 — Terminology cleanup (2026-05-21)
12
+
13
+ ### Breaking — intro output
14
+ - `textus intro` JSON: the `"extensions"` key is renamed to `"hooks"`. Consumers
15
+ reading `env["extensions"]` must switch to `env["hooks"]`. Wire protocol
16
+ remains `textus/2`; envelope shape on read/write is unchanged.
17
+
18
+ ### Internal Ruby renames
19
+ - `Textus::Store#load_extensions` → `Textus::Store#load_hooks`.
20
+ - `Textus::Intro.extensions_for` → `Textus::Intro.hooks_for`.
21
+ - Error string `"failed loading extension <file>"` → `"failed loading hook <file>"`.
22
+
23
+ ### Fixed
24
+ - `textus doctor` `:check`-hook failure hint pointed to `.textus/extensions/`,
25
+ which has never existed in 0.6+. Now correctly points to `.textus/hooks/`.
26
+
27
+ ### Docs
28
+ - SPEC.md §5.10: "single extension verb" → "single hook verb".
29
+ - Scaffolded `.textus/hooks/README.md` no longer mixes "hook" and "extension"
30
+ terminology.
31
+
32
+ ## 0.8.0 — Folder restructure & Zeitwerk autoload (2026-05-21)
33
+
34
+ ### Breaking — internal Ruby renames
35
+ Internal Ruby constants renamed. No deprecation aliases; downstream code referencing internals must update directly.
36
+ - `Textus::EventBus` → `Textus::Hooks::Dispatcher`
37
+ - `Textus::HookRegistry` → `Textus::Hooks::Registry`
38
+ - `Textus::BuiltinHooks` → `Textus::Hooks::Builtin`
39
+ - `Textus::Extensions` (module) → `Textus::Hooks::Loader`
40
+ - `Textus::StoreView` → `Textus::Store::View`
41
+ - `Textus::AuditLog` → `Textus::Store::AuditLog`
42
+ - `Textus::ManifestEntry` → `Textus::Manifest::Entry`
43
+ - `Textus::KeyDistance` → `Textus::Key::Distance`
44
+ - `Textus::Path` → `Textus::Key::Path`
45
+ - `Textus::SchemaTools` → `Textus::Schema::Tools`
46
+ - `Textus::CLI::<Verb>` → `Textus::CLI::Verb::<Verb>` (all 23 verbs)
47
+ - `Textus::CLI::<Name>Group` → `Textus::CLI::Group::<Name>` (key, schema, hook)
48
+ - `Textus::Doctor::Check::Extensions` → `Textus::Doctor::Check::Hooks`
49
+ - `Hooks::Registry#initialize` keyword `bus:` renamed to `dispatcher:`.
50
+
51
+ ### Breaking — doctor CLI surface
52
+ - `textus doctor --check=extensions` → `textus doctor --check=hooks`. The check name listed in `ALL_CHECKS` and the SPEC §10.2 enumeration changes from `"extensions"` to `"hooks"`, matching the hook subsystem rename in 0.6.
53
+ - Doctor issue `code` for broken hook files: `extension.load_failed` → `hook.load_failed`.
54
+ - Doctor::Check::Hooks now inspects `.textus/hooks/` (matches `Store#load_extensions`). Previously inspected `.textus/extensions/`, which was the pre-0.6 directory — the check was dead code on any store created with current `textus init`.
55
+
56
+ ### Added
57
+ - `Textus::Entry::Base` — explicit strategy interface for entry formats. Concrete strategies inherit and override.
58
+ - `Textus::Builder::Renderer` — explicit base for output renderers.
59
+ - `Textus::Doctor::Check` — explicit base for doctor checks. Each builtin check (9 total) is now its own file under `lib/textus/doctor/check/`.
60
+
61
+ ### Changed
62
+ - Per-format schema validation moved from `Store::Reader`/`Store::Writer` onto `Entry::Base#validate_against`. Reader/Writer no longer carry a `case mentry.format` switch.
63
+ - `Textus::Doctor` reduced to an orchestrator; the 9 builtin checks live under `Doctor::Check::*`.
64
+ - `lib/textus.rb` switched to Zeitwerk autoload. The manual `require_relative` tree (75 lines) is gone.
65
+ - `lib/textus/builder/renderers/` directory renamed to `renderer/` (singular) to match `Builder::Renderer::*` namespace.
66
+
67
+ ### Migration
68
+ External code referencing the old internal constants must rename. `Textus.hook`, `Textus.with_registry`, the entire CLI surface, and the `textus/2` wire format are unchanged. The published API (`Store`, `Manifest`, `Envelope`, `Etag`, `Role`, `Error` hierarchy, `Builder`, `Doctor`, `Refresh`, `Init`, `CLI.run`) is unchanged.
69
+
70
+ ## 0.7.0 — Reader/Writer split, EventBus, Builder pipeline (2026-05-21)
71
+
72
+ ### Added
73
+ - `Textus::EventBus` is now the publish/subscribe core for lifecycle events. Embedded callers can `store.bus.subscribe(:put, :name) { ... }` outside the `.textus/hooks/` directory. Hook semantics, audit behavior, and the 2-second timeout are unchanged.
74
+
75
+ ### Changed
76
+ - Internal: extracted `Textus::Path` and `Textus::Envelope` value modules; `Manifest`, `Store`, `Staleness`, and `Builder` now share the same path/envelope construction.
77
+ - Internal: split `Textus::Store` into `Store::Reader` and `Store::Writer`. Public API unchanged. `Mover`, `Validator`, and `Staleness` now take explicit collaborators instead of the full store.
78
+ - Internal: removed `Store::Events`; replaced by the bus.
79
+ - Internal: restructured `Textus::Builder` as a step pipeline (`LoadSources → Project → Render → Write`) with one renderer per format (`markdown/text/json/yaml`). Adding a new output format is now a single-file change.
80
+
81
+ ## 0.6.1 — Deprecation cleanup
82
+
83
+ ### Breaking
84
+ - Flat verb aliases promised "removed in 0.6" are now actually removed:
85
+ - `textus mv` → `textus key mv`
86
+ - `textus uid` → `textus key uid`
87
+ - `textus migrate-keys` → `textus key migrate`
88
+ - `textus schema-init` → `textus schema init`
89
+ - `textus schema-diff` → `textus schema diff`
90
+ - `textus schema-migrate` → `textus schema migrate`
91
+ - `textus schema KEY` (positional) → `textus schema show KEY`
92
+ - `textus action NAME` → `textus hook run NAME`
93
+ - `Textus::CLI::Action` class renamed to `Textus::CLI::HookRun`; file `cli/action.rb` → `cli/hook_run.rb`.
94
+ - `Textus::CLI::DeprecatedAliasMixin` module deleted (no remaining users).
95
+ - `textus migrate v2` command removed along with `Textus::MigrateV2` module and `Textus::CLI::Migrate` class. The migration was a one-line manifest rewrite (`version: textus/1` → `version: textus/2`); on-disk entry shapes never changed. To upgrade a `textus/1` manifest, edit `.textus/manifest.yaml` directly. `Manifest.load` still detects the old version and prints the exact edit in its error message.
96
+
97
+ ## 0.6.0 — Hook unification
98
+
99
+ ### Breaking
100
+ - Four DSL verbs (`Textus.action`, `Textus.reducer`, `Textus.hook`, `Textus.doctor_check`) collapsed into one: `Textus.hook(event, name, **opts) { ... }`.
101
+ - `ExtensionRegistry` class renamed to `HookRegistry`.
102
+ - `.textus/extensions/` directory renamed to `.textus/hooks/`. No back-compat read.
103
+ - Manifest: `source.action:` → `source.fetch:` (also renames the registry event from `:action` to `:fetch` and the `ManifestEntry#action`/`#action_config` accessors to `#fetch`/`#fetch_config`).
104
+ - Manifest: `projection.reducer:` → `projection.reduce:`.
105
+ - CLI: `textus extension list` → `textus hook list`; output rows keyed by `event`+`mode` instead of `kind`.
106
+ - CLI: `textus put --action=NAME` → `textus put --fetch=NAME`.
107
+ - Pub-sub hooks gain an optional `keys:` glob filter for per-key scoping.
108
+ - Hook signatures standardized: `store:` is now mandatory and first on every hook (`:reduce` previously had no `store:`). Event-specific kwargs follow.
109
+ - `:accept` event renames `pending_key:` → `key:` to match every other lifecycle event.
110
+ - All event names are now verbs in a uniform grammar (RPC verbs `:fetch :reduce :check`; pub-sub verbs `:put :delete :refresh :build :accept`).
111
+
112
+ ### New
113
+ - `EVENTS` metadata table on `HookRegistry` is the single source of truth for event names, argument shapes, return shapes, and failure semantics (rpc vs pubsub).
114
+ - Shape-check at registration: callable kwargs are verified against the EVENTS table at load time; mismatched signatures raise `UsageError` immediately instead of surfacing at fire time.
12
115
 
13
116
  ## 0.5.0 — Wire protocol `textus/2`; CLI restructure; Store split (breaking)
14
117
 
data/README.md CHANGED
@@ -7,14 +7,14 @@
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/1`. SPEC: [`SPEC.md`](SPEC.md). Implementation notes: [`docs/`](docs/).
10
+ Reference implementation in Ruby. Wire format `textus/2`. 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/1`. Stable; breaking changes require `textus/2`.
17
- - **Gem version:** semver, currently `0.2.0`. Gem `0.x.y` and `1.x` both speak `textus/1`.
16
+ - **Protocol wire string:** `textus/2`. Stable; breaking changes require `textus/3`.
17
+ - **Gem version:** semver, currently `0.8.0`. 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,7 +45,7 @@ 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
- extensions/ # one .rb per action / reducer / hook / doctor_check
48
+ hooks/ # one .rb per hook
49
49
  sentinels/ # publish bookkeeping
50
50
  zones/
51
51
  canon/ # human-only — identity, voice, decisions
@@ -60,23 +60,25 @@ Manifest `path:` fields are relative to `.textus/zones/`. So `working.network.or
60
60
  Read and write:
61
61
 
62
62
  ```sh
63
- textus get working.network.org.jane --format=json
64
- textus list --zone=working --format=json
65
- echo '{"frontmatter":{"name":"bob","org":"acme"},"body":"hi\n"}' \
66
- | textus put working.network.org.bob --as=human --stdin --format=json
67
- textus stale --zone=derived --format=json
63
+ textus get working.network.org.jane
64
+ textus list --zone=working
65
+ echo '{"_meta":{"name":"bob","org":"acme"},"body":"hi\n"}' \
66
+ | textus put working.network.org.bob --as=human --stdin
67
+ textus stale --zone=derived
68
68
  ```
69
69
 
70
+ (All verbs return JSON envelopes by default; pass `--format=json` explicitly if you prefer.)
71
+
70
72
  For the full shape — Claude plugin with agents, skills, commands, pending walkthrough, intake action — see [`examples/claude-plugin/`](examples/claude-plugin/).
71
73
 
72
- ## What 0.2 ships
74
+ ## What ships today
73
75
 
74
76
  - **Per-entry formats.** `format: markdown | json | yaml | text` on a manifest entry. `cat .textus/zones/derived/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`).
75
77
  - **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.
76
78
  - **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.
77
79
  - **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.
78
- - **`textus intro`.** One-shot store orientation: zones with writers + purposes, entry families with schemas and publish targets, loaded extensions, write flows per role, the full CLI verb table. The boot signal for any agent — one tool call and it knows your store.
79
- - **`textus doctor`.** Health check across 8 categories: missing schemas/templates, broken extensions, illegal nested keys, sentinel drift, audit log readability. Returns `ok: true` only when nothing is wrong; warnings and info don't flip the bit.
80
+ - **`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.
81
+ - **`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.
80
82
  - **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.
81
83
 
82
84
  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.
@@ -89,7 +91,7 @@ All verbs accept `--format=json` and return the envelope defined in SPEC §8. Wr
89
91
 
90
92
  | Verb | Purpose |
91
93
  |---|---|
92
- | `intro` | Store orientation: zones, entries, extensions, write flows, CLI map |
94
+ | `intro` | Store orientation: zones, entries, hooks, write flows, CLI map |
93
95
  | `list [--prefix=K] [--zone=Z]` | Enumerate keys |
94
96
  | `where K` | Resolve a key to its filesystem path |
95
97
  | `get K` | Full envelope (frontmatter, body, uid, etag, format) |
@@ -98,14 +100,14 @@ All verbs accept `--format=json` and return the envelope defined in SPEC §8. Wr
98
100
  | `deps K` / `rdeps K` | Forward / reverse projection dependencies |
99
101
  | `published` | List `publish_to:` targets and their backing keys |
100
102
  | `doctor --check=schema_violations` | Validate every entry against its schema |
101
- | `extension list [--kind=K]` | Registered actions, reducers, hooks, doctor_checks |
103
+ | `hook list [--event=E]` | Registered hooks grouped by event (fetch, reduce, check, put, delete, refresh, build, accept) |
102
104
 
103
105
  **Write:**
104
106
 
105
107
  | Verb | Role |
106
108
  |---|---|
107
109
  | `put K --stdin --as=R [--action=NAME]` | per zone |
108
- | `extension run NAME [--key=val] [--as=R]` | per zone written (invoke a registered action) |
110
+ | `hook run NAME [--key=val] [--as=R]` | per zone written (invoke a registered fetch hook) |
109
111
  | `delete K --if-etag=E --as=R` | per zone |
110
112
  | `refresh K --as=script` | per zone (typically `script`) |
111
113
  | `key mv old new --as=R [--dry-run]` | per zone (same-zone moves; uid preserved) |
@@ -128,8 +130,6 @@ All verbs accept `--format=json` and return the envelope defined in SPEC §8. Wr
128
130
  | `schema diff NAME` | Compare a schema against entries that claim it |
129
131
  | `schema migrate NAME [--rename=OLD:NEW]` | Rewrite frontmatter keys across affected entries |
130
132
 
131
- **Deprecated (removed in 0.6):** `mv`, `uid`, `migrate-keys`, `schema-init`, `schema-diff`, `schema-migrate`, `extensions`, `action`.
132
-
133
133
  ## Zones and roles
134
134
 
135
135
  | Zone | `writable_by` | Purpose |
@@ -148,16 +148,24 @@ Derived entries declare a `projection:` (`select`, `pluck`, `sort_by`, `limit`,
148
148
 
149
149
  `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.
150
150
 
151
- ## Extensions
151
+ ## Extension points
152
+
153
+ textus exposes one DSL verb:
154
+
155
+ ```ruby
156
+ Textus.hook(event, name, **opts) { |args| ... }
157
+ ```
158
+
159
+ Drop `.rb` files into `.textus/hooks/`. Events:
152
160
 
153
- Four DSL verbs, registered in `.textus/extensions/*.rb`. Each `Store` gets its own registry — no global state.
161
+ - `:fetch` bring bytes in from elsewhere (returns `{frontmatter:, body:}`)
162
+ - `:reduce` — transform rows during projection (returns rows)
163
+ - `:check` — custom doctor check (returns issues)
164
+ - `:put`, `:delete`, `:refresh`, `:build`, `:accept` — react to lifecycle events
154
165
 
155
- - **`Textus.action(:name) do |config:, store:, args:|`** — runs in three invocation modes (intake refresh, `textus action` verb, `put --action`). Returns `{frontmatter:, body:}`, `{content:}`, or `{body:}` when its return is consumed (intake and put-fetch); writes via `store.put` for side-effectful work (verb mode). The store normalizes all three return shapes. Configured via `source.action` in the manifest for intake. Five built-ins ship: `json`, `csv`, `markdown-links`, `ical-events`, `rss`.
156
- - **`Textus.reducer(:name) do |rows:, config:|`** — shapes rows in a derived projection. Pure function. Configured via `projection.reducer`. May return an Array (templated builds) or a Hash (templateless json/yaml).
157
- - **`Textus.hook(:event, :name) do |kwargs|`** — fires on `:put`, `:delete`, `:refresh`, `:build`, or `:accept`. In-process; 2 s timeout per hook; failures land in the audit log as `event_error` rows.
158
- - **`Textus.doctor_check(:name) do |store:|`** — contributes whole-tree validators to `textus doctor`. Returns an array of issue hashes `{code, level, subject, message, fix}` that merge into the doctor report. Timeouts and exceptions surface as `doctor_check.*` issues; they do not abort the doctor run.
166
+ See SPEC.md §5.10 for the full contract.
159
167
 
160
- 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 and §5.11.
168
+ 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.
161
169
 
162
170
  ## Examples
163
171