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,83 +0,0 @@
1
- require "json"
2
- require "digest"
3
- require "fileutils"
4
-
5
- module Textus
6
- module Port
7
- # Persistence adapter for sentinel files. Owns the on-disk JSON shape, the
8
- # path layout (<store_root>/.run/sentinels/<target-rel-to-repo>.textus-managed.json
9
- # — runtime, git-ignored, ADR 0070), and all File/FileUtils I/O.
10
- # Core::Sentinel is a pure value object that depends on this port for
11
- # reads and writes.
12
- class SentinelStore
13
- SUFFIX = ".textus-managed.json".freeze
14
-
15
- def write!(target:, source:, store_root:)
16
- path = sentinel_path(target, store_root)
17
- FileUtils.mkdir_p(File.dirname(path))
18
- repo_root = File.dirname(store_root)
19
- File.write(path, JSON.generate(
20
- "source" => rel_or_abs(source, repo_root),
21
- "target" => rel_or_abs(target, repo_root),
22
- "sha256" => Digest::SHA256.hexdigest(File.binread(target)),
23
- "mode" => "copy",
24
- ))
25
- end
26
-
27
- def load(path, repo_root)
28
- raw = JSON.parse(File.read(path))
29
- Textus::Port::Sentinel.new(
30
- target: absolutize(raw["target"], repo_root),
31
- source: absolutize(raw["source"], repo_root),
32
- sha256: raw["sha256"],
33
- mode: raw["mode"],
34
- )
35
- rescue JSON::ParserError, Errno::ENOENT
36
- nil
37
- end
38
-
39
- def sentinel_path(target, store_root)
40
- repo_root = File.dirname(store_root)
41
- rel = relative_to(target, repo_root) || File.basename(target)
42
- File.join(Textus::Store::Layout.new(store_root).sentinels_root, rel + SUFFIX)
43
- end
44
-
45
- # Absolute target paths of every sentinel recorded under `target_dir`.
46
- def targets_under(target_dir, store_root)
47
- repo_root = File.dirname(store_root)
48
- rel = relative_to(target_dir, repo_root) or return []
49
- root = Textus::Store::Layout.new(store_root).sentinels_root
50
- sdir = File.join(root, rel)
51
- return [] unless File.directory?(sdir)
52
-
53
- prefix = root + "/"
54
- Dir.glob(File.join(sdir, "**", "*#{SUFFIX}")).map do |spath|
55
- # strip the sentinel-store prefix and the .textus-managed.json suffix to recover the repo-relative target path
56
- trel = spath.delete_prefix(prefix).delete_suffix(SUFFIX)
57
- File.join(repo_root, trel)
58
- end
59
- end
60
-
61
- private
62
-
63
- def rel_or_abs(path, repo_root)
64
- relative_to(path, repo_root) || File.expand_path(path)
65
- end
66
-
67
- def relative_to(path, repo_root)
68
- path = File.expand_path(path)
69
- base = File.expand_path(repo_root)
70
- return nil unless path.start_with?(base + File::SEPARATOR)
71
-
72
- path[(base.length + 1)..]
73
- end
74
-
75
- def absolutize(path, repo_root)
76
- return path if path.nil?
77
- return path if File.absolute_path?(path)
78
-
79
- File.expand_path(path, repo_root)
80
- end
81
- end
82
- end
83
- end
@@ -1,19 +0,0 @@
1
- module Textus
2
- module Port
3
- module Storage
4
- # Read-only filesystem query port. The narrow interface that pure
5
- # domain logic (staleness checks, sentinel value) depends on, so the
6
- # domain never touches File/Dir directly. FileStore owns the write side.
7
- class FileStat
8
- def exists?(path) = File.exist?(path)
9
- def directory?(path) = File.directory?(path)
10
- def read(path) = File.binread(path)
11
- def mtime(path) = File.mtime(path)
12
-
13
- # Ruby 3.3+ guarantees Dir.glob returns a sorted Array; no explicit sort
14
- # needed, but callers can rely on ordered results for stable behaviour.
15
- def glob(pattern) = Dir.glob(pattern)
16
- end
17
- end
18
- end
19
- end
@@ -1,49 +0,0 @@
1
- require "fileutils"
2
-
3
- module Textus
4
- module Port
5
- module Storage
6
- # Pure filesystem I/O port. Wraps File/FileUtils/Etag with no knowledge
7
- # of envelopes, entries, schemas, or audit.
8
- class FileStore
9
- include Interface
10
-
11
- def read(path) = File.binread(path)
12
-
13
- def write(path, bytes)
14
- FileUtils.mkdir_p(File.dirname(path))
15
- File.binwrite(path, bytes)
16
- end
17
-
18
- # Raises Errno::ENOENT if absent — mirrors File.delete and matches the
19
- # semantics used by Store::Writer (which guards with File.exist? first).
20
- def delete(path) = File.delete(path)
21
-
22
- def exists?(path) = File.exist?(path)
23
-
24
- def etag(path) = Value::Etag.for_file(path)
25
-
26
- # Convenience filesystem ops so callers can go through the port
27
- # instead of calling FileUtils/Dir directly. Keeps filesystem
28
- # semantics in one place for easier testing and replacement.
29
- def mkdir_p(path)
30
- FileUtils.mkdir_p(path)
31
- end
32
-
33
- def mv(from_path, to_path)
34
- FileUtils.mkdir_p(File.dirname(to_path))
35
- FileUtils.mv(from_path, to_path)
36
- end
37
-
38
- def rmdir(path)
39
- Dir.rmdir(path)
40
- end
41
-
42
- def dir_empty?(dir)
43
- # Dir.empty? exists on modern Rubies; wrap for clarity
44
- Dir.empty?(dir)
45
- end
46
- end
47
- end
48
- end
49
- end
@@ -1,17 +0,0 @@
1
- module Textus
2
- module Port
3
- module Storage
4
- module Interface
5
- def read(path) = raise NotImplementedError
6
- def write(path, bytes) = raise NotImplementedError
7
- def delete(path) = raise NotImplementedError
8
- def exists?(path) = raise NotImplementedError
9
- def etag(path) = raise NotImplementedError
10
- def mkdir_p(path) = raise NotImplementedError
11
- def mv(from_path, to_path) = raise NotImplementedError
12
- def rmdir(path) = raise NotImplementedError
13
- def dir_empty?(dir) = raise NotImplementedError
14
- end
15
- end
16
- end
17
- end
@@ -1,149 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "fileutils"
4
- require "sqlite3"
5
-
6
- module Textus
7
- module Port
8
- # SQLite-backed runtime store for textus state. Owns the connection,
9
- # schema setup, WAL mode, and transaction boundary for the index and queue.
10
- class Store
11
- attr_reader :path, :connection
12
-
13
- def initialize(root:)
14
- @path = Textus::Store::Layout.new(root).store_db_path
15
- FileUtils.mkdir_p(File.dirname(@path))
16
- @connection = SQLite3::Database.new(@path)
17
- @connection.results_as_hash = true
18
- end
19
-
20
- def execute(sql, params = [])
21
- @connection.execute(sql, params)
22
- end
23
-
24
- def query_value(sql, params = [])
25
- @connection.get_first_value(sql, params)
26
- end
27
-
28
- def setup!
29
- execute("PRAGMA journal_mode=WAL")
30
- execute("PRAGMA foreign_keys=ON")
31
- connection.execute_batch(<<~SQL)
32
- CREATE TABLE IF NOT EXISTS entries (
33
- key TEXT PRIMARY KEY,
34
- lane TEXT NOT NULL,
35
- format TEXT NOT NULL,
36
- etag TEXT,
37
- content TEXT,
38
- extra TEXT,
39
- indexed_at TEXT NOT NULL
40
- ) STRICT;
41
-
42
- CREATE VIRTUAL TABLE IF NOT EXISTS entries_fts USING fts5(
43
- key, lane, content,
44
- content=entries, content_rowid=rowid
45
- );
46
-
47
- CREATE TABLE IF NOT EXISTS jobs (
48
- id TEXT PRIMARY KEY,
49
- type TEXT NOT NULL,
50
- args TEXT NOT NULL,
51
- state TEXT NOT NULL DEFAULT 'ready',
52
- role TEXT NOT NULL,
53
- attempts INTEGER NOT NULL DEFAULT 0,
54
- max_attempts INTEGER NOT NULL DEFAULT 3,
55
- errors TEXT,
56
- lease TEXT,
57
- created_at TEXT NOT NULL,
58
- updated_at TEXT NOT NULL
59
- ) STRICT;
60
-
61
- CREATE INDEX IF NOT EXISTS idx_jobs_state ON jobs(state);
62
- CREATE INDEX IF NOT EXISTS idx_entries_lane ON entries(lane);
63
-
64
- CREATE TABLE IF NOT EXISTS audit_events (
65
- seq INTEGER PRIMARY KEY,
66
- ts TEXT NOT NULL,
67
- role TEXT NOT NULL,
68
- verb TEXT NOT NULL,
69
- key TEXT NOT NULL,
70
- etag_before TEXT,
71
- etag_after TEXT
72
- ) STRICT;
73
-
74
- CREATE INDEX IF NOT EXISTS idx_audit_events_seq ON audit_events(seq);
75
- SQL
76
- # Idempotent migration: add schema_ref column if missing (existing stores).
77
- execute("ALTER TABLE entries ADD COLUMN schema_ref TEXT") rescue nil # rubocop:disable Style/RescueModifier
78
- self
79
- end
80
-
81
- def search_entries(q: nil, schema: nil, lane: nil, prefix: nil) # rubocop:disable Naming/MethodParameterName
82
- return nil if q.nil? && schema.nil?
83
-
84
- if q
85
- fts_search(q: q, schema: schema, lane: lane, prefix: prefix)
86
- else
87
- schema_search(schema: schema, lane: lane, prefix: prefix)
88
- end
89
- end
90
-
91
- def insert_audit_event(seq:, ts:, role:, verb:, key:, etag_before:, etag_after:) # rubocop:disable Naming/MethodParameterName
92
- execute(
93
- "INSERT OR IGNORE INTO audit_events (seq, ts, role, verb, key, etag_before, etag_after) VALUES (?, ?, ?, ?, ?, ?, ?)",
94
- [seq, ts, role, verb, key, etag_before, etag_after],
95
- )
96
- end
97
-
98
- def audit_events_since(seq:)
99
- execute(
100
- "SELECT seq, ts, role, verb, key, etag_before, etag_after FROM audit_events WHERE seq > ? ORDER BY seq",
101
- [seq],
102
- )
103
- end
104
-
105
- def transaction
106
- connection.transaction
107
- yield
108
- connection.commit
109
- rescue StandardError
110
- connection.rollback if connection.transaction_active?
111
- raise
112
- end
113
-
114
- def close
115
- connection.close unless connection.closed?
116
- end
117
-
118
- def self.open(root)
119
- store = new(root: root)
120
- store.setup!
121
- return store unless block_given?
122
-
123
- yield store
124
- ensure
125
- store&.close
126
- end
127
- private :connection
128
-
129
- def fts_search(q:, schema:, lane:, prefix:) # rubocop:disable Naming/MethodParameterName
130
- sql = "SELECT e.key, e.lane, e.schema_ref FROM entries e JOIN entries_fts fts ON e.rowid = fts.rowid WHERE entries_fts MATCH ?"
131
- params = [q]
132
- sql += " AND e.lane = ?" and params << lane if lane
133
- sql += " AND e.schema_ref = ?" and params << schema if schema
134
- sql += " AND (e.key = ? OR e.key LIKE ?)" and params.push(prefix, "#{prefix}.%") if prefix
135
- execute(sql, params)
136
- end
137
- private :fts_search
138
-
139
- def schema_search(schema:, lane:, prefix:)
140
- sql = "SELECT key, lane, schema_ref FROM entries WHERE schema_ref = ?"
141
- params = [schema]
142
- sql += " AND lane = ?" and params << lane if lane
143
- sql += " AND (key = ? OR key LIKE ?)" and params.push(prefix, "#{prefix}.%") if prefix
144
- execute(sql, params)
145
- end
146
- private :schema_search
147
- end
148
- end
149
- end
@@ -1,48 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "socket"
4
-
5
- module Textus
6
- module Port
7
- # Flock-based watcher presence lock. Held for the watcher's lifetime.
8
- # Process death releases the flock automatically.
9
- class WatcherLock
10
- def initialize(root)
11
- @path = Textus::Store::Layout.new(root).lock_path("watcher")
12
- @file = nil
13
- FileUtils.mkdir_p(File.dirname(@path))
14
- end
15
-
16
- def self.running?(root)
17
- path = Textus::Store::Layout.new(root).lock_path("watcher")
18
- return false unless File.exist?(path)
19
-
20
- File.open(path, "r+") do |file|
21
- got = file.flock(File::LOCK_EX | File::LOCK_NB)
22
- file.flock(File::LOCK_UN) if got
23
- return !got
24
- end
25
- rescue Errno::ENOENT
26
- false
27
- end
28
-
29
- def acquire
30
- @file = File.open(@path, File::RDWR | File::CREAT, 0o644)
31
- raise "watcher already running" unless @file.flock(File::LOCK_EX | File::LOCK_NB)
32
-
33
- @file.write("pid=#{Process.pid} host=#{Socket.gethostname}\n")
34
- @file.flush
35
- self
36
- end
37
-
38
- def release
39
- return unless @file
40
-
41
- @file.flock(File::LOCK_UN)
42
- @file.close
43
- FileUtils.rm_f(@path)
44
- @file = nil
45
- end
46
- end
47
- end
48
- end
@@ -1,40 +0,0 @@
1
- module Textus
2
- module Produce
3
- class Engine
4
- def self.converge(container:, call:, keys:)
5
- new(container:, call:).run(keys)
6
- end
7
-
8
- def initialize(container:, call:)
9
- @container = container
10
- @call = call
11
- end
12
-
13
- def run(keys)
14
- results = { completed: [], failed: [] }
15
- Array(keys).each { |key| produce_one(key, results) }
16
- results
17
- end
18
-
19
- alias call run
20
-
21
- private
22
-
23
- def produce_one(key, results)
24
- workflow = @container.workflows.for(key)
25
- if workflow
26
- Workflow::Runner.new(workflow, container: @container, call: @call).run(key)
27
- else
28
- publish_only(key)
29
- end
30
- results[:completed] << key
31
- rescue StandardError => e
32
- results[:failed] << { key: key, error: e.message }
33
- end
34
-
35
- def publish_only(key)
36
- Textus::Produce::Publisher.call(container: @container, call: @call, key: key)
37
- end
38
- end
39
- end
40
- end
@@ -1,21 +0,0 @@
1
- module Textus
2
- module Produce
3
- module Publisher
4
- def self.call(container:, call:, key:)
5
- entry = container.manifest.resolver.resolve(key).entry
6
- return unless entry.publish_tree || !Array(entry.publish_to).empty?
7
-
8
- entry_path = container.manifest.resolver.resolve(key).path
9
- return unless entry.publish_tree || container.file_store.exists?(entry_path)
10
-
11
- reader = Textus::Store::Entry::Reader.from(container: container)
12
- pctx = Textus::Manifest::Entry::Base::PublishContext.new(
13
- container: container,
14
- call: call,
15
- reader: reader.method(:read),
16
- )
17
- entry.publish_via(pctx)
18
- end
19
- end
20
- end
21
- end
@@ -1,20 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "erb"
4
-
5
- module Textus
6
- module Produce
7
- class Render
8
- def initialize(template_loader:)
9
- @template_loader = template_loader
10
- end
11
-
12
- def bytes_for(target:, data:, boot:)
13
- raise ArgumentError.new("Produce::Render called for a verbatim target #{target.to.inspect}") unless target.renders?
14
-
15
- ctx = target.inject_boot ? data.merge("boot" => boot) : data
16
- ERB.new(@template_loader.call(target.template), trim_mode: "-").result_with_hash(ctx)
17
- end
18
- end
19
- end
20
- end
@@ -1,42 +0,0 @@
1
- module Textus
2
- class Schema
3
- class Registry
4
- def initialize(dir)
5
- @dir = dir
6
- @schemas = {}
7
- load_all
8
- end
9
-
10
- def fetch(name)
11
- @schemas[name] || raise(IoError.new("schema not found: #{File.join(@dir, "#{name}.yaml")}"))
12
- end
13
-
14
- def fetch_or_nil(name)
15
- return nil if name.nil?
16
-
17
- fetch(name)
18
- end
19
-
20
- def all
21
- @schemas.values
22
- end
23
-
24
- def by_name
25
- @schemas.dup
26
- end
27
-
28
- private
29
-
30
- def load_all
31
- return unless File.directory?(@dir)
32
-
33
- Dir.glob(File.join(@dir, "*.yaml")).each do |path|
34
- name = File.basename(path, ".yaml")
35
- @schemas[name] = Schema.load(path)
36
- rescue StandardError => e
37
- warn "textus: failed to load schema '#{name}' at #{path}: #{e.message}"
38
- end
39
- end
40
- end
41
- end
42
- end
@@ -1,108 +0,0 @@
1
- require "yaml"
2
- require "fileutils"
3
-
4
- module Textus
5
- class Schema
6
- module Tools
7
- # textus schema init NAME --from=KEY → infer YAML schema from an entry's frontmatter
8
- def self.init(store, name:, from:)
9
- env = pure_get(store, Textus::Value::Role::DEFAULT, from)
10
- meta = env.meta
11
- schema = {
12
- "name" => name,
13
- "required" => meta.keys,
14
- "optional" => [],
15
- "fields" => meta.each_with_object({}) { |(k, v), h| h[k] = { "type" => infer_type(v) } },
16
- }
17
- geom = Textus::Store::Layout.new(store.root)
18
- FileUtils.mkdir_p(geom.schemas_dir)
19
- target = geom.schema_path(name)
20
- File.write(target, YAML.dump(schema))
21
- { "protocol" => PROTOCOL, "schema_name" => name, "path" => target }
22
- end
23
-
24
- # textus schema diff NAME → list keys whose frontmatter violates the schema
25
- def self.diff(store, name:)
26
- schema = load_schema(store, name)
27
- drift = []
28
- store.manifest.resolver.enumerate.each do |row|
29
- env = pure_get(store, Textus::Value::Role::DEFAULT, row[:key])
30
- begin
31
- schema.validate!(env.meta)
32
- rescue SchemaViolation => e
33
- drift << { "key" => row[:key], "details" => e.details }
34
- end
35
- end
36
- { "protocol" => PROTOCOL, "schema_name" => name, "drift" => drift }
37
- end
38
-
39
- # textus schema migrate NAME --rename=OLD:NEW → rewrites frontmatter across affected entries
40
- # If --rename is omitted, falls back to schema.evolution.migrate_from.
41
- def self.migrate(store, name:, rename: nil)
42
- renames =
43
- if rename
44
- old_field, new_field = rename.split(":", 2)
45
- raise UsageError.new("--rename=OLD:NEW") unless old_field && new_field && !new_field.empty?
46
-
47
- { old_field => new_field }
48
- else
49
- load_schema(store, name).evolution["migrate_from"] || {}
50
- end
51
- raise UsageError.new("schema migrate needs --rename=OLD:NEW or schema.evolution.migrate_from") if renames.empty?
52
-
53
- authority = accept_role_for(store)
54
- ops = store.with_role(authority)
55
- touched = []
56
- store.manifest.resolver.enumerate.each do |row|
57
- env = pure_get(store, authority, row[:key])
58
- meta = env.meta.dup
59
- changed = false
60
- renames.each do |old, new|
61
- if meta.key?(old)
62
- meta[new] = meta.delete(old)
63
- changed = true
64
- end
65
- end
66
- next unless changed
67
-
68
- ops.put(row[:key], meta: meta, body: env.body)
69
- touched << row[:key]
70
- end
71
- { "protocol" => PROTOCOL, "migrated" => touched, "renames" => renames }
72
- end
73
-
74
- def self.infer_type(value)
75
- case value
76
- when String then "string"
77
- when Numeric then "number"
78
- when true, false then "boolean"
79
- when Array then "array"
80
- when Hash then "object"
81
- else "string"
82
- end
83
- end
84
-
85
- # Orchestrator-free read: schema tooling must never trigger a fetch
86
- # while inspecting/migrating entries (ADR 0062).
87
- def self.pure_get(store, role, key)
88
- store.with_role(role).get(key)
89
- end
90
-
91
- def self.load_schema(store, name)
92
- store.schemas.fetch(name)
93
- rescue IoError
94
- raise UsageError.new("schema not found: #{name}")
95
- end
96
-
97
- def self.accept_role_for(store)
98
- authority = store.manifest.policy.roles_with_capability("author").first
99
- return authority if authority
100
-
101
- raise UsageError.new(
102
- "schema migrate requires a role holding the 'author' capability; " \
103
- "none declared (add e.g. `- { name: owner, can: [author] }` to roles:)",
104
- )
105
- end
106
- end
107
- end
108
- end