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,54 @@
1
+ module Textus
2
+ module Lanes
3
+ module Ingest
4
+ module Handlers
5
+ SOURCE_KINDS = %w[url file asset].freeze
6
+
7
+ module_function
8
+
9
+ def ingest(kind:, slug:, ctx:, call:, url: nil, path: nil, lane: nil, label: nil)
10
+ raise Textus::UsageError.new("kind must be one of #{SOURCE_KINDS.join("|")}") unless SOURCE_KINDS.include?(kind)
11
+
12
+ case kind
13
+ when "url" then raise Textus::UsageError.new("url required") unless url
14
+ when "file" then raise Textus::UsageError.new("path required") unless path
15
+ when "asset"
16
+ raise Textus::UsageError.new("path required") unless path
17
+ raise Textus::UsageError.new("lane required") unless lane
18
+ end
19
+
20
+ now = Time.now.utc
21
+ key = KeyBuilder.derive_key(now, kind, slug)
22
+ content_hash = KeyBuilder.compute_hash(kind:, url:, path:)
23
+
24
+ entry_type = EntryTypes.for(kind)
25
+ time = now
26
+ structured = entry_type.build(url:, path:, lane:, label:, time:, layout: ctx.layout)
27
+
28
+ lookup = Textus::Infra::Store::Index::Lookup.new(ctx.container.store.db)
29
+ duplicate_key = Dedup.find(lookup, content_hash, url:)
30
+
31
+ if duplicate_key && duplicate_key != key
32
+ old_content = ctx.read(key: duplicate_key)&.content || {}
33
+ Resolver.supersede(duplicate_key, key, ctx:, call:)
34
+
35
+ EntryTypes::Asset.move(old_content["asset"], lane, ctx.layout) if (kind == "asset") && old_content["asset"]
36
+
37
+ old_count = old_content["ingest_count"] || 1
38
+ structured["ingest_count"] = old_count + 1
39
+ structured["duplicated_at"] = now.utc.iso8601
40
+ structured["supersedes"] = duplicate_key
41
+ else
42
+ structured["ingest_count"] = 1
43
+ end
44
+
45
+ env = ctx.put(key:, meta: {}, body: nil, content: structured, call:)
46
+
47
+ IndexRebuilder.rebuild!(ctx.container.store.db, ctx.manifest.resolver)
48
+
49
+ { "key" => key, "uid" => env.uid, "etag" => env.etag }
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,13 @@
1
+ module Textus
2
+ module Lanes
3
+ module Ingest
4
+ module IndexRebuilder
5
+ module_function
6
+
7
+ def rebuild!(db, resolver)
8
+ Textus::Infra::Store::Index::Builder.new(db).rebuild!(resolver:)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,33 @@
1
+ module Textus
2
+ module Lanes
3
+ module Ingest
4
+ module KeyBuilder
5
+ CONTENT_HASH_ALGO = "sha256"
6
+
7
+ module_function
8
+
9
+ def derive_key(now, kind, slug)
10
+ date = now.strftime("%Y.%m.%d")
11
+ "raw.#{date}.#{kind}-#{slug}"
12
+ end
13
+
14
+ def compute_hash(kind:, url: nil, path: nil)
15
+ digest = Digest::SHA256.new
16
+ case kind
17
+ when "url"
18
+ raise ArgumentError.new("url required") unless url
19
+
20
+ digest.update(url)
21
+ when "file", "asset"
22
+ raise ArgumentError.new("path required") unless path
23
+
24
+ digest.file(path)
25
+ else
26
+ raise ArgumentError.new("unknown kind: #{kind}")
27
+ end
28
+ "#{CONTENT_HASH_ALGO}:#{digest.hexdigest}"
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,21 @@
1
+ module Textus
2
+ module Lanes
3
+ module Ingest
4
+ module Resolver
5
+ module_function
6
+
7
+ def supersede(old_key, new_key, ctx:, call:)
8
+ old_env = ctx.read(key: old_key)
9
+ old_content = old_env&.content || {}
10
+ tombstone = {}
11
+ %w[ingested_at ingest_count].each { |k| tombstone[k] = old_content[k] if old_content.key?(k) }
12
+ source_kind = old_content.dig("source", "kind")
13
+ tombstone["source"] = { "kind" => source_kind } if source_kind
14
+ tombstone["superseded_by"] = new_key
15
+
16
+ ctx.put(key: old_key, meta: {}, body: nil, content: tombstone, call:)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,15 @@
1
+ module Textus
2
+ module Lanes
3
+ module Knowledge
4
+ module Freeform
5
+ def self.entries(manifest)
6
+ manifest.data.entries.reject { |e| e.key.start_with?("knowledge.loop.") }
7
+ end
8
+
9
+ def self.get(store, key)
10
+ store.get(key:)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,53 @@
1
+ module Textus
2
+ module Lanes
3
+ module Knowledge
4
+ module Handlers
5
+ module_function
6
+
7
+ LOOP_NODE_KEYS = {
8
+ "intent" => "knowledge.loop.intent",
9
+ "constraint.repo" => "knowledge.loop.constraint.repo",
10
+ "constraint.protocol" => "knowledge.loop.constraint.protocol",
11
+ "judgment.engineering" => "knowledge.loop.judgment.engineering",
12
+ "judgment.agent-behavior" => "knowledge.loop.judgment.agent-behavior",
13
+ "evidence" => "knowledge.loop.evidence",
14
+ "execution" => "knowledge.loop.execution.runbook",
15
+ "adoption" => "knowledge.loop.adoption",
16
+ }.freeze
17
+
18
+ def loop(ctx:, call:)
19
+ nodes = LOOP_NODE_KEYS.map { |name, key| { "name" => name, "key" => key } }
20
+ role = call&.role || "agent"
21
+ runner = Runner.new(ctx.manifest, cursor_store: ctx.cursor_store)
22
+ feedback = recent_feedback(ctx)
23
+ {
24
+ "loop" => nodes,
25
+ "count" => nodes.size,
26
+ "flow" => runner.flow,
27
+ "current_node" => runner.current_position(role),
28
+ "feedback" => feedback,
29
+ }
30
+ end
31
+
32
+ def recent_feedback(ctx)
33
+ rows = ctx.entry_index&.search(schema: "loop-feedback", lane: "knowledge", query: nil, prefix: nil)
34
+ return [] unless rows&.any?
35
+
36
+ rows.map do |row|
37
+ env = ctx.read(key: row["key"])
38
+ meta = env&.meta || {}
39
+ {
40
+ "key" => row["key"],
41
+ "node" => meta["node"],
42
+ "confidence" => meta["confidence"],
43
+ "signal" => meta["signal"],
44
+ "summary" => env&.body || meta["summary"],
45
+ }
46
+ end
47
+ rescue Textus::Error
48
+ []
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,45 @@
1
+ module Textus
2
+ module Lanes
3
+ module Knowledge
4
+ class Runner
5
+ def initialize(manifest, cursor_store: nil)
6
+ @manifest = manifest
7
+ @cursor_store = cursor_store
8
+ end
9
+
10
+ def loop_nodes
11
+ @manifest.data.entries.select do |e|
12
+ e.key.start_with?("knowledge.loop.") && e.key.count(".") == 2
13
+ end
14
+ end
15
+
16
+ def flow
17
+ loop_nodes.map { |e| e.key.split(".").last }
18
+ end
19
+
20
+ def current_node(active_index: 0)
21
+ nodes = loop_nodes
22
+ return nil if nodes.empty?
23
+
24
+ idx = active_index % nodes.size
25
+ { name: nodes[idx].key.split(".").last, index: idx, total: nodes.size }
26
+ end
27
+
28
+ def advance(active_index: 0)
29
+ current_node(active_index: active_index + 1).merge(advanced: true)
30
+ end
31
+
32
+ def current_position(role)
33
+ idx = @cursor_store ? @cursor_store.get_loop(role)[:index] : 0
34
+ current_node(active_index: idx)
35
+ end
36
+
37
+ def advance_role(role)
38
+ idx = @cursor_store ? @cursor_store.get_loop(role)[:index] + 1 : 1
39
+ @cursor_store&.set_loop(role, idx)
40
+ current_node(active_index: idx).merge(advanced: true)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,16 @@
1
+ module Textus
2
+ module Lanes
3
+ module Knowledge
4
+ module Verbs
5
+ end
6
+ end
7
+ end
8
+ end
9
+
10
+ Textus::Protocol::VerbRegistry.register(
11
+ Textus::Protocol::VerbRegistry::VerbSpec.new(
12
+ verb: :loop, summary: "Return the knowledge loop structure — node names and flow order.",
13
+ args: [], surfaces: %i[cli mcp], category: :read,
14
+ lane: :knowledge
15
+ ),
16
+ )
@@ -0,0 +1,164 @@
1
+ module Textus
2
+ module Lanes
3
+ module Scratchpad
4
+ module Handlers
5
+ PROPOSALS_PARENT_KEY = "scratchpad.proposals"
6
+
7
+ module_function
8
+
9
+ SESSION_PARENT_KEY = "scratchpad.sessions"
10
+
11
+ def session_open(ctx:, call:)
12
+ uuid = SecureRandom.uuid
13
+ key = "scratchpad.sessions.#{uuid}.session"
14
+
15
+ content = { "session_id" => uuid, "created_at" => Time.now.utc.iso8601, "status" => "active" }
16
+ ctx.put(key:, meta: {}, body: JSON.generate(content), content: nil, call:)
17
+
18
+ boot = begin
19
+ Textus::Protocol::Boot.build(container: ctx.container)
20
+ rescue StandardError
21
+ nil
22
+ end
23
+ result = { "session_id" => uuid, "status" => "active" }
24
+ result["boot"] = boot if boot
25
+ result
26
+ end
27
+
28
+ def parent_mentry(ctx)
29
+ ctx.manifest.data.entries.find { |e| e.key == SESSION_PARENT_KEY }
30
+ end
31
+
32
+ def propose(key:, ctx:, call:, meta: nil, data: nil)
33
+ body, content = data.is_a?(Hash) ? [nil, data] : [data, nil]
34
+
35
+ target_key = meta&.dig("proposal", "target_key")
36
+ if target_key
37
+ resolution = ctx.manifest.resolver.resolve(target_key)
38
+ Textus::Protocol::EntryConstraint.validate_naming!(target_key, resolution.entry)
39
+ end
40
+
41
+ full_key = "#{PROPOSALS_PARENT_KEY}.#{key}"
42
+ env = ctx.put(key: full_key, meta: meta || {}, body:, content:, call:)
43
+ env.to_h_for_wire
44
+ end
45
+
46
+ def accept(pending_key:, ctx:, call:, dry_run: false)
47
+ env = ctx.read(key: pending_key)
48
+ proposal = env&.meta&.dig("proposal")
49
+ raise Textus::ProposalError.new("entry has no proposal block: #{pending_key}") unless proposal
50
+
51
+ target = proposal["target_key"]
52
+ raise Textus::ProposalError.new("proposal missing target_key") unless target
53
+
54
+ action = proposal["action"] || "put"
55
+
56
+ if dry_run
57
+ target_env = ctx.read(key: target)
58
+ body_diff = Textus::Protocol::Diff.body(target_env&.body, env.body)
59
+ meta_diff = Textus::Protocol::Diff.meta(target_env&.meta&.dig("_meta") || {}, env.meta&.dig("_meta") || {})
60
+ result = { "dry_run" => true, "pending_key" => pending_key, "target_key" => target, "action" => action }
61
+ result["body"] = body_diff if body_diff
62
+ result["meta"] = meta_diff if meta_diff
63
+ result["summary"] = Textus::Protocol::Diff.summary(result)
64
+ return result
65
+ end
66
+
67
+ case action
68
+ when "put"
69
+ target_meta = env.meta.to_h.except("proposal")
70
+
71
+ # Parse body frontmatter to extract schema-relevant meta fields
72
+ resolution = ctx.manifest.resolver.resolve(target)
73
+ if env.body && !env.body.empty?
74
+ parsed = Textus::Protocol::Format.for(resolution.entry.format).parse(env.body)
75
+ body_meta = parsed["_meta"] || {}
76
+ target_meta = body_meta.merge(target_meta)
77
+ end
78
+
79
+ # Re-validate constraints at promotion time
80
+ payload = Textus::Value::Payload.new(meta: target_meta, body: env.body, content: env.content)
81
+ Textus::Protocol::EntryConstraint.validate_naming!(target, resolution.entry)
82
+ Textus::Protocol::EntryConstraint.validate_schema!(payload, resolution.entry, schemas: ctx.schemas)
83
+ Textus::Protocol::EntryConstraint.validate_format!(payload, resolution.entry)
84
+
85
+ ctx.put(key: target, meta: target_meta, body: env.body, content: env.content, call:)
86
+ when "delete"
87
+ ctx.delete(key: target, call:)
88
+ else
89
+ raise Textus::ProposalError.new("unknown action: #{action}")
90
+ end
91
+
92
+ ctx.delete(key: pending_key, call:)
93
+ { "protocol" => Textus::PROTOCOL, "accepted" => pending_key,
94
+ "target_key" => target, "action" => action, "cascade_key" => target }
95
+ end
96
+
97
+ def reject(pending_key:, ctx:, call:, reason: nil)
98
+ env = ctx.read(key: pending_key)
99
+ proposal = env&.meta&.dig("proposal")
100
+ raise Textus::ProposalError.new("entry has no proposal block: #{pending_key}") unless proposal
101
+
102
+ target_key = proposal["target_key"]
103
+ ctx.delete(key: pending_key, call:)
104
+ result = { "protocol" => Textus::PROTOCOL, "rejected" => pending_key, "target_key" => target_key }
105
+ result["reason"] = reason if reason
106
+ result
107
+ end
108
+
109
+ def diff(pending_key:, ctx:, call:)
110
+ env = ctx.read(key: pending_key)
111
+ proposal = env&.meta&.dig("proposal")
112
+ raise Textus::ProposalError.new("entry has no proposal block: #{pending_key}") unless proposal
113
+
114
+ target_key = proposal["target_key"]
115
+ raise Textus::ProposalError.new("proposal missing target_key") unless target_key
116
+
117
+ target_env = ctx.read(key: target_key)
118
+
119
+ body_diff = Textus::Protocol::Diff.body(target_env&.body, env.body)
120
+ meta_diff = Textus::Protocol::Diff.meta(target_env&.meta&.dig("_meta") || {}, env.meta&.dig("_meta") || {})
121
+
122
+ entry = ctx.manifest.data.entries.find { |e| e.key == target_key }
123
+ target_schema = entry&.schema_ref
124
+ proposal_schema = env&.meta&.dig("_meta", "schema")
125
+ schema_diff = nil
126
+ if proposal_schema && target_schema != proposal_schema
127
+ schema_diff = Textus::Protocol::Diff.schema({ "schema" => target_schema }, { "schema" => proposal_schema })
128
+ end
129
+
130
+ result = { "pending_key" => pending_key, "target_key" => target_key }
131
+ result["body"] = body_diff if body_diff
132
+ result["meta"] = meta_diff if meta_diff
133
+ result["schema"] = schema_diff if schema_diff
134
+ result["summary"] = Textus::Protocol::Diff.summary(result)
135
+ result
136
+ end
137
+
138
+ def session_close(session_id:, ctx:, call:, nodes_checked: nil)
139
+ key = "scratchpad.sessions.#{session_id}.session"
140
+ env = ctx.read(key:)
141
+ raise Textus::UsageError.new("session not found: #{session_id}") unless env
142
+
143
+ current = JSON.parse(env.body || "{}")
144
+ content = current.merge("status" => "sealed", "closed_at" => Time.now.utc.iso8601)
145
+ ctx.put(key:, meta: {}, body: JSON.generate(content), content: nil, call:)
146
+
147
+ feedback_key = "scratchpad.sessions.#{session_id}.feedback"
148
+ duration = current["created_at"] ? "#{((Time.now.utc - Time.parse(current["created_at"])) / 60).round}m" : "unknown"
149
+ feedback = {
150
+ "node" => "feedback",
151
+ "confidence" => "medium",
152
+ "signal" => "session_closed",
153
+ "summary" => "Session #{session_id[0..8]} sealed after #{duration}. " \
154
+ "Created at #{current["created_at"]}, closed at #{Time.now.utc.iso8601}.",
155
+ }
156
+ feedback["nodes_checked"] = nodes_checked if nodes_checked
157
+ ctx.put(key: feedback_key, meta: {}, body: JSON.generate(feedback), content: nil, call:)
158
+
159
+ { "session_id" => session_id, "status" => "sealed", "feedback_key" => feedback_key }
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,73 @@
1
+ module Textus
2
+ module Lanes
3
+ module Scratchpad
4
+ module Verbs
5
+ end
6
+ end
7
+ end
8
+ end
9
+
10
+ Textus::Protocol::VerbRegistry.register(
11
+ Textus::Protocol::VerbRegistry::VerbSpec.new(
12
+ verb: :session_open, summary: "Create a new agent session with workspace directories.",
13
+ args: [], surfaces: %i[cli mcp], category: :write,
14
+ lane: :scratchpad
15
+ ),
16
+ )
17
+ Textus::Protocol::VerbRegistry.register(
18
+ Textus::Protocol::VerbRegistry::VerbSpec.new(
19
+ verb: :session_close, summary: "Seal an active session by ID.",
20
+ args: [Textus::Protocol::VerbRegistry::ArgSpec.arg(name: :session_id, required: true,
21
+ description: "UUID of the session to close"),
22
+ Textus::Protocol::VerbRegistry::ArgSpec.arg(name: :nodes_checked, type: Array,
23
+ description: "list of loop nodes checked during the session")],
24
+ surfaces: %i[cli mcp], category: :write,
25
+ lane: :scratchpad
26
+ ),
27
+ )
28
+
29
+ Textus::Protocol::VerbRegistry.register(
30
+ Textus::Protocol::VerbRegistry::VerbSpec.new(
31
+ verb: :propose, summary: "Write a proposal to scratchpad.proposals for human review.",
32
+ args: [Textus::Protocol::VerbRegistry::ArgSpec.arg(name: :key, required: true, positional: true,
33
+ description: "key relative to propose_lane, e.g. 'decisions.feature-x'"),
34
+ Textus::Protocol::VerbRegistry::ArgSpec.arg(name: :meta, type: Hash, wire_name: :_meta,
35
+ description: "frontmatter. Include a 'proposal:' block naming the target_key"),
36
+ Textus::Protocol::VerbRegistry::ArgSpec.arg(name: :data,
37
+ description: "unified payload — String (body) or Hash (content)")],
38
+ surfaces: %i[cli mcp], category: :write,
39
+ lane: :scratchpad
40
+ ),
41
+ )
42
+ Textus::Protocol::VerbRegistry.register(
43
+ Textus::Protocol::VerbRegistry::VerbSpec.new(
44
+ verb: :accept, summary: "Apply a scratchpad.proposals entry to its target zone; requires author.",
45
+ args: [Textus::Protocol::VerbRegistry::ArgSpec.arg(name: :pending_key, required: true, positional: true,
46
+ description: "the queued proposal's key"),
47
+ Textus::Protocol::VerbRegistry::ArgSpec.arg(name: :dry_run, type: :boolean, default: false,
48
+ description: "when true, compute diff without promoting")],
49
+ surfaces: %i[cli mcp], category: :write,
50
+ lane: :scratchpad
51
+ ),
52
+ )
53
+ Textus::Protocol::VerbRegistry.register(
54
+ Textus::Protocol::VerbRegistry::VerbSpec.new(
55
+ verb: :reject, summary: "Discard a scratchpad.proposals entry without applying it.",
56
+ args: [Textus::Protocol::VerbRegistry::ArgSpec.arg(name: :pending_key, required: true, positional: true,
57
+ description: "the queued proposal's key"),
58
+ Textus::Protocol::VerbRegistry::ArgSpec.arg(name: :reason,
59
+ description: "optional explanation for the rejection")],
60
+ surfaces: %i[cli mcp], category: :write,
61
+ lane: :scratchpad
62
+ ),
63
+ )
64
+ Textus::Protocol::VerbRegistry.register(
65
+ Textus::Protocol::VerbRegistry::VerbSpec.new(
66
+ verb: :diff, summary: "Preview the diff between a scratchpad.proposals entry and its target.",
67
+ args: [Textus::Protocol::VerbRegistry::ArgSpec.arg(name: :pending_key, required: true, positional: true,
68
+ description: "the queued proposal's key")],
69
+ surfaces: %i[cli mcp],
70
+ category: :read,
71
+ lane: :scratchpad
72
+ ),
73
+ )
@@ -1,8 +1,5 @@
1
1
  module Textus
