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,48 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Action
5
- class KeyMvPrefix < Base
6
- verb :key_mv_prefix
7
- summary "Bulk-rename every leaf key under from_prefix to to_prefix. Dry-run returns a Plan; apply with dry_run: false."
8
- surfaces :cli, :mcp
9
- cli "key mv-prefix"
10
- arg :from_prefix, String, required: true, positional: true,
11
- description: "dotted prefix whose leaf keys are renamed"
12
- arg :to_prefix, String, required: true, positional: true,
13
- description: "dotted prefix the keys are renamed to"
14
- arg :dry_run, :boolean, default: false,
15
- description: "when true, returns the planned moves without applying them; defaults " \
16
- "to false, so omitting it applies the rename immediately"
17
- view { |v, _i| v.to_h }
18
-
19
- def self.call(container:, call:, from_prefix:, to_prefix:, dry_run: false)
20
- return Failure(code: :usage_error, message: "from_prefix and to_prefix required") if from_prefix.nil? || to_prefix.nil?
21
-
22
- leaves = Textus::Action::List.leaf_keys(container: container, prefix: from_prefix)
23
-
24
- if leaves.include?(from_prefix)
25
- return Failure(code: :usage_error,
26
- message: "from_prefix '#{from_prefix}' is itself a leaf — use `mv` to rename a single key")
27
- end
28
-
29
- warnings = []
30
- warnings << "no keys under #{from_prefix}" if leaves.empty?
31
-
32
- steps = leaves.map do |old_key|
33
- tail = old_key.delete_prefix("#{from_prefix}.")
34
- new_key = "#{to_prefix}.#{tail}"
35
- { "op" => "mv", "from" => old_key, "to" => new_key }
36
- end
37
-
38
- plan = Textus::Store::Jobs::Plan.new(steps: steps, warnings: warnings)
39
- return Success(plan) if dry_run
40
-
41
- steps.each do |step|
42
- Value::Result.unwrap(Textus::Action::KeyMv.call(container: container, call: call, old_key: step["from"], new_key: step["to"]))
43
- end
44
- Success(plan)
45
- end
46
- end
47
- end
48
- end
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Action
5
- class List < Base
6
- verb :list
7
- summary "List keys filtered by lane and/or prefix."
8
- surfaces :cli, :mcp
9
- arg :prefix, String,
10
- description: "restrict to keys starting with this dotted prefix, e.g. 'knowledge.runbooks'"
11
- arg :lane, String,
12
- description: "restrict to one lane by name (see `boot` lanes); combine with prefix to narrow further"
13
- view(:cli) { |rows| { "entries" => rows } }
14
-
15
- def self.call(container:, call: nil, prefix: nil, lane: nil) # rubocop:disable Lint/UnusedMethodArgument
16
- manifest = container.manifest
17
- rows = manifest.resolver.enumerate(prefix: prefix)
18
- rows = rows.select { |row| row[:manifest_entry].lane == lane } if lane
19
- Success(rows.map { |row| { "key" => row[:key], "lane" => row[:manifest_entry].lane, "path" => row[:path] } })
20
- end
21
-
22
- def self.leaf_keys(container:, prefix: nil, lane: nil)
23
- rows = Value::Result.unwrap(call(container: container, prefix: prefix, lane: lane))
24
- rows.map { |row| row.is_a?(Hash) ? (row["key"] || row[:key]) : row }
25
- end
26
- end
27
- end
28
- end
@@ -1,42 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Action
5
- class Propose < Base
6
- verb :propose
7
- summary "Write a proposal to the role's propose_lane. Auto-prefixes the key."
8
- surfaces :cli, :mcp
9
- cli_stdin :json
10
- arg :key, String, required: true, positional: true,
11
- description: "key relative to propose_lane, e.g. 'decisions.feature-x'"
12
- arg :meta, Hash, required: false, wire_name: :_meta,
13
- description: "frontmatter; reads back as `_meta` from `get`. Include a 'proposal:' block naming the target_key"
14
- arg :body, String,
15
- description: "markdown/text payload for markdown-format entries; omit (use `content`) for json/yaml entries. Do not send both"
16
- arg :content, Hash,
17
- description: "structured payload for json/yaml-format entries; omit (use `body`) for markdown entries. Do not send both"
18
- view { |env, _i| env.to_h_for_wire }
19
-
20
- def self.call(container:, call:, key:, meta: nil, body: nil, content: nil)
21
- zone = container.manifest.policy.propose_lane_for(call.role)
22
- unless zone
23
- return Failure(code: :propose_forbidden,
24
- message: "role '#{call.role}' has no writable propose_lane",
25
- details: { "role" => call.role })
26
- end
27
-
28
- mentry = container.manifest.resolver.resolve("#{zone}.#{key}").entry
29
- Success(container.compositor.write(
30
- "#{zone}.#{key}",
31
- mentry: mentry,
32
- payload: Textus::Store::Envelope::Writer::Payload.new(
33
- meta: meta || {},
34
- body: body,
35
- content: content,
36
- ),
37
- call: call,
38
- ))
39
- end
40
- end
41
- end
42
- end
@@ -1,22 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Action
5
- class Published < Base
6
- verb :published
7
- summary "List all entries that declare a publish_to target."
8
- surfaces :cli
9
- cli "published"
10
-
11
- def args
12
- {}
13
- end
14
-
15
- def self.call(container:, **)
16
- Success(container.manifest.data.entries.reject { |entry| entry.publish_to.empty? }.map do |entry|
17
- { "key" => entry.key, "publish_to" => entry.publish_to }
18
- end)
19
- end
20
- end
21
- end
22
- end
@@ -1,49 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "time"
4
-
5
- module Textus
6
- module Action
7
- class Pulse < Base
8
- verb :pulse
9
- summary "Delta since cursor — changed entries, pending proposals, index freshness."
10
- surfaces :cli, :mcp
11
- arg :since, Integer, session_default: :cursor,
12
- description: "audit seq to diff from; defaults to the session cursor"
13
-
14
- def self.call(container:, call:, since: nil, **)
15
- manifest = container.manifest
16
- audit_log = container.audit_log
17
- root = container.root
18
- since ||= Textus::Store::Cursor.new(root: root, role: call.role).read
19
-
20
- changed = Value::Result.unwrap(Textus::Action::Audit.call(container: container, seq_since: since))
21
-
22
- result = {
23
- "cursor" => audit_log.latest_seq,
24
- "changed" => changed,
25
- "pending_review" => review_keys(manifest, container),
26
- "contract_etag" => Textus::Value::Etag.for_contract(root),
27
- "index_etag" => index_etag(container),
28
- }
29
-
30
- Textus::Store::Cursor.new(root: root, role: call.role).write(result["cursor"])
31
- Success(result)
32
- end
33
-
34
- def self.review_keys(manifest, container)
35
- queue = manifest.policy.queue_lane
36
- return [] unless queue
37
-
38
- Textus::Action::List.leaf_keys(container: container, lane: queue)
39
- end
40
-
41
- def self.index_etag(container)
42
- path = container.manifest.resolver.resolve("artifacts.system.index").path
43
- File.exist?(path) ? container.file_store.etag(path) : nil
44
- rescue Textus::Error
45
- nil
46
- end
47
- end
48
- end
49
- end
@@ -1,38 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Action
5
- class Put < Base
6
- verb :put
7
- summary "Create or update an entry. Schema-validated. Returns {uid, etag}."
8
- surfaces :cli, :mcp
9
- arg :key, String, required: true, positional: true,
10
- description: "dotted entry key, e.g. 'knowledge.project'; must resolve to a zone the role may write"
11
- arg :meta, Hash, required: false, wire_name: :_meta,
12
- description: "frontmatter; reads back as `_meta` from `get`. Schema-validated — call `schema KEY` first"
13
- arg :body, String,
14
- description: "markdown/text payload for markdown-format entries; omit (use `content`) for json/yaml entries. Do not send both"
15
- arg :content, Hash,
16
- description: "structured payload for json/yaml-format entries; omit (use `body`) for markdown entries. Do not send both"
17
- arg :if_etag, String,
18
- description: "optimistic-concurrency guard: the etag you last read; the write is rejected if the entry changed since"
19
- view { |env| { "uid" => env.uid, "etag" => env.etag } }
20
-
21
- def self.call(container:, call:, key:, meta: nil, body: nil, content: nil, if_etag: nil) # rubocop:disable Metrics/ParameterLists
22
- Textus::Manifest::Data.validate_key!(key)
23
- mentry = container.manifest.resolver.resolve(key).entry
24
- Success(container.compositor.write(
25
- key,
26
- mentry: mentry,
27
- payload: Textus::Store::Envelope::Writer::Payload.new(
28
- meta: meta,
29
- body: body,
30
- content: content,
31
- ),
32
- if_etag: if_etag,
33
- call: call,
34
- ))
35
- end
36
- end
37
- end
38
- end
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Action
5
- class Rdeps < Base
6
- verb :rdeps
7
- summary "List the derived entries that depend on a key (reverse deps / impact set)."
8
- surfaces :cli, :mcp
9
- arg :key, String, required: true, positional: true,
10
- description: "dotted key whose dependents (what would be stranded if it moved) you want"
11
-
12
- def self.call(container:, key:, **)
13
- manifest = container.manifest
14
- rdeps = manifest.data.entries.each_with_object([]) do |entry, acc|
15
- next unless entry.external?
16
-
17
- sources = Array(entry.source&.sources).compact
18
- acc << entry.key if sources.any? { |source| source == key || key.start_with?("#{source}.") }
19
- end
20
- Success({ "key" => key, "rdeps" => rdeps })
21
- end
22
- end
23
- end
24
- end
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Action
5
- class Reject < Base
6
- verb :reject
7
- summary "discard a queued proposal without applying it"
8
- surfaces :cli, :mcp
9
- cli "reject"
10
- arg :pending_key, String, required: true, positional: true, description: "the queued proposal's key"
11
-
12
- def self.call(container:, call:, pending_key:)
13
- mentry = container.manifest.resolver.resolve(pending_key).entry
14
- unless mentry.in_proposal_lane?(container.manifest.policy)
15
- return Failure(code: :proposal_error, message: "reject: '#{pending_key}' is not in a proposal zone (zone=#{mentry.lane})")
16
- end
17
-
18
- env = container.compositor.read(pending_key)
19
- parsed = proposal_from(env, key: pending_key)
20
- return parsed if parsed.is_a?(Dry::Monads::Result::Failure)
21
-
22
- target_key = parsed[:target_key]
23
- container.compositor.delete(pending_key, mentry: mentry, call: call)
24
- Success("protocol" => Textus::PROTOCOL, "rejected" => pending_key, "target_key" => target_key)
25
- end
26
- end
27
- end
28
- end
@@ -1,81 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Action
5
- class RuleExplain < Base
6
- verb :rule_explain
7
- summary "Effective rules for a key. Lean {lifecycle, guard} by default; detail: true adds matched blocks + guard predicates."
8
- surfaces :cli, :mcp
9
- cli "rule explain"
10
- arg :key, String, required: true, positional: true,
11
- description: "dotted key whose effective rules you want (lifecycle ttl/action, write guard, ...)"
12
- arg :detail, :boolean,
13
- description: "defaults false: lean {lifecycle, guard}. detail: true adds matched blocks + guard predicates per transition."
14
- view(:cli) { |r| { "verb" => "rule_explain" }.merge(r.transform_keys(&:to_s)) }
15
-
16
- def self.call(container:, call:, key:, detail: nil) # rubocop:disable Lint/UnusedMethodArgument
17
- manifest = container.manifest
18
- Success(detail ? explain(manifest, key) : effective(manifest, key))
19
- end
20
-
21
- REGISTRY = Textus::Manifest::Schema::FIELD_REGISTRY
22
- LEAN_FIELDS = REGISTRY.select { |_, m| m[:in_rule_explain].include?(:lean) }.keys.freeze
23
- DETAIL_FIELDS = REGISTRY.select { |_, m| m[:in_rule_explain].include?(:detail) }.keys.freeze
24
- EFFECTIVE_FIELDS = DETAIL_FIELDS.select { |f| REGISTRY[f][:policy_class] }.freeze
25
-
26
- def self.effective(manifest, key)
27
- set = manifest.rules.for(key)
28
- LEAN_FIELDS.each_with_object({}) do |field, out|
29
- value = set.public_send(field)
30
- out[field.to_s] = lean_value(field, value) unless value.nil?
31
- end
32
- end
33
-
34
- def self.lean_value(field, value)
35
- case field
36
- when :retention
37
- retention_hash(value, string_keys: true)
38
- when :react
39
- value.to_h
40
- else
41
- value
42
- end
43
- end
44
-
45
- def self.explain(manifest, key)
46
- matching = manifest.rules.explain(key)
47
- winners = manifest.rules.for(key)
48
- {
49
- key: key,
50
- matched_blocks: matching.map do |block|
51
- { match: block.match }.merge(DETAIL_FIELDS.to_h { |f| [f, !block.public_send(f).nil?] })
52
- end,
53
- effective: EFFECTIVE_FIELDS.to_h { |f| [f, effective_value(f, winners.public_send(f))] },
54
- guards: Textus::Gate::Auth::FLOOR.keys.to_h do |action|
55
- floor = Textus::Gate::Auth::FLOOR.fetch(action, [])
56
- rule = Array(manifest.rules.for(key).guard&.dig(action.to_s))
57
- [action, { floor: floor, rule: rule }]
58
- end,
59
- }
60
- end
61
-
62
- def self.effective_value(field, value)
63
- return nil if value.nil?
64
-
65
- case field
66
- when :retention
67
- retention_hash(value, string_keys: false)
68
- when :react
69
- value.to_h
70
- else
71
- value
72
- end
73
- end
74
-
75
- def self.retention_hash(retention, string_keys:)
76
- h = { ttl_seconds: retention.ttl_seconds, action: retention.action }
77
- string_keys ? h.transform_keys(&:to_s) : h
78
- end
79
- end
80
- end
81
- end
@@ -1,62 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "yaml"
4
-
5
- module Textus
6
- module Action
7
- class RuleLint < Base
8
- verb :rule_lint
9
- summary "Diff candidate manifest YAML's rules against the live manifest. No writes."
10
- surfaces :cli, :mcp
11
- cli "rule lint"
12
- arg :candidate_yaml, String, required: true, wire_name: :against, source: :file,
13
- description: "path to candidate manifest YAML; its `rules:` block is diffed against the live manifest"
14
- view { |v, _i| v.to_h }
15
-
16
- def self.call(container:, call:, candidate_yaml:) # rubocop:disable Lint/UnusedMethodArgument
17
- root = container.root
18
- live_rules = current_rules(root)
19
- candidate_result = parse_candidate(candidate_yaml)
20
- return candidate_result if candidate_result.is_a?(Dry::Monads::Result::Failure)
21
-
22
- candidate_rules = candidate_result
23
-
24
- live_by_match = live_rules.to_h { |rule| [rule["match"], rule] }
25
- candidate_by_match = candidate_rules.to_h { |rule| [rule["match"], rule] }
26
-
27
- steps = (candidate_by_match.keys - live_by_match.keys).map do |match|
28
- { "op" => "add_rule", "match" => match, "rule" => candidate_by_match[match] }
29
- end
30
- (live_by_match.keys - candidate_by_match.keys).each do |match|
31
- steps << { "op" => "remove_rule", "match" => match }
32
- end
33
- (live_by_match.keys & candidate_by_match.keys).each do |match|
34
- next if live_by_match[match] == candidate_by_match[match]
35
-
36
- steps << {
37
- "op" => "change_rule",
38
- "match" => match,
39
- "from" => live_by_match[match],
40
- "to" => candidate_by_match[match],
41
- }
42
- end
43
-
44
- Success(Textus::Store::Jobs::Plan.new(steps: steps, warnings: []))
45
- end
46
-
47
- def self.current_rules(root)
48
- raw = YAML.safe_load_file(File.join(root, "manifest.yaml"), permitted_classes: [Symbol], aliases: false)
49
- Array(raw["rules"])
50
- end
51
-
52
- def self.parse_candidate(yaml_text)
53
- raw = YAML.safe_load(yaml_text, permitted_classes: [Symbol], aliases: false)
54
- return Failure(code: :usage_error, message: "candidate is not a YAML mapping") unless raw.is_a?(Hash)
55
-
56
- Array(raw["rules"])
57
- rescue Psych::Exception => e
58
- Failure(code: :usage_error, message: "candidate YAML parse error: #{e.message}")
59
- end
60
- end
61
- end
62
- end
@@ -1,38 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Action
5
- class RuleList < Base
6
- verb :rule_list
7
- summary "List every rule block in the manifest."
8
- surfaces :cli
9
- cli "rule list"
10
- view(:cli) { |policies| { "verb" => "rule_list", "policies" => policies } }
11
-
12
- def self.call(container:, call:, **_options) # rubocop:disable Lint/UnusedMethodArgument
13
- manifest = container.manifest
14
- Success(manifest.rules.blocks.map do |block|
15
- row = { "match" => block.match }
16
- LIST_FIELDS.each do |field|
17
- value = block.public_send(field)
18
- row[field.to_s] = serialize(field, value) unless value.nil?
19
- end
20
- row
21
- end)
22
- end
23
-
24
- LIST_FIELDS = Textus::Manifest::Schema::FIELD_REGISTRY.select { |_, m| m[:in_rule_list] }.keys.freeze
25
-
26
- def self.serialize(field, value)
27
- case field
28
- when :retention
29
- { "ttl_seconds" => value.ttl_seconds, "action" => value.action.to_s }
30
- when :react
31
- value.to_h
32
- else
33
- value
34
- end
35
- end
36
- end
37
- end
38
- end
@@ -1,22 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Action
5
- class SchemaEnvelope < Base
6
- verb :schema_show
7
- summary "Return the schema (field shape) for an entry's family, by key."
8
- surfaces :cli, :mcp
9
- cli "schema show"
10
- arg :key, String, required: true, positional: true,
11
- description: "any key in the family whose schema you want; returns required/optional fields and their types"
12
-
13
- def self.call(container:, key:, **)
14
- manifest = container.manifest
15
- schemas = container.schemas
16
- mentry = manifest.resolver.resolve(key).entry
17
- schema = schemas.fetch_or_nil(mentry.schema)
18
- Success({ "protocol" => Textus::PROTOCOL, "key" => key, "schema_ref" => mentry.schema, "schema" => schema&.to_h })
19
- end
20
- end
21
- end
22
- end
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Action
5
- class Uid < Base
6
- verb :uid
7
- summary "Return the stable UID of an entry without reading its body."
8
- surfaces :cli
9
- cli "key uid"
10
- arg :key, String, required: true, positional: true, description: "entry key"
11
- view(:cli) { |uid, inputs| { "key" => inputs[:key], "uid" => uid } }
12
-
13
- def self.call(container:, call:, key:)
14
- envelope = Value::Result.unwrap(Textus::Action::Get.call(container: container, call: call, key: key))
15
- Success(envelope.uid)
16
- end
17
- end
18
- end
19
- end
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Action
5
- class Where < Base
6
- verb :where
7
- summary "Resolve a key to its zone, owner, and path without reading the body."
8
- surfaces :cli, :mcp
9
- arg :key, String, required: true, positional: true,
10
- description: "dotted key to locate (returns zone, owner, path; does not read content)"
11
-
12
- def self.call(container:, key:, call: nil) # rubocop:disable Lint/UnusedMethodArgument
13
- manifest = container.manifest
14
- res = manifest.resolver.resolve(key)
15
- mentry = res.entry
16
- path = res.path
17
- Success({ "protocol" => Textus::PROTOCOL, "key" => key, "lane" => mentry.lane, "owner" => mentry.owner, "path" => path })
18
- end
19
- end
20
- end
21
- end