textus 0.55.1 → 0.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (329) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +175 -2768
  3. data/README.md +17 -18
  4. data/lib/textus/artifact.rb +8 -0
  5. data/lib/textus/errors.rb +2 -39
  6. data/lib/textus/human.rb +7 -0
  7. data/lib/textus/infra/adapters/concurrency_adapter.rb +15 -0
  8. data/lib/textus/infra/clock.rb +7 -0
  9. data/lib/textus/infra/database.rb +127 -0
  10. data/lib/textus/infra/file_system.rb +54 -0
  11. data/lib/textus/infra/locks.rb +50 -0
  12. data/lib/textus/infra/port/clock.rb +18 -0
  13. data/lib/textus/infra/port/publisher.rb +71 -0
  14. data/lib/textus/infra/port/sentinel.rb +25 -0
  15. data/lib/textus/infra/port/sentinel_store.rb +93 -0
  16. data/lib/textus/infra/store/audit_log.rb +42 -0
  17. data/lib/textus/infra/store/base.rb +25 -0
  18. data/lib/textus/infra/store/cursor_store.rb +27 -0
  19. data/lib/textus/infra/store/entry_index.rb +31 -0
  20. data/lib/textus/infra/store/envelope/meta.rb +63 -0
  21. data/lib/textus/infra/store/file_store.rb +23 -0
  22. data/lib/textus/infra/store/index/builder.rb +28 -0
  23. data/lib/textus/infra/store/index/lookup.rb +23 -0
  24. data/lib/textus/infra/store/link_store.rb +43 -0
  25. data/lib/textus/infra/store/workflow_queue.rb +54 -0
  26. data/lib/textus/infra/store.rb +29 -0
  27. data/lib/textus/ingest.rb +8 -0
  28. data/lib/textus/lanes/artifact/cache.rb +35 -0
  29. data/lib/textus/lanes/artifact/handlers.rb +29 -0
  30. data/lib/textus/lanes/artifact/lifecycle.rb +23 -0
  31. data/lib/textus/lanes/artifact/verbs.rb +18 -0
  32. data/lib/textus/lanes/ingest/dedup.rb +17 -0
  33. data/lib/textus/lanes/ingest/entry_types/asset.rb +45 -0
  34. data/lib/textus/lanes/ingest/entry_types/link.rb +16 -0
  35. data/lib/textus/lanes/ingest/entry_types/text.rb +17 -0
  36. data/lib/textus/lanes/ingest/entry_types.rb +25 -0
  37. data/lib/textus/lanes/ingest/handlers.rb +54 -0
  38. data/lib/textus/lanes/ingest/index_rebuilder.rb +13 -0
  39. data/lib/textus/lanes/ingest/key_builder.rb +33 -0
  40. data/lib/textus/lanes/ingest/resolver.rb +21 -0
  41. data/lib/textus/lanes/knowledge/freeform.rb +15 -0
  42. data/lib/textus/lanes/knowledge/handlers.rb +53 -0
  43. data/lib/textus/lanes/knowledge/runner.rb +45 -0
  44. data/lib/textus/lanes/knowledge/verbs.rb +16 -0
  45. data/lib/textus/lanes/scratchpad/handlers.rb +164 -0
  46. data/lib/textus/lanes/scratchpad/verbs.rb +73 -0
  47. data/lib/textus/{gate → protocol}/binder.rb +2 -10
  48. data/lib/textus/protocol/boot/catalog.rb +34 -0
  49. data/lib/textus/protocol/boot/context.rb +124 -0
  50. data/lib/textus/protocol/boot/protocol.rb +33 -0
  51. data/lib/textus/protocol/boot.rb +42 -0
  52. data/lib/textus/protocol/builder/domain.rb +17 -0
  53. data/lib/textus/protocol/builder/infrastructure.rb +17 -0
  54. data/lib/textus/protocol/builder/runtime.rb +46 -0
  55. data/lib/textus/protocol/builder.rb +11 -0
  56. data/lib/textus/protocol/container.rb +22 -0
  57. data/lib/textus/protocol/diff.rb +170 -0
  58. data/lib/textus/protocol/entry_constraint.rb +54 -0
  59. data/lib/textus/protocol/envelope.rb +70 -0
  60. data/lib/textus/protocol/format/base.rb +80 -0
  61. data/lib/textus/protocol/format/bash.rb +10 -0
  62. data/lib/textus/protocol/format/javascript.rb +10 -0
  63. data/lib/textus/protocol/format/json.rb +82 -0
  64. data/lib/textus/protocol/format/markdown.rb +53 -0
  65. data/lib/textus/protocol/format/python.rb +10 -0
  66. data/lib/textus/protocol/format/ruby.rb +10 -0
  67. data/lib/textus/protocol/format/script.rb +55 -0
  68. data/lib/textus/protocol/format/text.rb +48 -0
  69. data/lib/textus/protocol/format/yaml.rb +105 -0
  70. data/lib/textus/protocol/format.rb +73 -0
  71. data/lib/textus/protocol/freshness/ttl_evaluator.rb +61 -0
  72. data/lib/textus/protocol/gate.rb +61 -0
  73. data/lib/textus/protocol/handlers/command_context.rb +19 -0
  74. data/lib/textus/protocol/handlers/concern.rb +15 -0
  75. data/lib/textus/protocol/handlers/ops/audit_filter.rb +37 -0
  76. data/lib/textus/protocol/handlers/ops/bulk_planner.rb +38 -0
  77. data/lib/textus/protocol/handlers/ops/data_mv.rb +49 -0
  78. data/lib/textus/protocol/handlers/ops/git_blame.rb +65 -0
  79. data/lib/textus/protocol/handlers/ops/pulse_coordinator.rb +37 -0
  80. data/lib/textus/protocol/handlers/ops/source_expander.rb +41 -0
  81. data/lib/textus/protocol/handlers/query_context.rb +41 -0
  82. data/lib/textus/protocol/handlers/read.rb +67 -0
  83. data/lib/textus/protocol/handlers/system.rb +93 -0
  84. data/lib/textus/protocol/handlers/write.rb +57 -0
  85. data/lib/textus/protocol/init.rb +143 -0
  86. data/lib/textus/protocol/key/grammar.rb +35 -0
  87. data/lib/textus/protocol/key/matching.rb +23 -0
  88. data/lib/textus/protocol/key/path.rb +23 -0
  89. data/lib/textus/protocol/key.rb +25 -0
  90. data/lib/textus/protocol/lane.rb +12 -0
  91. data/lib/textus/{store/geometry.rb → protocol/layout.rb} +32 -22
  92. data/lib/textus/protocol/links/resolver.rb +33 -0
  93. data/lib/textus/protocol/links/uri_rewriter.rb +30 -0
  94. data/lib/textus/protocol/links.rb +10 -0
  95. data/lib/textus/protocol/manifest/data.rb +90 -0
  96. data/lib/textus/protocol/manifest/entry/base.rb +95 -0
  97. data/lib/textus/protocol/manifest/entry/leaf.rb +19 -0
  98. data/lib/textus/protocol/manifest/entry/nested.rb +19 -0
  99. data/lib/textus/protocol/manifest/entry/parser.rb +71 -0
  100. data/lib/textus/protocol/manifest/entry/publish.rb +209 -0
  101. data/lib/textus/protocol/manifest/entry.rb +10 -0
  102. data/lib/textus/protocol/manifest/policy/publish_target.rb +36 -0
  103. data/lib/textus/protocol/manifest/policy.rb +69 -0
  104. data/lib/textus/protocol/manifest/resolver.rb +156 -0
  105. data/lib/textus/protocol/manifest/schema/contract.rb +53 -0
  106. data/lib/textus/protocol/manifest/schema/validator.rb +52 -0
  107. data/lib/textus/protocol/manifest/schema.rb +40 -0
  108. data/lib/textus/protocol/manifest.rb +62 -0
  109. data/lib/textus/protocol/produce/render/context.rb +43 -0
  110. data/lib/textus/protocol/produce/render.rb +44 -0
  111. data/lib/textus/protocol/schema/registry.rb +44 -0
  112. data/lib/textus/protocol/schema/tools.rb +104 -0
  113. data/lib/textus/protocol/schema.rb +117 -0
  114. data/lib/textus/protocol/session/bridge.rb +11 -0
  115. data/lib/textus/protocol/session/context.rb +5 -0
  116. data/lib/textus/protocol/session.rb +87 -0
  117. data/lib/textus/protocol/store_engine/delete.rb +37 -0
  118. data/lib/textus/protocol/store_engine/move.rb +109 -0
  119. data/lib/textus/protocol/store_engine/pipeline.rb +61 -0
  120. data/lib/textus/protocol/store_engine/put.rb +104 -0
  121. data/lib/textus/protocol/store_engine.rb +86 -0
  122. data/lib/textus/protocol/v4.rb +25 -0
  123. data/lib/textus/protocol/verb_registry/arg_spec.rb +19 -0
  124. data/lib/textus/protocol/verb_registry/core_verbs.rb +164 -0
  125. data/lib/textus/protocol/verb_registry/verb_spec.rb +42 -0
  126. data/lib/textus/protocol/verb_registry.rb +42 -0
  127. data/lib/textus/surface/cli/group/{rule.rb → web.rb} +2 -2
  128. data/lib/textus/surface/cli/runner.rb +25 -25
  129. data/lib/textus/surface/cli/sources.rb +1 -3
  130. data/lib/textus/surface/cli/verb/get.rb +3 -6
  131. data/lib/textus/surface/cli/verb/init.rb +1 -1
  132. data/lib/textus/surface/cli/verb/put.rb +8 -6
  133. data/lib/textus/surface/cli/verb/web_serve.rb +22 -0
  134. data/lib/textus/surface/cli.rb +25 -6
  135. data/lib/textus/surface/dispatch.rb +17 -0
  136. data/lib/textus/surface/mcp/adapters/mcp_adapter.rb +28 -0
  137. data/lib/textus/surface/mcp/catalog.rb +26 -62
  138. data/lib/textus/surface/mcp/cursor_manager.rb +22 -0
  139. data/lib/textus/surface/mcp/drift_annotation.rb +19 -0
  140. data/lib/textus/surface/mcp/errors.rb +0 -10
  141. data/lib/textus/surface/mcp/server.rb +46 -46
  142. data/lib/textus/surface/watcher.rb +18 -12
  143. data/lib/textus/surface/web.rb +50 -0
  144. data/lib/textus/value/call.rb +1 -1
  145. data/lib/textus/{core → value}/duration.rb +1 -4
  146. data/lib/textus/value/etag.rb +7 -7
  147. data/lib/textus/value/payload.rb +7 -0
  148. data/lib/textus/version.rb +1 -1
  149. data/lib/textus/workflow/consumer.rb +39 -0
  150. data/lib/textus/workflow/dsl/lifecycle.rb +32 -0
  151. data/lib/textus/workflow/dsl/pattern_matcher.rb +20 -0
  152. data/lib/textus/workflow/dsl/step_builder.rb +56 -0
  153. data/lib/textus/workflow/dsl.rb +111 -21
  154. data/lib/textus/workflow/event_emitter.rb +21 -0
  155. data/lib/textus/workflow/publisher.rb +59 -0
  156. data/lib/textus/workflow/registry.rb +31 -8
  157. data/lib/textus/workflow/runner.rb +25 -61
  158. data/lib/textus/workflow/scheduler.rb +45 -0
  159. data/lib/textus/workflow/step_executor.rb +78 -0
  160. data/lib/textus/workflow/step_helpers.rb +102 -0
  161. data/lib/textus/workflow.rb +2 -5
  162. data/lib/textus.rb +16 -80
  163. metadata +167 -183
  164. data/SPEC.md +0 -908
  165. data/docs/architecture/README.md +0 -273
  166. data/docs/reference/conventions.md +0 -138
  167. data/lib/textus/action/accept.rb +0 -46
  168. data/lib/textus/action/audit.rb +0 -94
  169. data/lib/textus/action/base.rb +0 -42
  170. data/lib/textus/action/blame.rb +0 -79
  171. data/lib/textus/action/boot.rb +0 -15
  172. data/lib/textus/action/data_mv.rb +0 -58
  173. data/lib/textus/action/deps.rb +0 -19
  174. data/lib/textus/action/doctor.rb +0 -17
  175. data/lib/textus/action/drain.rb +0 -31
  176. data/lib/textus/action/enqueue.rb +0 -37
  177. data/lib/textus/action/get.rb +0 -34
  178. data/lib/textus/action/ingest.rb +0 -199
  179. data/lib/textus/action/jobs.rb +0 -27
  180. data/lib/textus/action/key_delete.rb +0 -26
  181. data/lib/textus/action/key_delete_prefix.rb +0 -35
  182. data/lib/textus/action/key_mv.rb +0 -122
  183. data/lib/textus/action/key_mv_prefix.rb +0 -48
  184. data/lib/textus/action/list.rb +0 -28
  185. data/lib/textus/action/propose.rb +0 -42
  186. data/lib/textus/action/published.rb +0 -22
  187. data/lib/textus/action/pulse.rb +0 -49
  188. data/lib/textus/action/put.rb +0 -38
  189. data/lib/textus/action/rdeps.rb +0 -24
  190. data/lib/textus/action/reject.rb +0 -28
  191. data/lib/textus/action/rule_explain.rb +0 -81
  192. data/lib/textus/action/rule_lint.rb +0 -62
  193. data/lib/textus/action/rule_list.rb +0 -38
  194. data/lib/textus/action/schema_envelope.rb +0 -22
  195. data/lib/textus/action/uid.rb +0 -19
  196. data/lib/textus/action/where.rb +0 -21
  197. data/lib/textus/boot.rb +0 -202
  198. data/lib/textus/contract/arg.rb +0 -10
  199. data/lib/textus/contract/dsl.rb +0 -88
  200. data/lib/textus/contract/spec.rb +0 -25
  201. data/lib/textus/contract.rb +0 -12
  202. data/lib/textus/core/freshness/evaluator.rb +0 -150
  203. data/lib/textus/core/freshness/verdict.rb +0 -34
  204. data/lib/textus/core/freshness.rb +0 -11
  205. data/lib/textus/core/retention/sweep.rb +0 -57
  206. data/lib/textus/core/retention.rb +0 -11
  207. data/lib/textus/core/sentinel.rb +0 -30
  208. data/lib/textus/doctor/check/audit_log.rb +0 -34
  209. data/lib/textus/doctor/check/generator_drift.rb +0 -29
  210. data/lib/textus/doctor/check/illegal_keys.rb +0 -60
  211. data/lib/textus/doctor/check/manifest_files.rb +0 -25
  212. data/lib/textus/doctor/check/notebook_sources.rb +0 -50
  213. data/lib/textus/doctor/check/orphaned_publish_targets.rb +0 -35
  214. data/lib/textus/doctor/check/proposal_targets.rb +0 -45
  215. data/lib/textus/doctor/check/protocol_version.rb +0 -47
  216. data/lib/textus/doctor/check/publish_tree_index_overlap.rb +0 -48
  217. data/lib/textus/doctor/check/raw_asset_paths.rb +0 -50
  218. data/lib/textus/doctor/check/rule_ambiguity.rb +0 -49
  219. data/lib/textus/doctor/check/schema_parse_error.rb +0 -28
  220. data/lib/textus/doctor/check/schema_violations.rb +0 -28
  221. data/lib/textus/doctor/check/schemas.rb +0 -26
  222. data/lib/textus/doctor/check/sentinels.rb +0 -60
  223. data/lib/textus/doctor/check/stale_reviewed_stamp.rb +0 -54
  224. data/lib/textus/doctor/check/templates.rb +0 -29
  225. data/lib/textus/doctor/check/unowned_schema_fields.rb +0 -40
  226. data/lib/textus/doctor/check.rb +0 -41
  227. data/lib/textus/doctor/validator.rb +0 -93
  228. data/lib/textus/doctor.rb +0 -54
  229. data/lib/textus/format/base.rb +0 -49
  230. data/lib/textus/format/json.rb +0 -102
  231. data/lib/textus/format/markdown.rb +0 -80
  232. data/lib/textus/format/shared.rb +0 -17
  233. data/lib/textus/format/text.rb +0 -55
  234. data/lib/textus/format/yaml.rb +0 -125
  235. data/lib/textus/format.rb +0 -46
  236. data/lib/textus/gate/auth.rb +0 -212
  237. data/lib/textus/gate.rb +0 -92
  238. data/lib/textus/init.rb +0 -157
  239. data/lib/textus/jobs.rb +0 -9
  240. data/lib/textus/key/distance.rb +0 -55
  241. data/lib/textus/key/grammar.rb +0 -33
  242. data/lib/textus/key/matching.rb +0 -24
  243. data/lib/textus/key/path.rb +0 -28
  244. data/lib/textus/manifest/capabilities.rb +0 -29
  245. data/lib/textus/manifest/data.rb +0 -102
  246. data/lib/textus/manifest/entry/base.rb +0 -105
  247. data/lib/textus/manifest/entry/ignore_matcher.rb +0 -46
  248. data/lib/textus/manifest/entry/leaf.rb +0 -17
  249. data/lib/textus/manifest/entry/nested.rb +0 -37
  250. data/lib/textus/manifest/entry/parser.rb +0 -99
  251. data/lib/textus/manifest/entry/produced.rb +0 -36
  252. data/lib/textus/manifest/entry/publish/mode.rb +0 -45
  253. data/lib/textus/manifest/entry/publish/none.rb +0 -14
  254. data/lib/textus/manifest/entry/publish/subtree_mirror.rb +0 -73
  255. data/lib/textus/manifest/entry/publish/template.rb +0 -16
  256. data/lib/textus/manifest/entry/publish/to_paths.rb +0 -76
  257. data/lib/textus/manifest/entry/publish/tree.rb +0 -50
  258. data/lib/textus/manifest/entry/publish.rb +0 -45
  259. data/lib/textus/manifest/entry/validators/format_matrix.rb +0 -21
  260. data/lib/textus/manifest/entry/validators/ignore.rb +0 -28
  261. data/lib/textus/manifest/entry/validators/publish.rb +0 -30
  262. data/lib/textus/manifest/entry/validators.rb +0 -18
  263. data/lib/textus/manifest/entry.rb +0 -8
  264. data/lib/textus/manifest/policy/matcher.rb +0 -51
  265. data/lib/textus/manifest/policy/publish_target.rb +0 -34
  266. data/lib/textus/manifest/policy/react.rb +0 -30
  267. data/lib/textus/manifest/policy/retention.rb +0 -29
  268. data/lib/textus/manifest/policy/source.rb +0 -30
  269. data/lib/textus/manifest/policy.rb +0 -95
  270. data/lib/textus/manifest/resolver.rb +0 -118
  271. data/lib/textus/manifest/rules.rb +0 -86
  272. data/lib/textus/manifest/schema/contract.rb +0 -61
  273. data/lib/textus/manifest/schema/keys.rb +0 -90
  274. data/lib/textus/manifest/schema/semantics/cross_field.rb +0 -53
  275. data/lib/textus/manifest/schema/semantics/invariants.rb +0 -125
  276. data/lib/textus/manifest/schema/semantics/migration.rb +0 -83
  277. data/lib/textus/manifest/schema/semantics.rb +0 -27
  278. data/lib/textus/manifest/schema/validator.rb +0 -48
  279. data/lib/textus/manifest/schema/vocabulary.rb +0 -25
  280. data/lib/textus/manifest/schema.rb +0 -30
  281. data/lib/textus/manifest.rb +0 -65
  282. data/lib/textus/meta.rb +0 -54
  283. data/lib/textus/port/audit_log.rb +0 -268
  284. data/lib/textus/port/build_lock.rb +0 -64
  285. data/lib/textus/port/clock.rb +0 -10
  286. data/lib/textus/port/publisher.rb +0 -60
  287. data/lib/textus/port/sentinel_store.rb +0 -83
  288. data/lib/textus/port/storage/file_stat.rb +0 -19
  289. data/lib/textus/port/storage/file_store.rb +0 -26
  290. data/lib/textus/port/store.rb +0 -93
  291. data/lib/textus/port/watcher_lock.rb +0 -48
  292. data/lib/textus/produce/engine.rb +0 -50
  293. data/lib/textus/produce/render.rb +0 -20
  294. data/lib/textus/schema/tools.rb +0 -115
  295. data/lib/textus/schema.rb +0 -100
  296. data/lib/textus/schemas.rb +0 -54
  297. data/lib/textus/store/compositor.rb +0 -34
  298. data/lib/textus/store/container.rb +0 -43
  299. data/lib/textus/store/cursor.rb +0 -26
  300. data/lib/textus/store/envelope/reader.rb +0 -43
  301. data/lib/textus/store/envelope/writer.rb +0 -195
  302. data/lib/textus/store/index/builder.rb +0 -74
  303. data/lib/textus/store/index/lookup.rb +0 -60
  304. data/lib/textus/store/jobs/base.rb +0 -13
  305. data/lib/textus/store/jobs/index.rb +0 -15
  306. data/lib/textus/store/jobs/materialize.rb +0 -15
  307. data/lib/textus/store/jobs/plan.rb +0 -11
  308. data/lib/textus/store/jobs/planner.rb +0 -104
  309. data/lib/textus/store/jobs/queue.rb +0 -154
  310. data/lib/textus/store/jobs/registry.rb +0 -19
  311. data/lib/textus/store/jobs/retention.rb +0 -50
  312. data/lib/textus/store/jobs/sweep.rb +0 -21
  313. data/lib/textus/store/jobs/worker.rb +0 -64
  314. data/lib/textus/store/session.rb +0 -37
  315. data/lib/textus/store.rb +0 -99
  316. data/lib/textus/surface/cli/verb/doctor.rb +0 -19
  317. data/lib/textus/surface/cli/verb/schema_diff.rb +0 -17
  318. data/lib/textus/surface/cli/verb/schema_init.rb +0 -21
  319. data/lib/textus/surface/cli/verb/schema_migrate.rb +0 -21
  320. data/lib/textus/surface/mcp.rb +0 -8
  321. data/lib/textus/surface/projector.rb +0 -27
  322. data/lib/textus/surface/role_scope.rb +0 -34
  323. data/lib/textus/value/command.rb +0 -16
  324. data/lib/textus/value/envelope.rb +0 -89
  325. data/lib/textus/value/result.rb +0 -26
  326. data/lib/textus/workflow/collector.rb +0 -27
  327. data/lib/textus/workflow/context.rb +0 -5
  328. data/lib/textus/workflow/loader.rb +0 -17
  329. data/lib/textus/workflow/pattern.rb +0 -18
