textus 0.43.2 → 0.45.1

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 (88) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +57 -0
  3. data/README.md +3 -3
  4. data/SPEC.md +15 -13
  5. data/docs/architecture/README.md +28 -9
  6. data/docs/reference/conventions.md +8 -9
  7. data/lib/textus/boot.rb +3 -4
  8. data/lib/textus/cli/group/fetch.rb +2 -2
  9. data/lib/textus/cli/group.rb +1 -0
  10. data/lib/textus/cli/runner.rb +187 -0
  11. data/lib/textus/cli/verb/build.rb +4 -4
  12. data/lib/textus/cli/verb/{fetch_stale.rb → fetch_all.rb} +2 -2
  13. data/lib/textus/cli/verb/get.rb +6 -5
  14. data/lib/textus/cli/verb/put.rb +3 -3
  15. data/lib/textus/cli/verb.rb +3 -0
  16. data/lib/textus/cli.rb +8 -2
  17. data/lib/textus/contract/around.rb +29 -0
  18. data/lib/textus/contract/binder.rb +88 -0
  19. data/lib/textus/contract/resources/cursor.rb +26 -0
  20. data/lib/textus/contract/sources.rb +39 -0
  21. data/lib/textus/contract/view.rb +15 -0
  22. data/lib/textus/contract.rb +68 -8
  23. data/lib/textus/dispatcher.rb +5 -6
  24. data/lib/textus/hooks/context.rb +24 -2
  25. data/lib/textus/maintenance/key_delete_prefix.rb +6 -4
  26. data/lib/textus/maintenance/key_mv_prefix.rb +7 -5
  27. data/lib/textus/maintenance/migrate.rb +12 -9
  28. data/lib/textus/maintenance/rule_lint.rb +4 -3
  29. data/lib/textus/maintenance/zone_mv.rb +7 -5
  30. data/lib/textus/manifest/entry/base.rb +1 -1
  31. data/lib/textus/mcp/catalog.rb +6 -33
  32. data/lib/textus/projection.rb +2 -2
  33. data/lib/textus/read/audit.rb +19 -0
  34. data/lib/textus/read/blame.rb +11 -1
  35. data/lib/textus/read/deps.rb +15 -1
  36. data/lib/textus/read/doctor.rb +8 -0
  37. data/lib/textus/read/freshness.rb +10 -0
  38. data/lib/textus/read/get.rb +86 -21
  39. data/lib/textus/read/list.rb +1 -0
  40. data/lib/textus/read/published.rb +7 -0
  41. data/lib/textus/read/pulse.rb +1 -0
  42. data/lib/textus/read/rdeps.rb +14 -0
  43. data/lib/textus/read/rule_explain.rb +84 -0
  44. data/lib/textus/read/rule_list.rb +39 -0
  45. data/lib/textus/read/schema_envelope.rb +3 -2
  46. data/lib/textus/read/uid.rb +9 -0
  47. data/lib/textus/read/where.rb +8 -0
  48. data/lib/textus/role_scope.rb +34 -6
  49. data/lib/textus/schema/tools.rb +12 -3
  50. data/lib/textus/version.rb +1 -1
  51. data/lib/textus/write/accept.rb +8 -0
  52. data/lib/textus/write/{publish.rb → build.rb} +16 -7
  53. data/lib/textus/write/delete.rb +13 -0
  54. data/lib/textus/write/fetch_all.rb +1 -0
  55. data/lib/textus/write/fetch_orchestrator.rb +1 -1
  56. data/lib/textus/write/fetch_worker.rb +1 -1
  57. data/lib/textus/write/mv.rb +16 -0
  58. data/lib/textus/write/propose.rb +7 -2
  59. data/lib/textus/write/put.rb +2 -2
  60. data/lib/textus/write/reject.rb +8 -0
  61. data/lib/textus/write/retention_sweep.rb +9 -0
  62. metadata +11 -29
  63. data/lib/textus/cli/verb/accept.rb +0 -16
  64. data/lib/textus/cli/verb/audit.rb +0 -34
  65. data/lib/textus/cli/verb/blame.rb +0 -17
  66. data/lib/textus/cli/verb/delete.rb +0 -17
  67. data/lib/textus/cli/verb/deps.rb +0 -14
  68. data/lib/textus/cli/verb/freshness.rb +0 -17
  69. data/lib/textus/cli/verb/key_delete.rb +0 -24
  70. data/lib/textus/cli/verb/list.rb +0 -16
  71. data/lib/textus/cli/verb/migrate.rb +0 -18
  72. data/lib/textus/cli/verb/mv.rb +0 -27
  73. data/lib/textus/cli/verb/propose.rb +0 -28
  74. data/lib/textus/cli/verb/published.rb +0 -13
  75. data/lib/textus/cli/verb/pulse.rb +0 -26
  76. data/lib/textus/cli/verb/rdeps.rb +0 -14
  77. data/lib/textus/cli/verb/reject.rb +0 -16
  78. data/lib/textus/cli/verb/retain.rb +0 -19
  79. data/lib/textus/cli/verb/rule_explain.rb +0 -16
  80. data/lib/textus/cli/verb/rule_lint.rb +0 -18
  81. data/lib/textus/cli/verb/rule_list.rb +0 -29
  82. data/lib/textus/cli/verb/schema.rb +0 -15
  83. data/lib/textus/cli/verb/uid.rb +0 -15
  84. data/lib/textus/cli/verb/where.rb +0 -14
  85. data/lib/textus/cli/verb/zone_mv.rb +0 -19
  86. data/lib/textus/read/get_or_fetch.rb +0 -69
  87. data/lib/textus/read/policy_explain.rb +0 -46
  88. data/lib/textus/read/rules.rb +0 -25
