textus 0.55.1 → 0.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (329) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +175 -2768
  3. data/README.md +17 -18
  4. data/lib/textus/artifact.rb +8 -0
  5. data/lib/textus/errors.rb +2 -39
  6. data/lib/textus/human.rb +7 -0
  7. data/lib/textus/infra/adapters/concurrency_adapter.rb +15 -0
  8. data/lib/textus/infra/clock.rb +7 -0
  9. data/lib/textus/infra/database.rb +127 -0
  10. data/lib/textus/infra/file_system.rb +54 -0
  11. data/lib/textus/infra/locks.rb +50 -0
  12. data/lib/textus/infra/port/clock.rb +18 -0
  13. data/lib/textus/infra/port/publisher.rb +71 -0
  14. data/lib/textus/infra/port/sentinel.rb +25 -0
  15. data/lib/textus/infra/port/sentinel_store.rb +93 -0
  16. data/lib/textus/infra/store/audit_log.rb +42 -0
  17. data/lib/textus/infra/store/base.rb +25 -0
  18. data/lib/textus/infra/store/cursor_store.rb +27 -0
  19. data/lib/textus/infra/store/entry_index.rb +31 -0
  20. data/lib/textus/infra/store/envelope/meta.rb +63 -0
  21. data/lib/textus/infra/store/file_store.rb +23 -0
  22. data/lib/textus/infra/store/index/builder.rb +28 -0
  23. data/lib/textus/infra/store/index/lookup.rb +23 -0
  24. data/lib/textus/infra/store/link_store.rb +43 -0
  25. data/lib/textus/infra/store/workflow_queue.rb +54 -0
  26. data/lib/textus/infra/store.rb +29 -0
  27. data/lib/textus/ingest.rb +8 -0
  28. data/lib/textus/lanes/artifact/cache.rb +35 -0
  29. data/lib/textus/lanes/artifact/handlers.rb +29 -0
  30. data/lib/textus/lanes/artifact/lifecycle.rb +23 -0
  31. data/lib/textus/lanes/artifact/verbs.rb +18 -0
  32. data/lib/textus/lanes/ingest/dedup.rb +17 -0
  33. data/lib/textus/lanes/ingest/entry_types/asset.rb +45 -0
  34. data/lib/textus/lanes/ingest/entry_types/link.rb +16 -0
  35. data/lib/textus/lanes/ingest/entry_types/text.rb +17 -0
  36. data/lib/textus/lanes/ingest/entry_types.rb +25 -0
  37. data/lib/textus/lanes/ingest/handlers.rb +54 -0
  38. data/lib/textus/lanes/ingest/index_rebuilder.rb +13 -0
  39. data/lib/textus/lanes/ingest/key_builder.rb +33 -0
  40. data/lib/textus/lanes/ingest/resolver.rb +21 -0
  41. data/lib/textus/lanes/knowledge/freeform.rb +15 -0
  42. data/lib/textus/lanes/knowledge/handlers.rb +53 -0
  43. data/lib/textus/lanes/knowledge/runner.rb +45 -0
  44. data/lib/textus/lanes/knowledge/verbs.rb +16 -0
  45. data/lib/textus/lanes/scratchpad/handlers.rb +164 -0
  46. data/lib/textus/lanes/scratchpad/verbs.rb +73 -0
  47. data/lib/textus/{gate → protocol}/binder.rb +2 -10
  48. data/lib/textus/protocol/boot/catalog.rb +34 -0
  49. data/lib/textus/protocol/boot/context.rb +124 -0
  50. data/lib/textus/protocol/boot/protocol.rb +33 -0
  51. data/lib/textus/protocol/boot.rb +42 -0
  52. data/lib/textus/protocol/builder/domain.rb +17 -0
  53. data/lib/textus/protocol/builder/infrastructure.rb +17 -0
  54. data/lib/textus/protocol/builder/runtime.rb +46 -0
  55. data/lib/textus/protocol/builder.rb +11 -0
  56. data/lib/textus/protocol/container.rb +22 -0
  57. data/lib/textus/protocol/diff.rb +170 -0
  58. data/lib/textus/protocol/entry_constraint.rb +54 -0
  59. data/lib/textus/protocol/envelope.rb +70 -0
  60. data/lib/textus/protocol/format/base.rb +80 -0
  61. data/lib/textus/protocol/format/bash.rb +10 -0
  62. data/lib/textus/protocol/format/javascript.rb +10 -0
  63. data/lib/textus/protocol/format/json.rb +82 -0
  64. data/lib/textus/protocol/format/markdown.rb +53 -0
  65. data/lib/textus/protocol/format/python.rb +10 -0
  66. data/lib/textus/protocol/format/ruby.rb +10 -0
  67. data/lib/textus/protocol/format/script.rb +55 -0
  68. data/lib/textus/protocol/format/text.rb +48 -0
  69. data/lib/textus/protocol/format/yaml.rb +105 -0
  70. data/lib/textus/protocol/format.rb +73 -0
  71. data/lib/textus/protocol/freshness/ttl_evaluator.rb +61 -0
  72. data/lib/textus/protocol/gate.rb +61 -0
  73. data/lib/textus/protocol/handlers/command_context.rb +19 -0
  74. data/lib/textus/protocol/handlers/concern.rb +15 -0
  75. data/lib/textus/protocol/handlers/ops/audit_filter.rb +37 -0
  76. data/lib/textus/protocol/handlers/ops/bulk_planner.rb +38 -0
  77. data/lib/textus/protocol/handlers/ops/data_mv.rb +49 -0
  78. data/lib/textus/protocol/handlers/ops/git_blame.rb +65 -0
  79. data/lib/textus/protocol/handlers/ops/pulse_coordinator.rb +37 -0
  80. data/lib/textus/protocol/handlers/ops/source_expander.rb +41 -0
  81. data/lib/textus/protocol/handlers/query_context.rb +41 -0
  82. data/lib/textus/protocol/handlers/read.rb +67 -0
  83. data/lib/textus/protocol/handlers/system.rb +93 -0
  84. data/lib/textus/protocol/handlers/write.rb +57 -0
  85. data/lib/textus/protocol/init.rb +143 -0
  86. data/lib/textus/protocol/key/grammar.rb +35 -0
  87. data/lib/textus/protocol/key/matching.rb +23 -0
  88. data/lib/textus/protocol/key/path.rb +23 -0
  89. data/lib/textus/protocol/key.rb +25 -0
  90. data/lib/textus/protocol/lane.rb +12 -0
  91. data/lib/textus/{store/geometry.rb → protocol/layout.rb} +32 -22
  92. data/lib/textus/protocol/links/resolver.rb +33 -0
  93. data/lib/textus/protocol/links/uri_rewriter.rb +30 -0
  94. data/lib/textus/protocol/links.rb +10 -0
  95. data/lib/textus/protocol/manifest/data.rb +90 -0
  96. data/lib/textus/protocol/manifest/entry/base.rb +95 -0
  97. data/lib/textus/protocol/manifest/entry/leaf.rb +19 -0
  98. data/lib/textus/protocol/manifest/entry/nested.rb +19 -0
  99. data/lib/textus/protocol/manifest/entry/parser.rb +71 -0
  100. data/lib/textus/protocol/manifest/entry/publish.rb +209 -0
  101. data/lib/textus/protocol/manifest/entry.rb +10 -0
  102. data/lib/textus/protocol/manifest/policy/publish_target.rb +36 -0
  103. data/lib/textus/protocol/manifest/policy.rb +69 -0
  104. data/lib/textus/protocol/manifest/resolver.rb +156 -0
  105. data/lib/textus/protocol/manifest/schema/contract.rb +53 -0
  106. data/lib/textus/protocol/manifest/schema/validator.rb +52 -0
  107. data/lib/textus/protocol/manifest/schema.rb +40 -0
  108. data/lib/textus/protocol/manifest.rb +62 -0
  109. data/lib/textus/protocol/produce/render/context.rb +43 -0
  110. data/lib/textus/protocol/produce/render.rb +44 -0
  111. data/lib/textus/protocol/schema/registry.rb +44 -0
  112. data/lib/textus/protocol/schema/tools.rb +104 -0
  113. data/lib/textus/protocol/schema.rb +117 -0
  114. data/lib/textus/protocol/session/bridge.rb +11 -0
  115. data/lib/textus/protocol/session/context.rb +5 -0
  116. data/lib/textus/protocol/session.rb +87 -0
  117. data/lib/textus/protocol/store_engine/delete.rb +37 -0
  118. data/lib/textus/protocol/store_engine/move.rb +109 -0
  119. data/lib/textus/protocol/store_engine/pipeline.rb +61 -0
  120. data/lib/textus/protocol/store_engine/put.rb +104 -0
  121. data/lib/textus/protocol/store_engine.rb +86 -0
  122. data/lib/textus/protocol/v4.rb +25 -0
  123. data/lib/textus/protocol/verb_registry/arg_spec.rb +19 -0
  124. data/lib/textus/protocol/verb_registry/core_verbs.rb +164 -0
  125. data/lib/textus/protocol/verb_registry/verb_spec.rb +42 -0
  126. data/lib/textus/protocol/verb_registry.rb +42 -0
  127. data/lib/textus/surface/cli/group/{rule.rb → web.rb} +2 -2
  128. data/lib/textus/surface/cli/runner.rb +25 -25
  129. data/lib/textus/surface/cli/sources.rb +1 -3
  130. data/lib/textus/surface/cli/verb/get.rb +3 -6
  131. data/lib/textus/surface/cli/verb/init.rb +1 -1
  132. data/lib/textus/surface/cli/verb/put.rb +8 -6
  133. data/lib/textus/surface/cli/verb/web_serve.rb +22 -0
  134. data/lib/textus/surface/cli.rb +25 -6
  135. data/lib/textus/surface/dispatch.rb +17 -0
  136. data/lib/textus/surface/mcp/adapters/mcp_adapter.rb +28 -0
  137. data/lib/textus/surface/mcp/catalog.rb +26 -62
  138. data/lib/textus/surface/mcp/cursor_manager.rb +22 -0
  139. data/lib/textus/surface/mcp/drift_annotation.rb +19 -0
  140. data/lib/textus/surface/mcp/errors.rb +0 -10
  141. data/lib/textus/surface/mcp/server.rb +46 -46
  142. data/lib/textus/surface/watcher.rb +18 -12
  143. data/lib/textus/surface/web.rb +50 -0
  144. data/lib/textus/value/call.rb +1 -1
  145. data/lib/textus/{core → value}/duration.rb +1 -4
  146. data/lib/textus/value/etag.rb +7 -7
  147. data/lib/textus/value/payload.rb +7 -0
  148. data/lib/textus/version.rb +1 -1
  149. data/lib/textus/workflow/consumer.rb +39 -0
  150. data/lib/textus/workflow/dsl/lifecycle.rb +32 -0
  151. data/lib/textus/workflow/dsl/pattern_matcher.rb +20 -0
  152. data/lib/textus/workflow/dsl/step_builder.rb +56 -0
  153. data/lib/textus/workflow/dsl.rb +111 -21
  154. data/lib/textus/workflow/event_emitter.rb +21 -0
  155. data/lib/textus/workflow/publisher.rb +59 -0
  156. data/lib/textus/workflow/registry.rb +31 -8
  157. data/lib/textus/workflow/runner.rb +25 -61
  158. data/lib/textus/workflow/scheduler.rb +45 -0
  159. data/lib/textus/workflow/step_executor.rb +78 -0
  160. data/lib/textus/workflow/step_helpers.rb +102 -0
  161. data/lib/textus/workflow.rb +2 -5
  162. data/lib/textus.rb +16 -80
  163. metadata +167 -183
  164. data/SPEC.md +0 -908
  165. data/docs/architecture/README.md +0 -273
  166. data/docs/reference/conventions.md +0 -138
  167. data/lib/textus/action/accept.rb +0 -46
  168. data/lib/textus/action/audit.rb +0 -94
  169. data/lib/textus/action/base.rb +0 -42
  170. data/lib/textus/action/blame.rb +0 -79
  171. data/lib/textus/action/boot.rb +0 -15
  172. data/lib/textus/action/data_mv.rb +0 -58
  173. data/lib/textus/action/deps.rb +0 -19
  174. data/lib/textus/action/doctor.rb +0 -17
  175. data/lib/textus/action/drain.rb +0 -31
  176. data/lib/textus/action/enqueue.rb +0 -37
  177. data/lib/textus/action/get.rb +0 -34
  178. data/lib/textus/action/ingest.rb +0 -199
  179. data/lib/textus/action/jobs.rb +0 -27
  180. data/lib/textus/action/key_delete.rb +0 -26
  181. data/lib/textus/action/key_delete_prefix.rb +0 -35
  182. data/lib/textus/action/key_mv.rb +0 -122
  183. data/lib/textus/action/key_mv_prefix.rb +0 -48
  184. data/lib/textus/action/list.rb +0 -28
  185. data/lib/textus/action/propose.rb +0 -42
  186. data/lib/textus/action/published.rb +0 -22
  187. data/lib/textus/action/pulse.rb +0 -49
  188. data/lib/textus/action/put.rb +0 -38
  189. data/lib/textus/action/rdeps.rb +0 -24
  190. data/lib/textus/action/reject.rb +0 -28
  191. data/lib/textus/action/rule_explain.rb +0 -81
  192. data/lib/textus/action/rule_lint.rb +0 -62
  193. data/lib/textus/action/rule_list.rb +0 -38
  194. data/lib/textus/action/schema_envelope.rb +0 -22
  195. data/lib/textus/action/uid.rb +0 -19
  196. data/lib/textus/action/where.rb +0 -21
  197. data/lib/textus/boot.rb +0 -202
  198. data/lib/textus/contract/arg.rb +0 -10
  199. data/lib/textus/contract/dsl.rb +0 -88
  200. data/lib/textus/contract/spec.rb +0 -25
  201. data/lib/textus/contract.rb +0 -12
  202. data/lib/textus/core/freshness/evaluator.rb +0 -150
  203. data/lib/textus/core/freshness/verdict.rb +0 -34
  204. data/lib/textus/core/freshness.rb +0 -11
  205. data/lib/textus/core/retention/sweep.rb +0 -57
  206. data/lib/textus/core/retention.rb +0 -11
  207. data/lib/textus/core/sentinel.rb +0 -30
  208. data/lib/textus/doctor/check/audit_log.rb +0 -34
  209. data/lib/textus/doctor/check/generator_drift.rb +0 -29
  210. data/lib/textus/doctor/check/illegal_keys.rb +0 -60
  211. data/lib/textus/doctor/check/manifest_files.rb +0 -25
  212. data/lib/textus/doctor/check/notebook_sources.rb +0 -50
  213. data/lib/textus/doctor/check/orphaned_publish_targets.rb +0 -35
  214. data/lib/textus/doctor/check/proposal_targets.rb +0 -45
  215. data/lib/textus/doctor/check/protocol_version.rb +0 -47
  216. data/lib/textus/doctor/check/publish_tree_index_overlap.rb +0 -48
  217. data/lib/textus/doctor/check/raw_asset_paths.rb +0 -50
  218. data/lib/textus/doctor/check/rule_ambiguity.rb +0 -49
  219. data/lib/textus/doctor/check/schema_parse_error.rb +0 -28
  220. data/lib/textus/doctor/check/schema_violations.rb +0 -28
  221. data/lib/textus/doctor/check/schemas.rb +0 -26
  222. data/lib/textus/doctor/check/sentinels.rb +0 -60
  223. data/lib/textus/doctor/check/stale_reviewed_stamp.rb +0 -54
  224. data/lib/textus/doctor/check/templates.rb +0 -29
  225. data/lib/textus/doctor/check/unowned_schema_fields.rb +0 -40
  226. data/lib/textus/doctor/check.rb +0 -41
  227. data/lib/textus/doctor/validator.rb +0 -93
  228. data/lib/textus/doctor.rb +0 -54
  229. data/lib/textus/format/base.rb +0 -49
  230. data/lib/textus/format/json.rb +0 -102
  231. data/lib/textus/format/markdown.rb +0 -80
  232. data/lib/textus/format/shared.rb +0 -17
  233. data/lib/textus/format/text.rb +0 -55
  234. data/lib/textus/format/yaml.rb +0 -125
  235. data/lib/textus/format.rb +0 -46
  236. data/lib/textus/gate/auth.rb +0 -212
  237. data/lib/textus/gate.rb +0 -92
  238. data/lib/textus/init.rb +0 -157
  239. data/lib/textus/jobs.rb +0 -9
  240. data/lib/textus/key/distance.rb +0 -55
  241. data/lib/textus/key/grammar.rb +0 -33
  242. data/lib/textus/key/matching.rb +0 -24
  243. data/lib/textus/key/path.rb +0 -28
  244. data/lib/textus/manifest/capabilities.rb +0 -29
  245. data/lib/textus/manifest/data.rb +0 -102
  246. data/lib/textus/manifest/entry/base.rb +0 -105
  247. data/lib/textus/manifest/entry/ignore_matcher.rb +0 -46
  248. data/lib/textus/manifest/entry/leaf.rb +0 -17
  249. data/lib/textus/manifest/entry/nested.rb +0 -37
  250. data/lib/textus/manifest/entry/parser.rb +0 -99
  251. data/lib/textus/manifest/entry/produced.rb +0 -36
  252. data/lib/textus/manifest/entry/publish/mode.rb +0 -45
  253. data/lib/textus/manifest/entry/publish/none.rb +0 -14
  254. data/lib/textus/manifest/entry/publish/subtree_mirror.rb +0 -73
  255. data/lib/textus/manifest/entry/publish/template.rb +0 -16
  256. data/lib/textus/manifest/entry/publish/to_paths.rb +0 -76
  257. data/lib/textus/manifest/entry/publish/tree.rb +0 -50
  258. data/lib/textus/manifest/entry/publish.rb +0 -45
  259. data/lib/textus/manifest/entry/validators/format_matrix.rb +0 -21
  260. data/lib/textus/manifest/entry/validators/ignore.rb +0 -28
  261. data/lib/textus/manifest/entry/validators/publish.rb +0 -30
  262. data/lib/textus/manifest/entry/validators.rb +0 -18
  263. data/lib/textus/manifest/entry.rb +0 -8
  264. data/lib/textus/manifest/policy/matcher.rb +0 -51
  265. data/lib/textus/manifest/policy/publish_target.rb +0 -34
  266. data/lib/textus/manifest/policy/react.rb +0 -30
  267. data/lib/textus/manifest/policy/retention.rb +0 -29
  268. data/lib/textus/manifest/policy/source.rb +0 -30
  269. data/lib/textus/manifest/policy.rb +0 -95
  270. data/lib/textus/manifest/resolver.rb +0 -118
  271. data/lib/textus/manifest/rules.rb +0 -86
  272. data/lib/textus/manifest/schema/contract.rb +0 -61
  273. data/lib/textus/manifest/schema/keys.rb +0 -90
  274. data/lib/textus/manifest/schema/semantics/cross_field.rb +0 -53
  275. data/lib/textus/manifest/schema/semantics/invariants.rb +0 -125
  276. data/lib/textus/manifest/schema/semantics/migration.rb +0 -83
  277. data/lib/textus/manifest/schema/semantics.rb +0 -27
  278. data/lib/textus/manifest/schema/validator.rb +0 -48
  279. data/lib/textus/manifest/schema/vocabulary.rb +0 -25
  280. data/lib/textus/manifest/schema.rb +0 -30
  281. data/lib/textus/manifest.rb +0 -65
  282. data/lib/textus/meta.rb +0 -54
  283. data/lib/textus/port/audit_log.rb +0 -268
  284. data/lib/textus/port/build_lock.rb +0 -64
  285. data/lib/textus/port/clock.rb +0 -10
  286. data/lib/textus/port/publisher.rb +0 -60
  287. data/lib/textus/port/sentinel_store.rb +0 -83
  288. data/lib/textus/port/storage/file_stat.rb +0 -19
  289. data/lib/textus/port/storage/file_store.rb +0 -26
  290. data/lib/textus/port/store.rb +0 -93
  291. data/lib/textus/port/watcher_lock.rb +0 -48
  292. data/lib/textus/produce/engine.rb +0 -50
  293. data/lib/textus/produce/render.rb +0 -20
  294. data/lib/textus/schema/tools.rb +0 -115
  295. data/lib/textus/schema.rb +0 -100
  296. data/lib/textus/schemas.rb +0 -54
  297. data/lib/textus/store/compositor.rb +0 -34
  298. data/lib/textus/store/container.rb +0 -43
  299. data/lib/textus/store/cursor.rb +0 -26
  300. data/lib/textus/store/envelope/reader.rb +0 -43
  301. data/lib/textus/store/envelope/writer.rb +0 -195
  302. data/lib/textus/store/index/builder.rb +0 -74
  303. data/lib/textus/store/index/lookup.rb +0 -60
  304. data/lib/textus/store/jobs/base.rb +0 -13
  305. data/lib/textus/store/jobs/index.rb +0 -15
  306. data/lib/textus/store/jobs/materialize.rb +0 -15
  307. data/lib/textus/store/jobs/plan.rb +0 -11
  308. data/lib/textus/store/jobs/planner.rb +0 -104
  309. data/lib/textus/store/jobs/queue.rb +0 -154
  310. data/lib/textus/store/jobs/registry.rb +0 -19
  311. data/lib/textus/store/jobs/retention.rb +0 -50
  312. data/lib/textus/store/jobs/sweep.rb +0 -21
  313. data/lib/textus/store/jobs/worker.rb +0 -64
  314. data/lib/textus/store/session.rb +0 -37
  315. data/lib/textus/store.rb +0 -99
  316. data/lib/textus/surface/cli/verb/doctor.rb +0 -19
  317. data/lib/textus/surface/cli/verb/schema_diff.rb +0 -17
  318. data/lib/textus/surface/cli/verb/schema_init.rb +0 -21
  319. data/lib/textus/surface/cli/verb/schema_migrate.rb +0 -21
  320. data/lib/textus/surface/mcp.rb +0 -8
  321. data/lib/textus/surface/projector.rb +0 -27
  322. data/lib/textus/surface/role_scope.rb +0 -34
  323. data/lib/textus/value/command.rb +0 -16
  324. data/lib/textus/value/envelope.rb +0 -89
  325. data/lib/textus/value/result.rb +0 -26
  326. data/lib/textus/workflow/collector.rb +0 -27
  327. data/lib/textus/workflow/context.rb +0 -5
  328. data/lib/textus/workflow/loader.rb +0 -17
  329. data/lib/textus/workflow/pattern.rb +0 -18
