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,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
3
- class Geometry
4
- RUN = ".state"
2
+ module Protocol
3
+ class Layout
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,21 +24,32 @@ 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 sentinels_root = File.join(run_root, "tracking", "sentinels")
36
- def store_db_path = File.join(run_root, "store.db")
37
-
38
- # -- asset paths --
39
- def asset_path(kind, date_str, zone, filename)
40
- File.join(@root, ASSETS, kind, date_str, zone.to_s, filename)
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
+
36
+ def asset_raw_dir(date_path, zone)
37
+ File.join(@root, ASSETS, "raw", date_path, zone.to_s)
38
+ end
39
+
40
+ def asset_sentinel_path
41
+ File.join(@root, ASSETS, ".gitignore")
42
+ end
43
+
44
+ def asset_resolve(rel_path)
45
+ File.join(@root, ASSETS, rel_path)
46
+ end
47
+
48
+ def archive_path(source_path)
49
+ rel = source_path.delete_prefix("#{@root}/")
50
+ File.join(@root, "archive", rel)
41
51
  end
42
52
 
43
- # -- config paths --
44
53
  def manifest_path = File.join(@root, "manifest.yaml")
45
54
  def schemas_dir = File.join(@root, "schemas")
46
55
  def schema_path(name) = File.join(schemas_dir, "#{name}.yaml")
@@ -49,10 +58,8 @@ module Textus
49
58
  def hooks_dir = File.join(@root, "hooks")
50
59
  def schemas_glob = File.join(schemas_dir, "**", "*")
51
60
 
52
- # -- gitignore --
53
61
  def gitignore_body(untracked_entries: [])
54
- lines = ["# textus runtime artifacts — safe to delete, never commit",
55
- "#{RUN}/"]
62
+ lines = ["# textus runtime artifacts — safe to delete, never commit", *runtime_ignore_dirs]
56
63
  unless untracked_entries.empty?
57
64
  lines << "# tracked:false entries — protocol-readable, not committed"
58
65
  lines.concat(untracked_entries)
@@ -60,7 +67,6 @@ module Textus
60
67
  "#{lines.join("\n")}\n"
61
68
  end
62
69
 
63
- # -- lane boundary (replaces Writer#zone_floor) --
64
70
  def lane_floor(path)
65
71
  prefix = "#{data_root}/"
66
72
  return nil unless path.start_with?(prefix)
@@ -71,6 +77,10 @@ module Textus
71
77
 
72
78
  private
73
79
 
80
+ def runtime_ignore_dirs
81
+ ["db/", "log/", "run/", "track/", ".state/"]
82
+ end
83
+
74
84
  def normalize_relative_path(path)
75
85
  return path if path.start_with?("data/")
