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,41 @@
1
+ module Textus
2
+ module Protocol
3
+ module Handlers
4
+ class QueryContext
5
+ def initialize(store_engine, container, call)
6
+ @store_engine = store_engine
7
+ @container = container
8
+ @call = call
9
+ end
10
+
11
+ attr_reader :call, :store_engine, :container
12
+
13
+ # StoreEngine delegation (read side)
14
+ def read(key:) = @store_engine.read(key:)
15
+ def resolve(key:) = @store_engine.resolve(key:)
16
+ def list(...) = @store_engine.list(...)
17
+ def exists?(key:) = @store_engine.exists?(key:)
18
+ def mentry_for(key:) = @store_engine.mentry_for(key:)
19
+
20
+ # Narrow file queries (avoid raw file_system access)
21
+ def file_exists?(key)
22
+ path = @store_engine.resolve(key:).path
23
+ @store_engine.file_system.exists?(path)
24
+ end
25
+
26
+ def etag_for(key)
27
+ path = @store_engine.resolve(key:).path
28
+ @store_engine.file_system.etag(path) if @store_engine.file_system.exists?(path)
29
+ end
30
+
31
+ # Container metadata (read-only)
32
+ def manifest = @container.manifest
33
+ def schemas = @container.schemas
34
+ def layout = @container.layout
35
+ def link_store = @container.store.link_store
36
+ def entry_index = @container.store.entry_index
37
+ def cursor_store = @container.store.cursor_store
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,67 @@
1
+ module Textus
2
+ module Protocol
3
+ module Handlers
4
+ module Read
5
+ include Concern
6
+
7
+ module_function
8
+
9
+ def get(key:, ctx:, call:)
10
+ env = ctx.read(key:)
11
+ unless env
12
+ suggestions = ctx.manifest.resolver.suggestions_for(key)
13
+ raise Textus::UnknownKey.new(key, suggestions:)
14
+ end
15
+
16
+ Ops::SourceExpander.new.call(env, ctx:, depth: 0).to_h_for_wire
17
+ end
18
+
19
+ def list(ctx:, call:, prefix: nil, lane: nil, q: nil, schema: nil)
20
+ ctx.list(prefix:, lane:, q:, schema:)
21
+ end
22
+
23
+ def where(key:, ctx:, call:)
24
+ res = ctx.resolve(key:)
25
+ response(key:, lane: res.entry.lane, path: res.path)
26
+ end
27
+
28
+ def uid(key:, ctx:, call:)
29
+ env = ctx.read(key:)
30
+ raise Textus::UnknownKey.new(key) unless env
31
+
32
+ env.uid
33
+ end
34
+
35
+ def deps(key:, ctx:, call:)
36
+ entry = ctx.manifest.data.entries.find { |e| e.key == key }
37
+ deps_list = entry&.external? ? Array(entry.source&.sources).compact : []
38
+ { "key" => key, "deps" => deps_list.uniq }
39
+ end
40
+
41
+ def rdeps(key:, ctx:, call:)
42
+ source_rdeps = ctx.manifest.data.entries.each_with_object([]) do |entry, acc|
43
+ next unless entry.external?
44
+
45
+ sources = Array(entry.source&.sources).compact
46
+ acc << entry.key if sources.any? { |s| s == key || key.start_with?("#{s}.") }
47
+ end
48
+ link_rdeps = ctx.link_store.links_to(key)
49
+ { "key" => key, "rdeps" => (source_rdeps + link_rdeps).uniq.sort }
50
+ end
51
+
52
+ def graph(key:, ctx:, call:, depth: nil)
53
+ from = ctx.link_store.links_from(key)
54
+ to = ctx.link_store.links_to(key)
55
+ { "key" => key, "neighbors" => (from + to).uniq.sort,
56
+ "reachable" => ctx.link_store.reachable(from: key, depth:).sort }
57
+ end
58
+
59
+ def schema_show(key:, ctx:, call:)
60
+ mentry = ctx.mentry_for(key:)
61
+ schema = ctx.schemas.fetch_or_nil(mentry.schema)
62
+ response(key:, schema_ref: mentry.schema, schema: schema&.to_h)
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,93 @@
1
+ module Textus
2
+ module Protocol
3
+ module Handlers
4
+ module System
5
+ include Concern
6
+ extend self
7
+
8
+ def boot(ctx:, call:)
9
+ proxy = Textus::Protocol::Container.new(
10
+ store: ctx.container.store,
11
+ layout: ctx.layout,
12
+ manifest: ctx.manifest, schemas: ctx.schemas,
13
+ workflows: nil,
14
+ root: ctx.layout.root
15
+ )
16
+ Textus::Protocol::Boot.build(container: proxy)
17
+ end
18
+
19
+ def schema_list(ctx:, call:)
20
+ Textus::Protocol::Boot.build_schemas(ctx.schemas)
21
+ end
22
+
23
+ def audit(ctx:, call:, key: nil, lane: nil, role: nil, verb: nil, since: nil, seq_since: nil, correlation_id: nil, limit: nil)
24
+ filters = { key:, lane:, role:, verb:, since:, seq_since:, correlation_id:, limit: }
25
+ Ops::AuditFilter.new.call(ctx:, filters:)
26
+ end
27
+
28
+ def blame(key:, ctx:, call:)
29
+ audit_result = audit(key:, ctx:, call:)
30
+ audit_rows = audit_result["rows"] || []
31
+ git_map = Ops::GitBlame.new.call(key:, ctx:)
32
+ audit_rows.map { |row| row.merge("git" => git_map ? git_map[row["ts"]] : nil) }
33
+ end
34
+
35
+ def pulse(ctx:, call:, since: nil)
36
+ Ops::PulseCoordinator.new.call(role: call.role, ctx:, since:)
37
+ end
38
+
39
+ def workflow_spec(name:, ctx:, call:)
40
+ registry = ctx.workflow_registry || Textus::Workflow::Registry.new(ctx.workflows)
41
+ wf = registry.for(name)
42
+ {
43
+ "name" => wf.name, "type" => wf.workflow_type,
44
+ "match_patterns" => wf.on_patterns || [],
45
+ "handles_events" => wf.handled_events,
46
+ "steps" => wf.steps.map { |s| { "name" => s.name.to_s, "type" => step_type(s) } },
47
+ "publish_targets" => wf.publish_targets.map do |t|
48
+ h = {}
49
+ h["to"] = t.to if t.to
50
+ h["template"] = t.template if t.template
51
+ h
52
+ end,
53
+ "priority" => wf.priority,
54
+ "max_attempts" => wf.max_attempts,
55
+ "ttl" => wf.ttl&.to_s,
56
+ "on_expire" => wf.on_expire
57
+ }
58
+ end
59
+
60
+ def drain(ctx:, call:)
61
+ registry = ctx.workflow_registry || Textus::Workflow::Registry.new(ctx.workflows)
62
+ reader = ->(key:) { ctx.read(key:) }
63
+
64
+ Textus::Workflow::Scheduler.new(
65
+ registry:, workflow_queue: ctx.workflow_queue, reader:,
66
+ ).seed_expired
67
+
68
+ consumer = Textus::Workflow::Consumer.new(registry:, container: ctx.container, call:)
69
+ completed = 0
70
+ failed = 0
71
+ loop do
72
+ row = ctx.workflow_queue.pop
73
+ break unless row
74
+
75
+ result = consumer.consume(row, ctx.workflow_queue)
76
+ result[:state] == "done" ? completed += 1 : failed += 1
77
+ end
78
+
79
+ response(ok: failed.zero?, completed:, failed:)
80
+ end
81
+
82
+ private
83
+
84
+ def step_type(step)
85
+ case step
86
+ when Textus::Workflow::DSL::Parallel then "parallel"
87
+ else "step"
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,57 @@
1
+ module Textus
2
+ module Protocol
3
+ module Handlers
4
+ module Write
5
+ include Concern
6
+
7
+ module_function
8
+
9
+ def put(key:, ctx:, call:, meta: nil, if_etag: nil, format: nil, data: nil)
10
+ body, content = data.is_a?(Hash) ? [nil, data] : [data, nil]
11
+ env = ctx.put(key:, meta: meta || {}, body:, content:, if_etag:, call:, format:)
12
+ response(ok: true, key:, uid: env.uid, etag: env.etag)
13
+ end
14
+
15
+ def delete(key:, ctx:, call:, if_etag: nil)
16
+ ctx.delete(key:, if_etag:, call:)
17
+ response(ok: true, key:, deleted: true)
18
+ end
19
+
20
+ def key_delete(key:, ctx:, call:, if_etag: nil)
21
+ delete(key:, ctx:, call:, if_etag:)
22
+ end
23
+
24
+ def move(old_key:, new_key:, ctx:, call:, if_etag: nil, dry_run: false)
25
+ mctx = ctx.move(from_key: old_key, to_key: new_key, call:, if_etag:, dry_run:)
26
+ base = response(ok: true, from_key: mctx.from_key, to_key: mctx.to_key,
27
+ from_path: mctx.from_path, to_path: mctx.to_path)
28
+ return base.merge("dry_run" => true) if mctx.dry_run
29
+
30
+ base.merge("uid" => mctx.envelope.uid, "envelope" => mctx.envelope.to_h_for_wire)
31
+ end
32
+
33
+ def key_mv_prefix(from_prefix:, to_prefix:, ctx:, call:, dry_run: false)
34
+ planner = Ops::BulkPlanner.new
35
+ plan = planner.plan_mv(from_prefix:, to_prefix:, ctx:)
36
+ return plan.to_h if dry_run
37
+
38
+ plan.steps.each { |s| move(old_key: s["from"], new_key: s["to"], ctx:, call:) }
39
+ plan.to_h
40
+ end
41
+
42
+ def key_delete_prefix(prefix:, ctx:, call:, dry_run: false)
43
+ planner = Ops::BulkPlanner.new
44
+ plan = planner.plan_delete(prefix:, ctx:)
45
+ return plan.to_h if dry_run
46
+
47
+ plan.steps.each { |s| delete(key: s["key"], ctx:, call:) }
48
+ plan.to_h
49
+ end
50
+
51
+ def data_mv(from:, to:, ctx:, call:, dry_run: false)
52
+ Ops::DataMv.new.call(from:, to:, ctx:, dry_run:)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,143 @@
1
+ require "fileutils"
2
+ require "pathname"
3
+ require "yaml"
4
+
5
+ module Textus
6
+ module Protocol
7
+ module Init
8
+ ZONES = %w[knowledge scratchpad proposals artifacts raw].freeze
9
+
10
+ DEFAULT_MANIFEST = <<~YAML
11
+ version: textus/4
12
+ data:
13
+ knowledge:
14
+ - key: knowledge.loop.execution
15
+ naming: sequential
16
+ schema: runbook
17
+ - key: knowledge.loop.intent
18
+ naming: sequential
19
+ - key: knowledge.loop.constraint
20
+ - key: knowledge.loop.judgment
21
+ - key: knowledge.loop.evidence
22
+ naming: sequential
23
+ - key: knowledge.loop.feedback
24
+ scratchpad:
25
+ - key: scratchpad.notes
26
+ tracked: false
27
+ - key: scratchpad.sessions
28
+ tracked: false
29
+ - key: scratchpad.scripts
30
+ tracked: false
31
+ proposals:
32
+ - key: proposals.notes
33
+ raw:
34
+ - key: raw
35
+ format: yaml
36
+ artifacts:
37
+ - key: artifacts.boot
38
+ format: json
39
+ YAML
40
+
41
+ AGENT_ENTRIES = <<~YAML
42
+ data:
43
+ artifacts:
44
+ - key: artifacts.derived.orientation
45
+ format: json
46
+ YAML
47
+
48
+ def self.run(target_root, with_agent: false)
49
+ check_target!(target_root)
50
+ scaffold_dir = File.expand_path("init/templates", __dir__)
51
+ create_directories(target_root)
52
+ write_manifest(target_root, with_agent:)
53
+ mcp_status = scaffold_agent(target_root, scaffold_dir, with_agent:)
54
+ write_gitignore(target_root)
55
+ build_result(target_root, with_agent:, mcp_status:)
56
+ end
57
+
58
+ def self.check_target!(target_root)
59
+ raise UsageError.new(".textus/ already exists at #{target_root}") if File.directory?(target_root)
60
+ end
61
+
62
+ ASSET_ZONES = %w[raw].freeze
63
+
64
+ def self.create_directories(target_root)
65
+ FileUtils.mkdir_p(File.join(target_root, "schemas"))
66
+ FileUtils.mkdir_p(File.join(target_root, "templates"))
67
+ FileUtils.mkdir_p(File.join(target_root, "workflows"))
68
+ ZONES.each do |z|
69
+ dir = File.join(target_root, "data", z)
70
+ FileUtils.mkdir_p(dir)
71
+ File.write(File.join(dir, ".gitkeep"), "")
72
+ end
73
+ ASSET_ZONES.each do |z|
74
+ dir = File.join(target_root, "assets", z)
75
+ FileUtils.mkdir_p(dir)
76
+ File.write(File.join(dir, ".gitkeep"), "")
77
+ end
78
+ end
79
+
80
+ def self.write_manifest(target_root, with_agent:)
81
+ File.write(File.join(target_root, "manifest.yaml"), manifest_yaml(with_agent: with_agent))
82
+ end
83
+
84
+ def self.scaffold_agent(target_root, scaffold_dir, with_agent:)
85
+ return nil unless with_agent
86
+
87
+ scaffold_agent_profile(target_root, scaffold_dir)
88
+ write_mcp_config(target_root, scaffold_dir)
89
+ end
90
+
91
+ def self.write_gitignore(target_root)
92
+ File.write(File.join(target_root, ".gitignore"), derived_gitignore(target_root))
93
+ end
94
+
95
+ def self.build_result(target_root, with_agent:, mcp_status:)
96
+ result = { "protocol" => PROTOCOL, "initialized" => target_root, "profile" => with_agent ? "agent" : "default" }
97
+ result["mcp_config"] = mcp_status if with_agent
98
+ result
99
+ end
100
+
101
+ def self.manifest_yaml(with_agent:)
102
+ return DEFAULT_MANIFEST unless with_agent
103
+
104
+ base = YAML.safe_load(DEFAULT_MANIFEST)
105
+ extra = YAML.safe_load(AGENT_ENTRIES)
106
+ base["data"].merge!(extra["data"]) { |_lane, existing, added| existing + added }
107
+ YAML.dump(base).sub(/\A---\n/, "")
108
+ end
109
+
110
+ def self.scaffold_agent_profile(target_root, scaffold_dir)
111
+ {
112
+ "project.schema.yaml" => File.join("schemas", "project.yaml"),
113
+ "runbook.schema.yaml" => File.join("schemas", "runbook.yaml"),
114
+ "orientation.erb" => File.join("templates", "orientation.erb"),
115
+ }.each do |src, dest|
116
+ File.write(File.join(target_root, dest), File.read(File.join(scaffold_dir, src)))
117
+ end
118
+ end
119
+
120
+ def self.write_mcp_config(target_root, scaffold_dir)
121
+ dest = File.join(File.dirname(target_root), ".mcp.json")
122
+ return "skipped" if File.exist?(dest)
123
+
124
+ File.write(dest, File.read(File.join(scaffold_dir, "mcp.json")))
125
+ "written"
126
+ end
127
+
128
+ def self.derived_gitignore(target_root)
129
+ manifest = Textus::Protocol::Manifest.load(target_root)
130
+ root = Pathname.new(target_root)
131
+ untracked = manifest.data.entries.reject(&:tracked?).map do |e|
132
+ if e.nested?
133
+ rel = e.path.start_with?("data/") ? e.path : File.join("data", e.path)
134
+ "#{rel}/"
135
+ else
136
+ Pathname.new(Textus::Protocol::Key::Path.resolve(manifest.data, e)).relative_path_from(root).to_s
137
+ end
138
+ end
139
+ Textus::Protocol::Layout.new(target_root).gitignore_body(untracked_entries: untracked)
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,35 @@
1
+ module Textus
2
+ module Protocol
3
+ module Key
4
+ module Grammar
5
+ SEGMENT = /\A[a-z0-9][a-z0-9-]*\z/
6
+ MAX_SEGMENTS = 8
7
+ MAX_SEGMENT_LEN = 64
8
+
9
+ module_function
10
+
11
+ def validate!(key)
12
+ raise UsageError.new("key must be a String") unless key.is_a?(String)
13
+ raise UsageError.new("empty key") if key.empty?
14
+
15
+ segs = key.split(".")
16
+ raise UsageError.new("key '#{key}' has #{segs.length} segments (max #{MAX_SEGMENTS})") if segs.length > MAX_SEGMENTS
17
+
18
+ segs.each do |seg|
19
+ if seg.empty?
20
+ raise UsageError.new("empty segment in key '#{key}'")
21
+ elsif seg.length > MAX_SEGMENT_LEN
22
+ raise UsageError.new("segment '#{seg}' in key '#{key}' exceeds #{MAX_SEGMENT_LEN} chars")
23
+ elsif !seg.match?(SEGMENT)
24
+ raise UsageError.new(
25
+ "invalid key segment '#{seg}' in '#{key}': must match [a-z0-9][a-z0-9-]* " \
26
+ "(lowercase, digits, hyphens; no underscores or uppercase)",
27
+ )
28
+ end
29
+ end
30
+ true
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,23 @@
1
+ module Textus
2
+ module Protocol
3
+ module Key
4
+ module Matching
5
+ module_function
6
+
7
+ def matches_prefix?(key, prefix, nested: false)
8
+ return true if key == prefix || key.start_with?("#{prefix}.")
9
+
10
+ nested && prefix.start_with?("#{key}.")
11
+ end
12
+
13
+ def match?(pattern, key)
14
+ segs = pattern.split(".")
15
+ key_segs = key.split(".")
16
+ return false if segs.length != key_segs.length
17
+
18
+ segs.zip(key_segs).all? { |p, k| p == "*" || p == k }
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ module Textus
2
+ module Protocol
3
+ module Key
4
+ module Path
5
+ def self.resolve(data, mentry)
6
+ primary_ext = Textus::Protocol::Format.for(mentry.format).extensions.first
7
+ rel_path = normalize_relative_path(mentry.path)
8
+ if File.extname(mentry.path) == ""
9
+ File.join(data.root, rel_path + primary_ext)
10
+ else
11
+ File.join(data.root, rel_path)
12
+ end
13
+ end
14
+
15
+ def self.normalize_relative_path(path)
16
+ return path if path.start_with?("data/")
17
+
18
+ File.join("data", path)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,25 @@
1
+ module Textus
2
+ module Protocol
3
+ module Key
4
+ module_function
5
+
6
+ Resolution = ::Data.define(:entry, :remaining)
7
+
8
+ def resolve(key, entries)
9
+ entry = entries.find { |e| e.key == key }
10
+ return Resolution.new(entry:, remaining: []) if entry
11
+
12
+ candidates = entries.select { |e| e.nested? && key.start_with?("#{e.key}.") }
13
+ return nil if candidates.empty?
14
+
15
+ best = candidates.max_by { |e| e.key.length }
16
+ tail = key.delete_prefix("#{best.key}.")
17
+ Resolution.new(entry: best, remaining: tail.split("."))
18
+ end
19
+
20
+ def match?(pattern, key)
21
+ Matching.match?(pattern, key)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,12 @@
1
+ module Textus
2
+ module Protocol
3
+ module Lane
4
+ def self.handler_for(lane_name)
5
+ ns = lane_name.to_s.split("_").map(&:capitalize).join
6
+ Textus::Lanes.const_get(ns)::Handlers
7
+ rescue NameError
8
+ raise ArgumentError.new("no handler for lane: #{lane_name}")
9
+ end
10
+ end
11
+ end
12
+ end
@@ -1,7 +1,6 @@
1
1
  module Textus
