textus 0.54.2 → 0.55.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.
Files changed (242) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -1
  3. data/README.md +17 -10
  4. data/SPEC.md +39 -11
  5. data/docs/architecture/README.md +23 -11
  6. data/docs/reference/conventions.md +5 -2
  7. data/exe/textus +1 -1
  8. data/lib/textus/boot.rb +64 -93
  9. data/lib/textus/dispatch/binder.rb +48 -0
  10. data/lib/textus/dispatch/contracts.rb +63 -0
  11. data/lib/textus/dispatch/handler_registry.rb +21 -0
  12. data/lib/textus/dispatch/middleware/audit_index.rb +51 -0
  13. data/lib/textus/dispatch/middleware/auth.rb +40 -0
  14. data/lib/textus/dispatch/middleware/base.rb +26 -0
  15. data/lib/textus/dispatch/middleware/binder.rb +20 -0
  16. data/lib/textus/dispatch/middleware/cascade.rb +53 -0
  17. data/lib/textus/dispatch/pipeline.rb +35 -0
  18. data/lib/textus/doctor/check/audit_log.rb +2 -2
  19. data/lib/textus/doctor/check/generator_drift.rb +4 -4
  20. data/lib/textus/doctor/check/orphaned_publish_targets.rb +3 -3
  21. data/lib/textus/doctor/check/protocol_version.rb +2 -2
  22. data/lib/textus/doctor/check/raw_asset_paths.rb +1 -1
  23. data/lib/textus/doctor/check/schema_parse_error.rb +1 -1
  24. data/lib/textus/doctor/check/schema_violations.rb +2 -2
  25. data/lib/textus/doctor/check/schemas.rb +1 -1
  26. data/lib/textus/doctor/check/{notebook_sources.rb → scratchpad_sources.rb} +10 -5
  27. data/lib/textus/doctor/check/sentinels.rb +4 -4
  28. data/lib/textus/doctor/check/templates.rb +1 -1
  29. data/lib/textus/doctor/check/unowned_schema_fields.rb +1 -1
  30. data/lib/textus/doctor/check.rb +9 -10
  31. data/lib/textus/doctor.rb +2 -2
  32. data/lib/textus/errors.rb +8 -0
  33. data/lib/textus/format/base.rb +34 -10
  34. data/lib/textus/format/json.rb +5 -27
  35. data/lib/textus/format/markdown.rb +5 -27
  36. data/lib/textus/format/text.rb +5 -4
  37. data/lib/textus/format/yaml.rb +30 -27
  38. data/lib/textus/format.rb +22 -1
  39. data/lib/textus/handlers/maintenance/boot_store.rb +15 -0
  40. data/lib/textus/handlers/maintenance/doctor_store.rb +15 -0
  41. data/lib/textus/handlers/maintenance/drain_store.rb +21 -0
  42. data/lib/textus/handlers/maintenance/ingest_entry.rb +159 -0
  43. data/lib/textus/handlers/maintenance/jobs_action.rb +21 -0
  44. data/lib/textus/handlers/maintenance/published_entries.rb +17 -0
  45. data/lib/textus/handlers/maintenance/rule_explain.rb +77 -0
  46. data/lib/textus/handlers/maintenance/rule_lint.rb +54 -0
  47. data/lib/textus/handlers/maintenance/rule_list.rb +32 -0
  48. data/lib/textus/handlers/maintenance/schema_envelope.rb +19 -0
  49. data/lib/textus/handlers/read/audit_entries.rb +48 -0
  50. data/lib/textus/handlers/read/blame_entry.rb +71 -0
  51. data/lib/textus/handlers/read/deps_entry.rb +17 -0
  52. data/lib/textus/handlers/read/get_entry.rb +68 -0
  53. data/lib/textus/handlers/read/list_keys.rb +36 -0
  54. data/lib/textus/handlers/read/pulse_entries.rb +66 -0
  55. data/lib/textus/handlers/read/rdeps_entry.rb +21 -0
  56. data/lib/textus/handlers/read/uid_entry.rb +18 -0
  57. data/lib/textus/handlers/read/where_entry.rb +18 -0
  58. data/lib/textus/handlers/write/accept_proposal.rb +39 -0
  59. data/lib/textus/handlers/write/data_mv.rb +55 -0
  60. data/lib/textus/handlers/write/delete_key.rb +17 -0
  61. data/lib/textus/handlers/write/enqueue_job.rb +27 -0
  62. data/lib/textus/handlers/write/key_delete_prefix.rb +32 -0
  63. data/lib/textus/handlers/write/key_mv_prefix.rb +45 -0
  64. data/lib/textus/handlers/write/move_key.rb +80 -0
  65. data/lib/textus/handlers/write/propose_entry.rb +29 -0
  66. data/lib/textus/handlers/write/put_entry.rb +29 -0
  67. data/lib/textus/handlers/write/reject_proposal.rb +29 -0
  68. data/lib/textus/init.rb +5 -7
  69. data/lib/textus/jobs.rb +3 -9
  70. data/lib/textus/manifest/capabilities.rb +4 -4
  71. data/lib/textus/manifest/entry/base.rb +3 -3
  72. data/lib/textus/manifest/entry/publish/subtree_mirror.rb +2 -2
  73. data/lib/textus/manifest/entry/publish/to_paths.rb +2 -2
  74. data/lib/textus/manifest/policy/predicates/author_held.rb +22 -0
  75. data/lib/textus/manifest/policy/predicates/etag_match.rb +18 -0
  76. data/lib/textus/manifest/policy/predicates/fresh_within.rb +13 -0
  77. data/lib/textus/manifest/policy/predicates/lane_deletable_by.rb +31 -0
  78. data/lib/textus/manifest/policy/predicates/lane_writable_by.rb +23 -0
  79. data/lib/textus/manifest/policy/predicates/raw_lane_ingest_only.rb +25 -0
  80. data/lib/textus/manifest/policy/predicates/raw_write_once.rb +24 -0
  81. data/lib/textus/manifest/policy/predicates/schema_valid.rb +41 -0
  82. data/lib/textus/manifest/policy/predicates/target_is_canon.rb +20 -0
  83. data/lib/textus/manifest/policy/predicates.rb +54 -0
  84. data/lib/textus/manifest/policy/retention.rb +1 -1
  85. data/lib/textus/manifest/schema/semantics/cross_field.rb +53 -0
  86. data/lib/textus/manifest/schema/semantics/invariants.rb +125 -0
  87. data/lib/textus/manifest/schema/semantics/migration.rb +83 -0
  88. data/lib/textus/manifest/schema/semantics.rb +11 -216
  89. data/lib/textus/orchestration.rb +55 -0
  90. data/lib/textus/{ports → port}/audit_log.rb +46 -6
  91. data/lib/textus/{ports → port}/build_lock.rb +2 -2
  92. data/lib/textus/{ports → port}/clock.rb +1 -1
  93. data/lib/textus/{ports → port}/publisher.rb +5 -5
  94. data/lib/textus/{core → port}/sentinel.rb +1 -6
  95. data/lib/textus/{ports → port}/sentinel_store.rb +4 -4
  96. data/lib/textus/{ports → port}/storage/file_stat.rb +1 -1
  97. data/lib/textus/port/storage/file_store.rb +49 -0
  98. data/lib/textus/port/storage/interface.rb +17 -0
  99. data/lib/textus/port/store.rb +149 -0
  100. data/lib/textus/{ports → port}/watcher_lock.rb +3 -3
  101. data/lib/textus/produce/engine.rb +1 -11
  102. data/lib/textus/produce/publisher.rb +21 -0
  103. data/lib/textus/schema/registry.rb +42 -0
  104. data/lib/textus/schema/tools.rb +7 -10
  105. data/lib/textus/store/container.rb +173 -0
  106. data/lib/textus/store/cursor.rb +26 -0
  107. data/lib/textus/store/entry/reader.rb +47 -0
  108. data/lib/textus/store/entry/writer.rb +219 -0
  109. data/lib/textus/store/envelope/meta.rb +61 -0
  110. data/lib/textus/store/freshness/drift_detector.rb +93 -0
  111. data/lib/textus/store/freshness/evaluator.rb +20 -0
  112. data/lib/textus/store/freshness/ttl_evaluator.rb +57 -0
  113. data/lib/textus/{core → store}/freshness/verdict.rb +1 -11
  114. data/lib/textus/store/freshness.rb +8 -0
  115. data/lib/textus/store/index/builder.rb +76 -0
  116. data/lib/textus/store/index/lookup.rb +60 -0
  117. data/lib/textus/store/jobs/base.rb +13 -0
  118. data/lib/textus/store/jobs/index.rb +15 -0
  119. data/lib/textus/store/jobs/materialize.rb +15 -0
  120. data/lib/textus/store/jobs/plan.rb +11 -0
  121. data/lib/textus/store/jobs/planner.rb +124 -0
  122. data/lib/textus/store/jobs/queue.rb +162 -0
  123. data/lib/textus/store/jobs/registry.rb +19 -0
  124. data/lib/textus/store/jobs/retention/base.rb +52 -0
  125. data/lib/textus/store/jobs/retention/sweep.rb +55 -0
  126. data/lib/textus/store/jobs/retention.rb +8 -0
  127. data/lib/textus/store/jobs/sweep.rb +21 -0
  128. data/lib/textus/store/jobs/worker.rb +64 -0
  129. data/lib/textus/store/layout.rb +97 -0
  130. data/lib/textus/store.rb +63 -32
  131. data/lib/textus/{surfaces → surface}/cli/group/data.rb +1 -1
  132. data/lib/textus/{surfaces → surface}/cli/group/key.rb +1 -1
  133. data/lib/textus/{surfaces → surface}/cli/group/mcp.rb +1 -1
  134. data/lib/textus/{surfaces → surface}/cli/group/rule.rb +1 -1
  135. data/lib/textus/{surfaces → surface}/cli/group/schema.rb +1 -1
  136. data/lib/textus/{surfaces → surface}/cli/group.rb +2 -2
  137. data/lib/textus/{surfaces → surface}/cli/runner.rb +14 -68
  138. data/lib/textus/surface/cli/sources.rb +41 -0
  139. data/lib/textus/{surfaces → surface}/cli/verb/doctor.rb +5 -6
  140. data/lib/textus/{surfaces → surface}/cli/verb/get.rb +7 -5
  141. data/lib/textus/{surfaces → surface}/cli/verb/init.rb +1 -1
  142. data/lib/textus/{surfaces → surface}/cli/verb/mcp_serve.rb +3 -3
  143. data/lib/textus/surface/cli/verb/put.rb +27 -0
  144. data/lib/textus/{surfaces → surface}/cli/verb/schema_diff.rb +1 -1
  145. data/lib/textus/{surfaces → surface}/cli/verb/schema_init.rb +1 -1
  146. data/lib/textus/{surfaces → surface}/cli/verb/schema_migrate.rb +1 -1
  147. data/lib/textus/{surfaces → surface}/cli/verb/watch.rb +2 -2
  148. data/lib/textus/{surfaces → surface}/cli/verb.rb +3 -8
  149. data/lib/textus/{surfaces → surface}/cli.rb +1 -1
  150. data/lib/textus/surface/mcp/catalog.rb +58 -0
  151. data/lib/textus/{surfaces → surface}/mcp/errors.rb +1 -11
  152. data/lib/textus/surface/mcp/projector.rb +20 -0
  153. data/lib/textus/{surfaces → surface}/mcp/server.rb +24 -35
  154. data/lib/textus/{surfaces → surface}/mcp.rb +2 -2
  155. data/lib/textus/{surfaces → surface}/watcher.rb +8 -8
  156. data/lib/textus/value/call.rb +30 -0
  157. data/lib/textus/value/command.rb +16 -0
  158. data/lib/textus/{core → value}/duration.rb +1 -4
  159. data/lib/textus/value/envelope.rb +90 -0
  160. data/lib/textus/value/etag.rb +39 -0
  161. data/lib/textus/value/payload.rb +7 -0
  162. data/lib/textus/value/result.rb +46 -0
  163. data/lib/textus/value/role.rb +38 -0
  164. data/lib/textus/value/types.rb +13 -0
  165. data/lib/textus/{uid.rb → value/uid.rb} +9 -7
  166. data/lib/textus/verb_registry.rb +417 -0
  167. data/lib/textus/version.rb +1 -1
  168. data/lib/textus/workflow/loader.rb +4 -4
  169. data/lib/textus/workflow/runner.rb +11 -33
  170. data/lib/textus.rb +4 -69
  171. metadata +142 -105
  172. data/lib/textus/action/accept.rb +0 -44
  173. data/lib/textus/action/audit.rb +0 -131
  174. data/lib/textus/action/base.rb +0 -42
  175. data/lib/textus/action/blame.rb +0 -97
  176. data/lib/textus/action/boot.rb +0 -17
  177. data/lib/textus/action/data_mv.rb +0 -69
  178. data/lib/textus/action/deps.rb +0 -34
  179. data/lib/textus/action/doctor.rb +0 -24
  180. data/lib/textus/action/drain.rb +0 -39
  181. data/lib/textus/action/enqueue.rb +0 -53
  182. data/lib/textus/action/get.rb +0 -78
  183. data/lib/textus/action/ingest.rb +0 -203
  184. data/lib/textus/action/jobs.rb +0 -36
  185. data/lib/textus/action/key_delete.rb +0 -36
  186. data/lib/textus/action/key_delete_prefix.rb +0 -44
  187. data/lib/textus/action/key_mv.rb +0 -129
  188. data/lib/textus/action/key_mv_prefix.rb +0 -57
  189. data/lib/textus/action/list.rb +0 -42
  190. data/lib/textus/action/propose.rb +0 -52
  191. data/lib/textus/action/published.rb +0 -24
  192. data/lib/textus/action/pulse.rb +0 -56
  193. data/lib/textus/action/put.rb +0 -52
  194. data/lib/textus/action/rdeps.rb +0 -39
  195. data/lib/textus/action/reject.rb +0 -37
  196. data/lib/textus/action/rule_explain.rb +0 -91
  197. data/lib/textus/action/rule_lint.rb +0 -68
  198. data/lib/textus/action/rule_list.rb +0 -42
  199. data/lib/textus/action/schema_envelope.rb +0 -29
  200. data/lib/textus/action/uid.rb +0 -33
  201. data/lib/textus/action/where.rb +0 -36
  202. data/lib/textus/action/write_verb.rb +0 -44
  203. data/lib/textus/call.rb +0 -28
  204. data/lib/textus/command.rb +0 -41
  205. data/lib/textus/container.rb +0 -26
  206. data/lib/textus/contract/around.rb +0 -29
  207. data/lib/textus/contract/binder.rb +0 -88
  208. data/lib/textus/contract/resources/build_lock.rb +0 -17
  209. data/lib/textus/contract/resources/cursor.rb +0 -26
  210. data/lib/textus/contract/sources.rb +0 -39
  211. data/lib/textus/contract/view.rb +0 -15
  212. data/lib/textus/contract.rb +0 -174
  213. data/lib/textus/core/freshness/evaluator.rb +0 -150
  214. data/lib/textus/core/freshness.rb +0 -11
  215. data/lib/textus/core/retention/sweep.rb +0 -57
  216. data/lib/textus/core/retention.rb +0 -11
  217. data/lib/textus/cursor_store.rb +0 -24
  218. data/lib/textus/envelope/reader.rb +0 -46
  219. data/lib/textus/envelope/writer.rb +0 -209
  220. data/lib/textus/envelope.rb +0 -79
  221. data/lib/textus/etag.rb +0 -36
  222. data/lib/textus/gate/auth.rb +0 -227
  223. data/lib/textus/gate.rb +0 -116
  224. data/lib/textus/jobs/base.rb +0 -23
  225. data/lib/textus/jobs/materialize.rb +0 -20
  226. data/lib/textus/jobs/plan.rb +0 -9
  227. data/lib/textus/jobs/planner.rb +0 -101
  228. data/lib/textus/jobs/retention.rb +0 -48
  229. data/lib/textus/jobs/sweep.rb +0 -27
  230. data/lib/textus/jobs/worker.rb +0 -67
  231. data/lib/textus/layout.rb +0 -91
  232. data/lib/textus/ports/job_store/job.rb +0 -65
  233. data/lib/textus/ports/job_store.rb +0 -123
  234. data/lib/textus/ports/raw_index.rb +0 -61
  235. data/lib/textus/ports/storage/file_store.rb +0 -26
  236. data/lib/textus/role.rb +0 -36
  237. data/lib/textus/schemas.rb +0 -54
  238. data/lib/textus/session.rb +0 -35
  239. data/lib/textus/surfaces/cli/verb/put.rb +0 -30
  240. data/lib/textus/surfaces/mcp/catalog.rb +0 -111
  241. data/lib/textus/surfaces/role_scope.rb +0 -34
  242. data/lib/textus/types.rb +0 -15
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 53f01828833f9c5e6cb0249fe513b92d6afbb210d90cfca10f51c560c8b2e2ca
4
- data.tar.gz: 1956232435c941479290225e20168979beb270a6e2f5874df37c302e170a43e7
3
+ metadata.gz: 68d4176e50949378dcf62b98617c28bcf6f553c196b24f9263a97d8d5c6f382f
4
+ data.tar.gz: 7de0c2d9ee56d71e09cc0821d19b8ffcc00e3573fccc20ef3e610988ec5ff43c
5
5
  SHA512:
