textus 0.55.1 → 0.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (329) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +175 -2768
  3. data/README.md +17 -18
  4. data/lib/textus/artifact.rb +8 -0
  5. data/lib/textus/errors.rb +2 -39
  6. data/lib/textus/human.rb +7 -0
  7. data/lib/textus/infra/adapters/concurrency_adapter.rb +15 -0
  8. data/lib/textus/infra/clock.rb +7 -0
  9. data/lib/textus/infra/database.rb +127 -0
  10. data/lib/textus/infra/file_system.rb +54 -0
  11. data/lib/textus/infra/locks.rb +50 -0
  12. data/lib/textus/infra/port/clock.rb +18 -0
  13. data/lib/textus/infra/port/publisher.rb +71 -0
  14. data/lib/textus/infra/port/sentinel.rb +25 -0
  15. data/lib/textus/infra/port/sentinel_store.rb +93 -0
  16. data/lib/textus/infra/store/audit_log.rb +42 -0
  17. data/lib/textus/infra/store/base.rb +25 -0
  18. data/lib/textus/infra/store/cursor_store.rb +27 -0
  19. data/lib/textus/infra/store/entry_index.rb +31 -0
  20. data/lib/textus/infra/store/envelope/meta.rb +63 -0
  21. data/lib/textus/infra/store/file_store.rb +23 -0
  22. data/lib/textus/infra/store/index/builder.rb +28 -0
  23. data/lib/textus/infra/store/index/lookup.rb +23 -0
  24. data/lib/textus/infra/store/link_store.rb +43 -0
  25. data/lib/textus/infra/store/workflow_queue.rb +54 -0
  26. data/lib/textus/infra/store.rb +29 -0
  27. data/lib/textus/ingest.rb +8 -0
  28. data/lib/textus/lanes/artifact/cache.rb +35 -0
  29. data/lib/textus/lanes/artifact/handlers.rb +29 -0
  30. data/lib/textus/lanes/artifact/lifecycle.rb +23 -0
  31. data/lib/textus/lanes/artifact/verbs.rb +18 -0
  32. data/lib/textus/lanes/ingest/dedup.rb +17 -0
  33. data/lib/textus/lanes/ingest/entry_types/asset.rb +45 -0
  34. data/lib/textus/lanes/ingest/entry_types/link.rb +16 -0
  35. data/lib/textus/lanes/ingest/entry_types/text.rb +17 -0
  36. data/lib/textus/lanes/ingest/entry_types.rb +25 -0
  37. data/lib/textus/lanes/ingest/handlers.rb +54 -0
  38. data/lib/textus/lanes/ingest/index_rebuilder.rb +13 -0
  39. data/lib/textus/lanes/ingest/key_builder.rb +33 -0
  40. data/lib/textus/lanes/ingest/resolver.rb +21 -0
  41. data/lib/textus/lanes/knowledge/freeform.rb +15 -0
  42. data/lib/textus/lanes/knowledge/handlers.rb +53 -0
  43. data/lib/textus/lanes/knowledge/runner.rb +45 -0
  44. data/lib/textus/lanes/knowledge/verbs.rb +16 -0
  45. data/lib/textus/lanes/scratchpad/handlers.rb +164 -0
  46. data/lib/textus/lanes/scratchpad/verbs.rb +73 -0
  47. data/lib/textus/{gate → protocol}/binder.rb +2 -10
  48. data/lib/textus/protocol/boot/catalog.rb +34 -0
  49. data/lib/textus/protocol/boot/context.rb +124 -0
  50. data/lib/textus/protocol/boot/protocol.rb +33 -0
  51. data/lib/textus/protocol/boot.rb +42 -0
  52. data/lib/textus/protocol/builder/domain.rb +17 -0
  53. data/lib/textus/protocol/builder/infrastructure.rb +17 -0
  54. data/lib/textus/protocol/builder/runtime.rb +46 -0
  55. data/lib/textus/protocol/builder.rb +11 -0
  56. data/lib/textus/protocol/container.rb +22 -0
  57. data/lib/textus/protocol/diff.rb +170 -0
  58. data/lib/textus/protocol/entry_constraint.rb +54 -0
  59. data/lib/textus/protocol/envelope.rb +70 -0
  60. data/lib/textus/protocol/format/base.rb +80 -0
  61. data/lib/textus/protocol/format/bash.rb +10 -0
  62. data/lib/textus/protocol/format/javascript.rb +10 -0
  63. data/lib/textus/protocol/format/json.rb +82 -0
  64. data/lib/textus/protocol/format/markdown.rb +53 -0
  65. data/lib/textus/protocol/format/python.rb +10 -0
  66. data/lib/textus/protocol/format/ruby.rb +10 -0
  67. data/lib/textus/protocol/format/script.rb +55 -0
  68. data/lib/textus/protocol/format/text.rb +48 -0
  69. data/lib/textus/protocol/format/yaml.rb +105 -0
  70. data/lib/textus/protocol/format.rb +73 -0
  71. data/lib/textus/protocol/freshness/ttl_evaluator.rb +61 -0
  72. data/lib/textus/protocol/gate.rb +61 -0
  73. data/lib/textus/protocol/handlers/command_context.rb +19 -0
  74. data/lib/textus/protocol/handlers/concern.rb +15 -0
  75. data/lib/textus/protocol/handlers/ops/audit_filter.rb +37 -0
  76. data/lib/textus/protocol/handlers/ops/bulk_planner.rb +38 -0
  77. data/lib/textus/protocol/handlers/ops/data_mv.rb +49 -0
  78. data/lib/textus/protocol/handlers/ops/git_blame.rb +65 -0
  79. data/lib/textus/protocol/handlers/ops/pulse_coordinator.rb +37 -0
  80. data/lib/textus/protocol/handlers/ops/source_expander.rb +41 -0
  81. data/lib/textus/protocol/handlers/query_context.rb +41 -0
  82. data/lib/textus/protocol/handlers/read.rb +67 -0
  83. data/lib/textus/protocol/handlers/system.rb +93 -0
  84. data/lib/textus/protocol/handlers/write.rb +57 -0
  85. data/lib/textus/protocol/init.rb +143 -0
  86. data/lib/textus/protocol/key/grammar.rb +35 -0
  87. data/lib/textus/protocol/key/matching.rb +23 -0
  88. data/lib/textus/protocol/key/path.rb +23 -0
  89. data/lib/textus/protocol/key.rb +25 -0
  90. data/lib/textus/protocol/lane.rb +12 -0
  91. data/lib/textus/{store/geometry.rb → protocol/layout.rb} +32 -22
  92. data/lib/textus/protocol/links/resolver.rb +33 -0
  93. data/lib/textus/protocol/links/uri_rewriter.rb +30 -0
  94. data/lib/textus/protocol/links.rb +10 -0
  95. data/lib/textus/protocol/manifest/data.rb +90 -0
  96. data/lib/textus/protocol/manifest/entry/base.rb +95 -0
  97. data/lib/textus/protocol/manifest/entry/leaf.rb +19 -0
  98. data/lib/textus/protocol/manifest/entry/nested.rb +19 -0
  99. data/lib/textus/protocol/manifest/entry/parser.rb +71 -0
  100. data/lib/textus/protocol/manifest/entry/publish.rb +209 -0
  101. data/lib/textus/protocol/manifest/entry.rb +10 -0
  102. data/lib/textus/protocol/manifest/policy/publish_target.rb +36 -0
  103. data/lib/textus/protocol/manifest/policy.rb +69 -0
  104. data/lib/textus/protocol/manifest/resolver.rb +156 -0
  105. data/lib/textus/protocol/manifest/schema/contract.rb +53 -0
  106. data/lib/textus/protocol/manifest/schema/validator.rb +52 -0
  107. data/lib/textus/protocol/manifest/schema.rb +40 -0
  108. data/lib/textus/protocol/manifest.rb +62 -0
  109. data/lib/textus/protocol/produce/render/context.rb +43 -0
  110. data/lib/textus/protocol/produce/render.rb +44 -0
  111. data/lib/textus/protocol/schema/registry.rb +44 -0
  112. data/lib/textus/protocol/schema/tools.rb +104 -0
  113. data/lib/textus/protocol/schema.rb +117 -0
  114. data/lib/textus/protocol/session/bridge.rb +11 -0
  115. data/lib/textus/protocol/session/context.rb +5 -0
  116. data/lib/textus/protocol/session.rb +87 -0
  117. data/lib/textus/protocol/store_engine/delete.rb +37 -0
  118. data/lib/textus/protocol/store_engine/move.rb +109 -0
  119. data/lib/textus/protocol/store_engine/pipeline.rb +61 -0
  120. data/lib/textus/protocol/store_engine/put.rb +104 -0
  121. data/lib/textus/protocol/store_engine.rb +86 -0
  122. data/lib/textus/protocol/v4.rb +25 -0
  123. data/lib/textus/protocol/verb_registry/arg_spec.rb +19 -0
  124. data/lib/textus/protocol/verb_registry/core_verbs.rb +164 -0
  125. data/lib/textus/protocol/verb_registry/verb_spec.rb +42 -0
  126. data/lib/textus/protocol/verb_registry.rb +42 -0
  127. data/lib/textus/surface/cli/group/{rule.rb → web.rb} +2 -2
  128. data/lib/textus/surface/cli/runner.rb +25 -25
  129. data/lib/textus/surface/cli/sources.rb +1 -3
  130. data/lib/textus/surface/cli/verb/get.rb +3 -6
  131. data/lib/textus/surface/cli/verb/init.rb +1 -1
  132. data/lib/textus/surface/cli/verb/put.rb +8 -6
  133. data/lib/textus/surface/cli/verb/web_serve.rb +22 -0
  134. data/lib/textus/surface/cli.rb +25 -6
  135. data/lib/textus/surface/dispatch.rb +17 -0
  136. data/lib/textus/surface/mcp/adapters/mcp_adapter.rb +28 -0
  137. data/lib/textus/surface/mcp/catalog.rb +26 -62
  138. data/lib/textus/surface/mcp/cursor_manager.rb +22 -0
  139. data/lib/textus/surface/mcp/drift_annotation.rb +19 -0
  140. data/lib/textus/surface/mcp/errors.rb +0 -10
  141. data/lib/textus/surface/mcp/server.rb +46 -46
  142. data/lib/textus/surface/watcher.rb +18 -12
  143. data/lib/textus/surface/web.rb +50 -0
  144. data/lib/textus/value/call.rb +1 -1
  145. data/lib/textus/{core → value}/duration.rb +1 -4
  146. data/lib/textus/value/etag.rb +7 -7
  147. data/lib/textus/value/payload.rb +7 -0
  148. data/lib/textus/version.rb +1 -1
  149. data/lib/textus/workflow/consumer.rb +39 -0
  150. data/lib/textus/workflow/dsl/lifecycle.rb +32 -0
  151. data/lib/textus/workflow/dsl/pattern_matcher.rb +20 -0
  152. data/lib/textus/workflow/dsl/step_builder.rb +56 -0
  153. data/lib/textus/workflow/dsl.rb +111 -21
  154. data/lib/textus/workflow/event_emitter.rb +21 -0
  155. data/lib/textus/workflow/publisher.rb +59 -0
  156. data/lib/textus/workflow/registry.rb +31 -8
  157. data/lib/textus/workflow/runner.rb +25 -61
  158. data/lib/textus/workflow/scheduler.rb +45 -0
  159. data/lib/textus/workflow/step_executor.rb +78 -0
  160. data/lib/textus/workflow/step_helpers.rb +102 -0
  161. data/lib/textus/workflow.rb +2 -5
  162. data/lib/textus.rb +16 -80
  163. metadata +167 -183
  164. data/SPEC.md +0 -908
  165. data/docs/architecture/README.md +0 -273
  166. data/docs/reference/conventions.md +0 -138
  167. data/lib/textus/action/accept.rb +0 -46
  168. data/lib/textus/action/audit.rb +0 -94
  169. data/lib/textus/action/base.rb +0 -42
  170. data/lib/textus/action/blame.rb +0 -79
  171. data/lib/textus/action/boot.rb +0 -15
  172. data/lib/textus/action/data_mv.rb +0 -58
  173. data/lib/textus/action/deps.rb +0 -19
  174. data/lib/textus/action/doctor.rb +0 -17
  175. data/lib/textus/action/drain.rb +0 -31
  176. data/lib/textus/action/enqueue.rb +0 -37
  177. data/lib/textus/action/get.rb +0 -34
  178. data/lib/textus/action/ingest.rb +0 -199
  179. data/lib/textus/action/jobs.rb +0 -27
  180. data/lib/textus/action/key_delete.rb +0 -26
  181. data/lib/textus/action/key_delete_prefix.rb +0 -35
  182. data/lib/textus/action/key_mv.rb +0 -122
  183. data/lib/textus/action/key_mv_prefix.rb +0 -48
  184. data/lib/textus/action/list.rb +0 -28
  185. data/lib/textus/action/propose.rb +0 -42
  186. data/lib/textus/action/published.rb +0 -22
  187. data/lib/textus/action/pulse.rb +0 -49
  188. data/lib/textus/action/put.rb +0 -38
  189. data/lib/textus/action/rdeps.rb +0 -24
  190. data/lib/textus/action/reject.rb +0 -28
  191. data/lib/textus/action/rule_explain.rb +0 -81
  192. data/lib/textus/action/rule_lint.rb +0 -62
  193. data/lib/textus/action/rule_list.rb +0 -38
  194. data/lib/textus/action/schema_envelope.rb +0 -22
  195. data/lib/textus/action/uid.rb +0 -19
  196. data/lib/textus/action/where.rb +0 -21
  197. data/lib/textus/boot.rb +0 -202
  198. data/lib/textus/contract/arg.rb +0 -10
  199. data/lib/textus/contract/dsl.rb +0 -88
  200. data/lib/textus/contract/spec.rb +0 -25
  201. data/lib/textus/contract.rb +0 -12
  202. data/lib/textus/core/freshness/evaluator.rb +0 -150
  203. data/lib/textus/core/freshness/verdict.rb +0 -34
  204. data/lib/textus/core/freshness.rb +0 -11
  205. data/lib/textus/core/retention/sweep.rb +0 -57
  206. data/lib/textus/core/retention.rb +0 -11
  207. data/lib/textus/core/sentinel.rb +0 -30
  208. data/lib/textus/doctor/check/audit_log.rb +0 -34
  209. data/lib/textus/doctor/check/generator_drift.rb +0 -29
  210. data/lib/textus/doctor/check/illegal_keys.rb +0 -60
  211. data/lib/textus/doctor/check/manifest_files.rb +0 -25
  212. data/lib/textus/doctor/check/notebook_sources.rb +0 -50
  213. data/lib/textus/doctor/check/orphaned_publish_targets.rb +0 -35
  214. data/lib/textus/doctor/check/proposal_targets.rb +0 -45
  215. data/lib/textus/doctor/check/protocol_version.rb +0 -47
  216. data/lib/textus/doctor/check/publish_tree_index_overlap.rb +0 -48
  217. data/lib/textus/doctor/check/raw_asset_paths.rb +0 -50
  218. data/lib/textus/doctor/check/rule_ambiguity.rb +0 -49
  219. data/lib/textus/doctor/check/schema_parse_error.rb +0 -28
  220. data/lib/textus/doctor/check/schema_violations.rb +0 -28
  221. data/lib/textus/doctor/check/schemas.rb +0 -26
  222. data/lib/textus/doctor/check/sentinels.rb +0 -60
  223. data/lib/textus/doctor/check/stale_reviewed_stamp.rb +0 -54
  224. data/lib/textus/doctor/check/templates.rb +0 -29
  225. data/lib/textus/doctor/check/unowned_schema_fields.rb +0 -40
  226. data/lib/textus/doctor/check.rb +0 -41
  227. data/lib/textus/doctor/validator.rb +0 -93
  228. data/lib/textus/doctor.rb +0 -54
  229. data/lib/textus/format/base.rb +0 -49
  230. data/lib/textus/format/json.rb +0 -102
  231. data/lib/textus/format/markdown.rb +0 -80
  232. data/lib/textus/format/shared.rb +0 -17
  233. data/lib/textus/format/text.rb +0 -55
  234. data/lib/textus/format/yaml.rb +0 -125
  235. data/lib/textus/format.rb +0 -46
  236. data/lib/textus/gate/auth.rb +0 -212
  237. data/lib/textus/gate.rb +0 -92
  238. data/lib/textus/init.rb +0 -157
  239. data/lib/textus/jobs.rb +0 -9
  240. data/lib/textus/key/distance.rb +0 -55
  241. data/lib/textus/key/grammar.rb +0 -33
  242. data/lib/textus/key/matching.rb +0 -24
  243. data/lib/textus/key/path.rb +0 -28
  244. data/lib/textus/manifest/capabilities.rb +0 -29
  245. data/lib/textus/manifest/data.rb +0 -102
  246. data/lib/textus/manifest/entry/base.rb +0 -105
  247. data/lib/textus/manifest/entry/ignore_matcher.rb +0 -46
  248. data/lib/textus/manifest/entry/leaf.rb +0 -17
  249. data/lib/textus/manifest/entry/nested.rb +0 -37
  250. data/lib/textus/manifest/entry/parser.rb +0 -99
  251. data/lib/textus/manifest/entry/produced.rb +0 -36
  252. data/lib/textus/manifest/entry/publish/mode.rb +0 -45
  253. data/lib/textus/manifest/entry/publish/none.rb +0 -14
  254. data/lib/textus/manifest/entry/publish/subtree_mirror.rb +0 -73
  255. data/lib/textus/manifest/entry/publish/template.rb +0 -16
  256. data/lib/textus/manifest/entry/publish/to_paths.rb +0 -76
  257. data/lib/textus/manifest/entry/publish/tree.rb +0 -50
  258. data/lib/textus/manifest/entry/publish.rb +0 -45
  259. data/lib/textus/manifest/entry/validators/format_matrix.rb +0 -21
  260. data/lib/textus/manifest/entry/validators/ignore.rb +0 -28
  261. data/lib/textus/manifest/entry/validators/publish.rb +0 -30
  262. data/lib/textus/manifest/entry/validators.rb +0 -18
  263. data/lib/textus/manifest/entry.rb +0 -8
  264. data/lib/textus/manifest/policy/matcher.rb +0 -51
  265. data/lib/textus/manifest/policy/publish_target.rb +0 -34
  266. data/lib/textus/manifest/policy/react.rb +0 -30
  267. data/lib/textus/manifest/policy/retention.rb +0 -29
  268. data/lib/textus/manifest/policy/source.rb +0 -30
  269. data/lib/textus/manifest/policy.rb +0 -95
  270. data/lib/textus/manifest/resolver.rb +0 -118
  271. data/lib/textus/manifest/rules.rb +0 -86
  272. data/lib/textus/manifest/schema/contract.rb +0 -61
  273. data/lib/textus/manifest/schema/keys.rb +0 -90
  274. data/lib/textus/manifest/schema/semantics/cross_field.rb +0 -53
  275. data/lib/textus/manifest/schema/semantics/invariants.rb +0 -125
  276. data/lib/textus/manifest/schema/semantics/migration.rb +0 -83
  277. data/lib/textus/manifest/schema/semantics.rb +0 -27
  278. data/lib/textus/manifest/schema/validator.rb +0 -48
  279. data/lib/textus/manifest/schema/vocabulary.rb +0 -25
  280. data/lib/textus/manifest/schema.rb +0 -30
  281. data/lib/textus/manifest.rb +0 -65
  282. data/lib/textus/meta.rb +0 -54
  283. data/lib/textus/port/audit_log.rb +0 -268
  284. data/lib/textus/port/build_lock.rb +0 -64
  285. data/lib/textus/port/clock.rb +0 -10
  286. data/lib/textus/port/publisher.rb +0 -60
  287. data/lib/textus/port/sentinel_store.rb +0 -83
  288. data/lib/textus/port/storage/file_stat.rb +0 -19
  289. data/lib/textus/port/storage/file_store.rb +0 -26
  290. data/lib/textus/port/store.rb +0 -93
  291. data/lib/textus/port/watcher_lock.rb +0 -48
  292. data/lib/textus/produce/engine.rb +0 -50
  293. data/lib/textus/produce/render.rb +0 -20
  294. data/lib/textus/schema/tools.rb +0 -115
  295. data/lib/textus/schema.rb +0 -100
  296. data/lib/textus/schemas.rb +0 -54
  297. data/lib/textus/store/compositor.rb +0 -34
  298. data/lib/textus/store/container.rb +0 -43
  299. data/lib/textus/store/cursor.rb +0 -26
  300. data/lib/textus/store/envelope/reader.rb +0 -43
  301. data/lib/textus/store/envelope/writer.rb +0 -195
  302. data/lib/textus/store/index/builder.rb +0 -74
  303. data/lib/textus/store/index/lookup.rb +0 -60
  304. data/lib/textus/store/jobs/base.rb +0 -13
  305. data/lib/textus/store/jobs/index.rb +0 -15
  306. data/lib/textus/store/jobs/materialize.rb +0 -15
  307. data/lib/textus/store/jobs/plan.rb +0 -11
  308. data/lib/textus/store/jobs/planner.rb +0 -104
  309. data/lib/textus/store/jobs/queue.rb +0 -154
  310. data/lib/textus/store/jobs/registry.rb +0 -19
  311. data/lib/textus/store/jobs/retention.rb +0 -50
  312. data/lib/textus/store/jobs/sweep.rb +0 -21
  313. data/lib/textus/store/jobs/worker.rb +0 -64
  314. data/lib/textus/store/session.rb +0 -37
  315. data/lib/textus/store.rb +0 -99
  316. data/lib/textus/surface/cli/verb/doctor.rb +0 -19
  317. data/lib/textus/surface/cli/verb/schema_diff.rb +0 -17
  318. data/lib/textus/surface/cli/verb/schema_init.rb +0 -21
  319. data/lib/textus/surface/cli/verb/schema_migrate.rb +0 -21
  320. data/lib/textus/surface/mcp.rb +0 -8
  321. data/lib/textus/surface/projector.rb +0 -27
  322. data/lib/textus/surface/role_scope.rb +0 -34
  323. data/lib/textus/value/command.rb +0 -16
  324. data/lib/textus/value/envelope.rb +0 -89
  325. data/lib/textus/value/result.rb +0 -26
  326. data/lib/textus/workflow/collector.rb +0 -27
  327. data/lib/textus/workflow/context.rb +0 -5
  328. data/lib/textus/workflow/loader.rb +0 -17
  329. data/lib/textus/workflow/pattern.rb +0 -18
