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,51 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Dispatch
5
- module Middleware
6
- # Shadows successful write operations into the SQLite audit_events table
7
- # synchronously after dispatch, without the command knowing. Read verbs
8
- # and failed writes pass through unchanged.
9
- class AuditIndex < Base
10
- middleware_name :audit_index
11
-
12
- INDEXED_CONTRACTS = [
13
- Contracts::PutEntry,
14
- Contracts::DeleteKey,
15
- Contracts::MoveKey,
16
- ].freeze
17
-
18
- def initialize(job_store:, audit_log:)
19
- super()
20
- @job_store = job_store
21
- @audit_log = audit_log
22
- end
23
-
24
- def call(container:, command:, call:, next_handler:) # rubocop:disable Lint/UnusedMethodArgument
25
- result = next_handler.call(command, call)
26
- return result unless result.success? && INDEXED_CONTRACTS.include?(command.class)
27
-
28
- key = command.respond_to?(:key) ? command.key : nil
29
- return result unless key
30
-
31
- seq = @audit_log.latest_seq
32
- verb = command.class.name.split("::").last
33
- .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
34
- .gsub(/([a-z\d])([A-Z])/, '\1_\2')
35
- .downcase
36
-
37
- @job_store.insert_audit_event(
38
- seq: seq,
39
- ts: Time.now.utc.iso8601,
40
- role: call.role,
41
- verb: verb,
42
- key: key,
43
- etag_before: nil,
44
- etag_after: result.value.is_a?(Hash) ? result.value["etag"] : nil,
45
- )
46
- result
47
- end
48
- end
49
- end
50
- end
51
- end
@@ -1,40 +0,0 @@
1
- module Textus
2
- module Dispatch
3
- module Middleware
4
- class Auth < Base
5
- middleware_name :auth
6
-
7
- def call(container:, command:, call:, next_handler:)
8
- verb = VerbRegistry.contract_to_verb!(command.class).to_sym
9
- key = key_for(command)
10
-
11
- rule_preds = key ? rule_declared_predicates(verb, container.manifest, key) : []
12
-
13
- Manifest::Policy::Predicates.evaluate(
14
- manifest: container.manifest, schemas: container.schemas,
15
- action: verb, actor: call.role, key: key,
16
- rule_predicates: rule_preds
17
- )
18
-
19
- next_handler.call(command, call)
20
- end
21
-
22
- private
23
-
24
- def rule_declared_predicates(verb, manifest, key)
25
- guard_map = manifest.rules.for(key).guard
26
- return [] if guard_map.nil?
27
-
28
- Array(guard_map[verb.to_s])
29
- end
30
-
31
- def key_for(command)
32
- if command.respond_to?(:key) then command.key
33
- elsif command.respond_to?(:old_key) then command.old_key
34
- elsif command.respond_to?(:pending_key) then command.pending_key
35
- end
36
- end
37
- end
38
- end
39
- end
40
- end
@@ -1,26 +0,0 @@
1
- module Textus
2
- module Dispatch
3
- module Middleware
4
- class Base
5
- def self.inherited(subclass)
6
- super
7
- subclass.instance_variable_set(:@middleware_name, nil)
8
- end
9
-
10
- class << self
11
- def middleware_name(name = nil)
12
- if name
13
- @middleware_name = name.to_s
14
- else
15
- @middleware_name || name.split("::").last.downcase
16
- end
17
- end
18
- end
19
-
20
- def call(container:, command:, call:, next_handler:)
21
- raise NotImplementedError
22
- end
23
- end
24
- end
25
- end
26
- end
@@ -1,20 +0,0 @@
1
- module Textus
2
- module Dispatch
3
- module Middleware
4
- class Binder < Base
5
- middleware_name :binder
6
-
7
- def call(container:, command:, call:, next_handler:) # rubocop:disable Lint/UnusedMethodArgument
8
- return next_handler.call(command, call) unless command.is_a?(Dispatch::Binder::Pending)
9
-
10
- spec = command.spec
11
- contract_class = VerbRegistry.contract_class_for(spec.verb) or
12
- raise Textus::UsageError.new("unknown command verb: #{spec.verb}")
13
- resolved = Dispatch::Binder.bind(spec, command.inputs)
14
- built = Dispatch::Pipeline.build_command(contract_class, resolved)
15
- next_handler.call(built, call)
16
- end
17
- end
18
- end
19
- end
20
- end
@@ -1,53 +0,0 @@
1
- module Textus
2
- module Dispatch
3
- module Middleware
4
- class Cascade < Base
5
- middleware_name :cascade
6
-
7
- CASCADE_VERBS = %i[put propose accept reject key_mv key_delete].freeze
8
-
9
- TRIGGER_TYPE_MAP = {
10
- Contracts::PutEntry => "entry.written",
11
- Contracts::ProposeEntry => "entry.written",
12
- Contracts::DeleteKey => "entry.deleted",
13
- Contracts::MoveKey => "entry.moved",
14
- Contracts::AcceptProposal => "proposal.accepted",
15
- Contracts::RejectProposal => "proposal.rejected",
16
- }.freeze
17
-
18
- def call(container:, command:, call:, next_handler:)
19
- result = next_handler.call(command, call)
20
- return result unless result.success? && cascadable?(command)
21
-
22
- key = cascade_key(command)
23
- return result unless key
24
-
25
- trigger_type = TRIGGER_TYPE_MAP[command.class]
26
- jobs = Textus::Store::Jobs::Planner.new(container: container).plan(
27
- trigger: { "type" => trigger_type, "target" => key },
28
- role: call.role,
29
- )
30
- queue = Textus::Store::Jobs::Queue.new(store: container.job_store)
31
- jobs.each { |j| queue.enqueue(j) }
32
- result
33
- end
34
-
35
- private
36
-
37
- def cascadable?(command)
38
- CASCADE_VERBS.include?(VerbRegistry.contract_to_verb!(command.class).to_sym)
39
- end
40
-
41
- def cascade_key(command)
42
- case command
43
- when Contracts::PutEntry, Contracts::DeleteKey then command.key
44
- when Contracts::MoveKey then command.new_key
45
- when Contracts::AcceptProposal,
46
- Contracts::RejectProposal then command.pending_key
47
- when Contracts::ProposeEntry then command.key
48
- end
49
- end
50
- end
51
- end
52
- end
53
- end
@@ -1,35 +0,0 @@
1
- module Textus
2
- module Dispatch
3
- class Pipeline
4
- attr_reader :container
5
-
6
- def initialize(registry:, container:, middleware: [])
7
- @registry = registry
8
- @middleware = middleware
9
- @container = container
10
- end
11
-
12
- def dispatch(command, call:)
13
- stack = @middleware.reverse.reduce(->(cmd, c) { execute(cmd, c) }) do |next_mw, mw|
14
- ->(cmd, c) { mw.call(container: @container, command: cmd, call: c, next_handler: next_mw) }
15
- end
16
- stack.call(command, call)
17
- end
18
-
19
- def self.build_command(contract_class, inputs)
20
- members = contract_class.members
21
- kwargs = members.to_h do |member|
22
- [member, inputs[member]]
23
- end
24
- contract_class.new(**kwargs)
25
- end
26
-
27
- private
28
-
29
- def execute(command, call)
30
- handler = @registry.for(command.class)
31
- handler.call(command, call)
32
- end
33
- end
34
- end
35
- end
@@ -1,34 +0,0 @@
1
- module Textus
2
- module Doctor
3
- class Check
4
- class AuditLog < Check
5
- def call
6
- path = Textus::Store::Layout.new(root).audit_log_path
7
- Textus::Port::AuditLog.new(root).verify_integrity.map do |v|
8
- {
9
- "code" => "audit.parse_error",
10
- "level" => "warning",
11
- "subject" => "#{path}:#{v["lineno"]}",
12
- "message" => violation_message(v),
13
- "fix" => "inspect #{path} at line #{v["lineno"]} and remove the corrupted row",
14
- }
15
- end
16
- end
17
-
18
- private
19
-
20
- def violation_message(v)
21
- case v["reason"]
22
- when "invalid_json"
23
- "audit log line #{v["lineno"]} is invalid JSON: #{v["detail"]}"
24
- when "short_tsv"
25
- "audit log line #{v["lineno"]} #{v["detail"]} " \
26
- "(consider migrating to NDJSON)"
27
- else
28
- v["detail"]
29
- end
30
- end
31
- end
32
- end
33
- end
34
- end
@@ -1,29 +0,0 @@
1
- module Textus
2
- module Doctor
3
- class Check
4
- # ADR 0079: generator/build drift — a derived/external entry whose sources
5
- # changed since its generated.at. Dependency-based (not age-based), so it
6
- # stays OUT of the lifecycle/freshness unification and lives here as a
7
- # health signal. This is the surviving home for what the removed `stale`
8
- # verb reported.
9
- class GeneratorDrift < Check
10
- def call
11
- detector = Textus::Store::Freshness::DriftDetector.new(
12
- manifest: manifest,
13
- file_stat: Textus::Port::Storage::FileStat.new,
14
- clock: Textus::Port::Clock.new,
15
- )
16
- manifest.data.entries.flat_map { |m| detector.drift_rows(m) }.map do |row|
17
- {
18
- "code" => "generator_drift",
19
- "level" => "warning",
20
- "subject" => row["key"],
21
- "message" => row["reason"],
22
- "fix" => "rematerialize the entry: `textus drain`",
23
- }
24
- end
25
- end
26
- end
27
- end
28
- end
29
- end
@@ -1,60 +0,0 @@
1
- module Textus
2
- module Doctor
3
- class Check
4
- class IllegalKeys < Check
5
- def call
6
- out = []
7
- manifest.data.entries.each do |entry|
8
- next unless entry.nested?
9
- next if entry.publish_mode.keyless? # publish_tree files are opaque payload, never keys (ADR 0047)
10
-
11
- base = File.join(root, Textus::Key::Path.normalize_relative_path(entry.path))
12
- next unless File.directory?(base)
13
-
14
- check_all_paths(entry, base, out)
15
- end
16
- out
17
- end
18
-
19
- private
20
-
21
- def check_all_paths(entry, base, out)
22
- walk_nested(base) do |abs_path, is_dir|
23
- rel = abs_path.sub(%r{\A#{Regexp.escape(base)}/?}, "")
24
- next if entry.ignored?(rel)
25
-
26
- basename = File.basename(abs_path)
27
- stem = is_dir ? basename : basename.sub(/#{Regexp.escape(File.extname(basename))}\z/, "")
28
- next if stem.match?(Key::Grammar::SEGMENT)
29
-
30
- out << issue(abs_path, stem)
31
- end
32
- end
33
-
34
- def issue(abs_path, stem)
35
- {
36
- "code" => "key.illegal",
37
- "level" => "error",
38
- "subject" => abs_path,
39
- "path" => abs_path,
40
- "message" => "illegal key segment '#{stem}' at #{abs_path}",
41
- "fix" => "rename the file/directory so each segment matches [a-z0-9][a-z0-9-]* " \
42
- "(lowercase, digits, hyphens)",
43
- }
44
- end
45
-
46
- def walk_nested(root, &block)
47
- Dir.each_child(root) do |name|
48
- abs = File.join(root, name)
49
- if File.directory?(abs)
50
- walk_nested(abs, &block)
51
- yield abs, true
52
- else
53
- yield abs, false
54
- end
55
- end
56
- end
57
- end
58
- end
59
- end
60
- end
@@ -1,25 +0,0 @@
1
- module Textus
2
- module Doctor
3
- class Check
4
- class ManifestFiles < Check
5
- def call
6
- manifest.data.entries.each_with_object([]) do |entry, out|
7
- next if entry.nested?
8
-
9
- path = Textus::Key::Path.resolve(manifest.data, entry)
10
- next if File.exist?(path)
11
-
12
- out << {
13
- "code" => "manifest.missing_file",
14
- "level" => "info",
15
- "subject" => entry.key,
16
- "message" => "declared entry has no file on disk at #{path}",
17
- "fix" => "create the entry with 'textus put #{entry.key} --stdin --as=<role>' " \
18
- "(or leave empty if not yet authored)",
19
- }
20
- end
21
- end
22
- end
23
- end
24
- end
25
- end
@@ -1,35 +0,0 @@
1
- module Textus
2
- module Doctor
3
- class Check
4
- # Flags published files whose recorded source no longer exists in the
5
- # store. Per-leaf prune (ADR 0046) reconciles within a still-present
6
- # leaf; a renamed or removed *whole* leaf orphans its entire target
7
- # directory, which a per-entry build won't revisit. This check catches
8
- # that drift without making `build` scan globally.
9
- class OrphanedPublishTargets < Check
10
- def call
11
- sdir = Textus::Store::Layout.new(root).sentinels_root
12
- return [] unless File.directory?(sdir)
13
-
14
- repo_root = File.dirname(root)
15
- store = Textus::Port::SentinelStore.new
16
- glob = File.join(sdir, "**", "*#{Textus::Port::SentinelStore::SUFFIX}")
17
- Dir.glob(glob).filter_map do |spath|
18
- sentinel = store.load(spath, repo_root)
19
- next nil if sentinel.nil? || sentinel.source.nil?
20
- next nil if File.exist?(sentinel.source)
21
-
22
- {
23
- "code" => "publish.orphaned_target",
24
- "level" => "warning",
25
- "subject" => sentinel.target,
26
- "message" => "published file #{sentinel.target} has no source in the store " \
27
- "(recorded source #{sentinel.source} is gone) — likely a renamed or removed leaf",
28
- "fix" => "remove the stale copy and its sentinel: rm '#{sentinel.target}' '#{spath}'",
29
- }
30
- end
31
- end
32
- end
33
- end
34
- end
35
- end
@@ -1,45 +0,0 @@
1
- module Textus
2
- module Doctor
3
- class Check
4
- # Flags pending proposals whose `proposal.target_key` cannot ever be
5
- # accepted: it points at a non-canon zone or resolves to no declared
6
- # entry (ADR 0035). Reads the live queue zone; silent when there is no
7
- # queue zone. Warnings, not errors — they are stale junk, not store
8
- # corruption (the accept gate already refuses them).
9
- class ProposalTargets < Check
10
- def call
11
- queue = manifest.policy.queue_lane
12
- return [] unless queue
13
-
14
- dispatch(:list, lane: queue).filter_map { |row| issue_for(row["key"]) }
15
- end
16
-
17
- private
18
-
19
- def issue_for(key)
20
- target = dispatch(:get, key).meta&.dig("proposal", "target_key")
21
- return nil if target.nil? # not a proposal entry — skip
22
-
23
- zone = manifest.resolver.resolve(target).entry.lane
24
- return nil if manifest.policy.declared_kind(zone.to_s) == :canon
25
-
26
- {
27
- "code" => "proposal.target_not_canon",
28
- "level" => "warning",
29
- "subject" => key,
30
- "message" => "proposal '#{key}' targets '#{target}' in zone '#{zone}' (not canon); it can never be accepted",
31
- "fix" => "delete the proposal, or repoint target_key at a canon zone",
32
- }
33
- rescue Textus::UnknownKey
34
- {
35
- "code" => "proposal.target_unresolved",
36
- "level" => "warning",
37
- "subject" => key,
38
- "message" => "proposal '#{key}' targets '#{target}', which resolves to no declared entry",
39
- "fix" => "delete the proposal, or fix target_key",
40
- }
41
- end
42
- end
43
- end
44
- end
45
- end
@@ -1,47 +0,0 @@
1
- require "yaml"
2
-
3
- module Textus
4
- module Doctor
5
- class Check
6
- # Runs as a standalone module (Check::ProtocolVersion.run(root:)) and also
7
- # as a class-based doctor check (ProtocolVersion.new(store).call).
8
- class ProtocolVersion < Check
9
- # Standalone interface: root is the project root (parent of .textus/).
10
- def self.run(root:)
11
- path = File.join(root, ".textus", "manifest.yaml")
12
- return [] unless File.exist?(path)
13
-
14
- doc = YAML.safe_load_file(path, aliases: false) || {}
15
- version = doc["version"]
16
- return [] if version == "textus/4"
17
-
18
- [{
19
- "code" => "protocol_mismatch",
20
- "severity" => "error",
21
- "message" => "Store reports version=#{version.inspect}; this gem expects textus/4.",
22
- "hint" => "Upgrade the store's manifest version to textus/4 (see CHANGELOG for breaking changes).",
23
- }]
24
- end
25
-
26
- # Doctor check interface: root is the .textus/ directory itself,
27
- # so manifest.yaml lives directly inside it.
28
- def call
29
- path = geometry.manifest_path
30
- return [] unless File.exist?(path)
31
-
32
- doc = YAML.safe_load_file(path, aliases: false) || {}
33
- version = doc["version"]
34
- return [] if version == "textus/4"
35
-
36
- [{
37
- "code" => "protocol_mismatch",
38
- "level" => "error",
39
- "subject" => path,
40
- "message" => "Store reports version=#{version.inspect}; this gem expects textus/4.",
41
- "fix" => "Upgrade the store's manifest version to textus/4 (see CHANGELOG for breaking changes).",
42
- }]
43
- end
44
- end
45
- end
46
- end
47
- end
@@ -1,48 +0,0 @@
1
- module Textus
2
- module Doctor
3
- class Check
4
- # ADR 0047 Decision 4. A publish_tree entry prunes its WHOLE target dir on
5
- # every build. If a derived entry's publish_to writes a file into that same
6
- # dir, the tree's prune will delete it unless the tree `ignore`s that
7
- # filename. Warn so the author adds the ignore before prune eats the index.
8
- class PublishTreeIndexOverlap < Check
9
- def call
10
- entries = manifest.data.entries
11
- trees = entries.select { |e| e.nested? && e.publish_tree }
12
- return [] if trees.empty?
13
-
14
- derived_targets = entries.flat_map do |e|
15
- Array(e.publish_to).map { |rel| [e, rel] }
16
- end
17
-
18
- trees.flat_map do |tree|
19
- target_prefix = "#{tree.publish_tree.chomp("/")}/"
20
- derived_targets.filter_map do |(derived, rel)|
21
- next nil unless rel.start_with?(target_prefix)
22
-
23
- rel_to_target = rel.delete_prefix(target_prefix)
24
- next nil if tree.ignored?(rel_to_target)
25
-
26
- issue(tree, derived, rel, rel_to_target)
27
- end
28
- end
29
- end
30
-
31
- private
32
-
33
- def issue(tree, derived, rel, rel_to_target)
34
- basename = File.basename(rel_to_target)
35
- {
36
- "code" => "publish.tree_index_overlap",
37
- "level" => "warning",
38
- "subject" => tree.key,
39
- "message" => "publish_tree '#{tree.publish_tree}' overlaps derived entry " \
40
- "'#{derived.key}' publish_to '#{rel}'; the tree's prune will delete it on rebuild",
41
- "fix" => "add a glob covering '#{rel_to_target}' to entry '#{tree.key}' ignore " \
42
- "(e.g. ignore: [\"**/#{basename}\"])",
43
- }
44
- end
45
- end
46
- end
47
- end
48
- end
@@ -1,50 +0,0 @@
1
- module Textus
2
- module Doctor
3
- class Check
4
- class RawAssetPaths < Check
5
- def call
6
- raw_lane = manifest.policy.lanes_of_kind(:raw).first
7
- return [] unless raw_lane
8
-
9
- issues = []
10
- manifest.resolver.enumerate.each do |row|
11
- next unless row[:key].start_with?("raw.")
12
- next unless row[:path] && File.exist?(row[:path])
13
-
14
- raw_content = load_content(row[:path])
15
- next unless raw_content.is_a?(Hash)
16
-
17
- asset = raw_content["asset"]
18
- next unless asset.is_a?(String) && !asset.empty?
19
-
20
- asset_path = find_asset_path(asset)
21
- next if File.exist?(asset_path)
22
-
23
- issues << {
24
- "code" => "raw_asset.missing_file",
25
- "level" => "error",
26
- "subject" => row[:key],
27
- "message" => "raw entry '#{row[:key]}' references asset '#{asset}' " \
28
- "which does not exist at #{asset_path}",
29
- "fix" => "re-ingest the asset: textus key-delete #{row[:key]}, then textus ingest",
30
- }
31
- end
32
- issues
33
- end
34
-
35
- private
36
-
37
- def load_content(path)
38
- require "yaml"
39
- YAML.safe_load_file(path)
40
- rescue StandardError
41
- nil
42
- end
43
-
44
- def find_asset_path(asset_rel)
45
- File.join(geometry.root, "assets", asset_rel)
46
- end
47
- end
48
- end
49
- end
50
- end
@@ -1,49 +0,0 @@
1
- module Textus
2
- module Doctor
3
- class Check
4
- # Flags entries whose key is matched by two or more rule blocks of the
5
- # SAME specificity in the same slot (lifecycle / handler_permit /
6
- # guard / materialize). Ties are non-deterministic in the parser's pick step, so
7
- # they're a configuration smell — surface them.
8
- class RuleAmbiguity < Check
9
- SLOTS = Textus::Manifest::Schema::FIELD_REGISTRY.select { |_, m| m[:in_ambiguity] }.keys.freeze
10
-
11
- def call
12
- out = []
13
- rules = manifest.rules
14
- manifest.data.entries.each do |mentry|
15
- matches = rules.explain(mentry.key)
16
- next if matches.length < 2
17
-
18
- SLOTS.each { |slot| out.concat(ambiguities_for(mentry, slot, matches)) }
19
- end
20
- out
21
- end
22
-
23
- private
24
-
25
- def ambiguities_for(mentry, slot, matches)
26
- carriers = matches.select { |b| b.public_send(slot) }
27
- return [] if carriers.length < 2
28
-
29
- by_specificity = carriers.group_by { |b| Textus::Manifest::Policy::Matcher.specificity(b.match) }
30
- tied = by_specificity.values.select { |group| group.length > 1 }
31
- tied.map { |group| issue_for(mentry, slot, group) }
32
- end
33
-
34
- def issue_for(mentry, slot, group)
35
- globs = group.map(&:match).sort
36
- {
37
- "code" => "rule.ambiguity",
38
- "level" => "warning",
39
- "subject" => mentry.key,
40
- "message" => "entry '#{mentry.key}' matches #{group.length} rule blocks at the same " \
41
- "specificity for #{slot}: #{globs.join(", ")}",
42
- "fix" => "narrow one of the conflicting match: globs in .textus/manifest.yaml so a single " \
43
- "block wins for this key",
44
- }
45
- end
46
- end
47
- end
48
- end
49
- end