textus 0.55.1 → 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 (329) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +175 -2768
  3. data/README.md +17 -18
  4. data/lib/textus/artifact.rb +8 -0
  5. data/lib/textus/errors.rb +2 -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/{gate → protocol}/binder.rb +2 -10
  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/protocol/freshness/ttl_evaluator.rb +61 -0
  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/geometry.rb → protocol/layout.rb} +32 -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 +25 -25
  129. data/lib/textus/surface/cli/sources.rb +1 -3
  130. data/lib/textus/surface/cli/verb/get.rb +3 -6
  131. data/lib/textus/surface/cli/verb/init.rb +1 -1
  132. data/lib/textus/surface/cli/verb/put.rb +8 -6
  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 +26 -62
  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/errors.rb +0 -10
  141. data/lib/textus/surface/mcp/server.rb +46 -46
  142. data/lib/textus/surface/watcher.rb +18 -12
  143. data/lib/textus/surface/web.rb +50 -0
  144. data/lib/textus/value/call.rb +1 -1
  145. data/lib/textus/{core → value}/duration.rb +1 -4
  146. data/lib/textus/value/etag.rb +7 -7
  147. data/lib/textus/value/payload.rb +7 -0
  148. data/lib/textus/version.rb +1 -1
  149. data/lib/textus/workflow/consumer.rb +39 -0
  150. data/lib/textus/workflow/dsl/lifecycle.rb +32 -0
  151. data/lib/textus/workflow/dsl/pattern_matcher.rb +20 -0
  152. data/lib/textus/workflow/dsl/step_builder.rb +56 -0
  153. data/lib/textus/workflow/dsl.rb +111 -21
  154. data/lib/textus/workflow/event_emitter.rb +21 -0
  155. data/lib/textus/workflow/publisher.rb +59 -0
  156. data/lib/textus/workflow/registry.rb +31 -8
  157. data/lib/textus/workflow/runner.rb +25 -61
  158. data/lib/textus/workflow/scheduler.rb +45 -0
  159. data/lib/textus/workflow/step_executor.rb +78 -0
  160. data/lib/textus/workflow/step_helpers.rb +102 -0
  161. data/lib/textus/workflow.rb +2 -5
  162. data/lib/textus.rb +16 -80
  163. metadata +167 -183
  164. data/SPEC.md +0 -908
  165. data/docs/architecture/README.md +0 -273
  166. data/docs/reference/conventions.md +0 -138
  167. data/lib/textus/action/accept.rb +0 -46
  168. data/lib/textus/action/audit.rb +0 -94
  169. data/lib/textus/action/base.rb +0 -42
  170. data/lib/textus/action/blame.rb +0 -79
  171. data/lib/textus/action/boot.rb +0 -15
  172. data/lib/textus/action/data_mv.rb +0 -58
  173. data/lib/textus/action/deps.rb +0 -19
  174. data/lib/textus/action/doctor.rb +0 -17
  175. data/lib/textus/action/drain.rb +0 -31
  176. data/lib/textus/action/enqueue.rb +0 -37
  177. data/lib/textus/action/get.rb +0 -34
  178. data/lib/textus/action/ingest.rb +0 -199
  179. data/lib/textus/action/jobs.rb +0 -27
  180. data/lib/textus/action/key_delete.rb +0 -26
  181. data/lib/textus/action/key_delete_prefix.rb +0 -35
  182. data/lib/textus/action/key_mv.rb +0 -122
  183. data/lib/textus/action/key_mv_prefix.rb +0 -48
  184. data/lib/textus/action/list.rb +0 -28
  185. data/lib/textus/action/propose.rb +0 -42
  186. data/lib/textus/action/published.rb +0 -22
  187. data/lib/textus/action/pulse.rb +0 -49
  188. data/lib/textus/action/put.rb +0 -38
  189. data/lib/textus/action/rdeps.rb +0 -24
  190. data/lib/textus/action/reject.rb +0 -28
  191. data/lib/textus/action/rule_explain.rb +0 -81
  192. data/lib/textus/action/rule_lint.rb +0 -62
  193. data/lib/textus/action/rule_list.rb +0 -38
  194. data/lib/textus/action/schema_envelope.rb +0 -22
  195. data/lib/textus/action/uid.rb +0 -19
  196. data/lib/textus/action/where.rb +0 -21
  197. data/lib/textus/boot.rb +0 -202
  198. data/lib/textus/contract/arg.rb +0 -10
  199. data/lib/textus/contract/dsl.rb +0 -88
  200. data/lib/textus/contract/spec.rb +0 -25
  201. data/lib/textus/contract.rb +0 -12
  202. data/lib/textus/core/freshness/evaluator.rb +0 -150
  203. data/lib/textus/core/freshness/verdict.rb +0 -34
  204. data/lib/textus/core/freshness.rb +0 -11
  205. data/lib/textus/core/retention/sweep.rb +0 -57
  206. data/lib/textus/core/retention.rb +0 -11
  207. data/lib/textus/core/sentinel.rb +0 -30
  208. data/lib/textus/doctor/check/audit_log.rb +0 -34
  209. data/lib/textus/doctor/check/generator_drift.rb +0 -29
  210. data/lib/textus/doctor/check/illegal_keys.rb +0 -60
  211. data/lib/textus/doctor/check/manifest_files.rb +0 -25
  212. data/lib/textus/doctor/check/notebook_sources.rb +0 -50
  213. data/lib/textus/doctor/check/orphaned_publish_targets.rb +0 -35
  214. data/lib/textus/doctor/check/proposal_targets.rb +0 -45
  215. data/lib/textus/doctor/check/protocol_version.rb +0 -47
  216. data/lib/textus/doctor/check/publish_tree_index_overlap.rb +0 -48
  217. data/lib/textus/doctor/check/raw_asset_paths.rb +0 -50
  218. data/lib/textus/doctor/check/rule_ambiguity.rb +0 -49
  219. data/lib/textus/doctor/check/schema_parse_error.rb +0 -28
  220. data/lib/textus/doctor/check/schema_violations.rb +0 -28
  221. data/lib/textus/doctor/check/schemas.rb +0 -26
  222. data/lib/textus/doctor/check/sentinels.rb +0 -60
  223. data/lib/textus/doctor/check/stale_reviewed_stamp.rb +0 -54
  224. data/lib/textus/doctor/check/templates.rb +0 -29
  225. data/lib/textus/doctor/check/unowned_schema_fields.rb +0 -40
  226. data/lib/textus/doctor/check.rb +0 -41
  227. data/lib/textus/doctor/validator.rb +0 -93
  228. data/lib/textus/doctor.rb +0 -54
  229. data/lib/textus/format/base.rb +0 -49
  230. data/lib/textus/format/json.rb +0 -102
  231. data/lib/textus/format/markdown.rb +0 -80
  232. data/lib/textus/format/shared.rb +0 -17
  233. data/lib/textus/format/text.rb +0 -55
  234. data/lib/textus/format/yaml.rb +0 -125
  235. data/lib/textus/format.rb +0 -46
  236. data/lib/textus/gate/auth.rb +0 -212
  237. data/lib/textus/gate.rb +0 -92
  238. data/lib/textus/init.rb +0 -157
  239. data/lib/textus/jobs.rb +0 -9
  240. data/lib/textus/key/distance.rb +0 -55
  241. data/lib/textus/key/grammar.rb +0 -33
  242. data/lib/textus/key/matching.rb +0 -24
  243. data/lib/textus/key/path.rb +0 -28
  244. data/lib/textus/manifest/capabilities.rb +0 -29
  245. data/lib/textus/manifest/data.rb +0 -102
  246. data/lib/textus/manifest/entry/base.rb +0 -105
  247. data/lib/textus/manifest/entry/ignore_matcher.rb +0 -46
  248. data/lib/textus/manifest/entry/leaf.rb +0 -17
  249. data/lib/textus/manifest/entry/nested.rb +0 -37
  250. data/lib/textus/manifest/entry/parser.rb +0 -99
  251. data/lib/textus/manifest/entry/produced.rb +0 -36
  252. data/lib/textus/manifest/entry/publish/mode.rb +0 -45
  253. data/lib/textus/manifest/entry/publish/none.rb +0 -14
  254. data/lib/textus/manifest/entry/publish/subtree_mirror.rb +0 -73
  255. data/lib/textus/manifest/entry/publish/template.rb +0 -16
  256. data/lib/textus/manifest/entry/publish/to_paths.rb +0 -76
  257. data/lib/textus/manifest/entry/publish/tree.rb +0 -50
  258. data/lib/textus/manifest/entry/publish.rb +0 -45
  259. data/lib/textus/manifest/entry/validators/format_matrix.rb +0 -21
  260. data/lib/textus/manifest/entry/validators/ignore.rb +0 -28
  261. data/lib/textus/manifest/entry/validators/publish.rb +0 -30
  262. data/lib/textus/manifest/entry/validators.rb +0 -18
  263. data/lib/textus/manifest/entry.rb +0 -8
  264. data/lib/textus/manifest/policy/matcher.rb +0 -51
  265. data/lib/textus/manifest/policy/publish_target.rb +0 -34
  266. data/lib/textus/manifest/policy/react.rb +0 -30
  267. data/lib/textus/manifest/policy/retention.rb +0 -29
  268. data/lib/textus/manifest/policy/source.rb +0 -30
  269. data/lib/textus/manifest/policy.rb +0 -95
  270. data/lib/textus/manifest/resolver.rb +0 -118
  271. data/lib/textus/manifest/rules.rb +0 -86
  272. data/lib/textus/manifest/schema/contract.rb +0 -61
  273. data/lib/textus/manifest/schema/keys.rb +0 -90
  274. data/lib/textus/manifest/schema/semantics/cross_field.rb +0 -53
  275. data/lib/textus/manifest/schema/semantics/invariants.rb +0 -125
  276. data/lib/textus/manifest/schema/semantics/migration.rb +0 -83
  277. data/lib/textus/manifest/schema/semantics.rb +0 -27
  278. data/lib/textus/manifest/schema/validator.rb +0 -48
  279. data/lib/textus/manifest/schema/vocabulary.rb +0 -25
  280. data/lib/textus/manifest/schema.rb +0 -30
  281. data/lib/textus/manifest.rb +0 -65
  282. data/lib/textus/meta.rb +0 -54
  283. data/lib/textus/port/audit_log.rb +0 -268
  284. data/lib/textus/port/build_lock.rb +0 -64
  285. data/lib/textus/port/clock.rb +0 -10
  286. data/lib/textus/port/publisher.rb +0 -60
  287. data/lib/textus/port/sentinel_store.rb +0 -83
  288. data/lib/textus/port/storage/file_stat.rb +0 -19
  289. data/lib/textus/port/storage/file_store.rb +0 -26
  290. data/lib/textus/port/store.rb +0 -93
  291. data/lib/textus/port/watcher_lock.rb +0 -48
  292. data/lib/textus/produce/engine.rb +0 -50
  293. data/lib/textus/produce/render.rb +0 -20
  294. data/lib/textus/schema/tools.rb +0 -115
  295. data/lib/textus/schema.rb +0 -100
  296. data/lib/textus/schemas.rb +0 -54
  297. data/lib/textus/store/compositor.rb +0 -34
  298. data/lib/textus/store/container.rb +0 -43
  299. data/lib/textus/store/cursor.rb +0 -26
  300. data/lib/textus/store/envelope/reader.rb +0 -43
  301. data/lib/textus/store/envelope/writer.rb +0 -195
  302. data/lib/textus/store/index/builder.rb +0 -74
  303. data/lib/textus/store/index/lookup.rb +0 -60
  304. data/lib/textus/store/jobs/base.rb +0 -13
  305. data/lib/textus/store/jobs/index.rb +0 -15
  306. data/lib/textus/store/jobs/materialize.rb +0 -15
  307. data/lib/textus/store/jobs/plan.rb +0 -11
  308. data/lib/textus/store/jobs/planner.rb +0 -104
  309. data/lib/textus/store/jobs/queue.rb +0 -154
  310. data/lib/textus/store/jobs/registry.rb +0 -19
  311. data/lib/textus/store/jobs/retention.rb +0 -50
  312. data/lib/textus/store/jobs/sweep.rb +0 -21
  313. data/lib/textus/store/jobs/worker.rb +0 -64
  314. data/lib/textus/store/session.rb +0 -37
  315. data/lib/textus/store.rb +0 -99
  316. data/lib/textus/surface/cli/verb/doctor.rb +0 -19
  317. data/lib/textus/surface/cli/verb/schema_diff.rb +0 -17
  318. data/lib/textus/surface/cli/verb/schema_init.rb +0 -21
  319. data/lib/textus/surface/cli/verb/schema_migrate.rb +0 -21
  320. data/lib/textus/surface/mcp.rb +0 -8
  321. data/lib/textus/surface/projector.rb +0 -27
  322. data/lib/textus/surface/role_scope.rb +0 -34
  323. data/lib/textus/value/command.rb +0 -16
  324. data/lib/textus/value/envelope.rb +0 -89
  325. data/lib/textus/value/result.rb +0 -26
  326. data/lib/textus/workflow/collector.rb +0 -27
  327. data/lib/textus/workflow/context.rb +0 -5
  328. data/lib/textus/workflow/loader.rb +0 -17
  329. data/lib/textus/workflow/pattern.rb +0 -18
