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
@@ -0,0 +1,21 @@
1
+ module Textus
2
+ module Lanes
3
+ module Ingest
4
+ module Resolver
5
+ module_function
6
+
7
+ def supersede(old_key, new_key, ctx:, call:)
8
+ old_env = ctx.read(key: old_key)
9
+ old_content = old_env&.content || {}
10
+ tombstone = {}
11
+ %w[ingested_at ingest_count].each { |k| tombstone[k] = old_content[k] if old_content.key?(k) }
12
+ source_kind = old_content.dig("source", "kind")
13
+ tombstone["source"] = { "kind" => source_kind } if source_kind
14
+ tombstone["superseded_by"] = new_key
15
+
16
+ ctx.put(key: old_key, meta: {}, body: nil, content: tombstone, call:)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,15 @@
1
+ module Textus
2
+ module Lanes
3
+ module Knowledge
4
+ module Freeform
5
+ def self.entries(manifest)
6
+ manifest.data.entries.reject { |e| e.key.start_with?("knowledge.loop.") }
7
+ end
8
+
9
+ def self.get(store, key)
10
+ store.get(key:)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,53 @@
1
+ module Textus
2
+ module Lanes
3
+ module Knowledge
4
+ module Handlers
5
+ module_function
6
+
7
+ LOOP_NODE_KEYS = {
8
+ "intent" => "knowledge.loop.intent",
9
+ "constraint.repo" => "knowledge.loop.constraint.repo",
10
+ "constraint.protocol" => "knowledge.loop.constraint.protocol",
11
+ "judgment.engineering" => "knowledge.loop.judgment.engineering",
12
+ "judgment.agent-behavior" => "knowledge.loop.judgment.agent-behavior",
13
+ "evidence" => "knowledge.loop.evidence",
14
+ "execution" => "knowledge.loop.execution.runbook",
15
+ "adoption" => "knowledge.loop.adoption",
16
+ }.freeze
17
+
18
+ def loop(ctx:, call:)
19
+ nodes = LOOP_NODE_KEYS.map { |name, key| { "name" => name, "key" => key } }
20
+ role = call&.role || "agent"
21
+ runner = Runner.new(ctx.manifest, cursor_store: ctx.cursor_store)
22
+ feedback = recent_feedback(ctx)
23
+ {
24
+ "loop" => nodes,
25
+ "count" => nodes.size,
26
+ "flow" => runner.flow,
27
+ "current_node" => runner.current_position(role),
28
+ "feedback" => feedback,
29
+ }
30
+ end
31
+
32
+ def recent_feedback(ctx)
33
+ rows = ctx.entry_index&.search(schema: "loop-feedback", lane: "knowledge", query: nil, prefix: nil)
34
+ return [] unless rows&.any?
35
+
36
+ rows.map do |row|
37
+ env = ctx.read(key: row["key"])
38
+ meta = env&.meta || {}
39
+ {
40
+ "key" => row["key"],
41
+ "node" => meta["node"],
42
+ "confidence" => meta["confidence"],
43
+ "signal" => meta["signal"],
44
+ "summary" => env&.body || meta["summary"],
45
+ }
46
+ end
47
+ rescue Textus::Error
48
+ []
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,45 @@
1
+ module Textus
2
+ module Lanes
3
+ module Knowledge
4
+ class Runner
5
+ def initialize(manifest, cursor_store: nil)
6
+ @manifest = manifest
7
+ @cursor_store = cursor_store
8
+ end
9
+
10
+ def loop_nodes
11
+ @manifest.data.entries.select do |e|
12
+ e.key.start_with?("knowledge.loop.") && e.key.count(".") == 2
13
+ end
14
+ end
15
+
16
+ def flow
17
+ loop_nodes.map { |e| e.key.split(".").last }
18
+ end
19
+
20
+ def current_node(active_index: 0)
21
+ nodes = loop_nodes
22
+ return nil if nodes.empty?
23
+
24
+ idx = active_index % nodes.size
25
+ { name: nodes[idx].key.split(".").last, index: idx, total: nodes.size }
26
+ end
27
+
28
+ def advance(active_index: 0)
29
+ current_node(active_index: active_index + 1).merge(advanced: true)
30
+ end
31
+
32
+ def current_position(role)
33
+ idx = @cursor_store ? @cursor_store.get_loop(role)[:index] : 0
34
+ current_node(active_index: idx)
35
+ end
36
+
37
+ def advance_role(role)
38
+ idx = @cursor_store ? @cursor_store.get_loop(role)[:index] + 1 : 1
39
+ @cursor_store&.set_loop(role, idx)
40
+ current_node(active_index: idx).merge(advanced: true)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,16 @@
1
+ module Textus
2
+ module Lanes
3
+ module Knowledge
4
+ module Verbs
5
+ end
6
+ end
7
+ end
8
+ end
9
+
10
+ Textus::Protocol::VerbRegistry.register(
11
+ Textus::Protocol::VerbRegistry::VerbSpec.new(
12
+ verb: :loop, summary: "Return the knowledge loop structure — node names and flow order.",
13
+ args: [], surfaces: %i[cli mcp], category: :read,
14
+ lane: :knowledge
15
+ ),
16
+ )
@@ -0,0 +1,164 @@
1
+ module Textus
2
+ module Lanes
3
+ module Scratchpad
4
+ module Handlers
5
+ PROPOSALS_PARENT_KEY = "scratchpad.proposals"
6
+
7
+ module_function
8
+
9
+ SESSION_PARENT_KEY = "scratchpad.sessions"
10
+
11
+ def session_open(ctx:, call:)
12
+ uuid = SecureRandom.uuid
13
+ key = "scratchpad.sessions.#{uuid}.session"
14
+
15
+ content = { "session_id" => uuid, "created_at" => Time.now.utc.iso8601, "status" => "active" }
16
+ ctx.put(key:, meta: {}, body: JSON.generate(content), content: nil, call:)
17
+
18
+ boot = begin
19
+ Textus::Protocol::Boot.build(container: ctx.container)
20
+ rescue StandardError
21
+ nil
22
+ end
23
+ result = { "session_id" => uuid, "status" => "active" }
24
+ result["boot"] = boot if boot
25
+ result
26
+ end
27
+
28
+ def parent_mentry(ctx)
29
+ ctx.manifest.data.entries.find { |e| e.key == SESSION_PARENT_KEY }
30
+ end
31
+
32
+ def propose(key:, ctx:, call:, meta: nil, data: nil)
33
+ body, content = data.is_a?(Hash) ? [nil, data] : [data, nil]
34
+
35
+ target_key = meta&.dig("proposal", "target_key")
36
+ if target_key
37
+ resolution = ctx.manifest.resolver.resolve(target_key)
38
+ Textus::Protocol::EntryConstraint.validate_naming!(target_key, resolution.entry)
39
+ end
40
+
41
+ full_key = "#{PROPOSALS_PARENT_KEY}.#{key}"
42
+ env = ctx.put(key: full_key, meta: meta || {}, body:, content:, call:)
43
+ env.to_h_for_wire
44
+ end
45
+
46
+ def accept(pending_key:, ctx:, call:, dry_run: false)
47
+ env = ctx.read(key: pending_key)
48
+ proposal = env&.meta&.dig("proposal")
49
+ raise Textus::ProposalError.new("entry has no proposal block: #{pending_key}") unless proposal
50
+
51
+ target = proposal["target_key"]
52
+ raise Textus::ProposalError.new("proposal missing target_key") unless target
53
+
54
+ action = proposal["action"] || "put"
55
+
56
+ if dry_run
57
+ target_env = ctx.read(key: target)
58
+ body_diff = Textus::Protocol::Diff.body(target_env&.body, env.body)
59
+ meta_diff = Textus::Protocol::Diff.meta(target_env&.meta&.dig("_meta") || {}, env.meta&.dig("_meta") || {})
60
+ result = { "dry_run" => true, "pending_key" => pending_key, "target_key" => target, "action" => action }
61
+ result["body"] = body_diff if body_diff
62
+ result["meta"] = meta_diff if meta_diff
63
+ result["summary"] = Textus::Protocol::Diff.summary(result)
64
+ return result
65
+ end
66
+
67
+ case action
68
+ when "put"
69
+ target_meta = env.meta.to_h.except("proposal")
70
+
71
+ # Parse body frontmatter to extract schema-relevant meta fields
72
+ resolution = ctx.manifest.resolver.resolve(target)
73
+ if env.body && !env.body.empty?
74
+ parsed = Textus::Protocol::Format.for(resolution.entry.format).parse(env.body)
75
+ body_meta = parsed["_meta"] || {}
76
+ target_meta = body_meta.merge(target_meta)
77
+ end
78
+
79
+ # Re-validate constraints at promotion time
80
+ payload = Textus::Value::Payload.new(meta: target_meta, body: env.body, content: env.content)
81
+ Textus::Protocol::EntryConstraint.validate_naming!(target, resolution.entry)
82
+ Textus::Protocol::EntryConstraint.validate_schema!(payload, resolution.entry, schemas: ctx.schemas)
83
+ Textus::Protocol::EntryConstraint.validate_format!(payload, resolution.entry)
84
+
85
+ ctx.put(key: target, meta: target_meta, body: env.body, content: env.content, call:)
86
+ when "delete"
87
+ ctx.delete(key: target, call:)
88
+ else
89
+ raise Textus::ProposalError.new("unknown action: #{action}")
90
+ end
91
+
92
+ ctx.delete(key: pending_key, call:)
93
+ { "protocol" => Textus::PROTOCOL, "accepted" => pending_key,
94
+ "target_key" => target, "action" => action, "cascade_key" => target }
95
+ end
96
+
97
+ def reject(pending_key:, ctx:, call:, reason: nil)
98
+ env = ctx.read(key: pending_key)
99
+ proposal = env&.meta&.dig("proposal")
100
+ raise Textus::ProposalError.new("entry has no proposal block: #{pending_key}") unless proposal
101
+
102
+ target_key = proposal["target_key"]
103
+ ctx.delete(key: pending_key, call:)
104
+ result = { "protocol" => Textus::PROTOCOL, "rejected" => pending_key, "target_key" => target_key }
105
+ result["reason"] = reason if reason
106
+ result
107
+ end
108
+
109
+ def diff(pending_key:, ctx:, call:)
110
+ env = ctx.read(key: pending_key)
111
+ proposal = env&.meta&.dig("proposal")
112
+ raise Textus::ProposalError.new("entry has no proposal block: #{pending_key}") unless proposal
113
+
114
+ target_key = proposal["target_key"]
115
+ raise Textus::ProposalError.new("proposal missing target_key") unless target_key
116
+
117
+ target_env = ctx.read(key: target_key)
118
+
119
+ body_diff = Textus::Protocol::Diff.body(target_env&.body, env.body)
120
+ meta_diff = Textus::Protocol::Diff.meta(target_env&.meta&.dig("_meta") || {}, env.meta&.dig("_meta") || {})
121
+
122
+ entry = ctx.manifest.data.entries.find { |e| e.key == target_key }
123
+ target_schema = entry&.schema_ref
124
+ proposal_schema = env&.meta&.dig("_meta", "schema")
125
+ schema_diff = nil
126
+ if proposal_schema && target_schema != proposal_schema
127
+ schema_diff = Textus::Protocol::Diff.schema({ "schema" => target_schema }, { "schema" => proposal_schema })
128
+ end
129
+
130
+ result = { "pending_key" => pending_key, "target_key" => target_key }
131
+ result["body"] = body_diff if body_diff
132
+ result["meta"] = meta_diff if meta_diff
133
+ result["schema"] = schema_diff if schema_diff
134
+ result["summary"] = Textus::Protocol::Diff.summary(result)
135
+ result
136
+ end
137
+
138
+ def session_close(session_id:, ctx:, call:, nodes_checked: nil)
139
+ key = "scratchpad.sessions.#{session_id}.session"
140
+ env = ctx.read(key:)
141
+ raise Textus::UsageError.new("session not found: #{session_id}") unless env
142
+
143
+ current = JSON.parse(env.body || "{}")
144
+ content = current.merge("status" => "sealed", "closed_at" => Time.now.utc.iso8601)
145
+ ctx.put(key:, meta: {}, body: JSON.generate(content), content: nil, call:)
146
+
147
+ feedback_key = "scratchpad.sessions.#{session_id}.feedback"
148
+ duration = current["created_at"] ? "#{((Time.now.utc - Time.parse(current["created_at"])) / 60).round}m" : "unknown"
149
+ feedback = {
150
+ "node" => "feedback",
151
+ "confidence" => "medium",
152
+ "signal" => "session_closed",
153
+ "summary" => "Session #{session_id[0..8]} sealed after #{duration}. " \
154
+ "Created at #{current["created_at"]}, closed at #{Time.now.utc.iso8601}.",
155
+ }
156
+ feedback["nodes_checked"] = nodes_checked if nodes_checked
157
+ ctx.put(key: feedback_key, meta: {}, body: JSON.generate(feedback), content: nil, call:)
158
+
159
+ { "session_id" => session_id, "status" => "sealed", "feedback_key" => feedback_key }
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,73 @@
1
+ module Textus
2
+ module Lanes
3
+ module Scratchpad
4
+ module Verbs
5
+ end
6
+ end
7
+ end
8
+ end
9
+
10
+ Textus::Protocol::VerbRegistry.register(
11
+ Textus::Protocol::VerbRegistry::VerbSpec.new(
12
+ verb: :session_open, summary: "Create a new agent session with workspace directories.",
13
+ args: [], surfaces: %i[cli mcp], category: :write,
14
+ lane: :scratchpad
15
+ ),
16
+ )
17
+ Textus::Protocol::VerbRegistry.register(
18
+ Textus::Protocol::VerbRegistry::VerbSpec.new(
19
+ verb: :session_close, summary: "Seal an active session by ID.",
20
+ args: [Textus::Protocol::VerbRegistry::ArgSpec.arg(name: :session_id, required: true,
21
+ description: "UUID of the session to close"),
22
+ Textus::Protocol::VerbRegistry::ArgSpec.arg(name: :nodes_checked, type: Array,
23
+ description: "list of loop nodes checked during the session")],
24
+ surfaces: %i[cli mcp], category: :write,
25
+ lane: :scratchpad
26
+ ),
27
+ )
28
+
29
+ Textus::Protocol::VerbRegistry.register(
30
+ Textus::Protocol::VerbRegistry::VerbSpec.new(
31
+ verb: :propose, summary: "Write a proposal to scratchpad.proposals for human review.",
32
+ args: [Textus::Protocol::VerbRegistry::ArgSpec.arg(name: :key, required: true, positional: true,
33
+ description: "key relative to propose_lane, e.g. 'decisions.feature-x'"),
34
+ Textus::Protocol::VerbRegistry::ArgSpec.arg(name: :meta, type: Hash, wire_name: :_meta,
35
+ description: "frontmatter. Include a 'proposal:' block naming the target_key"),
36
+ Textus::Protocol::VerbRegistry::ArgSpec.arg(name: :data,
37
+ description: "unified payload — String (body) or Hash (content)")],
38
+ surfaces: %i[cli mcp], category: :write,
39
+ lane: :scratchpad
40
+ ),
41
+ )
42
+ Textus::Protocol::VerbRegistry.register(
43
+ Textus::Protocol::VerbRegistry::VerbSpec.new(
44
+ verb: :accept, summary: "Apply a scratchpad.proposals entry to its target zone; requires author.",
45
+ args: [Textus::Protocol::VerbRegistry::ArgSpec.arg(name: :pending_key, required: true, positional: true,
46
+ description: "the queued proposal's key"),
47
+ Textus::Protocol::VerbRegistry::ArgSpec.arg(name: :dry_run, type: :boolean, default: false,
48
+ description: "when true, compute diff without promoting")],
49
+ surfaces: %i[cli mcp], category: :write,
50
+ lane: :scratchpad
51
+ ),
52
+ )
53
+ Textus::Protocol::VerbRegistry.register(
54
+ Textus::Protocol::VerbRegistry::VerbSpec.new(
55
+ verb: :reject, summary: "Discard a scratchpad.proposals entry without applying it.",
56
+ args: [Textus::Protocol::VerbRegistry::ArgSpec.arg(name: :pending_key, required: true, positional: true,
57
+ description: "the queued proposal's key"),
58
+ Textus::Protocol::VerbRegistry::ArgSpec.arg(name: :reason,
59
+ description: "optional explanation for the rejection")],
60
+ surfaces: %i[cli mcp], category: :write,
61
+ lane: :scratchpad
62
+ ),
63
+ )
64
+ Textus::Protocol::VerbRegistry.register(
65
+ Textus::Protocol::VerbRegistry::VerbSpec.new(
66
+ verb: :diff, summary: "Preview the diff between a scratchpad.proposals entry and its target.",
67
+ args: [Textus::Protocol::VerbRegistry::ArgSpec.arg(name: :pending_key, required: true, positional: true,
68
+ description: "the queued proposal's key")],
69
+ surfaces: %i[cli mcp],
70
+ category: :read,
71
+ lane: :scratchpad
72
+ ),
73
+ )
@@ -1,5 +1,5 @@
1
1
  module Textus
