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,42 @@
1
+ module Textus
2
+ module Infra
3
+ class Store
4
+ class AuditLog < Base
5
+ def append(role:, verb:, key:, etag_before: nil, etag_after: nil, correlation_id: nil)
6
+ table(:audit_events).insert(
7
+ ts: Time.now.utc.iso8601,
8
+ role:, verb:, key:,
9
+ etag_before:, etag_after:, correlation_id:
10
+ )
11
+ end
12
+
13
+ def list(seq_since: nil, key: nil, role: nil, verb: nil, correlation_id: nil, limit: nil)
14
+ dataset = table(:audit_events)
15
+ dataset = dataset.where { seq > seq_since } if seq_since
16
+ dataset = dataset.where(key:) if key
17
+ dataset = dataset.where(role:) if role
18
+ dataset = dataset.where(verb:) if verb
19
+ dataset = dataset.where(correlation_id:) if correlation_id
20
+ dataset = dataset.limit(limit) if limit
21
+ results(dataset.order(:seq).all)
22
+ end
23
+
24
+ def latest
25
+ result(table(:audit_events).order(Sequel.desc(:seq)).first) || { seq: 0 }
26
+ end
27
+
28
+ def min_available
29
+ result(table(:audit_events).order(:seq).first)&.dig(:seq)
30
+ end
31
+
32
+ def last_writer(key)
33
+ row = table(:audit_events)
34
+ .where(key:, verb: %w[put delete key_delete])
35
+ .order(Sequel.desc(:seq))
36
+ .first
37
+ result(row)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,25 @@
1
+ module Textus
2
+ module Infra
3
+ class Store
4
+ class Base
5
+ def initialize(database)
6
+ @db = database
7
+ end
8
+
9
+ private
10
+
11
+ def table(name)
12
+ @db[name]
13
+ end
14
+
15
+ def result(row)
16
+ row&.transform_keys(&:to_sym)
17
+ end
18
+
19
+ def results(rows)
20
+ rows.map { |r| result(r) }
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,27 @@
1
+ module Textus
2
+ module Infra
3
+ class Store
4
+ class CursorStore < Base
5
+ def get(role)
6
+ result(table(:cursors).where(role:).first) || { role:, seq: 0 }
7
+ end
8
+
9
+ def set(role, seq)
10
+ table(:cursors).insert_conflict(
11
+ update: { seq:, updated_at: Time.now.utc },
12
+ ).insert(role:, seq:, updated_at: Time.now.utc)
13
+ end
14
+
15
+ def get_loop(role)
16
+ result(table(:loop_positions).where(role:).first) || { role:, index: 0 }
17
+ end
18
+
19
+ def set_loop(role, index)
20
+ table(:loop_positions).insert_conflict(
21
+ update: { index:, updated_at: Time.now.utc },
22
+ ).insert(role:, index:, updated_at: Time.now.utc)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,31 @@
1
+ module Textus
2
+ module Infra
3
+ class Store
4
+ class EntryIndex < Base
5
+ def record(key:, lane:, format:, schema:, content:)
6
+ table(:entries).insert_conflict(:replace).insert(
7
+ key:, lane:, format:, schema:, content:,
8
+ indexed_at: Time.now.utc
9
+ )
10
+ end
11
+
12
+ def remove(key:)
13
+ table(:entries).where(key:).delete
14
+ end
15
+
16
+ def search(query: nil, schema: nil, lane: nil, prefix: nil)
17
+ dataset = table(:entries).select(:key, :lane, :format, :schema, :content, :indexed_at)
18
+ if query
19
+ match_query = query.to_s.gsub("-", " ")
20
+ matching_rowids = @db[:entries_fts].where(Sequel.lit("entries_fts MATCH ?", match_query)).select_map(:rowid)
21
+ dataset = dataset.where(Sequel[:entries][:rowid] => matching_rowids)
22
+ end
23
+ dataset = dataset.where(schema:) if schema
24
+ dataset = dataset.where(lane:) if lane
25
+ dataset = dataset.where(Sequel.like(:key, "#{prefix}%")) if prefix
26
+ results(dataset.all)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,63 @@
1
+ require "securerandom"
2
+
3
+ module Textus
4
+ module Infra
5
+ class Store
6
+ module Envelope
7
+ module Meta
8
+ NO_META_FORMATS = %w[text].freeze
9
+
10
+ FIELDS = {
11
+ "uid" => {
12
+ inject: lambda { |meta, content, existing_meta, **_opts|
13
+ m = meta.is_a?(Hash) ? meta.dup : {}
14
+ existing = existing_meta.is_a?(Hash) ? existing_meta["uid"] : nil
15
+ m["uid"] = existing || Textus::Value::Uid.mint unless m["uid"].is_a?(String) && !m["uid"].empty?
16
+ [m, content]
17
+ },
18
+ },
19
+ "sources" => {
20
+ inject: lambda { |meta, content, existing_meta, etag_for: nil|
21
+ m = meta.is_a?(Hash) ? meta.dup : {}
22
+ existing = existing_meta.is_a?(Hash) ? existing_meta["sources"] : nil
23
+
24
+ if m.key?("sources")
25
+ raise Textus::BadContent.new(nil, "_meta.sources must be an array") unless m["sources"].is_a?(Array)
26
+
27
+ m["sources"] = m["sources"].map { |s| Meta.normalize_source!(s, etag_for) }
28
+ elsif existing.is_a?(Array) && !existing.empty?
29
+ m["sources"] = existing
30
+ end
31
+
32
+ [m, content]
33
+ },
34
+ },
35
+ }.freeze
36
+
37
+ def self.inject_all(meta, content, existing_meta = {}, format: nil, etag_for: nil)
38
+ return [meta, content] if NO_META_FORMATS.include?(format)
39
+
40
+ FIELDS.each_value do |field|
41
+ meta, content = field[:inject].call(meta, content, existing_meta, etag_for: etag_for)
42
+ end
43
+
44
+ [meta, content]
45
+ end
46
+
47
+ def self.normalize_source!(src, etag_for)
48
+ key = case src
49
+ when String then src
50
+ when Hash then src["key"]
51
+ end
52
+
53
+ raise Textus::BadContent.new(nil, "each source must be a string key or { key: } object") unless key.is_a?(String)
54
+ raise Textus::BadContent.new(nil, "each source key must be a non-empty string") if key.empty?
55
+
56
+ etag = etag_for&.call(key)
57
+ etag ? { "key" => key, "etag" => etag } : { "key" => key }
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,23 @@
1
+ module Textus
2
+ module Infra
3
+ class Store
4
+ class FileStore
5
+ def initialize(file_system)
6
+ @fs = file_system
7
+ end
8
+
9
+ def read(path) = @fs.read(path)
10
+
11
+ def write(path, bytes)
12
+ @fs.mkdir_p(File.dirname(path))
13
+ @fs.write(path, bytes)
14
+ end
15
+
16
+ def delete(path) = @fs.delete(path)
17
+ def move(from, to) = @fs.mv(from, to)
18
+ def exists?(path) = @fs.exists?(path)
19
+ def etag(path) = Textus::Value::Etag.for_file(path)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,28 @@
1
+ module Textus
2
+ module Infra
3
+ class Store
4
+ module Index
5
+ class Builder
6
+ def initialize(db)
7
+ @db = db
8
+ end
9
+
10
+ def rebuild!(resolver:)
11
+ @db[:entries].each do |row|
12
+ key = row[:key]
13
+ mentry = resolver.resolve(key)
14
+ next unless mentry
15
+
16
+ entry = mentry.entry
17
+ @db[:entries].insert_conflict(:replace).insert(
18
+ key:, lane: entry.lane, format: entry.format,
19
+ schema: entry.schema, content: row[:content],
20
+ indexed_at: Time.now.utc
21
+ )
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,23 @@
1
+ module Textus
2
+ module Infra
3
+ class Store
4
+ module Index
5
+ class Lookup
6
+ def initialize(db)
7
+ @db = db
8
+ end
9
+
10
+ def find_by_hash(hash)
11
+ row = @db[:entries].where(Sequel.lit("json_extract(content, '$.content_hash') = ?", hash)).first
12
+ row&.dig(:key)
13
+ end
14
+
15
+ def find_by_url(url)
16
+ row = @db[:entries].where(Sequel.lit("json_extract(content, '$.source.url') = ?", url)).first
17
+ row&.dig(:key)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,43 @@
1
+ module Textus
2
+ module Infra
3
+ class Store
4
+ class LinkStore
5
+ def initialize(database)
6
+ @db = database
7
+ end
8
+
9
+ def add_edge(from_key:, to_key:)
10
+ @db[:link_edges].insert_ignore.insert(from_key:, to_key:)
11
+ end
12
+ alias record add_edge
13
+
14
+ def remove_edge(from_key:, to_key:)
15
+ @db[:link_edges].where(from_key:, to_key:).delete
16
+ end
17
+
18
+ def links_from(key)
19
+ @db[:link_edges].where(from_key: key).select_map(:to_key)
20
+ end
21
+
22
+ def links_to(key)
23
+ @db[:link_edges].where(to_key: key).select_map(:from_key)
24
+ end
25
+
26
+ def reachable(from:, depth: nil)
27
+ visited = Set.new
28
+ queue = [[from, 0]]
29
+ while queue.any?
30
+ current, d = queue.shift
31
+ next if visited.include?(current)
32
+ next if depth && d > depth
33
+
34
+ visited.add(current)
35
+ links_from(current).each { |to| queue << [to, d + 1] }
36
+ end
37
+ visited.delete(from)
38
+ visited.to_a
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,54 @@
1
+ module Textus
2
+ module Infra
3
+ class Store
4
+ class WorkflowQueue < Base
5
+ def push(event_type:, workflow_type:, key:, lane:, payload: nil,
6
+ created_at: Time.now.utc, scheduled_at: nil,
7
+ state: "pending", retry_count: 0, max_attempts: 3)
8
+ table(:workflow_events).insert(
9
+ event_type: event_type.to_s, workflow_type:, key:, lane:,
10
+ payload: payload&.to_json, created_at:, scheduled_at:,
11
+ state:, retry_count:, max_attempts:
12
+ )
13
+ end
14
+
15
+ def pop
16
+ row = table(:workflow_events)
17
+ .where(state: "pending")
18
+ .where(Sequel.|({ scheduled_at: nil }, Sequel.lit("scheduled_at <= datetime('now')")))
19
+ .order(:created_at, :id)
20
+ .limit(1)
21
+ .for_update
22
+ .first
23
+ result(row)
24
+ end
25
+
26
+ def pending?(event_type:, key:)
27
+ table(:workflow_events)
28
+ .where(event_type: event_type.to_s, key: key)
29
+ .where(state: %w[pending processing])
30
+ .any?
31
+ end
32
+
33
+ def mark_processing(event_id)
34
+ table(:workflow_events).where(id: event_id).update(state: "processing")
35
+ end
36
+
37
+ def complete(event_id)
38
+ table(:workflow_events).where(id: event_id).update(state: "done")
39
+ end
40
+
41
+ def fail(event_id, retry_count:, error:)
42
+ table(:workflow_events).where(id: event_id).update(state: "failed", retry_count:, error:)
43
+ end
44
+
45
+ def reschedule(event_id, retry_count:, delay:, error:)
46
+ table(:workflow_events).where(id: event_id).update(
47
+ state: "pending", retry_count:, error:,
48
+ scheduled_at: Sequel.lit("datetime('now', '+#{delay.to_i} seconds')")
49
+ )
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,29 @@
1
+ module Textus
2
+ module Infra
3
+ class Store
4
+ def initialize(database:, file_system:)
5
+ @db = database.connection
6
+ @file_system = file_system
7
+ end
8
+
9
+ attr_reader :file_system, :db
10
+
11
+ def cursor_store = @cursor_store ||= CursorStore.new(@db)
12
+ def link_store = @link_store ||= LinkStore.new(@db)
13
+ def entry_index = @entry_index ||= EntryIndex.new(@db)
14
+ def audit_log = @audit_log ||= AuditLog.new(@db)
15
+ def workflow_queue = @workflow_queue ||= WorkflowQueue.new(@db)
16
+ def file_store = @file_store ||= FileStore.new(@file_system)
17
+
18
+ def transaction(&) = @db.transaction(&)
19
+ end
20
+ end
21
+ end
22
+
23
+ require_relative "store/base"
24
+ require_relative "store/cursor_store"
25
+ require_relative "store/audit_log"
26
+ require_relative "store/workflow_queue"
27
+ require_relative "store/entry_index"
28
+ require_relative "store/file_store"
29
+ require_relative "store/link_store"
@@ -0,0 +1,8 @@
1
+ module Textus
2
+ module Ingest
3
+ end
4
+ end
5
+
6
+ require_relative "lanes/ingest/resolver"
7
+ require_relative "lanes/ingest/entry_types"
8
+ require_relative "lanes/ingest/handlers"
@@ -0,0 +1,35 @@
1
+ module Textus
2
+ module Lanes
3
+ module Artifact
4
+ class Cache
5
+ def initialize(store, ttl_default: 3600 * 24 * 30)
6
+ @store = store
7
+ @ttl_default = ttl_default
8
+ end
9
+
10
+ def ttl(key)
11
+ entry = @store.get(key:)
12
+ return @ttl_default unless entry
13
+
14
+ entry["ttl"] || @ttl_default
15
+ end
16
+
17
+ def stale?(key)
18
+ entry = @store.get(key:)
19
+ return true unless entry
20
+ return true if entry["stale"] || entry["fetching"]
21
+
22
+ generated_at = entry["generated_at"]
23
+ return true unless generated_at
24
+
25
+ age = Time.now - Time.parse(generated_at)
26
+ age > ttl(key)
27
+ end
28
+
29
+ def expire(key)
30
+ @store.put(key:, meta: { "stale" => true })
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,29 @@
1
+ module Textus
2
+ module Lanes
3
+ module Artifact
4
+ module Handlers
5
+ module_function
6
+
7
+ def get(key:, store:, ctx:, call:)
8
+ store.get(key:)
9
+ end
10
+
11
+ def list(store:, ctx:, call:, prefix: nil)
12
+ store.list(prefix: prefix || "artifact")
13
+ end
14
+
15
+ def status(store:, ctx:, call:)
16
+ store.list(prefix: "artifact").map do |row|
17
+ cache = Cache.new(store)
18
+ { key: row[:key], stale: cache.stale?(row[:key]) }
19
+ end
20
+ end
21
+
22
+ def refresh(key:, store:, ctx:, call:)
23
+ cache = Cache.new(store)
24
+ cache.expire(key)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,23 @@
1
+ module Textus
2
+ module Lanes
3
+ module Artifact
4
+ class Lifecycle
5
+ def initialize(cache)
6
+ @cache = cache
7
+ end
8
+
9
+ def on_stale(key)
10
+ @cache.expire(key)
11
+ end
12
+
13
+ def on_expire(key)
14
+ @cache.expire(key)
15
+ end
16
+
17
+ def retention(key)
18
+ @cache.ttl(key)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,18 @@
1
+ module Textus
2
+ module Lanes
3
+ module Artifact
4
+ module Verbs
5
+ Textus::Protocol::VerbRegistry.register(
6
+ Textus::Protocol::VerbRegistry::VerbSpec.new(
7
+ :workflow_spec, "Return the full definition of a named workflow — match, steps, publish targets.",
8
+ [Textus::Protocol::VerbRegistry::ArgSpec.arg(
9
+ name: :name, required: true, positional: true,
10
+ description: "workflow type name (e.g. 'boot', 'decisions-log', 'materialize')"
11
+ )],
12
+ %i[cli mcp], :read
13
+ ),
14
+ )
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,17 @@
1
+ module Textus
2
+ module Lanes
3
+ module Ingest
4
+ module Dedup
5
+ module_function
6
+
7
+ def find(lookup, content_hash, url: nil)
8
+ dup = lookup.find_by_hash(content_hash)
9
+ return dup if dup
10
+ return nil unless url
11
+
12
+ lookup.find_by_url(url)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,45 @@
1
+ module Textus
2
+ module Lanes
3
+ module Ingest
4
+ module EntryTypes
5
+ module Asset
6
+ module_function
7
+
8
+ def build(path:, lane:, time:, layout:, label: nil, **)
9
+ asset_rel = copy(path, lane:, time:, layout:)
10
+ { "source" => { "kind" => "asset", "label" => label || File.basename(path) },
11
+ "asset" => asset_rel, "ingested_at" => time.utc.iso8601, "body" => nil }
12
+ end
13
+
14
+ def copy(path, lane:, time:, layout:)
15
+ date_path = time.strftime("%Y/%m/%d")
16
+ filename = File.basename(path)
17
+ assets_dir = layout.asset_raw_dir(date_path, lane)
18
+ FileUtils.mkdir_p(assets_dir)
19
+ FileUtils.cp(path, File.join(assets_dir, filename))
20
+ sentinel = layout.asset_sentinel_path
21
+ File.write(sentinel, "*\n") unless File.exist?(sentinel)
22
+ "raw/#{date_path}/#{lane}/#{filename}"
23
+ end
24
+
25
+ def move(old_rel, lane, layout)
26
+ old_path = layout.asset_resolve(old_rel)
27
+ return unless File.exist?(old_path)
28
+
29
+ now = Time.now.utc
30
+ date_path = now.strftime("%Y/%m/%d")
31
+ filename = File.basename(old_path)
32
+ new_dir = layout.asset_raw_dir(date_path, lane)
33
+ new_path = File.join(new_dir, filename)
34
+ return if old_path == new_path
35
+
36
+ FileUtils.mkdir_p(new_dir)
37
+ FileUtils.mv(old_path, new_path)
38
+ rescue Errno::ENOENT, Errno::EACCES => e
39
+ warn "[textus ingest] could not move asset #{old_rel}: #{e.message}"
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,16 @@
1
+ module Textus
2
+ module Lanes
3
+ module Ingest
4
+ module EntryTypes
5
+ module Link
6
+ module_function
7
+
8
+ def build(url:, time:, label: nil, **)
9
+ { "source" => { "kind" => "url", "url" => url, "label" => label || url },
10
+ "ingested_at" => time.utc.iso8601, "body" => nil }
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,17 @@
1
+ module Textus
2
+ module Lanes
3
+ module Ingest
4
+ module EntryTypes
5
+ module Text
6
+ module_function
7
+
8
+ def build(path:, time:, label: nil, **)
9
+ { "source" => { "kind" => "file", "path" => path,
10
+ "label" => label || File.basename(path) },
11
+ "ingested_at" => time.utc.iso8601, "body" => File.read(path) }
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,25 @@
1
+ module Textus
2
+ module Lanes
3
+ module Ingest
4
+ module EntryTypes
5
+ require_relative "entry_types/link"
6
+ require_relative "entry_types/asset"
7
+ require_relative "entry_types/text"
8
+
9
+ module_function
10
+
11
+ def for(kind)
12
+ registry[kind] or raise(ArgumentError.new("unknown ingest kind: #{kind}"))
13
+ end
14
+
15
+ def registry
16
+ @registry ||= {
17
+ "link" => Link,
18
+ "asset" => Asset,
19
+ "text" => Text,
20
+ }.freeze
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end