2
- class Store
2
+ module Protocol
3
3
  class Layout
4
- RUN = ".state"
5
4
  DATA = "data"
6
5
  ASSETS = "assets"
7
6
 
@@ -12,12 +11,11 @@ module Textus
12
11
 
13
12
  attr_reader :root
14
13
 
15
- # -- data paths --
16
14
  def data_root = File.join(@root, DATA)
17
15
  def lane_path(lane_name) = File.join(data_root, lane_name.to_s)
18
16
 
19
17
  def entry_path(mentry)
20
- primary_ext = Format.for(mentry.format).extensions.first
18
+ primary_ext = Textus::Protocol::Format.for(mentry.format).extensions.first
21
19
  rel = normalize_relative_path(mentry.path)
22
20
  if File.extname(mentry.path) == ""
23
21
  File.join(@root, rel + primary_ext)
@@ -26,19 +24,15 @@ module Textus
26
24
  end
27
25
  end
28
26
 
29
- # -- runtime paths --
30
- def run_root = File.join(@root, RUN)
31
- def cursor_path(role) = File.join(run_root, "ephemeral", "cursors", role.to_s)
32
- def lock_path(name) = File.join(run_root, "ephemeral", "locks", "#{name}.lock")
33
- def audit_dir_path = File.join(run_root, "audit")
34
- def audit_log_path = File.join(audit_dir_path, "audit.log")
35
- def audit_rotated_log_path(n) = File.join(audit_dir_path, "audit.log.#{n}")
36
- def audit_rotated_meta_path(n) = File.join(audit_dir_path, "audit.log.#{n}.meta.json")
37
- def audit_log_glob = File.join(audit_dir_path, "audit.log.*")
38
- def sentinels_root = File.join(run_root, "tracking", "sentinels")
39
- def store_db_path = File.join(run_root, "store.db")
40
-
41
- # -- asset paths --
27
+ def db_root = File.join(@root, "db")
28
+ def db_path = File.join(db_root, "store.db")
29
+ def log_root = File.join(@root, "log")
30
+ def run_root = File.join(@root, "run")
31
+ def lock_path(name) = File.join(run_root, "locks", "#{name}.lock")
32
+ def track_root = File.join(@root, "track")
33
+ def sentinels_root = File.join(track_root, "sentinels")
34
+ def cursor_path(role) = File.join(track_root, "cursors", role.to_s)
35
+
42
36
  def asset_raw_dir(date_path, zone)