@@ -1,40 +0,0 @@
1
- module Textus
2
- module Doctor
3
- class Check
4
- class UnownedSchemaFields < Check
5
- def call
6
- dir = geometry.schemas_dir
7
- return [] unless File.directory?(dir)
8
-
9
- Dir.glob(File.join(dir, "*.yaml")).flat_map do |path|
10
- issues_for(path)
11
- end
12
- end
13
-
14
- private
15
-
16
- def issues_for(path)
17
- schema = safe_load(path)
18
- return [] if schema.nil?
19
-
20
- unowned = schema.unowned_fields
21
- return [] if unowned.empty?
22
-
23
- [{
24
- "code" => "schema.unowned_fields",
25
- "level" => "info",
26
- "subject" => schema.name || File.basename(path, ".yaml"),
27
- "message" => "schema has fields without maintained_by: #{unowned.join(", ")}",
28
- "fix" => "add 'maintained_by: <role>' to each field in #{path} (optional but recommended)",
29
- }]
30
- end
31
-
32
- def safe_load(path)
33
- Schema.load(path)
34
- rescue StandardError
35
- nil
36
- end
37
- end
38
- end
39
- end
40
- end
@@ -1,41 +0,0 @@
1
- module Textus
2
- module Doctor
3
- # Abstract base for a single doctor check. Each concrete check inspects
4
- # one slice of store health and returns an array of issue hashes:
5
- # { "code" => String, "level" => "error"|"warning"|"info",
6
- # "subject" => String, "message" => String, "fix" => String (optional) }
7
- class Check
8
- # Snake-case name used in --checks flag and ALL_CHECKS list. Default
9
- # derives from the class name; override if the SPEC name diverges.
10
- def self.name_key
11
- @name_key ||= name.split("::").last
12
- .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
13
- .gsub(/([a-z\d])([A-Z])/, '\1_\2')
14
- .downcase
15
- end
16
-
17
- def initialize(container, role: Textus::Value::Role::DEFAULT)
18
- @container = container
19
- @role = role
20
- end
21
-
22
- def call
23
- raise NotImplementedError.new("#{self.class.name}#call not implemented")
24
- end
25
-
26
- protected
27
-
28
- def root = @container.root
29
- def geometry = @container.geometry
30
- def manifest = @container.manifest
31
-
32
- # Dispatch a verb through Gate.
33
- def dispatch(verb, *args, **kwargs)
34
- klass = Textus::Action::VERBS[verb]
35
- spec = klass.contract if klass.respond_to?(:contract?) && klass.contract?
36
- inputs = spec ? Textus::Gate::Binder.inputs_from_ordered(spec, args, kwargs) : kwargs
37
- @container.gate.dispatch(spec:, inputs:, role: @role)
38
- end
39
- end
40
- end
41
- end
@@ -1,93 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Doctor
5
- class Validator
6
- def initialize(reader:, manifest:, audit_log:, schema_for:)
7
- @reader = reader
8
- @manifest = manifest
9
- @audit_log = audit_log
10
- @schema_for = schema_for
11
- end
12
-
13
- def call(container:, call:)
14
- @container = container
15
- @call = call
16
- violations = []
17
- check_content_violations(violations)
18
- check_role_authority_violations(violations)
19
- { "protocol" => Textus::PROTOCOL, "ok" => violations.empty?, "violations" => violations }
20
- end
21
-
22
- private
23
-
24
- def check_content_violations(violations)
25
- @manifest.resolver.enumerate.each do |row|
26
- key = row[:key]
27
- mentry = row[:manifest_entry]
28
- env = fetch_envelope(key, violations) or next
29
- schema = mentry.schema && @schema_for.call(mentry.schema)
30
- next unless schema
31
-
32
- begin
33
- validate_schema!(schema, env, mentry.format)
34
- rescue Textus::Error => e
35
- violations << { "key" => key, "code" => e.code, "message" => e.message }
36
- end
37
- end
38
- end
39
-
40
- def check_role_authority_violations(violations)
41
- @manifest.resolver.enumerate.each do |row|
42
- mentry = row[:manifest_entry]
43
- next unless mentry.schema
44
-
45
- schema = @schema_for.call(mentry.schema)
46
- next unless schema
47
-
48
- env = begin
49
- @reader.call(row[:key], @container, @call)
50
- rescue StandardError
51
- next
52
- end
53
- append_authority_violations(violations, row[:key], env, schema)
54
- end
55
- end
56
-
57
- def append_authority_violations(violations, key, env, schema)
58
- last_writer = @audit_log.last_writer_for(key)
59
- return if last_writer.nil?
60
-
61
- last_writer_is_authority =
62
- @manifest.policy.roles_with_capability("author").include?(last_writer)
63
- env.meta.each_key do |field|
64
- owner = schema.maintained_by(field)
65
- next if owner.nil? || last_writer == owner || last_writer_is_authority
66
-
67
- violations << {
68
- "key" => key,
69
- "code" => "role_authority",
70
- "field" => field,
71
- "expected" => owner,
72
- "last_writer" => last_writer,
73
- }
74
- end
75
- end
76
-
77
- def fetch_envelope(key, violations)
78
- @reader.call(key, @container, @call)
79
- rescue Textus::Error => e
80
- violations << { "key" => key, "code" => e.code, "message" => e.message }
81
- nil
82
- end
83
-
84
- def validate_schema!(schema, envelope, format)
85
- payload = case format
86
- when "json", "yaml" then envelope.content || {}
87
- else envelope.meta || {}
88
- end
89
- schema.validate!(payload)
90
- end
91
- end
92
- end
93
- end
data/lib/textus/doctor.rb DELETED
@@ -1,54 +0,0 @@
1
- module Textus
2
- # Health check for a Textus store. Returns a JSON-friendly Hash envelope
3
- # with an `issues` array and a summary. Each issue is a Hash with
4
- # `code`, `level`, `subject`, `message`, and optionally `fix`.
5
- module Doctor
6
- LEVELS = %w[error warning info].freeze
7
-
8
- CHECKS = [
9
- Check::ProtocolVersion,
10
- Check::ManifestFiles,
11
- Check::Schemas,
12
- Check::SchemaParseError,
13
- Check::Templates,
14
- Check::IllegalKeys,
15
- Check::Sentinels,
16
- Check::AuditLog,
17
- Check::UnownedSchemaFields,
18
- Check::SchemaViolations,
19
- Check::RuleAmbiguity,
20
- Check::OrphanedPublishTargets,
21
- Check::PublishTreeIndexOverlap,
22
- Check::ProposalTargets,
23
- Check::GeneratorDrift,
24
- Check::RawAssetPaths,
25
- Check::NotebookSources,
26
- Check::StaleReviewedStamp,
27
- ].freeze
28
-
29
- ALL_CHECKS = CHECKS.map(&:name_key).freeze
30
-
31
- module_function
32
-
33
- def build(container:, checks: nil, role: Textus::Value::Role::DEFAULT)
34
- selected_keys = checks ? Array(checks).map(&:to_s) : ALL_CHECKS
35
- unknown = selected_keys - ALL_CHECKS
36
- unless unknown.empty?
37
- raise UsageError.new(
38
- "unknown doctor check: #{unknown.first}. Valid checks: #{ALL_CHECKS.join(", ")}",
39
- )
40
- end
41
-
42
- selected = CHECKS.select { |c| selected_keys.include?(c.name_key) }
43
- issues = selected.flat_map { |c| c.new(container, role:).call }
44
-
45
- summary = LEVELS.to_h { |l| [l, issues.count { |i| i["level"] == l }] }
46
- {
47
- "protocol" => Textus::PROTOCOL,
48
- "ok" => summary["error"].zero?,
49
- "issues" => issues,
50
- "summary" => summary,
51
- }
52
- end
53
- end
54
- end
@@ -1,49 +0,0 @@
1
- module Textus
2
- module Format
3
- class Base
4
- def self.parse(_raw, path: nil)
5
- _ = path
6
- raise NotImplementedError.new("#{name}.parse not implemented")
7
- end
8
-
9
- def self.serialize(meta: {}, body: "", content: nil)
10
- _ = meta
11
- _ = body
12
- _ = content
13
- raise NotImplementedError.new("#{name}.serialize not implemented")
14
- end
15
-
16
- def self.extensions
17
- raise NotImplementedError.new("#{name}.extensions not implemented")
18
- end
19
-
20
- def self.validate_against(schema, parsed)
21
- schema.validate!(parsed["_meta"] || {})
22
- end
23
-
24
- def self.nested_glob
25
- raise NotImplementedError.new("#{name}.nested_glob not implemented")
26
- end
27
-
28
- def self.validate_path_extension(_path, _nested)
29
- raise NotImplementedError.new("#{name}.validate_path_extension not implemented")
30
- end
31
-
32
- def self.enforce_name_match!(_path, _meta)
33
- raise NotImplementedError.new("#{name}.enforce_name_match! not implemented")
34
- end
35
-
36
- def self.serialize_for_put(meta:, body:, content:, path:)
37
- _ = meta
38
- _ = body
39
- _ = content
40
- _ = path
41
- raise NotImplementedError.new("#{name}.serialize_for_put not implemented")
42
- end
43
-
44
- def self.rewrite_name(_path, _basename)
45
- raise NotImplementedError.new("#{name}.rewrite_name not implemented")
46
- end
47
- end
48
- end
49
- end
@@ -1,102 +0,0 @@
1
- require "json"
2
-
3
- module Textus
4
- module Format
5
- class Json < Base
6
- def self.parse(raw, path: nil)
7
- raw = raw.dup.force_encoding(Encoding::UTF_8)
8
- raise BadFrontmatter.new(path, "entry is not valid UTF-8") unless raw.valid_encoding?
9
-
10
- begin
11
- parsed = ::JSON.parse(raw)
12
- rescue ::JSON::ParserError => e
13
- raise BadFrontmatter.new(path, "JSON parse failed: #{e.message}")
14
- end
15
- raise BadFrontmatter.new(path, "JSON top-level must be an object") unless parsed.is_a?(Hash)
16
-
17
- meta = parsed["_meta"]
18
- fm = meta.is_a?(Hash) ? meta : {}
19
- content_without_meta = parsed.except("_meta")
20
- { "_meta" => fm, "body" => raw, "content" => content_without_meta }
21
- end
22
-
23
- def self.serialize(meta:, body:, content: nil)
24
- if content.is_a?(Hash)
25
- on_disk = meta && !meta.empty? ? { "_meta" => meta }.merge(content) : content
26
- out = ::JSON.pretty_generate(on_disk)
27
- out += "\n" unless out.end_with?("\n")
28
- out
29
- elsif body && !body.to_s.empty?
30
- b = body.to_s
31
- b += "\n" unless b.end_with?("\n")
32
- b
33
- else
34
- raise UsageError.new("json serialize requires :content or :body")
35
- end
36
- end
37
-
38
- def self.validate_against(schema, parsed)
39
- schema.validate!(parsed["content"] || {})
40
- end
41
-
42
- def self.extensions = [".json"]
43
-
44
- def self.nested_glob = "**/*.json"
45
-
46
- def self.serialize_for_put(meta:, body:, content:, path:)
47
- raise UsageError.new("put for json requires content: or body:") if content.nil? && (body.nil? || body.to_s.empty?)
48
-
49
- if content.nil?
50
- begin
51
- parsed = parse(body.to_s, path: path)
52
- rescue BadFrontmatter => e
53
- raise BadContent.new(path, "bad_content: #{e.message}")
54
- end
55
- [body.to_s, parsed["_meta"], body.to_s, parsed["content"]]
56
- else
57
- bytes = serialize(meta: meta, body: "", content: content)
58
- [bytes, meta, bytes, content]
59
- end
60
- end
61
-
62
- def self.rewrite_name(path, basename) # rubocop:disable Naming/PredicateMethod
63
- raw = File.binread(path)
64
- parsed = parse(raw, path: path)
65
- meta = parsed["_meta"]
66
- return false unless meta.is_a?(Hash) && meta["name"].is_a?(String) && meta["name"] != basename
67
-
68
- new_meta = meta.merge("name" => basename)
69
- File.binwrite(path, serialize(meta: new_meta, body: "", content: parsed["content"]))
70
- true
71
- end
72
-
73
- def self.enforce_name_match!(path, meta)
74
- return unless meta.is_a?(Hash) && meta["name"]
75
-
76
- ext = extensions.first
77
- basename = File.basename(path, ext)
78
- return if meta["name"] == basename
79
-
80
- raise BadFrontmatter.new(path, "name '#{meta["name"]}' does not match basename '#{basename}'")
81
- end
82
-
83
- def self.validate_path_extension(path, nested)
84
- ext = File.extname(path)
85
- if nested
86
- return if ext == ""
87
-
88
- raise UsageError.new("nested json path must not have an extension")
89
- end
90
-
91
- return if ext == ".json"
92
-
93
- raise UsageError.new("json format requires '.json' path (got #{ext.inspect})")
94
- end
95
-
96
- def self.data_to_payload(data)
97
- data = data.transform_keys(&:to_s) if data.is_a?(Hash)
98
- { meta: data["_meta"] || {}, body: nil, content: data["content"] || data }
99
- end
100
- end
101
- end
102
- end
@@ -1,80 +0,0 @@
1
- require "yaml"
2
-
3
- module Textus
4
- module Format
5
- class Markdown < Base
6
- def self.parse(raw, path: nil)
7
- raw = raw.dup.force_encoding(Encoding::UTF_8)
8
- raise BadFrontmatter.new(path, "entry is not valid UTF-8") unless raw.valid_encoding?
9
- return { "_meta" => {}, "body" => raw, "content" => nil } unless raw.start_with?("---\n") || raw.start_with?("---\r\n")
10
-
11
- lines = raw.split(/\r?\n/, -1)
12
- close_idx = lines[1..].index("---")
13
- raise BadFrontmatter.new(path, "frontmatter not terminated") unless close_idx
14
-
15
- close_idx += 1
16
- fm_yaml = lines[1...close_idx].join("\n")
17
- body = lines[(close_idx + 1)..].join("\n")
18
- begin
19
- fm = fm_yaml.strip.empty? ? {} : ::YAML.safe_load(fm_yaml, permitted_classes: [Date, Time], aliases: false)
20
- rescue Psych::SyntaxError => e
21
- raise BadFrontmatter.new(path, "YAML parse failed: #{e.message}")
22
- end
23
- fm = {} unless fm.is_a?(Hash)
24
- { "_meta" => fm, "body" => body, "content" => nil }
25
- end
26
-
27
- def self.serialize(meta:, body:, content: nil)
28
- _ = content
29
- fm_yaml = meta.empty? ? "" : ::YAML.dump(meta).sub(/\A---\n/, "")
30
- body = body.to_s
31
- body += "\n" unless body.empty? || body.end_with?("\n")
32
- "---\n#{fm_yaml}---\n#{body}"
33
- end
34
-
35
- def self.extensions = [".md"]
36
-
37
- def self.nested_glob = "**/*.md"
38
-
39
- def self.serialize_for_put(meta:, body:, content:, path:)
40
- _ = path
41
- _ = content
42
- bytes = serialize(meta: meta || {}, body: body.to_s)
43
- [bytes, meta, body.to_s, nil]
44
- end
45
-
46
- def self.rewrite_name(path, basename) # rubocop:disable Naming/PredicateMethod
47
- raw = File.binread(path)
48
- parsed = parse(raw, path: path)
49
- meta = parsed["_meta"] || {}
50
- return false unless meta.is_a?(Hash) && meta["name"].is_a?(String) && meta["name"] != basename
51
-
52
- new_meta = meta.merge("name" => basename)
53
- File.binwrite(path, serialize(meta: new_meta, body: parsed["body"]))
54
- true
55
- end
56
-
57
- def self.enforce_name_match!(path, meta)
58
- return unless meta.is_a?(Hash) && meta["name"]
59
-
60
- ext = extensions.first
61
- basename = File.basename(path, ext)
62
- return if meta["name"] == basename
63
-
64
- raise BadFrontmatter.new(path, "name '#{meta["name"]}' does not match basename '#{basename}'")
65
- end
66
-
67
- def self.validate_path_extension(path, _nested)
68
- ext = File.extname(path)
69
- return if ["", ".md"].include?(ext)
70
-
71
- raise UsageError.new("markdown format requires '.md' path (got #{ext.inspect})")
72
- end
73
-
74
- def self.data_to_payload(data)
75
- data = data.transform_keys(&:to_s) if data.is_a?(Hash)
76
- { meta: data["_meta"] || {}, body: (data["body"] || "").to_s, content: nil }
77
- end
78
- end
79
- end
80
- end
@@ -1,17 +0,0 @@
1
- module Textus
2
- module Format
3
- module Shared
4
- ENFORCE_NAME_RE = /\.(md|json|yaml|yml|txt)\z/i
5
-
6
- def self.enforce_name_match!(path, meta, extensions)
7
- return unless meta.is_a?(Hash) && meta["name"]
8
-
9
- ext = extensions.first
10
- basename = File.basename(path, ext)
11
- return if meta["name"] == basename
12
-
13
- raise BadFrontmatter.new(path, "name '#{meta["name"]}' does not match basename '#{basename}'")
14
- end
15
- end
16
- end
17
- end
@@ -1,55 +0,0 @@
1
- module Textus
2
- module Format
3
- class Text < Base
4
- def self.parse(raw, path: nil)
5
- raw = raw.dup.force_encoding(Encoding::UTF_8)
6
- raise BadFrontmatter.new(path, "entry is not valid UTF-8") unless raw.valid_encoding?
7
-
8
- { "_meta" => {}, "body" => raw, "content" => nil }
9
- end
10
-
11
- def self.serialize(meta:, body:, content: nil)
12
- _ = meta
13
- _ = content
14
- b = body.to_s
15
- b += "\n" unless b.empty? || b.end_with?("\n")
16
- b
17
- end
18
-
19
- def self.extensions = [".txt"]
20
-
21
- def self.nested_glob = "**/*.txt"
22
-
23
- def self.enforce_name_match!(_path, _meta); end
24
-
25
- def self.serialize_for_put(meta:, body:, content:, path:)
26
- _ = path
27
- _ = content
28
- bytes = serialize(meta: meta || {}, body: body.to_s)
29
- [bytes, meta, body.to_s, nil]
30
- end
31
-
32
- def self.rewrite_name(_path, _basename) # rubocop:disable Naming/PredicateMethod
33
- false
34
- end
35
-
36
- def self.validate_path_extension(path, nested)
37
- ext = File.extname(path)
38
- if nested
39
- return if ext == ""
40
-
41
- raise UsageError.new("nested text path must not have an extension")
42
- end
43
-
44
- return if [".txt", ""].include?(ext)
45
-
46
- raise UsageError.new("text format requires '.txt' or no extension (got #{ext.inspect})")
47
- end
48
-
49
- def self.data_to_payload(data)
50
- data = data.transform_keys(&:to_s) if data.is_a?(Hash)
51
- { meta: data["_meta"] || {}, body: (data["body"] || "").to_s, content: nil }
52
- end
53
- end
54
- end
55
- end
@@ -1,125 +0,0 @@
1
- require "yaml"
2
-
3
- module Textus
4
- module Format
5
- class Yaml < Base
6
- def self.parse(raw, path: nil)
7
- raw = raw.dup.force_encoding(Encoding::UTF_8)
8
- raise BadFrontmatter.new(path, "entry is not valid UTF-8") unless raw.valid_encoding?
9
-
10
- begin
11
- parsed = ::YAML.safe_load(raw, permitted_classes: [Date, Time], aliases: false)
12
- rescue Psych::SyntaxError, Psych::AliasesNotEnabled, Psych::DisallowedClass => e
13
- raise BadFrontmatter.new(path, "YAML parse failed: #{e.message}")
14
- end
15
- raise BadFrontmatter.new(path, "YAML top-level must be a mapping") unless parsed.is_a?(Hash)
16
-
17
- meta = parsed["_meta"]
18
- fm = meta.is_a?(Hash) ? meta : {}
19
- content_without_meta = parsed.except("_meta")
20
- { "_meta" => fm, "body" => raw, "content" => content_without_meta }
21
- end
22
-
23
- def self.serialize(meta:, body:, content: nil)
24
- if content.is_a?(Hash)
25
- on_disk = meta && !meta.empty? ? { "_meta" => meta }.merge(content) : content
26
- ::YAML.dump(on_disk).sub(/\A---\n/, "")
27
- elsif body && !body.to_s.empty?
28
- b = body.to_s
29
- b += "\n" unless b.end_with?("\n")
30
- b
31
- else
32
- raise UsageError.new("yaml serialize requires :content or :body")
33
- end
34
- end
35
-
36
- def self.validate_against(schema, parsed)
37
- schema.validate!(parsed["content"] || {})
38
- end
39
-
40
- RAW_REQUIRED = %w[ingested_at content_hash].freeze
41
- RAW_SOURCE_KINDS = %w[url file asset].freeze
42
-
43
- def self.validate_raw_entry!(parsed, lane)
44
- return unless lane == "raw"
45
-
46
- content = parsed["content"] || {}
47
- return if content["superseded_by"]
48
-
49
- missing = RAW_REQUIRED.reject { |f| content[f] }
50
- raise Textus::BadContent.new(nil, "raw entry missing required field(s): #{missing.join(", ")}") if missing.any?
51
-
52
- source = content["source"] || {}
53
- kind = source["kind"]
54
- unless RAW_SOURCE_KINDS.include?(kind)
55
- raise Textus::BadContent.new(
56
- nil, "raw entry source.kind must be #{RAW_SOURCE_KINDS.join("|")}, got #{kind.inspect}"
57
- )
58
- end
59
-
60
- return unless kind == "url" && !source["url"]
61
-
62
- raise Textus::BadContent.new(nil, "raw entry with source.kind=url must have source.url")
63
- end
64
-
65
- def self.extensions = [".yaml", ".yml"]
66
-
67
- def self.nested_glob = "**/*.{yaml,yml}"
68
-
69
- def self.serialize_for_put(meta:, body:, content:, path:)
70
- raise UsageError.new("put for yaml requires content: or body:") if content.nil? && (body.nil? || body.to_s.empty?)
71
-
72
- if content.nil?
73
- begin
74
- parsed = parse(body.to_s, path: path)
75
- rescue BadFrontmatter => e
76
- raise BadContent.new(path, "bad_content: #{e.message}")
77
- end
78
- [body.to_s, parsed["_meta"], body.to_s, parsed["content"]]
79
- else
80
- bytes = serialize(meta: meta, body: "", content: content)
81
- [bytes, meta, bytes, content]
82
- end
83
- end
84
-
85
- def self.rewrite_name(path, basename) # rubocop:disable Naming/PredicateMethod
86
- raw = File.binread(path)
87
- parsed = parse(raw, path: path)
88
- meta = parsed["_meta"]
89
- return false unless meta.is_a?(Hash) && meta["name"].is_a?(String) && meta["name"] != basename
90
-
91
- new_meta = meta.merge("name" => basename)
92
- File.binwrite(path, serialize(meta: new_meta, body: "", content: parsed["content"]))
93
- true
94
- end
95
-
96
- def self.enforce_name_match!(path, meta)
97
- return unless meta.is_a?(Hash) && meta["name"]
98
-
99
- ext = extensions.first
100
- basename = File.basename(path, ext)
101
- return if meta["name"] == basename
102
-
103
- raise BadFrontmatter.new(path, "name '#{meta["name"]}' does not match basename '#{basename}'")
104
- end
105
-
106
- def self.validate_path_extension(path, nested)
107
- ext = File.extname(path)
108
- if nested
109
- return if ext == ""
110
-
111
- raise UsageError.new("nested yaml path must not have an extension")
112
- end
113
-
114
- return if [".yaml", ".yml"].include?(ext)
115
-
116
- raise UsageError.new("yaml format requires '.yaml' or '.yml' path (got #{ext.inspect})")
117
- end
118
-
119
- def self.data_to_payload(data)
120
- data = data.transform_keys(&:to_s) if data.is_a?(Hash)
121
- { meta: data["_meta"] || {}, body: nil, content: data["content"] || data }
122
- end
123
- end
124
- end
125
- end