textus 0.55.2 → 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 (338) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +175 -2768
  3. data/README.md +8 -9
  4. data/lib/textus/artifact.rb +8 -0
  5. data/lib/textus/errors.rb +0 -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/{dispatch → protocol}/binder.rb +1 -7
  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/{store → protocol}/freshness/ttl_evaluator.rb +7 -3
  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 → protocol}/layout.rb +16 -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 +22 -21
  129. data/lib/textus/surface/cli/sources.rb +1 -3
  130. data/lib/textus/surface/cli/verb/get.rb +2 -7
  131. data/lib/textus/surface/cli/verb/init.rb +1 -1
  132. data/lib/textus/surface/cli/verb/put.rb +7 -7
  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 +12 -12
  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/server.rb +35 -24
  141. data/lib/textus/surface/watcher.rb +18 -12
  142. data/lib/textus/surface/web.rb +50 -0
  143. data/lib/textus/value/call.rb +1 -1
  144. data/lib/textus/value/etag.rb +7 -7
  145. data/lib/textus/version.rb +1 -1
  146. data/lib/textus/workflow/consumer.rb +39 -0
  147. data/lib/textus/workflow/dsl/lifecycle.rb +32 -0
  148. data/lib/textus/workflow/dsl/pattern_matcher.rb +20 -0
  149. data/lib/textus/workflow/dsl/step_builder.rb +56 -0
  150. data/lib/textus/workflow/dsl.rb +111 -21
  151. data/lib/textus/workflow/event_emitter.rb +21 -0
  152. data/lib/textus/workflow/publisher.rb +59 -0
  153. data/lib/textus/workflow/registry.rb +31 -8
  154. data/lib/textus/workflow/runner.rb +25 -53
  155. data/lib/textus/workflow/scheduler.rb +45 -0
  156. data/lib/textus/workflow/step_executor.rb +78 -0
  157. data/lib/textus/workflow/step_helpers.rb +102 -0
  158. data/lib/textus/workflow.rb +2 -5
  159. data/lib/textus.rb +16 -16
  160. metadata +169 -185
  161. data/SPEC.md +0 -909
  162. data/docs/architecture/README.md +0 -273
  163. data/docs/reference/conventions.md +0 -141
  164. data/lib/textus/boot.rb +0 -181
  165. data/lib/textus/dispatch/contracts.rb +0 -63
  166. data/lib/textus/dispatch/handler_registry.rb +0 -21
  167. data/lib/textus/dispatch/middleware/audit_index.rb +0 -51
  168. data/lib/textus/dispatch/middleware/auth.rb +0 -40
  169. data/lib/textus/dispatch/middleware/base.rb +0 -26
  170. data/lib/textus/dispatch/middleware/binder.rb +0 -20
  171. data/lib/textus/dispatch/middleware/cascade.rb +0 -53
  172. data/lib/textus/dispatch/pipeline.rb +0 -35
  173. data/lib/textus/doctor/check/audit_log.rb +0 -34
  174. data/lib/textus/doctor/check/generator_drift.rb +0 -29
  175. data/lib/textus/doctor/check/illegal_keys.rb +0 -60
  176. data/lib/textus/doctor/check/manifest_files.rb +0 -25
  177. data/lib/textus/doctor/check/orphaned_publish_targets.rb +0 -35
  178. data/lib/textus/doctor/check/proposal_targets.rb +0 -45
  179. data/lib/textus/doctor/check/protocol_version.rb +0 -47
  180. data/lib/textus/doctor/check/publish_tree_index_overlap.rb +0 -48
  181. data/lib/textus/doctor/check/raw_asset_paths.rb +0 -50
  182. data/lib/textus/doctor/check/rule_ambiguity.rb +0 -49
  183. data/lib/textus/doctor/check/schema_parse_error.rb +0 -28
  184. data/lib/textus/doctor/check/schema_violations.rb +0 -28
  185. data/lib/textus/doctor/check/schemas.rb +0 -26
  186. data/lib/textus/doctor/check/scratchpad_sources.rb +0 -55
  187. data/lib/textus/doctor/check/sentinels.rb +0 -60
  188. data/lib/textus/doctor/check/stale_reviewed_stamp.rb +0 -54
  189. data/lib/textus/doctor/check/templates.rb +0 -29
  190. data/lib/textus/doctor/check/unowned_schema_fields.rb +0 -40
  191. data/lib/textus/doctor/check.rb +0 -43
  192. data/lib/textus/doctor/validator.rb +0 -93
  193. data/lib/textus/doctor.rb +0 -54
  194. data/lib/textus/format/base.rb +0 -77
  195. data/lib/textus/format/json.rb +0 -81
  196. data/lib/textus/format/markdown.rb +0 -59
  197. data/lib/textus/format/text.rb +0 -55
  198. data/lib/textus/format/yaml.rb +0 -104
  199. data/lib/textus/format.rb +0 -61
  200. data/lib/textus/handlers/maintenance/boot_store.rb +0 -15
  201. data/lib/textus/handlers/maintenance/doctor_store.rb +0 -15
  202. data/lib/textus/handlers/maintenance/drain_store.rb +0 -21
  203. data/lib/textus/handlers/maintenance/ingest_entry.rb +0 -159
  204. data/lib/textus/handlers/maintenance/jobs_action.rb +0 -21
  205. data/lib/textus/handlers/maintenance/published_entries.rb +0 -17
  206. data/lib/textus/handlers/maintenance/rule_explain.rb +0 -77
  207. data/lib/textus/handlers/maintenance/rule_lint.rb +0 -54
  208. data/lib/textus/handlers/maintenance/rule_list.rb +0 -32
  209. data/lib/textus/handlers/maintenance/schema_envelope.rb +0 -19
  210. data/lib/textus/handlers/read/audit_entries.rb +0 -48
  211. data/lib/textus/handlers/read/blame_entry.rb +0 -71
  212. data/lib/textus/handlers/read/deps_entry.rb +0 -17
  213. data/lib/textus/handlers/read/get_entry.rb +0 -68
  214. data/lib/textus/handlers/read/list_keys.rb +0 -36
  215. data/lib/textus/handlers/read/pulse_entries.rb +0 -66
  216. data/lib/textus/handlers/read/rdeps_entry.rb +0 -21
  217. data/lib/textus/handlers/read/uid_entry.rb +0 -18
  218. data/lib/textus/handlers/read/where_entry.rb +0 -18
  219. data/lib/textus/handlers/write/accept_proposal.rb +0 -39
  220. data/lib/textus/handlers/write/data_mv.rb +0 -55
  221. data/lib/textus/handlers/write/delete_key.rb +0 -17
  222. data/lib/textus/handlers/write/enqueue_job.rb +0 -27
  223. data/lib/textus/handlers/write/key_delete_prefix.rb +0 -32
  224. data/lib/textus/handlers/write/key_mv_prefix.rb +0 -45
  225. data/lib/textus/handlers/write/move_key.rb +0 -80
  226. data/lib/textus/handlers/write/propose_entry.rb +0 -29
  227. data/lib/textus/handlers/write/put_entry.rb +0 -29
  228. data/lib/textus/handlers/write/reject_proposal.rb +0 -29
  229. data/lib/textus/init.rb +0 -157
  230. data/lib/textus/jobs.rb +0 -9
  231. data/lib/textus/key/distance.rb +0 -55
  232. data/lib/textus/key/grammar.rb +0 -33
  233. data/lib/textus/key/matching.rb +0 -24
  234. data/lib/textus/key/path.rb +0 -28
  235. data/lib/textus/manifest/capabilities.rb +0 -29
  236. data/lib/textus/manifest/data.rb +0 -102
  237. data/lib/textus/manifest/entry/base.rb +0 -105
  238. data/lib/textus/manifest/entry/ignore_matcher.rb +0 -46
  239. data/lib/textus/manifest/entry/leaf.rb +0 -17
  240. data/lib/textus/manifest/entry/nested.rb +0 -37
  241. data/lib/textus/manifest/entry/parser.rb +0 -99
  242. data/lib/textus/manifest/entry/produced.rb +0 -36
  243. data/lib/textus/manifest/entry/publish/mode.rb +0 -45
  244. data/lib/textus/manifest/entry/publish/none.rb +0 -14
  245. data/lib/textus/manifest/entry/publish/subtree_mirror.rb +0 -73
  246. data/lib/textus/manifest/entry/publish/template.rb +0 -16
  247. data/lib/textus/manifest/entry/publish/to_paths.rb +0 -76
  248. data/lib/textus/manifest/entry/publish/tree.rb +0 -50
  249. data/lib/textus/manifest/entry/publish.rb +0 -45
  250. data/lib/textus/manifest/entry/validators/format_matrix.rb +0 -21
  251. data/lib/textus/manifest/entry/validators/ignore.rb +0 -28
  252. data/lib/textus/manifest/entry/validators/publish.rb +0 -30
  253. data/lib/textus/manifest/entry/validators.rb +0 -18
  254. data/lib/textus/manifest/entry.rb +0 -8
  255. data/lib/textus/manifest/policy/matcher.rb +0 -51
  256. data/lib/textus/manifest/policy/predicates/author_held.rb +0 -22
  257. data/lib/textus/manifest/policy/predicates/etag_match.rb +0 -18
  258. data/lib/textus/manifest/policy/predicates/fresh_within.rb +0 -13
  259. data/lib/textus/manifest/policy/predicates/lane_deletable_by.rb +0 -31
  260. data/lib/textus/manifest/policy/predicates/lane_writable_by.rb +0 -23
  261. data/lib/textus/manifest/policy/predicates/raw_lane_ingest_only.rb +0 -25
  262. data/lib/textus/manifest/policy/predicates/raw_write_once.rb +0 -24
  263. data/lib/textus/manifest/policy/predicates/schema_valid.rb +0 -41
  264. data/lib/textus/manifest/policy/predicates/target_is_canon.rb +0 -20
  265. data/lib/textus/manifest/policy/predicates.rb +0 -54
  266. data/lib/textus/manifest/policy/publish_target.rb +0 -34
  267. data/lib/textus/manifest/policy/react.rb +0 -30
  268. data/lib/textus/manifest/policy/retention.rb +0 -29
  269. data/lib/textus/manifest/policy/source.rb +0 -30
  270. data/lib/textus/manifest/policy.rb +0 -95
  271. data/lib/textus/manifest/resolver.rb +0 -118
  272. data/lib/textus/manifest/rules.rb +0 -86
  273. data/lib/textus/manifest/schema/contract.rb +0 -61
  274. data/lib/textus/manifest/schema/keys.rb +0 -90
  275. data/lib/textus/manifest/schema/semantics/cross_field.rb +0 -53
  276. data/lib/textus/manifest/schema/semantics/invariants.rb +0 -125
  277. data/lib/textus/manifest/schema/semantics/migration.rb +0 -83
  278. data/lib/textus/manifest/schema/semantics.rb +0 -27
  279. data/lib/textus/manifest/schema/validator.rb +0 -48
  280. data/lib/textus/manifest/schema/vocabulary.rb +0 -25
  281. data/lib/textus/manifest/schema.rb +0 -30
  282. data/lib/textus/manifest.rb +0 -65
  283. data/lib/textus/orchestration.rb +0 -55
  284. data/lib/textus/port/audit_log.rb +0 -268
  285. data/lib/textus/port/build_lock.rb +0 -64
  286. data/lib/textus/port/clock.rb +0 -10
  287. data/lib/textus/port/publisher.rb +0 -60
  288. data/lib/textus/port/sentinel.rb +0 -25
  289. data/lib/textus/port/sentinel_store.rb +0 -83
  290. data/lib/textus/port/storage/file_stat.rb +0 -19
  291. data/lib/textus/port/storage/file_store.rb +0 -49
  292. data/lib/textus/port/storage/interface.rb +0 -17
  293. data/lib/textus/port/store.rb +0 -149
  294. data/lib/textus/port/watcher_lock.rb +0 -48
  295. data/lib/textus/produce/engine.rb +0 -40
  296. data/lib/textus/produce/publisher.rb +0 -21
  297. data/lib/textus/produce/render.rb +0 -20
  298. data/lib/textus/schema/registry.rb +0 -42
  299. data/lib/textus/schema/tools.rb +0 -108
  300. data/lib/textus/schema.rb +0 -100
  301. data/lib/textus/store/container.rb +0 -173
  302. data/lib/textus/store/cursor.rb +0 -26
  303. data/lib/textus/store/entry/reader.rb +0 -47
  304. data/lib/textus/store/entry/writer.rb +0 -219
  305. data/lib/textus/store/envelope/meta.rb +0 -61
  306. data/lib/textus/store/freshness/drift_detector.rb +0 -93
  307. data/lib/textus/store/freshness/evaluator.rb +0 -20
  308. data/lib/textus/store/freshness/verdict.rb +0 -24
  309. data/lib/textus/store/freshness.rb +0 -8
  310. data/lib/textus/store/index/builder.rb +0 -76
  311. data/lib/textus/store/index/lookup.rb +0 -60
  312. data/lib/textus/store/jobs/base.rb +0 -13
  313. data/lib/textus/store/jobs/index.rb +0 -15
  314. data/lib/textus/store/jobs/materialize.rb +0 -15
  315. data/lib/textus/store/jobs/plan.rb +0 -11
  316. data/lib/textus/store/jobs/planner.rb +0 -124
  317. data/lib/textus/store/jobs/queue.rb +0 -162
  318. data/lib/textus/store/jobs/registry.rb +0 -19
  319. data/lib/textus/store/jobs/retention/base.rb +0 -52
  320. data/lib/textus/store/jobs/retention/sweep.rb +0 -55
  321. data/lib/textus/store/jobs/retention.rb +0 -8
  322. data/lib/textus/store/jobs/sweep.rb +0 -21
  323. data/lib/textus/store/jobs/worker.rb +0 -64
  324. data/lib/textus/store.rb +0 -122
  325. data/lib/textus/surface/cli/verb/doctor.rb +0 -20
  326. data/lib/textus/surface/cli/verb/schema_diff.rb +0 -17
  327. data/lib/textus/surface/cli/verb/schema_init.rb +0 -21
  328. data/lib/textus/surface/cli/verb/schema_migrate.rb +0 -21
  329. data/lib/textus/surface/mcp/projector.rb +0 -20
  330. data/lib/textus/surface/mcp.rb +0 -8
  331. data/lib/textus/value/command.rb +0 -16
  332. data/lib/textus/value/envelope.rb +0 -90
  333. data/lib/textus/value/result.rb +0 -46
  334. data/lib/textus/verb_registry.rb +0 -417
  335. data/lib/textus/workflow/collector.rb +0 -27
  336. data/lib/textus/workflow/context.rb +0 -5
  337. data/lib/textus/workflow/loader.rb +0 -17
  338. data/lib/textus/workflow/pattern.rb +0 -18