43
37
  File.join(@root, ASSETS, "raw", date_path, zone.to_s)
44
38
  end
@@ -56,7 +50,6 @@ module Textus
56
50
  File.join(@root, "archive", rel)
57
51
  end
58
52
 
59
- # -- config paths --
60
53
  def manifest_path = File.join(@root, "manifest.yaml")
61
54
  def schemas_dir = File.join(@root, "schemas")
62
55
  def schema_path(name) = File.join(schemas_dir, "#{name}.yaml")
@@ -65,10 +58,8 @@ module Textus
65
58
  def hooks_dir = File.join(@root, "hooks")
66
59
  def schemas_glob = File.join(schemas_dir, "**", "*")
67
60
 
68
- # -- gitignore --
69
61
  def gitignore_body(untracked_entries: [])
70
- lines = ["# textus runtime artifacts — safe to delete, never commit",
71
- "#{RUN}/"]
62
+ lines = ["# textus runtime artifacts — safe to delete, never commit", *runtime_ignore_dirs]
72
63
  unless untracked_entries.empty?
73
64
  lines << "# tracked:false entries — protocol-readable, not committed"
74
65
  lines.concat(untracked_entries)
@@ -76,7 +67,6 @@ module Textus
76
67
  "#{lines.join("\n")}\n"
