textus 0.55.1 → 0.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (329) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +175 -2768
  3. data/README.md +17 -18
  4. data/lib/textus/artifact.rb +8 -0
  5. data/lib/textus/errors.rb +2 -39
  6. data/lib/textus/human.rb +7 -0
  7. data/lib/textus/infra/adapters/concurrency_adapter.rb +15 -0
  8. data/lib/textus/infra/clock.rb +7 -0
  9. data/lib/textus/infra/database.rb +127 -0
  10. data/lib/textus/infra/file_system.rb +54 -0
  11. data/lib/textus/infra/locks.rb +50 -0
  12. data/lib/textus/infra/port/clock.rb +18 -0
  13. data/lib/textus/infra/port/publisher.rb +71 -0
  14. data/lib/textus/infra/port/sentinel.rb +25 -0
  15. data/lib/textus/infra/port/sentinel_store.rb +93 -0
  16. data/lib/textus/infra/store/audit_log.rb +42 -0
  17. data/lib/textus/infra/store/base.rb +25 -0
  18. data/lib/textus/infra/store/cursor_store.rb +27 -0
  19. data/lib/textus/infra/store/entry_index.rb +31 -0
  20. data/lib/textus/infra/store/envelope/meta.rb +63 -0
  21. data/lib/textus/infra/store/file_store.rb +23 -0
  22. data/lib/textus/infra/store/index/builder.rb +28 -0
  23. data/lib/textus/infra/store/index/lookup.rb +23 -0
  24. data/lib/textus/infra/store/link_store.rb +43 -0
  25. data/lib/textus/infra/store/workflow_queue.rb +54 -0
  26. data/lib/textus/infra/store.rb +29 -0
  27. data/lib/textus/ingest.rb +8 -0
  28. data/lib/textus/lanes/artifact/cache.rb +35 -0
  29. data/lib/textus/lanes/artifact/handlers.rb +29 -0
  30. data/lib/textus/lanes/artifact/lifecycle.rb +23 -0
  31. data/lib/textus/lanes/artifact/verbs.rb +18 -0
  32. data/lib/textus/lanes/ingest/dedup.rb +17 -0
  33. data/lib/textus/lanes/ingest/entry_types/asset.rb +45 -0
  34. data/lib/textus/lanes/ingest/entry_types/link.rb +16 -0
  35. data/lib/textus/lanes/ingest/entry_types/text.rb +17 -0
  36. data/lib/textus/lanes/ingest/entry_types.rb +25 -0
  37. data/lib/textus/lanes/ingest/handlers.rb +54 -0
  38. data/lib/textus/lanes/ingest/index_rebuilder.rb +13 -0
  39. data/lib/textus/lanes/ingest/key_builder.rb +33 -0
  40. data/lib/textus/lanes/ingest/resolver.rb +21 -0
  41. data/lib/textus/lanes/knowledge/freeform.rb +15 -0
  42. data/lib/textus/lanes/knowledge/handlers.rb +53 -0
  43. data/lib/textus/lanes/knowledge/runner.rb +45 -0
  44. data/lib/textus/lanes/knowledge/verbs.rb +16 -0
  45. data/lib/textus/lanes/scratchpad/handlers.rb +164 -0
  46. data/lib/textus/lanes/scratchpad/verbs.rb +73 -0
  47. data/lib/textus/{gate → protocol}/binder.rb +2 -10
  48. data/lib/textus/protocol/boot/catalog.rb +34 -0
  49. data/lib/textus/protocol/boot/context.rb +124 -0
  50. data/lib/textus/protocol/boot/protocol.rb +33 -0
  51. data/lib/textus/protocol/boot.rb +42 -0
  52. data/lib/textus/protocol/builder/domain.rb +17 -0
  53. data/lib/textus/protocol/builder/infrastructure.rb +17 -0
  54. data/lib/textus/protocol/builder/runtime.rb +46 -0
  55. data/lib/textus/protocol/builder.rb +11 -0
  56. data/lib/textus/protocol/container.rb +22 -0
  57. data/lib/textus/protocol/diff.rb +170 -0
  58. data/lib/textus/protocol/entry_constraint.rb +54 -0
  59. data/lib/textus/protocol/envelope.rb +70 -0
  60. data/lib/textus/protocol/format/base.rb +80 -0
  61. data/lib/textus/protocol/format/bash.rb +10 -0
  62. data/lib/textus/protocol/format/javascript.rb +10 -0
  63. data/lib/textus/protocol/format/json.rb +82 -0
  64. data/lib/textus/protocol/format/markdown.rb +53 -0
  65. data/lib/textus/protocol/format/python.rb +10 -0
  66. data/lib/textus/protocol/format/ruby.rb +10 -0
  67. data/lib/textus/protocol/format/script.rb +55 -0
  68. data/lib/textus/protocol/format/text.rb +48 -0
  69. data/lib/textus/protocol/format/yaml.rb +105 -0
  70. data/lib/textus/protocol/format.rb +73 -0
  71. data/lib/textus/protocol/freshness/ttl_evaluator.rb +61 -0
  72. data/lib/textus/protocol/gate.rb +61 -0
  73. data/lib/textus/protocol/handlers/command_context.rb +19 -0
  74. data/lib/textus/protocol/handlers/concern.rb +15 -0
  75. data/lib/textus/protocol/handlers/ops/audit_filter.rb +37 -0
  76. data/lib/textus/protocol/handlers/ops/bulk_planner.rb +38 -0
  77. data/lib/textus/protocol/handlers/ops/data_mv.rb +49 -0
  78. data/lib/textus/protocol/handlers/ops/git_blame.rb +65 -0
  79. data/lib/textus/protocol/handlers/ops/pulse_coordinator.rb +37 -0
  80. data/lib/textus/protocol/handlers/ops/source_expander.rb +41 -0
  81. data/lib/textus/protocol/handlers/query_context.rb +41 -0
  82. data/lib/textus/protocol/handlers/read.rb +67 -0
  83. data/lib/textus/protocol/handlers/system.rb +93 -0
  84. data/lib/textus/protocol/handlers/write.rb +57 -0
  85. data/lib/textus/protocol/init.rb +143 -0
  86. data/lib/textus/protocol/key/grammar.rb +35 -0
  87. data/lib/textus/protocol/key/matching.rb +23 -0
  88. data/lib/textus/protocol/key/path.rb +23 -0
  89. data/lib/textus/protocol/key.rb +25 -0
  90. data/lib/textus/protocol/lane.rb +12 -0
  91. data/lib/textus/{store/geometry.rb → protocol/layout.rb} +32 -22
  92. data/lib/textus/protocol/links/resolver.rb +33 -0
  93. data/lib/textus/protocol/links/uri_rewriter.rb +30 -0
  94. data/lib/textus/protocol/links.rb +10 -0
  95. data/lib/textus/protocol/manifest/data.rb +90 -0
  96. data/lib/textus/protocol/manifest/entry/base.rb +95 -0
  97. data/lib/textus/protocol/manifest/entry/leaf.rb +19 -0
  98. data/lib/textus/protocol/manifest/entry/nested.rb +19 -0
  99. data/lib/textus/protocol/manifest/entry/parser.rb +71 -0
  100. data/lib/textus/protocol/manifest/entry/publish.rb +209 -0
  101. data/lib/textus/protocol/manifest/entry.rb +10 -0
  102. data/lib/textus/protocol/manifest/policy/publish_target.rb +36 -0
  103. data/lib/textus/protocol/manifest/policy.rb +69 -0
  104. data/lib/textus/protocol/manifest/resolver.rb +156 -0
  105. data/lib/textus/protocol/manifest/schema/contract.rb +53 -0
  106. data/lib/textus/protocol/manifest/schema/validator.rb +52 -0
  107. data/lib/textus/protocol/manifest/schema.rb +40 -0
  108. data/lib/textus/protocol/manifest.rb +62 -0
  109. data/lib/textus/protocol/produce/render/context.rb +43 -0
  110. data/lib/textus/protocol/produce/render.rb +44 -0
  111. data/lib/textus/protocol/schema/registry.rb +44 -0
  112. data/lib/textus/protocol/schema/tools.rb +104 -0
  113. data/lib/textus/protocol/schema.rb +117 -0
  114. data/lib/textus/protocol/session/bridge.rb +11 -0
  115. data/lib/textus/protocol/session/context.rb +5 -0
  116. data/lib/textus/protocol/session.rb +87 -0
  117. data/lib/textus/protocol/store_engine/delete.rb +37 -0
  118. data/lib/textus/protocol/store_engine/move.rb +109 -0
  119. data/lib/textus/protocol/store_engine/pipeline.rb +61 -0
  120. data/lib/textus/protocol/store_engine/put.rb +104 -0
  121. data/lib/textus/protocol/store_engine.rb +86 -0
  122. data/lib/textus/protocol/v4.rb +25 -0
  123. data/lib/textus/protocol/verb_registry/arg_spec.rb +19 -0
  124. data/lib/textus/protocol/verb_registry/core_verbs.rb +164 -0
  125. data/lib/textus/protocol/verb_registry/verb_spec.rb +42 -0
  126. data/lib/textus/protocol/verb_registry.rb +42 -0
  127. data/lib/textus/surface/cli/group/{rule.rb → web.rb} +2 -2
  128. data/lib/textus/surface/cli/runner.rb +25 -25
  129. data/lib/textus/surface/cli/sources.rb +1 -3
  130. data/lib/textus/surface/cli/verb/get.rb +3 -6
  131. data/lib/textus/surface/cli/verb/init.rb +1 -1
  132. data/lib/textus/surface/cli/verb/put.rb +8 -6
  133. data/lib/textus/surface/cli/verb/web_serve.rb +22 -0
  134. data/lib/textus/surface/cli.rb +25 -6
  135. data/lib/textus/surface/dispatch.rb +17 -0
  136. data/lib/textus/surface/mcp/adapters/mcp_adapter.rb +28 -0
  137. data/lib/textus/surface/mcp/catalog.rb +26 -62
  138. data/lib/textus/surface/mcp/cursor_manager.rb +22 -0
  139. data/lib/textus/surface/mcp/drift_annotation.rb +19 -0
  140. data/lib/textus/surface/mcp/errors.rb +0 -10
  141. data/lib/textus/surface/mcp/server.rb +46 -46
  142. data/lib/textus/surface/watcher.rb +18 -12
  143. data/lib/textus/surface/web.rb +50 -0
  144. data/lib/textus/value/call.rb +1 -1
  145. data/lib/textus/{core → value}/duration.rb +1 -4
  146. data/lib/textus/value/etag.rb +7 -7
  147. data/lib/textus/value/payload.rb +7 -0
  148. data/lib/textus/version.rb +1 -1
  149. data/lib/textus/workflow/consumer.rb +39 -0
  150. data/lib/textus/workflow/dsl/lifecycle.rb +32 -0
  151. data/lib/textus/workflow/dsl/pattern_matcher.rb +20 -0
  152. data/lib/textus/workflow/dsl/step_builder.rb +56 -0
  153. data/lib/textus/workflow/dsl.rb +111 -21
  154. data/lib/textus/workflow/event_emitter.rb +21 -0
  155. data/lib/textus/workflow/publisher.rb +59 -0
  156. data/lib/textus/workflow/registry.rb +31 -8
  157. data/lib/textus/workflow/runner.rb +25 -61
  158. data/lib/textus/workflow/scheduler.rb +45 -0
  159. data/lib/textus/workflow/step_executor.rb +78 -0
  160. data/lib/textus/workflow/step_helpers.rb +102 -0
  161. data/lib/textus/workflow.rb +2 -5
  162. data/lib/textus.rb +16 -80
  163. metadata +167 -183
  164. data/SPEC.md +0 -908
  165. data/docs/architecture/README.md +0 -273
  166. data/docs/reference/conventions.md +0 -138
  167. data/lib/textus/action/accept.rb +0 -46
  168. data/lib/textus/action/audit.rb +0 -94
  169. data/lib/textus/action/base.rb +0 -42
  170. data/lib/textus/action/blame.rb +0 -79
  171. data/lib/textus/action/boot.rb +0 -15
  172. data/lib/textus/action/data_mv.rb +0 -58
  173. data/lib/textus/action/deps.rb +0 -19
  174. data/lib/textus/action/doctor.rb +0 -17
  175. data/lib/textus/action/drain.rb +0 -31
  176. data/lib/textus/action/enqueue.rb +0 -37
  177. data/lib/textus/action/get.rb +0 -34
  178. data/lib/textus/action/ingest.rb +0 -199
  179. data/lib/textus/action/jobs.rb +0 -27
  180. data/lib/textus/action/key_delete.rb +0 -26
  181. data/lib/textus/action/key_delete_prefix.rb +0 -35
  182. data/lib/textus/action/key_mv.rb +0 -122
  183. data/lib/textus/action/key_mv_prefix.rb +0 -48
  184. data/lib/textus/action/list.rb +0 -28
  185. data/lib/textus/action/propose.rb +0 -42
  186. data/lib/textus/action/published.rb +0 -22
  187. data/lib/textus/action/pulse.rb +0 -49
  188. data/lib/textus/action/put.rb +0 -38
  189. data/lib/textus/action/rdeps.rb +0 -24
  190. data/lib/textus/action/reject.rb +0 -28
  191. data/lib/textus/action/rule_explain.rb +0 -81
  192. data/lib/textus/action/rule_lint.rb +0 -62
  193. data/lib/textus/action/rule_list.rb +0 -38
  194. data/lib/textus/action/schema_envelope.rb +0 -22
  195. data/lib/textus/action/uid.rb +0 -19
  196. data/lib/textus/action/where.rb +0 -21
  197. data/lib/textus/boot.rb +0 -202
  198. data/lib/textus/contract/arg.rb +0 -10
  199. data/lib/textus/contract/dsl.rb +0 -88
  200. data/lib/textus/contract/spec.rb +0 -25
  201. data/lib/textus/contract.rb +0 -12
  202. data/lib/textus/core/freshness/evaluator.rb +0 -150
  203. data/lib/textus/core/freshness/verdict.rb +0 -34
  204. data/lib/textus/core/freshness.rb +0 -11
  205. data/lib/textus/core/retention/sweep.rb +0 -57
  206. data/lib/textus/core/retention.rb +0 -11
  207. data/lib/textus/core/sentinel.rb +0 -30
  208. data/lib/textus/doctor/check/audit_log.rb +0 -34
  209. data/lib/textus/doctor/check/generator_drift.rb +0 -29
  210. data/lib/textus/doctor/check/illegal_keys.rb +0 -60
  211. data/lib/textus/doctor/check/manifest_files.rb +0 -25
  212. data/lib/textus/doctor/check/notebook_sources.rb +0 -50
  213. data/lib/textus/doctor/check/orphaned_publish_targets.rb +0 -35
  214. data/lib/textus/doctor/check/proposal_targets.rb +0 -45
  215. data/lib/textus/doctor/check/protocol_version.rb +0 -47
  216. data/lib/textus/doctor/check/publish_tree_index_overlap.rb +0 -48
  217. data/lib/textus/doctor/check/raw_asset_paths.rb +0 -50
  218. data/lib/textus/doctor/check/rule_ambiguity.rb +0 -49
  219. data/lib/textus/doctor/check/schema_parse_error.rb +0 -28
  220. data/lib/textus/doctor/check/schema_violations.rb +0 -28
  221. data/lib/textus/doctor/check/schemas.rb +0 -26
  222. data/lib/textus/doctor/check/sentinels.rb +0 -60
  223. data/lib/textus/doctor/check/stale_reviewed_stamp.rb +0 -54
  224. data/lib/textus/doctor/check/templates.rb +0 -29
  225. data/lib/textus/doctor/check/unowned_schema_fields.rb +0 -40
  226. data/lib/textus/doctor/check.rb +0 -41
  227. data/lib/textus/doctor/validator.rb +0 -93
  228. data/lib/textus/doctor.rb +0 -54
  229. data/lib/textus/format/base.rb +0 -49
  230. data/lib/textus/format/json.rb +0 -102
  231. data/lib/textus/format/markdown.rb +0 -80
  232. data/lib/textus/format/shared.rb +0 -17
  233. data/lib/textus/format/text.rb +0 -55
  234. data/lib/textus/format/yaml.rb +0 -125
  235. data/lib/textus/format.rb +0 -46
  236. data/lib/textus/gate/auth.rb +0 -212
  237. data/lib/textus/gate.rb +0 -92
  238. data/lib/textus/init.rb +0 -157
  239. data/lib/textus/jobs.rb +0 -9
  240. data/lib/textus/key/distance.rb +0 -55
  241. data/lib/textus/key/grammar.rb +0 -33
  242. data/lib/textus/key/matching.rb +0 -24
  243. data/lib/textus/key/path.rb +0 -28
  244. data/lib/textus/manifest/capabilities.rb +0 -29
  245. data/lib/textus/manifest/data.rb +0 -102
  246. data/lib/textus/manifest/entry/base.rb +0 -105
  247. data/lib/textus/manifest/entry/ignore_matcher.rb +0 -46
  248. data/lib/textus/manifest/entry/leaf.rb +0 -17
  249. data/lib/textus/manifest/entry/nested.rb +0 -37
  250. data/lib/textus/manifest/entry/parser.rb +0 -99
  251. data/lib/textus/manifest/entry/produced.rb +0 -36
  252. data/lib/textus/manifest/entry/publish/mode.rb +0 -45
  253. data/lib/textus/manifest/entry/publish/none.rb +0 -14
  254. data/lib/textus/manifest/entry/publish/subtree_mirror.rb +0 -73
  255. data/lib/textus/manifest/entry/publish/template.rb +0 -16
  256. data/lib/textus/manifest/entry/publish/to_paths.rb +0 -76
  257. data/lib/textus/manifest/entry/publish/tree.rb +0 -50
  258. data/lib/textus/manifest/entry/publish.rb +0 -45
  259. data/lib/textus/manifest/entry/validators/format_matrix.rb +0 -21
  260. data/lib/textus/manifest/entry/validators/ignore.rb +0 -28
  261. data/lib/textus/manifest/entry/validators/publish.rb +0 -30
  262. data/lib/textus/manifest/entry/validators.rb +0 -18
  263. data/lib/textus/manifest/entry.rb +0 -8
  264. data/lib/textus/manifest/policy/matcher.rb +0 -51
  265. data/lib/textus/manifest/policy/publish_target.rb +0 -34
  266. data/lib/textus/manifest/policy/react.rb +0 -30
  267. data/lib/textus/manifest/policy/retention.rb +0 -29
  268. data/lib/textus/manifest/policy/source.rb +0 -30
  269. data/lib/textus/manifest/policy.rb +0 -95
  270. data/lib/textus/manifest/resolver.rb +0 -118
  271. data/lib/textus/manifest/rules.rb +0 -86
  272. data/lib/textus/manifest/schema/contract.rb +0 -61
  273. data/lib/textus/manifest/schema/keys.rb +0 -90
  274. data/lib/textus/manifest/schema/semantics/cross_field.rb +0 -53
  275. data/lib/textus/manifest/schema/semantics/invariants.rb +0 -125
  276. data/lib/textus/manifest/schema/semantics/migration.rb +0 -83
  277. data/lib/textus/manifest/schema/semantics.rb +0 -27
  278. data/lib/textus/manifest/schema/validator.rb +0 -48
  279. data/lib/textus/manifest/schema/vocabulary.rb +0 -25
  280. data/lib/textus/manifest/schema.rb +0 -30
  281. data/lib/textus/manifest.rb +0 -65
  282. data/lib/textus/meta.rb +0 -54
  283. data/lib/textus/port/audit_log.rb +0 -268
  284. data/lib/textus/port/build_lock.rb +0 -64
  285. data/lib/textus/port/clock.rb +0 -10
  286. data/lib/textus/port/publisher.rb +0 -60
  287. data/lib/textus/port/sentinel_store.rb +0 -83
  288. data/lib/textus/port/storage/file_stat.rb +0 -19
  289. data/lib/textus/port/storage/file_store.rb +0 -26
  290. data/lib/textus/port/store.rb +0 -93
  291. data/lib/textus/port/watcher_lock.rb +0 -48
  292. data/lib/textus/produce/engine.rb +0 -50
  293. data/lib/textus/produce/render.rb +0 -20
  294. data/lib/textus/schema/tools.rb +0 -115
  295. data/lib/textus/schema.rb +0 -100
  296. data/lib/textus/schemas.rb +0 -54
  297. data/lib/textus/store/compositor.rb +0 -34
  298. data/lib/textus/store/container.rb +0 -43
  299. data/lib/textus/store/cursor.rb +0 -26
  300. data/lib/textus/store/envelope/reader.rb +0 -43
  301. data/lib/textus/store/envelope/writer.rb +0 -195
  302. data/lib/textus/store/index/builder.rb +0 -74
  303. data/lib/textus/store/index/lookup.rb +0 -60
  304. data/lib/textus/store/jobs/base.rb +0 -13
  305. data/lib/textus/store/jobs/index.rb +0 -15
  306. data/lib/textus/store/jobs/materialize.rb +0 -15
  307. data/lib/textus/store/jobs/plan.rb +0 -11
  308. data/lib/textus/store/jobs/planner.rb +0 -104
  309. data/lib/textus/store/jobs/queue.rb +0 -154
  310. data/lib/textus/store/jobs/registry.rb +0 -19
  311. data/lib/textus/store/jobs/retention.rb +0 -50
  312. data/lib/textus/store/jobs/sweep.rb +0 -21
  313. data/lib/textus/store/jobs/worker.rb +0 -64
  314. data/lib/textus/store/session.rb +0 -37
  315. data/lib/textus/store.rb +0 -99
  316. data/lib/textus/surface/cli/verb/doctor.rb +0 -19
  317. data/lib/textus/surface/cli/verb/schema_diff.rb +0 -17
  318. data/lib/textus/surface/cli/verb/schema_init.rb +0 -21
  319. data/lib/textus/surface/cli/verb/schema_migrate.rb +0 -21
  320. data/lib/textus/surface/mcp.rb +0 -8
  321. data/lib/textus/surface/projector.rb +0 -27
  322. data/lib/textus/surface/role_scope.rb +0 -34
  323. data/lib/textus/value/command.rb +0 -16
  324. data/lib/textus/value/envelope.rb +0 -89
  325. data/lib/textus/value/result.rb +0 -26
  326. data/lib/textus/workflow/collector.rb +0 -27
  327. data/lib/textus/workflow/context.rb +0 -5
  328. data/lib/textus/workflow/loader.rb +0 -17
  329. data/lib/textus/workflow/pattern.rb +0 -18