@@ -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
@@ -0,0 +1,17 @@
1
+ module Textus
2
+ module Protocol
3
+ class Builder
4
+ class Infrastructure
5
+ Components = Data.define(:database, :file_system, :store, :layout)
6
+
7
+ def call(root)
8
+ layout = Layout.new(root)
9
+ database = Infra::Database.new(layout:).setup!
10
+ file_system = Infra::FileSystem.new
11
+ store = Infra::Store.new(database:, file_system:)
12
+ Components.new(database:, file_system:, store:, layout:)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,46 @@
1
+ module Textus
2
+ module Protocol
3
+ class Builder
4
+ class Runtime
5
+ Components = Data.define(:store_engine, :gate, :ttl_evaluator, :container)
6
+
7
+ def call(infra:, domain:, root:)
8
+ event_emitter = Textus::Workflow::EventEmitter.new(
9
+ infra.store.workflow_queue,
10
+ registry: domain.workflow_registry,
11
+ )
12
+
13
+ store_engine = StoreEngine.new(
14
+ file_system: infra.file_system, manifest: domain.manifest,
15
+ layout: infra.layout, store: infra.store,
16
+ event_emitter:, schemas: domain.schemas
17
+ )
18
+
19
+ ttl_evaluator = Freshness::TtlEvaluator.new(
20
+ manifest: domain.manifest,
21
+ file_stat: infra.file_system,
22
+ clock: Infra::Clock.new,
23
+ )
24
+
25
+ container = Container.new(
26
+ store: infra.store, layout: infra.layout,
27
+ manifest: domain.manifest, schemas: domain.schemas,
28
+ workflows: domain.workflows,
29
+ store_engine: store_engine,
30
+ root:, ttl_evaluator:
31
+ )
32
+
33
+ gate = Gate.new(
34
+ manifest: domain.manifest,
35
+ resolver: domain.manifest.resolver,
36
+ container:,
37
+ store_engine:,
38
+ )
39
+
40
+ container.gate = gate
41
+ Components.new(store_engine:, gate:, ttl_evaluator:, container:)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,11 @@
1
+ module Textus
2
+ module Protocol
3
+ class Builder
4
+ def call(root)
5
+ infra = Infrastructure.new.call(root)
6
+ domain = Domain.new.call(root, layout: infra.layout)
7
+ Runtime.new.call(infra:, domain:, root:).container
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,22 @@
1
+ module Textus
2
+ module Protocol
3
+ class Container
4
+ attr_accessor :gate
5
+ attr_reader :store, :layout, :manifest, :schemas, :workflows, :workflow_registry, :root, :ttl_evaluator, :store_engine
6
+
7
+ def initialize(store:, layout:, manifest:, schemas:, workflows:,
8
+ root: nil, gate: nil, ttl_evaluator: nil, store_engine: nil)
9
+ @store = store
10
+ @layout = layout
11
+ @manifest = manifest
12
+ @schemas = schemas
13
+ @workflows = workflows
14
+ @workflow_registry = workflows ? Textus::Workflow::Registry.new(workflows) : nil
15
+ @root = root
16
+ @gate = gate
17
+ @ttl_evaluator = ttl_evaluator
18
+ @store_engine = store_engine
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,170 @@
1
+ module Textus
2
+ module Protocol
3
+ module Diff
4
+ module_function
5
+
6
+ def body(a_text, b_text)
7
+ a_lines = (a_text || "").lines.map(&:chomp)
8
+ b_lines = (b_text || "").lines.map(&:chomp)
9
+ hunks = build_hunks(myers_diff(a_lines, b_lines), a_lines, b_lines)
10
+ { "type" => "body", "hunks" => hunks, "stats" => stats(hunks) }
11
+ end
12
+
13
+ def meta(a_hash, b_hash)
14
+ a = a_hash || {}
15
+ b = b_hash || {}
16
+ all_keys = (a.keys + b.keys).uniq.sort
17
+ changes = all_keys.filter_map do |k|
18
+ av = a[k]
19
+ bv = b[k]
20
+ next if av == bv
21
+
22
+ { "key" => k.to_s, "old" => av, "new" => bv }
23
+ end
24
+ { "type" => "meta", "changes" => changes, "stats" => { "changed" => changes.size } }
25
+ end
26
+
27
+ def schema(a_schema, b_schema)
28
+ meta(a_schema, b_schema).merge("type" => "schema")
29
+ end
30
+
31
+ def summary(diff_result)
32
+ parts = []
33
+ parts << "#{diff_result.dig("body", "stats", "additions") || 0}+" if diff_result["body"]
34
+ parts << "#{diff_result.dig("body", "stats", "deletions") || 0}-" if diff_result["body"]
35
+ parts << "#{diff_result.dig("meta", "stats", "changed") || 0}~" if diff_result["meta"]
36
+ parts.join(" ")
37
+ end
38
+
39
+ def myers_diff(a_lines, b_lines)
40
+ a_count = a_lines.size
41
+ b_count = b_lines.size
42
+ max_d = a_count + b_count
43
+ v = Hash.new(0)
44
+ v[1] = 0
45
+ trace = []
46
+
47
+ (0..max_d).each do |d|
48
+ (-d..d).step(2).each do |k|
49
+ x = if k == -d || (k != d && v[k - 1] < v[k + 1])
50
+ v[k + 1]
51
+ else
52
+ v[k - 1] + 1
53
+ end
54
+ y = x - k
55
+ while x < a_count && y < b_count && a_lines[x] == b_lines[y]
56
+ x += 1
57
+ y += 1
58
+ end
59
+ v[k] = x
60
+ return backtrack(trace, x, y) if x >= a_count && y >= b_count
61
+ end
62
+ trace << v.dup
63
+ end
64
+ []
65
+ end
66
+
67
+ def backtrack(trace, x_pos, y_pos)
68
+ script = []
69
+ d = trace.size - 1
70
+ return script if d.negative?
71
+
72
+ k = x_pos - y_pos
73
+ while d.positive?
74
+ prev_v = trace[d - 1]
75
+ prev_k = if k == -d || (k != d && (prev_v[k - 1] || -1) < (prev_v[k + 1] || -1))
76
+ k + 1
77
+ else
78
+ k - 1
79
+ end
80
+ prev_x = prev_v[prev_k] || 0
81
+ prev_y = prev_x - prev_k
82
+
83
+ while x_pos > prev_x && y_pos > prev_y
84
+ script.unshift([:eq, x_pos - 1, y_pos - 1])
85
+ x_pos -= 1
86
+ y_pos -= 1
87
+ end
88
+
89
+ if x_pos > prev_x
90
+ script.unshift([:del, x_pos - 1, nil])
91
+ x_pos -= 1
92
+ elsif y_pos > prev_y
93
+ script.unshift([:ins, nil, y_pos - 1])
94
+ y_pos -= 1
95
+ end
96
+
97
+ d -= 1
98
+ k = prev_k
99
+ x_pos = prev_x
100
+ y_pos = prev_y
101
+ end
102
+
103
+ script
104
+ end
105
+
106
+ def build_hunks(script, a_lines, b_lines)
107
+ return [] if script.empty?
108
+
109
+ hunks = []
110
+ i = 0
111
+ while i < script.size
112
+ next_context = find_next_change(script, i)
113
+ break unless next_context
114
+
115
+ start = [next_context - 3, 0].max
116
+ hunk_end_script = find_hunk_end(script, next_context)
117
+ hunk_end = [hunk_end_script + 3, script.size - 1].min
118
+
119
+ a_start = nil
120
+ b_start = nil
121
+ a_lines_hunk = []
122
+ b_lines_hunk = []
123
+
124
+ (start..hunk_end).each do |j|
125
+ op, ai, bi = script[j]
126
+ a_start ||= ai if ai
127
+ b_start ||= bi if bi
128
+
129
+ case op
130
+ when :eq
131
+ a_lines_hunk << { "type" => "context", "line" => a_lines[ai] }
132
+ b_lines_hunk << { "type" => "context", "line" => b_lines[bi] }
133
+ when :del
134
+ a_lines_hunk << { "type" => "deletion", "line" => a_lines[ai] }
135
+ when :ins
136
+ b_lines_hunk << { "type" => "addition", "line" => b_lines[bi] }
137
+ end
138
+ end
139
+
140
+ hunks << {
141
+ "a_start" => a_start, "b_start" => b_start,
142
+ "a_lines" => a_lines_hunk, "b_lines" => b_lines_hunk
143
+ }
144
+
145
+ i = hunk_end + 1
146
+ end
147
+
148
+ hunks
149
+ end
150
+
151
+ def find_next_change(script, from)
152
+ (from...script.size).find { |i| script[i][0] != :eq }
153
+ end
154
+
155
+ def find_hunk_end(script, from)
156
+ last_change = from
157
+ (from...[script.size, from + 6].min).each do |i|
158
+ last_change = i if script[i][0] != :eq
159
+ end
160
+ last_change
161
+ end
162
+
163
+ def stats(hunks)
164
+ additions = hunks.sum { |h| h["b_lines"].count { |l| l["type"] == "addition" } }
165
+ deletions = hunks.sum { |h| h["a_lines"].count { |l| l["type"] == "deletion" } }
166
+ { "additions" => additions, "deletions" => deletions, "hunks" => hunks.size }
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,54 @@
1
+ module Textus
2
+ module Protocol
3
+ module EntryConstraint
4
+ module_function
5
+
6
+ def validate_naming!(key, mentry)
7
+ naming = mentry.naming
8
+ return unless naming
9
+ return unless key.start_with?("#{mentry.key}.")
10
+
11
+ last = key.split(".").last
12
+
13
+ case naming
14
+ when "sequential"
15
+ return if last.match?(/\A\d{4}-[a-z][a-z0-9-]*\z/)
16
+
17
+ raise UsageError.new(
18
+ "naming violation: '#{key}' last segment must match NNNN-slug " \
19
+ "(entry '#{mentry.key}' has naming: sequential)",
20
+ )
21
+ when "dated"
22
+ return if last.match?(/\A\d{4}-\d{2}-\d{2}-[a-z][a-z0-9-]*\z/)
23
+
24
+ raise UsageError.new(
25
+ "naming violation: '#{key}' last segment must match YYYY-MM-DD-slug " \
26
+ "(entry '#{mentry.key}' has naming: dated)",
27
+ )
28
+ end
29
+ end
30
+
31
+ def validate_schema!(payload, mentry, schemas:)
32
+ schema_name = mentry.schema
33
+ return unless schema_name && schemas
34
+
35
+ schema = schemas.fetch_or_nil(schema_name)
36
+ return unless schema
37
+ return if mentry.lane == "scratchpad"
38
+
39
+ schema.validate!(payload.meta || {})
40
+ rescue Textus::SchemaViolation
41
+ raise if mentry.lane != "scratchpad"
42
+ end
43
+
44
+ def validate_format!(payload, mentry, format: nil)
45
+ fmt_name = format || mentry.format
46
+ fmt = Textus::Protocol::Format.for(fmt_name)
47
+ fmt.validate_raw_entry!(
48
+ { "_meta" => payload.meta, "content" => payload.content },
49
+ mentry.lane,
50
+ )
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,70 @@
1
+ require "dry-struct"
2
+
3
+ module Textus
4
+ module Protocol
5
+ class Envelope < Dry::Struct
6
+ attribute :protocol, Textus::Value::Types::String
7
+ attribute :key, Textus::Value::Types::String
8
+ attribute :lane, Textus::Value::Types::String
9
+ attribute :path, Textus::Value::Types::String
10
+ attribute :format, Textus::Value::Types::FormatName
11
+ attribute :etag, Textus::Value::Types::String
12
+ attribute :uid, Textus::Value::Types::String.optional
13
+ attribute :sources, Textus::Value::Types::Array.of(Textus::Value::Types::Any).optional
14
+ attribute :schema_ref, Textus::Value::Types::String.optional
15
+ attribute :meta, Textus::Value::Types::Hash.default({}.freeze)
16
+ attribute :body, Textus::Value::Types::String.optional
17
+ attribute :content, Textus::Value::Types::Any.optional
18
+ attribute :freshness, Textus::Value::Types::Any.optional
19
+
20
+ def self.build(key:, mentry:, path:, meta:, body:, etag:, content: nil, freshness: nil)
21
+ new(
22
+ protocol: Textus::PROTOCOL,
23
+ key:,
24
+ lane: mentry.lane,
25
+ path:,
26
+ format: mentry.format,
27
+ uid: extract_uid(meta),
28
+ sources: extract_sources(meta),
29
+ etag:,
30
+ schema_ref: mentry.schema,
31
+ meta: meta || {},
32
+ body:,
33
+ content:,
34
+ freshness:,
35
+ )
36
+ end
37
+
38
+ def with(**attrs) = self.class.new(to_h.merge(attrs))
39
+
40
+ def to_h_for_wire
41
+ h = {
42
+ "protocol" => protocol, "key" => key, "lane" => lane,
43
+ "path" => path, "format" => format, "_meta" => meta,
44
+ "body" => body, "etag" => etag, "schema_ref" => schema_ref, "uid" => uid
45
+ }
46
+ h["sources"] = sources if sources
47
+ h["content"] = content unless content.nil?
48
+ freshness&.to_h_for_wire&.each { |k, v| h[k] = v }
49
+ h
50
+ end
51
+
52
+ def stale? = freshness.is_a?(Hash) && freshness["stale"] == true
53
+
54
+ def fetching? = freshness.is_a?(Hash) && freshness["fetching"] == true
55
+
56
+ def self.extract_uid(meta)
57
+ v = meta.is_a?(Hash) ? meta["uid"] : nil
58
+ v.is_a?(String) ? v : nil
59
+ end
60
+
61
+ def self.extract_sources(meta)
62
+ v = meta.is_a?(Hash) ? meta["sources"] : nil
63
+ return nil unless v.is_a?(Array) && !v.empty?
64
+
65
+ valid = v.select { |s| s.is_a?(String) || (s.is_a?(Hash) && s["key"].is_a?(String)) }
66
+ valid.empty? ? nil : valid
67
+ end
68
+ end
69
+ end
70
+ end