2
- class Gate
3
- # Raised when a required arg is absent from the bound input. Surface
4
- # adapters translate this to their native error (MCP ToolError, CLI
5
- # UsageError); a direct Ruby call lets it surface as-is.
2
+ module Protocol
6
3
  class MissingArgs < Textus::Error
7
4
  attr_reader :spec, :missing
8
5
 
@@ -13,21 +10,16 @@ module Textus
13
10
  end
14
11
  end
15
12
 
16
- # Validates and resolves a by-name inputs hash against a contract spec.
17
- # Returns a flat hash with defaults and session_defaults filled in.
18
- # Every caller receives the same shape — no positional/kwarg split.
19
13
  module Binder
20
14
  module_function
21
15
 
22
- def bind(spec, inputs, session: nil)
16
+ def bind(spec, inputs)
23
17
  missing = spec.required_args.reject { |a| inputs.key?(a.name) }
24
18
  raise MissingArgs.new(spec, missing) unless missing.empty?
25
19
 
26
20
  spec.args.each_with_object({}) do |a, h|
27
21
  if inputs.key?(a.name)
28
22
  h[a.name] = inputs[a.name]
29
- elsif a.session_default && session
30
- h[a.name] = session.public_send(a.session_default)
31
23
  elsif !a.default.nil?