data/lib/textus/boot.rb DELETED
@@ -1,202 +0,0 @@
1
- module Textus
2
- # Read-only "what's in this store and how do I use it" envelope.
3
- # Boot is side-effect-free. Reads from pre-computed artifacts and
4
- # the store catalog rather than computing inline.
5
- module Boot
6
- PROTOCOL_ID = PROTOCOL
7
-
8
- # Human-readable name(s) for the live lane(s) of a given kind, or `fallback`
9
- # when the manifest declares none. Lets write-flow guidance name the live
10
- # lane by kind instead of a hardcoded instance name (ADR 0034).
11
- def self.lane_label(manifest, kind, fallback)
12
- lanes = manifest.policy.lanes_of_kind(kind)
13
- lanes.empty? ? fallback : lanes.join(", ")
14
- end
15
-
16
- # Static, store-independent parts of the agent-facing protocol. The
17
- # `recipes` and `role_resolution` blocks are derived per-manifest in
18
- # agent_protocol(...) because lane and role names are user-configurable.
19
- AGENT_PROTOCOL_TEMPLATE = {
20
- "envelope_shape" => {
21
- "summary" => "every read/write payload is a JSON envelope with _meta, body, uid, and etag",
22
- "fields" => {
23
- "_meta" => "hash of structured frontmatter; schema-validated per entry family",
24
- "body" => "string payload (markdown/text) or nil for json/yaml formats where body lives in _meta",
25
- "uid" => "stable 16-char hex identifier; preserved across writes and key renames",
26
- "etag" => "content hash; pass back on writes to detect concurrent edits",
27
- },
28
- "ref" => "SPEC.md §8",
29
- },
30
- }.freeze
31
-
32
- # Curated agent-facing verb catalog. This declares which verbs the operator
33
- # CLI surfaces and in what order — the editorial presentation. The summary of
34
- # each verb is a fact, not presentation: it is derived from `contract.summary`
35
- # at load time (ADR 0039). A literal "summary" survives here only for grouped
36
- # CLI tokens (schema/key/rule/hook) that aggregate several sub-contracts and so
37
- # have no single contract to derive from. CLI_VERBS itself is assigned in
38
- # textus.rb after Zeitwerk eager_load so all contract files are present.
39
- CURATED_CLI_VERBS = [
40
- { "name" => "boot" },
41
- { "name" => "list" },
42
- { "name" => "get" },
43
- { "name" => "ingest" },
44
- { "name" => "where" },
45
- { "name" => "schema", "summary" => "schema operations: 'schema show KEY', 'schema diff', 'schema init', 'schema migrate'" },
46
- { "name" => "put" },
47
- { "name" => "propose" },
48
- { "name" => "accept" },
49
- { "name" => "enqueue" },
50
- { "name" => "key", "summary" => "key operations: 'key delete', 'key mv', 'key uid'" },
51
- { "name" => "drain" },
52
- { "name" => "audit" },
53
- { "name" => "blame" },
54
- { "name" => "rule", "summary" => "inspect effective rules: 'rule list', 'rule explain KEY'" },
55
- { "name" => "doctor" },
56
- { "name" => "jobs" },
57
- { "name" => "pulse" },
58
- ].freeze
59
-
60
- # verb token => contract.summary, for every Dispatcher verb that carries a
61
- # contract. The single source for a verb's one-line summary (ADR 0039).
62
- def self.contract_summaries
63
- Textus::Action::VERBS.values
64
- .select { |k| k.respond_to?(:contract?) && k.contract? }
65
- .to_h { |k| [k.contract.verb.to_s, k.contract.summary] }
66
- end
67
-
68
- # Build the CLI verb catalog: each summary is derived from its contract when
69
- # one exists, falling back to the curated editorial string for grouped tokens
70
- # (schema/key/rule/hook). Called once from textus.rb after eager_load.
71
- def self.build_cli_verbs
72
- summaries = contract_summaries
73
- CURATED_CLI_VERBS.map do |entry|
74
- derived = summaries[entry["name"]]
75
- derived ? entry.merge("summary" => derived) : entry
76
- end
77
- end
78
-
79
- def self.agent_quickstart(manifest, audit_log)
80
- agent_role = manifest.policy.proposer_role
81
-
82
- writable_lanes = manifest.data.declared_lane_kinds.keys.each_with_object([]) do |lane_name, acc|
83
- next unless agent_role
84
-
85
- verb = manifest.policy.verb_for_lane(lane_name)
86
- writers = manifest.policy.roles_with_capability(verb)
87
- acc << lane_name if writers.include?(agent_role)
88
- end
89
-
90
- propose_lane = manifest.policy.propose_lane_for(agent_role)
91
-
92
- {
93
- "read_verbs" => Textus::Surface::MCP::Catalog.read_verbs,
94
- "write_verbs" => agent_role ? Textus::Surface::MCP::Catalog.write_verbs : [],
95
- "writable_lanes" => writable_lanes,
96
- "propose_lane" => propose_lane,
97
- "latest_seq" => audit_log.latest_seq,
98
- }
99
- end
100
-
101
- def self.recipes(manifest)
102
- queue = manifest.policy.queue_lane
103
- feeds = lane_label(manifest, :machine, "the machine lane")
104
- {
105
- "read" => {
106
- "purpose" => "find and read an entry",
107
- "steps" => [
108
- "list (lane:, prefix:) — discover keys without reading bodies",
109
- "get KEY — returns the entry envelope",
110
- ],
111
- },
112
- "write" => {
113
- "purpose" => "create or update an entry",
114
- "steps" => [
115
- "schema KEY — learn the _meta field shape (required, optional, field types) before writing",
116
- "assemble an envelope: { _meta: {…}, body: \"…\" }",
117
- "put KEY — persist it (role-gated); pass if_etag to guard a concurrent edit",
118
- ],
119
- },
120
- "propose" => {
121
- "purpose" => "agent suggests a change for human review",
122
- "agent_steps" => [
123
- "propose KEY — writes the change into the #{queue} lane for review",
124
- ],
125
- "human_steps" => [
126
- "accept #{queue}.KEY — promotes the proposal into its target lane",
127
- ],
128
- },
129
- "drain" => {
130
- "purpose" => "keep the machine-maintained lanes fresh — re-pull stale intake entries from their declared source",
131
- "steps" => [
132
- "pulse — its `stale` list names entries past their ttl",
133
- "drain (lane: #{feeds}) — re-pull the stale entries",
134
- ],
135
- },
136
- }
137
- end
138
-
139
- def self.agent_protocol(manifest)
140
- AGENT_PROTOCOL_TEMPLATE.merge(
141
- "recipes" => recipes(manifest),
142
- "role_resolution" => {
143
- "summary" => "write role is resolved in order: --as flag, TEXTUS_ROLE env var, .textus/role file, " \
144
- "then a transport default ('human' for CLI, 'agent' for MCP)",
145
- "roles" => manifest.data.role_caps.keys,
146
- "ref" => "SPEC.md §5",
147
- },
148
- )
149
- end
150
-
151
- def self.build(container:)
152
- manifest = container.manifest
153
- etag = Textus::Value::Etag.for_contract(container.root)
154
-
155
- {
156
- "protocol" => PROTOCOL_ID,
157
- "store_root" => container.root,
158
- "contract_etag" => etag,
159
- "lanes" => lanes_for(manifest),
160
- "agent_quickstart" => agent_quickstart(manifest, container.audit_log),
161
- "orientation" => read_artifact_content(container, "artifacts.config.orientation"),
162
- "context" => read_boot_context(container),
163
- "agent_protocol" => agent_protocol(manifest),
164
- }.compact
165
- end
166
-
167
- def self.read_artifact_content(container, key)
168
- res = container.manifest.resolver.resolve(key)
169
- return nil unless res.path && File.exist?(res.path)
170
-
171
- call = Textus::Value::Call.build(role: Textus::Value::Role::DEFAULT)
172
- env = Textus::Action::Get.call(container: container, call: call, key: key)
173
- env&.content
174
- rescue Textus::Error
175
- nil
176
- end
177
-
178
- def self.read_boot_context(container)
179
- res = container.manifest.resolver.resolve("knowledge.boot")
180
- return nil unless res.path && File.exist?(res.path)
181
-
182
- call = Textus::Value::Call.build(role: Textus::Value::Role::DEFAULT)
183
- env = Textus::Action::Get.call(container: container, call: call, key: "knowledge.boot")
184
- body = env&.body&.strip
185
- body.nil? || body.empty? ? nil : body
186
- rescue Textus::Error
187
- nil
188
- end
189
-
190
- def self.lanes_for(manifest)
191
- manifest.data.declared_lane_kinds.keys.map do |name|
192
- verb = manifest.policy.verb_for_lane(name)
193
- row = { "name" => name, "writers" => manifest.policy.roles_with_capability(verb) }
194
- kind = manifest.policy.declared_kind(name)
195
- row["kind"] = kind.to_s if kind
196
- purpose = manifest.data.lane_descs[name]
197
- row["purpose"] = purpose if purpose && !purpose.empty?
198
- row
199
- end
200
- end
201
- end
202
- end
@@ -1,10 +0,0 @@
1
- module Textus
2
- module Contract
3
- Arg = Data.define(
4
- :name, :type, :required, :positional, :session_default,
5
- :description, :wire_name, :default, :source, :coerce, :cli_default
6
- ) do
7
- def wire = wire_name || name
8
- end
9
- end
10
- end
@@ -1,88 +0,0 @@
1
- module Textus
2
- module Contract
3
- module DSL
4
- def verb(name = nil)
5
- if name
6
- raise "contract already built; declare verb before reading .contract" if defined?(@contract) && @contract
7
-
8
- @__verb = name
9
- else
10
- @__verb
11
- end
12
- end
13
-
14
- def summary(text = nil)
15
- if text
16
- raise "contract already built; declare summary before reading .contract" if defined?(@contract) && @contract
17
-
18
- @__summary = text
19
- else
20
- @__summary
21
- end
22
- end
23
-
24
- def surfaces(*list)
25
- if list.empty?
26
- @__surfaces ||= []
27
- else
28
- raise "contract already built; declare surfaces before reading .contract" if defined?(@contract) && @contract
29
-
30
- @__surfaces = list
31
- end
32
- end
33
-
34
- def cli(path = nil)
35
- if path
36
- raise "contract already built; declare cli before reading .contract" if defined?(@contract) && @contract
37
-
38
- @__cli = path.to_s
39
- else
40
- @__cli
41
- end
42
- end
43
-
44
- def arg(name, type, required: false, positional: false, session_default: nil, description: nil, wire_name: nil, default: nil, source: nil, coerce: nil, cli_default: :__unset) # rubocop:disable Metrics/ParameterLists,Layout/LineLength
45
- raise "contract already built; declare args before reading .contract" if defined?(@contract) && @contract
46
-
47
- (@__args ||= []) << Arg.new(
48
- name: name, type: type, required: required,
49
- positional: positional, session_default: session_default,
50
- description: description, wire_name: wire_name, default: default,
51
- source: source, coerce: coerce, cli_default: cli_default
52
- )
53
- end
54
-
55
- def cli_stdin(mode = :__read)
56
- return @__cli_stdin if mode == :__read
57
-
58
- raise "contract already built; declare cli_stdin before reading .contract" if defined?(@contract) && @contract
59
-
60
- @__cli_stdin = mode
61
- end
62
-
63
- def view(surface = :default, &blk)
64
- return (@__views ||= {})[surface] unless blk
65
-
66
- raise "contract already built; declare view before reading .contract" if defined?(@contract) && @contract
67
-
68
- (@__views ||= {})[surface] = blk
69
- end
70
-
71
- def contract?
72
- !@__verb.nil?
73
- end
74
-
75
- def contract
76
- @contract ||= Spec.new(
77
- verb: @__verb,
78
- summary: @__summary,
79
- args: (@__args || []).freeze,
80
- surfaces: (@__surfaces || []).freeze,
81
- views: ((@__views ||= {})[:default] ||= ->(v, _i) { v }) && @__views,
82
- cli: @__cli,
83
- cli_stdin: @__cli_stdin,
84
- )
85
- end
86
- end
87
- end
88
- end
@@ -1,25 +0,0 @@
1
- module Textus
2
- module Contract
3
- Spec = Data.define(:verb, :summary, :args, :surfaces, :views, :cli, :cli_stdin) do
4
- def mcp? = surfaces.include?(:mcp)
5
- def cli? = surfaces.include?(:cli)
6
-
7
- def view(surface = :default) = views[surface] || views.fetch(:default)
8
- def cli_path = cli || verb.to_s
9
- def cli_words = cli_path.split
10
- def cli_group = cli_words.size > 1 ? cli_words.first : nil
11
- def cli_leaf = cli_words.last
12
-
13
- def required_args = args.select(&:required)
14
-
15
- def input_schema
16
- props = args.to_h do |a|
17
- h = { "type" => Contract.json_type(a.type) }
18
- h["description"] = a.description if a.description
19
- [a.wire.to_s, h]
20
- end
21
- { type: "object", properties: props, required: required_args.map { |a| a.wire.to_s } }
22
- end
23
- end
24
- end
25
- end
@@ -1,12 +0,0 @@
1
- module Textus
2
- module Contract
3
- JSON_TYPES = {
4
- String => "string", Integer => "integer", Hash => "object",
5
- Array => "array", :boolean => "boolean"
6
- }.freeze
7
-
8
- def self.json_type(type)
9
- JSON_TYPES.fetch(type) { raise ArgumentError.new("no JSON type mapping for #{type.inspect}") }
10
- end
11
- end
12
- end
@@ -1,150 +0,0 @@
1
- require "time"
2
-
3
- module Textus
4
- module Core
5
- module Freshness
6
- # The single currency evaluator (ADR 0099). Answers "is the stored data
7
- # stale relative to its retention rule?" and detects generator drift for
8
- # external entries.
9
- # - retention rule TTL -> AGE signal: now - file_basis > ttl_seconds
10
- # - external -> DRIFT signal: a source changed since generated.at
11
- # (surfaced by the doctor generator_drift check).
12
- class Evaluator
13
- def initialize(manifest:, file_stat:, clock:)
14
- @manifest = manifest
15
- @file_stat = file_stat
16
- @clock = clock
17
- end
18
-
19
- # Per-entry currency Verdict driven by the retention rule TTL (if any).
20
- def verdict(mentry)
21
- ttl = @manifest.rules.for(mentry.key).retention&.ttl_seconds
22
- return fresh if ttl.nil?
23
-
24
- stale = age_stale?(file_basis(mentry), ttl)
25
- Verdict.build(stale: stale, reason: stale ? "ttl exceeded" : nil, fetching: false)
26
- end
27
-
28
- # Keys of entries past their retention rule TTL — the refresh produce scope.
29
- def stale_keys(prefix: nil, lane: nil)
30
- @manifest.data.entries.select { |m| due?(m, prefix: prefix, lane: lane) }.map(&:key)
31
- end
32
-
33
- alias stale_intake_keys stale_keys
34
-
35
- # File basis as a Time (or nil): file mtime when present, else nil.
36
- def file_basis(mentry)
37
- path = @manifest.resolver.resolve(mentry.key).path
38
- return nil unless @file_stat.exists?(path)
39
-
40
- @file_stat.mtime(path)
41
- end
42
-
43
- # Generator-drift rows for one entry (replaces Staleness::GeneratorCheck#
44
- # rows_for) — consumed by the doctor generator_drift check.
45
- def drift_rows(mentry)
46
- return [] unless drift_applicable?(mentry)
47
-
48
- path = Textus::Key::Path.resolve(@manifest.data, mentry)
49
- reason = drift_reason(mentry, path)
50
- reason ? [drift_row(mentry, path, reason)] : []
51
- end
52
-
53
- private
54
-
55
- def fresh = Verdict.build(stale: false, reason: nil, fetching: false)
56
-
57
- def due?(mentry, prefix:, lane:)
58
- return false if lane && mentry.lane != lane
59
- return false if prefix && !mentry.key.start_with?(prefix)
60
-
61
- ttl = @manifest.rules.for(mentry.key).retention&.ttl_seconds
62
- return false if ttl.nil?
63
-
64
- path = @manifest.resolver.resolve(mentry.key).path
65
- return true unless @file_stat.exists?(path)
66
-
67
- age_stale?(file_basis(mentry), ttl)
68
- end
69
-
70
- # The one age comparison. A never-recorded entry (nil basis) is stale.
71
- def age_stale?(basis, ttl)
72
- return true if basis.nil?
73
-
74
- (@clock.now - basis).to_i > ttl
75
- end
76
-
77
- # --- generator drift (lifted from Staleness::GeneratorCheck) ---
78
-
79
- def drift_applicable?(mentry) = mentry.external?
80
-
81
- def drift_reason(mentry, path)
82
- return "derived entry has never been generated" unless @file_stat.exists?(path)
83
-
84
- generated_at = generated_at_of(mentry, path)
85
- return "missing generated.at frontmatter" unless generated_at
86
-
87
- gen_time = parse_time(generated_at)
88
- return "unparseable generated.at: #{generated_at.inspect}" unless gen_time
89
-
90
- offender = newest_source_after(mentry.source, gen_time)
91
- "source '#{offender}' modified after generated.at" if offender
92
- end
93
-
94
- def generated_at_of(mentry, path)
95
- Format.for(mentry.format).parse(@file_stat.read(path), path: path)["_meta"].dig("generated", "at")
96
- end
97
-
98
- def parse_time(str)
99
- Time.parse(str.to_s)
100
- rescue StandardError
101
- nil
102
- end
103
-
104
- def newest_source_after(external_src, gen_time)
105
- Array(external_src.sources).each do |src|
106
- offender = check_source(src, gen_time)
107
- return offender if offender
108
- end
109
- nil
110
- end
111
-
112
- def check_source(src, gen_time)
113
- if src.match?(/\A[a-z0-9.][a-z0-9._-]*\z/) && !src.include?("/")
114
- @manifest.resolver.enumerate(prefix: src).each do |row|
115
- return src if @file_stat.mtime(row[:path]) > gen_time
116
- end
117
- nil
118
- else
119
- check_filesystem_source(src, gen_time)
120
- end
121
- end
122
-
123
- def check_filesystem_source(src, gen_time)
124
- abs = absolutize_source(src)
125
- if @file_stat.directory?(abs)
126
- dir_has_newer_file?(abs, gen_time) ? src : nil
127
- elsif @file_stat.exists?(abs) && @file_stat.mtime(abs) > gen_time
128
- src
129
- end
130
- end
131
-
132
- def absolutize_source(src)
133
- File.absolute_path?(src) ? src : File.join(File.dirname(@manifest.data.root), src)
134
- end
135
-
136
- def dir_has_newer_file?(abs, gen_time)
137
- @file_stat.glob(File.join(abs, "**", "*")).any? do |fpath|
138
- file?(fpath) && @file_stat.mtime(fpath) > gen_time
139
- end
140
- end
141
-
142
- def file?(fpath) = !@file_stat.directory?(fpath) && @file_stat.exists?(fpath)
143
-
144
- def drift_row(mentry, path, reason)
145
- { "key" => mentry.key, "path" => path, "generator" => mentry.source.command, "reason" => reason }
146
- end
147
- end
148
- end
149
- end
150
- end
@@ -1,34 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Core
5
- module Freshness
6
- # Value object describing the freshness annotation attached to an Envelope
7
- # after a currency evaluation (ADR 0099 — was Core::Freshness).
8
- #
9
- # Note on wire format: `#to_h_for_wire` is intentionally narrower than the
10
- # full field set. It emits the legacy keys ("stale", "stale_reason",
11
- # "fetching", and "fetch_error" when present) so the CLI JSON wire stays
12
- # byte-identical with textus/4. The gem-side fields `checked_at` and
13
- # `ttl_remaining_ms` are NOT emitted on the wire.
14
- Verdict = Data.define(
15
- :stale, :fetching, :reason, :fetch_error, :checked_at, :ttl_remaining_ms
16
- ) do
17
- def self.build(stale:, fetching: false, reason: nil, fetch_error: nil,
18
- checked_at: nil, ttl_remaining_ms: nil)
19
- new(
20
- stale: stale, fetching: fetching, reason: reason,
21
- fetch_error: fetch_error, checked_at: checked_at,
22
- ttl_remaining_ms: ttl_remaining_ms
23
- )
24
- end
25
-
26
- def to_h_for_wire
27
- h = { "stale" => stale, "stale_reason" => reason, "fetching" => fetching }
28
- h["fetch_error"] = fetch_error unless fetch_error.nil?
29
- h
30
- end
31
- end
32
- end
33
- end
34
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Core
5
- # Currency — "is the stored data stale relative to its source?" (ADR 0099).
6
- # The home of the single Freshness evaluator and its Verdict value object.
7
- # Distinct from Core::Retention (GC dueness, Q2).
8
- module Freshness
9
- end
10
- end
11
- end
@@ -1,57 +0,0 @@
1
- require "time"
2
-
3
- module Textus
4
- module Core
5
- module Retention
6
- # Retention sweep reporter (ADR 0093/0099). Which entries are past their
7
- # `retention:` ttl and the destructive action that applies. Age basis: file
8
- # mtime. Only drop/archive. Renamed off the Core::Retention vs
9
- # Manifest::Policy::Retention collision (ADR 0099).
10
- class Sweep
11
- def self.expired?(ttl_seconds:, mtime:, now:)
12
- return false if ttl_seconds.nil? || mtime.nil?
13
-
14
- (now - mtime).to_i > ttl_seconds
15
- end
16
-
17
- def initialize(manifest:, file_stat:, clock:)
18
- @manifest = manifest
19
- @file_stat = file_stat
20
- @clock = clock
21
- end
22
-
23
- def call(prefix: nil, lane: nil)
24
- @manifest.data.entries
25
- .select { |m| matches?(m, prefix: prefix, lane: lane) }
26
- .flat_map { |m| rows_for(m) }
27
- end
28
-
29
- private
30
-
31
- def matches?(mentry, prefix:, lane:)
32
- return false if lane && mentry.lane != lane
33
- return false if prefix && !Textus::Key::Matching.matches_prefix?(
34
- mentry.key, prefix, nested: mentry.is_a?(Textus::Manifest::Entry::Nested)
35
- )
36
-
37
- true
38
- end
39
-
40
- def rows_for(mentry)
41
- policy = @manifest.rules.for(mentry.key).retention
42
- return [] if policy.nil?
43
-
44
- @manifest.resolver.enumerate(prefix: mentry.key).filter_map do |row|
45
- path = row[:path]
46
- next unless @file_stat.exists?(path)
47
- next unless self.class.expired?(
48
- ttl_seconds: policy.ttl_seconds, mtime: @file_stat.mtime(path), now: @clock.now,
49
- )
50
-
51
- { "key" => row[:key], "path" => path, "action" => policy.action.to_s }
52
- end
53
- end
54
- end
55
- end
56
- end
57
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Core
5
- # Retention — "is the entry old enough to retire?" (Q2, ADR 0093/0099).
6
- # GC dueness, orthogonal to Freshness (content currency). The reporter is
7
- # Core::Retention::Sweep; the manifest rule policy is Manifest::Policy::Retention.
8
- module Retention
9
- end
10
- end
11
- end
@@ -1,30 +0,0 @@
1
- require "digest"
2
-
3
- module Textus
4
- module Core
5
- # Pure value object representing a published-file sentinel. Holds the
6
- # recorded target path, source path, sha256 checksum, and publish mode.
7
- # Has no filesystem I/O — path layout and persistence live in
8
- # Ports::SentinelStore; predicate methods accept a FileStat port for
9
- # existence and content checks.
10
- class Sentinel
11
- attr_reader :target, :source, :sha256, :mode
12
-
13
- def initialize(target:, source:, sha256:, mode:)
14
- @target = target
15
- @source = source
16
- @sha256 = sha256
17
- @mode = mode
18
- end
19
-
20
- def orphan?(file_stat) = @target.nil? || !file_stat.exists?(@target)
21
-
22
- def drift?(file_stat)
23
- return false if orphan?(file_stat)
24
- return false if @sha256.nil?
25
-
26
- Digest::SHA256.hexdigest(file_stat.read(@target)) != @sha256
27
- end
28
- end
29
- end
30
- end
@@ -1,34 +0,0 @@
1
- module Textus
2
- module Doctor
3
- class Check
4
- class AuditLog < Check
5
- def call
6
- path = Textus::Store::Geometry.new(root).audit_log_path
7
- Textus::Port::AuditLog.new(root).verify_integrity.map do |v|
8
- {
9
- "code" => "audit.parse_error",
10
- "level" => "warning",
11
- "subject" => "#{path}:#{v["lineno"]}",
12
- "message" => violation_message(v),
13
- "fix" => "inspect #{path} at line #{v["lineno"]} and remove the corrupted row",
14
- }
15
- end
16
- end
17
-
18
- private
19
-
20
- def violation_message(v)
21
- case v["reason"]
22
- when "invalid_json"
23
- "audit log line #{v["lineno"]} is invalid JSON: #{v["detail"]}"
24
- when "short_tsv"
25
- "audit log line #{v["lineno"]} #{v["detail"]} " \
26
- "(consider migrating to NDJSON)"
27
- else
28
- v["detail"]
29
- end
30
- end
31
- end
32
- end
33
- end
34
- end