textus 0.55.2 → 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 (338) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +175 -2768
  3. data/README.md +8 -9
  4. data/lib/textus/artifact.rb +8 -0
  5. data/lib/textus/errors.rb +0 -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/{dispatch → protocol}/binder.rb +1 -7
  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/{store → protocol}/freshness/ttl_evaluator.rb +7 -3
  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 → protocol}/layout.rb +16 -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 +22 -21
  129. data/lib/textus/surface/cli/sources.rb +1 -3
  130. data/lib/textus/surface/cli/verb/get.rb +2 -7
  131. data/lib/textus/surface/cli/verb/init.rb +1 -1
  132. data/lib/textus/surface/cli/verb/put.rb +7 -7
  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 +12 -12
  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/server.rb +35 -24
  141. data/lib/textus/surface/watcher.rb +18 -12
  142. data/lib/textus/surface/web.rb +50 -0
  143. data/lib/textus/value/call.rb +1 -1
  144. data/lib/textus/value/etag.rb +7 -7
  145. data/lib/textus/version.rb +1 -1
  146. data/lib/textus/workflow/consumer.rb +39 -0
  147. data/lib/textus/workflow/dsl/lifecycle.rb +32 -0
  148. data/lib/textus/workflow/dsl/pattern_matcher.rb +20 -0
  149. data/lib/textus/workflow/dsl/step_builder.rb +56 -0
  150. data/lib/textus/workflow/dsl.rb +111 -21
  151. data/lib/textus/workflow/event_emitter.rb +21 -0
  152. data/lib/textus/workflow/publisher.rb +59 -0
  153. data/lib/textus/workflow/registry.rb +31 -8
  154. data/lib/textus/workflow/runner.rb +25 -53
  155. data/lib/textus/workflow/scheduler.rb +45 -0
  156. data/lib/textus/workflow/step_executor.rb +78 -0
  157. data/lib/textus/workflow/step_helpers.rb +102 -0
  158. data/lib/textus/workflow.rb +2 -5
  159. data/lib/textus.rb +16 -16
  160. metadata +169 -185
  161. data/SPEC.md +0 -909
  162. data/docs/architecture/README.md +0 -273
  163. data/docs/reference/conventions.md +0 -141
  164. data/lib/textus/boot.rb +0 -181
  165. data/lib/textus/dispatch/contracts.rb +0 -63
  166. data/lib/textus/dispatch/handler_registry.rb +0 -21
  167. data/lib/textus/dispatch/middleware/audit_index.rb +0 -51
  168. data/lib/textus/dispatch/middleware/auth.rb +0 -40
  169. data/lib/textus/dispatch/middleware/base.rb +0 -26
  170. data/lib/textus/dispatch/middleware/binder.rb +0 -20
  171. data/lib/textus/dispatch/middleware/cascade.rb +0 -53
  172. data/lib/textus/dispatch/pipeline.rb +0 -35
  173. data/lib/textus/doctor/check/audit_log.rb +0 -34
  174. data/lib/textus/doctor/check/generator_drift.rb +0 -29
  175. data/lib/textus/doctor/check/illegal_keys.rb +0 -60
  176. data/lib/textus/doctor/check/manifest_files.rb +0 -25
  177. data/lib/textus/doctor/check/orphaned_publish_targets.rb +0 -35
  178. data/lib/textus/doctor/check/proposal_targets.rb +0 -45
  179. data/lib/textus/doctor/check/protocol_version.rb +0 -47
  180. data/lib/textus/doctor/check/publish_tree_index_overlap.rb +0 -48
  181. data/lib/textus/doctor/check/raw_asset_paths.rb +0 -50
  182. data/lib/textus/doctor/check/rule_ambiguity.rb +0 -49
  183. data/lib/textus/doctor/check/schema_parse_error.rb +0 -28
  184. data/lib/textus/doctor/check/schema_violations.rb +0 -28
  185. data/lib/textus/doctor/check/schemas.rb +0 -26
  186. data/lib/textus/doctor/check/scratchpad_sources.rb +0 -55
  187. data/lib/textus/doctor/check/sentinels.rb +0 -60
  188. data/lib/textus/doctor/check/stale_reviewed_stamp.rb +0 -54
  189. data/lib/textus/doctor/check/templates.rb +0 -29
  190. data/lib/textus/doctor/check/unowned_schema_fields.rb +0 -40
  191. data/lib/textus/doctor/check.rb +0 -43
  192. data/lib/textus/doctor/validator.rb +0 -93
  193. data/lib/textus/doctor.rb +0 -54
  194. data/lib/textus/format/base.rb +0 -77
  195. data/lib/textus/format/json.rb +0 -81
  196. data/lib/textus/format/markdown.rb +0 -59
  197. data/lib/textus/format/text.rb +0 -55
  198. data/lib/textus/format/yaml.rb +0 -104
  199. data/lib/textus/format.rb +0 -61
  200. data/lib/textus/handlers/maintenance/boot_store.rb +0 -15
  201. data/lib/textus/handlers/maintenance/doctor_store.rb +0 -15
  202. data/lib/textus/handlers/maintenance/drain_store.rb +0 -21
  203. data/lib/textus/handlers/maintenance/ingest_entry.rb +0 -159
  204. data/lib/textus/handlers/maintenance/jobs_action.rb +0 -21
  205. data/lib/textus/handlers/maintenance/published_entries.rb +0 -17
  206. data/lib/textus/handlers/maintenance/rule_explain.rb +0 -77
  207. data/lib/textus/handlers/maintenance/rule_lint.rb +0 -54
  208. data/lib/textus/handlers/maintenance/rule_list.rb +0 -32
  209. data/lib/textus/handlers/maintenance/schema_envelope.rb +0 -19
  210. data/lib/textus/handlers/read/audit_entries.rb +0 -48
  211. data/lib/textus/handlers/read/blame_entry.rb +0 -71
  212. data/lib/textus/handlers/read/deps_entry.rb +0 -17
  213. data/lib/textus/handlers/read/get_entry.rb +0 -68
  214. data/lib/textus/handlers/read/list_keys.rb +0 -36
  215. data/lib/textus/handlers/read/pulse_entries.rb +0 -66
  216. data/lib/textus/handlers/read/rdeps_entry.rb +0 -21
  217. data/lib/textus/handlers/read/uid_entry.rb +0 -18
  218. data/lib/textus/handlers/read/where_entry.rb +0 -18
  219. data/lib/textus/handlers/write/accept_proposal.rb +0 -39
  220. data/lib/textus/handlers/write/data_mv.rb +0 -55
  221. data/lib/textus/handlers/write/delete_key.rb +0 -17
  222. data/lib/textus/handlers/write/enqueue_job.rb +0 -27
  223. data/lib/textus/handlers/write/key_delete_prefix.rb +0 -32
  224. data/lib/textus/handlers/write/key_mv_prefix.rb +0 -45
  225. data/lib/textus/handlers/write/move_key.rb +0 -80
  226. data/lib/textus/handlers/write/propose_entry.rb +0 -29
  227. data/lib/textus/handlers/write/put_entry.rb +0 -29
  228. data/lib/textus/handlers/write/reject_proposal.rb +0 -29
  229. data/lib/textus/init.rb +0 -157
  230. data/lib/textus/jobs.rb +0 -9
  231. data/lib/textus/key/distance.rb +0 -55
  232. data/lib/textus/key/grammar.rb +0 -33
  233. data/lib/textus/key/matching.rb +0 -24
  234. data/lib/textus/key/path.rb +0 -28
  235. data/lib/textus/manifest/capabilities.rb +0 -29
  236. data/lib/textus/manifest/data.rb +0 -102
  237. data/lib/textus/manifest/entry/base.rb +0 -105
  238. data/lib/textus/manifest/entry/ignore_matcher.rb +0 -46
  239. data/lib/textus/manifest/entry/leaf.rb +0 -17
  240. data/lib/textus/manifest/entry/nested.rb +0 -37
  241. data/lib/textus/manifest/entry/parser.rb +0 -99
  242. data/lib/textus/manifest/entry/produced.rb +0 -36
  243. data/lib/textus/manifest/entry/publish/mode.rb +0 -45
  244. data/lib/textus/manifest/entry/publish/none.rb +0 -14
  245. data/lib/textus/manifest/entry/publish/subtree_mirror.rb +0 -73
  246. data/lib/textus/manifest/entry/publish/template.rb +0 -16
  247. data/lib/textus/manifest/entry/publish/to_paths.rb +0 -76
  248. data/lib/textus/manifest/entry/publish/tree.rb +0 -50
  249. data/lib/textus/manifest/entry/publish.rb +0 -45
  250. data/lib/textus/manifest/entry/validators/format_matrix.rb +0 -21
  251. data/lib/textus/manifest/entry/validators/ignore.rb +0 -28
  252. data/lib/textus/manifest/entry/validators/publish.rb +0 -30
  253. data/lib/textus/manifest/entry/validators.rb +0 -18
  254. data/lib/textus/manifest/entry.rb +0 -8
  255. data/lib/textus/manifest/policy/matcher.rb +0 -51
  256. data/lib/textus/manifest/policy/predicates/author_held.rb +0 -22
  257. data/lib/textus/manifest/policy/predicates/etag_match.rb +0 -18
  258. data/lib/textus/manifest/policy/predicates/fresh_within.rb +0 -13
  259. data/lib/textus/manifest/policy/predicates/lane_deletable_by.rb +0 -31
  260. data/lib/textus/manifest/policy/predicates/lane_writable_by.rb +0 -23
  261. data/lib/textus/manifest/policy/predicates/raw_lane_ingest_only.rb +0 -25
  262. data/lib/textus/manifest/policy/predicates/raw_write_once.rb +0 -24
  263. data/lib/textus/manifest/policy/predicates/schema_valid.rb +0 -41
  264. data/lib/textus/manifest/policy/predicates/target_is_canon.rb +0 -20
  265. data/lib/textus/manifest/policy/predicates.rb +0 -54
  266. data/lib/textus/manifest/policy/publish_target.rb +0 -34
  267. data/lib/textus/manifest/policy/react.rb +0 -30
  268. data/lib/textus/manifest/policy/retention.rb +0 -29
  269. data/lib/textus/manifest/policy/source.rb +0 -30
  270. data/lib/textus/manifest/policy.rb +0 -95
  271. data/lib/textus/manifest/resolver.rb +0 -118
  272. data/lib/textus/manifest/rules.rb +0 -86
  273. data/lib/textus/manifest/schema/contract.rb +0 -61
  274. data/lib/textus/manifest/schema/keys.rb +0 -90
  275. data/lib/textus/manifest/schema/semantics/cross_field.rb +0 -53
  276. data/lib/textus/manifest/schema/semantics/invariants.rb +0 -125
  277. data/lib/textus/manifest/schema/semantics/migration.rb +0 -83
  278. data/lib/textus/manifest/schema/semantics.rb +0 -27
  279. data/lib/textus/manifest/schema/validator.rb +0 -48
  280. data/lib/textus/manifest/schema/vocabulary.rb +0 -25
  281. data/lib/textus/manifest/schema.rb +0 -30
  282. data/lib/textus/manifest.rb +0 -65
  283. data/lib/textus/orchestration.rb +0 -55
  284. data/lib/textus/port/audit_log.rb +0 -268
  285. data/lib/textus/port/build_lock.rb +0 -64
  286. data/lib/textus/port/clock.rb +0 -10
  287. data/lib/textus/port/publisher.rb +0 -60
  288. data/lib/textus/port/sentinel.rb +0 -25
  289. data/lib/textus/port/sentinel_store.rb +0 -83
  290. data/lib/textus/port/storage/file_stat.rb +0 -19
  291. data/lib/textus/port/storage/file_store.rb +0 -49
  292. data/lib/textus/port/storage/interface.rb +0 -17
  293. data/lib/textus/port/store.rb +0 -149
  294. data/lib/textus/port/watcher_lock.rb +0 -48
  295. data/lib/textus/produce/engine.rb +0 -40
  296. data/lib/textus/produce/publisher.rb +0 -21
  297. data/lib/textus/produce/render.rb +0 -20
  298. data/lib/textus/schema/registry.rb +0 -42
  299. data/lib/textus/schema/tools.rb +0 -108
  300. data/lib/textus/schema.rb +0 -100
  301. data/lib/textus/store/container.rb +0 -173
  302. data/lib/textus/store/cursor.rb +0 -26
  303. data/lib/textus/store/entry/reader.rb +0 -47
  304. data/lib/textus/store/entry/writer.rb +0 -219
  305. data/lib/textus/store/envelope/meta.rb +0 -61
  306. data/lib/textus/store/freshness/drift_detector.rb +0 -93
  307. data/lib/textus/store/freshness/evaluator.rb +0 -20
  308. data/lib/textus/store/freshness/verdict.rb +0 -24
  309. data/lib/textus/store/freshness.rb +0 -8
  310. data/lib/textus/store/index/builder.rb +0 -76
  311. data/lib/textus/store/index/lookup.rb +0 -60
  312. data/lib/textus/store/jobs/base.rb +0 -13
  313. data/lib/textus/store/jobs/index.rb +0 -15
  314. data/lib/textus/store/jobs/materialize.rb +0 -15
  315. data/lib/textus/store/jobs/plan.rb +0 -11
  316. data/lib/textus/store/jobs/planner.rb +0 -124
  317. data/lib/textus/store/jobs/queue.rb +0 -162
  318. data/lib/textus/store/jobs/registry.rb +0 -19
  319. data/lib/textus/store/jobs/retention/base.rb +0 -52
  320. data/lib/textus/store/jobs/retention/sweep.rb +0 -55
  321. data/lib/textus/store/jobs/retention.rb +0 -8
  322. data/lib/textus/store/jobs/sweep.rb +0 -21
  323. data/lib/textus/store/jobs/worker.rb +0 -64
  324. data/lib/textus/store.rb +0 -122
  325. data/lib/textus/surface/cli/verb/doctor.rb +0 -20
  326. data/lib/textus/surface/cli/verb/schema_diff.rb +0 -17
  327. data/lib/textus/surface/cli/verb/schema_init.rb +0 -21
  328. data/lib/textus/surface/cli/verb/schema_migrate.rb +0 -21
  329. data/lib/textus/surface/mcp/projector.rb +0 -20
  330. data/lib/textus/surface/mcp.rb +0 -8
  331. data/lib/textus/value/command.rb +0 -16
  332. data/lib/textus/value/envelope.rb +0 -90
  333. data/lib/textus/value/result.rb +0 -46
  334. data/lib/textus/verb_registry.rb +0 -417
  335. data/lib/textus/workflow/collector.rb +0 -27
  336. data/lib/textus/workflow/context.rb +0 -5
  337. data/lib/textus/workflow/loader.rb +0 -17
  338. data/lib/textus/workflow/pattern.rb +0 -18