@@ -1,80 +0,0 @@
1
- module Textus
2
- module Handlers
3
- module Write
4
- class MoveKey
5
- def initialize(container:, manifest:)
6
- @container = container
7
- @manifest = manifest
8
- end
9
-
10
- def call(command, call)
11
- Textus::Manifest::Data.validate_key!(command.old_key)
12
- Textus::Manifest::Data.validate_key!(command.new_key)
13
-
14
- return Value::Result.failure(:usage_error, "mv: old and new keys are identical") if command.old_key == command.new_key
15
-
16
- old_res = @manifest.resolver.resolve(command.old_key)
17
- new_res = @manifest.resolver.resolve(command.new_key)
18
-
19
- reader = Store::Entry::Reader.from(container: @container)
20
-
21
- unless reader.exists?(command.old_key)
22
- return Value::Result.failure(:not_found,
23
- "source key '#{command.old_key}' not found")
24
- end
25
-
26
- zone_check = validate_zone(old_res.entry, new_res.entry)
27
- return zone_check if zone_check
28
-
29
- if reader.exists?(command.new_key)
30
- return Value::Result.failure(:usage_error, "mv: target '#{command.new_key}' already exists at #{new_res.path}")
31
- end
32
-
33
- pre_env = reader.read(command.old_key)
34
- writer = Store::Entry::Writer.from(container: @container, call: call)
35
- unless pre_env.uid
36
- writer.put(
37
- command.old_key, mentry: old_res.entry,
38
- payload: Textus::Value::Payload.new(meta: pre_env.meta, body: pre_env.body, content: pre_env.content)
39
- )
40
- end
41
-
42
- if command.dry_run
43
- return Value::Result.success({
44
- "protocol" => Textus::PROTOCOL, "ok" => true, "dry_run" => true,
45
- "from_key" => command.old_key, "to_key" => command.new_key,
46
- "from_path" => old_res.path, "to_path" => new_res.path,
47
- "uid" => pre_env.uid
48
- })
49
- end
50
-
51
- envelope = writer.move(
52
- from_key: command.old_key, to_key: command.new_key,
53
- new_mentry: new_res.entry
54
- )
55
-
56
- Value::Result.success({
57
- "protocol" => Textus::PROTOCOL, "ok" => true,
58
- "from_key" => command.old_key, "to_key" => command.new_key,
59
- "from_path" => old_res.path, "to_path" => new_res.path,
60
- "uid" => envelope.uid, "envelope" => envelope.to_h_for_wire
61
- })
62
- end
63
-
64
- private
65
-
66
- def validate_zone(old_mentry, new_mentry)
67
- if old_mentry.lane != new_mentry.lane
68
- return Value::Result.failure(:usage_error,
69
- "mv: cross-zone refused (#{old_mentry.lane} -> #{new_mentry.lane}). Use put+delete.")
70
- end
71
- if old_mentry.format != new_mentry.format
72
- return Value::Result.failure(:usage_error,
73
- "mv: format mismatch (#{old_mentry.format} -> #{new_mentry.format}); refusing.")
74
- end
75
- nil
76
- end
77
- end
78
- end
79
- end
80
- end
@@ -1,29 +0,0 @@
1
- module Textus
2
- module Handlers
3
- module Write
4
- class ProposeEntry
5
- def initialize(container:)
6
- @container = container
7
- end
8
-
9
- def call(command, call)
10
- zone = @container.manifest.policy.propose_lane_for(call.role)
11
- unless zone
12
- return Value::Result.failure(:propose_forbidden,
13
- "role '#{call.role}' has no writable propose_lane",
14
- details: { "role" => call.role })
15
- end
16
-
17
- key = "#{zone}.#{command.key}"
18
- mentry = @container.manifest.resolver.resolve(key).entry
19
- writer = Store::Entry::Writer.from(container: @container, call: call)
20
- envelope = writer.put(
21
- key, mentry: mentry,
22
- payload: Textus::Value::Payload.new(meta: command.meta || {}, body: command.body, content: command.content)
23
- )
24
- Value::Result.success(envelope)
25
- end
26
- end
27
- end
28
- end
29
- end
@@ -1,29 +0,0 @@
1
- module Textus
2
- module Handlers
3
- module Write
4
- class PutEntry
5
- def initialize(container:)
6
- @container = container
7
- end
8
-
9
- def call(command, call)
10
- Textus::Manifest::Data.validate_key!(command.key)
11
- mentry = @container.manifest.resolver.resolve(command.key).entry
12
-
13
- writer = Store::Entry::Writer.from(container: @container, call: call)
14
- envelope = writer.put(
15
- command.key,
16
- mentry: mentry,
17
- payload: Textus::Value::Payload.new(
18
- meta: command.meta,
19
- body: command.body,
20
- content: command.content,
21
- ),
22
- if_etag: command.if_etag,
23
- )
24
- Value::Result.success(envelope)
25
- end
26
- end
27
- end
28
- end
29
- end
@@ -1,29 +0,0 @@
1
- module Textus
2
- module Handlers
3
- module Write
4
- class RejectProposal
5
- def initialize(container:)
6
- @container = container
7
- end
8
-
9
- def call(command, call)
10
- mentry = @container.manifest.resolver.resolve(command.pending_key).entry
11
- unless mentry.in_proposal_lane?(@container.manifest.policy)
12
- return Value::Result.failure(:proposal_error,
13
- "reject: '#{command.pending_key}' is not in a proposal zone (zone=#{mentry.lane})")
14
- end
15
-
16
- reader = Store::Entry::Reader.from(container: @container)
17
- env = reader.read(command.pending_key)
18
- proposal = env&.meta&.dig("proposal") or
19
- return Value::Result.failure(:proposal_error, "entry has no proposal block: #{command.pending_key}")
20
- target_key = proposal["target_key"]
21
-
22
- writer = Store::Entry::Writer.from(container: @container, call: call)
23
- writer.delete(command.pending_key, mentry: mentry)
24
- Value::Result.success("protocol" => Textus::PROTOCOL, "rejected" => command.pending_key, "target_key" => target_key)
25
- end
26
- end
27
- end
28
- end
29
- end
data/lib/textus/init.rb DELETED
@@ -1,157 +0,0 @@
1
- require "fileutils"
2
- require "pathname"
3
-
4
- module Textus
5
- module Init
6
- ZONES = %w[knowledge scratchpad proposals artifacts raw].freeze
7
-
8
- DEFAULT_MANIFEST = <<~YAML
9
- version: textus/4
10
- roles:
11
- - { name: human, can: [author, propose] }
12
- - { name: agent, can: [propose, keep] }
13
- - { name: automation, can: [converge] }
14
- lanes:
15
- - { name: knowledge, kind: canon, desc: "the maintained source of truth (identity.* lives here)" }
16
- - { name: scratchpad, kind: workspace, owner: agent, desc: "the agent's own durable working notes" }
17
- - { name: proposals, kind: queue, desc: "changes awaiting your accept" }
18
- - { name: artifacts, kind: machine, desc: "machine-maintained: external inputs (artifacts.feeds.*) + computed outputs (artifacts.derived.*)" }
19
- entries:
20
- - { key: knowledge.identity, path: data/knowledge/identity.md, lane: knowledge, schema: null, owner: human:self, kind: leaf }
21
- - { key: knowledge.notes, path: data/knowledge/notes, lane: knowledge, schema: null, owner: human:self, nested: true, kind: nested }
22
- - { key: scratchpad.notes, path: data/scratchpad/notes, lane: scratchpad, schema: null, owner: agent:self, nested: true, kind: nested }
23
- - { key: proposals.notes, path: data/proposals/notes, lane: proposals, schema: null, owner: agent:self, nested: true, kind: nested }
24
- # A per-host snapshot, populated by a registered workflow. Nested so it
25
- # grows to a fleet — add leaves over SSH without renaming. tracked:false →
26
- # gitignored (machine info can be sensitive/noisy) but still protocol-readable
27
- # via `textus get artifacts.feeds.machines.local`. Delete to opt out. (ADR 0043)
28
- - key: artifacts.feeds.machines
29
- path: data/artifacts/feeds/machines
30
- lane: artifacts
31
- format: yaml
32
- nested: true
33
- tracked: false
34
- kind: nested
35
- rules: []
36
- YAML
37
-
38
- AGENT_ENTRIES = <<~YAML.gsub(/^/, " ")
39
- - { key: knowledge.project, path: data/knowledge/project.md, lane: knowledge, schema: project, owner: human:self, kind: leaf }
40
- - { key: knowledge.runbooks, path: data/knowledge/runbooks, lane: knowledge, schema: runbook, owner: human:self, nested: true, kind: nested }
41
- - key: artifacts.derived.orientation
42
- path: data/artifacts/derived/orientation.json
43
- lane: artifacts
44
- publish:
45
- - { to: CLAUDE.md, template: orientation.erb, inject_boot: true }
46
- - { to: AGENTS.md, template: orientation.erb, inject_boot: true }
47
- kind: produced
48
- YAML
49
-
50
- def self.run(target_root, with_agent: false)
51
- check_target!(target_root)
52
- scaffold_dir = File.expand_path("init/templates", __dir__)
53
- create_directories(target_root)
54
- write_manifest(target_root, with_agent:)
55
- mcp_status = scaffold_agent(target_root, scaffold_dir, with_agent:)
56
- setup_state_dirs(target_root)
57
- write_gitignore(target_root)
58
- build_result(target_root, with_agent:, mcp_status:)
59
- end
60
-
61
- def self.check_target!(target_root)
62
- raise UsageError.new(".textus/ already exists at #{target_root}") if File.directory?(target_root)
63
- end
64
-
65
- ASSET_ZONES = %w[raw].freeze
66
-
67
- def self.create_directories(target_root)
68
- FileUtils.mkdir_p(File.join(target_root, "schemas"))
69
- FileUtils.mkdir_p(File.join(target_root, "templates"))
70
- FileUtils.mkdir_p(File.join(target_root, "workflows"))
71
- ZONES.each do |z|
72
- dir = File.join(target_root, "data", z)
73
- FileUtils.mkdir_p(dir)
74
- File.write(File.join(dir, ".gitkeep"), "")
75
- end
76
- ASSET_ZONES.each do |z|
77
- dir = File.join(target_root, "assets", z)
78
- FileUtils.mkdir_p(dir)
79
- File.write(File.join(dir, ".gitkeep"), "")
80
- end
81
- end
82
-
83
- def self.write_manifest(target_root, with_agent:)
84
- File.write(File.join(target_root, "manifest.yaml"), manifest_yaml(with_agent: with_agent))
85
- end
86
-
87
- def self.scaffold_agent(target_root, scaffold_dir, with_agent:)
88
- return nil unless with_agent
89
-
90
- scaffold_agent_profile(target_root, scaffold_dir)
91
- write_mcp_config(target_root, scaffold_dir)
92
- end
93
-
94
- def self.setup_state_dirs(target_root)
95
- FileUtils.mkdir_p(Textus::Store::Layout.new(target_root).audit_dir_path)
96
- end
97
-
98
- def self.write_gitignore(target_root)
99
- File.write(File.join(target_root, ".gitignore"), derived_gitignore(target_root))
100
- end
101
-
102
- def self.build_result(target_root, with_agent:, mcp_status:)
103
- result = { "protocol" => PROTOCOL, "initialized" => target_root, "profile" => with_agent ? "agent" : "default" }
104
- result["mcp_config"] = mcp_status if with_agent
105
- result
106
- end
107
-
108
- # Composes the agent profile by inserting AGENT_ENTRIES immediately before the
109
- # top-level `rules:` block of DEFAULT_MANIFEST — that block is load-bearing for
110
- # this `.sub`; removing it from DEFAULT_MANIFEST would silently drop the entries.
111
- def self.manifest_yaml(with_agent:)
112
- return DEFAULT_MANIFEST unless with_agent
113
-
114
- DEFAULT_MANIFEST.sub(/^rules:/, "#{AGENT_ENTRIES}rules:")
115
- end
116
-
117
- # Copies the proven orientation bundle into a freshly-init'd store.
118
- def self.scaffold_agent_profile(target_root, scaffold_dir)
119
- {
120
- "project.schema.yaml" => File.join("schemas", "project.yaml"),
121
- "runbook.schema.yaml" => File.join("schemas", "runbook.yaml"),
122
- "orientation.erb" => File.join("templates", "orientation.erb"),
123
- }.each do |src, dest|
124
- File.write(File.join(target_root, dest), File.read(File.join(scaffold_dir, src)))
125
- end
126
- end
127
-
128
- # The one file init writes outside .textus/: a starter .mcp.json at the
129
- # project root. Write-once — never clobber a hand-authored config. The
130
- # command form assumes a gem-installed `textus` on PATH; the user owns
131
- # the file after this first write.
132
- def self.write_mcp_config(target_root, scaffold_dir)
133
- dest = File.join(File.dirname(target_root), ".mcp.json")
134
- return "skipped" if File.exist?(dest)
135
-
136
- File.write(dest, File.read(File.join(scaffold_dir, "mcp.json")))
137
- "written"
138
- end
139
-
140
- # The store's `.gitignore` is generated, never hand-kept (ADR 0038), and now
141
- # derived from the manifest: the run subtree plus every `tracked: false`
142
- # entry's resolved path (ADR 0043).
143
- def self.derived_gitignore(target_root)
144
- manifest = Textus::Manifest.load(target_root)
145
- root = Pathname.new(target_root)
146
- untracked = manifest.data.entries.reject(&:tracked?).map do |e|
147
- if e.nested? # a whole subtree of leaf files (artifacts.feeds.machines.* → data/artifacts/feeds/machines/)
148
- rel = e.path.start_with?("data/") ? e.path : File.join("data", e.path)
149
- "#{rel}/"
150
- else
151
- Pathname.new(Textus::Key::Path.resolve(manifest.data, e)).relative_path_from(root).to_s
152
- end
153
- end
154
- Textus::Store::Layout.new(target_root).gitignore_body(untracked_entries: untracked)
155
- end
156
- end
157
- end
data/lib/textus/jobs.rb DELETED
@@ -1,9 +0,0 @@
1
- module Textus
2
- module Jobs
3
- def self.fetch(type)
4
- Store::Jobs::Registry.fetch(type)
5
- rescue Store::Jobs::Registry::UnknownJob
6
- raise Textus::UsageError.new("unknown job type: #{type}")
7
- end
8
- end
9
- end
@@ -1,55 +0,0 @@
1
- module Textus
2
- module Key
3
- # Small utilities for ranking key suggestions. Bounded inputs only —
4
- # Levenshtein is O(n*m) so we refuse to compute on long strings.
5
- class Distance
6
- MAX_LEN = 200
7
-
8
- # Length of the shared dot-separated prefix between two dotted keys.
9
- def self.shared_prefix_segments(left, right)
10
- asegs = left.split(".")
11
- bsegs = right.split(".")
12
- n = [asegs.length, bsegs.length].min
13
- i = 0
14
- i += 1 while i < n && asegs[i] == bsegs[i]
15
- i
16
- end
17
-
18
- # Classic iterative Levenshtein with two rows. Bounded to MAX_LEN.
19
- def self.levenshtein(left, right)
20
- return nil if left.length > MAX_LEN || right.length > MAX_LEN
21
- return right.length if left.empty?
22
- return left.length if right.empty?
23
-
24
- prev = (0..right.length).to_a
25
- curr = Array.new(right.length + 1, 0)
26
- (1..left.length).each do |i|
27
- curr[0] = i
28
- (1..right.length).each do |j|
29
- cost = left[i - 1] == right[j - 1] ? 0 : 1
30
- curr[j] = [
31
- curr[j - 1] + 1, # insertion
32
- prev[j] + 1, # deletion
33
- prev[j - 1] + cost, # substitution
34
- ].min
35
- end
36
- prev, curr = curr, prev
37
- end
38
- prev[right.length]
39
- end
40
-
41
- # Rank candidate keys against requested. Returns up to `limit` keys.
42
- # Sort: longer shared prefix first; then smaller Levenshtein distance.
43
- def self.suggest(requested, candidates, limit: 5)
44
- return [] if requested.nil? || requested.empty?
45
-
46
- scored = candidates.first(200).map do |k|
47
- prefix = shared_prefix_segments(requested, k)
48
- dist = levenshtein(requested, k) || Float::INFINITY
49
- [k, prefix, dist]
50
- end
51
- scored.sort_by { |(_, prefix, dist)| [-prefix, dist] }.first(limit).map(&:first)
52
- end
53
- end
54
- end
55
- end
@@ -1,33 +0,0 @@
1
- module Textus
2
- module Key
3
- module Grammar
4
- SEGMENT = /\A[a-z0-9][a-z0-9-]*\z/
5
- MAX_SEGMENTS = 8
6
- MAX_SEGMENT_LEN = 64
7
-
8
- module_function
9
-
10
- def validate!(key) # rubocop:disable Naming/PredicateMethod
11
- raise UsageError.new("key must be a String") unless key.is_a?(String)
12
- raise UsageError.new("empty key") if key.empty?
13
-
14
- segs = key.split(".")
15
- raise UsageError.new("key '#{key}' has #{segs.length} segments (max #{MAX_SEGMENTS})") if segs.length > MAX_SEGMENTS
16
-
17
- segs.each do |seg|
18
- if seg.empty?
19
- raise UsageError.new("empty segment in key '#{key}'")
20
- elsif seg.length > MAX_SEGMENT_LEN
21
- raise UsageError.new("segment '#{seg}' in key '#{key}' exceeds #{MAX_SEGMENT_LEN} chars")
22
- elsif !seg.match?(SEGMENT)
23
- raise UsageError.new(
24
- "invalid key segment '#{seg}' in '#{key}': must match [a-z0-9][a-z0-9-]* " \
25
- "(lowercase, digits, hyphens; no underscores or uppercase)",
26
- )
27
- end
28
- end
29
- true
30
- end
31
- end
32
- end
33
- end
@@ -1,24 +0,0 @@
1
- module Textus
2
- module Key
3
- # Dotted-key scope matching, shared by all prefix-scoped sweeps
4
- # (WS4 / ADR 0089-era cleanup). Canonicalised here so every consumer
5
- # uses a consistent dotted-boundary check with proper Nested ancestor
6
- # handling. ADR 0093: Produce is the sole engine calling this.
7
- module Matching
8
- module_function
9
-
10
- # Is `key` within the `prefix` scope?
11
- # - exact match, or a dotted descendant (the `prefix.` boundary, so
12
- # prefix "art" does NOT match key "artifacts"), and
13
- # - for a nested entry, also when `prefix` descends INTO it — the nested
14
- # parent owns the leaf the prefix names (e.g. prefix
15
- # "feeds.machines.host1" still selects the nested entry
16
- # "feeds.machines").
17
- def matches_prefix?(key, prefix, nested: false)
18
- return true if key == prefix || key.start_with?("#{prefix}.")
19
-
20
- nested && prefix.start_with?("#{key}.")
21
- end
22
- end
23
- end
24
- end
@@ -1,28 +0,0 @@
1
- module Textus
2
- module Key
3
- module Path
4
- # Returns the absolute filesystem path for a manifest entry (the leaf file,
5
- # not a nested directory). Adds the format's primary extension when the
6
- # manifest entry's `path:` is extensionless.
7
- #
8
- # The first argument is a Manifest::Data (or anything responding to .root);
9
- # callers historically passed the whole Manifest but should now pass
10
- # `manifest.data`.
11
- def self.resolve(data, mentry)
12
- primary_ext = Format.for(mentry.format).extensions.first
13
- rel_path = normalize_relative_path(mentry.path)
14
- if File.extname(mentry.path) == ""
15
- File.join(data.root, rel_path + primary_ext)
16
- else
17
- File.join(data.root, rel_path)
18
- end
19
- end
20
-
21
- def self.normalize_relative_path(path)
22
- return path if path.start_with?("data/")
23
-
24
- File.join("data", path)
25
- end
26
- end
27
- end
28
- end
@@ -1,29 +0,0 @@
1
- module Textus
2
- class Manifest
3
- # Resolves a manifest's `roles:` block (or the absence of one) into a
4
- # capability map: { role_name => [verbs] }. Verbs are a subset of the
5
- # closed capability set (Schema::CAPABILITIES). See ADR 0030.
6
- module Capabilities
7
- # Fallback role set for a manifest that omits `roles:` entirely. Agent
8
- # is intentionally minimal here (`propose` only) — narrower than the
9
- # `textus init` scaffold, which declares `agent: [propose, keep]` so the
10
- # default `scratchpad` workspace is writable. A roles-less manifest that
11
- # declares a `kind: workspace` zone is therefore rejected at load (no
12
- # `keep`-holder); declare `roles:` to opt into a workspace lane (ADR 0033).
13
- DEFAULT_MAPPING = {
14
- Textus::Value::Role::HUMAN => %w[author propose].freeze,
15
- Textus::Value::Role::AGENT => %w[propose].freeze,
16
- Textus::Value::Role::AUTOMATION => %w[converge].freeze,
17
- }.freeze
18
-
19
- # Returns { role_name => [verbs] }. When `roles:` is declared we use
20
- # exactly that; defaults are *not* layered in (declaring roles is an
21
- # opt-in to a fully user-defined vocabulary).
22
- def self.resolve(raw_roles)
23
- return DEFAULT_MAPPING if raw_roles.nil?
24
-
25
- raw_roles.to_h { |r| [r["name"], Array(r["can"]).freeze] }.freeze
26
- end
27
- end
28
- end
29
- end
@@ -1,102 +0,0 @@
1
- require_relative "schema"
2
- require_relative "capabilities"
3
-
4
- module Textus
5
- class Manifest
6
- # Immutable, parsed view of a manifest YAML document.
7
- #
8
- # Holds raw structural data (lanes, entries, audit_config, role_caps)
9
- # but no behaviour beyond accessors. Behaviour (lane authority, key
10
- # resolution, rules) lives on Manifest::Policy / Resolver / Rules.
11
- class Data
12
- AUDIT_DEFAULTS = { max_size: 10_485_760, keep: 5 }.freeze
13
- WORKER_DEFAULTS = { pool: 4, poll: 5, lease_ttl: 60, max_attempts: 3 }.freeze
14
-
15
- attr_reader :raw, :root, :entries, :declared_lane_kinds,
16
- :lane_descs, :lane_owners,
17
- :audit_config, :worker_config, :role_caps, :policy
18
-
19
- def self.validate_key!(key)
20
- raise UsageError.new("empty key") if key.nil? || key.empty?
21
-
22
- Key::Grammar.validate!(key)
23
- end
24
-
25
- # Forwarder used by Resolver and Entry classes that received a Data
26
- # but were written against the historical Manifest API.
27
- def validate_key!(key) = self.class.validate_key!(key)
28
-
29
- def self.parse(raw, root:)
30
- raise BadFrontmatter.new(File.join(root.to_s, "manifest.yaml"), "manifest must declare lanes:") if Array(raw["lanes"]).empty?
31
-
32
- Schema.validate!(raw)
33
- new(raw: raw, root: root)
34
- end
35
-
36
- def initialize(raw:, root:)
37
- @raw = raw
38
- @root = root
39
- # Write authority is derived from capabilities × lane-kind (ADR 0030),
40
- # not a per-lane writer list. "Which lanes are declared" lives in the
41
- # one kind-keyed map below (declared_lane_kinds); membership checks by
42
- # read-side callers (boot, maintenance/data_mv) use its keyset (ADR 0034).
43
- @declared_lane_kinds = Array(raw["lanes"]).to_h do |z|
44
- [z["name"], z["kind"]&.to_sym]
45
- end
46
- @lane_descs = Array(raw["lanes"]).to_h { |z| [z["name"], z["desc"]] }
47
- # Only lanes that actually declare an owner — keep nil-tombstones out so a
48
- # future `lane_owners.key?(name)` means "owner declared", not "lane exists".
49
- @lane_owners = Array(raw["lanes"]).to_h { |z| [z["name"], z["owner"]] }.compact
50
- @audit_config = build_audit_config(raw)
51
- @worker_config = build_worker_config(raw)
52
- @role_caps = Capabilities.resolve(raw["roles"])
53
- # Policy is constructed before entries because Entry validators
54
- # use the entry's own `derived?` and similar helpers that call into
55
- # Policy; Policy must exist before entries are built.
56
- @policy = Policy.new(self)
57
- @entries = build_entries(raw)
58
- validate_declared_keys!
59
- freeze
60
- end
61
-
62
- private
63
-
64
- def build_audit_config(raw)
65
- a = raw["audit"] || {}
66
- {
67
- max_size: a["max_size"] || AUDIT_DEFAULTS[:max_size],
68
- keep: a["keep"] || AUDIT_DEFAULTS[:keep],
69
- }.freeze
70
- end
71
-
72
- # Worker/queue tunables (ADR: job-queue execution model). All optional;
73
- # the daemon (serve) and batch drain read these, falling back to defaults
74
- # so a manifest with no `worker:` block runs the queue out of the box.
75
- def build_worker_config(raw)
76
- w = raw["worker"] || {}
77
- {
78
- pool: w["pool"] || WORKER_DEFAULTS[:pool],
79
- poll: w["poll"] || WORKER_DEFAULTS[:poll],
80
- lease_ttl: w["lease_ttl"] || WORKER_DEFAULTS[:lease_ttl],
81
- max_attempts: w["max_attempts"] || WORKER_DEFAULTS[:max_attempts],
82
- }.freeze
83
- end
84
-
85
- def build_entries(raw)
86
- Array(raw["entries"]).map do |e|
87
- entry = Manifest::Entry::Parser.call(e)
88
- Manifest::Entry::Validators.run_all(entry, policy: @policy)
89
- entry
90
- end.freeze
91
- end
92
-
93
- def validate_declared_keys!
94
- @entries.each do |e|
95
- raise UsageError.new("empty key") if e.key.nil? || e.key.empty?
96
-
97
- Key::Grammar.validate!(e.key)
98
- end
99
- end
100
- end
101
- end
102
- end