@@ -1,273 +0,0 @@
1
- # Textus architecture
2
-
3
- > **Explanation** · for contributors · **read this first** for orientation before SPEC
4
- > **SSoT for** the Ruby implementation layout (layers, container, ports, dispatch paths) · **reviewed** 2026-06 (v0.54)
5
-
6
- ```mermaid
7
- flowchart TD
8
- surfaces["Surfaces — CLI · MCP · RoleScope"]
9
- contract["Contract — per-verb DSL (Arg, Spec, DSL)"]
10
- dispatch["Dispatch — Binder · Around · CommandBuilder · Dispatcher"]
11
- gate["Gate — routing (VERB_COMMAND) · Auth"]
12
- action["Actions — per-verb use cases (action/)"]
13
- jobs["Jobs — Worker · Planner · Materialize · Retention · Sweep"]
14
- produce["Produce — Engine · Render"]
15
- workflow["Workflow — Runner · Registry · Loader · DSL"]
16
- manifest["Manifest — declarative config, no IO (policy/, schema/, entry/)"]
17
- core["Core — pure value types (Freshness, Retention, Duration, Sentinel)"]
18
- ports["Ports — IO adapters (FileStore, AuditLog, JobStore, Publisher…)"]
19
- surfaces --> dispatch
20
- surfaces -.->|declare| contract
21
- contract --> dispatch
22
- dispatch --> gate
23
- gate --> action
24
- action --> jobs
25
- action --> produce
26
- produce --> workflow
27
- action --> manifest
28
- action --> core
29
- action --> ports
30
- jobs --> produce
31
- ```
32
-
33
- *Dependency rule: inward only.* Surfaces → Dispatch → Gate → Actions → inner layers. Actions never reference surfaces.
34
-
35
- ### What lives in each layer
36
-
37
- **Surfaces**
38
-
39
- ```
40
- surfaces/cli/ CLI command generation from contracts
41
- sources.rb CLI-only input acquisition (stdin parsing, file sourcing, coercion)
42
- surfaces/mcp/ MCP server — stdio JSON-RPC 2.0, tools derived from contracts
43
- surfaces/role_scope.rb
44
- (Store#as(role)) — holds (container, role, dry_run, correlation_id);
45
- all verb methods injected via define_method in textus.rb
46
- ```
47
-
48
- **Contract + Dispatch**
49
-
50
- ```
51
- contract/ Per-verb DSL — verb, summary, surfaces, arg, view, around.
52
- Pure declaration: Spec and DSL only.
53
-
54
- dispatch/ Runtime machinery consumed by every surface.
55
- binder.rb Binder — validates required args, resolves session/literal defaults.
56
- around.rb Around — registry of stateful wrappers (cursor, build_lock).
57
- view.rb View.render(spec, surface, result, inputs) — output shaping.
58
- command_builder.rb
59
- CommandBuilder — command construction + role injection.
60
- dispatcher.rb Dispatcher — unified pipeline: Around → Binder → Command → Gate.
61
- resources/ Around resource implementations (Cursor, BuildLock).
62
- ```
63
-
64
- **Gate + Auth**
65
-
66
- ```
67
- gate.rb VERB_COMMAND (verb symbol → Command class)
68
- Gate#dispatch(cmd) — Auth → action call
69
- gate/auth.rb Authorization engine — FLOOR predicates + rule guards
70
- ```
71
-
72
- **Actions**
73
-
74
- ```
75
- action/{get,list,put,key_delete,key_mv,accept,reject,propose,
76
- drain,enqueue,audit,blame,deps,rdeps,published,boot,doctor,
77
- rule_explain,rule_list,rule_lint,pulse,
78
- data_mv,key_mv_prefix,key_delete_prefix,
79
- schema_envelope,where,uid,jobs}.rb
80
- ```
81
-
82
- **Jobs**
83
-
84
- ```
85
- jobs/planner.rb Rules-driven job planning — seeds materialize/refresh/sweep jobs
86
- jobs/worker.rb Queue drain — leases and runs jobs from JobStore
87
- jobs/materialize.rb Per-key produce job → Produce::Engine.converge
88
- jobs/refresh.rb Per-key refresh job (retention-rule-triggered)
89
- jobs/retention.rb Retention policy check
90
- jobs/sweep.rb Lane sweep job
91
- ```
92
-
93
- **Produce**
94
-
95
- ```
96
- produce/engine.rb Engine.converge(container:, call:, keys:)
97
- For each key: looks up a registered workflow and runs it,
98
- or publishes existing bytes for entries with publish targets.
99
- produce/render.rb Mustache template expansion for publish targets
100
- ```
101
-
102
- **Workflow**
103
-
104
- ```
105
- workflow/runner.rb Runs a Definition: build_context → execute_steps → publish
106
- workflow/registry.rb In-memory list of registered Definitions; Registry#for(key)
107
- workflow/loader.rb Loads .textus/workflows/*.rb files via Collector DSL
108
- workflow/dsl.rb Textus.workflow "name" { match "...", step :fetch { ... }, publish }
109
- workflow/context.rb Context passed to each step (key, entry, config, container, call)
110
- workflow/pattern.rb Key-matching pattern (glob via File.fnmatch?)
111
- workflow/errors.rb NotFound, StepFailed
112
- ```
113
-
114
- **Core (pure value types)**
115
-
116
- ```
117
- Freshness::{Verdict,Evaluator}
118
- Core::Duration Core::Sentinel
119
- ```
120
-
121
- **Infrastructure**
122
-
123
- ```
124
- Store (composition root — wires ports, vends Container)
125
- Storage::FileStore (bytes-only port: read/write/delete/exists?/etag)
126
- Manifest (Data, Resolver, Policy, Rules)
127
- Schemas (eager-load cache)
128
- Ports::{AuditLog,JobStore,Publisher,Clock,
129
- BuildLock,SentinelStore}
130
- Entry::{Markdown,Json,Yaml,Text} (format strategies)
131
- ```
132
-
133
- ## How a verb becomes a method
134
-
135
- All actions live under `lib/textus/action/`. The shape is uniform:
136
-
137
- ```ruby
138
- module Textus
139
- module Action
140
- class Get < Base
141
- extend Textus::Contract::DSL
142
-
143
- verb :get
144
- summary "Read an entry by key."
145
- surfaces :cli, :mcp
146
- arg :key, String, required: true, positional: true
147
-
148
- BURN = :sync
149
-
150
- def initialize(key:)
151
- super()
152
- @key = key
153
- end
154
-
155
- def call(container:, call:)
156
- ...
157
- end
158
- end
159
- end
160
- end
161
- ```
162
-
163
- Verbs are looked up in a static frozen table (`Textus::Action::VERBS`) that maps `:get → Action::Get`, `:put → Action::Put`, etc. Adding a new verb is one entry in `VERBS` plus the class — no metaprogramming.
164
-
165
- ## Container
166
-
167
- Use cases never see the raw `Store`. `Textus::Container` is a single record holding the wired collaborators:
168
-
169
- ```ruby
170
- Container = Data.define(
171
- :manifest, :file_store, :schemas, :root,
172
- :audit_log, :workflows, :gate
173
- )
174
- ```
175
-
176
- The `Store` builds one `Container` at boot; every action receives it via `(container:, call:)`. Workflow steps receive a `Workflow::Context` (key, entry, config, container, call).
177
-
178
- ## Ports
179
-
180
- | Class | Role |
181
- |---|---|
182
- | `Ports::Storage::FileStore` | Bytes-only FS I/O — `read`, `write`, `delete`, `exists?`, `etag`. |
183
- | `Ports::AuditLog` | Append-only structured log (`audit.log`). Owns seq numbering, file-locking, rotation. |
184
- | `Ports::Clock` | Supplies `Time.now` — module-function so tests can swap without DI boilerplate. |
185
- | `Ports::Publisher` | Copies a built artifact to a repo-relative consumer path and writes a sentinel. |
186
- | `Ports::BuildLock` | Process-exclusive `flock` guard over the produce pipeline. |
187
- | `Ports::JobStore` | Persistent job queue used by `drain` workers; tracks ready/leased/done/failed jobs. |
188
- | `Ports::SentinelStore` | Reads and writes per-target sentinel files for managed-file detection. |
189
-
190
- Application use cases access ports only through `Container` fields — never through the raw `Store`.
191
-
192
- ### Envelope
193
-
194
- `Envelope::Reader` and `Envelope::Writer` split the envelope pipeline into read-only parse and write-with-audit halves.
195
-
196
- **Reader** (`lib/textus/envelope/reader.rb`) — resolves a key through `manifest.resolver`, reads bytes via `FileStore`, parses via the format strategy, returns an `Envelope`. No audit, no permissions.
197
-
198
- **Writer** (`lib/textus/envelope/writer.rb`) — owns the full write pipeline: serialize → schema-validate → etag-check → `FileStore#write` → `AuditLog#append`. The audit append is the commit step.
199
-
200
- Both are built from a `Container` via named constructors — `Writer.from(container:, call:)` and `Reader.from(container:)`.
201
-
202
- ## Manifest carving
203
-
204
- `Manifest` is a `Data.define` struct with four named members:
205
-
206
- | Member | Class | Responsibility |
207
- |---|---|---|
208
- | `data` | `Manifest::Data` | Frozen value: raw, root, lanes, entries, audit_config, role_caps. |
209
- | `resolver` | `Manifest::Resolver` | Key → `Resolution(entry, path, remaining)`. Nested entry enumeration and fuzzy-match suggestions. |
210
- | `policy` | `Manifest::Policy` | Zone/capability authority — `verb_for_lane`, `roles_with_capability`, `propose_lane_for(role)`. |
211
- | `rules` | `Manifest::Rules` | Pattern-matched rule engine. `rules.for(key)` → `RuleSet(guard, retention, react)`. |
212
-
213
- ## Read path (`store.get(key)`)
214
-
215
- `Action::Get` is a **pure read** — it resolves the path, reads bytes, parses the envelope, and annotates a freshness verdict. It never ingests and never mutates.
216
-
217
- 1. CLI/MCP surface calls `store.as(role).get(key)`.
218
- 2. `Gate#dispatch` runs Auth → `Action::Get#call`.
219
- 3. `Get` resolves path via `manifest.resolver`, reads bytes via `file_store`, parses the envelope, annotates `freshness` based on retention-rule TTL (if any).
220
-
221
- Staleness is age-based (retention-rule TTL vs file mtime). A stale entry is returned stale — the read does not refresh it; `drain` does.
222
-
223
- ## Write path (`store.put(key, ...)`)
224
-
225
- 1. CLI/MCP surface calls `store.as(role).put(key, meta:, body:)`.
226
- 2. `Gate#dispatch` runs Auth → `Action::Put#call`.
227
- 3. `Put` validates, resolves manifest entry, delegates to `Envelope::Writer#put` (serialize → schema-validate → etag-check → FileStore#write → AuditLog#append).
228
- 4. `WriteVerb#cascade_to_rdeps` enqueues `materialize` jobs for any entries with publish_tree that depend on the written key.
229
-
230
- `Action::{KeyDelete,KeyMv,Accept,Reject,Propose}` follow the same shape.
231
-
232
- ## Workflow path (`drain` + materialize jobs)
233
-
234
- The workflow system owns the produce pipeline. `Produce::Engine.converge(container:, call:, keys:)` is the entry point that `Jobs::Materialize` calls.
235
-
236
- For each key, `Engine#produce_one`:
237
-
238
- 1. Looks up a registered workflow via `container.workflows.for(key)` (Pattern matching against the key).
239
- 2. If a workflow matches: `Workflow::Runner.new(definition, container:, call:).run(key)`.
240
- - `Runner` builds a `Workflow::Context`, executes each step in sequence, then publishes.
241
- - **Built-in publish**: writes the step output to the entry via `Envelope::Writer`, then calls `entry.publish_via(pctx)` for any `publish_to` or `publish_tree` targets.
242
- 3. If no workflow matches but the entry has publish targets: runs `publish_only` — publishes existing store bytes via `entry.publish_via` without re-fetching.
243
-
244
- **Workflow files** live in `.textus/workflows/*.rb`. The DSL:
245
-
246
- ```ruby
247
- Textus.workflow "my-producer" do
248
- match "artifacts.derived.*"
249
- step :fetch do |data, ctx|
250
- { content: { "key" => ctx.key } }
251
- end
252
- publish # uses built-in publish (writes to entry, then to publish targets)
253
- end
254
- ```
255
-
256
- ## Agent surface (boot + pulse + MCP)
257
-
258
- ```
259
- agent/plugin ──▶ gate (CLI | MCP) ──▶ Store ──▶ memory (.textus/)
260
- ```
261
-
262
- Two transports, one façade:
263
-
264
- - **CLI** — human/script surface. `textus boot`, `textus pulse --since=N`, `textus get/put/...`.
265
- - **MCP** — agent surface. `textus mcp serve` runs a stdio JSON-RPC 2.0 server. Tools are auto-derived from contracts. Session state (cursor, role, contract_etag) is server-side.
266
-
267
- The agent loop:
268
-
269
- 1. **Session start:** `boot()` → contract envelope (lanes, entries, workflows, write_flows, agent_quickstart).
270
- 2. **Per turn:** `pulse(since=cursor)` → `{cursor, changed, stale, pending_review, doctor, next_due_at}`.
271
- 3. **On demand:** `get`, `put`, `propose`, `schema_show`, `rule_explain`, `drain`.
272
-
273
- Contract drift surfaces as `ContractDrift` (contract_etag mismatch — a change to manifest/schemas); audit cursor expiry as `CursorExpired`. Both signal "call `boot` again."
@@ -1,138 +0,0 @@
1
- # Conventions
2
-
3
- > **Reference** · for integrators · **read when** you're shaping a `.textus/` tree and want the idiomatic choices
4
- > **SSoT for** idiomatic key naming, schema design, and automation integration · **reviewed** 2026-06 (v0.43)
5
-
6
- Guidelines for shaping a `.textus/` tree, naming keys, organising schemas, and integrating with build automation. The spec ([`../../SPEC.md`](../../SPEC.md)) defines what's enforceable; this document captures what's *idiomatic*.
7
-
8
- ## Key naming
9
-
10
- - **Segments are lowercase, kebab- or snake-case.** The grammar `^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?$` is the hard limit. Prefer `acme-dashboard` over `acmedashboard` when there's a natural word break.
11
- - **Lead with the lane in the key path.** `working.projects.acme.dashboard`, not `projects.acme.dashboard`. The lane prefix makes it obvious from the key alone whether a write will be accepted.
12
- - **Mirror the directory structure.** If `working.projects.acme.dashboard` resolves to `working/projects/acme/dashboard.md`, do not invent shortcuts that diverge.
13
- - **Don't pluralise the leaf.** `working.network.org.jane`, not `working.network.org.janes`. Pluralise the container, not the entry.
14
-
15
- ## Lane layout
16
-
17
- Recommended top-level layout — the spec allows alternatives, but this is what tooling will default to:
18
-
19
- ```
20
- .textus/
21
- manifest.yaml
22
- schemas/ # YAML schema definitions
23
- workflows/ # Textus.workflow DSL files for produced entry acquisition
24
- templates/ # ERB templates for publish rendering
25
- data/
26
- knowledge/ # authored truth: identity, voice, decisions — author-holders write
27
- notebook/ # agent's own durable lane (workspace) — keep-holders write
28
- proposals/ # AI proposals awaiting accept (propose)
29
- artifacts/ # computed outputs produced by drain — never edit by hand
30
- raw/ # write-once external source material (ingest)
31
- ```
32
-
33
- Inside `knowledge/`, group by **domain** (identity, people, projects, decisions, runbooks), not by file type or date. `knowledge.identity.*` is the convention for slow-changing identity facts. Inside `artifacts/`, group by **producer** (`artifacts/catalogs/`, `artifacts/indexes/`) so it's clear which build job owns what.
34
-
35
- ## Schema design
36
-
37
- - **One schema per entry type, not per directory.** `person.yaml`, `project.yaml`, `decision.yaml` — applied across multiple subtrees if the shape matches.
38
- - **Required = "this entry is meaningless without it."** Everything else is `optional`. Resist the urge to mark organisational metadata (like `tags`) required.
39
- - **Prefer `enum` over free-text** for low-cardinality fields (relationship type, status, severity). Agents are far better at picking from a list than at producing exact strings.
40
- - **Cap string lengths** with `max:` where the field has a natural bound (names, summaries). Skip for prose body — bodies are not schema-validated, only frontmatter is.
41
-
42
- ## Owner strings
43
-
44
- The `owner:` field in the manifest is **advisory metadata**, not an ACL. Use it to label *who's expected to write here*. The form is `<archetype>` or `<archetype>:<subject>`; the archetype must be one of `human`, `agent`, `automation` (validated at load — ADR 0045), and the subject is free-form:
45
-
46
- - `human:network` — humans curate
47
- - `agent:planner` — a specific named agent
48
- - `automation:catalog-skills` — a specific build job
49
-
50
- Tooling around `git blame` or audit logs may filter on owner; the gem itself only echoes it back in envelopes.
51
-
52
- ## Produced entries
53
-
54
- A produced entry declares `source: { from: external, command: "true", sources: [] }` and a matching `Textus.workflow` block in `.textus/workflows/**/*.rb`. The `source:` acquires **data** — it never renders; rendering is a publish concern (below).
55
-
56
- ```yaml
57
- - key: artifacts.feeds.skills
58
- lane: artifacts
59
- kind: produced
60
- format: json
61
- source: { from: external, command: "true", sources: [] }
62
- publish:
63
- - { to: docs/reference/skills.md, template: feeds/skills.erb }
64
- ```
65
-
66
- The matching workflow block:
67
-
68
- ```ruby
69
- # .textus/workflows/feeds/agentskills.rb
70
- Textus.workflow "agentskills" do
71
- match "artifacts.feeds.skills"
72
-
73
- step :fetch do |_, _ctx|
74
- { "content" => { "skills" => [...], "count" => 1 } }
75
- end
76
-
77
- publish
78
- end
79
- ```
80
-
81
- `drain` discovers workflows, matches entries, runs the steps, and writes the result back to the entry's data path. Publishing then copies or renders each `publish:` target.
82
-
83
- Age-based GC uses a `retention:` rule in the top-level `rules:` block:
84
-
85
- ```yaml
86
- rules:
87
- - match: artifacts.feeds.**
88
- retention: { ttl: 90d, action: archive }
89
- ```
90
-
91
- Full contract in [`../../SPEC.md` §5.2](../../SPEC.md).
92
-
93
- ## Freshness
94
-
95
- A `get` is always a pure read (ADR 0089) — it never triggers a re-produce:
96
-
97
- ```sh
98
- textus pulse --output=json # `stale` lists entries past their ttl
99
- textus drain --as=automation # re-produces stale entries
100
- ```
101
-
102
- See [`./lanes.md`](lanes.md) for lane semantics and [`../how-to/configuring-lanes.md`](../how-to/configuring-lanes.md) for setting up produced entries and workflows.
103
-
104
- ### Read vs. refresh
105
-
106
- There is one public read operation, and it is pure (ADR 0089):
107
-
108
- | Operation | Behaviour | Use for |
109
- |-----------|-----------|---------|
110
- | `ops.get` | A pure on-disk read annotated with a freshness verdict — it NEVER ingests, regardless of the entry's `action`. A stale `refresh` entry reads back stale until the next `drain`. | every caller — interactive reads, dashboards, scripts, and internal pipelines (materializer, projection, schema tooling, accept/reject/publish, uid, validator) |
111
-
112
- Refreshing a stale entry is `drain`'s job (or a `hook run` event), never a read's — so no caller can accidentally trigger network I/O by reading.
113
-
114
- ## Body content
115
-
116
- - **Bodies are Markdown.** Headings, lists, code fences — whatever a human or agent finds useful.
117
- - **The schema does not validate the body.** If a field belongs in structured data, put it in frontmatter, not the body.
118
- - **Keep entries short.** If a project entry hits 500 lines, it probably wants to be split into sub-entries (e.g. `working.projects.acme.dashboard` + `working.projects.acme.api`) rather than one mega-document.
119
-
120
- ## Concurrency
121
-
122
- For multi-writer environments, **always pass `if_etag`** on `put`. The gem treats etag-less writes as last-writer-wins on purpose (single-writer scripts, fresh-file creation), but anything resembling a daemon or a long-running agent should round-trip the etag.
123
-
124
- ## Application layering
125
-
126
- The application layer is organised around three shapes — `Manifest` as a composition record, a single `Container` capability record handed to every use case, and a split envelope reader/writer. See [ADR 0018](../architecture/decisions/0018-manifest-carving.md), [ADR 0017](../architecture/decisions/0017-envelope-io-split.md), [ADR 0022](../architecture/decisions/0022-container-call-dispatcher.md), and [ADR 0023](../architecture/decisions/0023-uniform-use-case-shape.md).
127
-
128
- - **`Manifest` is a composition record** (`Data.define(:data, :resolver, :policy, :rules)`). Reach individual concerns through the field accessors: `manifest.data.entries`, `manifest.policy.permission_for(lane)`, `manifest.resolver.resolve(key)`, `manifest.rules.for(key)`.
129
- - **Use cases are plain `(container:, call:)` classes.** Each is a single class under `lib/textus/action/` with `def initialize(...)` (args from contract) and a `#call(container:, call:)` method; verbs are looked up in the static `Textus::Action::VERBS` table. `Container` is a `Dry::Struct` record bundling the wired ports + manifest (`manifest`, `file_store`, `schemas`, `root`, `audit_log`, `workflows`, `gate`); `Call` is the immutable per-invocation value (`role`, `correlation_id`, `now`, `dry_run`). All dispatch routes through `Gate#dispatch(spec:, inputs:, role:, correlation_id:, session:)` — the single seam that binds inputs via `Gate::Binder`, checks auth, and invokes the action. `store.as(role)` returns a `RoleScope` that forwards verbs to `Gate::dispatch`.
130
- - **Write path is split**: `Envelope::IO::Reader` owns read/parse (existing-uid lookup, raw read, parse), and `Envelope::IO::Writer` owns put/delete/move + the audit-append invariant (every public method's final action is `@audit_log.append(...)`).
131
-
132
- The user-facing CLI surface, the wire envelope shape, and the protocol version (`textus/4`) are unchanged.
133
-
134
- ## Pairing with other tools
135
-
136
- - **MCP servers**: a thin server that exposes `textus get` and `textus put` as tools is the recommended way to give Claude/agents access. Don't bake MCP into this gem.
137
- - **Vector stores**: index `body` content into a vector store if you want fuzzy retrieval. `frontmatter` stays in textus as the source of truth for deterministic facts.
138
- - **CI**: run `textus doctor` (the `generator_drift` check) or `textus pulse` (the `stale` list) in CI to catch drift between derived entries and their sources.
@@ -1,46 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Action
5
- class Accept < Base
6
- verb :accept
7
- summary "apply a queued proposal to its target zone; requires the author capability"
8
- surfaces :cli, :mcp
9
- cli "accept"
10
- arg :pending_key, String, required: true, positional: true, description: "the queued proposal's key"
11
-
12
- def self.call(container:, call:, pending_key:)
13
- env = container.compositor.read(pending_key)
14
- parsed = proposal_from(env, key: pending_key)
15
- return parsed if parsed.is_a?(Dry::Monads::Result::Failure)
16
-
17
- target = parsed[:target_key]
18
- action = parsed[:proposal]["action"] || "put"
19
-
20
- case action
21
- when "put"
22
- mentry = container.manifest.resolver.resolve(target).entry
23
- container.compositor.write(
24
- target,
25
- mentry: mentry,
26
- payload: Textus::Store::Envelope::Writer::Payload.new(
27
- meta: env.meta["_meta"] || {},
28
- body: env.body,
29
- content: nil,
30
- ),
31
- call: call,
32
- )
33
- when "delete"
34
- container.compositor.delete(target, call: call)
35
- else
36
- return Failure(code: :proposal_error, message: "unknown action: #{action}")
37
- end
38
-
39
- container.compositor.delete(pending_key, call: call)
40
-
41
- Success("protocol" => Textus::PROTOCOL, "accepted" => pending_key, "target_key" => target, "action" => action,
42
- "cascade_key" => target)
43
- end
44
- end
45
- end
46
- end
@@ -1,94 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
- require "time"
5
-
6
- module Textus
7
- module Action
8
- class Audit < Base
9
- verb :audit
10
- summary "Query the audit log with optional filters."
11
- surfaces :cli
12
- cli "audit"
13
- arg :key, String, required: false, description: "filter to rows for this key"
14
- arg :lane, String, required: false, description: "filter to keys in this lane"
15
- arg :role, String, required: false, description: "filter to rows written under this role"
16
- arg :verb, String, required: false, description: "filter to rows for this verb"
17
- arg :since, String, required: false,
18
- coerce: ->(s) { Textus::Action::Audit.parse_since(s, now: Time.now) },
19
- description: "ISO-8601 timestamp or relative offset (e.g. 1h, 30m)"
20
- arg :seq_since, Integer, required: false, description: "return rows with seq > this cursor value"
21
- arg :correlation_id, String, required: false, description: "filter to rows with this correlation_id"
22
- arg :limit, Integer, required: false, description: "maximum number of rows to return"
23
- view(:cli) { |rows, _i| { "verb" => "audit", "rows" => rows } }
24
-
25
- def self.call(container:, key: nil, lane: nil, role: nil, verb: nil, since: nil, seq_since: nil, correlation_id: nil, limit: nil, **) # rubocop:disable Metrics/ParameterLists
26
- audit_log = container.audit_log
27
- manifest = container.manifest
28
-
29
- cursor_check = check_cursor_expiry(seq_since, audit_log)
30
- return cursor_check if cursor_check.is_a?(Dry::Monads::Result::Failure)
31
-
32
- Success(audit_log.scan(
33
- seq_since: seq_since,
34
- key: key,
35
- role: role,
36
- verb: verb,
37
- correlation_id: correlation_id,
38
- limit: limit,
39
- ).select do |row|
40
- next false if lane && !key_in_lane?(row["key"], lane, manifest)
41
- next false if since && (row["ts"].nil? || Time.parse(row["ts"]) < since)
42
-
43
- true
44
- end)
45
- end
46
-
47
- def self.parse_since(str, now: Time.now.utc)
48
- return nil if str.nil? || str.empty?
49
- return Time.parse(str) if str =~ /\A\d{4}-\d{2}-\d{2}/
50
-
51
- match = str.match(/\A(\d+)([smhd])\z/) or return nil
52
- mult = { "s" => 1, "m" => 60, "h" => 3600, "d" => 86_400 }[match[2]]
53
- now - (match[1].to_i * mult)
54
- end
55
-
56
- Query = Data.define(:key, :lane, :role, :verb, :since, :seq_since, :correlation_id, :limit) do
57
- # rubocop:disable Metrics/ParameterLists
58
- def self.build(key: nil, lane: nil, role: nil, verb: nil,
59
- since: nil, seq_since: nil, correlation_id: nil, limit: nil)
60
- new(key:, lane:, role:, verb:, since:, seq_since:, correlation_id:, limit:)
61
- end
62
- # rubocop:enable Metrics/ParameterLists
63
-
64
- def matches?(row)
65
- return false if key && row["key"] != key
66
- return false if role && row["role"] != role
67
- return false if verb && row["verb"] != verb
68
- return false if since && (row["ts"].nil? || Time.parse(row["ts"]) < since)
69
- return false if seq_since && (row["seq"].nil? || row["seq"] <= seq_since)
70
- return false if correlation_id && row.dig("extras", "correlation_id") != correlation_id
71
-
72
- true
73
- end
74
- end
75
-
76
- def self.check_cursor_expiry(seq_since, audit_log)
77
- return unless seq_since
78
-
79
- min = audit_log.min_available_seq
80
- return unless min && seq_since < min - 1
81
-
82
- Failure(code: :cursor_expired, message: "requested seq #{seq_since} is below minimum available #{min}",
83
- details: { requested: seq_since, min_available: min })
84
- end
85
-
86
- def self.key_in_lane?(key, lane, manifest)
87
- mentry = manifest.resolver.resolve(key).entry
88
- mentry && mentry.lane == lane
89
- rescue Textus::Error
90
- false
91
- end
92
- end
93
- end
94
- end
@@ -1,42 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Action
5
- @registry = {}
6
-
7
- def self.registry = @registry
8
-
9
- def self.register(klass)
10
- @registry[klass.name.gsub("::", "/").downcase] = klass
11
- end
12
-
13
- def self.fetch(type)
14
- return @registry[type] if @registry[type]
15
-
16
- match = @registry.values.find { |k| k.const_defined?(:TYPE, false) && type == k::TYPE }
17
- raise Textus::UsageError.new("unknown action type: #{type}") unless match
18
-
19
- @registry[type] = match
20
- end
21
-
22
- class Base
23
- extend Contract::DSL
24
- extend Dry::Monads[:result]
25
-
26
- def self.inherited(subclass)
27
- super
28
- Textus::Action.register(subclass) if subclass.name
29
- end
30
-
31
- def self.call(**)
32
- raise NotImplementedError.new("#{name}.call")
33
- end
34
-
35
- def self.proposal_from(env, key:)
36
- proposal = env.meta&.dig("proposal") or return Failure(code: :proposal_error, message: "entry has no proposal block: #{key}")
37
- target = proposal["target_key"] or return Failure(code: :proposal_error, message: "proposal missing target_key")
38
- { proposal:, target_key: target }
39
- end
40
- end
41
- end
42
- end
@@ -1,79 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "open3"
4
-
5
- module Textus
6
- module Action
7
- class Blame < Base
8
- verb :blame
9
- summary "Annotate audit rows for a key with the git commit that introduced each file state."
10
- surfaces :cli
11
- cli "blame"
12
- arg :key, String, required: true, positional: true, description: "entry key to blame"
13
- arg :limit, Integer, required: false, description: "maximum number of audit rows to return"
14
- view(:cli) { |rows, inputs| { "verb" => "blame", "key" => inputs[:key], "rows" => rows } }
15
-
16
- def self.call(container:, key:, limit: nil, **)
17
- manifest = container.manifest
18
- root = container.root
19
-
20
- audit_result = Textus::Action::Audit.call(container: container, key: key, limit: limit)
21
- audit_rows = Value::Result.unwrap(audit_result)
22
- path = resolve_path(key, manifest: manifest)
23
- return Success(audit_rows.map { |row| row.merge("git" => nil) }) unless git_tracked?(path, root: root)
24
-
25
- Success(audit_rows.map { |row| row.merge("git" => git_commit_at(path, timestamp: row["ts"], root: root)) })
26
- end
27
-
28
- def self.resolve_path(key, manifest:)
29
- res = manifest.resolver.resolve(key)
30
- mentry = res.entry
31
- path = res.path
32
- path || Textus::Key::Path.resolve(manifest.data, mentry)
33
- rescue Textus::Error
34
- nil
35
- end
36
-
37
- def self.git_tracked?(path, root:)
38
- return false if path.nil?
39
- return false unless File.exist?(path)
40
- return false unless git_repo?(root)
41
-
42
- _out, _err, status = Open3.capture3(
43
- "git", "ls-files", "--error-unmatch", path,
44
- chdir: root
45
- )
46
- status.success?
47
- rescue Errno::ENOENT
48
- false
49
- end
50
-
51
- def self.git_repo?(root)
52
- dir = root
53
- loop do
54
- return true if File.directory?(File.join(dir, ".git"))
55
-
56
- parent = File.dirname(dir)
57
- return false if parent == dir
58
-
59
- dir = parent
60
- end
61
- end
62
-
63
- def self.git_commit_at(path, timestamp:, root:)
64
- args = ["git", "log", "-1"]
65
- args << "--before=#{timestamp}" if timestamp
66
- args += ["--format=%H%x09%an%x09%aI%x09%s", "--", path]
67
- out, _err, status = Open3.capture3(*args, chdir: root)
68
- return nil unless status.success?
69
-
70
- sha, author, date, subject = out.strip.split("\t", 4)
71
- return nil if sha.nil? || sha.empty?
72
-
73
- { "sha" => sha, "author" => author, "date" => date, "subject" => subject }
74
- rescue Errno::ENOENT
75
- nil
76
- end
77
- end
78
- end
79
- end