textus 0.43.1 → 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 +70 -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 +10 -7
  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 +78 -10
  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 -8
  28. data/lib/textus/maintenance/rule_lint.rb +4 -2
  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 +17 -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 +88 -22
  39. data/lib/textus/read/list.rb +3 -2
  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 +5 -3
  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 +3 -2
  55. data/lib/textus/write/fetch_orchestrator.rb +1 -1
  56. data/lib/textus/write/fetch_worker.rb +3 -2
  57. data/lib/textus/write/mv.rb +16 -0
  58. data/lib/textus/write/propose.rb +12 -4
  59. data/lib/textus/write/put.rb +11 -6
  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 -24
@@ -1,16 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- class Reject < Verb
5
- command_name "reject"
6
-
7
- option :as_flag, "--as=ROLE"
8
-
9
- def call(store)
10
- key = positional.shift or raise UsageError.new("reject requires a key")
11
- emit(session_for(store).reject(key))
12
- end
13
- end
14
- end
15
- end
16
- end
@@ -1,19 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- class Retain < Verb
5
- command_name "retain"
6
-
7
- option :prefix, "--prefix=KEY"
8
- option :zone, "--zone=Z"
9
- option :as_flag, "--as=ROLE"
10
-
11
- def call(store)
12
- result = session_for(store).retention_sweep(prefix: prefix, zone: zone)
13
- emit(result)
14
- result["ok"] ? 0 : 1
15
- end
16
- end
17
- end
18
- end
19
- end
@@ -1,16 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- class RuleExplain < Verb
5
- command_name "explain"
6
- parent_group Group::Rule
7
-
8
- def call(store)
9
- key = positional.shift or raise UsageError.new("policy explain requires a KEY")
10
- result = session_for(store).policy_explain(key: key)
11
- emit({ "verb" => "policy_explain" }.merge(result.transform_keys(&:to_s)))
12
- end
13
- end
14
- end
15
- end
16
- end
@@ -1,18 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- class RuleLint < Verb
5
- command_name "lint"
6
- parent_group Group::Rule
7
-
8
- option :against, "--against=FILE"
9
-
10
- def call(store)
11
- path = against or raise UsageError.new("rule lint --against=FILE required")
12
- yaml = File.read(path)
13
- emit(session_for(store).rule_lint(candidate_yaml: yaml).to_h)
14
- end
15
- end
16
- end
17
- end
18
- end
@@ -1,29 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- class RuleList < Verb
5
- command_name "list"
6
- parent_group Group::Rule
7
-
8
- def call(store)
9
- policies = store.manifest.rules.blocks.map do |b|
10
- row = { "match" => b.match }
11
- if b.fetch
12
- row["fetch"] = {
13
- "ttl_seconds" => b.fetch.ttl_seconds,
14
- "on_stale" => b.fetch.on_stale,
15
- "sync_budget_ms" => b.fetch.sync_budget_ms,
16
- "fetch_timeout_seconds" => b.fetch.fetch_timeout_seconds,
17
- }
18
- end
19
- row["handler_allowlist"] = b.handler_allowlist.handlers if b.handler_allowlist
20
- row["guard"] = b.guard if b.guard
21
- row["retention"] = { "expire_after" => b.retention.expire_after, "archive_after" => b.retention.archive_after } if b.retention
22
- row
23
- end
24
- emit({ "verb" => "policy_list", "policies" => policies })
25
- end
26
- end
27
- end
28
- end
29
- end
@@ -1,15 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- class Schema < Verb
5
- command_name "show"
6
- parent_group Group::Schema
7
-
8
- def call(store)
9
- key = positional.shift or raise UsageError.new("schema requires a key")
10
- emit(session_for(store).schema(key))
11
- end
12
- end
13
- end
14
- end
15
- end
@@ -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,24 +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
-
14
- def initialize(container:, call: nil) # rubocop:disable Lint/UnusedMethodArgument
15
- @manifest = container.manifest
16
- end
17
-
18
- def call(key)
19
- set = @manifest.rules.for(key)
20
- { "fetch" => set.fetch&.to_h, "guard" => set.guard }.compact
21
- end
22
- end
23
- end
24
- end