textus 0.52.0 → 0.54.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 (261) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +59 -0
  3. data/README.md +62 -54
  4. data/SPEC.md +64 -192
  5. data/docs/architecture/README.md +157 -156
  6. data/exe/textus +1 -1
  7. data/lib/textus/action/accept.rb +44 -0
  8. data/lib/textus/action/audit.rb +131 -0
  9. data/lib/textus/action/base.rb +42 -0
  10. data/lib/textus/{read → action}/blame.rb +28 -22
  11. data/lib/textus/action/boot.rb +17 -0
  12. data/lib/textus/action/data_mv.rb +69 -0
  13. data/lib/textus/action/deps.rb +34 -0
  14. data/lib/textus/action/doctor.rb +24 -0
  15. data/lib/textus/action/drain.rb +39 -0
  16. data/lib/textus/action/enqueue.rb +53 -0
  17. data/lib/textus/action/get.rb +78 -0
  18. data/lib/textus/action/ingest.rb +135 -0
  19. data/lib/textus/action/jobs.rb +36 -0
  20. data/lib/textus/action/key_delete.rb +36 -0
  21. data/lib/textus/action/key_delete_prefix.rb +44 -0
  22. data/lib/textus/action/key_mv.rb +129 -0
  23. data/lib/textus/action/key_mv_prefix.rb +57 -0
  24. data/lib/textus/action/list.rb +42 -0
  25. data/lib/textus/action/propose.rb +52 -0
  26. data/lib/textus/action/published.rb +24 -0
  27. data/lib/textus/action/pulse.rb +56 -0
  28. data/lib/textus/action/put.rb +52 -0
  29. data/lib/textus/action/rdeps.rb +39 -0
  30. data/lib/textus/action/reject.rb +37 -0
  31. data/lib/textus/action/rule_explain.rb +91 -0
  32. data/lib/textus/action/rule_lint.rb +68 -0
  33. data/lib/textus/action/rule_list.rb +42 -0
  34. data/lib/textus/action/schema_envelope.rb +29 -0
  35. data/lib/textus/action/uid.rb +33 -0
  36. data/lib/textus/action/where.rb +36 -0
  37. data/lib/textus/action/write_verb.rb +44 -0
  38. data/lib/textus/boot.rb +64 -132
  39. data/lib/textus/command.rb +41 -0
  40. data/lib/textus/container.rb +1 -1
  41. data/lib/textus/{domain → core}/duration.rb +1 -1
  42. data/lib/textus/{domain → core}/freshness/evaluator.rb +25 -41
  43. data/lib/textus/{domain → core}/freshness/verdict.rb +2 -2
  44. data/lib/textus/{domain → core}/freshness.rb +2 -2
  45. data/lib/textus/{domain → core}/retention/sweep.rb +7 -7
  46. data/lib/textus/{domain → core}/retention.rb +2 -2
  47. data/lib/textus/{domain → core}/sentinel.rb +1 -1
  48. data/lib/textus/doctor/check/generator_drift.rb +1 -1
  49. data/lib/textus/doctor/check/illegal_keys.rb +1 -1
  50. data/lib/textus/doctor/check/notebook_sources.rb +50 -0
  51. data/lib/textus/doctor/check/proposal_targets.rb +3 -3
  52. data/lib/textus/doctor/check/raw_asset_paths.rb +50 -0
  53. data/lib/textus/doctor/check/rule_ambiguity.rb +2 -2
  54. data/lib/textus/doctor/check/schema_violations.rb +8 -2
  55. data/lib/textus/doctor/check.rb +11 -9
  56. data/lib/textus/{read → doctor}/validator.rb +22 -13
  57. data/lib/textus/doctor.rb +4 -44
  58. data/lib/textus/envelope/reader.rb +46 -0
  59. data/lib/textus/envelope/writer.rb +209 -0
  60. data/lib/textus/envelope.rb +5 -3
  61. data/lib/textus/errors.rb +25 -13
  62. data/lib/textus/{entry → format}/base.rb +1 -6
  63. data/lib/textus/{entry → format}/json.rb +1 -4
  64. data/lib/textus/{entry → format}/markdown.rb +2 -6
  65. data/lib/textus/{entry → format}/text.rb +2 -6
  66. data/lib/textus/{entry → format}/yaml.rb +1 -4
  67. data/lib/textus/{entry.rb → format.rb} +9 -10
  68. data/lib/textus/gate/auth.rb +227 -0
  69. data/lib/textus/gate.rb +116 -0
  70. data/lib/textus/init.rb +59 -105
  71. data/lib/textus/jobs/base.rb +23 -0
  72. data/lib/textus/jobs/materialize.rb +20 -0
  73. data/lib/textus/jobs/plan.rb +9 -0
  74. data/lib/textus/jobs/planner.rb +101 -0
  75. data/lib/textus/jobs/retention.rb +48 -0
  76. data/lib/textus/jobs/sweep.rb +27 -0
  77. data/lib/textus/jobs/worker.rb +67 -0
  78. data/lib/textus/jobs.rb +15 -0
  79. data/lib/textus/key/path.rb +10 -3
  80. data/lib/textus/layout.rb +13 -0
  81. data/lib/textus/manifest/data.rb +14 -14
  82. data/lib/textus/manifest/entry/base.rb +14 -25
  83. data/lib/textus/manifest/entry/parser.rb +31 -14
  84. data/lib/textus/manifest/entry/produced.rb +13 -33
  85. data/lib/textus/manifest/entry/publish/mode.rb +1 -1
  86. data/lib/textus/manifest/entry/publish/to_paths.rb +4 -3
  87. data/lib/textus/manifest/entry/publish.rb +0 -12
  88. data/lib/textus/manifest/entry/validators/format_matrix.rb +1 -1
  89. data/lib/textus/manifest/entry/validators.rb +0 -1
  90. data/lib/textus/{domain → manifest}/policy/matcher.rb +2 -2
  91. data/lib/textus/{domain → manifest}/policy/publish_target.rb +2 -2
  92. data/lib/textus/manifest/policy/react.rb +30 -0
  93. data/lib/textus/{domain → manifest}/policy/retention.rb +3 -3
  94. data/lib/textus/manifest/policy/source.rb +30 -0
  95. data/lib/textus/manifest/policy.rb +38 -53
  96. data/lib/textus/manifest/resolver.rb +5 -4
  97. data/lib/textus/manifest/rules.rb +4 -4
  98. data/lib/textus/manifest/schema/keys.rb +20 -28
  99. data/lib/textus/manifest/schema/validator.rb +27 -33
  100. data/lib/textus/manifest/schema/vocabulary.rb +2 -1
  101. data/lib/textus/manifest/schema.rb +2 -2
  102. data/lib/textus/manifest.rb +2 -2
  103. data/lib/textus/ports/audit_log.rb +31 -5
  104. data/lib/textus/{domain/jobs → ports/job_store}/job.rb +19 -12
  105. data/lib/textus/ports/{queue.rb → job_store.rb} +7 -14
  106. data/lib/textus/ports/sentinel_store.rb +2 -2
  107. data/lib/textus/ports/watcher_lock.rb +48 -0
  108. data/lib/textus/produce/engine.rb +25 -70
  109. data/lib/textus/produce/render.rb +2 -7
  110. data/lib/textus/schema/tools.rb +4 -3
  111. data/lib/textus/session.rb +6 -3
  112. data/lib/textus/store.rb +12 -19
  113. data/lib/textus/surfaces/cli/group/data.rb +11 -0
  114. data/lib/textus/surfaces/cli/group/key.rb +11 -0
  115. data/lib/textus/surfaces/cli/group/mcp.rb +11 -0
  116. data/lib/textus/surfaces/cli/group/rule.rb +11 -0
  117. data/lib/textus/surfaces/cli/group/schema.rb +11 -0
  118. data/lib/textus/surfaces/cli/group.rb +50 -0
  119. data/lib/textus/surfaces/cli/runner.rb +236 -0
  120. data/lib/textus/surfaces/cli/verb/doctor.rb +21 -0
  121. data/lib/textus/surfaces/cli/verb/get.rb +21 -0
  122. data/lib/textus/surfaces/cli/verb/init.rb +20 -0
  123. data/lib/textus/surfaces/cli/verb/mcp_serve.rb +24 -0
  124. data/lib/textus/surfaces/cli/verb/put.rb +30 -0
  125. data/lib/textus/surfaces/cli/verb/schema_diff.rb +17 -0
  126. data/lib/textus/surfaces/cli/verb/schema_init.rb +21 -0
  127. data/lib/textus/surfaces/cli/verb/schema_migrate.rb +21 -0
  128. data/lib/textus/surfaces/cli/verb/watch.rb +19 -0
  129. data/lib/textus/surfaces/cli/verb.rb +111 -0
  130. data/lib/textus/surfaces/cli.rb +148 -0
  131. data/lib/textus/surfaces/mcp/catalog.rb +99 -0
  132. data/lib/textus/surfaces/mcp/errors.rb +25 -0
  133. data/lib/textus/surfaces/mcp/routing.rb +51 -0
  134. data/lib/textus/surfaces/mcp/server.rb +184 -0
  135. data/lib/textus/surfaces/mcp/session.rb +9 -0
  136. data/lib/textus/surfaces/mcp/tool_schemas.rb +17 -0
  137. data/lib/textus/surfaces/mcp.rb +8 -0
  138. data/lib/textus/surfaces/role_scope.rb +34 -0
  139. data/lib/textus/surfaces/watcher.rb +38 -0
  140. data/lib/textus/version.rb +1 -1
  141. data/lib/textus/workflow/collector.rb +27 -0
  142. data/lib/textus/workflow/context.rb +5 -0
  143. data/lib/textus/workflow/dsl.rb +44 -0
  144. data/lib/textus/workflow/loader.rb +17 -0
  145. data/lib/textus/workflow/pattern.rb +18 -0
  146. data/lib/textus/workflow/registry.rb +21 -0
  147. data/lib/textus/workflow/runner.rb +84 -0
  148. data/lib/textus/workflow.rb +28 -0
  149. data/lib/textus.rb +75 -22
  150. metadata +118 -132
  151. data/lib/textus/cli/group/hook.rb +0 -9
  152. data/lib/textus/cli/group/key.rb +0 -9
  153. data/lib/textus/cli/group/mcp.rb +0 -9
  154. data/lib/textus/cli/group/rule.rb +0 -9
  155. data/lib/textus/cli/group/schema.rb +0 -9
  156. data/lib/textus/cli/group/zone.rb +0 -9
  157. data/lib/textus/cli/group.rb +0 -48
  158. data/lib/textus/cli/runner.rb +0 -193
  159. data/lib/textus/cli/verb/doctor.rb +0 -17
  160. data/lib/textus/cli/verb/get.rb +0 -18
  161. data/lib/textus/cli/verb/hook_run.rb +0 -48
  162. data/lib/textus/cli/verb/hooks.rb +0 -50
  163. data/lib/textus/cli/verb/init.rb +0 -18
  164. data/lib/textus/cli/verb/mcp_serve.rb +0 -22
  165. data/lib/textus/cli/verb/put.rb +0 -30
  166. data/lib/textus/cli/verb/schema_diff.rb +0 -15
  167. data/lib/textus/cli/verb/schema_init.rb +0 -19
  168. data/lib/textus/cli/verb/schema_migrate.rb +0 -19
  169. data/lib/textus/cli/verb/serve.rb +0 -19
  170. data/lib/textus/cli/verb.rb +0 -116
  171. data/lib/textus/cli.rb +0 -138
  172. data/lib/textus/dispatcher.rb +0 -54
  173. data/lib/textus/doctor/check/handler_allowlist.rb +0 -34
  174. data/lib/textus/doctor/check/hooks.rb +0 -32
  175. data/lib/textus/doctor/check/intake_registration.rb +0 -46
  176. data/lib/textus/domain/action.rb +0 -9
  177. data/lib/textus/domain/jobs/registry.rb +0 -37
  178. data/lib/textus/domain/permission.rb +0 -7
  179. data/lib/textus/domain/policy/base_guards.rb +0 -25
  180. data/lib/textus/domain/policy/evaluation.rb +0 -15
  181. data/lib/textus/domain/policy/guard.rb +0 -35
  182. data/lib/textus/domain/policy/guard_factory.rb +0 -40
  183. data/lib/textus/domain/policy/handler_allowlist.rb +0 -17
  184. data/lib/textus/domain/policy/predicates/author_held.rb +0 -33
  185. data/lib/textus/domain/policy/predicates/etag_match.rb +0 -32
  186. data/lib/textus/domain/policy/predicates/fresh_within.rb +0 -59
  187. data/lib/textus/domain/policy/predicates/registry.rb +0 -39
  188. data/lib/textus/domain/policy/predicates/schema_valid.rb +0 -61
  189. data/lib/textus/domain/policy/predicates/target_is_canon.rb +0 -33
  190. data/lib/textus/domain/policy/predicates/zone_writable_by.rb +0 -39
  191. data/lib/textus/domain/policy/source.rb +0 -73
  192. data/lib/textus/envelope/io/reader.rb +0 -48
  193. data/lib/textus/envelope/io/writer.rb +0 -182
  194. data/lib/textus/hooks/builtin.rb +0 -70
  195. data/lib/textus/hooks/catalog.rb +0 -37
  196. data/lib/textus/hooks/context.rb +0 -72
  197. data/lib/textus/hooks/error_log.rb +0 -32
  198. data/lib/textus/hooks/event_bus.rb +0 -114
  199. data/lib/textus/hooks/fire_report.rb +0 -23
  200. data/lib/textus/hooks/loader.rb +0 -54
  201. data/lib/textus/hooks/rpc_registry.rb +0 -43
  202. data/lib/textus/hooks/signature.rb +0 -31
  203. data/lib/textus/init/templates/machine_intake.rb +0 -45
  204. data/lib/textus/init/templates/orientation_reducer.rb +0 -17
  205. data/lib/textus/jobs/handlers.rb +0 -62
  206. data/lib/textus/jobs/scheduler.rb +0 -36
  207. data/lib/textus/jobs/seeder.rb +0 -57
  208. data/lib/textus/maintenance/drain.rb +0 -42
  209. data/lib/textus/maintenance/key_delete_prefix.rb +0 -48
  210. data/lib/textus/maintenance/key_mv_prefix.rb +0 -68
  211. data/lib/textus/maintenance/retention/apply.rb +0 -52
  212. data/lib/textus/maintenance/rule_lint.rb +0 -66
  213. data/lib/textus/maintenance/serve.rb +0 -30
  214. data/lib/textus/maintenance/worker.rb +0 -74
  215. data/lib/textus/maintenance/zone_mv.rb +0 -64
  216. data/lib/textus/maintenance.rb +0 -15
  217. data/lib/textus/manifest/entry/validators/events.rb +0 -22
  218. data/lib/textus/mcp/catalog.rb +0 -70
  219. data/lib/textus/mcp/errors.rb +0 -32
  220. data/lib/textus/mcp/server.rb +0 -138
  221. data/lib/textus/mcp/session.rb +0 -7
  222. data/lib/textus/mcp/tool_schemas.rb +0 -15
  223. data/lib/textus/mcp.rb +0 -6
  224. data/lib/textus/mustache.rb +0 -117
  225. data/lib/textus/ports/audit_subscriber.rb +0 -42
  226. data/lib/textus/ports/produce_on_write_subscriber.rb +0 -73
  227. data/lib/textus/produce/acquire/handler.rb +0 -29
  228. data/lib/textus/produce/acquire/intake.rb +0 -130
  229. data/lib/textus/produce/acquire/projection.rb +0 -127
  230. data/lib/textus/produce/acquire/serializer/json.rb +0 -31
  231. data/lib/textus/produce/acquire/serializer/text.rb +0 -16
  232. data/lib/textus/produce/acquire/serializer/yaml.rb +0 -31
  233. data/lib/textus/produce/acquire/serializer.rb +0 -17
  234. data/lib/textus/produce/events.rb +0 -36
  235. data/lib/textus/projection.rb +0 -103
  236. data/lib/textus/read/audit.rb +0 -130
  237. data/lib/textus/read/boot.rb +0 -26
  238. data/lib/textus/read/capabilities.rb +0 -70
  239. data/lib/textus/read/deps.rb +0 -38
  240. data/lib/textus/read/doctor.rb +0 -27
  241. data/lib/textus/read/freshness.rb +0 -152
  242. data/lib/textus/read/get.rb +0 -73
  243. data/lib/textus/read/jobs.rb +0 -31
  244. data/lib/textus/read/list.rb +0 -24
  245. data/lib/textus/read/published.rb +0 -22
  246. data/lib/textus/read/pulse.rb +0 -98
  247. data/lib/textus/read/rdeps.rb +0 -39
  248. data/lib/textus/read/rule_explain.rb +0 -96
  249. data/lib/textus/read/rule_list.rb +0 -54
  250. data/lib/textus/read/schema_envelope.rb +0 -25
  251. data/lib/textus/read/uid.rb +0 -29
  252. data/lib/textus/read/validate_all.rb +0 -36
  253. data/lib/textus/read/where.rb +0 -24
  254. data/lib/textus/role_scope.rb +0 -78
  255. data/lib/textus/write/accept.rb +0 -58
  256. data/lib/textus/write/enqueue.rb +0 -50
  257. data/lib/textus/write/key_delete.rb +0 -65
  258. data/lib/textus/write/key_mv.rb +0 -141
  259. data/lib/textus/write/propose.rb +0 -54
  260. data/lib/textus/write/put.rb +0 -74
  261. data/lib/textus/write/reject.rb +0 -68