77
68
  end
78
69
 
79
- # -- lane boundary (replaces Writer#zone_floor) --
80
70
  def lane_floor(path)
81
71
  prefix = "#{data_root}/"
82
72
  return nil unless path.start_with?(prefix)
@@ -87,6 +77,10 @@ module Textus
87
77
 
88
78
  private
89
79
 
80
+ def runtime_ignore_dirs
81
+ ["db/", "log/", "run/", "track/", ".state/"]
82
+ end
83
+
90
84
  def normalize_relative_path(path)
91
85
  return path if path.start_with?("data/")
92
86
 
@@ -0,0 +1,33 @@
1
+ require "pathname"
2
+
3
+ module Textus
4
+ module Protocol
5
+ class Links
6
+ class Resolver
7
+ class UnknownKeyError < StandardError
8
+ end
9
+
10
+ def initialize(manifest:)
11
+ @manifest = manifest
12
+ end
13
+
14
+ def resolve(key:, from_path:)
15
+ entry = @manifest.data.entries.find { |e| e.key == key }
16
+ raise UnknownKeyError.new("unknown key: #{key}") unless entry
17
+
18
+ to = Array(entry.publish_to).first
19
+ return "`textus get #{key}`" if to.nil?
20
+
21
+ relative_path(from: from_path, to: to)
22
+ end
23
+
24
+ private
25
+
26
+ def relative_path(from:, to:)
27
+ from_dir = Pathname.new(from).dirname
28
+ Pathname.new(to).relative_path_from(from_dir).to_s
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,30 @@
1
+ module Textus
2
+ module Protocol
3
+ class Links
4
+ class UriRewriter
5
+ TEXTUS_URI = /\[([^\]]*)\]\(textus:([^#)\s]+)(#[^)\s]*)?\)/
6
+
7
+ def initialize(resolver:, from_path:, from_key: nil, edge_store: nil)
8
+ @resolver = resolver
9
+ @from_path = from_path
10
+ @from_key = from_key
11
+ @edge_store = edge_store
12
+ end
13
+
14
+ def rewrite(content)
15
+ content.gsub(TEXTUS_URI) do
16
+ text = Regexp.last_match(1)
17
+ key = Regexp.last_match(2)
18
+ anchor = Regexp.last_match(3).to_s
19
+
20
+ resolved = @resolver.resolve(key: key, from_path: @from_path)
21
+ @edge_store&.record(from_key: @from_key, to_key: key)
22
+ "[#{text}](#{resolved}#{anchor})"
23
+ rescue Resolver::UnknownKeyError
24
+ "[#{text}](`textus get #{key}`)"
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end