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,29 +0,0 @@
1
- module Textus
2
- module Doctor
3
- class Check
4
- # ADR 0079: generator/build drift — a derived/external entry whose sources
5
- # changed since its generated.at. Dependency-based (not age-based), so it
6
- # stays OUT of the lifecycle/freshness unification and lives here as a
7
- # health signal. This is the surviving home for what the removed `stale`
8
- # verb reported.
9
- class GeneratorDrift < Check
10
- def call
11
- gen = Textus::Core::Freshness::Evaluator.new(
12
- manifest: manifest,
13
- file_stat: Textus::Port::Storage::FileStat.new,
14
- clock: Textus::Port::Clock.new,
15
- )
16
- manifest.data.entries.flat_map { |m| gen.drift_rows(m) }.map do |row|
17
- {
18
- "code" => "generator_drift",
19
- "level" => "warning",
20
- "subject" => row["key"],
21
- "message" => row["reason"],
22
- "fix" => "rematerialize the entry: `textus drain`",
23
- }
24
- end
25
- end
26
- end
27
- end
28
- end
29
- end
@@ -1,60 +0,0 @@
1
- module Textus
2
- module Doctor
3
- class Check
4
- class IllegalKeys < Check
5
- def call
6
- out = []
7
- manifest.data.entries.each do |entry|
8
- next unless entry.nested?
9
- next if entry.publish_mode.keyless? # publish_tree files are opaque payload, never keys (ADR 0047)
10
-
11
- base = File.join(root, Textus::Key::Path.normalize_relative_path(entry.path))
12
- next unless File.directory?(base)
13
-
14
- check_all_paths(entry, base, out)
15
- end
16
- out
17
- end
18
-
19
- private
20
-
21
- def check_all_paths(entry, base, out)
22
- walk_nested(base) do |abs_path, is_dir|
23
- rel = abs_path.sub(%r{\A#{Regexp.escape(base)}/?}, "")
24
- next if entry.ignored?(rel)
25
-
26
- basename = File.basename(abs_path)
27
- stem = is_dir ? basename : basename.sub(/#{Regexp.escape(File.extname(basename))}\z/, "")
28
- next if stem.match?(Key::Grammar::SEGMENT)
29
-
30
- out << issue(abs_path, stem)
31
- end
32
- end
33
-
34
- def issue(abs_path, stem)
35
- {
36
- "code" => "key.illegal",
37
- "level" => "error",
38
- "subject" => abs_path,
39
- "path" => abs_path,
40
- "message" => "illegal key segment '#{stem}' at #{abs_path}",
41
- "fix" => "rename the file/directory so each segment matches [a-z0-9][a-z0-9-]* " \
42
- "(lowercase, digits, hyphens)",
43
- }
44
- end
45
-
46
- def walk_nested(root, &block)
47
- Dir.each_child(root) do |name|
48
- abs = File.join(root, name)
49
- if File.directory?(abs)
50
- walk_nested(abs, &block)
51
- yield abs, true
52
- else
53
- yield abs, false
54
- end
55
- end
56
- end
57
- end
58
- end
59
- end
60
- end
@@ -1,25 +0,0 @@
1
- module Textus
2
- module Doctor
3
- class Check
4
- class ManifestFiles < Check
5
- def call
6
- manifest.data.entries.each_with_object([]) do |entry, out|
7
- next if entry.nested?
8
-
9
- path = Textus::Key::Path.resolve(manifest.data, entry)
10
- next if File.exist?(path)
11
-
12
- out << {
13
- "code" => "manifest.missing_file",
14
- "level" => "info",
15
- "subject" => entry.key,
16
- "message" => "declared entry has no file on disk at #{path}",
17
- "fix" => "create the entry with 'textus put #{entry.key} --stdin --as=<role>' " \
18
- "(or leave empty if not yet authored)",
19
- }
20
- end
21
- end
22
- end
23
- end
24
- end
25
- end
@@ -1,50 +0,0 @@
1
- module Textus
2
- module Doctor
3
- class Check
4
- class NotebookSources < Check
5
- def call
6
- issues = []
7
- manifest.resolver.enumerate.each do |row|
8
- next unless row[:key].start_with?("notebook.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" => "notebook.source_missing",
17
- "level" => "warning",
18
- "subject" => row[:key],
19
- "message" => "notebook 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"))
37
- rescue StandardError
38
- []
39
- end
40
-
41
- def raw_entry_exists?(key)
42
- path = manifest.resolver.resolve(key).path
43
- File.exist?(path)
44
- rescue Textus::UnknownKey, Textus::Error
45
- false
46
- end
47
- end
48
- end
49
- end
50
- end
@@ -1,35 +0,0 @@
1
- module Textus
2
- module Doctor
3
- class Check
4
- # Flags published files whose recorded source no longer exists in the
5
- # store. Per-leaf prune (ADR 0046) reconciles within a still-present
6
- # leaf; a renamed or removed *whole* leaf orphans its entire target
7
- # directory, which a per-entry build won't revisit. This check catches
8
- # that drift without making `build` scan globally.
9
- class OrphanedPublishTargets < Check
10
- def call
11
- sdir = Textus::Store::Geometry.new(root).sentinels_root
12
- return [] unless File.directory?(sdir)
13
-
14
- repo_root = File.dirname(root)
15
- store = Textus::Port::SentinelStore.new
16
- glob = File.join(sdir, "**", "*#{Textus::Port::SentinelStore::SUFFIX}")
17
- Dir.glob(glob).filter_map do |spath|
18
- sentinel = store.load(spath, repo_root)
19
- next nil if sentinel.nil? || sentinel.source.nil?
20
- next nil if File.exist?(sentinel.source)
21
-
22
- {
23
- "code" => "publish.orphaned_target",
24
- "level" => "warning",
25
- "subject" => sentinel.target,
26
- "message" => "published file #{sentinel.target} has no source in the store " \
27
- "(recorded source #{sentinel.source} is gone) — likely a renamed or removed leaf",
28
- "fix" => "remove the stale copy and its sentinel: rm '#{sentinel.target}' '#{spath}'",
29
- }
30
- end
31
- end
32
- end
33
- end
34
- end
35
- end
@@ -1,45 +0,0 @@
1
- module Textus
2
- module Doctor
3
- class Check
4
- # Flags pending proposals whose `proposal.target_key` cannot ever be
5
- # accepted: it points at a non-canon zone or resolves to no declared
6
- # entry (ADR 0035). Reads the live queue zone; silent when there is no
7
- # queue zone. Warnings, not errors — they are stale junk, not store
8
- # corruption (the accept gate already refuses them).
9
- class ProposalTargets < Check
10
- def call
11
- queue = manifest.policy.queue_lane
12
- return [] unless queue
13
-
14
- dispatch(:list, lane: queue).filter_map { |row| issue_for(row["key"]) }
15
- end
16
-
17
- private
18
-
19
- def issue_for(key)
20
- target = dispatch(:get, key).meta&.dig("proposal", "target_key")
21
- return nil if target.nil? # not a proposal entry — skip
22
-
23
- zone = manifest.resolver.resolve(target).entry.lane
24
- return nil if manifest.policy.declared_kind(zone.to_s) == :canon
25
-
26
- {
27
- "code" => "proposal.target_not_canon",
28
- "level" => "warning",
29
- "subject" => key,
30
- "message" => "proposal '#{key}' targets '#{target}' in zone '#{zone}' (not canon); it can never be accepted",
31
- "fix" => "delete the proposal, or repoint target_key at a canon zone",
32
- }
33
- rescue Textus::UnknownKey
34
- {
35
- "code" => "proposal.target_unresolved",
36
- "level" => "warning",
37
- "subject" => key,
38
- "message" => "proposal '#{key}' targets '#{target}', which resolves to no declared entry",
39
- "fix" => "delete the proposal, or fix target_key",
40
- }
41
- end
42
- end
43
- end
44
- end
45
- end
@@ -1,47 +0,0 @@
1
- require "yaml"
2
-
3
- module Textus
4
- module Doctor
5
- class Check
6
- # Runs as a standalone module (Check::ProtocolVersion.run(root:)) and also
7
- # as a class-based doctor check (ProtocolVersion.new(store).call).
8
- class ProtocolVersion < Check
9
- # Standalone interface: root is the project root (parent of .textus/).
10
- def self.run(root:)
11
- path = File.join(root, ".textus", "manifest.yaml")
12
- return [] unless File.exist?(path)
13
-
14
- doc = YAML.safe_load_file(path, aliases: false) || {}
15
- version = doc["version"]
16
- return [] if version == "textus/4"
17
-
18
- [{
19
- "code" => "protocol_mismatch",
20
- "severity" => "error",
21
- "message" => "Store reports version=#{version.inspect}; this gem expects textus/4.",
22
- "hint" => "Upgrade the store's manifest version to textus/4 (see CHANGELOG for breaking changes).",
23
- }]
24
- end
25
-
26
- # Doctor check interface: root is the .textus/ directory itself,
27
- # so manifest.yaml lives directly inside it.
28
- def call
29
- path = geometry.manifest_path
30
- return [] unless File.exist?(path)
31
-
32
- doc = YAML.safe_load_file(path, aliases: false) || {}
33
- version = doc["version"]
34
- return [] if version == "textus/4"
35
-
36
- [{
37
- "code" => "protocol_mismatch",
38
- "level" => "error",
39
- "subject" => path,
40
- "message" => "Store reports version=#{version.inspect}; this gem expects textus/4.",
41
- "fix" => "Upgrade the store's manifest version to textus/4 (see CHANGELOG for breaking changes).",
42
- }]
43
- end
44
- end
45
- end
46
- end
47
- end
@@ -1,48 +0,0 @@
1
- module Textus
2
- module Doctor
3
- class Check
4
- # ADR 0047 Decision 4. A publish_tree entry prunes its WHOLE target dir on
5
- # every build. If a derived entry's publish_to writes a file into that same
6
- # dir, the tree's prune will delete it unless the tree `ignore`s that
7
- # filename. Warn so the author adds the ignore before prune eats the index.
8
- class PublishTreeIndexOverlap < Check
9
- def call
10
- entries = manifest.data.entries
11
- trees = entries.select { |e| e.nested? && e.publish_tree }
12
- return [] if trees.empty?
13
-
14
- derived_targets = entries.flat_map do |e|
15
- Array(e.publish_to).map { |rel| [e, rel] }
16
- end
17
-
18
- trees.flat_map do |tree|
19
- target_prefix = "#{tree.publish_tree.chomp("/")}/"
20
- derived_targets.filter_map do |(derived, rel)|
21
- next nil unless rel.start_with?(target_prefix)
22
-
23
- rel_to_target = rel.delete_prefix(target_prefix)
24
- next nil if tree.ignored?(rel_to_target)
25
-
26
- issue(tree, derived, rel, rel_to_target)
27
- end
28
- end
29
- end
30
-
31
- private
32
-
33
- def issue(tree, derived, rel, rel_to_target)
34
- basename = File.basename(rel_to_target)
35
- {
36
- "code" => "publish.tree_index_overlap",
37
- "level" => "warning",
38
- "subject" => tree.key,
39
- "message" => "publish_tree '#{tree.publish_tree}' overlaps derived entry " \
40
- "'#{derived.key}' publish_to '#{rel}'; the tree's prune will delete it on rebuild",
41
- "fix" => "add a glob covering '#{rel_to_target}' to entry '#{tree.key}' ignore " \
42
- "(e.g. ignore: [\"**/#{basename}\"])",
43
- }
44
- end
45
- end
46
- end
47
- end
48
- end
@@ -1,50 +0,0 @@
1
- module Textus
2
- module Doctor
3
- class Check
4
- class RawAssetPaths < Check
5
- def call
6
- raw_lane = manifest.policy.lanes_of_kind(:raw).first
7
- return [] unless raw_lane
8
-
9
- issues = []
10
- manifest.resolver.enumerate.each do |row|
11
- next unless row[:key].start_with?("raw.")
12
- next unless row[:path] && File.exist?(row[:path])
13
-
14
- raw_content = load_content(row[:path])
15
- next unless raw_content.is_a?(Hash)
16
-
17
- asset = raw_content["asset"]
18
- next unless asset.is_a?(String) && !asset.empty?
19
-
20
- asset_path = find_asset_path(asset)
21
- next if File.exist?(asset_path)
22
-
23
- issues << {
24
- "code" => "raw_asset.missing_file",
25
- "level" => "error",
26
- "subject" => row[:key],
27
- "message" => "raw entry '#{row[:key]}' references asset '#{asset}' " \
28
- "which does not exist at #{asset_path}",
29
- "fix" => "re-ingest the asset: textus key-delete #{row[:key]}, then textus ingest",
30
- }
31
- end
32
- issues
33
- end
34
-
35
- private
36
-
37
- def load_content(path)
38
- require "yaml"
39
- YAML.safe_load_file(path)
40
- rescue StandardError
41
- nil
42
- end
43
-
44
- def find_asset_path(asset_rel)
45
- File.join(geometry.root, "assets", asset_rel)
46
- end
47
- end
48
- end
49
- end
50
- end
@@ -1,49 +0,0 @@
1
- module Textus
2
- module Doctor
3
- class Check
4
- # Flags entries whose key is matched by two or more rule blocks of the
5
- # SAME specificity in the same slot (lifecycle / handler_permit /
6
- # guard / materialize). Ties are non-deterministic in the parser's pick step, so
7
- # they're a configuration smell — surface them.
8
- class RuleAmbiguity < Check
9
- SLOTS = Textus::Manifest::Schema::FIELD_REGISTRY.select { |_, m| m[:in_ambiguity] }.keys.freeze
10
-
11
- def call
12
- out = []
13
- rules = manifest.rules
14
- manifest.data.entries.each do |mentry|
15
- matches = rules.explain(mentry.key)
16
- next if matches.length < 2
17
-
18
- SLOTS.each { |slot| out.concat(ambiguities_for(mentry, slot, matches)) }
19
- end
20
- out
21
- end
22
-
23
- private
24
-
25
- def ambiguities_for(mentry, slot, matches)
26
- carriers = matches.select { |b| b.public_send(slot) }
27
- return [] if carriers.length < 2
28
-
29
- by_specificity = carriers.group_by { |b| Textus::Manifest::Policy::Matcher.specificity(b.match) }
30
- tied = by_specificity.values.select { |group| group.length > 1 }
31
- tied.map { |group| issue_for(mentry, slot, group) }
32
- end
33
-
34
- def issue_for(mentry, slot, group)
35
- globs = group.map(&:match).sort
36
- {
37
- "code" => "rule.ambiguity",
38
- "level" => "warning",
39
- "subject" => mentry.key,
40
- "message" => "entry '#{mentry.key}' matches #{group.length} rule blocks at the same " \
41
- "specificity for #{slot}: #{globs.join(", ")}",
42
- "fix" => "narrow one of the conflicting match: globs in .textus/manifest.yaml so a single " \
43
- "block wins for this key",
44
- }
45
- end
46
- end
47
- end
48
- end
49
- end
@@ -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) { Value::Result.unwrap(Textus::Action::Get.call(container: ctnr, call: c, key: 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,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::Geometry.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