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,48 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- class Manifest
5
- module Schema
6
- # Orchestrates structural validation (dry-schema Contract) then cross-field
7
- # semantic checks (Semantics). Public interface unchanged: Validator.validate!(raw).
8
- module Validator
9
- module_function
10
-
11
- def validate!(raw)
12
- raise BadManifest.new("manifest must be a hash") unless raw.is_a?(Hash)
13
-
14
- # Root unknown-key check before Contract so it fires even when lanes: is empty.
15
- Semantics.walk(raw, ROOT_KEYS, "$")
16
-
17
- result = Contract.call(raw)
18
- raise BadManifest.new(format_first_error(result.errors.messages)) unless result.success?
19
-
20
- raise BadManifest.new("manifest must declare lanes:") if Array(raw["lanes"]).empty?
21
-
22
- Semantics.check!(raw)
23
- end
24
-
25
- # Format the first dry-schema error to match the legacy path-prefixed style:
26
- # "unknown key 'x' at '$.lanes[0]'" for extra-key errors;
27
- # "manifest structure error at <path>: <msg>" for type/value errors.
28
- def format_first_error(messages)
29
- msg = messages.first
30
- return "manifest structure error: unknown" unless msg
31
-
32
- parent = format_path(msg.path[0..-2])
33
- key = msg.path.last
34
-
35
- if msg.text == "is not allowed"
36
- "unknown key '#{key}' at '#{parent}'"
37
- else
38
- "manifest structure error at #{format_path(msg.path)}: #{msg.text}"
39
- end
40
- end
41
-
42
- def format_path(parts)
43
- "$" + Array(parts).map { |p| p.is_a?(Integer) ? "[#{p}]" : ".#{p}" }.join
44
- end
45
- end
46
- end
47
- end
48
- end
@@ -1,25 +0,0 @@
1
- module Textus
2
- class Manifest
3
- module Schema
4
- # The closed coordination vocabulary (ADR 0028; five in 0033; unified in
5
- # 0034; the quarantine + derived ZONE-KINDS folded into one `machine` kind
6
- # in ADR 0091). Each kind pairs with the capability that authorizes
7
- # originating bytes in it. ONE source of truth; the derived constants below
8
- # cannot drift. A BIJECTION again (0090 had two kinds → the converge capability; 0091
9
- # collapses them, so kind ↔ capability is 1:1).
10
- module Vocabulary
11
- LANES = {
12
- "canon" => "author",
13
- "workspace" => "keep",
14
- "machine" => "converge",
15
- "queue" => "propose",
16
- "raw" => "ingest",
17
- }.freeze
18
-
19
- LANE_KINDS = LANES.keys.freeze
20
- CAPABILITIES = LANES.values.uniq.freeze
21
- KIND_REQUIRES_VERB = LANES
22
- end
23
- end
24
- end
25
- end
@@ -1,30 +0,0 @@
1
- module Textus
2
- class Manifest
3
- # The manifest schema. Its data is split across Schema::Vocabulary (the
4
- # coordination vocabulary) and Schema::Keys (key whitelists + FIELD_REGISTRY)
5
- # as of ADR 0109; the validation walk lives in Schema::Validator (ADR 0107).
6
- # The constants are re-exported here so callers keep saying `Schema::LANES`.
7
- module Schema
8
- # Re-export the vocabulary.
9
- LANES = Vocabulary::LANES
10
- LANE_KINDS = Vocabulary::LANE_KINDS
11
- CAPABILITIES = Vocabulary::CAPABILITIES
12
- KIND_REQUIRES_VERB = Vocabulary::KIND_REQUIRES_VERB
13
- # Re-export the keys + registry.
14
- ROOT_KEYS = Keys::ROOT_KEYS
15
- ROLE_KEYS = Keys::ROLE_KEYS
16
- LANE_KEYS = Keys::LANE_KEYS
17
- ENTRY_KEYS = Keys::ENTRY_KEYS
18
- PUBLISH_KEYS = Keys::PUBLISH_KEYS
19
- SOURCE_KEYS = Keys::SOURCE_KEYS
20
- RETENTION_KEYS = Keys::RETENTION_KEYS
21
- AUDIT_KEYS = Keys::AUDIT_KEYS
22
- FIELD_REGISTRY = Keys::FIELD_REGISTRY
23
- RULE_KEYS = Keys::RULE_KEYS
24
- OWNER_SUBJECT_PATTERN = Keys::OWNER_SUBJECT_PATTERN
25
-
26
- # Public entry point — the validation walk lives in Schema::Validator (ADR 0107).
27
- def self.validate!(raw) = Validator.validate!(raw)
28
- end
29
- end
30
- end
@@ -1,65 +0,0 @@
1
- require "yaml"
2
-
3
- module Textus
4
- # Manifest is the composition record for a parsed manifest. It bundles
5
- # four collaborators:
6
- #
7
- # * data — frozen value: raw, root, zones, entries, audit_config, role_caps
8
- # * resolver — resolves keys → entry + path
9
- # * policy — zone/role authority (lane_writers, declared_kind, derived_entry?,
10
- # queue_lane?, permission_for, …)
11
- # * rules — match-block rule engine (lifecycle, handler allowlist, materialize, …)
12
- #
13
- # Use `manifest.data.entries`, `manifest.policy.declared_kind(z)`, etc.
14
- Manifest = Data.define(:data, :resolver, :policy, :rules)
15
- end
16
-
17
- require_relative "manifest/schema"
18
- require_relative "manifest/data"
19
- require_relative "manifest/policy"
20
- require_relative "manifest/resolver"
21
- require_relative "manifest/capabilities"
22
-
23
- # Reopen Textus::Manifest (defined above as a Data.define) to attach
24
- # class-level loaders and helpers.
25
- module Textus # rubocop:disable Style/OneClassPerFile
26
- class Manifest
27
- class << self
28
- def parse(yaml_text, root: ".")
29
- raw = YAML.safe_load(yaml_text, aliases: false)
30
- check_version!(raw, "<string>")
31
- build(raw, root)
32
- end
33
-
34
- def load(root)
35
- manifest_path = File.join(root, "manifest.yaml")
36
- raise IoError.new("manifest not found: #{manifest_path}") unless File.exist?(manifest_path)
37
-
38
- raw = YAML.safe_load_file(manifest_path, aliases: false)
39
- check_version!(raw, manifest_path)
40
- build(raw, root)
41
- end
42
-
43
- private
44
-
45
- def build(raw, root)
46
- data = Manifest::Data.parse(raw, root: root)
47
- new(
48
- data: data,
49
- resolver: Manifest::Resolver.new(data),
50
- policy: data.policy,
51
- rules: Manifest::Rules.parse(raw["rules"] || []),
52
- )
53
- end
54
-
55
- def check_version!(raw, source)
56
- return if raw["version"] == PROTOCOL
57
-
58
- raise BadFrontmatter.new(
59
- source,
60
- "unsupported manifest version #{raw["version"].inspect}; expected #{PROTOCOL.inspect}",
61
- )
62
- end
63
- end
64
- end
65
- end
@@ -1,55 +0,0 @@
1
- module Textus
2
- class Orchestration
3
- ListKeysQuery = Data.define(:prefix, :lane)
4
- MoveKeyCommand = Data.define(:old_key, :new_key, :if_etag, :dry_run)
5
- DeleteKeyCommand = Data.define(:key, :if_etag)
6
- AuditQuery = Data.define(:seq_since, :key, :lane, :role, :verb, :since, :correlation_id, :limit)
7
-
8
- def initialize(list_keys:, move_key:, delete_key:, audit_entries:)
9
- @list_keys = list_keys
10
- @move_key = move_key
11
- @delete_key = delete_key
12
- @audit_entries = audit_entries
13
- end
14
-
15
- def list_keys(prefix:, lane:, call:)
16
- query = ListKeysQuery.new(prefix: prefix, lane: lane)
17
- normalize(@list_keys.call(query, call), key: "rows")
18
- end
19
-
20
- def move_key(old_key:, new_key:, call:, if_etag: nil, dry_run: false)
21
- command = MoveKeyCommand.new(old_key: old_key, new_key: new_key, if_etag: if_etag, dry_run: dry_run)
22
- normalize(@move_key.call(command, call), key: "move")
23
- end
24
-
25
- def delete_key(key:, call:, if_etag: nil)
26
- command = DeleteKeyCommand.new(key: key, if_etag: if_etag)
27
- normalize(@delete_key.call(command, call), key: "delete")
28
- end
29
-
30
- # rubocop:disable Metrics/ParameterLists
31
- def audit_entries(call:, seq_since: nil, key: nil, lane: nil, role: nil, verb: nil, since: nil, correlation_id: nil, limit: nil)
32
- query = AuditQuery.new(
33
- seq_since: seq_since,
34
- key: key,
35
- lane: lane,
36
- role: role,
37
- verb: verb,
38
- since: since,
39
- correlation_id: correlation_id,
40
- limit: limit,
41
- )
42
- normalize(@audit_entries.call(query, call), key: "rows")
43
- end
44
- # rubocop:enable Metrics/ParameterLists
45
-
46
- private
47
-
48
- def normalize(result, key:)
49
- return result unless result.is_a?(Value::Result)
50
- return result if result.failure?
51
-
52
- Value::Result.success({ key => result.value })
53
- end
54
- end
55
- end
@@ -1,268 +0,0 @@
1
- require "fileutils"
2
- require "json"
3
- require "time"
4
-
5
- module Textus
6
- module Port
7
- # Append-only audit log adapter: writes and rotates the on-disk audit JSONL
8
- # under the store root. An instantiable class — it holds collaborators (the
9
- # root path + size/keep config), so each store binds its own instance. It
10
- # already satisfied ADR 0109's single-shape rule (every port is an
11
- # instantiable class) before that ADR's Clock/Publisher conversions, so it
12
- # was unchanged by them.
13
- class AuditLog
14
- DEFAULT_MAX_SIZE = 10_485_760
15
- DEFAULT_KEEP = 5
16
-
17
- def initialize(root = nil, layout: nil, max_size: DEFAULT_MAX_SIZE, keep: DEFAULT_KEEP)
18
- @geometry = layout || Textus::Store::Layout.new(root)
19
- @path = @geometry.audit_log_path
20
- @max_size = max_size
21
- @keep = keep
22
- end
23
-
24
- def last_writer_for(key)
25
- return nil unless File.exist?(@path)
26
-
27
- last_role = nil
28
- File.foreach(@path) do |line|
29
- parsed = parse_row(line.chomp)
30
- next unless parsed
31
- next unless parsed["key"] == key
32
- next unless %w[put delete key_delete].include?(parsed["verb"])
33
-
34
- last_role = parsed["role"]
35
- end
36
- last_role
37
- end
38
-
39
- def latest_seq
40
- return scan_max_seq(@path) if File.exist?(@path) && File.size(@path).positive?
41
-
42
- # Active log is empty/missing — consult the most recent rotated file's sidecar.
43
- meta = read_meta(1)
44
- return meta["max_seq"] if meta
45
-
46
- 0
47
- end
48
-
49
- def min_available_seq
50
- rotated_metas = (1..@keep).map { |n| read_meta(n) }.compact
51
- if rotated_metas.any?
52
- rotated_metas.map { |m| m["min_seq"] }.min
53
- elsif File.exist?(@path)
54
- File.foreach(@path) do |line|
55
- parsed = parse_row(line.chomp)
56
- return parsed["seq"] if parsed && parsed["seq"]
57
- end
58
- nil
59
- end
60
- end
61
-
62
- def append(role:, verb:, key:, etag_before:, etag_after:, extras: nil)
63
- FileUtils.mkdir_p(File.dirname(@path))
64
- File.open(@path, File::WRONLY | File::APPEND | File::CREAT, 0o644) do |f|
65
- f.flock(File::LOCK_EX)
66
- next_seq = current_max_seq_unlocked + 1
67
- row = assemble_row(next_seq, { role: role, verb: verb, key: key,
68
- etag_before: etag_before, etag_after: etag_after }, extras)
69
- f.write(JSON.generate(row) + "\n")
70
- f.flush
71
- rotate!(f) if f.size > @max_size
72
- end
73
- end
74
-
75
- # Scan log files with optional filters. Returns parsed row hashes.
76
- # Lane and timestamp filters are left to the caller (they need manifest
77
- # resolution and Time parsing the port shouldn't know about).
78
- def scan(seq_since: nil, key: nil, role: nil, verb: nil,
79
- correlation_id: nil, limit: nil)
80
- files = all_log_files
81
- return [] if files.empty?
82
-
83
- rows = []
84
- files.each do |file|
85
- File.foreach(file) do |line|
86
- parsed = parse_row(line.chomp)
87
- next unless parsed && matches?(parsed, seq_since:, key:, role:, verb:, correlation_id:)
88
-
89
- rows << parsed
90
- break if limit && rows.length >= limit
91
- end
92
- break if limit && rows.length >= limit
93
- end
94
- rows
95
- end
96
-
97
- # Returns an array of integrity-violation descriptors for the on-disk log.
98
- # Each entry is { "lineno" => Integer, "reason" => String, "detail" => String }.
99
- # Empty array means the log is well-formed (or doesn't exist yet).
100
- def verify_integrity
101
- return [] unless File.exist?(@path)
102
-
103
- [].tap do |out|
104
- iterate_with_prev_seq do |line, lineno, prev_seq|
105
- check_line_integrity(line, lineno, prev_seq, out)
106
- end
107
- end
108
- end
109
-
110
- private
111
-
112
- def iterate_with_prev_seq
113
- prev_seq = nil
114
- File.foreach(@path).with_index(1) do |line, lineno|
115
- yield line.chomp, lineno, prev_seq
116
- parsed = parse_row(line.chomp)
117
- prev_seq = parsed["seq"] if parsed&.dig("seq").is_a?(Integer)
118
- end
119
- end
120
-
121
- def check_line_integrity(line, lineno, prev_seq, out)
122
- violation = check_line(line, lineno)
123
- if violation
124
- out << violation
125
- return
126
- end
127
-
128
- parsed = parse_row(line)
129
- return unless parsed && (seq = parsed["seq"]).is_a?(Integer)
130
- return unless prev_seq && seq != prev_seq + 1
131
-
132
- out << {
133
- "lineno" => lineno,
134
- "reason" => "seq_gap",
135
- "detail" => "expected #{prev_seq + 1}, got #{seq}",
136
- }
137
- end
138
-
139
- def rotated(n)
140
- @geometry.audit_rotated_log_path(n)
141
- end
142
-
143
- def rotated_meta(n)
144
- @geometry.audit_rotated_meta_path(n)
145
- end
146
-
147
- # Caller holds the flock. Returns the highest seq across the active log,
148
- # OR the most-recent rotated file's max_seq if the active log is empty.
149
- def current_max_seq_unlocked
150
- return scan_max_seq(@path) if File.exist?(@path) && File.size(@path).positive?
151
-
152
- meta = read_meta(1)
153
- meta ? meta["max_seq"] : 0
154
- end
155
-
156
- def scan_max_seq(file)
157
- last = 0
158
- File.foreach(file) do |line|
159
- parsed = parse_row(line.chomp)
160
- last = parsed["seq"] if parsed && parsed["seq"].is_a?(Integer)
161
- end
162
- last
163
- end
164
-
165
- def scan_seq_range(file)
166
- min = nil
167
- max = 0
168
- File.foreach(file) do |line|
169
- parsed = parse_row(line.chomp)
170
- next unless parsed && parsed["seq"]
171
-
172
- min ||= parsed["seq"]
173
- max = parsed["seq"]
174
- end
175
- [min, max]
176
- end
177
-
178
- def read_meta(n)
179
- path = rotated_meta(n)
180
- return nil unless File.exist?(path)
181
-
182
- JSON.parse(File.read(path))
183
- rescue JSON::ParserError
184
- nil
185
- end
186
-
187
- def assemble_row(seq, fields, extras = nil)
188
- row = {
189
- "seq" => seq,
190
- "ts" => Time.now.utc.iso8601,
191
- "role" => fields[:role],
192
- "verb" => fields[:verb],
193
- "key" => fields[:key],
194
- "etag_before" => fields[:etag_before],
195
- "etag_after" => fields[:etag_after],
196
- }
197
-
198
- if extras.is_a?(Hash) && !extras.empty?
199
- extras = extras.dup
200
- %w[from_key to_key uid].each do |k|
201
- row[k] = extras.delete(k) if extras.key?(k)
202
- end
203
- row["extras"] = extras unless extras.empty?
204
- end
205
-
206
- row
207
- end
208
-
209
- # Called inside the flock, after a successful write that pushed size over max.
210
- # Renames audit.log → audit.log.1 (shifting older files), writes sidecar meta.
211
- def rotate!(open_file)
212
- open_file.flush
213
- min_seq, max_seq = scan_seq_range(@path)
214
- meta = { "min_seq" => min_seq, "max_seq" => max_seq, "rotated_at" => Time.now.utc.iso8601 }
215
-
216
- # Drop the file that would be shifted past @keep.
217
- FileUtils.rm_f(rotated(@keep))
218
- FileUtils.rm_f(rotated_meta(@keep))
219
-
220
- # Shift .N → .(N+1) for N = keep-1 down to 1.
221
- (@keep - 1).downto(1) do |n|
222
- File.rename(rotated(n), rotated(n + 1)) if File.exist?(rotated(n))
223
- File.rename(rotated_meta(n), rotated_meta(n + 1)) if File.exist?(rotated_meta(n))
224
- end
225
-
226
- # Active log → .1
227
- File.rename(@path, rotated(1))
228
- File.write(rotated_meta(1), JSON.generate(meta) + "\n")
229
- # Next append will create a fresh audit.log via File::CREAT.
230
- end
231
-
232
- def parse_row(line)
233
- return nil if line.empty?
234
-
235
- JSON.parse(line)
236
- rescue JSON::ParserError
237
- nil
238
- end
239
-
240
- def matches?(row, seq_since: nil, key: nil, role: nil, verb: nil, correlation_id: nil)
241
- return false if seq_since && row["seq"] <= seq_since
242
- return false if key && row["key"] != key
243
- return false if role && row["role"] != role
244
- return false if verb && row["verb"] != verb
245
- return false if correlation_id && row.dig("extras", "correlation_id") != correlation_id
246
-
247
- true
248
- end
249
-
250
- def all_log_files
251
- rotated = Dir.glob(@geometry.audit_log_glob)
252
- .reject { |path| path.end_with?(".meta.json") }
253
- .sort_by { |path| -path.scan(/\d+$/).first.to_i }
254
- active_log = File.exist?(@path) ? [@path] : []
255
- rotated + active_log
256
- end
257
-
258
- def check_line(stripped, lineno)
259
- return nil if stripped.empty?
260
-
261
- JSON.parse(stripped)
262
- nil
263
- rescue JSON::ParserError => e
264
- { "lineno" => lineno, "reason" => "invalid_json", "detail" => e.message }
265
- end
266
- end
267
- end
268
- end
@@ -1,64 +0,0 @@
1
- require "fileutils"
2
- require "socket"
3
- require "time"
4
-
5
- module Textus
6
- module Port
7
- # Cross-process build lock: a pid/host-stamped lockfile under the store root
8
- # that serializes converge's produce/sweep. An instantiable class — it holds
9
- # the root and lock state; `self.with(root:)` is a convenience that constructs
10
- # one and runs the block under the held lock. It already satisfied ADR 0109's
11
- # single-shape rule (every port is an instantiable class) before that ADR's
12
- # Clock/Publisher conversions, so it was unchanged by them.
13
- class BuildLock
14
- MAX_HOLDER_BYTES = 512
15
-
16
- def self.with(root:, &)
17
- new(root: root).acquire_or_raise(&)
18
- end
19
-
20
- def initialize(root:)
21
- @path = Textus::Store::Layout.new(root).lock_path("build")
22
- @file = nil
23
- end
24
-
25
- def acquire_or_raise
26
- FileUtils.mkdir_p(File.dirname(@path))
27
- @file = File.open(@path, File::RDWR | File::CREAT, 0o644)
28
- @file.close_on_exec = true
29
-
30
- unless @file.flock(File::LOCK_EX | File::LOCK_NB)
31
- holder = read_holder_safely
32
- @file.close
33
- @file = nil
34
- raise Textus::BuildInProgress.new(holder)
35
- end
36
-
37
- @file.truncate(0)
38
- @file.write("pid=#{Process.pid} started=#{Time.now.utc.iso8601} host=#{Socket.gethostname}\n")
39
- @file.flush
40
-
41
- yield
42
- ensure
43
- release
44
- end
45
-
46
- private
47
-
48
- def release
49
- return unless @file
50
-
51
- @file.flock(File::LOCK_UN)
52
- @file.close
53
- @file = nil
54
- end
55
-
56
- def read_holder_safely
57
- content = File.read(@path, MAX_HOLDER_BYTES)
58
- content.gsub(/[^[:print:]\t ]/, "").strip
59
- rescue StandardError
60
- "unknown"
61
- end
62
- end
63
- end
64
- end
@@ -1,10 +0,0 @@
1
- module Textus
2
- module Port
3
- # The wall clock. An instantiable class (ADR 0109) — uniform with the other
4
- # ports; `now` reads the system time. Callers that need a fixed time still
5
- # pass it as data via `Call#now`.
6
- class Clock
7
- def now = Time.now
8
- end
9
- end
10
- end
@@ -1,60 +0,0 @@
1
- require "fileutils"
2
-
3
- module Textus
4
- module Port
5
- # Publishes built artifacts from the store to repo-relative consumer paths.
6
- # Publish = copy + sentinel. The in-store file is already the consumer-shaped
7
- # artifact; no parsing or stripping.
8
- #
9
- # Sentinel I/O is delegated to Textus::Port::SentinelStore. Sentinels live
10
- # under `<store_root>/.run/sentinels/` (runtime, git-ignored — ADR 0070) and
11
- # mirror the target's repo-relative layout so consumer directories aren't
12
- # polluted with `.textus-managed.json` siblings.
13
- #
14
- # An instantiable class (ADR 0109).
15
- class Publisher
16
- def publish(source:, target:, store_root:, provenance_source: source)
17
- FileUtils.mkdir_p(File.dirname(target))
18
- guard_clobber(source, target, store_root)
19
- File.delete(target) if File.symlink?(target)
20
- FileUtils.cp(source, target)
21
- Textus::Port::SentinelStore.new.write!(target: target, source: provenance_source, store_root: store_root)
22
- end
23
-
24
- # Removes a previously-published file and its sentinel. No-op unless the
25
- # target is textus-managed — never deletes an unmanaged file.
26
- def unpublish(target:, store_root:)
27
- return unless managed?(target, store_root)
28
-
29
- FileUtils.rm_f(target)
30
- sentinel = Textus::Port::SentinelStore.new.sentinel_path(target, store_root)
31
- FileUtils.rm_f(sentinel)
32
- end
33
-
34
- private
35
-
36
- # Refuse to clobber an unmanaged target — EXCEPT adopt one whose bytes
37
- # already equal the source (ADR 0050: a migration copies files into the
38
- # store and publishes them back to where they already live, so the target
39
- # is byte-identical — nothing is at risk). Adoption writes no sentinel
40
- # here; the normal publish path below does, and the cp is a content no-op.
41
- # An unmanaged target whose content DIFFERS, or any unmanaged symlink, is
42
- # still refused — that is the guard's real job.
43
- def guard_clobber(source, target, store_root)
44
- return unless File.exist?(target) || File.symlink?(target)
45
- return if managed?(target, store_root)
46
- return if adoptable?(source, target)
47
-
48
- raise PublishError.new("refusing to clobber unmanaged file at #{target}", target: target)
49
- end
50
-
51
- def adoptable?(source, target)
52
- !File.symlink?(target) && File.file?(target) && FileUtils.identical?(source, target)
53
- end
54
-
55
- def managed?(target, store_root)
56
- File.exist?(Textus::Port::SentinelStore.new.sentinel_path(target, store_root))
57
- end
58
- end
59
- end
60
- end
@@ -1,25 +0,0 @@
1
- require "digest"
2
-
3
- module Textus
4
- module Port
5
- class Sentinel
6
- attr_reader :target, :source, :sha256, :mode
7
-
8
- def initialize(target:, source:, sha256:, mode:)
9
- @target = target
10
- @source = source
11
- @sha256 = sha256
12
- @mode = mode
13
- end
14
-
15
- def orphan?(file_stat) = @target.nil? || !file_stat.exists?(@target)
16
-
17
- def drift?(file_stat)
18
- return false if orphan?(file_stat)
19
- return false if @sha256.nil?
20
-
21
- Digest::SHA256.hexdigest(file_stat.read(@target)) != @sha256
22
- end
23
- end
24
- end
25
- end