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
@@ -0,0 +1,15 @@
1
+ module Textus
2
+ module Protocol
3
+ module Handlers
4
+ module Concern
5
+ def self.included(base)
6
+ base.extend(self)
7
+ end
8
+
9
+ def response(**fields)
10
+ { "protocol" => Textus::PROTOCOL, **fields }
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,37 @@
1
+ module Textus
2
+ module Protocol
3
+ module Handlers
4
+ module Ops
5
+ class AuditFilter
6
+ FILTER_KEYS = %i[key lane role verb since seq_since correlation_id limit].freeze
7
+
8
+ def call(ctx:, filters: {})
9
+ seq = filters[:seq_since]
10
+ if seq
11
+ min = ctx.audit_log.min_available
12
+ raise Textus::CursorExpired.new(requested: seq, min_available: min) if min && seq < min - 1
13
+ end
14
+
15
+ rows = ctx.audit_log.list(seq_since: filters[:seq_since], key: filters[:key], role: filters[:role], verb: filters[:verb],
16
+ correlation_id: filters[:correlation_id], limit: filters[:limit]).select do |row|
17
+ next false if filters[:lane] && !key_in_lane?(row["key"], filters[:lane], ctx)
18
+ next false if filters[:since] && row["ts"] && Time.parse(row["ts"]) < filters[:since]
19
+
20
+ true
21
+ end
22
+ { "verb" => "audit", "rows" => rows }
23
+ end
24
+
25
+ private
26
+
27
+ def key_in_lane?(key, lane_name, ctx)
28
+ mentry = ctx.mentry_for(key:)
29
+ mentry && mentry.lane == lane_name
30
+ rescue Textus::Error
31
+ false
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,38 @@
1
+ module Textus
2
+ module Protocol
3
+ module Handlers
4
+ module Ops
5
+ class BulkPlanner
6
+ Plan = Data.define(:steps, :warnings) do
7
+ def to_h
8
+ { "steps" => steps, "warnings" => warnings }
9
+ end
10
+ end
11
+
12
+ def plan_mv(from_prefix:, to_prefix:, ctx:)
13
+ raise Textus::UsageError.new("from_prefix and to_prefix required") unless from_prefix && to_prefix
14
+
15
+ leaves = ctx.list(prefix: from_prefix)
16
+ raise Textus::UsageError.new("from_prefix is a leaf — use mv") if leaves.any? { |r| r["key"] == from_prefix }
17
+
18
+ warnings = leaves.empty? ? ["no keys under #{from_prefix}"] : []
19
+ steps = leaves.map do |row|
20
+ tail = row["key"].delete_prefix("#{from_prefix}.")
21
+ { "op" => "mv", "from" => row["key"], "to" => "#{to_prefix}.#{tail}" }
22
+ end
23
+ Plan.new(steps:, warnings:)
24
+ end
25
+
26
+ def plan_delete(prefix:, ctx:)
27
+ raise Textus::UsageError.new("prefix required") unless prefix && !prefix.empty?
28
+
29
+ leaves = ctx.list(prefix:)
30
+ warnings = leaves.empty? ? ["no keys under #{prefix}"] : []
31
+ steps = leaves.map { |row| { "op" => "delete", "key" => row["key"] } }
32
+ Plan.new(steps:, warnings:)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,49 @@
1
+ require "fileutils"
2
+
3
+ module Textus
4
+ module Protocol
5
+ module Handlers
6
+ module Ops
7
+ class DataMv
8
+ Plan = Data.define(:steps, :warnings) do
9
+ def to_h
10
+ { "steps" => steps, "warnings" => warnings }
11
+ end
12
+ end
13
+
14
+ def call(from:, to:, ctx:, dry_run: false)
15
+ raise Textus::UsageError.new("from and to required") unless from && to
16
+ raise Textus::UsageError.new("data lane '#{from}' not declared") unless ctx.manifest.data.declared_lane_kinds.key?(from)
17
+
18
+ dest_dir = ctx.layout.lane_path(to)
19
+ raise Textus::UsageError.new("destination data/#{to} already exists") if File.exist?(dest_dir)
20
+
21
+ affected_keys = ctx.manifest.data.entries.select { |e| e.lane == from }.map(&:key)
22
+ steps = [{ "op" => "rename_zone", "from" => from, "to" => to }]
23
+ steps += affected_keys.map { |k| { "op" => "mv", "from" => k, "to" => "#{to}#{k[from.length..]}" } }
24
+ plan = Plan.new(steps:, warnings: [])
25
+ return plan.to_h if dry_run
26
+
27
+ rewrite_manifest!(from:, to:, ctx:)
28
+ FileUtils.mv(ctx.layout.lane_path(from), dest_dir)
29
+ plan.to_h
30
+ end
31
+
32
+ private
33
+
34
+ def rewrite_manifest!(from:, to:, ctx:)
35
+ path = ctx.layout.manifest_path
36
+ raw = YAML.safe_load_file(path, permitted_classes: [Symbol], aliases: false)
37
+ raw["lanes"].each { |lane| lane["name"] = to if lane["name"] == from }
38
+ raw["entries"].each do |entry|
39
+ entry["lane"] = to if entry["lane"] == from
40
+ entry["key"] = entry["key"].sub(/\A#{Regexp.escape(from)}(\.|\z)/, "#{to}\\1")
41
+ entry["path"] = entry["path"].sub(%r{\A(data/)?#{Regexp.escape(from)}(/|\z)}, "\\1#{to}\\2")
42
+ end
43
+ File.write(path, YAML.dump(raw))
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,65 @@
1
+ require "open3"
2
+
3
+ module Textus
4
+ module Protocol
5
+ module Handlers
6
+ module Ops
7
+ class GitBlame
8
+ def call(key:, ctx:)
9
+ root = ctx.manifest.data.root
10
+ path = resolve_blame_path(key, ctx)
11
+ return nil unless path && git_tracked?(path, root:)
12
+
13
+ audit_rows = ctx.audit_log.list(key:)
14
+ audit_rows.to_h { |row| [row["ts"], git_commit_at(path, timestamp: row["ts"], root:)] }
15
+ end
16
+
17
+ private
18
+
19
+ def resolve_blame_path(key, ctx)
20
+ res = ctx.resolve(key:)
21
+ res.path || Textus::Protocol::Key::Path.resolve(ctx.manifest.data, res.entry)
22
+ rescue Textus::Error
23
+ nil
24
+ end
25
+
26
+ def git_tracked?(path, root:)
27
+ return false if path.nil? || !File.exist?(path) || !git_repo?(root)
28
+
29
+ _out, _err, status = Open3.capture3("git", "ls-files", "--error-unmatch", path, chdir: root)
30
+ status.success?
31
+ rescue Errno::ENOENT
32
+ false
33
+ end
34
+
35
+ def git_repo?(root)
36
+ dir = root
37
+ loop do
38
+ return true if File.directory?(File.join(dir, ".git"))
39
+
40
+ parent = File.dirname(dir)
41
+ return false if parent == dir
42
+
43
+ dir = parent
44
+ end
45
+ end
46
+
47
+ def git_commit_at(path, timestamp:, root:)
48
+ args = ["git", "log", "-1"]
49
+ args << "--before=#{timestamp}" if timestamp
50
+ args += ["--format=%H%x09%an%x09%aI%x09%s", "--", path]
51
+ out, _err, status = Open3.capture3(*args, chdir: root)
52
+ return nil unless status.success?
53
+
54
+ sha, author, date, subject = out.strip.split("\t", 4)
55
+ return nil if sha.nil? || sha.empty?
56
+
57
+ { "sha" => sha, "author" => author, "date" => date, "subject" => subject }
58
+ rescue Errno::ENOENT
59
+ nil
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,37 @@
1
+ module Textus
2
+ module Protocol
3
+ module Handlers
4
+ module Ops
5
+ class PulseCoordinator
6
+ def call(role:, ctx:, since: nil)
7
+ cursor_val = since || ctx.cursor_store.get(role)[:seq]
8
+ audit_filter = AuditFilter.new
9
+ audit_result = audit_filter.call(ctx:, filters: { seq_since: cursor_val })
10
+ changed = audit_result["rows"] || []
11
+ queue = ctx.manifest.policy.queue_lane
12
+ pending = queue ? ctx.list(lane: queue) : []
13
+ runner = Textus::Lanes::Knowledge::Runner.new(ctx.manifest, cursor_store: ctx.cursor_store)
14
+ result = {
15
+ "cursor" => ctx.audit_log.latest[:seq],
16
+ "changed" => changed,
17
+ "pending_review" => pending.is_a?(Array) ? pending.map { |r| r["key"] } : [],
18
+ "contract_etag" => Textus::Value::Etag.for_contract(ctx.layout),
19
+ "index_etag" => pulse_index_etag(ctx),
20
+ "loop_state" => runner.current_position(role),
21
+ }
22
+ ctx.cursor_store.set(role, result["cursor"])
23
+ result
24
+ end
25
+
26
+ private
27
+
28
+ def pulse_index_etag(ctx)
29
+ ctx.etag_for("artifacts.system.index")
30
+ rescue Textus::Error
31
+ nil
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,41 @@
1
+ module Textus
2
+ module Protocol
3
+ module Handlers
4
+ module Ops
5
+ class SourceExpander
6
+ def call(env, ctx:, depth: 0)
7
+ return env if depth >= 5
8
+
9
+ raw_sources = Array(env.meta["sources"])
10
+ return env if raw_sources.empty?
11
+
12
+ expanded = raw_sources.map { |src| expand_one(src, ctx:, depth:) }
13
+ env.with(sources: expanded)
14
+ end
15
+
16
+ private
17
+
18
+ def expand_one(src, ctx:, depth:)
19
+ src = { "key" => src } if src.is_a?(String)
20
+ return src unless src.is_a?(Hash) && src["key"].is_a?(String)
21
+
22
+ key = src["key"]
23
+ stored_etag = src["etag"]
24
+ res = ctx.resolve(key:)
25
+ path = res.path
26
+ current_etag = File.exist?(path) ? ctx.file_system.etag(path) : nil
27
+ suspended = stored_etag && current_etag ? stored_etag != current_etag : false
28
+ result = src.merge("suspended" => suspended)
29
+ child_env = ctx.read(key:)
30
+ if child_env
31
+ child_expanded = call(child_env, ctx:, depth: depth + 1)
32
+ child_sources = Array(child_expanded.sources)
33
+ result = result.merge("sources" => child_sources) unless child_sources.empty?
34
+ end
35
+ result
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,41 @@
1
+ module Textus
2
+ module Protocol
3
+ module Handlers
4
+ class QueryContext
5
+ def initialize(store_engine, container, call)
6
+ @store_engine = store_engine
7
+ @container = container
8
+ @call = call
9
+ end
10
+
11
+ attr_reader :call, :store_engine, :container
12
+
13
+ # StoreEngine delegation (read side)
14
+ def read(key:) = @store_engine.read(key:)
15
+ def resolve(key:) = @store_engine.resolve(key:)
16
+ def list(...) = @store_engine.list(...)
17
+ def exists?(key:) = @store_engine.exists?(key:)
18
+ def mentry_for(key:) = @store_engine.mentry_for(key:)
19
+
20
+ # Narrow file queries (avoid raw file_system access)
21
+ def file_exists?(key)
22
+ path = @store_engine.resolve(key:).path
23
+ @store_engine.file_system.exists?(path)
24
+ end
25
+
26
+ def etag_for(key)
27
+ path = @store_engine.resolve(key:).path
28
+ @store_engine.file_system.etag(path) if @store_engine.file_system.exists?(path)
29
+ end
30
+
31
+ # Container metadata (read-only)
32
+ def manifest = @container.manifest
33
+ def schemas = @container.schemas
34
+ def layout = @container.layout
35
+ def link_store = @container.store.link_store
36
+ def entry_index = @container.store.entry_index
37
+ def cursor_store = @container.store.cursor_store
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,67 @@
1
+ module Textus
2
+ module Protocol
3
+ module Handlers
4
+ module Read
5
+ include Concern
6
+
7
+ module_function
8
+
9
+ def get(key:, ctx:, call:)
10
+ env = ctx.read(key:)
11
+ unless env
12
+ suggestions = ctx.manifest.resolver.suggestions_for(key)
13
+ raise Textus::UnknownKey.new(key, suggestions:)
14
+ end
15
+
16
+ Ops::SourceExpander.new.call(env, ctx:, depth: 0).to_h_for_wire
17
+ end
18
+
19
+ def list(ctx:, call:, prefix: nil, lane: nil, q: nil, schema: nil)
20
+ ctx.list(prefix:, lane:, q:, schema:)
21
+ end
22
+
23
+ def where(key:, ctx:, call:)
24
+ res = ctx.resolve(key:)
25
+ response(key:, lane: res.entry.lane, path: res.path)
26
+ end
27
+
28
+ def uid(key:, ctx:, call:)
29
+ env = ctx.read(key:)
30
+ raise Textus::UnknownKey.new(key) unless env
31
+
32
+ env.uid
33
+ end
34
+
35
+ def deps(key:, ctx:, call:)
36
+ entry = ctx.manifest.data.entries.find { |e| e.key == key }
37
+ deps_list = entry&.external? ? Array(entry.source&.sources).compact : []
38
+ { "key" => key, "deps" => deps_list.uniq }
39
+ end
40
+
41
+ def rdeps(key:, ctx:, call:)
42
+ source_rdeps = ctx.manifest.data.entries.each_with_object([]) do |entry, acc|
43
+ next unless entry.external?
44
+
45
+ sources = Array(entry.source&.sources).compact
46
+ acc << entry.key if sources.any? { |s| s == key || key.start_with?("#{s}.") }
47
+ end
48
+ link_rdeps = ctx.link_store.links_to(key)
49
+ { "key" => key, "rdeps" => (source_rdeps + link_rdeps).uniq.sort }
50
+ end
51
+
52
+ def graph(key:, ctx:, call:, depth: nil)
53
+ from = ctx.link_store.links_from(key)
54
+ to = ctx.link_store.links_to(key)
55
+ { "key" => key, "neighbors" => (from + to).uniq.sort,
56
+ "reachable" => ctx.link_store.reachable(from: key, depth:).sort }
57
+ end
58
+
59
+ def schema_show(key:, ctx:, call:)
60
+ mentry = ctx.mentry_for(key:)
61
+ schema = ctx.schemas.fetch_or_nil(mentry.schema)
62
+ response(key:, schema_ref: mentry.schema, schema: schema&.to_h)
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,93 @@
1
+ module Textus
2
+ module Protocol
3
+ module Handlers
4
+ module System
5
+ include Concern
6
+ extend self
7
+
8
+ def boot(ctx:, call:)
9
+ proxy = Textus::Protocol::Container.new(
10
+ store: ctx.container.store,
11
+ layout: ctx.layout,
12
+ manifest: ctx.manifest, schemas: ctx.schemas,
13
+ workflows: nil,
14
+ root: ctx.layout.root
15
+ )
16
+ Textus::Protocol::Boot.build(container: proxy)
17
+ end
18
+
19
+ def schema_list(ctx:, call:)
20
+ Textus::Protocol::Boot.build_schemas(ctx.schemas)
21
+ end
22
+
23
+ def audit(ctx:, call:, key: nil, lane: nil, role: nil, verb: nil, since: nil, seq_since: nil, correlation_id: nil, limit: nil)
24
+ filters = { key:, lane:, role:, verb:, since:, seq_since:, correlation_id:, limit: }
25
+ Ops::AuditFilter.new.call(ctx:, filters:)
26
+ end
27
+
28
+ def blame(key:, ctx:, call:)
29
+ audit_result = audit(key:, ctx:, call:)
30
+ audit_rows = audit_result["rows"] || []
31
+ git_map = Ops::GitBlame.new.call(key:, ctx:)
32
+ audit_rows.map { |row| row.merge("git" => git_map ? git_map[row["ts"]] : nil) }
33
+ end
34
+
35
+ def pulse(ctx:, call:, since: nil)
36
+ Ops::PulseCoordinator.new.call(role: call.role, ctx:, since:)
37
+ end
38
+
39
+ def workflow_spec(name:, ctx:, call:)
40
+ registry = ctx.workflow_registry || Textus::Workflow::Registry.new(ctx.workflows)
41
+ wf = registry.for(name)
42
+ {
43
+ "name" => wf.name, "type" => wf.workflow_type,
44
+ "match_patterns" => wf.on_patterns || [],
45
+ "handles_events" => wf.handled_events,
46
+ "steps" => wf.steps.map { |s| { "name" => s.name.to_s, "type" => step_type(s) } },
47
+ "publish_targets" => wf.publish_targets.map do |t|
48
+ h = {}
49
+ h["to"] = t.to if t.to
50
+ h["template"] = t.template if t.template
51
+ h
52
+ end,
53
+ "priority" => wf.priority,
54
+ "max_attempts" => wf.max_attempts,
55
+ "ttl" => wf.ttl&.to_s,
56
+ "on_expire" => wf.on_expire
57
+ }
58
+ end
59
+
60
+ def drain(ctx:, call:)
61
+ registry = ctx.workflow_registry || Textus::Workflow::Registry.new(ctx.workflows)
62
+ reader = ->(key:) { ctx.read(key:) }
63
+
64
+ Textus::Workflow::Scheduler.new(
65
+ registry:, workflow_queue: ctx.workflow_queue, reader:,
66
+ ).seed_expired
67
+
68
+ consumer = Textus::Workflow::Consumer.new(registry:, container: ctx.container, call:)
69
+ completed = 0
70
+ failed = 0
71
+ loop do
72
+ row = ctx.workflow_queue.pop
73
+ break unless row
74
+
75
+ result = consumer.consume(row, ctx.workflow_queue)
76
+ result[:state] == "done" ? completed += 1 : failed += 1
77
+ end
78
+
79
+ response(ok: failed.zero?, completed:, failed:)
80
+ end
81
+
82
+ private
83
+
84
+ def step_type(step)
85
+ case step
86
+ when Textus::Workflow::DSL::Parallel then "parallel"
87
+ else "step"
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,57 @@
1
+ module Textus
2
+ module Protocol
3
+ module Handlers
4
+ module Write
5
+ include Concern
6
+
7
+ module_function
8
+
9
+ def put(key:, ctx:, call:, meta: nil, if_etag: nil, format: nil, data: nil)
10
+ body, content = data.is_a?(Hash) ? [nil, data] : [data, nil]
11
+ env = ctx.put(key:, meta: meta || {}, body:, content:, if_etag:, call:, format:)
12
+ response(ok: true, key:, uid: env.uid, etag: env.etag)
13
+ end
14
+
15
+ def delete(key:, ctx:, call:, if_etag: nil)
16
+ ctx.delete(key:, if_etag:, call:)
17
+ response(ok: true, key:, deleted: true)
18
+ end
19
+
20
+ def key_delete(key:, ctx:, call:, if_etag: nil)
21
+ delete(key:, ctx:, call:, if_etag:)
22
+ end
23
+
24
+ def move(old_key:, new_key:, ctx:, call:, if_etag: nil, dry_run: false)
25
+ mctx = ctx.move(from_key: old_key, to_key: new_key, call:, if_etag:, dry_run:)
26
+ base = response(ok: true, from_key: mctx.from_key, to_key: mctx.to_key,
27
+ from_path: mctx.from_path, to_path: mctx.to_path)
28
+ return base.merge("dry_run" => true) if mctx.dry_run
29
+
30
+ base.merge("uid" => mctx.envelope.uid, "envelope" => mctx.envelope.to_h_for_wire)
31
+ end
32
+
33
+ def key_mv_prefix(from_prefix:, to_prefix:, ctx:, call:, dry_run: false)
34
+ planner = Ops::BulkPlanner.new
35
+ plan = planner.plan_mv(from_prefix:, to_prefix:, ctx:)
36
+ return plan.to_h if dry_run
37
+
38
+ plan.steps.each { |s| move(old_key: s["from"], new_key: s["to"], ctx:, call:) }
39
+ plan.to_h
40
+ end
41
+
42
+ def key_delete_prefix(prefix:, ctx:, call:, dry_run: false)
43
+ planner = Ops::BulkPlanner.new
44
+ plan = planner.plan_delete(prefix:, ctx:)
45
+ return plan.to_h if dry_run
46
+
47
+ plan.steps.each { |s| delete(key: s["key"], ctx:, call:) }
48
+ plan.to_h
49
+ end
50
+
51
+ def data_mv(from:, to:, ctx:, call:, dry_run: false)
52
+ Ops::DataMv.new.call(from:, to:, ctx:, dry_run:)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,143 @@
1
+ require "fileutils"
2
+ require "pathname"
3
+ require "yaml"
4
+
5
+ module Textus
6
+ module Protocol
7
+ module Init
8
+ ZONES = %w[knowledge scratchpad proposals artifacts raw].freeze
9
+
10
+ DEFAULT_MANIFEST = <<~YAML
11
+ version: textus/4
12
+ data:
13
+ knowledge:
14
+ - key: knowledge.loop.execution
15
+ naming: sequential
16
+ schema: runbook
17
+ - key: knowledge.loop.intent
18
+ naming: sequential
19
+ - key: knowledge.loop.constraint
20
+ - key: knowledge.loop.judgment
21
+ - key: knowledge.loop.evidence
22
+ naming: sequential
23
+ - key: knowledge.loop.feedback
24
+ scratchpad:
25
+ - key: scratchpad.notes
26
+ tracked: false
27
+ - key: scratchpad.sessions
28
+ tracked: false
29
+ - key: scratchpad.scripts
30
+ tracked: false
31
+ proposals:
32
+ - key: proposals.notes
33
+ raw:
34
+ - key: raw
35
+ format: yaml
36
+ artifacts:
37
+ - key: artifacts.boot
38
+ format: json
39
+ YAML
40
+
41
+ AGENT_ENTRIES = <<~YAML
42
+ data:
43
+ artifacts:
44
+ - key: artifacts.derived.orientation
45
+ format: json
46
+ YAML
47
+
48
+ def self.run(target_root, with_agent: false)
49
+ check_target!(target_root)
50
+ scaffold_dir = File.expand_path("init/templates", __dir__)
51
+ create_directories(target_root)
52
+ write_manifest(target_root, with_agent:)
53
+ mcp_status = scaffold_agent(target_root, scaffold_dir, with_agent:)
54
+ write_gitignore(target_root)
55
+ build_result(target_root, with_agent:, mcp_status:)
56
+ end
57
+
58
+ def self.check_target!(target_root)
59
+ raise UsageError.new(".textus/ already exists at #{target_root}") if File.directory?(target_root)
60
+ end
61
+
62
+ ASSET_ZONES = %w[raw].freeze
63
+
64
+ def self.create_directories(target_root)
65
+ FileUtils.mkdir_p(File.join(target_root, "schemas"))
66
+ FileUtils.mkdir_p(File.join(target_root, "templates"))
67
+ FileUtils.mkdir_p(File.join(target_root, "workflows"))
68
+ ZONES.each do |z|
69
+ dir = File.join(target_root, "data", z)
70
+ FileUtils.mkdir_p(dir)
71
+ File.write(File.join(dir, ".gitkeep"), "")
72
+ end
73
+ ASSET_ZONES.each do |z|
74
+ dir = File.join(target_root, "assets", z)
75
+ FileUtils.mkdir_p(dir)
76
+ File.write(File.join(dir, ".gitkeep"), "")
77
+ end
78
+ end
79
+
80
+ def self.write_manifest(target_root, with_agent:)
81
+ File.write(File.join(target_root, "manifest.yaml"), manifest_yaml(with_agent: with_agent))
82
+ end
83
+
84
+ def self.scaffold_agent(target_root, scaffold_dir, with_agent:)
85
+ return nil unless with_agent
86
+
87
+ scaffold_agent_profile(target_root, scaffold_dir)
88
+ write_mcp_config(target_root, scaffold_dir)
89
+ end
90
+
91
+ def self.write_gitignore(target_root)
92
+ File.write(File.join(target_root, ".gitignore"), derived_gitignore(target_root))
93
+ end
94
+
95
+ def self.build_result(target_root, with_agent:, mcp_status:)
96
+ result = { "protocol" => PROTOCOL, "initialized" => target_root, "profile" => with_agent ? "agent" : "default" }
97
+ result["mcp_config"] = mcp_status if with_agent
98
+ result
99
+ end
100
+
101
+ def self.manifest_yaml(with_agent:)
102
+ return DEFAULT_MANIFEST unless with_agent
103
+
104
+ base = YAML.safe_load(DEFAULT_MANIFEST)
105
+ extra = YAML.safe_load(AGENT_ENTRIES)
106
+ base["data"].merge!(extra["data"]) { |_lane, existing, added| existing + added }
107
+ YAML.dump(base).sub(/\A---\n/, "")
108
+ end
109
+
110
+ def self.scaffold_agent_profile(target_root, scaffold_dir)
111
+ {
112
+ "project.schema.yaml" => File.join("schemas", "project.yaml"),
113
+ "runbook.schema.yaml" => File.join("schemas", "runbook.yaml"),
114
+ "orientation.erb" => File.join("templates", "orientation.erb"),
115
+ }.each do |src, dest|
116
+ File.write(File.join(target_root, dest), File.read(File.join(scaffold_dir, src)))
117
+ end
118
+ end
119
+
120
+ def self.write_mcp_config(target_root, scaffold_dir)
121
+ dest = File.join(File.dirname(target_root), ".mcp.json")
122
+ return "skipped" if File.exist?(dest)
123
+
124
+ File.write(dest, File.read(File.join(scaffold_dir, "mcp.json")))
125
+ "written"
126
+ end
127
+
128
+ def self.derived_gitignore(target_root)
129
+ manifest = Textus::Protocol::Manifest.load(target_root)
130
+ root = Pathname.new(target_root)
131
+ untracked = manifest.data.entries.reject(&:tracked?).map do |e|
132
+ if e.nested?
133
+ rel = e.path.start_with?("data/") ? e.path : File.join("data", e.path)
134
+ "#{rel}/"
135
+ else
136
+ Pathname.new(Textus::Protocol::Key::Path.resolve(manifest.data, e)).relative_path_from(root).to_s
137
+ end
138
+ end
139
+ Textus::Protocol::Layout.new(target_root).gitignore_body(untracked_entries: untracked)
140
+ end
141
+ end
142
+ end
143
+ end