@@ -1,15 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- class Uid < Verb
5
- command_name "uid"
6
- parent_group Group::Key
7
-
8
- def call(store)
9
- key = positional.shift or raise UsageError.new("uid requires a key")
10
- emit({ "key" => key, "uid" => session_for(store).uid(key) })
11
- end
12
- end
13
- end
14
- end
15
- end
@@ -1,14 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- class Where < Verb
5
- command_name "where"
6
-
7
- def call(store)
8
- key = positional.shift or raise UsageError.new("where requires a key")
9
- emit(session_for(store).where(key))
10
- end
11
- end
12
- end
13
- end
14
- end
@@ -1,19 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- class ZoneMv < Verb
5
- command_name "mv"
6
- parent_group Group::Zone
7
-
8
- option :as_flag, "--as=ROLE"
9
- option :dry_run, "--dry-run"
10
-
11
- def call(store)
12
- from = positional.shift or raise UsageError.new("zone mv requires <from> <to>")
13
- to = positional.shift or raise UsageError.new("zone mv requires <from> <to>")
14
- emit(session_for(store).zone_mv(from: from, to: to, dry_run: dry_run || false).to_h)
15
- end
16
- end
17
- end
18
- end
19
- end
@@ -1,69 +0,0 @@
1
- module Textus
2
- module Read
3
- # Composes pure `Read::Get` with the fetch orchestrator: runs Get
4
- # to obtain the envelope and freshness verdict, then if the verdict
5
- # is stale and the rule's `on_stale` policy demands action, hands
6
- # off to the orchestrator. Use for interactive reads where the
7
- # caller wants the freshest obtainable envelope.
8
- #
9
- # Pure reads (build, projection, schema tooling) should use
10
- # `Read::Get` directly; it has no orchestrator dependency.
11
- class GetOrFetch
12
- def initialize(container:, call:, get: nil, orchestrator: nil)
13
- @container = container
14
- @call = call
15
- @manifest = container.manifest
16
- @get = get || Read::Get.new(container: container, call: call)
17
- @orchestrator = orchestrator || build_orchestrator
18
- end
19
-
20
- private
21
-
22
- def hook_context
23
- @hook_context ||= Textus::Hooks::Context.for(container: @container, call: @call)
24
- end
25
-
26
- def build_orchestrator
27
- worker = Textus::Write::FetchWorker.new(
28
- container: @container, call: @call,
29
- )
30
- Textus::Write::FetchOrchestrator.new(
31
- worker: worker, store_root: @container.root, events: @container.events,
32
- hook_context: hook_context
33
- )
34
- end
35
-
36
- public
37
-
38
- def call(key)
39
- envelope = @get.call(key)
40
- return nil if envelope.nil?
41
- return envelope unless envelope.freshness&.stale
42
-
43
- policy_set = @manifest.rules.for(key)
44
- fetch_policy = policy_set.fetch
45
- return envelope if fetch_policy.nil?
46
-
47
- policy = fetch_policy.to_freshness_policy
48
- verdict = Textus::Domain::Freshness::Verdict.stale(envelope.freshness.reason)
49
- action = policy.decide(verdict)
50
- outcome = @orchestrator.execute(action, key: key)
51
-
52
- case outcome
53
- when Textus::Domain::Outcome::Skipped
54
- envelope
55
- when Textus::Domain::Outcome::Fetched
56
- outcome.envelope.with(
57
- freshness: Textus::Domain::Freshness.build(stale: false, reason: nil, fetching: false),
58
- )
59
- when Textus::Domain::Outcome::Detached
60
- envelope.with(freshness: envelope.freshness.with(fetching: true))
61
- when Textus::Domain::Outcome::Failed
62
- envelope.with(
63
- freshness: envelope.freshness.with(fetch_error: outcome.error.message),
64
- )
65
- end
66
- end
67
- end
68
- end
69
- end
@@ -1,46 +0,0 @@
1
- module Textus
2
- module Read
3
- # For one key, surface every matching policy block along with the
4
- # per-slot effective value (which loses ties win-by-specificity) and the
5
- # effective guard predicate names for every write transition (ADR 0031).
6
- class PolicyExplain
7
- def initialize(container:, call: nil) # rubocop:disable Lint/UnusedMethodArgument
8
- @manifest = container.manifest
9
- @schemas = container.schemas
10
- end
11
-
12
- def call(key:)
13
- matching = @manifest.rules.explain(key)
14
- winners = @manifest.rules.for(key)
15
- factory = Textus::Domain::Policy::GuardFactory.new(manifest: @manifest, schemas: @schemas)
16
-
17
- {
18
- key: key,
19
- matched_blocks: matching.map do |b|
20
- {
21
- match: b.match,
22
- fetch: !b.fetch.nil?,
23
- handler_allowlist: !b.handler_allowlist.nil?,
24
- guard: !b.guard.nil?,
25
- retention: !b.retention.nil?,
26
- }
27
- end,
28
- effective: {
29
- fetch: winners.fetch && {
30
- ttl_seconds: winners.fetch.ttl_seconds,
31
- on_stale: winners.fetch.on_stale,
32
- },
33
- handler_allowlist: winners.handler_allowlist&.handlers,
34
- retention: winners.retention && {
35
- expire_after: winners.retention.expire_after,
36
- archive_after: winners.retention.archive_after,
37
- },
38
- },
39
- guards: Textus::Domain::Policy::BaseGuards::BASE.keys.to_h do |transition|
40
- [transition, factory.for(transition, key).predicates.map(&:name)]
41
- end,
42
- }
43
- end
44
- end
45
- end
46
- end
@@ -1,25 +0,0 @@
1
- module Textus
2
- module Read
3
- # Effective rule set (fetch + guard) for a key. Was the inlined MCP
4
- # `rules` tool; promoted to a first-class verb so MCP is a pure projection
5
- # (ADR 0039).
6
- class Rules
7
- extend Textus::Contract::DSL
8
-
9
- verb :rules
10
- summary "Return effective rules for a key (fetch, guard, ...)."
11
- surfaces :ruby, :mcp
12
- arg :key, String, required: true, positional: true,
13
- description: "dotted key whose effective rules you want (fetch ttl/action, write guard, ...)"
14
-
15
- def initialize(container:, call: nil) # rubocop:disable Lint/UnusedMethodArgument
16
- @manifest = container.manifest
17
- end
18
-
19
- def call(key)
20
- set = @manifest.rules.for(key)
21
- { "fetch" => set.fetch&.to_h, "guard" => set.guard }.compact
22
- end
23
- end
24
- end
25
- end