@@ -1,93 +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
- @root = root
15
- @path = Textus::Store::Geometry.new(root).store_db_path
16
- FileUtils.mkdir_p(File.dirname(@path))
17
- @connection = SQLite3::Database.new(@path)
18
- @connection.results_as_hash = true
19
- end
20
-
21
- def execute(sql, params = [])
22
- @connection.execute(sql, params)
23
- end
24
-
25
- def query_value(sql, params = [])
26
- @connection.get_first_value(sql, params)
27
- end
28
-
29
- def setup!
30
- execute("PRAGMA journal_mode=WAL")
31
- execute("PRAGMA foreign_keys=ON")
32
- connection.execute_batch(<<~SQL)
33
- CREATE TABLE IF NOT EXISTS entries (
34
- key TEXT PRIMARY KEY,
35
- lane TEXT NOT NULL,
36
- format TEXT NOT NULL,
37
- etag TEXT,
38
- content TEXT,
39
- extra TEXT,
40
- indexed_at TEXT NOT NULL
41
- ) STRICT;
42
-
43
- CREATE VIRTUAL TABLE IF NOT EXISTS entries_fts USING fts5(
44
- key, lane, content,
45
- content=entries, content_rowid=rowid
46
- );
47
-
48
- CREATE TABLE IF NOT EXISTS jobs (
49
- id TEXT PRIMARY KEY,
50
- type TEXT NOT NULL,
51
- args TEXT NOT NULL,
52
- state TEXT NOT NULL DEFAULT 'ready',
53
- role TEXT NOT NULL,
54
- attempts INTEGER NOT NULL DEFAULT 0,
55
- max_attempts INTEGER NOT NULL DEFAULT 3,
56
- errors TEXT,
57
- lease TEXT,
58
- created_at TEXT NOT NULL,
59
- updated_at TEXT NOT NULL
60
- ) STRICT;
61
-
62
- CREATE INDEX IF NOT EXISTS idx_jobs_state ON jobs(state);
63
- CREATE INDEX IF NOT EXISTS idx_entries_lane ON entries(lane);
64
- SQL
65
- self
66
- end
67
-
68
- def transaction
69
- connection.transaction
70
- yield
71
- connection.commit
72
- rescue StandardError
73
- connection.rollback if connection.transaction_active?
74
- raise
75
- end
76
-
77
- def close
78
- connection.close unless connection.closed?
79
- end
80
-
81
- def self.open(root)
82
- store = new(root: root)
83
- store.setup!
84
- return store unless block_given?
85
-
86
- yield store
87
- ensure
88
- store&.close
89
- end
90
- private :connection
91
- end
92
- end
93
- 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::Geometry.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::Geometry.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,50 +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
- entry = @container.manifest.resolver.resolve(key).entry
37
- return unless entry.publish_tree || !Array(entry.publish_to).empty?
38
-
39
- reader = Textus::Store::Envelope::Reader.from(container: @container)
40
- entry_path = @container.manifest.resolver.resolve(key).path
41
- return unless entry.publish_tree || File.exist?(entry_path)
42
-
43
- pctx = Textus::Manifest::Entry::Base::PublishContext.new(
44
- container: @container, call: @call, reader: reader.method(:read),
45
- )
46
- entry.publish_via(pctx)
47
- end
48
- end
49
- end
50
- 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,115 +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::Geometry.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.as(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
- scope = store.as(role)
89
- Value::Result.unwrap(
90
- Textus::Action::Get.call(
91
- container: scope.container,
92
- call: Textus::Value::Call.build(role: role),
93
- key: key,
94
- ),
95
- )
96
- end
97
-
98
- def self.load_schema(store, name)
99
- store.schemas.fetch(name)
100
- rescue IoError
101
- raise UsageError.new("schema not found: #{name}")
102
- end
103
-
104
- def self.accept_role_for(store)
105
- authority = store.manifest.policy.roles_with_capability("author").first
106
- return authority if authority
107
-
108
- raise UsageError.new(
109
- "schema migrate requires a role holding the 'author' capability; " \
110
- "none declared (add e.g. `- { name: owner, can: [author] }` to roles:)",
111
- )
112
- end
113
- end
114
- end
115
- end
data/lib/textus/schema.rb DELETED
@@ -1,100 +0,0 @@
1
- require "yaml"
2
-
3
- module Textus
4
- class Schema
5
- attr_reader :name, :required, :optional, :fields, :raw
6
-
7
- def self.load(path)
8
- raw = YAML.safe_load_file(path, permitted_classes: [Symbol, Date, Time], aliases: false)
9
- new(raw)
10
- end
11
-
12
- def initialize(raw)
13
- @raw = raw || {}
14
- @name = @raw["name"]
15
- @required = Array(@raw["required"])
16
- @optional = Array(@raw["optional"])
17
- @fields = @raw["fields"] || {}
18
- end
19
-
20
- def to_h
21
- @raw
22
- end
23
-
24
- def maintained_by(field)
25
- meta = @fields[field] or return nil
26
- meta["maintained_by"]
27
- end
28
-
29
- # Returns the list of field names whose spec is a Hash but lacks the
30
- # 'maintained_by' key. Used by Doctor::Check::UnownedSchemaFields.
31
- def unowned_fields
32
- @fields.each_with_object([]) do |(name, spec), acc|
33
- next unless spec.is_a?(Hash)
34
-
35
- acc << name if spec["maintained_by"].nil?
36
- end
37
- end
38
-
39
- def evolution
40
- raw = @raw["evolution"] || {}
41
- raw.each_with_object({}) do |(k, v), h|
42
- h[k] = v.is_a?(Date) || v.is_a?(Time) ? v.to_s : v
43
- end
44
- end
45
-
46
- # Returns nil on success; raises SchemaViolation on hard failure.
47
- # Unknown fields produce warnings, returned as a String[] alongside.
48
- def validate!(frontmatter)
49
- missing = @required - frontmatter.keys
50
- raise SchemaViolation.new("missing" => missing) unless missing.empty?
51
-
52
- known = (@required + @optional).uniq
53
- frontmatter.each do |k, v|
54
- next unless @fields.key?(k)
55
-
56
- check_type!(k, v, @fields[k])
57
- end
58
-
59
- warnings = frontmatter.keys - known
60
- warnings.map { |w| "unknown field: #{w}" }
61
- end
62
-
63
- private
64
-
65
- def check_type!(field, value, spec) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
66
- type = spec["type"]
67
- case type
68
- when "string"
69
- bad!(field, "expected string") unless value.is_a?(String)
70
- if (max = spec["max"]) && value.bytesize > max
71
- bad!(field, "exceeds max #{max}")
72
- end
73
- when "number"
74
- bad!(field, "expected number") unless value.is_a?(Numeric)
75
- when "boolean"
76
- bad!(field, "expected boolean") unless [true, false].include?(value)
77
- when "enum"
78
- values = Array(spec["values"])
79
- bad!(field, "not in enum #{values.inspect}") unless values.include?(value)
80
- when "array"
81
- bad!(field, "expected array") unless value.is_a?(Array)
82
- if (items = spec["items"])
83
- value.each_with_index { |v, i| check_type!("#{field}[#{i}]", v, items) }
84
- end
85
- when "object"
86
- bad!(field, "expected object") unless value.is_a?(Hash)
87
- if (sub = spec["fields"])
88
- sub.each { |fk, fspec| check_type!("#{field}.#{fk}", value[fk], fspec) if value.key?(fk) }
89
- end
90
- when nil
91
- # untyped — no check
92
- # unknown type spec — vendor extension; ignore
93
- end
94
- end
95
-
96
- def bad!(field, reason)
97
- raise SchemaViolation.new("field" => field, "reason" => reason)
98
- end
99
- end
100
- end
@@ -1,54 +0,0 @@
1
- module Textus
2
- # Eager-loading schema cache. Loads every *.yaml under +dir+ at construction.
3
- # A missing directory is treated as "no schemas" (does not raise) to mirror
4
- # the lazy behavior previously embedded in Store#schema_for.
5
- class Schemas
6
- def initialize(dir)
7
- @dir = dir
8
- @schemas = {}
9
- load_all
10
- end
11
-
12
- def fetch(name)
13
- @schemas[name] || raise(IoError.new("schema not found: #{File.join(@dir, "#{name}.yaml")}"))
14
- end
15
-
16
- # Only nil short-circuits. A missing-but-named schema still raises IoError.
17
- def fetch_or_nil(name)
18
- return nil if name.nil?
19
-
20
- fetch(name)
21
- end
22
-
23
- def all
24
- @schemas.values
25
- end
26
-
27
- # Name-keyed view: { canonical_name => Schema }. The key is the schema's
28
- # file stem, which is authoritative even when a schema file carries no
29
- # top-level `name:` (Schema#name reads the body and may be nil). Symmetric
30
- # with #all (values); use this when you need the names too.
31
- def by_name
32
- @schemas.dup
33
- end
34
-
35
- private
36
-
37
- def load_all
38
- return unless File.directory?(@dir)
39
-
40
- Dir.glob(File.join(@dir, "*.yaml")).each do |path|
41
- name = File.basename(path, ".yaml")
42
- begin
43
- @schemas[name] = Schema.load(path)
44
- rescue StandardError
45
- # Tolerate broken schema files at construction time so the rest of
46
- # the store remains loadable. Surfacing the failure is the job of
47
- # Doctor::Check::SchemaParseError. Lookups via #fetch still raise
48
- # IoError for the missing-but-named schema.
49
- next
50
- end
51
- end
52
- end
53
- end
54
- end
@@ -1,34 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- class Store
5
- class Compositor
6
- def initialize(container)
7
- @container = container
8
- end
9
-
10
- def write(key, mentry:, payload:, call:, if_etag: nil)
11
- Textus::Store::Envelope::Writer.from(container: @container, call: call)
12
- .put(key, mentry: mentry, payload: payload, if_etag: if_etag)
13
- end
14
-
15
- def read(key)
16
- Textus::Store::Envelope::Reader.from(container: @container).read(key)
17
- end
18
-
19
- def delete(key, call:, mentry: nil, if_etag: nil)
20
- Textus::Store::Envelope::Writer.from(container: @container, call: call)
21
- .delete(key, mentry: mentry, if_etag: if_etag)
22
- end
23
-
24
- def move(from_key:, to_key:, new_mentry:, call:, if_etag: nil)
25
- Textus::Store::Envelope::Writer.from(container: @container, call: call)
26
- .move(from_key: from_key, to_key: to_key, new_mentry: new_mentry, if_etag: if_etag)
27
- end
28
-
29
- def exists?(key)
30
- Textus::Store::Envelope::Reader.from(container: @container).exists?(key)
31
- end
32
- end
33
- end
34
- end
@@ -1,43 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- class Store
5
- class Container
6
- Infrastructure = Data.define(:file_store, :schemas, :audit_log, :job_store, :geometry)
7
- Coordination = Data.define(:manifest, :workflows, :gate, :compositor)
8
-
9
- def self.attribute_names
10
- @attribute_names ||= [:root] + Infrastructure.members + Coordination.members
11
- end
12
-
13
- def initialize(infra, coord)
14
- @infra = infra
15
- @coord = coord
16
- end
17
-
18
- attr_reader :infra, :coord
19
-
20
- def root
21
- @infra.geometry.root
22
- end
23
-
24
- Infrastructure.members.each do |name|
25
- define_method(name) { @infra.public_send(name) }
26
- end
27
-
28
- Coordination.members.each do |name|
29
- define_method(name) { @coord.public_send(name) }
30
- end
31
-
32
- def wire_gate!(gate, compositor)
33
- @coord = Coordination.new(
34
- manifest: @coord.manifest,
35
- workflows: @coord.workflows,
36
- gate:,
37
- compositor:,
38
- )
39
- self
40
- end
41
- end
42
- end
43
- end
@@ -1,26 +0,0 @@
1
- require "fileutils"
2
-
3
- module Textus
4
- class Store
5
- # Per-role cursor cache under <root>/.state/cursors/<role>. A convenience so
6
- # `textus pulse` (no --since) means "since I last looked". Gitignored;
7
- # losing it just re-emits recent deltas, never corrupts the store. ADR 0036/0038.
8
- class Cursor
9
- def initialize(root:, role:)
10
- @path = Store::Geometry.new(root).cursor_path(role)
11
- end
12
-
13
- def read
14
- Integer(File.read(@path).strip)
15
- rescue Errno::ENOENT, ArgumentError
16
- 0
17
- end
18
-
19
- def write(seq)
20
- FileUtils.mkdir_p(File.dirname(@path))
21
- File.write(@path, seq.to_s)
22
- seq
23
- end
24
- end
25
- end
26
- end
@@ -1,43 +0,0 @@
1
- module Textus
2
- class Store
3
- module Envelope
4
- # Read-only counterpart to EnvelopeWriter. Resolves a key, reads the
5
- # bytes, parses them via the format strategy, and hands back an
6
- # Envelope. Used by Mv (pre-move inspection) and by EnvelopeWriter
7
- # (existing-meta lookup for the uid/sources preservation step in #put).
8
- #
9
- # No audit, no events, no permission checks — those live one layer up.
10
- class Reader
11
- def self.from(container:)
12
- new(file_store: container.file_store, manifest: container.manifest,
13
- geometry: container.geometry)
14
- end
15
-
16
- def initialize(file_store:, manifest:, geometry:)
17
- @file_store = file_store
18
- @manifest = manifest
19
- @geometry = geometry
20
- end
21
-
22
- def read(key)
23
- res = @manifest.resolver.resolve(key)
24
- path = res.path
25
- return nil unless @file_store.exists?(path)
26
-
27
- mentry = res.entry
28
- raw = @file_store.read(path)
29
- parsed = Format.for(mentry.format).parse(raw, path: path)
30
- Textus::Value::Envelope.build(
31
- key: key, mentry: mentry, path: path,
32
- meta: parsed["_meta"], body: parsed["body"],
33
- etag: Value::Etag.for_bytes(raw), content: parsed["content"]
34
- )
35
- end
36
-
37
- def exists?(key)
38
- @file_store.exists?(@manifest.resolver.resolve(key).path)
39
- end
40
- end
41
- end
42
- end
43
- end