textus 0.55.2 → 0.56.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 (338) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +175 -2768
  3. data/README.md +8 -9
  4. data/lib/textus/artifact.rb +8 -0
  5. data/lib/textus/errors.rb +0 -39
  6. data/lib/textus/human.rb +7 -0
  7. data/lib/textus/infra/adapters/concurrency_adapter.rb +15 -0
  8. data/lib/textus/infra/clock.rb +7 -0
  9. data/lib/textus/infra/database.rb +127 -0
  10. data/lib/textus/infra/file_system.rb +54 -0
  11. data/lib/textus/infra/locks.rb +50 -0
  12. data/lib/textus/infra/port/clock.rb +18 -0
  13. data/lib/textus/infra/port/publisher.rb +71 -0
  14. data/lib/textus/infra/port/sentinel.rb +25 -0
  15. data/lib/textus/infra/port/sentinel_store.rb +93 -0
  16. data/lib/textus/infra/store/audit_log.rb +42 -0
  17. data/lib/textus/infra/store/base.rb +25 -0
  18. data/lib/textus/infra/store/cursor_store.rb +27 -0
  19. data/lib/textus/infra/store/entry_index.rb +31 -0
  20. data/lib/textus/infra/store/envelope/meta.rb +63 -0
  21. data/lib/textus/infra/store/file_store.rb +23 -0
  22. data/lib/textus/infra/store/index/builder.rb +28 -0
  23. data/lib/textus/infra/store/index/lookup.rb +23 -0
  24. data/lib/textus/infra/store/link_store.rb +43 -0
  25. data/lib/textus/infra/store/workflow_queue.rb +54 -0
  26. data/lib/textus/infra/store.rb +29 -0
  27. data/lib/textus/ingest.rb +8 -0
  28. data/lib/textus/lanes/artifact/cache.rb +35 -0
  29. data/lib/textus/lanes/artifact/handlers.rb +29 -0
  30. data/lib/textus/lanes/artifact/lifecycle.rb +23 -0
  31. data/lib/textus/lanes/artifact/verbs.rb +18 -0
  32. data/lib/textus/lanes/ingest/dedup.rb +17 -0
  33. data/lib/textus/lanes/ingest/entry_types/asset.rb +45 -0
  34. data/lib/textus/lanes/ingest/entry_types/link.rb +16 -0
  35. data/lib/textus/lanes/ingest/entry_types/text.rb +17 -0
  36. data/lib/textus/lanes/ingest/entry_types.rb +25 -0
  37. data/lib/textus/lanes/ingest/handlers.rb +54 -0
  38. data/lib/textus/lanes/ingest/index_rebuilder.rb +13 -0
  39. data/lib/textus/lanes/ingest/key_builder.rb +33 -0
  40. data/lib/textus/lanes/ingest/resolver.rb +21 -0
  41. data/lib/textus/lanes/knowledge/freeform.rb +15 -0
  42. data/lib/textus/lanes/knowledge/handlers.rb +53 -0
  43. data/lib/textus/lanes/knowledge/runner.rb +45 -0
  44. data/lib/textus/lanes/knowledge/verbs.rb +16 -0
  45. data/lib/textus/lanes/scratchpad/handlers.rb +164 -0
  46. data/lib/textus/lanes/scratchpad/verbs.rb +73 -0
  47. data/lib/textus/{dispatch → protocol}/binder.rb +1 -7
  48. data/lib/textus/protocol/boot/catalog.rb +34 -0
  49. data/lib/textus/protocol/boot/context.rb +124 -0
  50. data/lib/textus/protocol/boot/protocol.rb +33 -0
  51. data/lib/textus/protocol/boot.rb +42 -0
  52. data/lib/textus/protocol/builder/domain.rb +17 -0
  53. data/lib/textus/protocol/builder/infrastructure.rb +17 -0
  54. data/lib/textus/protocol/builder/runtime.rb +46 -0
  55. data/lib/textus/protocol/builder.rb +11 -0
  56. data/lib/textus/protocol/container.rb +22 -0
  57. data/lib/textus/protocol/diff.rb +170 -0
  58. data/lib/textus/protocol/entry_constraint.rb +54 -0
  59. data/lib/textus/protocol/envelope.rb +70 -0
  60. data/lib/textus/protocol/format/base.rb +80 -0
  61. data/lib/textus/protocol/format/bash.rb +10 -0
  62. data/lib/textus/protocol/format/javascript.rb +10 -0
  63. data/lib/textus/protocol/format/json.rb +82 -0
  64. data/lib/textus/protocol/format/markdown.rb +53 -0
  65. data/lib/textus/protocol/format/python.rb +10 -0
  66. data/lib/textus/protocol/format/ruby.rb +10 -0
  67. data/lib/textus/protocol/format/script.rb +55 -0
  68. data/lib/textus/protocol/format/text.rb +48 -0
  69. data/lib/textus/protocol/format/yaml.rb +105 -0
  70. data/lib/textus/protocol/format.rb +73 -0
  71. data/lib/textus/{store → protocol}/freshness/ttl_evaluator.rb +7 -3
  72. data/lib/textus/protocol/gate.rb +61 -0
  73. data/lib/textus/protocol/handlers/command_context.rb +19 -0
  74. data/lib/textus/protocol/handlers/concern.rb +15 -0
  75. data/lib/textus/protocol/handlers/ops/audit_filter.rb +37 -0
  76. data/lib/textus/protocol/handlers/ops/bulk_planner.rb +38 -0
  77. data/lib/textus/protocol/handlers/ops/data_mv.rb +49 -0
  78. data/lib/textus/protocol/handlers/ops/git_blame.rb +65 -0
  79. data/lib/textus/protocol/handlers/ops/pulse_coordinator.rb +37 -0
  80. data/lib/textus/protocol/handlers/ops/source_expander.rb +41 -0
  81. data/lib/textus/protocol/handlers/query_context.rb +41 -0
  82. data/lib/textus/protocol/handlers/read.rb +67 -0
  83. data/lib/textus/protocol/handlers/system.rb +93 -0
  84. data/lib/textus/protocol/handlers/write.rb +57 -0
  85. data/lib/textus/protocol/init.rb +143 -0
  86. data/lib/textus/protocol/key/grammar.rb +35 -0
  87. data/lib/textus/protocol/key/matching.rb +23 -0
  88. data/lib/textus/protocol/key/path.rb +23 -0
  89. data/lib/textus/protocol/key.rb +25 -0
  90. data/lib/textus/protocol/lane.rb +12 -0
  91. data/lib/textus/{store → protocol}/layout.rb +16 -22
  92. data/lib/textus/protocol/links/resolver.rb +33 -0
  93. data/lib/textus/protocol/links/uri_rewriter.rb +30 -0
  94. data/lib/textus/protocol/links.rb +10 -0
  95. data/lib/textus/protocol/manifest/data.rb +90 -0
  96. data/lib/textus/protocol/manifest/entry/base.rb +95 -0
  97. data/lib/textus/protocol/manifest/entry/leaf.rb +19 -0
  98. data/lib/textus/protocol/manifest/entry/nested.rb +19 -0
  99. data/lib/textus/protocol/manifest/entry/parser.rb +71 -0
  100. data/lib/textus/protocol/manifest/entry/publish.rb +209 -0
  101. data/lib/textus/protocol/manifest/entry.rb +10 -0
  102. data/lib/textus/protocol/manifest/policy/publish_target.rb +36 -0
  103. data/lib/textus/protocol/manifest/policy.rb +69 -0
  104. data/lib/textus/protocol/manifest/resolver.rb +156 -0
  105. data/lib/textus/protocol/manifest/schema/contract.rb +53 -0
  106. data/lib/textus/protocol/manifest/schema/validator.rb +52 -0
  107. data/lib/textus/protocol/manifest/schema.rb +40 -0
  108. data/lib/textus/protocol/manifest.rb +62 -0
  109. data/lib/textus/protocol/produce/render/context.rb +43 -0
  110. data/lib/textus/protocol/produce/render.rb +44 -0
  111. data/lib/textus/protocol/schema/registry.rb +44 -0
  112. data/lib/textus/protocol/schema/tools.rb +104 -0
  113. data/lib/textus/protocol/schema.rb +117 -0
  114. data/lib/textus/protocol/session/bridge.rb +11 -0
  115. data/lib/textus/protocol/session/context.rb +5 -0
  116. data/lib/textus/protocol/session.rb +87 -0
  117. data/lib/textus/protocol/store_engine/delete.rb +37 -0
  118. data/lib/textus/protocol/store_engine/move.rb +109 -0
  119. data/lib/textus/protocol/store_engine/pipeline.rb +61 -0
  120. data/lib/textus/protocol/store_engine/put.rb +104 -0
  121. data/lib/textus/protocol/store_engine.rb +86 -0
  122. data/lib/textus/protocol/v4.rb +25 -0
  123. data/lib/textus/protocol/verb_registry/arg_spec.rb +19 -0
  124. data/lib/textus/protocol/verb_registry/core_verbs.rb +164 -0
  125. data/lib/textus/protocol/verb_registry/verb_spec.rb +42 -0
  126. data/lib/textus/protocol/verb_registry.rb +42 -0
  127. data/lib/textus/surface/cli/group/{rule.rb → web.rb} +2 -2
  128. data/lib/textus/surface/cli/runner.rb +22 -21
  129. data/lib/textus/surface/cli/sources.rb +1 -3
  130. data/lib/textus/surface/cli/verb/get.rb +2 -7
  131. data/lib/textus/surface/cli/verb/init.rb +1 -1
  132. data/lib/textus/surface/cli/verb/put.rb +7 -7
  133. data/lib/textus/surface/cli/verb/web_serve.rb +22 -0
  134. data/lib/textus/surface/cli.rb +25 -6
  135. data/lib/textus/surface/dispatch.rb +17 -0
  136. data/lib/textus/surface/mcp/adapters/mcp_adapter.rb +28 -0
  137. data/lib/textus/surface/mcp/catalog.rb +12 -12
  138. data/lib/textus/surface/mcp/cursor_manager.rb +22 -0
  139. data/lib/textus/surface/mcp/drift_annotation.rb +19 -0
  140. data/lib/textus/surface/mcp/server.rb +35 -24
  141. data/lib/textus/surface/watcher.rb +18 -12
  142. data/lib/textus/surface/web.rb +50 -0
  143. data/lib/textus/value/call.rb +1 -1
  144. data/lib/textus/value/etag.rb +7 -7
  145. data/lib/textus/version.rb +1 -1
  146. data/lib/textus/workflow/consumer.rb +39 -0
  147. data/lib/textus/workflow/dsl/lifecycle.rb +32 -0
  148. data/lib/textus/workflow/dsl/pattern_matcher.rb +20 -0
  149. data/lib/textus/workflow/dsl/step_builder.rb +56 -0
  150. data/lib/textus/workflow/dsl.rb +111 -21
  151. data/lib/textus/workflow/event_emitter.rb +21 -0
  152. data/lib/textus/workflow/publisher.rb +59 -0
  153. data/lib/textus/workflow/registry.rb +31 -8
  154. data/lib/textus/workflow/runner.rb +25 -53
  155. data/lib/textus/workflow/scheduler.rb +45 -0
  156. data/lib/textus/workflow/step_executor.rb +78 -0
  157. data/lib/textus/workflow/step_helpers.rb +102 -0
  158. data/lib/textus/workflow.rb +2 -5
  159. data/lib/textus.rb +16 -16
  160. metadata +169 -185
  161. data/SPEC.md +0 -909
  162. data/docs/architecture/README.md +0 -273
  163. data/docs/reference/conventions.md +0 -141
  164. data/lib/textus/boot.rb +0 -181
  165. data/lib/textus/dispatch/contracts.rb +0 -63
  166. data/lib/textus/dispatch/handler_registry.rb +0 -21
  167. data/lib/textus/dispatch/middleware/audit_index.rb +0 -51
  168. data/lib/textus/dispatch/middleware/auth.rb +0 -40
  169. data/lib/textus/dispatch/middleware/base.rb +0 -26
  170. data/lib/textus/dispatch/middleware/binder.rb +0 -20
  171. data/lib/textus/dispatch/middleware/cascade.rb +0 -53
  172. data/lib/textus/dispatch/pipeline.rb +0 -35
  173. data/lib/textus/doctor/check/audit_log.rb +0 -34
  174. data/lib/textus/doctor/check/generator_drift.rb +0 -29
  175. data/lib/textus/doctor/check/illegal_keys.rb +0 -60
  176. data/lib/textus/doctor/check/manifest_files.rb +0 -25
  177. data/lib/textus/doctor/check/orphaned_publish_targets.rb +0 -35
  178. data/lib/textus/doctor/check/proposal_targets.rb +0 -45
  179. data/lib/textus/doctor/check/protocol_version.rb +0 -47
  180. data/lib/textus/doctor/check/publish_tree_index_overlap.rb +0 -48
  181. data/lib/textus/doctor/check/raw_asset_paths.rb +0 -50
  182. data/lib/textus/doctor/check/rule_ambiguity.rb +0 -49
  183. data/lib/textus/doctor/check/schema_parse_error.rb +0 -28
  184. data/lib/textus/doctor/check/schema_violations.rb +0 -28
  185. data/lib/textus/doctor/check/schemas.rb +0 -26
  186. data/lib/textus/doctor/check/scratchpad_sources.rb +0 -55
  187. data/lib/textus/doctor/check/sentinels.rb +0 -60
  188. data/lib/textus/doctor/check/stale_reviewed_stamp.rb +0 -54
  189. data/lib/textus/doctor/check/templates.rb +0 -29
  190. data/lib/textus/doctor/check/unowned_schema_fields.rb +0 -40
  191. data/lib/textus/doctor/check.rb +0 -43
  192. data/lib/textus/doctor/validator.rb +0 -93
  193. data/lib/textus/doctor.rb +0 -54
  194. data/lib/textus/format/base.rb +0 -77
  195. data/lib/textus/format/json.rb +0 -81
  196. data/lib/textus/format/markdown.rb +0 -59
  197. data/lib/textus/format/text.rb +0 -55
  198. data/lib/textus/format/yaml.rb +0 -104
  199. data/lib/textus/format.rb +0 -61
  200. data/lib/textus/handlers/maintenance/boot_store.rb +0 -15
  201. data/lib/textus/handlers/maintenance/doctor_store.rb +0 -15
  202. data/lib/textus/handlers/maintenance/drain_store.rb +0 -21
  203. data/lib/textus/handlers/maintenance/ingest_entry.rb +0 -159
  204. data/lib/textus/handlers/maintenance/jobs_action.rb +0 -21
  205. data/lib/textus/handlers/maintenance/published_entries.rb +0 -17
  206. data/lib/textus/handlers/maintenance/rule_explain.rb +0 -77
  207. data/lib/textus/handlers/maintenance/rule_lint.rb +0 -54
  208. data/lib/textus/handlers/maintenance/rule_list.rb +0 -32
  209. data/lib/textus/handlers/maintenance/schema_envelope.rb +0 -19
  210. data/lib/textus/handlers/read/audit_entries.rb +0 -48
  211. data/lib/textus/handlers/read/blame_entry.rb +0 -71
  212. data/lib/textus/handlers/read/deps_entry.rb +0 -17
  213. data/lib/textus/handlers/read/get_entry.rb +0 -68
  214. data/lib/textus/handlers/read/list_keys.rb +0 -36
  215. data/lib/textus/handlers/read/pulse_entries.rb +0 -66
  216. data/lib/textus/handlers/read/rdeps_entry.rb +0 -21
  217. data/lib/textus/handlers/read/uid_entry.rb +0 -18
  218. data/lib/textus/handlers/read/where_entry.rb +0 -18
  219. data/lib/textus/handlers/write/accept_proposal.rb +0 -39
  220. data/lib/textus/handlers/write/data_mv.rb +0 -55
  221. data/lib/textus/handlers/write/delete_key.rb +0 -17
  222. data/lib/textus/handlers/write/enqueue_job.rb +0 -27
  223. data/lib/textus/handlers/write/key_delete_prefix.rb +0 -32
  224. data/lib/textus/handlers/write/key_mv_prefix.rb +0 -45
  225. data/lib/textus/handlers/write/move_key.rb +0 -80
  226. data/lib/textus/handlers/write/propose_entry.rb +0 -29
  227. data/lib/textus/handlers/write/put_entry.rb +0 -29
  228. data/lib/textus/handlers/write/reject_proposal.rb +0 -29
  229. data/lib/textus/init.rb +0 -157
  230. data/lib/textus/jobs.rb +0 -9
  231. data/lib/textus/key/distance.rb +0 -55
  232. data/lib/textus/key/grammar.rb +0 -33
  233. data/lib/textus/key/matching.rb +0 -24
  234. data/lib/textus/key/path.rb +0 -28
  235. data/lib/textus/manifest/capabilities.rb +0 -29
  236. data/lib/textus/manifest/data.rb +0 -102
  237. data/lib/textus/manifest/entry/base.rb +0 -105
  238. data/lib/textus/manifest/entry/ignore_matcher.rb +0 -46
  239. data/lib/textus/manifest/entry/leaf.rb +0 -17
  240. data/lib/textus/manifest/entry/nested.rb +0 -37
  241. data/lib/textus/manifest/entry/parser.rb +0 -99
  242. data/lib/textus/manifest/entry/produced.rb +0 -36
  243. data/lib/textus/manifest/entry/publish/mode.rb +0 -45
  244. data/lib/textus/manifest/entry/publish/none.rb +0 -14
  245. data/lib/textus/manifest/entry/publish/subtree_mirror.rb +0 -73
  246. data/lib/textus/manifest/entry/publish/template.rb +0 -16
  247. data/lib/textus/manifest/entry/publish/to_paths.rb +0 -76
  248. data/lib/textus/manifest/entry/publish/tree.rb +0 -50
  249. data/lib/textus/manifest/entry/publish.rb +0 -45
  250. data/lib/textus/manifest/entry/validators/format_matrix.rb +0 -21
  251. data/lib/textus/manifest/entry/validators/ignore.rb +0 -28
  252. data/lib/textus/manifest/entry/validators/publish.rb +0 -30
  253. data/lib/textus/manifest/entry/validators.rb +0 -18
  254. data/lib/textus/manifest/entry.rb +0 -8
  255. data/lib/textus/manifest/policy/matcher.rb +0 -51
  256. data/lib/textus/manifest/policy/predicates/author_held.rb +0 -22
  257. data/lib/textus/manifest/policy/predicates/etag_match.rb +0 -18
  258. data/lib/textus/manifest/policy/predicates/fresh_within.rb +0 -13
  259. data/lib/textus/manifest/policy/predicates/lane_deletable_by.rb +0 -31
  260. data/lib/textus/manifest/policy/predicates/lane_writable_by.rb +0 -23
  261. data/lib/textus/manifest/policy/predicates/raw_lane_ingest_only.rb +0 -25
  262. data/lib/textus/manifest/policy/predicates/raw_write_once.rb +0 -24
  263. data/lib/textus/manifest/policy/predicates/schema_valid.rb +0 -41
  264. data/lib/textus/manifest/policy/predicates/target_is_canon.rb +0 -20
  265. data/lib/textus/manifest/policy/predicates.rb +0 -54
  266. data/lib/textus/manifest/policy/publish_target.rb +0 -34
  267. data/lib/textus/manifest/policy/react.rb +0 -30
  268. data/lib/textus/manifest/policy/retention.rb +0 -29
  269. data/lib/textus/manifest/policy/source.rb +0 -30
  270. data/lib/textus/manifest/policy.rb +0 -95
  271. data/lib/textus/manifest/resolver.rb +0 -118
  272. data/lib/textus/manifest/rules.rb +0 -86
  273. data/lib/textus/manifest/schema/contract.rb +0 -61
  274. data/lib/textus/manifest/schema/keys.rb +0 -90
  275. data/lib/textus/manifest/schema/semantics/cross_field.rb +0 -53
  276. data/lib/textus/manifest/schema/semantics/invariants.rb +0 -125
  277. data/lib/textus/manifest/schema/semantics/migration.rb +0 -83
  278. data/lib/textus/manifest/schema/semantics.rb +0 -27
  279. data/lib/textus/manifest/schema/validator.rb +0 -48
  280. data/lib/textus/manifest/schema/vocabulary.rb +0 -25
  281. data/lib/textus/manifest/schema.rb +0 -30
  282. data/lib/textus/manifest.rb +0 -65
  283. data/lib/textus/orchestration.rb +0 -55
  284. data/lib/textus/port/audit_log.rb +0 -268
  285. data/lib/textus/port/build_lock.rb +0 -64
  286. data/lib/textus/port/clock.rb +0 -10
  287. data/lib/textus/port/publisher.rb +0 -60
  288. data/lib/textus/port/sentinel.rb +0 -25
  289. data/lib/textus/port/sentinel_store.rb +0 -83
  290. data/lib/textus/port/storage/file_stat.rb +0 -19
  291. data/lib/textus/port/storage/file_store.rb +0 -49
  292. data/lib/textus/port/storage/interface.rb +0 -17
  293. data/lib/textus/port/store.rb +0 -149
  294. data/lib/textus/port/watcher_lock.rb +0 -48
  295. data/lib/textus/produce/engine.rb +0 -40
  296. data/lib/textus/produce/publisher.rb +0 -21
  297. data/lib/textus/produce/render.rb +0 -20
  298. data/lib/textus/schema/registry.rb +0 -42
  299. data/lib/textus/schema/tools.rb +0 -108
  300. data/lib/textus/schema.rb +0 -100
  301. data/lib/textus/store/container.rb +0 -173
  302. data/lib/textus/store/cursor.rb +0 -26
  303. data/lib/textus/store/entry/reader.rb +0 -47
  304. data/lib/textus/store/entry/writer.rb +0 -219
  305. data/lib/textus/store/envelope/meta.rb +0 -61
  306. data/lib/textus/store/freshness/drift_detector.rb +0 -93
  307. data/lib/textus/store/freshness/evaluator.rb +0 -20
  308. data/lib/textus/store/freshness/verdict.rb +0 -24
  309. data/lib/textus/store/freshness.rb +0 -8
  310. data/lib/textus/store/index/builder.rb +0 -76
  311. data/lib/textus/store/index/lookup.rb +0 -60
  312. data/lib/textus/store/jobs/base.rb +0 -13
  313. data/lib/textus/store/jobs/index.rb +0 -15
  314. data/lib/textus/store/jobs/materialize.rb +0 -15
  315. data/lib/textus/store/jobs/plan.rb +0 -11
  316. data/lib/textus/store/jobs/planner.rb +0 -124
  317. data/lib/textus/store/jobs/queue.rb +0 -162
  318. data/lib/textus/store/jobs/registry.rb +0 -19
  319. data/lib/textus/store/jobs/retention/base.rb +0 -52
  320. data/lib/textus/store/jobs/retention/sweep.rb +0 -55
  321. data/lib/textus/store/jobs/retention.rb +0 -8
  322. data/lib/textus/store/jobs/sweep.rb +0 -21
  323. data/lib/textus/store/jobs/worker.rb +0 -64
  324. data/lib/textus/store.rb +0 -122
  325. data/lib/textus/surface/cli/verb/doctor.rb +0 -20
  326. data/lib/textus/surface/cli/verb/schema_diff.rb +0 -17
  327. data/lib/textus/surface/cli/verb/schema_init.rb +0 -21
  328. data/lib/textus/surface/cli/verb/schema_migrate.rb +0 -21
  329. data/lib/textus/surface/mcp/projector.rb +0 -20
  330. data/lib/textus/surface/mcp.rb +0 -8
  331. data/lib/textus/value/command.rb +0 -16
  332. data/lib/textus/value/envelope.rb +0 -90
  333. data/lib/textus/value/result.rb +0 -46
  334. data/lib/textus/verb_registry.rb +0 -417
  335. data/lib/textus/workflow/collector.rb +0 -27
  336. data/lib/textus/workflow/context.rb +0 -5
  337. data/lib/textus/workflow/loader.rb +0 -17
  338. data/lib/textus/workflow/pattern.rb +0 -18
