textus 0.52.0 → 0.53.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 (254) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/README.md +62 -54
  4. data/SPEC.md +62 -187
  5. data/docs/architecture/README.md +88 -77
  6. data/exe/textus +1 -1
  7. data/lib/textus/action/accept.rb +53 -0
  8. data/lib/textus/action/audit.rb +133 -0
  9. data/lib/textus/action/base.rb +42 -0
  10. data/lib/textus/{read → action}/blame.rb +30 -22
  11. data/lib/textus/action/boot.rb +26 -0
  12. data/lib/textus/action/data_mv.rb +71 -0
  13. data/lib/textus/action/deps.rb +48 -0
  14. data/lib/textus/action/doctor.rb +26 -0
  15. data/lib/textus/action/drain.rb +41 -0
  16. data/lib/textus/action/enqueue.rb +55 -0
  17. data/lib/textus/action/get.rb +80 -0
  18. data/lib/textus/action/jobs.rb +38 -0
  19. data/lib/textus/action/key_delete.rb +46 -0
  20. data/lib/textus/action/key_delete_prefix.rb +46 -0
  21. data/lib/textus/action/key_mv.rb +143 -0
  22. data/lib/textus/action/key_mv_prefix.rb +59 -0
  23. data/lib/textus/action/list.rb +44 -0
  24. data/lib/textus/action/propose.rb +54 -0
  25. data/lib/textus/action/published.rb +26 -0
  26. data/lib/textus/action/pulse/scanner.rb +118 -0
  27. data/lib/textus/action/pulse.rb +87 -0
  28. data/lib/textus/action/put.rb +63 -0
  29. data/lib/textus/action/rdeps.rb +49 -0
  30. data/lib/textus/action/reject.rb +49 -0
  31. data/lib/textus/action/rule_explain.rb +95 -0
  32. data/lib/textus/action/rule_lint.rb +70 -0
  33. data/lib/textus/action/rule_list.rb +46 -0
  34. data/lib/textus/action/schema_envelope.rb +31 -0
  35. data/lib/textus/action/uid.rb +35 -0
  36. data/lib/textus/action/where.rb +38 -0
  37. data/lib/textus/action/write_verb.rb +58 -0
  38. data/lib/textus/background/job/base.rb +27 -0
  39. data/lib/textus/background/job/materialize.rb +31 -0
  40. data/lib/textus/background/job/refresh.rb +22 -0
  41. data/lib/textus/background/job/sweep.rb +31 -0
  42. data/lib/textus/background/job.rb +19 -0
  43. data/lib/textus/background/plan.rb +9 -0
  44. data/lib/textus/background/planner/plan.rb +113 -0
  45. data/lib/textus/{maintenance → background}/retention/apply.rb +7 -9
  46. data/lib/textus/background/worker.rb +67 -0
  47. data/lib/textus/boot.rb +53 -45
  48. data/lib/textus/command.rb +36 -0
  49. data/lib/textus/container.rb +1 -1
  50. data/lib/textus/{domain → core}/duration.rb +1 -1
  51. data/lib/textus/{domain → core}/freshness/evaluator.rb +11 -11
  52. data/lib/textus/{domain → core}/freshness/verdict.rb +2 -2
  53. data/lib/textus/{domain → core}/freshness.rb +2 -2
  54. data/lib/textus/{domain → core}/retention/sweep.rb +7 -7
  55. data/lib/textus/{domain → core}/retention.rb +2 -2
  56. data/lib/textus/{domain → core}/sentinel.rb +1 -1
  57. data/lib/textus/doctor/check/generator_drift.rb +1 -1
  58. data/lib/textus/doctor/check/handler_permit.rb +34 -0
  59. data/lib/textus/doctor/check/hooks.rb +11 -18
  60. data/lib/textus/doctor/check/illegal_keys.rb +1 -1
  61. data/lib/textus/doctor/check/intake_registration.rb +5 -5
  62. data/lib/textus/doctor/check/proposal_targets.rb +3 -3
  63. data/lib/textus/doctor/check/rule_ambiguity.rb +2 -2
  64. data/lib/textus/doctor/check/schema_violations.rb +8 -2
  65. data/lib/textus/doctor/check.rb +12 -9
  66. data/lib/textus/{read → doctor}/validator.rb +22 -13
  67. data/lib/textus/doctor.rb +6 -6
  68. data/lib/textus/envelope/io/writer.rb +65 -36
  69. data/lib/textus/envelope.rb +5 -3
  70. data/lib/textus/errors.rb +17 -9
  71. data/lib/textus/events.rb +21 -0
  72. data/lib/textus/gate/auth.rb +181 -0
  73. data/lib/textus/gate.rb +114 -0
  74. data/lib/textus/init/templates/machine_intake.rb +39 -35
  75. data/lib/textus/init/templates/orientation_reducer.rb +15 -11
  76. data/lib/textus/init.rb +90 -73
  77. data/lib/textus/key/path.rb +9 -2
  78. data/lib/textus/layout.rb +13 -0
  79. data/lib/textus/manifest/data.rb +14 -14
  80. data/lib/textus/manifest/entry/base.rb +15 -11
  81. data/lib/textus/manifest/entry/parser.rb +6 -6
  82. data/lib/textus/manifest/entry/produced.rb +3 -2
  83. data/lib/textus/manifest/entry/publish/mode.rb +1 -1
  84. data/lib/textus/manifest/entry/publish/to_paths.rb +2 -1
  85. data/lib/textus/manifest/entry/validators/events.rb +1 -1
  86. data/lib/textus/{domain/policy/handler_allowlist.rb → manifest/policy/handler_permit.rb} +4 -4
  87. data/lib/textus/{domain → manifest}/policy/matcher.rb +2 -2
  88. data/lib/textus/{domain → manifest}/policy/publish_target.rb +2 -2
  89. data/lib/textus/manifest/policy/react.rb +30 -0
  90. data/lib/textus/{domain → manifest}/policy/retention.rb +3 -3
  91. data/lib/textus/{domain → manifest}/policy/source.rb +24 -19
  92. data/lib/textus/manifest/policy.rb +36 -48
  93. data/lib/textus/manifest/resolver.rb +3 -2
  94. data/lib/textus/manifest/rules.rb +4 -4
  95. data/lib/textus/manifest/schema/keys.rb +17 -11
  96. data/lib/textus/manifest/schema/validator.rb +24 -22
  97. data/lib/textus/manifest/schema/vocabulary.rb +1 -1
  98. data/lib/textus/manifest/schema.rb +2 -2
  99. data/lib/textus/manifest.rb +2 -2
  100. data/lib/textus/{produce → pipeline}/acquire/handler.rb +2 -2
  101. data/lib/textus/{produce → pipeline}/acquire/intake.rb +22 -20
  102. data/lib/textus/{produce → pipeline}/acquire/projection.rb +13 -11
  103. data/lib/textus/{produce → pipeline}/acquire/serializer/json.rb +2 -2
  104. data/lib/textus/{produce → pipeline}/acquire/serializer/text.rb +1 -1
  105. data/lib/textus/{produce → pipeline}/acquire/serializer/yaml.rb +2 -2
  106. data/lib/textus/{produce → pipeline}/acquire/serializer.rb +1 -1
  107. data/lib/textus/{produce → pipeline}/engine.rb +7 -5
  108. data/lib/textus/{produce → pipeline}/render.rb +3 -1
  109. data/lib/textus/ports/audit_log.rb +31 -5
  110. data/lib/textus/ports/audit_subscriber.rb +4 -4
  111. data/lib/textus/{domain/jobs → ports/queue}/job.rb +19 -12
  112. data/lib/textus/ports/queue.rb +1 -1
  113. data/lib/textus/ports/sentinel_store.rb +2 -2
  114. data/lib/textus/ports/watcher_lock.rb +48 -0
  115. data/lib/textus/projection.rb +8 -8
  116. data/lib/textus/schema/tools.rb +4 -3
  117. data/lib/textus/session.rb +6 -3
  118. data/lib/textus/step/base.rb +35 -0
  119. data/lib/textus/step/builtin/csv_fetch.rb +19 -0
  120. data/lib/textus/step/builtin/ical_events_fetch.rb +30 -0
  121. data/lib/textus/step/builtin/json_fetch.rb +18 -0
  122. data/lib/textus/step/builtin/markdown_links_fetch.rb +20 -0
  123. data/lib/textus/step/builtin/rss_fetch.rb +26 -0
  124. data/lib/textus/step/builtin.rb +22 -0
  125. data/lib/textus/{hooks → step}/catalog.rb +3 -3
  126. data/lib/textus/{hooks → step}/context.rb +15 -13
  127. data/lib/textus/step/discovery.rb +24 -0
  128. data/lib/textus/{hooks → step}/error_log.rb +1 -1
  129. data/lib/textus/{hooks → step}/event_bus.rb +15 -16
  130. data/lib/textus/step/fetch.rb +13 -0
  131. data/lib/textus/{hooks → step}/fire_report.rb +1 -1
  132. data/lib/textus/step/loader.rb +108 -0
  133. data/lib/textus/step/observe.rb +31 -0
  134. data/lib/textus/step/registry_store.rb +66 -0
  135. data/lib/textus/{hooks → step}/signature.rb +1 -1
  136. data/lib/textus/step/transform.rb +12 -0
  137. data/lib/textus/step/validate.rb +11 -0
  138. data/lib/textus/step.rb +10 -0
  139. data/lib/textus/store.rb +17 -15
  140. data/lib/textus/surfaces/cli/group/data.rb +11 -0
  141. data/lib/textus/surfaces/cli/group/key.rb +11 -0
  142. data/lib/textus/surfaces/cli/group/mcp.rb +11 -0
  143. data/lib/textus/surfaces/cli/group/rule.rb +11 -0
  144. data/lib/textus/surfaces/cli/group/schema.rb +11 -0
  145. data/lib/textus/surfaces/cli/group.rb +50 -0
  146. data/lib/textus/surfaces/cli/runner.rb +236 -0
  147. data/lib/textus/surfaces/cli/verb/doctor.rb +21 -0
  148. data/lib/textus/surfaces/cli/verb/get.rb +21 -0
  149. data/lib/textus/surfaces/cli/verb/init.rb +20 -0
  150. data/lib/textus/surfaces/cli/verb/mcp_serve.rb +24 -0
  151. data/lib/textus/surfaces/cli/verb/put.rb +30 -0
  152. data/lib/textus/surfaces/cli/verb/schema_diff.rb +17 -0
  153. data/lib/textus/surfaces/cli/verb/schema_init.rb +21 -0
  154. data/lib/textus/surfaces/cli/verb/schema_migrate.rb +21 -0
  155. data/lib/textus/surfaces/cli/verb/watch.rb +19 -0
  156. data/lib/textus/surfaces/cli/verb.rb +111 -0
  157. data/lib/textus/surfaces/cli.rb +148 -0
  158. data/lib/textus/surfaces/mcp/catalog.rb +99 -0
  159. data/lib/textus/surfaces/mcp/errors.rb +34 -0
  160. data/lib/textus/surfaces/mcp/server.rb +145 -0
  161. data/lib/textus/surfaces/mcp/session.rb +9 -0
  162. data/lib/textus/surfaces/mcp/tool_schemas.rb +17 -0
  163. data/lib/textus/surfaces/mcp.rb +8 -0
  164. data/lib/textus/surfaces/role_scope.rb +38 -0
  165. data/lib/textus/surfaces/watcher.rb +38 -0
  166. data/lib/textus/version.rb +1 -1
  167. data/lib/textus.rb +64 -22
  168. metadata +132 -118
  169. data/lib/textus/cli/group/hook.rb +0 -9
  170. data/lib/textus/cli/group/key.rb +0 -9
  171. data/lib/textus/cli/group/mcp.rb +0 -9
  172. data/lib/textus/cli/group/rule.rb +0 -9
  173. data/lib/textus/cli/group/schema.rb +0 -9
  174. data/lib/textus/cli/group/zone.rb +0 -9
  175. data/lib/textus/cli/group.rb +0 -48
  176. data/lib/textus/cli/runner.rb +0 -193
  177. data/lib/textus/cli/verb/doctor.rb +0 -17
  178. data/lib/textus/cli/verb/get.rb +0 -18
  179. data/lib/textus/cli/verb/hook_run.rb +0 -48
  180. data/lib/textus/cli/verb/hooks.rb +0 -50
  181. data/lib/textus/cli/verb/init.rb +0 -18
  182. data/lib/textus/cli/verb/mcp_serve.rb +0 -22
  183. data/lib/textus/cli/verb/put.rb +0 -30
  184. data/lib/textus/cli/verb/schema_diff.rb +0 -15
  185. data/lib/textus/cli/verb/schema_init.rb +0 -19
  186. data/lib/textus/cli/verb/schema_migrate.rb +0 -19
  187. data/lib/textus/cli/verb/serve.rb +0 -19
  188. data/lib/textus/cli/verb.rb +0 -116
  189. data/lib/textus/cli.rb +0 -138
  190. data/lib/textus/dispatcher.rb +0 -54
  191. data/lib/textus/doctor/check/handler_allowlist.rb +0 -34
  192. data/lib/textus/domain/action.rb +0 -9
  193. data/lib/textus/domain/jobs/registry.rb +0 -37
  194. data/lib/textus/domain/permission.rb +0 -7
  195. data/lib/textus/domain/policy/base_guards.rb +0 -25
  196. data/lib/textus/domain/policy/evaluation.rb +0 -15
  197. data/lib/textus/domain/policy/guard.rb +0 -35
  198. data/lib/textus/domain/policy/guard_factory.rb +0 -40
  199. data/lib/textus/domain/policy/predicates/author_held.rb +0 -33
  200. data/lib/textus/domain/policy/predicates/etag_match.rb +0 -32
  201. data/lib/textus/domain/policy/predicates/fresh_within.rb +0 -59
  202. data/lib/textus/domain/policy/predicates/registry.rb +0 -39
  203. data/lib/textus/domain/policy/predicates/schema_valid.rb +0 -61
  204. data/lib/textus/domain/policy/predicates/target_is_canon.rb +0 -33
  205. data/lib/textus/domain/policy/predicates/zone_writable_by.rb +0 -39
  206. data/lib/textus/hooks/builtin.rb +0 -70
  207. data/lib/textus/hooks/loader.rb +0 -54
  208. data/lib/textus/hooks/rpc_registry.rb +0 -43
  209. data/lib/textus/jobs/handlers.rb +0 -62
  210. data/lib/textus/jobs/scheduler.rb +0 -36
  211. data/lib/textus/jobs/seeder.rb +0 -57
  212. data/lib/textus/maintenance/drain.rb +0 -42
  213. data/lib/textus/maintenance/key_delete_prefix.rb +0 -48
  214. data/lib/textus/maintenance/key_mv_prefix.rb +0 -68
  215. data/lib/textus/maintenance/rule_lint.rb +0 -66
  216. data/lib/textus/maintenance/serve.rb +0 -30
  217. data/lib/textus/maintenance/worker.rb +0 -74
  218. data/lib/textus/maintenance/zone_mv.rb +0 -64
  219. data/lib/textus/maintenance.rb +0 -15
  220. data/lib/textus/mcp/catalog.rb +0 -70
  221. data/lib/textus/mcp/errors.rb +0 -32
  222. data/lib/textus/mcp/server.rb +0 -138
  223. data/lib/textus/mcp/session.rb +0 -7
  224. data/lib/textus/mcp/tool_schemas.rb +0 -15
  225. data/lib/textus/mcp.rb +0 -6
  226. data/lib/textus/mustache.rb +0 -117
  227. data/lib/textus/ports/produce_on_write_subscriber.rb +0 -73
  228. data/lib/textus/produce/events.rb +0 -36
  229. data/lib/textus/read/audit.rb +0 -130
  230. data/lib/textus/read/boot.rb +0 -26
  231. data/lib/textus/read/capabilities.rb +0 -70
  232. data/lib/textus/read/deps.rb +0 -38
  233. data/lib/textus/read/doctor.rb +0 -27
  234. data/lib/textus/read/freshness.rb +0 -152
  235. data/lib/textus/read/get.rb +0 -73
  236. data/lib/textus/read/jobs.rb +0 -31
  237. data/lib/textus/read/list.rb +0 -24
  238. data/lib/textus/read/published.rb +0 -22
  239. data/lib/textus/read/pulse.rb +0 -98
  240. data/lib/textus/read/rdeps.rb +0 -39
  241. data/lib/textus/read/rule_explain.rb +0 -96
  242. data/lib/textus/read/rule_list.rb +0 -54
  243. data/lib/textus/read/schema_envelope.rb +0 -25
  244. data/lib/textus/read/uid.rb +0 -29
  245. data/lib/textus/read/validate_all.rb +0 -36
  246. data/lib/textus/read/where.rb +0 -24
  247. data/lib/textus/role_scope.rb +0 -78
  248. data/lib/textus/write/accept.rb +0 -58
  249. data/lib/textus/write/enqueue.rb +0 -50
  250. data/lib/textus/write/key_delete.rb +0 -65
  251. data/lib/textus/write/key_mv.rb +0 -141
  252. data/lib/textus/write/propose.rb +0 -54
  253. data/lib/textus/write/put.rb +0 -74
  254. data/lib/textus/write/reject.rb +0 -68