@@ -1,9 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Group
4
- class Hook < Group
5
- command_name "hook"
6
- end
7
- end
8
- end
9
- end
@@ -1,9 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Group
4
- class Key < Group
5
- command_name "key"
6
- end
7
- end
8
- end
9
- end
@@ -1,9 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Group
4
- class MCP < Group
5
- command_name "mcp"
6
- end
7
- end
8
- end
9
- end
@@ -1,9 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Group
4
- class Rule < Group
5
- command_name "rule"
6
- end
7
- end
8
- end
9
- end
@@ -1,9 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Group
4
- class Schema < Group
5
- command_name "schema"
6
- end
7
- end
8
- end
9
- end
@@ -1,9 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Group
4
- class Zone < Group
5
- command_name "zone"
6
- end
7
- end
8
- end
9
- end
@@ -1,48 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Group < Verb
4
- class << self
5
- # Subcommands are auto-derived: any Verb descendant whose
6
- # `parent_group` is this group counts as a subcommand. Sorted
7
- # alphabetically by command_name for stable help output.
8
- def subcommands
9
- Textus::CLI::Runner.install!
10
- Verb.descendants
11
- .select { |k| k.parent_group == self && k.command_name }
12
- .sort_by(&:command_name)
13
- .to_h { |k| [k.command_name, k] }
14
- end
15
-
16
- def needs_store?
17
- # Delegate to the matched subcommand at parse time; default true.
18
- true
19
- end
20
- end
21
-
22
- def parse(argv)
23
- subs = self.class.subcommands
24
- subname = argv.shift
25
- if subname.nil?
26
- raise UsageError.new(
27
- "#{self.class.command_name} requires a subcommand: #{subs.keys.join(", ")}",
28
- )
29
- end
30
-
31
- @sub_klass = subs[subname]
32
- unless @sub_klass
33
- raise UsageError.new(
34
- "unknown #{self.class.command_name} subcommand '#{subname}'. " \
35
- "Valid: #{subs.keys.join(", ")}",
36
- )
37
- end
38
-
39
- @sub = @sub_klass.new(stdin: @stdin, stdout: @stdout, stderr: @stderr, cwd: @cwd)
40
- @sub.parse(argv)
41
- end
42
-
43
- def call(store)
44
- @sub.call(@sub_klass.needs_store? ? store : nil)
45
- end
46
- end
47
- end
48
- end
@@ -1,193 +0,0 @@
1
- module Textus
2
- class CLI
3
- # Generates CLI::Verb (and CLI::Group) subclasses from per-verb contracts,
4
- # so the CLI surface is a projection of the contract — the operator-facing
5
- # mirror of MCP::Catalog (ADR 0063).
6
- module Runner
7
- # Subclassable base for contract-projected verbs. Carries the verb's
8
- # contract (class attr `spec`) and the generic dispatch, exposing one
9
- # overridable seam, #invoke, that defaults to the generic projection.
10
- # Escape-hatch verbs subclass this and override #invoke to add behavior
11
- # (suggestions, --stdin, BuildLock, multi-dispatch) WITHOUT restating the
12
- # verb name — `spec.verb` remains the single source of dispatch.
13
- class Base < Verb
14
- class << self
15
- attr_accessor :spec
16
-
17
- # ADR 0064: derive the CLI command name from the contract's cli_leaf
18
- # when not set explicitly, so an escape-hatch class never restates its
19
- # own name. The reconciliation spec proves command_name == cli_leaf for
20
- # every such class, so this is an equivalence, not a behavior change.
21
- def command_name(name = nil)
22
- return super if name
23
-
24
- super() || spec&.cli_leaf
25
- end
26
- end
27
-
28
- def spec = self.class.spec
29
-
30
- def call(store)
31
- invoke(store)
32
- end
33
-
34
- # Default: pure contract projection. Override in subclasses for behavior.
35
- def invoke(store)
36
- Runner.dispatch(self, store, spec)
37
- end
38
-
39
- def flag_values(s = spec)
40
- s.args.reject(&:positional).each_with_object({}) do |a, h|
41
- raw = respond_to?(a.name) ? public_send(a.name) : nil
42
- next if raw.nil?
43
-
44
- h[a.name] = Runner.coerce(a, raw)
45
- end
46
- end
47
- end
48
-
49
- module_function
50
-
51
- # Normalize parsed CLI input into the uniform by-name inputs hash and
52
- # dispatch through RoleScope's single bind+invoke site. A missing required
53
- # arg becomes a UsageError phrased in the operator's command path (parity
54
- # with the hand-written verbs).
55
- def dispatch(verb_instance, store, spec)
56
- inputs = Textus::Contract::Binder.inputs_from_ordered(
57
- spec, verb_instance.positional, verb_instance.flag_values(spec)
58
- )
59
- inputs = inputs.merge(Textus::Contract::Sources.from_stdin(spec, verb_instance.stdin)) if spec.cli_stdin
60
- inputs = Textus::Contract::Sources.acquire(spec, inputs)
61
- inputs = apply_cli_defaults(spec, inputs)
62
- scope = verb_instance.session_for(store)
63
- begin
64
- result = scope.dispatch_bound(spec.verb, inputs)
65
- rescue Textus::Contract::MissingArgs => e
66
- raise UsageError.new("#{spec.cli_path} requires #{e.missing.first.wire}")
67
- end
68
- verb_instance.emit(shape(spec, result, inputs))
69
- end
70
-
71
- # Fill CLI-specific defaults (cli_default:) for args the operator did not
72
- # pass, where the CLI default diverges from the contract default the agent
73
- # surfaces use — e.g. migrate/zone_mv apply by default on the CLI but plan
74
- # by default for agents (ADR 0068). The divergence is legible in the
75
- # contract, not hidden in a hand class.
76
- def apply_cli_defaults(spec, inputs)
77
- spec.args.each_with_object(inputs.dup) do |a, h|
78
- next if a.cli_default == :__unset || h.key?(a.name)
79
-
80
- h[a.name] = a.cli_default
81
- end
82
- end
83
-
84
- # Shape the use-case result for the CLI wire via the verb's :cli view
85
- # (falling back to the default view). The view is called uniformly as
86
- # (result, inputs); an inputs-aware view echoes an input such as the key
87
- # (ADR 0067).
88
- def shape(spec, result, inputs)
89
- Textus::Contract::View.render(spec, :cli, result, inputs)
90
- end
91
-
92
- # The default the CLI flag is generated against — `cli_default:` when the
93
- # operator-facing default diverges from the contract default the agent
94
- # surfaces use, else the contract `default`. This drives boolean flag
95
- # polarity so a verb that applies-by-default on the CLI but plans-by-default
96
- # for agents (migrate, zone_mv) gets a `--dry-run` flag, not `--no-dry-run`.
97
- def effective_default(arg)
98
- arg.cli_default == :__unset ? arg.default : arg.cli_default
99
- end
100
-
101
- def flagspec_for(arg)
102
- wire = arg.wire.to_s.tr("_", "-")
103
- if arg.type == :boolean
104
- effective_default(arg) == true ? "--no-#{wire}" : "--#{wire}"
105
- else
106
- "--#{wire}=VALUE"
107
- end
108
- end
109
-
110
- # NB: compare arg.type by equality, not `case`/`===` — `Integer === arg.type`
111
- # is false when arg.type is the Integer *class* (it tests instance-of), so a
112
- # `when Integer` branch would silently never coerce.
113
- def coerce(arg, raw)
114
- return effective_default(arg) != true if arg.type == :boolean
115
- return Integer(raw) if arg.type == Integer
116
-
117
- raw
118
- end
119
-
120
- def ensure_group(name)
121
- const = name.split("_").map(&:capitalize).join
122
- return Group.const_get(const, false) if Group.const_defined?(const, false)
123
-
124
- g = Class.new(Group) { command_name name }
125
- Group.const_set(const, g)
126
- g
127
- end
128
-
129
- # Contract verbs whose CLI behavior is a genuine `< Runner::Base` override
130
- # — behavior the generic projection cannot express (ADR 0068/0069):
131
- # get — raises UnknownKey with resolver suggestions (a CLI-only
132
- # affordance; the agent surface deliberately returns nil)
133
- # put — reads the entry JSON from --stdin (ADR 0089: just stores bytes,
134
- # no --fetch transform)
135
- # (build removed in ADR 0087: materialization is system-pushed via drain/serve)
136
- BEHAVIORAL_HATCHES = %i[get put].freeze
137
-
138
- # Contract verbs whose CLI is a plain `< Verb` command, not a projection at
139
- # all — composite reports assembled outside the contract.
140
- # (boot removed: its contract carries surfaces :cli + the :lean arg, so the
141
- # generic projection now generates it; the hand-authored CLI::Verb::Boot is
142
- # deleted in ADR 0101.)
143
- # (doctor retained: hand-authored to preserve --check=NAME flag spelling and
144
- # the exit_code: res["ok"] ? 0 : 1 behavior — two things the generic
145
- # projection cannot yet express; kept in ADR 0101 pending a future pass.)
146
- # (fetch/fetch_all were removed in ADR 0079: Produce::Acquire::Intake is now internal,
147
- # driven by the converge sweep (drain/serve) and hook run — ADR 0089 removed the
148
- # read-through that once also drove it.)
149
- NON_PROJECTED_CLI = %i[doctor].freeze
150
-
151
- # The installer skips generation for either category.
152
- HAND_AUTHORED_VERBS = (BEHAVIORAL_HATCHES + NON_PROJECTED_CLI).freeze
153
-
154
- def hand_authored?(verb) = HAND_AUTHORED_VERBS.include?(verb)
155
-
156
- def install!
157
- @installed ||= {}
158
- Textus::Dispatcher::VERBS.each_value do |klass|
159
- next unless klass.respond_to?(:contract?) && klass.contract?
160
-
161
- spec = klass.contract
162
- next unless spec.cli?
163
- next if hand_authored?(spec.verb)
164
- next if @installed[spec.verb]
165
-
166
- install_for(spec)
167
- @installed[spec.verb] = true
168
- end
169
- end
170
-
171
- def install_for(spec)
172
- group = spec.cli_group ? ensure_group(spec.cli_group) : nil
173
- leaf = spec.cli_leaf
174
- non_positional = spec.args.reject(&:positional)
175
-
176
- klass = Class.new(Base)
177
- klass.spec = spec
178
- klass.command_name leaf
179
- klass.parent_group group if group
180
- klass.option :as_flag, "--as=ROLE"
181
- klass.option :use_stdin, "--stdin" if spec.cli_stdin
182
- non_positional.each { |a| klass.option a.name, Runner.flagspec_for(a) }
183
-
184
- # Anchor the anonymous class to a constant so descendants discovery is
185
- # stable. Name it after the verb under a Generated namespace.
186
- const_name = spec.verb.to_s.split("_").map(&:capitalize).join
187
- gen = "Gen#{const_name}"
188
- Verb.const_set(gen, klass) unless Verb.const_defined?(gen, false)
189
- klass
190
- end
191
- end
192
- end
193
- end
@@ -1,17 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- class Doctor < Verb
5
- command_name "doctor"
6
-
7
- option :checks, "--check=NAME"
8
-
9
- def call(store)
10
- check_list = checks&.split(",")&.map(&:strip)
11
- res = store.doctor(checks: check_list)
12
- emit(res, exit_code: res["ok"] ? 0 : 1)
13
- end
14
- end
15
- end
16
- end
17
- end
@@ -1,18 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- class Get < Runner::Base
5
- self.spec = Textus::Read::Get.contract
6
- option :as_flag, "--as=ROLE"
7
-
8
- def invoke(store)
9
- key = positional.shift or raise UsageError.new("get requires a key")
10
- result = session_for(store).get(key)
11
- raise Textus::UnknownKey.new(key, suggestions: store.manifest.resolver.suggestions_for(key)) if result.nil?
12
-
13
- emit(result.to_h_for_wire)
14
- end
15
- end
16
- end
17
- end
18
- end
@@ -1,48 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- class HookRun < Verb
5
- command_name "run"
6
- parent_group Group::Hook
7
-
8
- def parse(argv)
9
- @raw_argv = argv
10
- end
11
-
12
- def call(store)
13
- name = @raw_argv.shift
14
- raise UsageError.new("hook run requires a name") if name.nil?
15
-
16
- as_flag = nil
17
- args = {}
18
- @raw_argv.each do |tok|
19
- case tok
20
- when /\A--as=(.+)\z/ then as_flag = ::Regexp.last_match(1)
21
- when /\A--output=/ then next
22
- when /\A--format=/ then raise FlagRenamed.new("--format", "--output")
23
- when /\A--([\w-]+)=(.*)\z/ then args[::Regexp.last_match(1)] = ::Regexp.last_match(2)
24
- else
25
- raise UsageError.new("unknown arg to 'hook run #{name}': #{tok}")
26
- end
27
- end
28
-
29
- # Validate --as resolves to a declared role (raises InvalidRole); hook
30
- # run has no role-scoped authority itself, so the result is discarded.
31
- Role.resolve(flag: as_flag, env: ENV, root: store.root)
32
-
33
- begin
34
- Textus::Produce::Acquire::Handler.invoke(
35
- caps: store.container, handler: name, config: {}, args: args, label: "hook run",
36
- )
37
- rescue Textus::Error
38
- raise
39
- rescue StandardError => e
40
- raise UsageError.new("hook run '#{name}' raised: #{e.class}: #{e.message}")
41
- end
42
-
43
- emit({ "action" => name, "ok" => true })
44
- end
45
- end
46
- end
47
- end
48
- end
@@ -1,50 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- class Hooks < Verb
5
- command_name "list"
6
- parent_group Group::Hook
7
-
8
- option :event_filter, "--event=E"
9
-
10
- def call(store) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
11
- subcommand = positional.first
12
- if subcommand
13
- raise UsageError.new("hook requires 'list'") unless subcommand == "list"
14
-
15
- positional.shift
16
- end
17
-
18
- rows = []
19
- Textus::Hooks::Catalog::RPC.each_key do |event|
20
- store.rpc.names(event).each do |name|
21
- rows << { "event" => event.to_s, "mode" => "rpc", "name" => name.to_s }
22
- end
23
- end
24
- Textus::Hooks::Catalog::PUBSUB.each_key do |event|
25
- store.events.pubsub_handlers(event).each do |h|
26
- row = { "event" => event.to_s, "mode" => "pubsub", "name" => h[:name].to_s }
27
- row["keys"] = Array(h[:keys]) if h[:keys]
28
- rows << row
29
- end
30
- end
31
- store.manifest.data.entries.each do |e|
32
- (e.respond_to?(:events) ? e.events : {}).each do |evt, defs|
33
- Array(defs).each do |defn|
34
- next unless defn["exec"]
35
-
36
- rows << {
37
- "event" => evt.to_s, "mode" => "manifest", "exec" => defn["exec"],
38
- "key" => e.key, "as" => defn["as"] || Textus::Role::AUTOMATION
39
- }
40
- end
41
- end
42
- end
43
- rows.select! { |r| r["event"] == event_filter } if event_filter
44
-
45
- emit({ "hooks" => rows })
46
- end
47
- end
48
- end
49
- end
50
- end
@@ -1,18 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- class Init < Verb
5
- command_name "init"
6
-
7
- option :with_agent, "--with-agent"
8
-
9
- def self.needs_store? = false
10
-
11
- def call(_store)
12
- target = File.join(@cwd, ".textus")
13
- emit(Textus::Init.run(target, with_agent: !!with_agent))
14
- end
15
- end
16
- end
17
- end
18
- end
@@ -1,22 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- # Launches the MCP stdio server in the current process. Blocks on stdin;
5
- # never returns until stdin closes. The connection acts as the `agent`
6
- # role by default (ADR 0040): the agent channel proposes, it does not
7
- # inherit the human's authority. Override per connection with --as, or
8
- # TEXTUS_ROLE / .textus/role (same chain as every other verb).
9
- class MCPServe < Verb
10
- command_name "serve"
11
- parent_group Group::MCP
12
- option :as_flag, "--as=ROLE"
13
-
14
- def call(store)
15
- role = resolved_role(store, default: Textus::Role::AGENT)
16
- Textus::MCP::Server.new(store: store, stdin: @stdin, stdout: @stdout, role: role).run
17
- 0
18
- end
19
- end
20
- end
21
- end
22
- end
@@ -1,30 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- class Put < Runner::Base
5
- self.spec = Textus::Write::Put.contract
6
-
7
- option :as_flag, "--as=ROLE"
8
- option :use_stdin, "--stdin"
9
-
10
- def invoke(store)
11
- key = positional.shift or raise UsageError.new("put requires a key")
12
- raise UsageError.new("put requires --stdin in v1") unless use_stdin
13
-
14
- role = resolved_role(store)
15
-
16
- # put only stores the stdin JSON (ADR 0089): no transform-on-write.
17
- # Ingest (running a handler over bytes) is system-pushed via drain/serve
18
- # and hook run, never a put flag.
19
- payload = JSON.parse(@stdin.read)
20
-
21
- meta = payload["_meta"] || {}
22
- body = payload["body"] || ""
23
- if_etag = payload["if_etag"]
24
- result = store.as(role).put(key, meta: meta, body: body, if_etag: if_etag)
25
- emit(result.to_h_for_wire)
26
- end
27
- end
28
- end
29
- end
30
- end
@@ -1,15 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- class SchemaDiff < Verb
5
- command_name "diff"
6
- parent_group Group::Schema
7
-
8
- def call(store)
9
- name = positional.shift or raise UsageError.new("schema diff NAME")
10
- emit(Textus::Schema::Tools.diff(store, name: name))
11
- end
12
- end
13
- end
14
- end
15
- end
@@ -1,19 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- class SchemaInit < Verb
5
- command_name "init"
6
- parent_group Group::Schema
7
-
8
- option :from_key, "--from=KEY"
9
-
10
- def call(store)
11
- name = positional.shift or raise UsageError.new("schema init NAME")
12
- raise UsageError.new("schema init requires --from=KEY") unless from_key
13
-
14
- emit(Textus::Schema::Tools.init(store, name: name, from: from_key))
15
- end
16
- end
17
- end
18
- end
19
- end
@@ -1,19 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- class SchemaMigrate < Verb
5
- command_name "migrate"
6
- parent_group Group::Schema
7
-
8
- option :rename, "--rename=O:N"
9
-
10
- def call(store)
11
- name = positional.shift or raise UsageError.new("schema migrate NAME")
12
- raise UsageError.new("schema migrate requires --rename=OLD:NEW") unless rename
13
-
14
- emit(Textus::Schema::Tools.migrate(store, name: name, rename: rename))
15
- end
16
- end
17
- end
18
- end
19
- end
@@ -1,19 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- # Launches the convergence daemon in the current process. Blocks forever;
5
- # reclaims crashed leases and drains the queue each tick (Phase 3 adds
6
- # scheduled TTL re-pull/sweep). CLI-only — agents enqueue work, they do not
7
- # run daemons. Acts as the automation role (the build authority).
8
- class Serve < Verb
9
- command_name "serve"
10
-
11
- def call(store)
12
- call = Textus::Call.build(role: Textus::Role::AUTOMATION)
13
- Textus::Maintenance::Serve.new(container: store.container, call: call).run
14
- 0
15
- end
16
- end
17
- end
18
- end
19
- end