@@ -1,28 +0,0 @@
1
- module Textus
2
- module Doctor
3
- class Check
4
- # Surfaces YAML parse failures for files in <store>/schemas/. Without
5
- # this check, malformed schemas are silently skipped by other doctor
6
- # checks (UnownedSchemaFields rescues, Schemas only checks filenames),
7
- # leaving the operator with no signal that a schema is broken.
8
- class SchemaParseError < Check
9
- def call
10
- dir = geometry.schemas_dir
11
- return [] unless File.directory?(dir)
12
-
13
- Dir.glob(File.join(dir, "*.yaml")).each_with_object([]) do |path, out|
14
- Schema.load(path)
15
- rescue StandardError => e
16
- out << {
17
- "code" => "schema.parse_error",
18
- "level" => "error",
19
- "subject" => path,
20
- "message" => "schema failed to parse: #{e.class}: #{e.message}",
21
- "fix" => "fix the YAML at #{path} (check indentation, quoted scalars, and aliases)",
22
- }
23
- end
24
- end
25
- end
26
- end
27
- end
28
- end
@@ -1,28 +0,0 @@
1
- module Textus
2
- module Doctor
3
- class Check
4
- class SchemaViolations < Check
5
- def call
6
- result = Textus::Doctor::Validator.new(
7
- reader: ->(key, ctnr, _c) { Textus::Store::Entry::Reader.from(container: ctnr).read(key) },
8
- manifest: @container.manifest,
9
- audit_log: @container.audit_log,
10
- schema_for: ->(name) { @container.schemas.fetch_or_nil(name) },
11
- ).call(container: @container, call: Textus::Value::Call.build(role: Textus::Value::Role::DEFAULT))
12
-
13
- result["violations"].map do |v|
14
- fix = v["expected"] &&
15
- "field '#{v["field"]}' should be written by '#{v["expected"]}' (last writer: #{v["last_writer"]})"
16
- {
17
- "code" => v["code"],
18
- "level" => "error",
19
- "subject" => v["key"],
20
- "message" => v["message"] || "#{v["code"]} on #{v["key"]}",
21
- "fix" => fix,
22
- }.compact
23
- end
24
- end
25
- end
26
- end
27
- end
28
- end
@@ -1,26 +0,0 @@
1
- module Textus
2
- module Doctor
3
- class Check
4
- class Schemas < Check
5
- def call
6
- out = []
7
- manifest.data.entries.each do |entry|
8
- next if entry.schema.nil?
9
-
10
- sp = geometry.schema_path(entry.schema)
11
- next if File.exist?(sp)
12
-
13
- out << {
14
- "code" => "schema.missing",
15
- "level" => "error",
16
- "subject" => entry.key,
17
- "message" => "schema '#{entry.schema}' not found at #{sp}",
18
- "fix" => "create the schema file or run 'textus schema init #{entry.schema} --from=<key>'",
19
- }
20
- end
21
- out
22
- end
23
- end
24
- end
25
- end
26
- end
@@ -1,55 +0,0 @@
1
- module Textus
2
- module Doctor
3
- class Check
4
- class ScratchpadSources < Check
5
- def call
6
- issues = []
7
- manifest.resolver.enumerate.each do |row|
8
- next unless row[:key].start_with?("scratchpad.notes.")
9
- next unless row[:path] && File.exist?(row[:path])
10
-
11
- sources = parse_sources(row[:path])
12
- sources.each do |raw_key|
13
- next if raw_entry_exists?(raw_key)
14
-
15
- issues << {
16
- "code" => "scratchpad.source_missing",
17
- "level" => "warning",
18
- "subject" => row[:key],
19
- "message" => "scratchpad entry '#{row[:key]}' references raw key '#{raw_key}' " \
20
- "which does not exist in the store",
21
- "fix" => "re-ingest the source: textus ingest ..., or remove the stale sources: entry",
22
- }
23
- end
24
- end
25
- issues
26
- end
27
-
28
- private
29
-
30
- def parse_sources(path)
31
- content = File.read(path)
32
- match = content.match(/\A---\n(.*?)\n---/m)
33
- return [] unless match
34
-
35
- front = YAML.safe_load(match[1])
36
- Array(front&.dig("sources")).filter_map do |s|
37
- case s
38
- when String then s
39
- when Hash then s["key"]
40
- end
41
- end
42
- rescue StandardError
43
- []
44
- end
45
-
46
- def raw_entry_exists?(key)
47
- path = manifest.resolver.resolve(key).path
48
- File.exist?(path)
49
- rescue Textus::UnknownKey, Textus::Error
50
- false
51
- end
52
- end
53
- end
54
- end
55
- end
@@ -1,60 +0,0 @@
1
- module Textus
2
- module Doctor
3
- class Check
4
- class Sentinels < Check
5
- def call
6
- store = Textus::Port::SentinelStore.new
7
- file_stat = Textus::Port::Storage::FileStat.new
8
- dir = Textus::Store::Layout.new(root).sentinels_root
9
- return [] unless file_stat.directory?(dir)
10
-
11
- repo_root = File.dirname(root)
12
- file_stat.glob(File.join(dir, "**", "*#{Textus::Port::SentinelStore::SUFFIX}")).flat_map do |sentinel_path|
13
- inspect_sentinel(sentinel_path, repo_root, store, file_stat)
14
- end
15
- end
16
-
17
- private
18
-
19
- def inspect_sentinel(sentinel_path, repo_root, store, file_stat)
20
- sentinel = store.load(sentinel_path, repo_root)
21
- return [parse_error_issue(sentinel_path)] if sentinel.nil?
22
- return [orphan_issue(sentinel_path, sentinel)] if sentinel.orphan?(file_stat)
23
- return [drift_issue(sentinel)] if sentinel.drift?(file_stat)
24
-
25
- []
26
- end
27
-
28
- def parse_error_issue(sentinel_path)
29
- {
30
- "code" => "sentinel.parse_error",
31
- "level" => "warning",
32
- "subject" => sentinel_path,
33
- "message" => "sentinel is not valid JSON",
34
- "fix" => "delete #{sentinel_path} and re-run 'textus drain' to regenerate",
35
- }
36
- end
37
-
38
- def orphan_issue(sentinel_path, sentinel)
39
- {
40
- "code" => "sentinel.orphan",
41
- "level" => "warning",
42
- "subject" => sentinel_path,
43
- "message" => "sentinel target #{sentinel.target.inspect} no longer exists",
44
- "fix" => "delete #{sentinel_path} (the published file is gone) or restore the target",
45
- }
46
- end
47
-
48
- def drift_issue(sentinel)
49
- {
50
- "code" => "sentinel.drift",
51
- "level" => "warning",
52
- "subject" => sentinel.target,
53
- "message" => "published file at #{sentinel.target} was modified out-of-band",
54
- "fix" => "re-run 'textus drain' to overwrite, or copy the manual edit back into the store source",
55
- }
56
- end
57
- end
58
- end
59
- end
60
- end
@@ -1,54 +0,0 @@
1
- module Textus
2
- module Doctor
3
- class Check
4
- # Warns when a knowledge-lane doc's `**reviewed** YYYY-MM (vX.Y)` stamp
5
- # is more than MINOR_THRESHOLD minor versions behind Textus::VERSION.
6
- # The stamp is the human staleness signal declared in docs conventions
7
- # (knowledge.docs-index); this check makes it machine-enforced.
8
- class StaleReviewedStamp < Check
9
- STAMP_RE = /\*\*reviewed\*\*\s+\d{4}-\d{2}\s+\(v(\d+\.\d+(?:\.\d+)?)\)/
10
- MINOR_THRESHOLD = 5
11
-
12
- def call
13
- current_minor = parse_minor(Textus::VERSION)
14
- issues = []
15
-
16
- manifest.resolver.enumerate.each do |row|
17
- next unless row[:key].to_s.start_with?("knowledge.")
18
- next unless row[:path] && File.file?(row[:path])
19
-
20
- body = File.read(row[:path])
21
- m = body.match(STAMP_RE)
22
- next unless m
23
-
24
- reviewed_minor = parse_minor(m[1])
25
- behind = current_minor - reviewed_minor
26
- next unless behind > MINOR_THRESHOLD
27
-
28
- issues << stale_issue(row[:key], m[1], behind)
29
- end
30
-
31
- issues
32
- end
33
-
34
- private
35
-
36
- def parse_minor(version_str)
37
- version_str.sub(/\Av/, "").split(".").map(&:to_i)[1] || 0
38
- end
39
-
40
- def stale_issue(key, stamp_version, behind)
41
- current_short = Textus::VERSION[/\A\d+\.\d+/]
42
- {
43
- "code" => "stale_reviewed_stamp",
44
- "level" => "warning",
45
- "subject" => key.to_s,
46
- "message" => "reviewed at v#{stamp_version}; current is v#{Textus::VERSION} " \
47
- "(#{behind} minor versions behind, threshold is #{MINOR_THRESHOLD})",
48
- "fix" => "review the doc and update the stamp to: **reviewed** YYYY-MM (v#{current_short})",
49
- }
50
- end
51
- end
52
- end
53
- end
54
- end
@@ -1,29 +0,0 @@
1
- module Textus
2
- module Doctor
3
- class Check
4
- class Templates < Check
5
- def call
6
- out = []
7
- manifest.data.entries.each do |entry|
8
- entry.publish_targets.each do |target|
9
- template = target.template
10
- next if template.nil?
11
-
12
- tp = geometry.template_path(template)
13
- next if File.exist?(tp)
14
-
15
- out << {
16
- "code" => "template.missing",
17
- "level" => "error",
18
- "subject" => entry.key,
19
- "message" => "template '#{template}' not found at #{tp}",
20
- "fix" => "create the file at #{tp} or update the publish target's template: field",
21
- }
22
- end
23
- end
24
- out
25
- end
26
- end
27
- end
28
- end
29
- end
@@ -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,43 +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.layout
30
- def manifest = @container.manifest
31
-
32
- def dispatch(verb, *args, **kwargs)
33
- contract_class = Textus::VerbRegistry.contract_class_for(verb)
34
- members = contract_class.members
35
- command_kwargs = members.each_with_index.to_h { |m, i| [m, args[i] || kwargs[m]] }
36
- command = contract_class.new(**command_kwargs)
37
- call = Textus::Value::Call.build(role: @role)
38
- result = @container.pipeline.dispatch(command, call: call)
39
- Textus::Value::Result.extract(result)
40
- end
41
- end
42
- end
43
- 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::ScratchpadSources,
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,77 +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
- ext = File.extname(path)
30
- if nested
31
- return if ext == ""
32
-
33
- raise UsageError.new("#{format_name} nested path must not have an extension")
34
- end
35
-
36
- return if extensions.include?(ext)
37
-
38
- raise UsageError.new("#{format_name} format requires '#{extensions.join("' or '")}' path (got #{ext.inspect})")
39
- end
40
-
41
- def self.enforce_name_match!(path, meta)
42
- return unless meta.is_a?(Hash) && meta["name"]
43
-
44
- ext = extensions.first
45
- basename = File.basename(path, ext)
46
- return if meta["name"] == basename
47
-
48
- raise BadFrontmatter.new(path, "name '#{meta["name"]}' does not match basename '#{basename}'")
49
- end
50
-
51
- def self.rewrite_name(path, basename) # rubocop:disable Naming/PredicateMethod
52
- raw = File.binread(path)
53
- parsed = parse(raw, path: path)
54
- meta = parsed["_meta"] || {}
55
- return false unless meta.is_a?(Hash) && meta["name"].is_a?(String) && meta["name"] != basename
56
-
57
- new_meta = meta.merge("name" => basename)
58
- File.binwrite(path, serialize(meta: new_meta, body: parsed["body"] || "", content: parsed["content"]))
59
- true
60
- end
61
-
62
- def self.format_name
63
- name.split("::").last.downcase
64
- end
65
-
66
- def self.validate_raw_entry!(_parsed, _lane); end
67
-
68
- def self.serialize_for_put(meta:, body:, content:, path:)
69
- _ = meta
70
- _ = body
71
- _ = content
72
- _ = path
73
- raise NotImplementedError.new("#{name}.serialize_for_put not implemented")
74
- end
75
- end
76
- end
77
- end
@@ -1,81 +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.validate_path_extension(path, nested)
63
- ext = File.extname(path)
64
- if nested
65
- return if ext == ""
66
-
67
- raise UsageError.new("nested json path must not have an extension")
68
- end
69
-
70
- return if ext == ".json"
71
-
72
- raise UsageError.new("json format requires '.json' path (got #{ext.inspect})")
73
- end
74
-
75
- def self.data_to_payload(data)
76
- data = data.transform_keys(&:to_s) if data.is_a?(Hash)
77
- { meta: data["_meta"] || {}, body: nil, content: data["content"] || data }
78
- end
79
- end
80
- end
81
- end