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
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Action
5
- class Boot < Base
6
- verb :boot
7
- summary "Return the orientation contract: lanes, agent_quickstart, agent_protocol, and pre-computed artifacts."
8
- surfaces :cli, :mcp
9
-
10
- def self.call(container:, **)
11
- Success(Textus::Boot.build(container: container))
12
- end
13
- end
14
- end
15
- end
@@ -1,58 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "yaml"
4
-
5
- module Textus
6
- module Action
7
- class DataMv < Base
8
- verb :data_mv
9
- summary "Rename a data lane — manifest + files. Refuses if destination exists."
10
- surfaces :cli, :mcp
11
- cli "data mv"
12
- arg :from, String, required: true, positional: true, description: "current data lane name"
13
- arg :to, String, required: true, positional: true,
14
- description: "new data lane name; refused if a lane by this name already exists"
15
- arg :dry_run, :boolean, default: false,
16
- description: "when true, returns the planned zone move without applying it; " \
17
- "defaults to false, so omitting it applies the move immediately"
18
- view { |v, _i| v.to_h }
19
-
20
- def self.call(container:, call:, from:, to:, dry_run: false, **) # rubocop:disable Lint/UnusedMethodArgument
21
- manifest = container.manifest
22
- geom = container.geometry
23
-
24
- return Failure(code: :usage_error, message: "from and to required") if from.nil? || to.nil? || from.empty? || to.empty?
25
- return Failure(code: :usage_error, message: "data lane '#{from}' not declared") unless manifest.data.declared_lane_kinds.key?(from)
26
-
27
- dest_dir = geom.lane_path(to)
28
- return Failure(code: :usage_error, message: "destination 'data/#{to}' already exists") if File.exist?(dest_dir)
29
-
30
- affected_keys = manifest.data.entries.select { |entry| entry.lane == from }.map(&:key)
31
-
32
- steps = [{ "op" => "rename_zone", "from" => from, "to" => to }]
33
- steps += affected_keys.map do |key|
34
- { "op" => "mv", "from" => key, "to" => "#{to}#{key[from.length..]}" }
35
- end
36
-
37
- plan = Textus::Store::Jobs::Plan.new(steps: steps, warnings: [])
38
- return Success(plan) if dry_run
39
-
40
- rewrite_manifest!(geom, from:, to:)
41
- FileUtils.mv(geom.lane_path(from), dest_dir)
42
- Success(plan)
43
- end
44
-
45
- def self.rewrite_manifest!(geom, from:, to:)
46
- path = geom.manifest_path
47
- raw = YAML.safe_load_file(path, permitted_classes: [Symbol], aliases: false)
48
- raw["lanes"].each { |lane| lane["name"] = to if lane["name"] == from }
49
- raw["entries"].each do |entry|
50
- entry["lane"] = to if entry["lane"] == from
51
- entry["key"] = entry["key"].sub(/\A#{Regexp.escape(from)}(\.|\z)/, "#{to}\\1")
52
- entry["path"] = entry["path"].sub(%r{\A(data/)?#{Regexp.escape(from)}(/|\z)}, "\\1#{to}\\2")
53
- end
54
- File.write(path, YAML.dump(raw))
55
- end
56
- end
57
- end
58
- end
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Action
5
- class Deps < Base
6
- verb :deps
7
- summary "List the keys a derived entry depends on (its projection/external sources)."
8
- surfaces :cli, :mcp
9
- arg :key, String, required: true, positional: true,
10
- description: "dotted key of the derived entry whose source keys you want"
11
-
12
- def self.call(container:, key:, **)
13
- entry = container.manifest.data.entries.find { |e| e.key == key }
14
- deps = entry&.external? ? Array(entry.source&.sources).compact : []
15
- Success({ "key" => key, "deps" => deps.uniq })
16
- end
17
- end
18
- end
19
- end
@@ -1,17 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Action
5
- class Doctor < Base
6
- verb :doctor
7
- summary "Run health checks on the textus store and report any issues."
8
- surfaces :cli
9
- cli "doctor"
10
- arg :checks, Array, required: false, description: "subset of check names to run (default: all)"
11
-
12
- def self.call(container:, call:, checks: nil, **)
13
- Success(Textus::Doctor.build(container: container, checks: checks, role: call.role))
14
- end
15
- end
16
- end
17
- end
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Action
5
- class Drain < Base
6
- verb :drain
7
- summary "Seed materialize + sweep jobs then drain the queue to empty. " \
8
- "Identical to one Watcher tick. Use when no watcher is running."
9
- surfaces :cli, :mcp
10
- arg :prefix, String, description: "restrict to keys under this dotted prefix"
11
- arg :lane, String, description: "restrict to entries in this lane"
12
-
13
- def self.call(container:, call:, prefix: nil, lane: nil) # rubocop:disable Lint/UnusedMethodArgument
14
- queue = Textus::Store::Jobs::Queue.new(store: container.job_store)
15
- Textus::Store::Jobs::Planner.seed(
16
- container: container,
17
- queue: queue,
18
- role: call.role,
19
- )
20
- queue.reclaim(now: Textus::Port::Clock.new.now)
21
- summary = Textus::Store::Jobs::Worker.for(container:, queue:).drain
22
- Success({
23
- "protocol" => Textus::PROTOCOL,
24
- "ok" => summary.failed.zero?,
25
- "completed" => summary.completed,
26
- "failed" => summary.failed,
27
- })
28
- end
29
- end
30
- end
31
- end
@@ -1,37 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Action
5
- class Enqueue < Base
6
- verb :enqueue
7
- summary "Push a registered job type onto the convergence queue, to be run by drain/serve."
8
- surfaces :cli, :mcp
9
- cli "enqueue"
10
- arg :type, String, required: true, positional: true,
11
- description: "registered job type (e.g. materialize, re-pull, sweep)"
12
- arg :args, Hash, default: {},
13
- description: "type-specific arguments (e.g. { key: ... } or { scope: ... })"
14
-
15
- def self.call(container:, call:, type:, args: {})
16
- action_class = Textus::Jobs.fetch(type.to_s)
17
-
18
- if action_class.const_defined?(:REQUIRED_ROLE) && call.role != action_class::REQUIRED_ROLE
19
- return Failure(code: :forbidden,
20
- message: "role '#{call.role}' is not authorized to enqueue this job type",
21
- details: { "role" => call.role, "required_role" => action_class::REQUIRED_ROLE })
22
- end
23
-
24
- job = Textus::Store::Jobs::Queue::Job.new(
25
- type: type,
26
- args: args,
27
- role: call.role,
28
- max_attempts: 3,
29
- )
30
- Textus::Store::Jobs::Queue.new(store: container.job_store).enqueue(job)
31
- Success({ "protocol" => Textus::PROTOCOL, "ok" => true, "id" => job.id })
32
- rescue Textus::UsageError
33
- Failure(code: :usage_error, message: "unregistered job type '#{type}'")
34
- end
35
- end
36
- end
37
- end
@@ -1,34 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Action
5
- class Get < Base
6
- verb :get
7
- summary "Read one entry - a pure on-disk read annotated with a freshness " \
8
- "verdict; never ingests (quarantine freshness is drain + hook " \
9
- "only, ADR 0089). Returns the envelope (uid, etag, _meta, body, " \
10
- "freshness)."
11
- surfaces :cli, :mcp
12
- arg :key, String, required: true, positional: true,
13
- description: "dotted entry key to read, e.g. 'knowledge.project'"
14
- view(:default) { |v, _i| v&.to_h_for_wire }
15
-
16
- def self.call(container:, call:, key:)
17
- envelope = container.compositor.read(key)
18
- return Failure(code: :not_found, message: "no entry at #{key}") unless envelope
19
-
20
- entry = container.manifest.resolver.resolve(key).entry
21
- file_stat = Textus::Port::Storage::FileStat.new
22
- Success(envelope.with(freshness: freshness_evaluator(container, call, file_stat).verdict(entry)))
23
- end
24
-
25
- def self.freshness_evaluator(container, call, file_stat)
26
- Textus::Core::Freshness::Evaluator.new(
27
- manifest: container.manifest,
28
- file_stat: file_stat,
29
- clock: call,
30
- )
31
- end
32
- end
33
- end
34
- end
@@ -1,199 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "fileutils"
4
- require "date"
5
- require "digest"
6
-
7
- module Textus
8
- module Action
9
- class Ingest < Base
10
- verb :ingest
11
- summary "Capture external source material into the raw lane. Write-once, agent-owned."
12
- surfaces :cli, :mcp
13
- arg :kind, String, required: true, positional: true,
14
- description: "source kind: url | file | asset"
15
- arg :slug, String, required: true,
16
- description: "human slug for the key suffix (kebab-case)"
17
- arg :url, String, description: "remote URL (required when kind=url)"
18
- arg :path, String, description: "local file path (required when kind=file or kind=asset)"
19
- arg :zone, String, description: "asset group subdirectory (required when kind=asset)"
20
- arg :label, String, description: "human label stored in source.label"
21
- view { |env| { "key" => env.key, "uid" => env.uid, "etag" => env.etag } }
22
-
23
- SOURCE_KINDS = %w[url file asset].freeze
24
- CONTENT_HASH_ALGO = "sha256"
25
- TOMBSTONE_RETAIN = %w[ingested_at].freeze
26
-
27
- def self.call(container:, call:, kind:, slug:, url: nil, path: nil, zone: nil, label: nil, **) # rubocop:disable Metrics/ParameterLists
28
- validation = validate_inputs(kind:, url:, path:, zone:)
29
- return validation if validation.is_a?(Dry::Monads::Result::Failure)
30
-
31
- now = Time.now.utc
32
- key = derive_key(now, kind:, slug:)
33
-
34
- content_hash = compute_content_hash(kind:, url:, path:)
35
- writer = Textus::Store::Envelope::Writer.from(container: container, call: call)
36
- mentry = container.manifest.resolver.resolve(key).entry
37
- ts = now.iso8601
38
- structured = build_structured(ts, container, now, content_hash, kind:, url:, path:, label:, zone:)
39
-
40
- store = container.job_store
41
- index = Textus::Store::Index::Lookup.new(store: store)
42
- duplicate_key = find_duplicate(index, content_hash, kind:, url:)
43
-
44
- result = if duplicate_key && duplicate_key != key
45
- supersede_entry(duplicate_key, key, structured, container, call, store: store, kind:, zone:)
46
- else
47
- env = write_raw_entry(key, structured, mentry, writer)
48
- rebuild_index(container, store)
49
- env
50
- end
51
- Success(result)
52
- end
53
-
54
- def self.validate_inputs(kind:, url:, path:, zone:)
55
- unless SOURCE_KINDS.include?(kind)
56
- return Failure(code: :usage_error,
57
- message: "ingest kind must be one of #{SOURCE_KINDS.join("|")}, got #{kind.inspect}")
58
- end
59
- case kind
60
- when "url"
61
- return Failure(code: :usage_error, message: "ingest url requires --url") unless url
62
- when "file"
63
- return Failure(code: :usage_error, message: "ingest file requires --path") unless path
64
- when "asset"
65
- return Failure(code: :usage_error, message: "ingest asset requires --path") unless path
66
- return Failure(code: :usage_error, message: "ingest asset requires --zone") unless zone
67
- end
68
- nil
69
- end
70
-
71
- # Key derivation for Gate pre-dispatch auth. Must match the runtime
72
- # derivation in #call so the same key is checked by auth and used by
73
- # the action body.
74
- def self.dispatch_key(kind:, slug:, **)
75
- derive_key(Time.now.utc, kind:, slug:)
76
- end
77
-
78
- def self.derive_key(now, kind:, slug:)
79
- date = now.strftime("%Y.%m.%d")
80
- "raw.#{date}.#{kind}-#{slug}"
81
- end
82
-
83
- def self.compute_content_hash(kind:, url:, path:)
84
- digest = Digest::SHA256.new
85
- case kind
86
- when "url"
87
- digest.update(url)
88
- when "file", "asset"
89
- digest.file(path)
90
- end
91
- "#{CONTENT_HASH_ALGO}:#{digest.hexdigest}"
92
- end
93
-
94
- def self.build_structured(timestamp, container, now, content_hash, kind:, url:, path:, label:, zone:) # rubocop:disable Metrics/ParameterLists
95
- base = { "ingested_at" => timestamp, "content_hash" => content_hash }
96
- case kind
97
- when "url"
98
- base.merge("source" => { "kind" => "url", "url" => url, "label" => label || url },
99
- "body" => nil)
100
- when "file"
101
- body_content = File.read(path)
102
- base.merge("source" => { "kind" => "file", "path" => path,
103
- "label" => label || File.basename(path) },
104
- "body" => body_content)
105
- when "asset"
106
- asset_rel = copy_asset_file(container, now, path:, zone:)
107
- base.merge("source" => { "kind" => "asset",
108
- "label" => label || File.basename(path) },
109
- "asset" => asset_rel,
110
- "body" => nil)
111
- end
112
- end
113
-
114
- def self.write_raw_entry(key, structured, mentry, writer)
115
- writer.put(key, mentry: mentry,
116
- payload: Textus::Store::Envelope::Writer::Payload.new(
117
- meta: nil, body: nil, content: structured,
118
- ))
119
- end
120
-
121
- def self.find_duplicate(index, content_hash, kind:, url:)
122
- dup = index.find_by_hash(content_hash)
123
- return dup if dup
124
-
125
- return unless kind == "url"
126
-
127
- index.find_by_url(url)
128
- end
129
-
130
- def self.rebuild_index(container, store)
131
- Textus::Store::Index::Builder.new(store: store).rebuild!(resolver: container.manifest.resolver)
132
- end
133
-
134
- def self.supersede_entry(old_key, new_key, structured, container, call, store:, kind:, zone:) # rubocop:disable Metrics/ParameterLists
135
- old_mentry = container.manifest.resolver.resolve(old_key).entry
136
- writer = Textus::Store::Envelope::Writer.from(container: container, call: call)
137
-
138
- reader = Textus::Store::Envelope::Reader.from(container: container)
139
- old_env = reader.read(old_key)
140
- old_content = old_env&.content || {}
141
- tombstone = {}
142
- TOMBSTONE_RETAIN.each do |k|
143
- tombstone[k] = old_content[k] if old_content.key?(k)
144
- end
145
- source_kind = old_content.dig("source", "kind")
146
- tombstone["source"] = { "kind" => source_kind } if source_kind
147
- tombstone["superseded_by"] = new_key
148
-
149
- writer.put(old_key, mentry: old_mentry,
150
- payload: Textus::Store::Envelope::Writer::Payload.new(
151
- meta: nil, body: nil, content: tombstone,
152
- ))
153
-
154
- structured["supersedes"] = old_key
155
- env = write_raw_entry(new_key, structured, container.manifest.resolver.resolve(new_key).entry, writer)
156
-
157
- move_asset_file(container, old_content["asset"], zone:) if kind == "asset" && old_content["asset"]
158
-
159
- rebuild_index(container, store)
160
- env
161
- end
162
-
163
- def self.move_asset_file(container, old_asset_rel, zone:)
164
- old_path = File.join(container.root, "assets", old_asset_rel)
165
- return unless File.exist?(old_path)
166
-
167
- now = Time.now.utc
168
- date_path = now.strftime("%Y/%m/%d")
169
- filename = File.basename(old_path)
170
- new_dir = File.join(container.root, "assets", "raw", date_path, zone)
171
- new_path = File.join(new_dir, filename)
172
-
173
- return if old_path == new_path
174
-
175
- FileUtils.mkdir_p(new_dir)
176
- FileUtils.mv(old_path, new_path)
177
- rescue Errno::ENOENT, Errno::EACCES => e
178
- warn "[textus ingest] could not move asset #{old_asset_rel}: #{e.message}"
179
- end
180
-
181
- def self.copy_asset_file(container, now, path:, zone:)
182
- date_path = now.strftime("%Y/%m/%d")
183
- filename = File.basename(path)
184
- assets_dir = File.join(container.root, "assets", "raw", date_path, zone)
185
- FileUtils.mkdir_p(assets_dir)
186
- FileUtils.cp(path, File.join(assets_dir, filename))
187
- create_gitignore_sentinel(container)
188
- "raw/#{date_path}/#{zone}/#{filename}"
189
- end
190
-
191
- def self.create_gitignore_sentinel(container)
192
- assets_root = File.join(container.root, "assets")
193
- FileUtils.mkdir_p(assets_root)
194
- sentinel = File.join(assets_root, ".gitignore")
195
- File.write(sentinel, "*\n") unless File.exist?(sentinel)
196
- end
197
- end
198
- end
199
- end
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Action
5
- class Jobs < Base
6
- verb :jobs
7
- summary "List queued jobs by state; retry a dead-lettered job or purge a state."
8
- surfaces :cli, :mcp
9
- cli "jobs"
10
- arg :state, String, default: "ready", description: "ready|leased|done|failed"
11
- arg :action, String, default: nil, description: "retry|purge (optional)"
12
- arg :job_id, String, default: nil, description: "job id (required for action=retry)"
13
-
14
- def self.call(container:, call:, state: "ready", action: nil, job_id: nil) # rubocop:disable Lint/UnusedMethodArgument
15
- queue = Textus::Store::Jobs::Queue.new(store: container.job_store)
16
- case action
17
- when "retry"
18
- queue.retry_failed(job_id)
19
- when "purge"
20
- queue.purge(state)
21
- end
22
-
23
- Success({ "protocol" => Textus::PROTOCOL, "ok" => true, "state" => state, "jobs" => queue.list(state) })
24
- end
25
- end
26
- end
27
- end
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Action
5
- class KeyDelete < Base
6
- verb :key_delete
7
- summary "Delete one entry by key. Single-key, lower blast radius than key_delete_prefix; " \
8
- "guarded by an optional optimistic-concurrency etag. Returns {ok, key, deleted}."
9
- surfaces :cli, :mcp
10
- cli "key delete"
11
- arg :key, String, required: true, positional: true,
12
- description: "dotted entry key to delete"
13
- arg :if_etag, String,
14
- description: "optimistic-concurrency guard: the etag you last read; the delete is rejected if the entry changed since"
15
-
16
- def self.call(container:, call:, key:, if_etag: nil)
17
- Textus::Manifest::Data.validate_key!(key)
18
- mentry = container.manifest.resolver.resolve(key).entry
19
-
20
- container.compositor.delete(key, mentry: mentry, if_etag: if_etag, call: call)
21
-
22
- Success({ "protocol" => Textus::PROTOCOL, "ok" => true, "key" => key, "deleted" => true })
23
- end
24
- end
25
- end
26
- end
@@ -1,35 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Action
5
- class KeyDeletePrefix < Base
6
- verb :key_delete_prefix
7
- summary "Bulk-delete every leaf key under prefix."
8
- surfaces :cli, :mcp
9
- cli "key delete-prefix"
10
- arg :prefix, String, required: true, positional: true,
11
- description: "every leaf key under this dotted prefix is deleted"
12
- arg :dry_run, :boolean, default: false,
13
- description: "when true, returns the keys that would be deleted without deleting them; " \
14
- "defaults to false, so omitting it deletes immediately"
15
- view { |v, _i| v.to_h }
16
-
17
- def self.call(container:, call:, prefix:, dry_run: false)
18
- return Failure(code: :usage_error, message: "prefix required") if prefix.nil? || prefix.empty?
19
-
20
- leaves = Textus::Action::List.leaf_keys(container: container, prefix: prefix)
21
-
22
- warnings = leaves.empty? ? ["no keys under #{prefix}"] : []
23
- steps = leaves.map { |key| { "op" => "delete", "key" => key } }
24
-
25
- plan = Textus::Store::Jobs::Plan.new(steps: steps, warnings: warnings)
26
- return Success(plan) if dry_run
27
-
28
- steps.each do |step|
29
- Value::Result.unwrap(Textus::Action::KeyDelete.call(container: container, call: call, key: step["key"]))
30
- end
31
- Success(plan)
32
- end
33
- end
34
- end
35
- end
@@ -1,122 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Action
5
- class KeyMv < Base
6
- verb :key_mv
7
- summary "Rename one entry (same zone + format). Refuses if the target exists. Single-key, lower blast radius than key_mv_prefix."
8
- surfaces :cli, :mcp
9
- cli "key mv"
10
- arg :old_key, String, required: true, positional: true,
11
- description: "current dotted key"
12
- arg :new_key, String, required: true, positional: true,
13
- description: "new dotted key (must be the same zone and format as old_key)"
14
- arg :dry_run, :boolean,
15
- description: "when true, returns the planned move (from/to paths, uid) without applying it; " \
16
- "defaults to false, so omitting it applies the move immediately " \
17
- "(unlike the bulk key_mv_prefix, which defaults to a dry-run plan)"
18
-
19
- def self.call(container:, call:, old_key:, new_key:, dry_run: false)
20
- execute_move(container: container, call: call, old_key: old_key, new_key: new_key, dry_run: dry_run)
21
- end
22
-
23
- def self.execute_move(container:, call:, old_key:, new_key:, dry_run:)
24
- prepared = prepare(container: container, old_key: old_key, new_key: new_key)
25
- return prepared if prepared.is_a?(Dry::Monads::Result::Failure)
26
-
27
- old_res, new_res = prepared
28
- if dry_run
29
- return Success(dry_run_result(container: container, old_key: old_key, new_key: new_key, old_res: old_res,
30
- new_res: new_res))
31
- end
32
-
33
- envelope = apply_move(container: container, call: call, old_key: old_key, new_key: new_key, old_res: old_res, new_res: new_res)
34
- Success(success_result(old_key: old_key, new_key: new_key, old_res: old_res, new_res: new_res, envelope: envelope))
35
- end
36
-
37
- def self.apply_move(container:, call:, old_key:, new_key:, old_res:, new_res:)
38
- ensure_uid!(container: container, call: call, old_key: old_key, old_mentry: old_res.entry)
39
- container.compositor.move(
40
- from_key: old_key,
41
- to_key: new_key,
42
- new_mentry: new_res.entry,
43
- call: call,
44
- )
45
- end
46
-
47
- def self.success_result(old_key:, new_key:, old_res:, new_res:, envelope:)
48
- {
49
- "protocol" => Textus::PROTOCOL,
50
- "ok" => true,
51
- "from_key" => old_key,
52
- "to_key" => new_key,
53
- "from_path" => old_res.path,
54
- "to_path" => new_res.path,
55
- "uid" => envelope.uid,
56
- "envelope" => envelope.to_h_for_wire,
57
- }
58
- end
59
-
60
- def self.prepare(container:, old_key:, new_key:)
61
- Textus::Manifest::Data.validate_key!(old_key)
62
- Textus::Manifest::Data.validate_key!(new_key)
63
- return Failure(code: :usage_error, message: "mv: old and new keys are identical") if old_key == new_key
64
-
65
- old_res = container.manifest.resolver.resolve(old_key)
66
- new_res = container.manifest.resolver.resolve(new_key)
67
- return Failure(code: :not_found, message: "source key '#{old_key}' not found") unless container.compositor.exists?(old_key)
68
-
69
- zone_check = validate_zone_and_format(old_mentry: old_res.entry, new_mentry: new_res.entry)
70
- return zone_check if zone_check.is_a?(Dry::Monads::Result::Failure)
71
-
72
- if container.compositor.exists?(new_key)
73
- return Failure(code: :usage_error, message: "mv: target '#{new_key}' already exists at #{new_res.path}")
74
- end
75
-
76
- [old_res, new_res]
77
- end
78
-
79
- def self.validate_zone_and_format(old_mentry:, new_mentry:)
80
- if old_mentry.lane != new_mentry.lane
81
- return Failure(code: :usage_error,
82
- message: "mv: cross-zone move refused (#{old_mentry.lane} -> #{new_mentry.lane}). " \
83
- "Use put+delete for cross-zone moves.")
84
- end
85
- return unless old_mentry.format != new_mentry.format
86
-
87
- Failure(code: :usage_error,
88
- message: "mv: format mismatch (#{old_mentry.format} -> #{new_mentry.format}); refusing.")
89
- end
90
-
91
- def self.ensure_uid!(container:, call:, old_key:, old_mentry:)
92
- pre_env = container.compositor.read(old_key)
93
- return if pre_env.uid
94
-
95
- container.compositor.write(
96
- old_key,
97
- mentry: old_mentry,
98
- payload: Textus::Store::Envelope::Writer::Payload.new(
99
- meta: pre_env.meta,
100
- body: pre_env.body,
101
- content: pre_env.content,
102
- ),
103
- call: call,
104
- )
105
- end
106
-
107
- def self.dry_run_result(container:, old_key:, new_key:, old_res:, new_res:)
108
- pre_env = container.compositor.read(old_key)
109
- {
110
- "protocol" => Textus::PROTOCOL,
111
- "ok" => true,
112
- "dry_run" => true,
113
- "from_key" => old_key,
114
- "to_key" => new_key,
115
- "from_path" => old_res.path,
116
- "to_path" => new_res.path,
117
- "uid" => pre_env.uid,
118
- }
119
- end
120
- end
121
- end
122
- end