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
data/lib/textus/format.rb DELETED
@@ -1,46 +0,0 @@
1
- module Textus
2
- module Format
3
- SEP = "---".freeze
4
-
5
- STRATEGIES = {
6
- "markdown" => -> { Format::Markdown },
7
- "json" => -> { Format::Json },
8
- "yaml" => -> { Format::Yaml },
9
- "text" => -> { Format::Text },
10
- }.freeze
11
-
12
- EXT_TO_FORMAT = {
13
- ".md" => "markdown",
14
- ".json" => "json",
15
- ".yaml" => "yaml",
16
- ".yml" => "yaml",
17
- ".txt" => "text",
18
- }.freeze
19
-
20
- def self.for(format)
21
- STRATEGIES.fetch(format.to_s) { raise Textus::UsageError.new("unknown entry format: #{format.inspect}") }.call
22
- end
23
-
24
- def self.infer_from_extension(ext)
25
- EXT_TO_FORMAT[ext]
26
- end
27
-
28
- def self.formats
29
- EXT_TO_FORMAT.values.uniq
30
- end
31
-
32
- def self.parse(raw, path: nil, format: "markdown")
33
- Format.for(format).parse(raw, path: path)
34
- end
35
-
36
- def self.serialize(meta: {}, body: "", content: nil, format: "markdown")
37
- Format.for(format).serialize(meta: meta, body: body, content: content)
38
- end
39
-
40
- def self.data_to_payload(data, format)
41
- return { meta: {}, body: "", content: nil } if data.nil?
42
-
43
- Format.for(format).data_to_payload(data)
44
- end
45
- end
46
- end
@@ -1,212 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- class Gate
5
- class Auth
6
- FLOOR = {
7
- put: %w[lane_writable_by raw_lane_ingest_only],
8
- key_delete: %w[lane_deletable_by],
9
- key_mv: %w[lane_writable_by raw_lane_ingest_only],
10
- accept: %w[author_held],
11
- reject: %w[author_held],
12
- propose: %w[lane_writable_by raw_lane_ingest_only],
13
- key_mv_prefix: %w[lane_writable_by raw_lane_ingest_only],
14
- key_delete_prefix: %w[lane_writable_by raw_lane_ingest_only],
15
- ingest: %w[lane_writable_by raw_write_once],
16
- }.freeze
17
-
18
- AuthContext = Struct.new(
19
- :actor, :actor_caps, :lane_verb,
20
- :action, :target, :envelope,
21
- :mentry, :manifest,
22
- keyword_init: true
23
- )
24
-
25
- def initialize(container)
26
- @manifest = container.manifest
27
- @schemas = container.schemas
28
- end
29
-
30
- def check!(cmd)
31
- key = extract_key(cmd)
32
- return unless key
33
-
34
- evaluate_predicates(
35
- action: cmd.verb,
36
- actor: cmd.role.to_s,
37
- key: key,
38
- envelope: nil,
39
- extra: {},
40
- )
41
- end
42
-
43
- # Backward-compatible check for inline action auth (accept, put, etc.).
44
- def check_action!(action:, actor:, key:, envelope: nil, extra: {})
45
- evaluate_predicates(
46
- action: action.to_sym,
47
- actor: actor,
48
- key: key,
49
- envelope: envelope,
50
- extra: extra,
51
- )
52
- end
53
-
54
- private
55
-
56
- def evaluate_predicates(action:, actor:, key:, envelope:, extra:)
57
- mentry = @manifest.resolver.resolve(key).entry
58
- lane_verb = @manifest.policy.verb_for_lane(mentry.lane.to_s)
59
- actor_caps = Set.new(@manifest.data.role_caps.fetch(actor, []))
60
-
61
- ctx = AuthContext.new(
62
- actor:, actor_caps:, lane_verb:,
63
- action:, target: key, envelope:,
64
- mentry:, manifest: @manifest
65
- )
66
-
67
- failures = []
68
- floor_preds = FLOOR.fetch(action, [])
69
- rule_preds = rule_declared_predicates(action, key)
70
- (floor_preds + rule_preds).uniq.each do |pred|
71
- result = evaluate(pred, ctx, extra)
72
- next if result[:pass]
73
- raise result[:error] if result[:error]
74
-
75
- failures << [pred, result[:reason]]
76
- end
77
- raise Textus::GuardFailed.new(failures) unless failures.empty?
78
- end
79
-
80
- def extract_key(cmd)
81
- cmd.params.key?(:pending_key) ? cmd.pending_key : cmd.key
82
- end
83
-
84
- def rule_declared_predicates(action, key)
85
- guard_map = @manifest.rules.for(key).guard
86
- return [] if guard_map.nil?
87
-
88
- Array(guard_map[action.to_s])
89
- end
90
-
91
- def evaluate(pred_name, ctx, extra)
92
- case pred_name
93
- when "lane_writable_by" then evaluate_lane_writable_by(ctx)
94
- when "author_held" then evaluate_author_held(ctx)
95
- when "target_is_canon" then evaluate_target_is_canon(ctx)
96
- when "etag_match" then evaluate_etag_match(ctx, extra)
97
- when "schema_valid" then evaluate_schema_valid(ctx)
98
- when "fresh_within" then { pass: true }
99
- when "raw_lane_ingest_only" then evaluate_raw_lane_ingest_only(ctx)
100
- when "raw_write_once" then evaluate_raw_write_once(ctx)
101
- when "lane_deletable_by" then evaluate_lane_deletable_by(ctx)
102
- else raise Textus::UsageError.new("unknown predicate '#{pred_name}'")
103
- end
104
- end
105
-
106
- def evaluate_lane_writable_by(ctx)
107
- pass = ctx.actor_caps.include?(ctx.lane_verb.to_s)
108
- return { pass: true } if pass
109
-
110
- holders = @manifest.policy.roles_with_capability(ctx.lane_verb.to_s)
111
- { pass: false, error: Textus::WriteForbidden.new(ctx.mentry.key, ctx.mentry.lane, verb: ctx.lane_verb, holders:) }
112
- end
113
-
114
- def evaluate_author_held(ctx)
115
- holders = @manifest.policy.roles_with_capability("author")
116
- pass = holders.include?(ctx.actor.to_s)
117
- reason = if pass
118
- nil
119
- elsif holders.empty?
120
- "no role holds the 'author' capability; #{ctx.action} is disabled"
121
- else
122
- "role '#{ctx.actor}' lacks the 'author' capability (held by: #{holders.join(", ")})"
123
- end
124
- { pass:, reason: }
125
- end
126
-
127
- def evaluate_target_is_canon(ctx)
128
- kind = @manifest.policy.declared_kind(ctx.mentry.lane.to_s)
129
- pass = kind == :canon
130
- { pass:, reason: pass ? nil : "target lane '#{ctx.mentry.lane}' is not canon (kind: #{kind})" }
131
- end
132
-
133
- def evaluate_etag_match(ctx, extra)
134
- if_etag = extra[:if_etag]
135
- return { pass: true } if if_etag.nil?
136
-
137
- current = ctx.envelope&.etag
138
- pass = current.nil? || current == if_etag
139
- { pass:, error: pass ? nil : Textus::EtagMismatch.new(ctx.target, if_etag, current) }
140
- end
141
-
142
- def evaluate_schema_valid(ctx)
143
- return { pass: true } unless ctx.envelope
144
-
145
- schema_ref = ctx.mentry.schema
146
- return { pass: true } unless schema_ref
147
-
148
- schema = @schemas.fetch_or_nil(schema_ref)
149
- return { pass: true } unless schema
150
-
151
- frontmatter = ctx.envelope.meta&.dig("_meta") || ctx.envelope.meta || {}
152
- begin
153
- schema.validate!(frontmatter)
154
- { pass: true }
155
- rescue Textus::SchemaViolation => e
156
- { pass: false, reason: schema_reason(e) }
157
- end
158
- end
159
-
160
- def evaluate_raw_lane_ingest_only(ctx)
161
- return { pass: true } unless @manifest.policy.declared_kind(ctx.mentry.lane.to_s) == :raw
162
- return { pass: true } if ctx.action == :ingest
163
-
164
- { pass: false, error: Textus::Error.new(
165
- :raw_lane_ingest_only,
166
- "raw lane '#{ctx.mentry.lane}' only accepts `textus ingest` — " \
167
- "use that verb instead of '#{ctx.action}'",
168
- ) }
169
- end
170
-
171
- def evaluate_raw_write_once(ctx)
172
- path = @manifest.resolver.resolve(ctx.target).path
173
- return { pass: true } unless File.exist?(path)
174
-
175
- { pass: false, error: Textus::Error.new(
176
- :raw_write_once,
177
- "raw entry '#{ctx.target}' already exists; " \
178
- "delete it first (`textus key-delete #{ctx.target}`), then re-ingest",
179
- ) }
180
- end
181
-
182
- # Deletion authority: the lane's write capability OR the author capability.
183
- # On raw-kind lanes only the author capability grants deletion (correction
184
- # escape hatch); the lane's own verb (ingest) is write-only. On all other
185
- # lane kinds the behaviour matches lane_writable_by — the lane's writer
186
- # can delete as before.
187
- def evaluate_lane_deletable_by(ctx)
188
- is_raw = @manifest.policy.declared_kind(ctx.mentry.lane.to_s) == :raw
189
- pass = if is_raw
190
- ctx.actor_caps.include?("author")
191
- else
192
- ctx.actor_caps.include?(ctx.lane_verb.to_s) || ctx.actor_caps.include?("author")
193
- end
194
- return { pass: true } if pass
195
-
196
- extra_holders = is_raw ? ["author"] : [ctx.lane_verb.to_s, "author"]
197
- holders = extra_holders.flat_map { |v| @manifest.policy.roles_with_capability(v) }.uniq
198
- { pass: false, error: Textus::WriteForbidden.new(ctx.mentry.key, ctx.mentry.lane,
199
- verb: ctx.lane_verb, holders:) }
200
- end
201
-
202
- def schema_reason(err)
203
- d = err.details
204
- return err.message.dup unless d.is_a?(Hash)
205
- return "missing required fields: #{Array(d["missing"]).join(", ")}" if d["missing"]
206
- return "field '#{d["field"]}': #{d["reason"]}" if d["field"]
207
-
208
- err.message.dup
209
- end
210
- end
211
- end
212
- end
data/lib/textus/gate.rb DELETED
@@ -1,92 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- class Gate
5
- def initialize(container)
6
- @container = container
7
- end
8
-
9
- def dispatch(spec:, inputs:, role:, correlation_id: nil, session: nil, surface: nil)
10
- resolved = Binder.bind(spec, inputs, session: session)
11
- cmd = Value::Command.new(verb: spec.verb, params: resolved.freeze, role: role)
12
-
13
- cmd = normalize_propose_key(cmd) if cmd.verb == :propose
14
- action_class = Textus::Action::VERBS.fetch(cmd.verb) do
15
- raise Textus::UsageError.new("unknown command verb: #{cmd.verb}")
16
- end
17
-
18
- auth = Gate::Auth.new(@container)
19
- auth.check!(cmd)
20
- check_dispatch_auth(cmd, resolved, auth)
21
- call_obj = build_call(cmd, correlation_id: correlation_id)
22
- result = run_action(action_class, resolved, call_obj)
23
- cascade(cmd, result, call_obj) if CASCADE_VERBS.include?(cmd.verb) && !call_obj.dry_run
24
- return result unless surface
25
-
26
- spec.view(surface).call(result, resolved)
27
- end
28
-
29
- CASCADE_VERBS = %i[put propose accept reject key_mv key_delete].freeze
30
-
31
- AUTH_KEYS = {
32
- key_mv: ->(params) { [params[:old_key], params[:new_key]] },
33
- ingest: ->(params) { Textus::Action::Ingest.dispatch_key(**params) },
34
- }.freeze
35
-
36
- private
37
-
38
- def check_dispatch_auth(cmd, resolved, auth)
39
- return unless (resolver = AUTH_KEYS[cmd.verb])
40
-
41
- keys = Array(resolver.call(resolved))
42
- keys.each { |k| auth.check_action!(action: cmd.verb, actor: cmd.role, key: k) }
43
- end
44
-
45
- def normalize_propose_key(cmd)
46
- return cmd if cmd.pending_key
47
-
48
- zone = @container.manifest.policy.propose_lane_for(cmd.role.to_s)
49
- key = zone ? "#{zone}.#{cmd.key}" : nil
50
- cmd.with(params: cmd.params.merge(pending_key: key))
51
- end
52
-
53
- def run_action(klass, params, call_obj)
54
- result = klass.call(container: @container, call: call_obj, **params)
55
- Value::Result.unwrap(result)
56
- end
57
-
58
- def build_call(cmd, correlation_id: nil)
59
- dry_run = cmd.params.key?(:dry_run) ? !cmd.params[:dry_run].nil? : false
60
- Textus::Value::Call.build(role: cmd.role, dry_run:, correlation_id: correlation_id)
61
- end
62
-
63
- def cascade(cmd, result, call)
64
- key = result.is_a?(Hash) ? result["cascade_key"] : nil
65
- key ||= cascade_key_from_params(cmd)
66
- return unless key
67
-
68
- rdeps_result = Textus::Action::Rdeps.call(container: @container, call: call, key: key)
69
- rdeps = Value::Result.unwrap(rdeps_result).fetch("rdeps", [])
70
- producible = rdeps.select { |dep_key| producible?(dep_key) }
71
- producible.each do |dep_key|
72
- Textus::Store::Jobs::Materialize.call(container: @container, call: call, key: dep_key)
73
- end
74
- end
75
-
76
- def cascade_key_from_params(cmd)
77
- case cmd.verb
78
- when :put, :key_delete then cmd.params[:key]
79
- when :key_mv then cmd.params[:new_key]
80
- when :propose, :reject then cmd.params[:pending_key]
81
- when :accept then nil
82
- end
83
- end
84
-
85
- def producible?(key)
86
- entry = @container.manifest.resolver.resolve(key).entry
87
- !entry.publish_tree.nil?
88
- rescue Textus::Error
89
- false
90
- end
91
- end
92
- end
data/lib/textus/init.rb DELETED
@@ -1,157 +0,0 @@
1
- require "fileutils"
2
- require "pathname"
3
-
4
- module Textus
5
- module Init
6
- ZONES = %w[knowledge notebook proposals artifacts raw].freeze
7
-
8
- DEFAULT_MANIFEST = <<~YAML
9
- version: textus/4
10
- roles:
11
- - { name: human, can: [author, propose] }
12
- - { name: agent, can: [propose, keep] }
13
- - { name: automation, can: [converge] }
14
- lanes:
15
- - { name: knowledge, kind: canon, desc: "the maintained source of truth (identity.* lives here)" }
16
- - { name: notebook, kind: workspace, owner: agent, desc: "the agent's own durable working notes" }
17
- - { name: proposals, kind: queue, desc: "changes awaiting your accept" }
18
- - { name: artifacts, kind: machine, desc: "machine-maintained: external inputs (artifacts.feeds.*) + computed outputs (artifacts.derived.*)" }
19
- entries:
20
- - { key: knowledge.identity, path: data/knowledge/identity.md, lane: knowledge, schema: null, owner: human:self, kind: leaf }
21
- - { key: knowledge.notes, path: data/knowledge/notes, lane: knowledge, schema: null, owner: human:self, nested: true, kind: nested }
22
- - { key: notebook.notes, path: data/notebook/notes, lane: notebook, schema: null, owner: agent:self, nested: true, kind: nested }
23
- - { key: proposals.notes, path: data/proposals/notes, lane: proposals, schema: null, owner: agent:self, nested: true, kind: nested }
24
- # A per-host snapshot, populated by a registered workflow. Nested so it
25
- # grows to a fleet — add leaves over SSH without renaming. tracked:false →
26
- # gitignored (machine info can be sensitive/noisy) but still protocol-readable
27
- # via `textus get artifacts.feeds.machines.local`. Delete to opt out. (ADR 0043)
28
- - key: artifacts.feeds.machines
29
- path: data/artifacts/feeds/machines
30
- lane: artifacts
31
- format: yaml
32
- nested: true
33
- tracked: false
34
- kind: nested
35
- rules: []
36
- YAML
37
-
38
- AGENT_ENTRIES = <<~YAML.gsub(/^/, " ")
39
- - { key: knowledge.project, path: data/knowledge/project.md, lane: knowledge, schema: project, owner: human:self, kind: leaf }
40
- - { key: knowledge.runbooks, path: data/knowledge/runbooks, lane: knowledge, schema: runbook, owner: human:self, nested: true, kind: nested }
41
- - key: artifacts.derived.orientation
42
- path: data/artifacts/derived/orientation.json
43
- lane: artifacts
44
- publish:
45
- - { to: CLAUDE.md, template: orientation.erb, inject_boot: true }
46
- - { to: AGENTS.md, template: orientation.erb, inject_boot: true }
47
- kind: produced
48
- YAML
49
-
50
- def self.run(target_root, with_agent: false)
51
- check_target!(target_root)
52
- scaffold_dir = File.expand_path("init/templates", __dir__)
53
- create_directories(target_root)
54
- write_manifest(target_root, with_agent:)
55
- mcp_status = scaffold_agent(target_root, scaffold_dir, with_agent:)
56
- setup_state_dirs(target_root)
57
- write_gitignore(target_root)
58
- build_result(target_root, with_agent:, mcp_status:)
59
- end
60
-
61
- def self.check_target!(target_root)
62
- raise UsageError.new(".textus/ already exists at #{target_root}") if File.directory?(target_root)
63
- end
64
-
65
- ASSET_ZONES = %w[raw].freeze
66
-
67
- def self.create_directories(target_root)
68
- FileUtils.mkdir_p(File.join(target_root, "schemas"))
69
- FileUtils.mkdir_p(File.join(target_root, "templates"))
70
- FileUtils.mkdir_p(File.join(target_root, "workflows"))
71
- ZONES.each do |z|
72
- dir = File.join(target_root, "data", z)
73
- FileUtils.mkdir_p(dir)
74
- File.write(File.join(dir, ".gitkeep"), "")
75
- end
76
- ASSET_ZONES.each do |z|
77
- dir = File.join(target_root, "assets", z)
78
- FileUtils.mkdir_p(dir)
79
- File.write(File.join(dir, ".gitkeep"), "")
80
- end
81
- end
82
-
83
- def self.write_manifest(target_root, with_agent:)
84
- File.write(File.join(target_root, "manifest.yaml"), manifest_yaml(with_agent: with_agent))
85
- end
86
-
87
- def self.scaffold_agent(target_root, scaffold_dir, with_agent:)
88
- return nil unless with_agent
89
-
90
- scaffold_agent_profile(target_root, scaffold_dir)
91
- write_mcp_config(target_root, scaffold_dir)
92
- end
93
-
94
- def self.setup_state_dirs(target_root)
95
- FileUtils.mkdir_p(Textus::Store::Geometry.new(target_root).audit_dir_path)
96
- end
97
-
98
- def self.write_gitignore(target_root)
99
- File.write(File.join(target_root, ".gitignore"), derived_gitignore(target_root))
100
- end
101
-
102
- def self.build_result(target_root, with_agent:, mcp_status:)
103
- result = { "protocol" => PROTOCOL, "initialized" => target_root, "profile" => with_agent ? "agent" : "default" }
104
- result["mcp_config"] = mcp_status if with_agent
105
- result
106
- end
107
-
108
- # Composes the agent profile by inserting AGENT_ENTRIES immediately before the
109
- # top-level `rules:` block of DEFAULT_MANIFEST — that block is load-bearing for
110
- # this `.sub`; removing it from DEFAULT_MANIFEST would silently drop the entries.
111
- def self.manifest_yaml(with_agent:)
112
- return DEFAULT_MANIFEST unless with_agent
113
-
114
- DEFAULT_MANIFEST.sub(/^rules:/, "#{AGENT_ENTRIES}rules:")
115
- end
116
-
117
- # Copies the proven orientation bundle into a freshly-init'd store.
118
- def self.scaffold_agent_profile(target_root, scaffold_dir)
119
- {
120
- "project.schema.yaml" => File.join("schemas", "project.yaml"),
121
- "runbook.schema.yaml" => File.join("schemas", "runbook.yaml"),
122
- "orientation.erb" => File.join("templates", "orientation.erb"),
123
- }.each do |src, dest|
124
- File.write(File.join(target_root, dest), File.read(File.join(scaffold_dir, src)))
125
- end
126
- end
127
-
128
- # The one file init writes outside .textus/: a starter .mcp.json at the
129
- # project root. Write-once — never clobber a hand-authored config. The
130
- # command form assumes a gem-installed `textus` on PATH; the user owns
131
- # the file after this first write.
132
- def self.write_mcp_config(target_root, scaffold_dir)
133
- dest = File.join(File.dirname(target_root), ".mcp.json")
134
- return "skipped" if File.exist?(dest)
135
-
136
- File.write(dest, File.read(File.join(scaffold_dir, "mcp.json")))
137
- "written"
138
- end
139
-
140
- # The store's `.gitignore` is generated, never hand-kept (ADR 0038), and now
141
- # derived from the manifest: the run subtree plus every `tracked: false`
142
- # entry's resolved path (ADR 0043).
143
- def self.derived_gitignore(target_root)
144
- manifest = Textus::Manifest.load(target_root)
145
- root = Pathname.new(target_root)
146
- untracked = manifest.data.entries.reject(&:tracked?).map do |e|
147
- if e.nested? # a whole subtree of leaf files (artifacts.feeds.machines.* → data/artifacts/feeds/machines/)
148
- rel = e.path.start_with?("data/") ? e.path : File.join("data", e.path)
149
- "#{rel}/"
150
- else
151
- Pathname.new(Textus::Key::Path.resolve(manifest.data, e)).relative_path_from(root).to_s
152
- end
153
- end
154
- Textus::Store::Geometry.new(target_root).gitignore_body(untracked_entries: untracked)
155
- end
156
- end
157
- end
data/lib/textus/jobs.rb DELETED
@@ -1,9 +0,0 @@
1
- module Textus
2
- module Jobs
3
- def self.fetch(type)
4
- Store::Jobs::Registry.fetch(type)
5
- rescue Store::Jobs::Registry::UnknownJob
6
- raise Textus::UsageError.new("unknown job type: #{type}")
7
- end
8
- end
9
- end
@@ -1,55 +0,0 @@
1
- module Textus
2
- module Key
3
- # Small utilities for ranking key suggestions. Bounded inputs only —
4
- # Levenshtein is O(n*m) so we refuse to compute on long strings.
5
- class Distance
6
- MAX_LEN = 200
7
-
8
- # Length of the shared dot-separated prefix between two dotted keys.
9
- def self.shared_prefix_segments(left, right)
10
- asegs = left.split(".")
11
- bsegs = right.split(".")
12
- n = [asegs.length, bsegs.length].min
13
- i = 0
14
- i += 1 while i < n && asegs[i] == bsegs[i]
15
- i
16
- end
17
-
18
- # Classic iterative Levenshtein with two rows. Bounded to MAX_LEN.
19
- def self.levenshtein(left, right)
20
- return nil if left.length > MAX_LEN || right.length > MAX_LEN
21
- return right.length if left.empty?
22
- return left.length if right.empty?
23
-
24
- prev = (0..right.length).to_a
25
- curr = Array.new(right.length + 1, 0)
26
- (1..left.length).each do |i|
27
- curr[0] = i
28
- (1..right.length).each do |j|
29
- cost = left[i - 1] == right[j - 1] ? 0 : 1
30
- curr[j] = [
31
- curr[j - 1] + 1, # insertion
32
- prev[j] + 1, # deletion
33
- prev[j - 1] + cost, # substitution
34
- ].min
35
- end
36
- prev, curr = curr, prev
37
- end
38
- prev[right.length]
39
- end
40
-
41
- # Rank candidate keys against requested. Returns up to `limit` keys.
42
- # Sort: longer shared prefix first; then smaller Levenshtein distance.
43
- def self.suggest(requested, candidates, limit: 5)
44
- return [] if requested.nil? || requested.empty?
45
-
46
- scored = candidates.first(200).map do |k|
47
- prefix = shared_prefix_segments(requested, k)
48
- dist = levenshtein(requested, k) || Float::INFINITY
49
- [k, prefix, dist]
50
- end
51
- scored.sort_by { |(_, prefix, dist)| [-prefix, dist] }.first(limit).map(&:first)
52
- end
53
- end
54
- end
55
- end
@@ -1,33 +0,0 @@
1
- module Textus
2
- module Key
3
- module Grammar
4
- SEGMENT = /\A[a-z0-9][a-z0-9-]*\z/
5
- MAX_SEGMENTS = 8
6
- MAX_SEGMENT_LEN = 64
7
-
8
- module_function
9
-
10
- def validate!(key) # rubocop:disable Naming/PredicateMethod
11
- raise UsageError.new("key must be a String") unless key.is_a?(String)
12
- raise UsageError.new("empty key") if key.empty?
13
-
14
- segs = key.split(".")
15
- raise UsageError.new("key '#{key}' has #{segs.length} segments (max #{MAX_SEGMENTS})") if segs.length > MAX_SEGMENTS
16
-
17
- segs.each do |seg|
18
- if seg.empty?
19
- raise UsageError.new("empty segment in key '#{key}'")
20
- elsif seg.length > MAX_SEGMENT_LEN
21
- raise UsageError.new("segment '#{seg}' in key '#{key}' exceeds #{MAX_SEGMENT_LEN} chars")
22
- elsif !seg.match?(SEGMENT)
23
- raise UsageError.new(
24
- "invalid key segment '#{seg}' in '#{key}': must match [a-z0-9][a-z0-9-]* " \
25
- "(lowercase, digits, hyphens; no underscores or uppercase)",
26
- )
27
- end
28
- end
29
- true
30
- end
31
- end
32
- end
33
- end
@@ -1,24 +0,0 @@
1
- module Textus
2
- module Key
3
- # Dotted-key scope matching, shared by all prefix-scoped sweeps
4
- # (WS4 / ADR 0089-era cleanup). Canonicalised here so every consumer
5
- # uses a consistent dotted-boundary check with proper Nested ancestor
6
- # handling. ADR 0093: Produce is the sole engine calling this.
7
- module Matching
8
- module_function
9
-
10
- # Is `key` within the `prefix` scope?
11
- # - exact match, or a dotted descendant (the `prefix.` boundary, so
12
- # prefix "art" does NOT match key "artifacts"), and
13
- # - for a nested entry, also when `prefix` descends INTO it — the nested
14
- # parent owns the leaf the prefix names (e.g. prefix
15
- # "feeds.machines.host1" still selects the nested entry
16
- # "feeds.machines").
17
- def matches_prefix?(key, prefix, nested: false)
18
- return true if key == prefix || key.start_with?("#{prefix}.")
19
-
20
- nested && prefix.start_with?("#{key}.")
21
- end
22
- end
23
- end
24
- end
@@ -1,28 +0,0 @@
1
- module Textus
2
- module Key
3
- module Path
4
- # Returns the absolute filesystem path for a manifest entry (the leaf file,
5
- # not a nested directory). Adds the format's primary extension when the
6
- # manifest entry's `path:` is extensionless.
7
- #
8
- # The first argument is a Manifest::Data (or anything responding to .root);
9
- # callers historically passed the whole Manifest but should now pass
10
- # `manifest.data`.
11
- def self.resolve(data, mentry)
12
- primary_ext = Format.for(mentry.format).extensions.first
13
- rel_path = normalize_relative_path(mentry.path)
14
- if File.extname(mentry.path) == ""
15
- File.join(data.root, rel_path + primary_ext)
16
- else
17
- File.join(data.root, rel_path)
18
- end
19
- end
20
-
21
- def self.normalize_relative_path(path)
22
- return path if path.start_with?("data/")
23
-
24
- File.join("data", path)
25
- end
26
- end
27
- end
28
- end