32
24
  h[a.name] = a.default
33
25
  end
@@ -0,0 +1,34 @@
1
+ module Textus
2
+ module Protocol
3
+ module Boot
4
+ module Catalog
5
+ CURATED_CLI_VERBS = [
6
+ { "name" => "boot" },
7
+ { "name" => "list" },
8
+ { "name" => "loop" },
9
+ { "name" => "get" },
10
+ { "name" => "ingest" },
11
+ { "name" => "where" },
12
+ { "name" => "schema", "summary" => "schema operations: 'schema show KEY', 'schema diff', 'schema init', 'schema migrate'" },
13
+ { "name" => "put" },
14
+ { "name" => "propose" },
15
+ { "name" => "accept" },
16
+ { "name" => "key", "summary" => "key operations: 'key delete', 'key mv', 'key uid'" },
17
+ { "name" => "drain" },
18
+ { "name" => "session", "summary" => "session lifecycle: session_open, session_close" },
19
+ { "name" => "audit" },
20
+ { "name" => "blame" },
21
+ { "name" => "pulse" },
22
+ ].freeze
23
+
24
+ def self.build
25
+ summaries = Textus::Protocol::VerbRegistry.registered.to_h { |s| [s.verb.to_s, s.summary] }
26
+ CURATED_CLI_VERBS.map do |entry|
27
+ derived = summaries[entry["name"]]
28
+ derived ? entry.merge("summary" => derived) : entry
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end