2
- module Dispatch
2
+ module Protocol
3
3
  class MissingArgs < Textus::Error
4
4
  attr_reader :spec, :missing
5
5
 
@@ -11,14 +11,8 @@ module Textus
11
11
  end
12
12
 
13
13
  module Binder
14
- Pending = Data.define(:spec, :inputs)
15
-
16
14
  module_function
17
15
 
18
- def command(spec, inputs)
19
- Pending.new(spec: spec, inputs: inputs)
20
- end
21
-
22
16
  def bind(spec, inputs)
23
17
  missing = spec.required_args.reject { |a| inputs.key?(a.name) }
24
18
  raise MissingArgs.new(spec, missing) unless missing.empty?
@@ -0,0 +1,34 @@
1
+ module Textus
2
+ module Protocol
3
+ module Boot
4
+ module Catalog
5
+ CURATED_CLI_VERBS = [
6
+ { "name" => "boot" },
7
+ { "name" => "list" },
8
+ { "name" => "loop" },
9
+ { "name" => "get" },
10
+ { "name" => "ingest" },
11
+ { "name" => "where" },
12
+ { "name" => "schema", "summary" => "schema operations: 'schema show KEY', 'schema diff', 'schema init', 'schema migrate'" },
13
+ { "name" => "put" },
14
+ { "name" => "propose" },
15
+ { "name" => "accept" },
16
+ { "name" => "key", "summary" => "key operations: 'key delete', 'key mv', 'key uid'" },
17
+ { "name" => "drain" },
18
+ { "name" => "session", "summary" => "session lifecycle: session_open, session_close" },
19
+ { "name" => "audit" },
20
+ { "name" => "blame" },
21
+ { "name" => "pulse" },
22
+ ].freeze
23
+
24
+ def self.build
25
+ summaries = Textus::Protocol::VerbRegistry.registered.to_h { |s| [s.verb.to_s, s.summary] }
26
+ CURATED_CLI_VERBS.map do |entry|
27
+ derived = summaries[entry["name"]]
28
+ derived ? entry.merge("summary" => derived) : entry
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,124 @@
1
+ module Textus
2
+ module Protocol
3
+ module Boot
4
+ module Context
5
+ def self.build(manifest:, event_store:, file_system:, layout:)
6
+ etag = Textus::Value::Etag.for_contract(layout)
7
+ latest_seq = event_store.latest[:seq]
8
+ artifact = read_artifact_content(manifest, file_system, layout)
9
+ context = read_boot_context(manifest, file_system, layout)
10
+
11
+ stable = artifact || inline_boot_content(manifest, latest_seq)
12
+ if stable["agent_quickstart"]
13
+ stable = stable.merge(
14
+ "agent_quickstart" => stable["agent_quickstart"].merge("latest_seq" => latest_seq),
15
+ )
16
+ end
17
+
18
+ payload = {
19
+ "protocol" => Textus::PROTOCOL,
20
+ "store_root" => layout.root,
21
+ "contract_etag" => etag,
22
+ }.merge(stable)
23
+ payload["context"] = context if context
24
+ payload
25
+ end
26
+
27
+ def self.read_artifact_content(manifest, file_system, _layout)
28
+ key = "artifacts.boot"
29
+ res = manifest.resolver.resolve(key)
30
+ return nil unless res.path && file_system.exists?(res.path)
31
+
32
+ mentry = res.entry
33
+ raw = file_system.read(res.path)
34
+ parsed = Textus::Protocol::Format.for(mentry.format).parse(raw, path: res.path)
35
+ parsed["content"]
36
+ rescue Textus::Error
37
+ nil
38
+ end
39
+
40
+ def self.read_boot_context(manifest, file_system, _layout)
41
+ res = manifest.resolver.resolve("knowledge.boot")
42
+ return nil unless res.path && file_system.exists?(res.path)
43
+
44
+ mentry = res.entry
45
+ raw = file_system.read(res.path)
46
+ parsed = Textus::Protocol::Format.for(mentry.format).parse(raw, path: res.path)
47
+ body = parsed["body"]&.strip
48
+ body.nil? || body.empty? ? nil : body
49
+ rescue Textus::Error
50
+ nil
51
+ end
52
+
53
+ def self.inline_boot_content(manifest, _latest_seq)
54
+ agent_role = manifest.policy.proposer_role
55
+ writable_lanes = manifest.data.declared_lane_kinds.keys.each_with_object([]) do |ln, acc|
56
+ next unless agent_role
57
+
58
+ writers = manifest.policy.writers_for(ln)
59
+ acc << ln if writers.include?(agent_role)
60
+ end
61
+
62
+ author_writers = manifest.policy.writers_for("knowledge")
63
+ agent_has_author = agent_role && author_writers.include?(agent_role)
64
+ write_verbs = if agent_role
65
+ mcp_verbs = Textus::Protocol::VerbRegistry.registered.select { |s| s.write? && s.mcp? }
66
+ unless agent_has_author
67
+ forbidden = %i[accept reject].to_set.freeze
68
+ mcp_verbs = mcp_verbs.reject { |s| forbidden.include?(s.verb) }
69
+ end
70
+ mcp_verbs.map { |s| s.verb.to_s }
71
+ else
72
+ []
73
+ end
74
+
75
+ meta_glossary = {
76
+ "knowledge.loop.adoption" => { "node" => "target loop node", "confidence" => "high|medium|low",
77
+ "signal" => "observation type", "summary" => "brief description" },
78
+ "scratchpad.proposals" => { "proposal" => "{ target_key, action }" },
79
+ "raw" => { "source" => "{ kind, url, path, label }" },
80
+ }.freeze
81
+
82
+ blueprints = manifest.data.entries.map do |entry|
83
+ bp = { "key_prefix" => entry.key, "format" => entry.format.to_s, "naming" => entry.naming.to_s }
84
+ bp["schema"] = entry.schema.to_s if entry.schema
85
+ meta = meta_glossary[entry.key]
86
+ bp["meta"] = meta if meta
87
+ bp
88
+ end
89
+
90
+ {
91
+ "lanes" => lanes_for(manifest),
92
+ "entry_blueprints" => blueprints,
93
+ "workflow_tips" => [
94
+ { "when" => "write an entry", "then" => "`data` param: String for body, Hash for content" },
95
+ { "when" => "edit a protected path", "then" => "`textus propose key=my-change data=...` — human reviews and accepts" },
96
+ { "when" => "explore recent changes", "then" => "`textus pulse` then `textus get <changed_key>`" },
97
+ { "when" => "find related entries", "then" => "`textus graph key=<key> depth=2`" },
98
+ ],
99
+ "agent_quickstart" => {
100
+ "read_verbs" => Textus::Protocol::VerbRegistry.registered.select { |s| s.read? && s.mcp? }.map { |s| s.verb.to_s },
101
+ "write_verbs" => write_verbs,
102
+ "writable_lanes" => writable_lanes,
103
+ "propose_lane" => manifest.policy.propose_lane_for(agent_role),
104
+ },
105
+ }
106
+ end
107
+
108
+ def self.lanes_for(manifest)
109
+ manifest.data.declared_lane_kinds.keys.map do |name|
110
+ row = { "name" => name, "writers" => manifest.policy.writers_for(name) }
111
+ purpose = manifest.data.lane_descs[name]
112
+ row["purpose"] = purpose if purpose && !purpose.empty?
113
+ row
114
+ end
115
+ end
116
+
117
+ class NullEntryIndex
118
+ def record(...) = nil
119
+ def remove(...) = nil
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,33 @@
1
+ module Textus
2
+ module Protocol
3
+ module Boot
4
+ module Protocol
5
+ ENVELOPE_SHAPE = {
6
+ "envelope_shape" => {
7
+ "summary" => "every read/write payload is a JSON envelope with _meta, body, uid, and etag",
8
+ "fields" => {
9
+ "_meta" => "hash of structured frontmatter; schema-validated per entry family",
10
+ "body" => "string payload (markdown/text) or nil for json/yaml formats",
11
+ "uid" => "stable 16-char hex identifier; preserved across writes and key renames",
12
+ "etag" => "content hash; pass back on writes to detect concurrent edits",
13
+ },
14
+ },
15
+ }.freeze
16
+
17
+ def self.build(manifest)
18
+ ENVELOPE_SHAPE.merge(
19
+ "lanes" => lanes_for(manifest),
20
+ "roles" => Textus::Protocol::V4::LANES.values.flat_map { |r| r[:writers] }.uniq,
21
+ "discipline" => "When edit/write tools are blocked for a path, use `textus propose` to " \
22
+ "suggest changes for human review. Never use shell scripts (sed, python, rm) " \
23
+ "to bypass permission gates — that breaks the audit trail and bypasses role gates.",
24
+ )
25
+ end
26
+
27
+ def self.lanes_for(manifest)
28
+ Context.lanes_for(manifest)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,42 @@
1
+ require_relative "boot/catalog"
2
+ require_relative "boot/protocol"
3
+ require_relative "boot/context"
4
+
5
+ module Textus
6
+ module Protocol
7
+ module Boot
8
+ PROTOCOL_ID = PROTOCOL
9
+ CURATED_CLI_VERBS = Catalog::CURATED_CLI_VERBS
10
+
11
+ def self.contract_summaries
12
+ Textus::Protocol::VerbRegistry.registered.to_h { |s| [s.verb.to_s, s.summary] }
13
+ end
14
+
15
+ def self.build(container:)
16
+ manifest = container.manifest
17
+ envelope = Context.build(
18
+ manifest:,
19
+ event_store: container.store.audit_log,
20
+ file_system: container.store.file_system,
21
+ layout: container.layout,
22
+ )
23
+ envelope.merge(
24
+ "catalog" => Catalog.build,
25
+ "agent_protocol" => Protocol.build(manifest),
26
+ "schemas" => build_schemas(container.schemas),
27
+ )
28
+ end
29
+
30
+ def self.build_schemas(registry)
31
+ return [] unless registry
32
+
33
+ registry.all.map do |s|
34
+ h = { "name" => s.name, "required" => s.required, "optional" => s.optional }
35
+ h["fields"] = s.fields unless s.fields.empty?
36
+ h["evolution"] = s.evolution unless s.evolution.empty?
37
+ h
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,17 @@
1
+ module Textus
2
+ module Protocol
3
+ class Builder
4
+ class Domain
5
+ Components = Data.define(:manifest, :schemas, :workflows, :workflow_registry)
6
+
7
+ def call(root, layout:)
8
+ manifest = Manifest.load(root)
9
+ schemas = Schema::Registry.new(layout.schemas_dir)
10
+ workflows = Workflow::Registry.load_all(root)
11
+ workflow_registry = Workflow::Registry.new(workflows)
12
+ Components.new(manifest:, schemas:, workflows:, workflow_registry:)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end