76
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
@@ -0,0 +1,10 @@
1
+ module Textus
2
+ module Protocol
3
+ # Namespace only — Resolver and UriRewriter are in links/.
4
+ # The real edge store is Textus::Infra::Store::LinkStore.
5
+ # rubocop:disable Lint/EmptyClass
6
+ class Links
7
+ end
8
+ # rubocop:enable Lint/EmptyClass
9
+ end
10
+ end
@@ -0,0 +1,90 @@
1
+ require_relative "schema"
2
+
3
+ module Textus
4
+ module Protocol
5
+ class Manifest
6
+ class Data
7
+ AUDIT_DEFAULTS = { max_size: 10_485_760, keep: 1000 }.freeze
8
+ WORKER_DEFAULTS = { pool: 4, poll: 5, lease_ttl: 60, max_attempts: 3 }.freeze
9
+
10
+ attr_reader :raw, :root, :entries, :declared_lane_kinds,
11
+ :lane_descs,
12
+ :audit_config, :worker_config, :role_caps
13
+
14
+ def self.validate_key!(key)
15
+ raise UsageError.new("empty key") if key.nil? || key.empty?
16
+
17
+ Protocol::Key::Grammar.validate!(key)
18
+ end
19
+
20
+ def validate_key!(key) = self.class.validate_key!(key)
21
+
22
+ def self.parse(raw, root:)
23
+ %w[roles lanes owner].each do |stale_key|
24
+ next unless raw.key?(stale_key)
25
+
26
+ raise BadManifest.new(
27
+ "unknown top-level key '#{stale_key}' — protocol textus/4 fixes roles/lanes in " \
28
+ "code; remove this section from manifest.yaml (see CHANGELOG)",
29
+ )
30
+ end
31
+ raise BadManifest.new("manifest must declare data:") if Array(raw["data"]).empty? && raw["data"] != {}
32
+
33
+ Schema.validate!(raw)
34
+ new(raw: raw, root: root)
35
+ end
36
+
37
+ def initialize(raw:, root:)
38
+ @raw = raw
39
+ @root = root
40
+ @declared_lane_kinds = Textus::Protocol::V4::LANES.keys.to_h { |name| [name, name] }
41
+ @lane_descs = Textus::Protocol::V4::LANES.transform_values { |row| row[:desc] }
42
+ @audit_config = build_audit_config(raw)
43
+ @worker_config = build_worker_config(raw)
44
+ @role_caps = {}
45
+ @entries = build_entries(raw)
46
+ validate_declared_keys!
47
+ freeze
48
+ end
49
+
50
+ private
51
+
52
+ def build_audit_config(raw)
53
+ a = raw["audit"] || {}
54
+ {
55
+ max_size: a["max_size"] || AUDIT_DEFAULTS[:max_size],
56
+ keep: a["keep"] || AUDIT_DEFAULTS[:keep],
57
+ }.freeze
58
+ end
59
+
60
+ def build_worker_config(raw)
61
+ w = raw["worker"] || {}
62
+ {
63
+ pool: w["pool"] || WORKER_DEFAULTS[:pool],
64
+ poll: w["poll"] || WORKER_DEFAULTS[:poll],
65
+ lease_ttl: w["lease_ttl"] || WORKER_DEFAULTS[:lease_ttl],
66
+ max_attempts: w["max_attempts"] || WORKER_DEFAULTS[:max_attempts],
67
+ }.freeze
68
+ end
69
+
70
+ def build_entries(raw)
71
+ (raw["data"] || {}).flat_map do |lane_name, lane_entries|
72
+ unless Textus::Protocol::V4::LANES.key?(lane_name)
73
+ raise BadManifest.new("unknown lane '#{lane_name}' (known: #{Textus::Protocol::V4::LANES.keys.join(", ")})")
74
+ end
75
+
76
+ Array(lane_entries).map { |e| Manifest::Entry::Parser.call(e.merge("lane" => lane_name), root: @root) }
77
+ end.freeze
78
+ end
79
+
80
+ def validate_declared_keys!
81
+ @entries.each do |e|
82
+ raise UsageError.new("empty key") if e.key.nil? || e.key.empty?
83
+
84
+ Protocol::Key::Grammar.validate!(e.key)
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,95 @@
1
+ module Textus
2
+ module Protocol
3
+ class Manifest
4
+ class Entry
5
+ class Base < Entry
6
+ attr_reader :raw, :key, :path, :lane, :format, :publish_targets, :schema, :naming
7
+
8
+ # rubocop:disable Lint/MissingSuper
9
+ def initialize(raw:, key:, path:, lane:, format:, schema: nil, naming: nil, publish_targets: [])
10
+ @raw = raw
11
+ @key = key
12
+ @path = path
13
+ @lane = lane
14
+ @schema = schema
15
+ @naming = naming
16
+ @format = format
17
+ @publish_targets = Array(publish_targets)
18
+ end
19
+ # rubocop:enable Lint/MissingSuper
20
+
21
+ def lane_writers(policy)
22
+ policy.writers_for(@lane)
23
+ rescue UsageError => e
24
+ raise UsageError.new("entry '#{@key}': #{e.message}")
25
+ end
26
+
27
+ def in_proposal_lane?(policy) = policy.queue_lane?(@lane)
28
+
29
+ def derived? = false
30
+ def nested? = false
31
+ def leaf? = false
32
+
33
+ # Production traits. Default false on all non-produced entries.
34
+ # Lets publish modes call these without a `respond_to?` guard.
35
+ def external? = false
36
+
37
+ # Whether git should track this entry's file. Default true; an entry
38
+ # marked `tracked: false` in the manifest stays protocol-readable but is
39
+ # listed in the generated `.gitignore` (ADR 0043). Cross-cutting, so it
40
+ # reads from raw here rather than threading through every constructor.
41
+ def tracked? = @raw["tracked"] != false
42
+
43
+ # Single source of truth is @publish_targets (ADR 0094). These
44
+ # derive the ADR-0049/0052 views the publish modes consume.
45
+ def publish_to = @publish_targets.select(&:to_target?).map(&:to)
46
+ def publish_tree = @publish_targets.find(&:tree_target?)&.tree
47
+
48
+ def events = {}
49
+
50
+ # Minimal context object passed into entry `publish_via` hooks.
51
+ # Everything beyond the three primitives is derived.
52
+ PublishContext = ::Data.define(:container, :reader) do
53
+ def manifest = container.manifest
54
+ def root = container.root
55
+ def repo_root = File.dirname(container.root)
56
+ def store_root = container.root
57
+
58
+ # No-op: event bus removed in workflow redesign; callers that fire
59
+ # :entry_published / :entry_produced remain unchanged in the source.
60
+ def emit(_event, **_payload) = nil
61
+
62
+ # Read a named template from the store's templates/ directory.
63
+ # Raises TemplateError when the file doesn't exist.
64
+ def read_template(name)
65
+ path = container.layout.template_path(name)
66
+ unless File.exist?(path)
67
+ raise Textus::TemplateError.new(
68
+ "template '#{name}' not found",
69
+ template_name: name,
70
+ )
71
+ end
72
+ File.read(path)
73
+ end
74
+ end
75
+
76
+ # ADR 0049: an entry resolves, once, to one Publish::* mode that owns its
77
+ # publish algorithm. A plain entry publishes via ToPaths (publish_to) or
78
+ # None; Nested resolves among the key/path-driven modes. Derived
79
+ # overrides publish_via to materialize first.
80
+ def publish_mode
81
+ @publish_mode ||= Publish.resolve(self)
82
+ end
83
+
84
+ def publish_via(pctx, prefix: nil)
85
+ publish_mode.publish(pctx, prefix: prefix)
86
+ end
87
+
88
+ def with_publish_targets(pts)
89
+ dup.tap { |e| e.instance_variable_set(:@publish_targets, pts) }
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,19 @@
1
+ module Textus
2
+ module Protocol
3
+ class Manifest
4
+ class Entry
5
+ class Leaf < Base
6
+ KIND = :leaf
7
+
8
+ def leaf? = true
9
+
10
+ def self.from_raw(common, _raw)
11
+ new(**common)
12
+ end
13
+
14
+ Entry::REGISTRY[KIND] = self
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ module Textus
2
+ module Protocol
3
+ class Manifest
4
+ class Entry
5
+ class Nested < Base
6
+ def nested? = true
7
+
8
+ KIND = :nested
9
+
10
+ def self.from_raw(common, _raw)
11
+ new(**common)
12
+ end
13
+
14
+ Entry::REGISTRY[KIND] = self
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,71 @@
1
+ module Textus
2
+ module Protocol
3
+ class Manifest
4
+ class Entry
5
+ module Parser
6
+ def self.call(raw, root: nil)
7
+ key = raw["key"] or raise UsageError.new("manifest entry missing key")
8
+ lane = raw["lane"] or raise UsageError.new("manifest entry '#{key}' missing lane")
9
+
10
+ naming = raw["naming"]
11
+ if naming && !Textus::Protocol::V4::NAMING_PATTERNS.key?(naming)
12
+ known_aliases = Textus::Protocol::V4::NAMING_PATTERNS.keys.join(", ")
13
+ raise BadManifest.new(
14
+ "entry '#{key}': unknown naming alias '#{naming}' (known: #{known_aliases})",
15
+ )
16
+ end
17
+
18
+ kind = infer_kind(key, raw, root:)
19
+ format = resolve_format(key, kind, root, raw["format"])
20
+ path = derive_path(key, kind, format)
21
+
22
+ common = {
23
+ raw: raw,
24
+ key: key, path: path, lane: lane,
25
+ format: format, naming: naming,
26
+ schema: raw["schema"]
27
+ }
28
+
29
+ klass = Entry::REGISTRY[kind]
30
+ raise BadManifest.new("entry '#{key}': unknown inferred kind: #{kind.inspect}") unless klass
31
+
32
+ klass.from_raw(common, raw)
33
+ end
34
+
35
+ def self.infer_kind(key, _raw, root:)
36
+ base = key.split(".").join("/")
37
+ dir = root ? File.join(root, "data", base) : nil
38
+
39
+ return :nested if dir && File.directory?(dir)
40
+ return :leaf if dir && Dir.glob("#{dir}.*").any? { |p| File.file?(p) }
41
+
42
+ :leaf
43
+ end
44
+ private_class_method :infer_kind
45
+
46
+ def self.resolve_format(key, kind, root, declared)
47
+ return declared || "markdown" if kind == :nested
48
+
49
+ dir = root ? File.join(root, "data", key.split(".").join("/")) : nil
50
+ if dir
51
+ existing = Dir.glob("#{dir}.*").find { |p| File.file?(p) }
52
+ return Textus::Protocol::Format.infer_from_extension(File.extname(existing)) if existing
53
+ end
54
+
55
+ declared || "markdown"
56
+ end
57
+
58
+ def self.derive_path(key, kind_sym, format)
59
+ dir_path = key.split(".").join("/")
60
+ return dir_path if kind_sym == :nested
61
+
62
+ ext = Textus::Protocol::Format.for(format).extensions.first
63
+ "#{dir_path}#{ext}"
64
+ end
65
+
66
+ private_class_method :derive_path
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end