@@ -1,273 +0,0 @@
1
- # Textus architecture
2
-
3
- > **Explanation** · for contributors · **read this first** for orientation before SPEC
4
- > **SSoT for** the Ruby implementation layout (layers, container, ports, dispatch paths) · **reviewed** 2026-06 (v0.54)
5
-
6
- ```mermaid
7
- flowchart TD
8
- surfaces["Surfaces — CLI · MCP · RoleScope"]
9
- contract["Contract — per-verb DSL (Arg, Spec, DSL)"]
10
- dispatch["Dispatch — Binder · Around · CommandBuilder · Dispatcher"]
11
- gate["Gate — routing (VERB_COMMAND) · Auth"]
12
- action["Actions — per-verb use cases (action/)"]
13
- jobs["Jobs — Worker · Planner · Materialize · Retention · Sweep"]
14
- produce["Produce — Engine · Render"]
15
- workflow["Workflow — Runner · Registry · Loader · DSL"]
16
- manifest["Manifest — declarative config, no IO (policy/, schema/, entry/)"]
17
- core["Core — pure value types (Freshness, Retention, Duration, Sentinel)"]
18
- ports["Ports — IO adapters (FileStore, AuditLog, JobStore, Publisher…)"]
19
- surfaces --> dispatch
20
- surfaces -.->|declare| contract
21
- contract --> dispatch
22
- dispatch --> gate
23
- gate --> action
24
- action --> jobs
25
- action --> produce
26
- produce --> workflow
27
- action --> manifest
28
- action --> core
29
- action --> ports
30
- jobs --> produce
31
- ```
32
-
33
- *Dependency rule: inward only.* Surfaces → Dispatch → Gate → Actions → inner layers. Actions never reference surfaces.
34
-
35
- ### What lives in each layer
36
-
37
- **Surfaces**
38
-
39
- ```
40
- surfaces/cli/ CLI command generation from contracts
41
- sources.rb CLI-only input acquisition (stdin parsing, file sourcing, coercion)
42
- surfaces/mcp/ MCP server — stdio JSON-RPC 2.0, tools derived from contracts
43
- surfaces/role_scope.rb
44
- (Store#with_role(role)) — holds (container, role, dry_run, correlation_id);
45
- all verb methods injected via define_method in textus.rb
46
- ```
47
-
48
- **Contract + Dispatch**
49
-
50
- ```
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).
62
- ```
63
-
64
- **Gate + Auth**
65
-
66
- ```
67
- gate.rb VERB_COMMAND (verb symbol → Command class)
68
- Gate#dispatch(cmd) — Auth → action call
69
- gate/auth.rb Authorization engine — FLOOR predicates + rule guards
70
- ```
71
-
72
- **Actions**
73
-
74
- ```
75
- action/{get,list,put,key_delete,key_mv,accept,reject,propose,
76
- drain,enqueue,audit,blame,deps,rdeps,published,boot,doctor,
77
- rule_explain,rule_list,rule_lint,pulse,
78
- data_mv,key_mv_prefix,key_delete_prefix,
79
- schema_envelope,where,uid,jobs}.rb
80
- ```
81
-
82
- **Jobs**
83
-
84
- ```
85
- jobs/planner.rb Rules-driven job planning — seeds materialize/refresh/sweep jobs
86
- jobs/worker.rb Queue drain — leases and runs jobs from JobStore
87
- jobs/materialize.rb Per-key produce job → Produce::Engine.converge
88
- jobs/refresh.rb Per-key refresh job (retention-rule-triggered)
89
- jobs/retention.rb Retention policy check
90
- jobs/sweep.rb Lane sweep job
91
- ```
92
-
93
- **Produce**
94
-
95
- ```
96
- produce/engine.rb Engine.converge(container:, call:, keys:)
97
- For each key: looks up a registered workflow and runs it,
98
- or publishes existing bytes for entries with publish targets.
99
- produce/render.rb Mustache template expansion for publish targets
100
- ```
101
-
102
- **Workflow**
103
-
104
- ```
105
- workflow/runner.rb Runs a Definition: build_context → execute_steps → publish
106
- workflow/registry.rb In-memory list of registered Definitions; Registry#for(key)
107
- workflow/loader.rb Loads .textus/workflows/*.rb files via Collector DSL
108
- workflow/dsl.rb Textus.workflow "name" { match "...", step :fetch { ... }, publish }
109
- workflow/context.rb Context passed to each step (key, entry, config, container, call)
110
- workflow/pattern.rb Key-matching pattern (glob via File.fnmatch?)
111
- workflow/errors.rb NotFound, StepFailed
112
- ```
113
-
114
- **Core (pure value types)**
115
-
116
- ```
117
- Freshness::{Verdict,Evaluator}
118
- Core::Duration Core::Sentinel
119
- ```
120
-
121
- **Infrastructure**
122
-
123
- ```
124
- Store (composition root — wires ports, vends Container)
125
- Storage::FileStore (bytes-only port: read/write/delete/exists?/etag)
126
- Manifest (Data, Resolver, Policy, Rules)
127
- Schemas (eager-load cache)
128
- Ports::{AuditLog,JobStore,Publisher,Clock,
129
- BuildLock,SentinelStore}
130
- Entry::{Markdown,Json,Yaml,Text} (format strategies)
131
- ```
132
-
133
- ## How a verb becomes a method
134
-
135
- All actions live under `lib/textus/action/`. The shape is uniform:
136
-
137
- ```ruby
138
- module Textus
139
- module Action
140
- class Get < Base
141
- extend Textus::Contract::DSL
142
-
143
- verb :get
144
- summary "Read an entry by key."
145
- surfaces :cli, :mcp
146
- arg :key, String, required: true, positional: true
147
-
148
- BURN = :sync
149
-
150
- def initialize(key:)
151
- super()
152
- @key = key
153
- end
154
-
155
- def call(container:, call:)
156
- ...
157
- end
158
- end
159
- end
160
- end
161
- ```
162
-
163
- Verbs are looked up in a static frozen table (`Textus::Action::VERBS`) that maps `:get → Action::Get`, `:put → Action::Put`, etc. Adding a new verb is one entry in `VERBS` plus the class — no metaprogramming.
164
-
165
- ## Container
166
-
167
- Use cases never see the raw `Store`. `Textus::Container` is a single record holding the wired collaborators:
168
-
169
- ```ruby
170
- Container = Data.define(
171
- :manifest, :file_store, :schemas, :root,
172
- :audit_log, :workflows, :gate
173
- )
174
- ```
175
-
176
- The `Store` builds one `Container` at boot; every action receives it via `(container:, call:)`. Workflow steps receive a `Workflow::Context` (key, entry, config, container, call).
177
-
178
- ## Ports
179
-
180
- | Class | Role |
181
- |---|---|
182
- | `Ports::Storage::FileStore` | Bytes-only FS I/O — `read`, `write`, `delete`, `exists?`, `etag`. |
183
- | `Ports::AuditLog` | Append-only structured log (`audit.log`). Owns seq numbering, file-locking, rotation. |
184
- | `Ports::Clock` | Supplies `Time.now` — module-function so tests can swap without DI boilerplate. |
185
- | `Ports::Publisher` | Copies a built artifact to a repo-relative consumer path and writes a sentinel. |
186
- | `Ports::BuildLock` | Process-exclusive `flock` guard over the produce pipeline. |
187
- | `Ports::JobStore` | Persistent job queue used by `drain` workers; tracks ready/leased/done/failed jobs. |
188
- | `Ports::SentinelStore` | Reads and writes per-target sentinel files for managed-file detection. |
189
-
190
- Application use cases access ports only through `Container` fields — never through the raw `Store`.
191
-
192
- ### Envelope
193
-
194
- `Envelope::Reader` and `Envelope::Writer` split the envelope pipeline into read-only parse and write-with-audit halves.
195
-
196
- **Reader** (`lib/textus/envelope/reader.rb`) — resolves a key through `manifest.resolver`, reads bytes via `FileStore`, parses via the format strategy, returns an `Envelope`. No audit, no permissions.
197
-
198
- **Writer** (`lib/textus/envelope/writer.rb`) — owns the full write pipeline: serialize → schema-validate → etag-check → `FileStore#write` → `AuditLog#append`. The audit append is the commit step.
199
-
200
- Both are built from a `Container` via named constructors — `Writer.from(container:, call:)` and `Reader.from(container:)`.
201
-
202
- ## Manifest carving
203
-
204
- `Manifest` is a `Data.define` struct with four named members:
205
-
206
- | Member | Class | Responsibility |
207
- |---|---|---|
208
- | `data` | `Manifest::Data` | Frozen value: raw, root, lanes, entries, audit_config, role_caps. |
209
- | `resolver` | `Manifest::Resolver` | Key → `Resolution(entry, path, remaining)`. Nested entry enumeration and fuzzy-match suggestions. |
210
- | `policy` | `Manifest::Policy` | Zone/capability authority — `verb_for_lane`, `roles_with_capability`, `propose_lane_for(role)`. |
211
- | `rules` | `Manifest::Rules` | Pattern-matched rule engine. `rules.for(key)` → `RuleSet(guard, retention, react)`. |
212
-
213
- ## Read path (`store.get(key)`)
214
-
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.
216
-
217
- 1. CLI/MCP surface calls `store.with_role(role).get(key)`.
218
- 2. `Gate#dispatch` runs Auth → `Action::Get#call`.
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).
220
-
221
- Staleness is age-based (retention-rule TTL vs file mtime). A stale entry is returned stale — the read does not refresh it; `drain` does.
222
-
223
- ## Write path (`store.put(key, ...)`)
224
-
225
- 1. CLI/MCP surface calls `store.with_role(role).put(key, meta:, body:)`.
226
- 2. `Gate#dispatch` runs Auth → `Action::Put#call`.
227
- 3. `Put` validates, resolves manifest entry, delegates to `Envelope::Writer#put` (serialize → schema-validate → etag-check → FileStore#write → AuditLog#append).
228
- 4. `WriteVerb#cascade_to_rdeps` enqueues `materialize` jobs for any entries with publish_tree that depend on the written key.
229
-
230
- `Action::{KeyDelete,KeyMv,Accept,Reject,Propose}` follow the same shape.
231
-
232
- ## Workflow path (`drain` + materialize jobs)
233
-
234
- The workflow system owns the produce pipeline. `Produce::Engine.converge(container:, call:, keys:)` is the entry point that `Jobs::Materialize` calls.
235
-
236
- For each key, `Engine#produce_one`:
237
-
238
- 1. Looks up a registered workflow via `container.workflows.for(key)` (Pattern matching against the key).
239
- 2. If a workflow matches: `Workflow::Runner.new(definition, container:, call:).run(key)`.
240
- - `Runner` builds a `Workflow::Context`, executes each step in sequence, then publishes.
241
- - **Built-in publish**: writes the step output to the entry via `Envelope::Writer`, then calls `entry.publish_via(pctx)` for any `publish_to` or `publish_tree` targets.
242
- 3. If no workflow matches but the entry has publish targets: runs `publish_only` — publishes existing store bytes via `entry.publish_via` without re-fetching.
243
-
244
- **Workflow files** live in `.textus/workflows/*.rb`. The DSL:
245
-
246
- ```ruby
247
- Textus.workflow "my-producer" do
248
- match "artifacts.derived.*"
249
- step :fetch do |data, ctx|
250
- { content: { "key" => ctx.key } }
251
- end
252
- publish # uses built-in publish (writes to entry, then to publish targets)
253
- end
254
- ```
255
-
256
- ## Agent surface (boot + pulse + MCP)
257
-
258
- ```
259
- agent/plugin ──▶ gate (CLI | MCP) ──▶ Store ──▶ memory (.textus/)
260
- ```
261
-
262
- Two transports, one façade:
263
-
264
- - **CLI** — human/script surface. `textus boot`, `textus pulse --since=N`, `textus get/put/...`.
265
- - **MCP** — agent surface. `textus mcp serve` runs a stdio JSON-RPC 2.0 server. Tools are auto-derived from contracts. Session state (cursor, role, contract_etag) is server-side.
266
-
267
- The agent loop:
268
-
269
- 1. **Session start:** `boot()` → contract envelope (lanes, entries, workflows, write_flows, agent_quickstart).
270
- 2. **Per turn:** `pulse(since=cursor)` → `{cursor, changed, stale, pending_review, doctor, next_due_at}`.
271
- 3. **On demand:** `get`, `put`, `propose`, `schema_show`, `rule_explain`, `drain`.
272
-
273
- Contract drift surfaces as `ContractDrift` (contract_etag mismatch — a change to manifest/schemas); audit cursor expiry as `CursorExpired`. Both signal "call `boot` again."
@@ -1,141 +0,0 @@
1
- # Conventions
2
-
3
- > **Reference** · for integrators · **read when** you're shaping a `.textus/` tree and want the idiomatic choices
4
- > **SSoT for** idiomatic key naming, schema design, and automation integration · **reviewed** 2026-06 (v0.43)
5
-
6
- Guidelines for shaping a `.textus/` tree, naming keys, organising schemas, and integrating with build automation. The spec ([`../../SPEC.md`](../../SPEC.md)) defines what's enforceable; this document captures what's *idiomatic*.
7
-
8
- ## Key naming
9
-
10
- - **Segments are lowercase, kebab- or snake-case.** The grammar `^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?$` is the hard limit. Prefer `acme-dashboard` over `acmedashboard` when there's a natural word break.
11
- - **Lead with the lane in the key path.** `working.projects.acme.dashboard`, not `projects.acme.dashboard`. The lane prefix makes it obvious from the key alone whether a write will be accepted.
12
- - **Mirror the directory structure.** If `working.projects.acme.dashboard` resolves to `working/projects/acme/dashboard.md`, do not invent shortcuts that diverge.
13
- - **Don't pluralise the leaf.** `working.network.org.jane`, not `working.network.org.janes`. Pluralise the container, not the entry.
14
-
15
- ## Lane layout
16
-
17
- Recommended top-level layout — the spec allows alternatives, but this is what tooling will default to:
18
-
19
- ```
20
- .textus/
21
- manifest.yaml
22
- schemas/ # YAML schema definitions
23
- workflows/ # Textus.workflow DSL files for produced entry acquisition
24
- templates/ # ERB templates for publish rendering
25
- data/
26
- knowledge/ # authored truth: identity, voice, decisions — author-holders write
27
- scratchpad/ # agent's own durable lane (workspace) — keep-holders write
28
- proposals/ # AI proposals awaiting accept (propose)
29
- artifacts/ # computed outputs produced by drain — never edit by hand
30
- raw/ # write-once external source material (ingest)
31
- ```
32
-
33
- Inside `knowledge/`, group by **domain** (identity, people, projects, decisions, runbooks), not by file type or date. `knowledge.identity.*` is the convention for slow-changing identity facts. Inside `artifacts/`, group by **producer** (`artifacts/catalogs/`, `artifacts/indexes/`) so it's clear which build job owns what.
34
-
35
- ## Schema design
36
-
37
- - **One schema per entry type, not per directory.** `person.yaml`, `project.yaml`, `decision.yaml` — applied across multiple subtrees if the shape matches.
38
- - **Required = "this entry is meaningless without it."** Everything else is `optional`. Resist the urge to mark organisational metadata (like `tags`) required.
39
- - **Prefer `enum` over free-text** for low-cardinality fields (relationship type, status, severity). Agents are far better at picking from a list than at producing exact strings.
40
- - **Cap string lengths** with `max:` where the field has a natural bound (names, summaries). Skip for prose body — bodies are not schema-validated, only frontmatter is.
41
-
42
- ## Owner strings
43
-
44
- The `owner:` field in the manifest is **advisory metadata**, not an ACL. Use it to label *who's expected to write here*. The form is `<archetype>` or `<archetype>:<subject>`; the archetype must be one of `human`, `agent`, `automation` (validated at load — ADR 0045), and the subject is free-form:
45
-
46
- - `human:network` — humans curate
47
- - `agent:planner` — a specific named agent
48
- - `automation:catalog-skills` — a specific build job
49
-
50
- Tooling around `git blame` or audit logs may filter on owner; the gem itself only echoes it back in envelopes.
51
-
52
- ## Produced entries
53
-
54
- A produced entry declares `source: { from: external, command: "true", sources: [] }` and a matching `Textus.workflow` block in `.textus/workflows/**/*.rb`. The `source:` acquires **data** — it never renders; rendering is a publish concern (below).
55
-
56
- ```yaml
57
- - key: artifacts.feeds.skills
58
- lane: artifacts
59
- kind: produced
60
- format: json
61
- source: { from: external, command: "true", sources: [] }
62
- publish:
63
- - { to: docs/reference/skills.md, template: feeds/skills.erb }
64
- ```
65
-
66
- The matching workflow block:
67
-
68
- ```ruby
69
- # .textus/workflows/feeds/agentskills.rb
70
- Textus.workflow "agentskills" do
71
- match "artifacts.feeds.skills"
72
-
73
- step :fetch do |_, _ctx|
74
- { "content" => { "skills" => [...], "count" => 1 } }
75
- end
76
-
77
- publish
78
- end
79
- ```
80
-
81
- `drain` discovers workflows, matches entries, runs the steps, and writes the result back to the entry's data path. Publishing then copies or renders each `publish:` target.
82
-
83
- Age-based GC uses a `retention:` rule in the top-level `rules:` block:
84
-
85
- ```yaml
86
- rules:
87
- - match: artifacts.feeds.**
88
- retention: { ttl: 90d, action: archive }
89
- ```
90
-
91
- Full contract in [`../../SPEC.md` §5.2](../../SPEC.md).
92
-
93
- ## Freshness
94
-
95
- A `get` is always a pure read (ADR 0089) — it never triggers a re-produce:
96
-
97
- ```sh
98
- textus pulse --output=json # `stale` lists entries past their ttl
99
- textus drain --as=automation # re-produces stale entries
100
- ```
101
-
102
- See [`./lanes.md`](lanes.md) for lane semantics and [`../how-to/configuring-lanes.md`](../how-to/configuring-lanes.md) for setting up produced entries and workflows.
103
-
104
- ### Read vs. refresh
105
-
106
- There is one public read operation, and it is pure (ADR 0089):
107
-
108
- | Operation | Behaviour | Use for |
109
- |-----------|-----------|---------|
110
- | `ops.get` | A pure on-disk read annotated with a freshness verdict — it NEVER ingests, regardless of the entry's `action`. A stale `refresh` entry reads back stale until the next `drain`. | every caller — interactive reads, dashboards, scripts, and internal pipelines (materializer, projection, schema tooling, accept/reject/publish, uid, validator) |
111
-
112
- Refreshing a stale entry is `drain`'s job (or a `hook run` event), never a read's — so no caller can accidentally trigger network I/O by reading.
113
-
114
- ## Body content
115
-
116
- - **Bodies are Markdown.** Headings, lists, code fences — whatever a human or agent finds useful.
117
- - **The schema does not validate the body.** If a field belongs in structured data, put it in frontmatter, not the body.
118
- - **Keep entries short.** If a project entry hits 500 lines, it probably wants to be split into sub-entries (e.g. `working.projects.acme.dashboard` + `working.projects.acme.api`) rather than one mega-document.
119
-
120
- ## Concurrency
121
-
122
- For multi-writer environments, **always pass `if_etag`** on `put`. The gem treats etag-less writes as last-writer-wins on purpose (single-writer scripts, fresh-file creation), but anything resembling a daemon or a long-running agent should round-trip the etag.
123
-
124
- ## Application layering
125
-
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
-
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
- - **`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`.
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(...)`).
134
-
135
- The user-facing CLI surface, the wire envelope shape, and the protocol version (`textus/4`) are unchanged.
136
-
137
- ## Pairing with other tools
138
-
139
- - **MCP servers**: a thin server that exposes `textus get` and `textus put` as tools is the recommended way to give Claude/agents access. Don't bake MCP into this gem.
140
- - **Vector stores**: index `body` content into a vector store if you want fuzzy retrieval. `frontmatter` stays in textus as the source of truth for deterministic facts.
141
- - **CI**: run `textus doctor` (the `generator_drift` check) or `textus pulse` (the `stale` list) in CI to catch drift between derived entries and their sources.
data/lib/textus/boot.rb DELETED
@@ -1,181 +0,0 @@
1
- module Textus
2
- # Read-only "what's in this store and how do I use it" envelope.
3
- # Boot is side-effect-free. Reads from pre-computed artifacts and
4
- # the store catalog rather than computing inline.
5
- module Boot
6
- PROTOCOL_ID = PROTOCOL
7
-
8
- # Human-readable name(s) for the live lane(s) of a given kind, or `fallback`
9
- # when the manifest declares none. Lets write-flow guidance name the live
10
- # lane by kind instead of a hardcoded instance name (ADR 0034).
11
- def self.lane_label(manifest, kind, fallback)
12
- lanes = manifest.policy.lanes_of_kind(kind)
13
- lanes.empty? ? fallback : lanes.join(", ")
14
- end
15
-
16
- # Static, store-independent parts of the agent-facing protocol. The
17
- # `recipes` and `role_resolution` blocks are derived per-manifest in
18
- # agent_protocol(...) because lane and role names are user-configurable.
19
- AGENT_PROTOCOL_TEMPLATE = {
20
- "envelope_shape" => {
21
- "summary" => "every read/write payload is a JSON envelope with _meta, body, uid, and etag",
22
- "fields" => {
23
- "_meta" => "hash of structured frontmatter; schema-validated per entry family",
24
- "body" => "string payload (markdown/text) or nil for json/yaml formats where body lives in _meta",
25
- "uid" => "stable 16-char hex identifier; preserved across writes and key renames",
26
- "etag" => "content hash; pass back on writes to detect concurrent edits",
27
- },
28
- "ref" => "SPEC.md §8",
29
- },
30
- }.freeze
31
-
32
- # Curated agent-facing verb catalog. This declares which verbs the operator
33
- # CLI surfaces and in what order — the editorial presentation. The summary of
34
- # each verb is a fact, not presentation: it is derived from `contract.summary`
35
- # at load time (ADR 0039). A literal "summary" survives here only for grouped
36
- # CLI tokens (schema/key/rule/hook) that aggregate several sub-contracts and so
37
- # have no single contract to derive from. CLI_VERBS itself is assigned in
38
- # textus.rb after Zeitwerk eager_load so all contract files are present.
39
- CURATED_CLI_VERBS = [
40
- { "name" => "boot" },
41
- { "name" => "list" },
42
- { "name" => "get" },
43
- { "name" => "ingest" },
44
- { "name" => "where" },
45
- { "name" => "schema", "summary" => "schema operations: 'schema show KEY', 'schema diff', 'schema init', 'schema migrate'" },
46
- { "name" => "put" },
47
- { "name" => "propose" },
48
- { "name" => "accept" },
49
- { "name" => "enqueue" },
50
- { "name" => "key", "summary" => "key operations: 'key delete', 'key mv', 'key uid'" },
51
- { "name" => "drain" },
52
- { "name" => "audit" },
53
- { "name" => "blame" },
54
- { "name" => "rule", "summary" => "inspect effective rules: 'rule list', 'rule explain KEY'" },
55
- { "name" => "doctor" },
56
- { "name" => "jobs" },
57
- { "name" => "pulse" },
58
- ].freeze
59
-
60
- def self.contract_summaries
61
- Textus::VerbRegistry.registered.to_h { |s| [s.verb.to_s, s.summary] }
62
- end
63
-
64
- # Build the CLI verb catalog: each summary is derived from its contract when
65
- # one exists, falling back to the curated editorial string for grouped tokens
66
- # (schema/key/rule/hook). Called once from textus.rb after eager_load.
67
- def self.build_cli_verbs
68
- summaries = contract_summaries
69
- CURATED_CLI_VERBS.map do |entry|
70
- derived = summaries[entry["name"]]
71
- derived ? entry.merge("summary" => derived) : entry
72
- end
73
- end
74
-
75
- def self.build(container:)
76
- etag = Textus::Value::Etag.for_contract(container.root)
77
- latest_seq = container.audit_log.latest_seq
78
- artifact = read_artifact_content(container, "artifacts.boot")
79
- context = read_boot_context(container)
80
-
81
- # Prefer pre-computed artifact (drain computes, boot reads).
82
- # Fall back to inline manifest projection for stores that have not yet
83
- # run drain (test fixtures, fresh inits).
84
- stable = artifact || inline_boot_content(container.manifest, latest_seq)
85
-
86
- if stable["agent_quickstart"]
87
- stable = stable.merge(
88
- "agent_quickstart" => stable["agent_quickstart"].merge("latest_seq" => latest_seq),
89
- )
90
- end
91
-
92
- payload = {
93
- "protocol" => PROTOCOL_ID,
94
- "store_root" => container.root,
95
- "contract_etag" => etag,
96
- }.merge(stable)
97
- payload["context"] = context if context
98
- payload
99
- end
100
-
101
- def self.inline_boot_content(manifest, _latest_seq)
102
- agent_role = manifest.policy.proposer_role
103
- writable_lanes = manifest.data.declared_lane_kinds.keys.each_with_object([]) do |ln, acc|
104
- next unless agent_role
105
-
106
- verb = manifest.policy.verb_for_lane(ln)
107
- writers = manifest.policy.roles_with_capability(verb)
108
- acc << ln if writers.include?(agent_role)
109
- end
110
-
111
- {
112
- "lanes" => lanes_for(manifest),
113
- "agent_quickstart" => {
114
- "read_verbs" => Textus::Surface::MCP::Catalog.read_verbs,
115
- "write_verbs" => agent_role ? Textus::Surface::MCP::Catalog.write_verbs : [],
116
- "writable_lanes" => writable_lanes,
117
- "propose_lane" => manifest.policy.propose_lane_for(agent_role),
118
- },
119
- "agent_protocol" => agent_protocol(manifest),
120
- }
121
- end
122
-
123
- def self.agent_protocol(manifest)
124
- queue = manifest.policy.queue_lane
125
- feeds = lane_label(manifest, :machine, "the machine lane")
126
- AGENT_PROTOCOL_TEMPLATE.merge(
127
- "recipes" => {
128
- "read" => { "purpose" => "find and read an entry",
129
- "steps" => ["list (lane:, prefix:) — discover keys", "get KEY — returns the entry envelope"] },
130
- "write" => { "purpose" => "create or update an entry",
131
- "steps" => ["schema KEY — learn field shape", "put KEY — persist it (role-gated)"] },
132
- "propose" => { "purpose" => "agent suggests a change for human review",
133
- "agent_steps" => ["propose KEY — writes to #{queue} lane"],
134
- "human_steps" => ["accept #{queue}.KEY — promotes to target lane"] },
135
- "drain" => { "purpose" => "keep machine lanes fresh",
136
- "steps" => ["pulse — stale list names overdue entries",
137
- "drain (lane: #{feeds}) — re-pull stale entries"] },
138
- },
139
- "role_resolution" => {
140
- "summary" => "write role is resolved in order: --as flag, TEXTUS_ROLE env var, .textus/role file, " \
141
- "then a transport default ('human' for CLI, 'agent' for MCP)",
142
- "roles" => manifest.data.role_caps.keys,
143
- "ref" => "SPEC.md §5",
144
- },
145
- )
146
- end
147
-
148
- def self.lanes_for(manifest)
149
- manifest.data.declared_lane_kinds.keys.map do |name|
150
- verb = manifest.policy.verb_for_lane(name)
151
- row = { "name" => name, "writers" => manifest.policy.roles_with_capability(verb) }
152
- kind = manifest.policy.declared_kind(name)
153
- row["kind"] = kind.to_s if kind
154
- purpose = manifest.data.lane_descs[name]
155
- row["purpose"] = purpose if purpose && !purpose.empty?
156
- row
157
- end
158
- end
159
-
160
- def self.read_artifact_content(container, key)
161
- res = container.manifest.resolver.resolve(key)
162
- return nil unless res.path && File.exist?(res.path)
163
-
164
- env = Textus::Store::Entry::Reader.from(container: container).read(key)
165
- env&.content
166
- rescue Textus::Error
167
- nil
168
- end
169
-
170
- def self.read_boot_context(container)
171
- res = container.manifest.resolver.resolve("knowledge.boot")
172
- return nil unless res.path && File.exist?(res.path)
173
-
174
- env = Textus::Store::Entry::Reader.from(container: container).read("knowledge.boot")
175
- body = env&.body&.strip
176
- body.nil? || body.empty? ? nil : body
177
- rescue Textus::Error
178
- nil
179
- end
180
- end
181
- end
@@ -1,63 +0,0 @@
1
- module Textus
2
- module Dispatch
3
- module Contracts
4
- GetEntry = Data.define(:key)
5
-
6
- PutEntry = Data.define(:key, :meta, :body, :content, :if_etag)
7
-
8
- ListKeys = Data.define(:prefix, :lane, :q, :schema)
9
-
10
- DeleteKey = Data.define(:key, :if_etag)
11
-
12
- MoveKey = Data.define(:old_key, :new_key, :if_etag, :dry_run)
13
-
14
- ProposeEntry = Data.define(:key, :meta, :body, :content)
15
-
16
- AcceptProposal = Data.define(:pending_key)
17
-
18
- RejectProposal = Data.define(:pending_key)
19
-
20
- EnqueueJob = Data.define(:type, :args)
21
-
22
- AuditEntries = Data.define(:key, :lane, :role, :verb, :since, :seq_since, :correlation_id, :limit)
23
-
24
- PulseEntries = Data.define(:since)
25
-
26
- BlameEntry = Data.define(:key, :limit)
27
-
28
- WhereEntry = Data.define(:key)
29
-
30
- UidEntry = Data.define(:key)
31
-
32
- DepsEntry = Data.define(:key)
33
-
34
- RdepsEntry = Data.define(:key)
35
-
36
- BootStore = Data.define
37
-
38
- DoctorStore = Data.define(:checks)
39
-
40
- PublishedEntries = Data.define
41
-
42
- RuleExplain = Data.define(:key, :detail)
43
-
44
- RuleList = Data.define
45
-
46
- SchemaEnvelope = Data.define(:key)
47
-
48
- DrainStore = Data.define(:prefix, :lane)
49
-
50
- IngestEntry = Data.define(:kind, :slug, :url, :path, :lane, :label)
51
-
52
- JobsAction = Data.define(:state, :action, :job_id)
53
-
54
- RuleLint = Data.define(:candidate_yaml)
55
-
56
- DataMv = Data.define(:from, :to, :dry_run)
57
-
58
- KeyMvPrefix = Data.define(:from_prefix, :to_prefix, :dry_run)
59
-
60
- KeyDeletePrefix = Data.define(:prefix, :dry_run)
61
- end
62
- end
63
- end
@@ -1,21 +0,0 @@
1
- module Textus
2
- module Dispatch
3
- class HandlerRegistry
4
- def initialize
5
- @handlers = {}
6
- end
7
-
8
- def register(command_class, handler)
9
- @handlers[command_class] = handler
10
- end
11
-
12
- def for(command_class)
13
- @handlers[command_class] || raise("no handler registered for #{command_class}")
14
- end
15
-
16
- def registered?(command_class)
17
- @handlers.key?(command_class)
18
- end
19
- end
20
- end
21
- end