@@ -1,141 +0,0 @@
1
- module Textus
2
- module Write
3
- class KeyMv
4
- extend Textus::Contract::DSL
5
-
6
- verb :key_mv
7
- summary "Rename one entry (same zone + format). Refuses if the target exists. Single-key, lower blast radius than key_mv_prefix."
8
- surfaces :cli, :mcp
9
- cli "key mv"
10
- arg :old_key, String, required: true, positional: true,
11
- description: "current dotted key"
12
- arg :new_key, String, required: true, positional: true,
13
- description: "new dotted key (must be the same zone and format as old_key)"
14
- arg :dry_run, :boolean,
15
- description: "when true, returns the planned move (from/to paths, uid) without applying it; " \
16
- "defaults to false, so omitting it applies the move immediately " \
17
- "(unlike the bulk key_mv_prefix, which defaults to a dry-run plan)"
18
- # `call` already returns a wire hash; identity response.
19
-
20
- def initialize(container:, call:)
21
- @container = container
22
- @call = call
23
- @manifest = container.manifest
24
- @events = container.events
25
- end
26
-
27
- def call(old_key, new_key, dry_run: false)
28
- old_res, new_res = prepare(old_key, new_key)
29
- return dry_run_result(old_key, new_key, old_res, new_res) if dry_run
30
-
31
- ensure_uid!(old_key, old_res.entry)
32
- envelope = writer.move(
33
- from_key: old_key, to_key: new_key,
34
- new_mentry: new_res.entry
35
- )
36
- publish_renamed(old_key, new_key, envelope)
37
- success_result(old_key, new_key, old_res, new_res, envelope)
38
- end
39
-
40
- private
41
-
42
- def hook_context
43
- @hook_context ||= Textus::Hooks::Context.for(container: @container, call: @call)
44
- end
45
-
46
- def prepare(old_key, new_key)
47
- Textus::Manifest::Data.validate_key!(old_key)
48
- Textus::Manifest::Data.validate_key!(new_key)
49
- raise UsageError.new("mv: old and new keys are identical") if old_key == new_key
50
-
51
- old_res = @manifest.resolver.resolve(old_key)
52
- new_res = @manifest.resolver.resolve(new_key)
53
- raise UnknownKey.new(old_key) unless reader.exists?(old_key)
54
-
55
- validate_zone_and_format!(old_res.entry, new_res.entry)
56
- guard_for(:key_mv, old_key).check!(eval_for(:key_mv, target_key: old_key))
57
- guard_for(:key_mv, new_key).check!(eval_for(:key_mv, target_key: new_key))
58
- raise UsageError.new("mv: target '#{new_key}' already exists at #{new_res.path}") if reader.exists?(new_key)
59
-
60
- [old_res, new_res]
61
- end
62
-
63
- def validate_zone_and_format!(old_mentry, new_mentry)
64
- if old_mentry.zone != new_mentry.zone
65
- raise UsageError.new(
66
- "mv: cross-zone move refused (#{old_mentry.zone} → #{new_mentry.zone}). " \
67
- "Use put+delete for cross-zone moves.",
68
- )
69
- end
70
- return if old_mentry.format == new_mentry.format
71
-
72
- raise UsageError.new("mv: format mismatch (#{old_mentry.format} → #{new_mentry.format}); refusing.")
73
- end
74
-
75
- # If the source file lacks a UID, rewrite it in-place via the writer
76
- # so a UID gets injected before the move. This produces one "put"
77
- # audit row, then the "mv" row from Writer#move.
78
- def ensure_uid!(old_key, old_mentry)
79
- pre_env = reader.read(old_key)
80
- return if pre_env.uid
81
-
82
- writer.put(
83
- old_key, mentry: old_mentry,
84
- payload: Textus::Envelope::IO::Writer::Payload.new(
85
- meta: pre_env.meta, body: pre_env.body, content: pre_env.content,
86
- )
87
- )
88
- end
89
-
90
- def publish_renamed(old_key, new_key, envelope)
91
- @events.publish(:entry_renamed,
92
- ctx: hook_context,
93
- key: new_key,
94
- from_key: old_key,
95
- to_key: new_key,
96
- envelope: envelope)
97
- end
98
-
99
- def dry_run_result(old_key, new_key, old_res, new_res)
100
- pre_env = reader.read(old_key)
101
- {
102
- "protocol" => PROTOCOL, "ok" => true, "dry_run" => true,
103
- "from_key" => old_key, "to_key" => new_key,
104
- "from_path" => old_res.path, "to_path" => new_res.path,
105
- "uid" => pre_env.uid
106
- }
107
- end
108
-
109
- def success_result(old_key, new_key, old_res, new_res, envelope)
110
- {
111
- "protocol" => PROTOCOL, "ok" => true,
112
- "from_key" => old_key, "to_key" => new_key,
113
- "from_path" => old_res.path, "to_path" => new_res.path,
114
- "uid" => envelope.uid,
115
- "envelope" => envelope.to_h_for_wire
116
- }
117
- end
118
-
119
- def guard_for(transition, key, if_etag: nil)
120
- Textus::Domain::Policy::GuardFactory.new(
121
- manifest: @manifest, schemas: @container.schemas, extra: { if_etag: if_etag },
122
- ).for(transition, key)
123
- end
124
-
125
- def eval_for(transition, target_key:, envelope: nil)
126
- Textus::Domain::Policy::Evaluation.new(
127
- actor: @call.role, transition: transition, origin: nil,
128
- target: target_key, envelope: envelope, manifest: @manifest
129
- )
130
- end
131
-
132
- def writer
133
- @writer ||= Textus::Envelope::IO::Writer.from(container: @container, call: @call)
134
- end
135
-
136
- def reader
137
- @reader ||= Textus::Envelope::IO::Reader.from(container: @container)
138
- end
139
- end
140
- end
141
- end
@@ -1,54 +0,0 @@
1
- module Textus
2
- module Write
3
- # Queue a proposal: resolve the acting role's propose_zone, prefix the key,
4
- # and write there via the Put verb. Was inlined in the MCP `propose` tool
5
- # and the CLI propose verb; promoted to a first-class verb so all three
6
- # transports share one implementation (ADR 0036, ADR 0039).
7
- class Propose
8
- extend Textus::Contract::DSL
9
-
10
- verb :propose
11
- summary "Write a proposal to the role's propose_zone. Auto-prefixes the key."
12
- surfaces :cli, :mcp
13
- cli_stdin :json
14
- arg :key, String, required: true, positional: true,
15
- description: "key relative to propose_zone, e.g. 'decisions.feature-x'"
16
- arg :meta, Hash, required: false, wire_name: :_meta,
17
- description: "frontmatter; reads back as `_meta` from `get`. Include a 'proposal:' block naming the target_key"
18
- arg :body, String,
19
- description: "markdown/text payload for markdown-format entries; omit (use `content`) for json/yaml entries. Do not send both"
20
- arg :content, Hash,
21
- description: "structured payload for json/yaml-format entries; omit (use `body`) for markdown entries. Do not send both"
22
- # ADR 0069: every surface receives the raw Envelope and self-shapes — no
23
- # surface pre-wires the result. Emitting the full wire envelope on every
24
- # surface is a superset of the old `{uid, etag, key}` (the accepted
25
- # breaking change; MCP/Ruby now get the full envelope too).
26
- view { |env, _i| env.to_h_for_wire }
27
-
28
- def initialize(container:, call:)
29
- @container = container
30
- @call = call
31
- @manifest = container.manifest
32
- end
33
-
34
- # if_etag is intentionally absent: a proposal is always a fresh queue write.
35
- def call(key, meta: nil, body: nil, content: nil)
36
- zone = @manifest.policy.propose_zone_for(@call.role)
37
- unless zone
38
- raise Textus::Error.new(
39
- "propose_forbidden",
40
- "role '#{@call.role}' has no writable propose_zone",
41
- details: { "role" => @call.role },
42
- hint: "the manifest must define a queue zone and '#{@call.role}' must hold the 'propose' capability",
43
- )
44
- end
45
-
46
- Textus::Dispatcher.invoke(
47
- :put, container: @container, call: @call,
48
- args: ["#{zone}.#{key}"],
49
- kwargs: { meta: meta || {}, body: body, content: content }
50
- )
51
- end
52
- end
53
- end
54
- end
@@ -1,74 +0,0 @@
1
- module Textus
2
- module Write
3
- class Put
4
- extend Textus::Contract::DSL
5
-
6
- verb :put
7
- summary "Create or update an entry. Schema-validated. Returns {uid, etag}."
8
- surfaces :cli, :mcp
9
- arg :key, String, required: true, positional: true,
10
- description: "dotted entry key, e.g. 'knowledge.project'; must resolve to a zone the role may write"
11
- arg :meta, Hash, required: false, wire_name: :_meta,
12
- description: "frontmatter; reads back as `_meta` from `get`. Schema-validated — call `schema KEY` first"
13
- arg :body, String,
14
- description: "markdown/text payload for markdown-format entries; omit (use `content`) for json/yaml entries. Do not send both"
15
- arg :content, Hash,
16
- description: "structured payload for json/yaml-format entries; omit (use `body`) for markdown entries. Do not send both"
17
- arg :if_etag, String,
18
- description: "optimistic-concurrency guard: the etag you last read; the write is rejected if the entry changed since"
19
- view { |env| { "uid" => env.uid, "etag" => env.etag } }
20
-
21
- def initialize(container:, call:)
22
- @container = container
23
- @call = call
24
- @manifest = container.manifest
25
- @events = container.events
26
- end
27
-
28
- def call(key, meta: nil, body: nil, content: nil, if_etag: nil)
29
- Textus::Manifest::Data.validate_key!(key)
30
- mentry = @manifest.resolver.resolve(key).entry
31
- guard_for(:put, key, if_etag: if_etag).check!(eval_for(:put, target_key: key))
32
-
33
- envelope = writer.put(
34
- key,
35
- mentry: mentry,
36
- payload: Textus::Envelope::IO::Writer::Payload.new(
37
- meta: meta, body: body, content: content,
38
- ),
39
- if_etag: if_etag,
40
- )
41
-
42
- @events.publish(:entry_written,
43
- ctx: hook_context,
44
- key: key,
45
- envelope: envelope)
46
-
47
- envelope
48
- end
49
-
50
- private
51
-
52
- def guard_for(transition, key, if_etag: nil)
53
- Textus::Domain::Policy::GuardFactory.new(
54
- manifest: @manifest, schemas: @container.schemas, extra: { if_etag: if_etag },
55
- ).for(transition, key)
56
- end
57
-
58
- def eval_for(transition, target_key:, envelope: nil)
59
- Textus::Domain::Policy::Evaluation.new(
60
- actor: @call.role, transition: transition, origin: nil,
61
- target: target_key, envelope: envelope, manifest: @manifest
62
- )
63
- end
64
-
65
- def hook_context
66
- @hook_context ||= Textus::Hooks::Context.for(container: @container, call: @call)
67
- end
68
-
69
- def writer
70
- @writer ||= Textus::Envelope::IO::Writer.from(container: @container, call: @call)
71
- end
72
- end
73
- end
74
- end
@@ -1,68 +0,0 @@
1
- module Textus
2
- module Write
3
- class Reject
4
- extend Textus::Contract::DSL
5
-
6
- verb :reject
7
- summary "discard a queued proposal without applying it"
8
- surfaces :cli, :mcp
9
- cli "reject"
10
- arg :pending_key, String, required: true, positional: true, description: "the queued proposal's key"
11
-
12
- def initialize(container:, call:)
13
- @container = container
14
- @call = call
15
- @manifest = container.manifest
16
- @schemas = container.schemas
17
- @events = container.events
18
- end
19
-
20
- def call(pending_key)
21
- guard.for(:reject, pending_key).check!(
22
- Textus::Domain::Policy::Evaluation.new(
23
- actor: @call.role, transition: :reject, origin: pending_key,
24
- target: pending_key, envelope: nil, manifest: @manifest
25
- ),
26
- )
27
-
28
- mentry = @manifest.resolver.resolve(pending_key).entry
29
- unless mentry.in_proposal_zone?(@manifest.policy)
30
- raise ProposalError.new("reject: '#{pending_key}' is not in a proposal zone (zone=#{mentry.zone})")
31
- end
32
-
33
- env = Textus::Read::Get.new(
34
- container: @container, call: @call,
35
- ).call(pending_key)
36
- proposal = env.meta&.dig("proposal") or
37
- raise ProposalError.new("entry has no proposal block: #{pending_key}")
38
- target_key = proposal["target_key"] or
39
- raise ProposalError.new("proposal missing target_key")
40
-
41
- delete_op.call(pending_key, suppress_events: true)
42
-
43
- @events.publish(:proposal_rejected,
44
- ctx: hook_context,
45
- key: pending_key,
46
- target_key: target_key)
47
-
48
- { "protocol" => PROTOCOL, "rejected" => pending_key, "target_key" => target_key }
49
- end
50
-
51
- private
52
-
53
- def guard
54
- @guard ||= Textus::Domain::Policy::GuardFactory.new(manifest: @manifest, schemas: @schemas)
55
- end
56
-
57
- def hook_context
58
- @hook_context ||= Textus::Hooks::Context.for(container: @container, call: @call)
59
- end
60
-
61
- def delete_op
62
- @delete_op ||= Textus::Write::KeyDelete.new(
63
- container: @container, call: @call,
64
- )
65
- end
66
- end
67
- end
68
- end