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
@@ -0,0 +1,156 @@
1
+ module Textus
2
+ module Protocol
3
+ class Manifest
4
+ class Resolver
5
+ Resolution = ::Data.define(:entry, :path, :remaining)
6
+
7
+ MAX_DISTANCE_LEN = 200
8
+
9
+ def initialize(data)
10
+ @data = data
11
+ end
12
+
13
+ def resolve(key)
14
+ @data.validate_key!(key)
15
+ dr = Protocol::Key.resolve(key, @data.entries)
16
+ raise UnknownKey.new(key, suggestions: suggestions_for(key)) unless dr
17
+ raise UnknownKey.new(key, suggestions: suggestions_for(key)) unless dr.entry
18
+
19
+ Resolution.new(entry: dr.entry, path: build_path(dr.entry, dr.remaining), remaining: dr.remaining)
20
+ end
21
+
22
+ def suggestions_for(key)
23
+ candidates = enumerate.map { |r| r[:key] }
24
+ candidates.concat(@data.entries.reject { |e| nested_entry?(e) }.map(&:key))
25
+ candidates.uniq!
26
+ suggest(key, candidates, limit: 5)
27
+ rescue StandardError
28
+ []
29
+ end
30
+
31
+ def enumerate(prefix: nil, include_keyless: false)
32
+ out = @data.entries.flat_map do |entry|
33
+ nested_entry?(entry) ? enumerate_nested(entry, include_keyless: include_keyless) : enumerate_leaf(entry)
34
+ end
35
+ out.select! { |row| row[:key] == prefix || row[:key].start_with?("#{prefix}.") } if prefix
36
+ out.sort_by { |row| row[:key] }
37
+ end
38
+
39
+ private
40
+
41
+ def suggest(requested, candidates, limit: 5)
42
+ return [] if requested.nil? || requested.empty?
43
+
44
+ scored = candidates.first(200).map do |k|
45
+ prefix = self.class.shared_prefix_segments(requested, k)
46
+ dist = self.class.levenshtein(requested, k) || Float::INFINITY
47
+ [k, prefix, dist]
48
+ end
49
+ scored.sort_by { |(_, prefix, dist)| [-prefix, dist] }.first(limit).map(&:first)
50
+ end
51
+
52
+ def nested_entry?(entry)
53
+ entry.nested?
54
+ end
55
+
56
+ def build_path(entry, remaining)
57
+ if remaining.empty?
58
+ resolve_leaf_path(entry)
59
+ else
60
+ fmt = Textus::Protocol::Format.for(entry.format)
61
+ base = Textus::Protocol::Key::Path.normalize_relative_path(entry.path)
62
+ path = File.join(@data.root, base, *remaining)
63
+ path + fmt.nested_ext(remaining.last)
64
+ end
65
+ end
66
+
67
+ def enumerate_leaf(entry)
68
+ fp = resolve_leaf_path(entry)
69
+ File.exist?(fp) ? [{ key: entry.key, path: fp, manifest_entry: entry }] : []
70
+ end
71
+
72
+ def enumerate_nested(entry, include_keyless: false)
73
+ # publish_tree mirrors opaque payload by path — its files are never
74
+ # enumerated as keys (ADR 0047). Ask the resolved mode, not the path.
75
+ # The `include_keyless:` override is used only by the projection lister
76
+ # so that `from: project` selects can read source data from keyless
77
+ # nested entries (e.g. knowledge.evidence) without exposing them as
78
+ # addressable store keys in the public `list` surface.
79
+ return [] if entry.publish_mode.keyless? && !include_keyless
80
+
81
+ base = File.join(@data.root, Textus::Protocol::Key::Path.normalize_relative_path(entry.path))
82
+ return [] unless File.directory?(base)
83
+
84
+ Dir.glob(File.join(base, nested_glob(entry.format)))
85
+ .filter_map { |path| nested_row_for(entry, base, path) }
86
+ end
87
+
88
+ def nested_row_for(entry, base, path)
89
+ rel = path.sub(%r{\A#{Regexp.escape(base)}/?}, "")
90
+
91
+ stripped = rel.sub(/#{Regexp.escape(File.extname(rel))}\z/, "")
92
+ segs = stripped.split("/").reject { |s| s.empty? || s == "." }
93
+ return nil if segs.empty?
94
+
95
+ illegal = segs.find { |s| !valid_segment?(s) }
96
+ if illegal
97
+ warn("textus: skipping illegal key segment '#{illegal}' at #{path} — " \
98
+ "rename to match [a-z0-9][a-z0-9-]* (run 'textus doctor' for the full list)")
99
+ return nil
100
+ end
101
+
102
+ { key: (entry.key.split(".") + segs).join("."), path: path, manifest_entry: entry }
103
+ end
104
+
105
+ def valid_segment?(seg)
106
+ return false if seg.nil? || seg.empty?
107
+ return false if seg.length > Protocol::Key::Grammar::MAX_SEGMENT_LEN
108
+
109
+ seg.match?(Protocol::Key::Grammar::SEGMENT)
110
+ end
111
+
112
+ def resolve_leaf_path(entry)
113
+ Textus::Protocol::Key::Path.resolve(@data, entry)
114
+ end
115
+
116
+ def nested_glob(format)
117
+ Textus::Protocol::Format.for(format).nested_glob
118
+ end
119
+ class << self
120
+ private
121
+
122
+ def shared_prefix_segments(left, right)
123
+ asegs = left.split(".")
124
+ bsegs = right.split(".")
125
+ n = [asegs.length, bsegs.length].min
126
+ i = 0
127
+ i += 1 while i < n && asegs[i] == bsegs[i]
128
+ i
129
+ end
130
+
131
+ def levenshtein(left, right)
132
+ return nil if left.length > MAX_DISTANCE_LEN || right.length > MAX_DISTANCE_LEN
133
+ return right.length if left.empty?
134
+ return left.length if right.empty?
135
+
136
+ prev = (0..right.length).to_a
137
+ curr = Array.new(right.length + 1, 0)
138
+ (1..left.length).each do |i|
139
+ curr[0] = i
140
+ (1..right.length).each do |j|
141
+ cost = left[i - 1] == right[j - 1] ? 0 : 1
142
+ curr[j] = [
143
+ curr[j - 1] + 1,
144
+ prev[j] + 1,
145
+ prev[j - 1] + cost,
146
+ ].min
147
+ end
148
+ prev, curr = curr, prev
149
+ end
150
+ prev[right.length]
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,53 @@
1
+ require "dry/schema"
2
+
3
+ module Textus
4
+ module Protocol
5
+ class Manifest
6
+ module Schema
7
+ # rubocop:disable Metrics/BlockLength
8
+ Contract = Dry::Schema.JSON do
9
+ optional(:lanes).value(:array).each do
10
+ hash do
11
+ required(:name).value(:string)
12
+ required(:kind).value(included_in?: LANE_KINDS)
13
+ optional(:owner).value(:string)
14
+ optional(:desc).value(:string)
15
+ end
16
+ end
17
+
18
+ optional(:roles).value(:array).each do
19
+ hash do
20
+ required(:name).value(:string)
21
+ optional(:can).value(:array).each(:string)
22
+ end
23
+ end
24
+
25
+ optional(:entries).value(:array).each do
26
+ hash do
27
+ required(:key).value(:string)
28
+ optional(:format).value(:string)
29
+ optional(:schema).maybe(:string)
30
+ optional(:tracked).value(:bool)
31
+ optional(:naming).value(:string)
32
+ end
33
+ end
34
+
35
+ optional(:data).value(:hash)
36
+
37
+ optional(:audit).hash do
38
+ optional(:max_size).value(:integer)
39
+ optional(:keep).value(:integer)
40
+ end
41
+ optional(:worker).hash do
42
+ optional(:pool).value(:integer)
43
+ optional(:poll).value(:integer)
44
+ optional(:lease_ttl).value(:integer)
45
+ optional(:max_attempts).value(:integer)
46
+ end
47
+ optional(:version).value(:string)
48
+ end
49
+ # rubocop:enable Metrics/BlockLength
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Textus
4
+ module Protocol
5
+ class Manifest
6
+ module Schema
7
+ # Orchestrates structural validation (dry-schema Contract) then cross-field
8
+ # semantic checks (Semantics). Public interface unchanged: Validator.validate!(raw).
9
+ module Validator
10
+ module_function
11
+
12
+ def validate!(raw)
13
+ raise BadManifest.new("manifest must be a hash") unless raw.is_a?(Hash)
14
+
15
+ # Root unknown-key check before Contract so it fires even when lanes: is empty.
16
+ Schema.walk(raw, ROOT_KEYS, "$")
17
+
18
+ result = Contract.call(raw)
19
+ raise BadManifest.new(format_first_error(result.errors.messages)) unless result.success?
20
+
21
+ if raw.key?("data")
22
+ raise BadManifest.new("manifest must declare data:") if raw["data"].nil?
23
+ elsif Array(raw["lanes"]).empty?
24
+ raise BadManifest.new("manifest must declare lanes:")
25
+ end
26
+ end
27
+
28
+ # Format the first dry-schema error to match the legacy path-prefixed style:
29
+ # "unknown key 'x' at '$.lanes[0]'" for extra-key errors;
30
+ # "manifest structure error at <path>: <msg>" for type/value errors.
31
+ def format_first_error(messages)
32
+ msg = messages.first
33
+ return "manifest structure error: unknown" unless msg
34
+
35
+ parent = format_path(msg.path[0..-2])
36
+ key = msg.path.last
37
+
38
+ if msg.text == "is not allowed"
39
+ "unknown key '#{key}' at '#{parent}'"
40
+ else
41
+ "manifest structure error at #{format_path(msg.path)}: #{msg.text}"
42
+ end
43
+ end
44
+
45
+ def format_path(parts)
46
+ "$" + Array(parts).map { |p| p.is_a?(Integer) ? "[#{p}]" : ".#{p}" }.join
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,40 @@
1
+ module Textus
2
+ module Protocol
3
+ class Manifest
4
+ module Schema
5
+ # The closed coordination vocabulary (ADR 0028; five in 0033; unified in
6
+ # 0034; the quarantine + derived ZONE-KINDS folded into one `machine` kind
7
+ # in ADR 0091). Each kind pairs with the capability that authorizes
8
+ # originating bytes in it.
9
+ LANES = {
10
+ "canon" => "author",
11
+ "workspace" => "keep",
12
+ "machine" => "converge",
13
+ "queue" => "propose",
14
+ "raw" => "ingest",
15
+ }.freeze
16
+
17
+ LANE_KINDS = LANES.keys.freeze
18
+ CAPABILITIES = LANES.values.uniq.freeze
19
+ KIND_REQUIRES_VERB = LANES
20
+
21
+ # Key whitelists and rule-field registry (ADR 0109).
22
+ ROOT_KEYS = %w[version roles owner lanes entries data audit worker].freeze
23
+ LANE_KEYS = %w[name kind owner desc].freeze
24
+ ENTRY_KEYS = %w[key schema format naming tracked].freeze
25
+ AUDIT_KEYS = %w[max_size keep].freeze
26
+
27
+ OWNER_SUBJECT_PATTERN = /\A[a-z][a-z0-9_-]*\z/
28
+
29
+ # Public entry point — structural validation then semantic checks.
30
+ def self.validate!(raw) = Validator.validate!(raw)
31
+
32
+ def self.walk(raw, allowed, path)
33
+ raw.each_key do |k|
34
+ raise BadManifest.new("unknown key '#{k}' at '#{path}'") unless allowed.include?(k)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,62 @@
1
+ require "yaml"
2
+
3
+ module Textus
4
+ module Protocol
5
+ Manifest = Data.define(:data, :resolver, :policy)
6
+ end
7
+ end
8
+
9
+ require_relative "manifest/schema"
10
+ require_relative "manifest/data"
11
+ require_relative "manifest/policy"
12
+ require_relative "manifest/resolver"
13
+
14
+ module Textus
15
+ module Protocol
16
+ class Manifest
17
+ class << self
18
+ def parse(yaml_text, root: ".")
19
+ raw = YAML.safe_load(yaml_text, aliases: true)
20
+ check_version!(raw, "<string>")
21
+ build(raw, root)
22
+ end
23
+
24
+ def load(root)
25
+ manifest_path = File.join(root, "manifest.yaml")
26
+ raise IoError.new("manifest not found: #{manifest_path}") unless File.exist?(manifest_path)
27
+
28
+ raw = YAML.safe_load_file(manifest_path, aliases: true)
29
+ check_version!(raw, manifest_path)
30
+ build(raw, root)
31
+ end
32
+
33
+ private
34
+
35
+ def build(raw, root)
36
+ # Phase 1: structural data + authority policy (no entry validation)
37
+ data = Manifest::Data.parse(raw, root: root)
38
+ policy = Manifest::Policy.new(data)
39
+
40
+ policy.entries = data.entries
41
+
42
+ new(
43
+ data:,
44
+ resolver: Manifest::Resolver.new(data),
45
+ policy:,
46
+ )
47
+ end
48
+
49
+ def check_version!(raw, source)
50
+ return if raw["version"] == PROTOCOL
51
+
52
+ raise BadFrontmatter.new(
53
+ source,
54
+ "unsupported manifest version #{raw["version"].inspect}; expected #{PROTOCOL.inspect}",
55
+ )
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ Textus::Manifest = Textus::Protocol::Manifest
@@ -0,0 +1,43 @@
1
+ module Textus
2
+ module Protocol
3
+ module Produce
4
+ class Render
5
+ class Context
6
+ def self.for(locals:, resolver: nil, from_path: nil, from_key: nil, edge_store: nil)
7
+ new(locals, resolver, from_path, from_key, edge_store)
8
+ end
9
+
10
+ def initialize(locals, resolver, from_path, from_key, edge_store)
11
+ @locals = locals
12
+ @resolver = resolver
13
+ @from_path = from_path
14
+ @from_key = from_key
15
+ @edge_store = edge_store
16
+ end
17
+
18
+ def to_erb_binding
19
+ mod = Module.new
20
+ @locals.each { |k, v| mod.define_method(k.to_sym) { v } }
21
+ obj = Object.new.extend(mod)
22
+
23
+ if @resolver && @from_path
24
+ resolver = @resolver
25
+ from_path = @from_path
26
+ from_key = @from_key
27
+ edge_store = @edge_store
28
+ obj.define_singleton_method(:textus_link) do |key|
29
+ resolved = resolver.resolve(key: key, from_path: from_path)
30
+ edge_store&.record(from_key: from_key, to_key: key)
31
+ resolved
32
+ rescue Textus::Protocol::Links::Resolver::UnknownKeyError
33
+ "`textus get #{key}`"
34
+ end
35
+ end
36
+
37
+ obj.instance_eval { binding }
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,44 @@
1
+ module Textus
2
+ module Protocol
3
+ module Produce
4
+ class Render
5
+ def initialize(template_loader:, manifest: nil, source_publish_path: nil, entry_key: nil, edge_store: nil)
6
+ @template_loader = template_loader
7
+ @manifest = manifest
8
+ @source_publish_path = source_publish_path
9
+ @entry_key = entry_key
10
+ @edge_store = edge_store
11
+ end
12
+
13
+ def bytes_for(target:, data:, boot:)
14
+ raise ArgumentError.new("Produce::Render called for a verbatim target #{target.to.inspect}") unless target.renders?
15
+
16
+ locals = target.inject_boot ? data.merge("boot" => boot) : data
17
+ resolver = @manifest ? Textus::Protocol::Links::Resolver.new(manifest: @manifest) : nil
18
+ ctx = Render::Context.for(
19
+ locals: locals,
20
+ resolver: resolver,
21
+ from_path: target.to,
22
+ from_key: @entry_key,
23
+ edge_store: @edge_store,
24
+ )
25
+ raw = ERB.new(@template_loader.call(target.template), trim_mode: "-").result(ctx.to_erb_binding)
26
+ rewrite(raw, target.to, resolver)
27
+ end
28
+
29
+ private
30
+
31
+ def rewrite(bytes, from_path, resolver)
32
+ return bytes unless resolver && bytes.include?("textus:")
33
+
34
+ Textus::Protocol::Links::UriRewriter.new(
35
+ resolver: resolver,
36
+ from_path: from_path,
37
+ from_key: @entry_key,
38
+ edge_store: @edge_store,
39
+ ).rewrite(bytes)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,44 @@
1
+ module Textus
2
+ module Protocol
3
+ class Schema
4
+ class Registry
5
+ def initialize(dir)
6
+ @dir = dir
7
+ @schemas = {}
8
+ load_all
9
+ end
10
+
11
+ def fetch(name)
12
+ @schemas[name] || raise(Textus::IoError.new("schema not found: #{File.join(@dir, "#{name}.yaml")}"))
13
+ end
14
+
15
+ def fetch_or_nil(name)
16
+ return nil if name.nil?
17
+
18
+ fetch(name)
19
+ end
20
+
21
+ def all
22
+ @schemas.values
23
+ end
24
+
25
+ def by_name
26
+ @schemas.dup
27
+ end
28
+
29
+ private
30
+
31
+ def load_all
32
+ return unless File.directory?(@dir)
33
+
34
+ Dir.glob(File.join(@dir, "**", "*.yaml")).each do |path|
35
+ name = path.delete_prefix("#{@dir}/").sub(/\.yaml\z/, "")
36
+ @schemas[name] = Textus::Protocol::Schema.load(path)
37
+ rescue StandardError => e
38
+ warn "textus: failed to load schema '#{name}' at #{path}: #{e.message}"
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,104 @@
1
+ require "yaml"
2
+ require "fileutils"
3
+
4
+ module Textus
5
+ module Protocol
6
+ class Schema
7
+ module Tools
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::Protocol::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
+ def self.diff(store, name:)
25
+ schema = load_schema(store, name)
26
+ drift = []
27
+ store.manifest.resolver.enumerate.each do |row|
28
+ env = pure_get(store, Textus::Value::Role::DEFAULT, row[:key])
29
+ begin
30
+ schema.validate!(env["_meta"])
31
+ rescue SchemaViolation => e
32
+ drift << { "key" => row[:key], "details" => e.details }
33
+ end
34
+ end
35
+ { "protocol" => PROTOCOL, "schema_name" => name, "drift" => drift }
36
+ end
37
+
38
+ def self.migrate(store, name:, rename: nil)
39
+ renames =
40
+ if rename
41
+ old_field, new_field = rename.split(":", 2)
42
+ raise UsageError.new("--rename=OLD:NEW") unless old_field && new_field && !new_field.empty?
43
+
44
+ { old_field => new_field }
45
+ else
46
+ load_schema(store, name).evolution["migrate_from"] || {}
47
+ end
48
+ raise UsageError.new("schema migrate needs --rename=OLD:NEW or schema.evolution.migrate_from") if renames.empty?
49
+
50
+ authority = accept_role_for(store)
51
+ ops = store.with_role(authority)
52
+ touched = []
53
+ store.manifest.resolver.enumerate.each do |row|
54
+ env = pure_get(store, authority, row[:key])
55
+ meta = (env["_meta"] || {}).dup
56
+ changed = false
57
+ renames.each do |old, new|
58
+ if meta.key?(old)
59
+ meta[new] = meta.delete(old)
60
+ changed = true
61
+ end
62
+ end
63
+ next unless changed
64
+
65
+ ops.put(key: row[:key], meta: meta, body: env["body"])
66
+ touched << row[:key]
67
+ end
68
+ { "protocol" => PROTOCOL, "migrated" => touched, "renames" => renames }
69
+ end
70
+
71
+ def self.infer_type(value)
72
+ case value
73
+ when String then "string"
74
+ when Numeric then "number"
75
+ when true, false then "boolean"
76
+ when Array then "array"
77
+ when Hash then "object"
78
+ else "string"
79
+ end
80
+ end
81
+
82
+ def self.pure_get(store, role, key)
83
+ store.with_role(role).get(key: key)
84
+ end
85
+
86
+ def self.load_schema(store, name)
87
+ store.schemas.fetch(name)
88
+ rescue IoError
89
+ raise UsageError.new("schema not found: #{name}")
90
+ end
91
+
92
+ def self.accept_role_for(store)
93
+ authority = store.manifest.policy.writers_for("knowledge").first
94
+ return authority if authority
95
+
96
+ raise UsageError.new(
97
+ "schema migrate requires a role that can write knowledge; " \
98
+ "none found in the fixed lane table",
99
+ )
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end