6
- metadata.gz: c086755b90faab7ecebdeebd722e9458dbede54c5efa9be212d6d861c999655093166a1f61826aa9e09ebee8cd1cf57996d958bd410c3b682763d911ed44abe5
7
- data.tar.gz: aad04f9470c3de178ad6c762ef3102528cbdc94922fedb79d121ffbadc23acc5dd3f52871f6b3c0c75664afdb968c12fa814b88fbedcbd0d914161e4e1f337ab
6
+ metadata.gz: 34322a4bfd59c817bdbaec436caba5cd8a25a7896065e16633270eb22bed7db20af339f45f7ee8ed632732f3679469328939d45490e3a6ce3528f654b00feba9
7
+ data.tar.gz: 6c4f94a584d8334e7dc68e543852bb687fa662c6e6eb1334cadf9d70ff01576552f71d773d462f03f3c4f0955496fe094c167df090880f473d3e850469f4c3cd
data/CHANGELOG.md CHANGED
@@ -9,6 +9,43 @@ The **gem version** (`0.x.y`) is distinct from the **protocol version**
9
9
  bump is a breaking change that requires a store migration; the gem version
10
10
  tracks both additive improvements and breaking protocol bumps independently.
11
11
 
12
+ ## 0.55.1 — 2026-06-22 — CI fix: converge_now purges done jobs
13
+
14
+ ### Fixed
15
+
16
+ - **publish_tree prune spec** — `converge_now` now purges "done" jobs before reseeding. The SQLite-backed job queue (`INSERT OR IGNORE`) was silently skipping re-enqueues for already-done jobs, so the second materialize never ran and the source-removed file was never pruned from the published tree.
17
+
18
+ ## 0.55.0 — 2026-06-22 — Architecture Deepening Phase 2
19
+
20
+ **ADR-0119**: dry-monad actions, container split, geometry authority.
21
+
22
+ ### Added
23
+
24
+ - **dry-monad Success/Failure actions** (PR #234): all 30 actions now return `Success(result)` or `Failure(code:, message:)` via `Dry::Monads`. `Value::Result.unwrap` converts Failure → `ActionError`. `be_success`/`be_failure` RSpec matchers replace `spec/unit/` tests.
25
+ - **`Container::Infrastructure` / `Container::Coordination`** (PR #234): single `LazyContainer` replaced by two `Data.define` records — Infrastructure owns manifest, file_store, geometry; Coordination owns gate, compositor, workflows, audit_log. Wired at boot via `wire_gate!`.
26
+ - **`Geometry` as sole path authority** (PR #234): `root`, `schemas_dir`, `hooks_dir`, `store_db_path` delegated to `Geometry`. Removed `root` from Infrastructure. High-value `File.join` callers converted (data_mv, ingest, base, loader, etag, schema/tools, 6 doctor checks).
27
+ - **`bin/db` helper**: sqlite3 wrapper for `.textus/.state/store.db` — `bin/db "SELECT * FROM jobs"` or just `bin/db` for interactive shell.
28
+ - **Centralized SQLite index** (PR #228, #233): `Store::Index::Builder` manages entries FTS index in `store.db`.
29
+
30
+ ### Changed
31
+
32
+ - **Gate reads `Action::VERBS` directly** (PR #234): `Gate::VERB_ACTIONS` deleted. Gate dispatches via `Action::VERBS[cmd.verb]`.
33
+ - **Composite actions** (PR #232): `Accept`, `Propose`, `Reject` separated as composite verbs with declarative `chain` steps, never instantiating sibling action classes directly.
34
+ - **`knowledge.decisions` → keyless nested mode**: decision files no longer enumerate in `list`; accessible via `get` with full key.
35
+
36
+ ### Fixed
37
+
38
+ - **CLI `--args` JSON parsing** (PR #233): `Runner.coerce` now `JSON.parse(raw)` for `Hash`-typed args — fixes `textus enqueue --args='{"key":"..."}'` which previously passed the raw JSON string to `Queue::Job`.
39
+ - **adr-log workflow convention drift** (PR #233): `Get.new(key:).call(...)` → `Get.call(key: k, ...)` — the old pattern raised silently since actions became class-method-only.
40
+ - **RuboCop `Lint/NoReturnInBeginEndBlocks`**: enqueue action restructured with method-level `rescue`.
41
+
42
+ ### Removed
43
+
44
+ - `Gate::VERB_ACTIONS` constant (replaced by `Action::VERBS`)
45
+ - `LazyContainer` (replaced by `Container::Infrastructure`/`Coordination`)
46
+ - `spec/unit/` directory + `spec/support/spec_layout.rb` (378 examples removed)
47
+ - Orphaned `.textus/.state/jobs.sqlite3` database file
48
+
12
49
  ## 0.54.2 — 2026-06-18 — ingest dedup, .run → .state rename
13
50
 
14
51
  ### Added
@@ -867,7 +904,7 @@ Protocol remains `textus/3`.
867
904
  `refresh`, `refresh_stale`, `schema`, `rules`). Session state (cursor,
868
905
  role, manifest_etag) held server-side. Manifest drift surfaces as
869
906
  `ContractDrift` (-32001); cursor expiry as `CursorExpired` (-32002).
870
- See [`docs/reference/mcp.md`](docs/reference/mcp.md) and [ADR 0015](docs/architecture/decisions/0015-agent-gate-mcp.md).
907
+ See [ADR 0015](docs/architecture/decisions/0015-agent-gate-mcp.md).
871
908
  - `examples/claude-plugin/.mcp.json` and migrated skills/commands/agents —
872
909
  zero `textus <verb>` shell strings remain in plugin markdown.
873
910
 
data/README.md CHANGED
@@ -1,4 +1,5 @@
1
- <!-- Generated from .textus/data/knowledge/readme.md edit there, then run `textus drain`. Do not hand-edit README.md (it is clobbered on drain and flagged by doctor). ADR 0103. -->
1
+ <!-- Generated from knowledge.readme.* fragments by the artifacts.docs.readme workflow. Edit the fragments under .textus/data/knowledge/readme/ and run `textus drain`. -->
2
+ <!-- This is a fragment of the README. Edit here; the README is composed from fragments by a workflow. -->
2
3
  <p align="center">
3
4
  <picture>
4
5
  <source media="(prefers-color-scheme: dark)" srcset="assets/branding/wordmark-dark.png">
@@ -18,6 +19,8 @@
18
19
 
19
20
  *textus* is Latin for "the fabric a text is woven from" — same root as *context*, from *con-texere*, "to weave together."
20
21
 
22
+
23
+ <!-- This is a fragment of the README. Edit here; the README is composed from fragments by a workflow. -->
21
24
  ## The idea
22
25
 
23
26
  Three actors write to your repo today:
@@ -36,7 +39,7 @@ flowchart LR
36
39
  end
37
40
 
38
41
  human -->|author| knowledge["knowledge<br/>(canon)"]
39
- agent -->|keep| notebook["notebook<br/>(workspace)"]
42
+ agent -->|keep| scratchpad["scratchpad<br/>(workspace)"]
40
43
  agent -->|propose| proposals["proposals<br/>(queue)"]
41
44
  automation -->|drain| artifacts["artifacts<br/>(machine)"]
42
45
  human -->|ingest| raw["raw<br/>(intake)"]
@@ -65,7 +68,7 @@ The point of those lanes is to **build context you can trust**. Place each lane
65
68
  LOW TRUST HIGH TRUST
66
69
  (unreviewed) (authoritative)
67
70
  ┌──────────────────────────┬───────────────────────────────┐
68
- DURABLE │ notebook │ knowledge ★ the goal │
71
+ DURABLE │ scratchpad │ knowledge ★ the goal │
69
72
  (kept) │ agent's working truth │ canon — a human authors │
70
73
  │ durable, but low-trust │ here · the context you ship │
71
74
  ├──────────────────────────┼───────────────────────────────┤
@@ -81,15 +84,17 @@ Without coordination, they overwrite each other and nothing remembers why. textu
81
84
 
82
85
  ```
83
86
  knowledge/ author only — who you are, what you decide, how you sound
84
- notebook/ keep only — agent's own durable lane (bytes climb to knowledge only via propose→accept)
87
+ scratchpad/ keep only — agent's own durable lane (bytes climb to knowledge only via propose→accept)
85
88
  proposals/ propose (agent+human) — proposals waiting on a human accept
86
89
  artifacts/ converge only — machine-maintained: computed outputs + external inputs
87
90
  ```
88
91
 
89
- An agent that tries to write directly into `knowledge/` gets `write_forbidden`. It writes to `proposals/` (to change authoritative content) or its own `notebook/` (for working memory). You accept the good proposals; textus promotes them, records the move, and audits both halves. Stable per-entry `uid:` means a reorganization doesn't break references. A monotonic audit cursor (`textus pulse --since=N`) means the next session — possibly a different agent, possibly a different model — picks up exactly where the last one left off.
92
+ An agent that tries to write directly into `knowledge/` gets `write_forbidden`. It writes to `proposals/` (to change authoritative content) or its own `scratchpad/` (for working memory). You accept the good proposals; textus promotes them, records the move, and audits both halves. Stable per-entry `uid:` means a reorganization doesn't break references. A monotonic audit cursor (`textus pulse --since=N`) means the next session — possibly a different agent, possibly a different model — picks up exactly where the last one left off.
90
93
 
91
94
  That's the load-bearing claim: **coordination is a protocol invariant, not a library convenience.**
92
95
 
96
+
97
+ <!-- This is a fragment of the README. Edit here; the README is composed from fragments by a workflow. -->
93
98
  ## See it in four commands
94
99
 
95
100
  ```sh
@@ -151,7 +156,7 @@ roles:
151
156
 
152
157
  lanes:
153
158
  - { name: knowledge, kind: canon } # author — canonical truth
154
- - { name: notebook, kind: workspace } # keep — agent's own durable lane
159
+ - { name: scratchpad, kind: workspace } # keep — agent's own durable lane
155
160
  - { name: proposals, kind: queue } # propose — proposals awaiting accept
156
161
  - { name: artifacts, kind: machine } # converge — computed outputs + external inputs
157
162
  ```
@@ -165,7 +170,7 @@ lanes:
165
170
  .gitignore # generated — ignores .state/ and any tracked:false entries
166
171
  data/ # one dir per lane; kinds + capabilities are in the manifest above
167
172
  knowledge/ # e.g. identity (knowledge.identity.*), voice, decisions, notes
168
- notebook/
173
+ scratchpad/
169
174
  proposals/
170
175
  artifacts/ # machine lane: computed outputs + external inputs
171
176
  .state/ # disposable runtime state — gitignored, safe to delete (ADR 0038)
@@ -202,7 +207,7 @@ For a worked store — knowledge entries, a staged proposal, schemas, ERB templa
202
207
  - **Agent loop.** `textus boot` orients a fresh session; `textus pulse --since=N` is the per-turn heartbeat (changed entries, pending proposals, index etag for catalog drift detection). ([docs/how-to/agents-mcp.md](docs/how-to/agents-mcp.md))
203
208
  - **MCP surface.** The official `mcp` Ruby SDK drives the stdio JSON-RPC server; protocol version auto-negotiated up to `2025-11-25`. Wire textus into Claude Code, Cursor, or any MCP host in one config block.
204
209
  - **`textus doctor`.** Health checks across schemas, workflow registrations, keys, sentinels, and the audit log.
205
- - **`raw` lane and `ingest` verb.** Write-once intake lane for external URL bookmarks, files, and binary assets. Three source kinds (`url`/`file`/`asset`); daily key derivation; notebook stub per ingest. See "Intake and ingest" section below.
210
+ - **`raw` lane and `ingest` verb.** Write-once intake lane for external URL bookmarks, files, and binary assets. Three source kinds (`url`/`file`/`asset`); daily key derivation; scratchpad stub per ingest. See "Intake and ingest" section below.
206
211
 
207
212
  ## CLI and lanes
208
213
 
@@ -276,8 +281,8 @@ textus ingest url agentskills-io-brainstorming \
276
281
  # see what landed in the raw lane
277
282
  textus list --lane=raw
278
283
 
279
- # a notebook stub was created alongside — annotate it
280
- textus get notebook.notes.raw
284
+ # a scratchpad stub was created alongside — annotate it
285
+ textus get scratchpad.notes.raw
281
286
  ```
282
287
 
283
288
  Stale produced entries are re-materialised by `drain`, not by reads — `get` is a pure read (ADR 0089).
@@ -316,3 +321,5 @@ Lefthook hooks (`brew bundle install` then `lefthook install`) run rubocop on `p
316
321
  ## License
317
322
 
318
323
  [MIT](LICENSE)
324
+
325
+
data/SPEC.md CHANGED
@@ -57,7 +57,7 @@ You **shape your own memory structure** inside `.textus/`. The protocol manages
57
57
  textus/4 names its concepts along six axes. Reviewers who internalize these can map any part of the spec to the right category:
58
58
 
59
59
  - **Actor** — who is interacting: roles such as `human`, `agent`, `automation`, each holding a set of capabilities (`propose`, `author`, `keep`, `converge`).
60
- - **Place** — where data lives: lanes such as `knowledge`, `notebook`, `raw`, `proposals`, `artifacts`.
60
+ - **Place** — where data lives: lanes such as `knowledge`, `scratchpad`, `raw`, `proposals`, `artifacts`.
61
61
  - **Thing** — what is stored: entries, fields, keys.
62
62
  - **Operation** — how you act on things: RPC and CLI verbs (`get`, `put`, `drain`, `serve`, `ingest`, …).
63
63
  - **Event** — what gets fired after an operation: pub-sub events (`:entry_written`, `:entry_produced`, `:entry_published`, …).
@@ -111,7 +111,7 @@ The root is `.textus/` at the project working directory. A typical tree:
111
111
  sentinels/ # byte-copied publish bookkeeping (see §5.3)
112
112
  data/ # ALL user content lives here
113
113
  knowledge/ # lane: knowledge (kind: canon — author-holders write)
114
- notebook/ # lane: notebook (kind: workspace — keep-holders write; agent's own durable lane)
114
+ scratchpad/ # lane: scratchpad (kind: workspace — keep-holders write; agent's own durable lane)
115
115
  proposals/ # lane: proposals (kind: queue — propose-holders write)
116
116
  artifacts/ # lane: artifacts (kind: machine — converge-holders write)
117
117
  raw/ # lane: raw (kind: raw — ingest-holders write; write-once)
@@ -149,7 +149,7 @@ roles:
149
149
  lanes:
150
150
  - name: knowledge
151
151
  kind: canon
152
- - name: notebook
152
+ - name: scratchpad
153
153
  kind: workspace
154
154
  owner: agent # optional, informational — agent's own lane
155
155
  desc: "agent's durable working memory; bytes climb to knowledge only via propose→accept"
@@ -241,7 +241,7 @@ Default scaffold — Setup-1 (roles `human=[author, propose, ingest]`, `agent=[p
241
241
  | Lane | `kind` | Required capability | Writable by (default) | Use case |
242
242
  |---|---|---|---|---|
243
243
  | `knowledge` | `canon` | `author` | `human` | Authored truth: identity, voice, decisions, network. |
244
- | `notebook` | `workspace` | `keep` | `agent` | Agent's own durable working memory. Bytes climb to `knowledge` only via propose→accept. |
244
+ | `scratchpad` | `workspace` | `keep` | `agent` | Agent's own durable working memory. Bytes climb to `knowledge` only via propose→accept. |
245
245
  | `proposals` | `queue` | `propose` | `agent`, `human` | Proposals awaiting human review via `textus accept`. |
246
246
  | `artifacts` | `machine` | `converge` | `automation` | Computed outputs produced by `drain` via the workflow DSL. |
247
247
  | `raw` | `raw` | `ingest` | `human`, `agent`, `automation` | Write-once external source material: URL bookmarks, files, binary assets. |
@@ -284,7 +284,7 @@ Every successful write records the resolved role and a wall-clock timestamp in `
284
284
 
285
285
  #### 5.1.1 Capabilities
286
286
 
287
- Roles declare **capabilities** — verbs from a closed four-element set. A
287
+ Roles declare **capabilities** — verbs from a closed five-element set. A
288
288
  manifest declares a `roles:` block mapping each role name to the capabilities
289
289
  it holds via `can:`:
290
290
 
@@ -296,7 +296,7 @@ roles:
296
296
  - { name: keeper, can: [keep] }
297
297
  ```
298
298
 
299
- Capability allow-list: `propose`, `author`, `keep`, `converge`. The mapping from
299
+ Capability allow-list: `propose`, `author`, `keep`, `converge`, `ingest`. The mapping from
300
300
  lane-kind to its required capability is a **bijection** (ADR 0091, which folded
301
301
  the former `quarantine` + `derived` kinds back into one `machine` kind — undoing
302
302
  the two-kind split of ADR 0090): each capability authorizes exactly one
@@ -308,8 +308,9 @@ lane-kind:
308
308
  | `keep` | `workspace` |
309
309
  | `propose` | `queue` |
310
310
  | `converge` | `machine` |
311
+ | `ingest` | `raw` |
311
312
 
312
- A manifest naming a folded capability — `ingest` or `build`, or the pre-0088
313
+ A manifest naming a folded capability — `build` or the pre-0088
313
314
  spelling `fetch` — in a `can:` list is rejected at load with a hint pointing to
314
315
  `converge` (ADR 0090, 0091, 0111).
315
316
 
@@ -425,7 +426,7 @@ The `raw` lane (`kind: raw`) is a write-once intake lane for external source mat
425
426
 
426
427
  **`access` field** — entries MAY carry `source.access: public | private` (field is `maintained_by: human`). Set `private` for sources not safe to reproduce publicly.
427
428
 
428
- **Notebook stub** — every ingest creates a `notebook.notes` stub with a backlink (`Ingested from raw.<key>`) so the agent or human can annotate the ingested material without touching the write-once record.
429
+ **Notebook stub** — every ingest creates a `scratchpad.notes` stub with a backlink (`Ingested from raw.<key>`) so the agent or human can annotate the ingested material without touching the write-once record.
429
430
 
430
431
  **Example — URL bookmark:**
431
432
 
@@ -438,6 +439,22 @@ textus ingest url agentskills-io-brainstorming \
438
439
 
439
440
  A `get` on a raw entry is a pure read — it returns the entry as stored and never re-fetches (ADR 0089).
440
441
 
442
+ **Raw entry shape.** Every raw-lane entry has a set of required fields
443
+ enforced by the format layer on write:
444
+
445
+ | Field | Required | Description |
446
+ |-------|----------|-------------|
447
+ | `ingested_at` | yes | ISO-8601 timestamp of ingestion |
448
+ | `content_hash` | yes | `sha256:<hex>` of the source content |
449
+ | `source.kind` | yes | One of `url`, `file`, `asset` |
450
+ | `source.url` | conditional | Required when `kind=url` |
451
+ | `source.label` | no | Human-readable label |
452
+ | `body` | no | Present only for `kind=file` |
453
+ | `asset` | no | Present only for `kind=asset` |
454
+
455
+ Tombstone entries (carrying `superseded_by:`) are exempt from shape
456
+ validation — they are stripped-down records pointing to the live entry.
457
+
441
458
  ### 5.5 Pending / accept workflow
442
459
 
443
460
  Proposal entries are full patches authored into the `proposals` queue lane (writable by `propose`-holders: `agent` and `human` by default) — `proposals` in the default scaffold (Setup-1) — typically by agents. The entry's frontmatter describes the patch it proposes against another lane:
@@ -646,6 +663,13 @@ The frontmatter `name:` field, when present, must match the file's basename (wit
646
663
  - Existing files without a uid continue to work. The envelope shows `"uid": null` until a put mints one.
647
664
  - `text` entries have no metadata channel and therefore no uid; their envelope always shows `"uid": null`.
648
665
 
666
+ **`sources:` (Source references).** Entries MAY carry a `sources` array in
667
+ their frontmatter to declare external provenance. Each element is a raw-lane
668
+ key string (starting with `raw.`). The array is preserved on write —
669
+ existing sources carry forward if no new `sources` are provided. The
670
+ envelope returns `sources` as a top-level array when non-empty; omitted
671
+ when absent.
672
+
649
673
  Entries in a `produced` lane SHOULD additionally carry the `generated:` block defined in §5.2. Implementations MUST treat unknown frontmatter fields as warnings, not errors, so build tooling can extend the metadata without breaking conformance.
650
674
 
651
675
  ## 8. Envelope (the wire format)
@@ -665,6 +689,9 @@ Every successful CLI response (`--output=json`) is a single JSON envelope:
665
689
  "etag": "sha256:8f3c…",
666
690
  "schema_ref": "person",
667
691
  "uid": "a1b2c3d4e5f60718",
692
+ "sources": [
693
+ { "key": "raw.2026.06.20.url-mcp-spec", "etag": "sha256:1a2b…", "suspended": false }
694
+ ],
668
695
  "stale": false,
669
696
  "stale_reason": null,
670
697
  "fetching": false
@@ -674,7 +701,7 @@ Every successful CLI response (`--output=json`) is a single JSON envelope:
674
701
  **Field rules:**
675
702
  - `protocol` MUST be the exact string `textus/4`.
676
703
  - `key` MUST be the canonical resolved key.
677
- - `lane` MUST be one of the lanes declared in the manifest (`knowledge`, `notebook`, `feeds`, `proposals`, `artifacts` in the default Setup-1 scaffold).
704
+ - `lane` MUST be one of the lanes declared in the manifest (`knowledge`, `scratchpad`, `proposals`, `artifacts`, `raw` in the default Setup-1 scaffold).
678
705
  - `path` MUST be an absolute filesystem path.
679
706
  - `format` MUST be one of `markdown`, `json`, `yaml`, `text` (§5.12). Absent envelopes are treated as `markdown` for back-compat.
680
707
  - `body` is the raw on-disk bytes as a UTF-8 string for every format.
@@ -682,6 +709,7 @@ Every successful CLI response (`--output=json`) is a single JSON envelope:
682
709
  - `etag` MUST be `sha256:<hex>` of the raw file bytes, computed identically for every format.
683
710
  - `schema_ref` MAY be `null` for entries in subtrees with `schema: null`.
684
711
  - `uid` is the stable Textus UID (§7) if the entry carries one, else `null`. Always present in the envelope.
712
+ - `sources` is an array of source objects. Each object has `key` (the referenced entry's key), `etag` (sha256 snapshot taken at write time, or absent when no snapshot exists), and `suspended` (`true` when the referenced entry's current on-disk etag differs from the stored snapshot — the source changed after this entry was last written). Present only when non-empty.
685
713
  - `stale` is `true` when the entry's `source.ttl` has elapsed and the entry has not yet been re-materialised; `false` otherwise. Only populated for produced entries with a declared `ttl`; always `false` for other entries.
686
714
  - `stale_reason` is a short human-readable string describing why the entry is stale (e.g. `"ttl_exceeded"`, `"never_fetched"`), or `null` when `stale` is `false`.
687
715
  - `fetching` is `true` when a background re-pull is in flight for this entry; `false` otherwise. Callers observing `stale: true, fetching: true` SHOULD retry after a short delay.
@@ -764,7 +792,7 @@ All verbs accept `--output=json` and emit a canonical envelope (success or error
764
792
 
765
793
  `read_verbs` is derived from the MCP verb catalog — the verbs the agent can actually call over its transport — so it lists the read/discovery verbs (`schema_show` for an entry's field shape, `rule_explain` for its retention/guard policy, and the graph reads `where`/`deps`/`rdeps`, ADR 0060) and never the CLI-only `audit`/`doctor`, nor `freshness` (the Ruby-only internal lifecycle scan, ADR 0085) (ADR 0056). An agent learns an entry's `_meta` shape by calling the `schema_show` verb before a `put`/`propose`, not by shelling out to a CLI. The graph reads `deps`/`rdeps` return a structured `{key, deps}`/`{key, rdeps}` envelope on every surface (CLI, Ruby, MCP) — a hash, not a bare array, consistent with the other structured read responses such as `where` (ADR 0060 amendment).
766
794
 
767
- The agent's MCP write surface includes the single-key `key_delete` and `key_mv` tools alongside their bulk `key_delete_prefix`/`key_mv_prefix` cousins (ADR 0060 amendment; the single-key tools were renamed from `delete`/`mv` to share the `key_` family stem in ADR 0082, which also removed the `migrate` YAML-plan orchestrator — its `zone_mv`/`key_mv_prefix`/`key_delete_prefix` ops remain individually callable). All of these apply by default; `dry_run: true` is a uniform opt-in preview that returns a Plan without mutating (ADR 0071 verbs are actions, dry-run is opt-in on every surface). Single-key `key_delete` additionally accepts an optional `if_etag` optimistic-concurrency check. The blast-radius reads (`where`/`deps`/`rdeps`) remain on MCP so an agent can look before it leaps. The promotion verbs `accept` and `reject` are also on MCP (ADR 0072): they are gated by the `author_held` capability floor, not by transport absence — a default-`agent` connection is refused, while a connection launched as a role holding `author` (`--as`/`TEXTUS_ROLE`/`.textus/role`, resolved once at launch per ADR 0040) can promote, closing the propose→accept loop over one transport. `drain` is also on MCP (ADR 0076, ADR 0087, ADR 0110): it is caller-agnostic and its produce jobs self-elevate — materialization always runs as the manifest's `converge`-capable actor regardless of the calling role, granting no authority over content (materialization is a pure, idempotent function of already-accepted canon, ADR 0070); the destructive retention sweep runs as the caller. Each produce job self-acquires the single-writer build lock, so a concurrent CLI, reactive, or background pass cannot collide with an MCP-triggered one — a held lock is a graceful soft-miss (ADR 0110).
795
+ The agent's MCP write surface includes the single-key `key_delete` and `key_mv` tools alongside their bulk `key_delete_prefix`/`key_mv_prefix` cousins (ADR 0060 amendment; the single-key tools were renamed from `delete`/`mv` to share the `key_` family stem in ADR 0082, which also removed the `migrate` YAML-plan orchestrator — its `data_mv`/`key_mv_prefix`/`key_delete_prefix` ops remain individually callable). The structural mutation verbs (`key_mv`, `key_mv_prefix`, `key_delete_prefix`, `data_mv`) accept `dry_run: true` as an opt-in preview that returns a Plan without mutating (ADR 0071). `drain` does not support `dry_run` (it is async-only, ADR 0110). Single-key `key_delete` additionally accepts an optional `if_etag` optimistic-concurrency check. The blast-radius reads (`where`/`deps`/`rdeps`) remain on MCP so an agent can look before it leaps. The promotion verbs `accept` and `reject` are also on MCP (ADR 0072): they are gated by the `author_held` capability floor, not by transport absence — a default-`agent` connection is refused, while a connection launched as a role holding `author` (`--as`/`TEXTUS_ROLE`/`.textus/role`, resolved once at launch per ADR 0040) can promote, closing the propose→accept loop over one transport. `drain` is also on MCP (ADR 0076, ADR 0087, ADR 0110): it is caller-agnostic and its produce jobs self-elevate — materialization always runs as the manifest's `converge`-capable actor regardless of the calling role, granting no authority over content (materialization is a pure, idempotent function of already-accepted canon, ADR 0070); the destructive retention sweep runs as the caller. Each produce job self-acquires the single-writer build lock, so a concurrent CLI, reactive, or background pass cannot collide with an MCP-triggered one — a held lock is a graceful soft-miss (ADR 0110).
768
796
 
769
797
  `latest_seq` is the current high-water mark of the audit log; agents should use it as the starting cursor for `pulse`.
770
798
 
@@ -792,7 +820,7 @@ The agent's MCP write surface includes the single-key `key_delete` and `key_mv`
792
820
 
793
821
  `if_etag` is optional on both `put` and `key_delete`. When provided, the write fails with `etag_mismatch` if the on-disk file's etag differs. When omitted, the write is unconditional (last-writer-wins).
794
822
 
795
- The lifecycle scan behind `pulse.stale`/`pulse.next_due_at` reports, per entry, one verdict (`fresh`, `expired`, or `no_policy`) against each intake entry's `source.ttl`. ADR 0085 removed the standalone `freshness` verb that used to render these rows; the scan is now Ruby-only (consumed by `pulse` and the hook context), and human drill-down into a single entry's verdict is `textus get KEY` (carries `stale`/`stale_reason`) plus `textus rule_explain KEY` (the `source.ttl` and retention policy). `textus drain` enqueues the convergence jobs — produce every in-scope derived entry, re-pull every stale intake entry, and a retention sweep — then drains the queue to empty (§5.11). Convergence is async-only (ADR 0110): there is no `--dry-run`.
823
+ The lifecycle scan runs per-entry at `get` time each `get` response carries `stale`/`stale_reason` when the entry has a TTL rule. ADR 0085 removed the standalone `freshness` verb; human drill-down into a single entry's verdict is `textus get KEY` (carries `stale`/`stale_reason`) plus `textus rule_explain KEY` (the `source.ttl` and retention policy). `pulse` does not include a `stale` list. `textus drain` enqueues the convergence jobs — produce every in-scope derived entry, re-pull every stale intake entry, and a retention sweep — then drains the queue to empty (§5.11). Convergence is async-only (ADR 0110): there is no `--dry-run`.
796
824
 
797
825
  `textus accept K --as=human` promotes a pending entry into its target lane: it copies the patch body into the target key, deletes the pending entry, and writes one audit line per side (§audit). Only a role holding the `author` capability (the trust anchor — `human` by default) may invoke `accept`.
798
826
 
@@ -6,7 +6,8 @@
6
6
  ```mermaid
7
7
  flowchart TD
8
8
  surfaces["Surfaces — CLI · MCP · RoleScope"]
9
- contract["Contract — per-verb DSL (source of truth for public interfaces)"]
9
+ contract["Contract — per-verb DSL (Arg, Spec, DSL)"]
10
+ dispatch["Dispatch — Binder · Around · CommandBuilder · Dispatcher"]
10
11
  gate["Gate — routing (VERB_COMMAND) · Auth"]
11
12
  action["Actions — per-verb use cases (action/)"]
12
13
  jobs["Jobs — Worker · Planner · Materialize · Retention · Sweep"]
@@ -15,8 +16,10 @@ flowchart TD
15
16
  manifest["Manifest — declarative config, no IO (policy/, schema/, entry/)"]
16
17
  core["Core — pure value types (Freshness, Retention, Duration, Sentinel)"]
17
18
  ports["Ports — IO adapters (FileStore, AuditLog, JobStore, Publisher…)"]
18
- surfaces --> contract
19
- contract --> gate
19
+ surfaces --> dispatch
20
+ surfaces -.->|declare| contract
21
+ contract --> dispatch
22
+ dispatch --> gate
20
23
  gate --> action
21
24
  action --> jobs
22
25
  action --> produce
@@ -27,7 +30,7 @@ flowchart TD
27
30
  jobs --> produce
28
31
  ```
29
32
 
30
- *Dependency rule: inward only.* Surfaces → Contract → Gate → Actions → inner layers. Actions never reference surfaces.
33
+ *Dependency rule: inward only.* Surfaces → Dispatch → Gate → Actions → inner layers. Actions never reference surfaces.
31
34
 
32
35
  ### What lives in each layer
33
36
 
@@ -35,18 +38,27 @@ flowchart TD
35
38
 
36
39
  ```
37
40
  surfaces/cli/ CLI command generation from contracts
41
+ sources.rb CLI-only input acquisition (stdin parsing, file sourcing, coercion)
38
42
  surfaces/mcp/ MCP server — stdio JSON-RPC 2.0, tools derived from contracts
39
43
  surfaces/role_scope.rb
40
- (Store#as(role)) — holds (container, role, dry_run, correlation_id);
44
+ (Store#with_role(role)) — holds (container, role, dry_run, correlation_id);
41
45
  all verb methods injected via define_method in textus.rb
42
46
  ```
43
47
 
44
- **Contract**
48
+ **Contract + Dispatch**
45
49
 
46
50
  ```
47
- contract/ Per-verb DSL — verb, summary, surfaces, arg, view.
48
- Contract::Binder.inputs_from_ordered splits the uniform inputs hash
49
- into positional/keyword args for every surface.
51
+ contract/ Per-verb DSL — verb, summary, surfaces, arg, view, around.
52
+ Pure declaration: Spec and DSL only.
53
+
54
+ dispatch/ Runtime machinery consumed by every surface.
55
+ binder.rb Binder — validates required args, resolves session/literal defaults.
56
+ around.rb Around — registry of stateful wrappers (cursor, build_lock).
57
+ view.rb View.render(spec, surface, result, inputs) — output shaping.
58
+ command_builder.rb
59
+ CommandBuilder — command construction + role injection.
60
+ dispatcher.rb Dispatcher — unified pipeline: Around → Binder → Command → Gate.
61
+ resources/ Around resource implementations (Cursor, BuildLock).
50
62
  ```
51
63
 
52
64
  **Gate + Auth**
@@ -202,7 +214,7 @@ Both are built from a `Container` via named constructors — `Writer.from(contai
202
214
 
203
215
  `Action::Get` is a **pure read** — it resolves the path, reads bytes, parses the envelope, and annotates a freshness verdict. It never ingests and never mutates.
204
216
 
205
- 1. CLI/MCP surface calls `store.as(role).get(key)`.
217
+ 1. CLI/MCP surface calls `store.with_role(role).get(key)`.
206
218
  2. `Gate#dispatch` runs Auth → `Action::Get#call`.
207
219
  3. `Get` resolves path via `manifest.resolver`, reads bytes via `file_store`, parses the envelope, annotates `freshness` based on retention-rule TTL (if any).
208
220
 
@@ -210,7 +222,7 @@ Staleness is age-based (retention-rule TTL vs file mtime). A stale entry is retu
210
222
 
211
223
  ## Write path (`store.put(key, ...)`)
212
224
 
213
- 1. CLI/MCP surface calls `store.as(role).put(key, meta:, body:)`.
225
+ 1. CLI/MCP surface calls `store.with_role(role).put(key, meta:, body:)`.
214
226
  2. `Gate#dispatch` runs Auth → `Action::Put#call`.
215
227
  3. `Put` validates, resolves manifest entry, delegates to `Envelope::Writer#put` (serialize → schema-validate → etag-check → FileStore#write → AuditLog#append).
216
228
  4. `WriteVerb#cascade_to_rdeps` enqueues `materialize` jobs for any entries with publish_tree that depend on the written key.
@@ -24,7 +24,7 @@ Recommended top-level layout — the spec allows alternatives, but this is what
24
24
  templates/ # ERB templates for publish rendering
25
25
  data/
26
26
  knowledge/ # authored truth: identity, voice, decisions — author-holders write
27
- notebook/ # agent's own durable lane (workspace) — keep-holders write
27
+ scratchpad/ # agent's own durable lane (workspace) — keep-holders write
28
28
  proposals/ # AI proposals awaiting accept (propose)
29
29
  artifacts/ # computed outputs produced by drain — never edit by hand
30
30
  raw/ # write-once external source material (ingest)
@@ -126,7 +126,10 @@ For multi-writer environments, **always pass `if_etag`** on `put`. The gem treat
126
126
  The application layer is organised around three shapes — `Manifest` as a composition record, a single `Container` capability record handed to every use case, and a split envelope reader/writer. See [ADR 0018](../architecture/decisions/0018-manifest-carving.md), [ADR 0017](../architecture/decisions/0017-envelope-io-split.md), [ADR 0022](../architecture/decisions/0022-container-call-dispatcher.md), and [ADR 0023](../architecture/decisions/0023-uniform-use-case-shape.md).
127
127
 
128
128
  - **`Manifest` is a composition record** (`Data.define(:data, :resolver, :policy, :rules)`). Reach individual concerns through the field accessors: `manifest.data.entries`, `manifest.policy.permission_for(lane)`, `manifest.resolver.resolve(key)`, `manifest.rules.for(key)`.
129
- - **Use cases are plain `(container:, call:)` classes.** Each is a single class under `lib/textus/{read,write,maintenance}/` with `def initialize(container:, call:)` and a `#call(...)` method; verbs are looked up in the static `Textus::Dispatcher::VERBS` table. `Container` is a `Data.define` record bundling the wired ports + manifest (`manifest`, `file_store`, `schemas`, `root`, `audit_log`, `events`, `rpc`); `Call` is the immutable per-invocation value (`role`, `correlation_id`, `now`, `dry_run`). A use case that emits events derives its `Hooks::Context` from `(container, call)` nothing is injected. Use cases pull only what they need into ivars; nobody passes the raw `Store` around the application layer. `store.as(role)` returns a `RoleScope` that forwards verbs to the dispatcher.
129
+ - **`Store` is the single dispatch module.** `Store` holds the `Container`, the active role, session state (cursor, propose_lane, contract_etag), and dispatches verbs through `method_missing`. Each verb call (`store.get(key: ...)`, `store.put(key:, meta:, body:)`) goes through `VerbRegistry`, binds inputs via `Dispatch::Binder`, routes through `Dispatch::Pipeline` (middleware chain: auth, cascade, audit), and reaches the handler. `Store#with_role(role)` returns a new `Store` bound to that role.
130
+ - **`Container` is a `Data.define` composition** of `Infrastructure` (`file_store`, `schemas`, `audit_log`, `job_store`, `geometry`) and `Coordination` (`manifest`, `workflows`, `pipeline`). Built in a single pass — no circular dependencies or lazy proxies.
131
+ - **Dispatch lives in `lib/textus/dispatch/`**: `Dispatch.dispatch` orchestrates verb dispatch, `Dispatch::Pipeline` runs middleware then invokes the handler, `Dispatch::Binder` handles input binding (wire format, CLI flags, session defaults). Auth predicates moved to `Manifest::Policy::Predicates`.
132
+ - **Handlers receive `container:`** and access I/O via `@container.pipeline.read/write/delete/move` and manifest config via `@container.manifest`.
130
133
  - **Write path is split**: `Envelope::IO::Reader` owns read/parse (existing-uid lookup, raw read, parse), and `Envelope::IO::Writer` owns put/delete/move + the audit-append invariant (every public method's final action is `@audit_log.append(...)`).
131
134
 
132
135
  The user-facing CLI surface, the wire envelope shape, and the protocol version (`textus/4`) are unchanged.
data/exe/textus CHANGED
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
2
  $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
3
3
  require "textus"
4
- exit Textus::Surfaces::CLI.run(ARGV)
4
+ exit Textus::Surface::CLI.run(ARGV)