textus 0.43.2 → 0.46.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 (101) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +70 -0
  3. data/README.md +56 -29
  4. data/SPEC.md +24 -22
  5. data/docs/architecture/README.md +32 -32
  6. data/docs/reference/conventions.md +8 -9
  7. data/lib/textus/boot.rb +4 -4
  8. data/lib/textus/builder/pipeline.rb +11 -42
  9. data/lib/textus/builder/renderer/markdown.rb +4 -8
  10. data/lib/textus/cli/group/fetch.rb +2 -2
  11. data/lib/textus/cli/group.rb +1 -0
  12. data/lib/textus/cli/runner.rb +187 -0
  13. data/lib/textus/cli/verb/build.rb +4 -4
  14. data/lib/textus/cli/verb/{fetch_stale.rb → fetch_all.rb} +2 -2
  15. data/lib/textus/cli/verb/get.rb +6 -5
  16. data/lib/textus/cli/verb/put.rb +3 -3
  17. data/lib/textus/cli/verb.rb +3 -0
  18. data/lib/textus/cli.rb +37 -3
  19. data/lib/textus/container.rb +3 -15
  20. data/lib/textus/contract/around.rb +29 -0
  21. data/lib/textus/contract/binder.rb +88 -0
  22. data/lib/textus/contract/resources/cursor.rb +26 -0
  23. data/lib/textus/contract/sources.rb +39 -0
  24. data/lib/textus/contract/view.rb +15 -0
  25. data/lib/textus/contract.rb +68 -8
  26. data/lib/textus/dispatcher.rb +6 -6
  27. data/lib/textus/doctor/check/orphaned_publish_targets.rb +1 -1
  28. data/lib/textus/doctor/check/sentinels.rb +1 -1
  29. data/lib/textus/domain/policy/predicates/fresh_within.rb +6 -5
  30. data/lib/textus/envelope/io/writer.rb +34 -0
  31. data/lib/textus/hooks/context.rb +24 -2
  32. data/lib/textus/layout.rb +8 -0
  33. data/lib/textus/maintenance/key_delete_prefix.rb +8 -5
  34. data/lib/textus/maintenance/key_mv_prefix.rb +18 -6
  35. data/lib/textus/maintenance/migrate.rb +14 -10
  36. data/lib/textus/maintenance/rule_lint.rb +5 -4
  37. data/lib/textus/maintenance/zone_mv.rb +9 -6
  38. data/lib/textus/manifest/entry/base.rb +1 -1
  39. data/lib/textus/mcp/catalog.rb +6 -33
  40. data/lib/textus/ports/publisher.rb +3 -2
  41. data/lib/textus/ports/sentinel_store.rb +8 -7
  42. data/lib/textus/projection.rb +6 -5
  43. data/lib/textus/read/audit.rb +19 -0
  44. data/lib/textus/read/blame.rb +11 -1
  45. data/lib/textus/read/boot.rb +1 -1
  46. data/lib/textus/read/capabilities.rb +70 -0
  47. data/lib/textus/read/deps.rb +15 -1
  48. data/lib/textus/read/doctor.rb +8 -0
  49. data/lib/textus/read/freshness.rb +10 -0
  50. data/lib/textus/read/get.rb +87 -22
  51. data/lib/textus/read/list.rb +2 -1
  52. data/lib/textus/read/published.rb +7 -0
  53. data/lib/textus/read/pulse.rb +2 -1
  54. data/lib/textus/read/rdeps.rb +14 -0
  55. data/lib/textus/read/rule_explain.rb +84 -0
  56. data/lib/textus/read/rule_list.rb +39 -0
  57. data/lib/textus/read/schema_envelope.rb +3 -2
  58. data/lib/textus/read/uid.rb +9 -0
  59. data/lib/textus/read/where.rb +8 -0
  60. data/lib/textus/role_scope.rb +34 -6
  61. data/lib/textus/schema/tools.rb +12 -3
  62. data/lib/textus/store.rb +47 -24
  63. data/lib/textus/version.rb +1 -1
  64. data/lib/textus/write/accept.rb +8 -0
  65. data/lib/textus/write/{publish.rb → build.rb} +16 -7
  66. data/lib/textus/write/delete.rb +13 -0
  67. data/lib/textus/write/fetch_all.rb +2 -1
  68. data/lib/textus/write/fetch_orchestrator.rb +1 -1
  69. data/lib/textus/write/fetch_worker.rb +2 -2
  70. data/lib/textus/write/mv.rb +16 -0
  71. data/lib/textus/write/propose.rb +8 -3
  72. data/lib/textus/write/put.rb +3 -3
  73. data/lib/textus/write/reject.rb +8 -0
  74. data/lib/textus/write/retention_sweep.rb +9 -0
  75. metadata +12 -29
  76. data/lib/textus/cli/verb/accept.rb +0 -16
  77. data/lib/textus/cli/verb/audit.rb +0 -34
  78. data/lib/textus/cli/verb/blame.rb +0 -17
  79. data/lib/textus/cli/verb/delete.rb +0 -17
  80. data/lib/textus/cli/verb/deps.rb +0 -14
  81. data/lib/textus/cli/verb/freshness.rb +0 -17
  82. data/lib/textus/cli/verb/key_delete.rb +0 -24
  83. data/lib/textus/cli/verb/list.rb +0 -16
  84. data/lib/textus/cli/verb/migrate.rb +0 -18
  85. data/lib/textus/cli/verb/mv.rb +0 -27
  86. data/lib/textus/cli/verb/propose.rb +0 -28
  87. data/lib/textus/cli/verb/published.rb +0 -13
  88. data/lib/textus/cli/verb/pulse.rb +0 -26
  89. data/lib/textus/cli/verb/rdeps.rb +0 -14
  90. data/lib/textus/cli/verb/reject.rb +0 -16
  91. data/lib/textus/cli/verb/retain.rb +0 -19
  92. data/lib/textus/cli/verb/rule_explain.rb +0 -16
  93. data/lib/textus/cli/verb/rule_lint.rb +0 -18
  94. data/lib/textus/cli/verb/rule_list.rb +0 -29
  95. data/lib/textus/cli/verb/schema.rb +0 -15
  96. data/lib/textus/cli/verb/uid.rb +0 -15
  97. data/lib/textus/cli/verb/where.rb +0 -14
  98. data/lib/textus/cli/verb/zone_mv.rb +0 -19
  99. data/lib/textus/read/get_or_fetch.rb +0 -69
  100. data/lib/textus/read/policy_explain.rb +0 -46
  101. data/lib/textus/read/rules.rb +0 -25
@@ -36,14 +36,42 @@ module Textus
36
36
  self.class.new(container: @container, role: @role, dry_run: true, correlation_id: @correlation_id)
37
37
  end
38
38
 
39
+ # Single bind + invoke site for every surface. `inputs` is the uniform
40
+ # by-name hash (the binder's currency). MCP/CLI build it from their raw
41
+ # transport shape and call this directly; the per-verb Ruby methods below
42
+ # normalize positional+keyword Ruby args into `inputs` and delegate here.
43
+ def dispatch_bound(verb, inputs, session: nil)
44
+ klass = Textus::Dispatcher::VERBS[verb]
45
+ spec = (klass.contract if klass.respond_to?(:contract?) && klass.contract?)
46
+
47
+ invoke = lambda do |effective_inputs|
48
+ args, kwargs =
49
+ if spec
50
+ Textus::Contract::Binder.bind(spec, effective_inputs, session: session)
51
+ else
52
+ [[], effective_inputs]
53
+ end
54
+ call_value = Textus::Call.build(role: @role, correlation_id: @correlation_id, dry_run: @dry_run)
55
+ Textus::Dispatcher.invoke(verb, container: @container, call: call_value, args: args, kwargs: kwargs)
56
+ end
57
+
58
+ if spec&.around
59
+ Textus::Contract::Around.with(spec.around, scope: self, inputs: inputs, session: session, &invoke)
60
+ else
61
+ invoke.call(inputs)
62
+ end
63
+ end
64
+
39
65
  Textus::Dispatcher::VERBS.each_key do |verb|
40
66
  define_method(verb) do |*args, **kwargs|
41
- call_value = Textus::Call.build(
42
- role: @role, correlation_id: @correlation_id, dry_run: @dry_run,
43
- )
44
- Textus::Dispatcher.invoke(
45
- verb, container: @container, call: call_value, args: args, kwargs: kwargs
46
- )
67
+ klass = Textus::Dispatcher::VERBS[verb]
68
+ inputs =
69
+ if klass.respond_to?(:contract?) && klass.contract?
70
+ Textus::Contract::Binder.inputs_from_ordered(klass.contract, args, kwargs)
71
+ else
72
+ kwargs
73
+ end
74
+ dispatch_bound(verb, inputs)
47
75
  end
48
76
  end
49
77
  end
@@ -6,7 +6,7 @@ module Textus
6
6
  module Tools
7
7
  # textus schema init NAME --from=KEY → infer YAML schema from an entry's frontmatter
8
8
  def self.init(store, name:, from:)
9
- env = store.as(Textus::Role::DEFAULT).get(from)
9
+ env = pure_get(store, Textus::Role::DEFAULT, from)
10
10
  meta = env.meta
11
11
  schema = {
12
12
  "name" => name,
@@ -25,7 +25,7 @@ module Textus
25
25
  schema = load_schema(store, name)
26
26
  drift = []
27
27
  store.manifest.resolver.enumerate.each do |row|
28
- env = store.as(Textus::Role::DEFAULT).get(row[:key])
28
+ env = pure_get(store, Textus::Role::DEFAULT, row[:key])
29
29
  begin
30
30
  schema.validate!(env.meta)
31
31
  rescue SchemaViolation => e
@@ -53,7 +53,7 @@ module Textus
53
53
  ops = store.as(authority)
54
54
  touched = []
55
55
  store.manifest.resolver.enumerate.each do |row|
56
- env = ops.get(row[:key])
56
+ env = pure_get(store, authority, row[:key])
57
57
  meta = env.meta.dup
58
58
  changed = false
59
59
  renames.each do |old, new|
@@ -81,6 +81,15 @@ module Textus
81
81
  end
82
82
  end
83
83
 
84
+ # Orchestrator-free read: schema tooling must never trigger a fetch
85
+ # while inspecting/migrating entries (ADR 0062).
86
+ def self.pure_get(store, role, key)
87
+ Textus::Read::Get.new(
88
+ container: store.as(role).container,
89
+ call: Textus::Call.build(role: role),
90
+ ).call(key)
91
+ end
92
+
84
93
  def self.load_schema(store, name)
85
94
  store.schemas.fetch(name)
86
95
  rescue IoError
data/lib/textus/store.rb CHANGED
@@ -2,52 +2,50 @@ require "fileutils"
2
2
 
3
3
  module Textus
4
4
  class Store
5
- attr_reader :root, :manifest, :schemas, :file_store, :audit_log, :events, :rpc
5
+ attr_reader :container
6
+
7
+ # Readers are derived from the Container's schema, so the field set lives
8
+ # in exactly one place (Container's Data.define). A new capability added
9
+ # there is automatically exposed on the Store.
10
+ Textus::Container.members.each do |field|
11
+ define_method(field) { @container.public_send(field) }
12
+ end
6
13
 
7
14
  def self.discover(start_dir = Dir.pwd, root: nil)
8
15
  explicit = root || ENV.fetch("TEXTUS_ROOT", nil)
9
16
  return discover_explicit(explicit) if explicit
10
17
 
11
- dir = File.expand_path(start_dir)
18
+ ascend_for_store(File.expand_path(start_dir)) ||
19
+ raise(IoError.new("no .textus directory found from #{start_dir}"))
20
+ end
21
+
22
+ private_class_method def self.ascend_for_store(dir)
12
23
  loop do
13
24
  candidate = File.join(dir, ".textus")
14
- return new(candidate) if File.directory?(candidate) && File.exist?(File.join(candidate, "manifest.yaml"))
25
+ return new(candidate) if store_dir?(candidate)
15
26
 
16
27
  parent = File.dirname(dir)
17
- break if parent == dir
28
+ return nil if parent == dir
18
29
 
19
30
  dir = parent
20
31
  end
21
- raise IoError.new("no .textus directory found from #{start_dir}")
22
32
  end
23
33
 
24
34
  private_class_method def self.discover_explicit(root_arg)
25
35
  abs = File.expand_path(root_arg)
26
- raise IoError.new("no textus store at #{abs}") unless File.directory?(abs) && File.exist?(File.join(abs, "manifest.yaml"))
36
+ raise IoError.new("no textus store at #{abs}") unless store_dir?(abs)
27
37
 
28
38
  new(abs)
29
39
  end
30
40
 
31
- def initialize(root)
32
- @root = File.expand_path(root)
33
- @manifest = Manifest.load(@root)
34
- @schemas = Schemas.new(File.join(@root, "schemas"))
35
- @file_store = Ports::Storage::FileStore.new
36
- @audit_log = Ports::AuditLog.new(
37
- @root,
38
- max_size: @manifest.data.audit_config[:max_size],
39
- keep: @manifest.data.audit_config[:keep],
40
- )
41
- @events = Hooks::EventBus.new
42
- @rpc = Hooks::RpcRegistry.new
43
- Ports::AuditSubscriber.new(@audit_log).attach(@events)
44
- Hooks::Builtin.register_all(events: @events, rpc: @rpc)
45
- Hooks::Loader.new(events: @events, rpc: @rpc).load_dir(File.join(@root, "hooks"))
46
- @events.publish(:store_loaded, ctx: Hooks::Context.new(scope: as(Role::DEFAULT)))
41
+ private_class_method def self.store_dir?(dir)
42
+ File.directory?(dir) && File.exist?(File.join(dir, "manifest.yaml"))
47
43
  end
48
44
 
49
- def container
50
- @container ||= Textus::Container.from_store(self)
45
+ def initialize(root)
46
+ @container = build_container(File.expand_path(root))
47
+ bootstrap_hooks
48
+ events.publish(:store_loaded, ctx: Hooks::Context.new(scope: as(Role::DEFAULT)))
51
49
  end
52
50
 
53
51
  # Build an agent Session oriented at the current cursor/manifest — the
@@ -70,5 +68,30 @@ module Textus
70
68
  as(role).public_send(verb, *args, **kwargs)
71
69
  end
72
70
  end
71
+
72
+ private
73
+
74
+ def build_container(root)
75
+ manifest = Manifest.load(root)
76
+ Container.new(
77
+ root: root,
78
+ manifest: manifest,
79
+ schemas: Schemas.new(File.join(root, "schemas")),
80
+ file_store: Ports::Storage::FileStore.new,
81
+ audit_log: Ports::AuditLog.new(
82
+ root,
83
+ max_size: manifest.data.audit_config[:max_size],
84
+ keep: manifest.data.audit_config[:keep],
85
+ ),
86
+ events: Hooks::EventBus.new,
87
+ rpc: Hooks::RpcRegistry.new,
88
+ )
89
+ end
90
+
91
+ def bootstrap_hooks
92
+ Ports::AuditSubscriber.new(audit_log).attach(events)
93
+ Hooks::Builtin.register_all(events: events, rpc: rpc)
94
+ Hooks::Loader.new(events: events, rpc: rpc).load_dir(File.join(root, "hooks"))
95
+ end
73
96
  end
74
97
  end
@@ -1,4 +1,4 @@
1
1
  module Textus
2
- VERSION = "0.43.2"
2
+ VERSION = "0.46.0"
3
3
  PROTOCOL = "textus/3"
4
4
  end
@@ -1,6 +1,14 @@
1
1
  module Textus
2
2
  module Write
3
3
  class Accept
4
+ extend Textus::Contract::DSL
5
+
6
+ verb :accept
7
+ summary "apply a queued proposal to its target zone; requires the author capability"
8
+ surfaces :cli, :mcp
9
+ cli "accept"
10
+ arg :pending_key, String, required: true, positional: true, description: "the queued proposal's key"
11
+
4
12
  def initialize(container:, call:)
5
13
  @container = container
6
14
  @call = call
@@ -1,14 +1,23 @@
1
1
  module Textus
2
2
  module Write
3
- # Single-pass publish use case: dispatches polymorphically to each
4
- # entry's `publish_via` method. Derived entries materialize their body
5
- # via Materializer; Nested entries mirror their subtree via publish_tree;
6
- # Leaf and Intake entries copy their stored body to publish_to targets.
7
- # The Publish layer owns wiring (context, accumulation) but not per-kind
8
- # logic.
3
+ # Single-pass build use case (the verb `build`, ADR 0061): dispatches
4
+ # polymorphically to each entry's `publish_via` method the copy-out step
5
+ # (`publish` is the output-destination concept the verb drives, not the verb).
6
+ # Derived entries materialize their body via Materializer; Nested entries
7
+ # mirror their subtree via publish_tree; Leaf and Intake entries copy their
8
+ # stored body to publish_to targets. The Build layer owns wiring (context,
9
+ # accumulation) but not per-kind logic.
9
10
  #
10
11
  # Return shape: { "protocol", "built", "published_leaves" }
11
- class Publish
12
+ class Build
13
+ extend Textus::Contract::DSL
14
+
15
+ verb :build
16
+ summary "materialize derived entries; publish_to and publish_tree fan out copies"
17
+ surfaces :cli
18
+ cli "build"
19
+ arg :prefix, String, required: false, description: "limit the build to keys under this prefix"
20
+
12
21
  def initialize(container:, call:)
13
22
  @container = container
14
23
  @call = call
@@ -1,6 +1,19 @@
1
1
  module Textus
2
2
  module Write
3
3
  class Delete
4
+ extend Textus::Contract::DSL
5
+
6
+ verb :delete
7
+ summary "Delete one entry by key. Single-key, lower blast radius than " \
8
+ "key_delete_prefix; guarded by an optional optimistic-concurrency etag. Returns {ok, key, deleted}."
9
+ surfaces :cli, :mcp
10
+ cli "key delete"
11
+ arg :key, String, required: true, positional: true,
12
+ description: "dotted entry key to delete"
13
+ arg :if_etag, String,
14
+ description: "optimistic-concurrency guard: the etag you last read; the delete is rejected if the entry changed since"
15
+ # `call` already returns a wire hash {protocol, ok, key, deleted}; identity response.
16
+
4
17
  def initialize(container:, call:)
5
18
  @container = container
6
19
  @call = call
@@ -5,7 +5,8 @@ module Textus
5
5
 
6
6
  verb :fetch_all
7
7
  summary "Fetch all stale quarantine entries, optionally scoped by zone/prefix."
8
- surfaces :cli, :ruby, :mcp
8
+ surfaces :cli, :mcp
9
+ cli "fetch all"
9
10
  arg :prefix, String, description: "only refresh stale entries whose key starts with this dotted prefix"
10
11
  arg :zone, String, description: "only refresh stale entries in this quarantine zone (see `pulse` stale list)"
11
12
 
@@ -2,7 +2,7 @@ module Textus
2
2
  module Write
3
3
  class FetchOrchestrator
4
4
  # Collaborator (not a Dispatcher verb): constructed directly by FetchWorker /
5
- # GetOrFetch, which pass their derived hook_context in. That's why this takes
5
+ # Read::Get, which pass their derived hook_context in. That's why this takes
6
6
  # hook_context: explicitly while verb use cases derive their own.
7
7
  def initialize(worker:, store_root:, events:, hook_context: nil, detached_spawner: nil)
8
8
  @worker = worker
@@ -7,10 +7,10 @@ module Textus
7
7
 
8
8
  verb :fetch
9
9
  summary "Run a fetch action for one quarantine entry."
10
- surfaces :cli, :ruby, :mcp
10
+ surfaces :cli, :mcp
11
11
  arg :key, String, required: true, positional: true,
12
12
  description: "quarantine-zone entry key to refresh using its declared intake action"
13
- response { |outcome| { "outcome" => outcome.class.name.split("::").last.downcase } }
13
+ view { |outcome| { "outcome" => outcome.class.name.split("::").last.downcase } }
14
14
 
15
15
  FETCH_TIMEOUT_SECONDS = IntakeFetch::FETCH_TIMEOUT_SECONDS
16
16
 
@@ -1,6 +1,22 @@
1
1
  module Textus
2
2
  module Write
3
3
  class Mv
4
+ extend Textus::Contract::DSL
5
+
6
+ verb :mv
7
+ summary "Rename one entry (same zone + format). Refuses if the target exists. Single-key, lower blast radius than key_mv_prefix."
8
+ surfaces :cli, :mcp
9
+ cli "key mv"
10
+ arg :old_key, String, required: true, positional: true,
11
+ description: "current dotted key"
12
+ arg :new_key, String, required: true, positional: true,
13
+ description: "new dotted key (must be the same zone and format as old_key)"
14
+ arg :dry_run, :boolean,
15
+ description: "when true, returns the planned move (from/to paths, uid) without applying it; " \
16
+ "defaults to false, so omitting it applies the move immediately " \
17
+ "(unlike the bulk key_mv_prefix, which defaults to a dry-run plan)"
18
+ # `call` already returns a wire hash; identity response.
19
+
4
20
  def initialize(container:, call:)
5
21
  @container = container
6
22
  @call = call
@@ -9,16 +9,21 @@ module Textus
9
9
 
10
10
  verb :propose
11
11
  summary "Write a proposal to the role's propose_zone. Auto-prefixes the key."
12
- surfaces :cli, :ruby, :mcp
12
+ surfaces :cli, :mcp
13
+ cli_stdin :json
13
14
  arg :key, String, required: true, positional: true,
14
15
  description: "key relative to propose_zone, e.g. 'decisions.feature-x'"
15
- arg :meta, Hash, required: true, wire_name: :_meta,
16
+ arg :meta, Hash, required: false, wire_name: :_meta,
16
17
  description: "frontmatter; reads back as `_meta` from `get`. Include a 'proposal:' block naming the target_key"
17
18
  arg :body, String,
18
19
  description: "markdown/text payload for markdown-format entries; omit (use `content`) for json/yaml entries. Do not send both"
19
20
  arg :content, Hash,
20
21
  description: "structured payload for json/yaml-format entries; omit (use `body`) for markdown entries. Do not send both"
21
- response { |env| { "uid" => env.uid, "etag" => env.etag, "key" => env.key } }
22
+ # ADR 0069: every surface receives the raw Envelope and self-shapes no
23
+ # surface pre-wires the result. Emitting the full wire envelope on every
24
+ # surface is a superset of the old `{uid, etag, key}` (the accepted
25
+ # breaking change; MCP/Ruby now get the full envelope too).
26
+ view { |env, _i| env.to_h_for_wire }
22
27
 
23
28
  def initialize(container:, call:)
24
29
  @container = container
@@ -5,10 +5,10 @@ module Textus
5
5
 
6
6
  verb :put
7
7
  summary "Create or update an entry. Schema-validated. Returns {uid, etag}."
8
- surfaces :cli, :ruby, :mcp
8
+ surfaces :cli, :mcp
9
9
  arg :key, String, required: true, positional: true,
10
10
  description: "dotted entry key, e.g. 'knowledge.project'; must resolve to a zone the role may write"
11
- arg :meta, Hash, required: true, wire_name: :_meta,
11
+ arg :meta, Hash, required: false, wire_name: :_meta,
12
12
  description: "frontmatter; reads back as `_meta` from `get`. Schema-validated — call `schema KEY` first"
13
13
  arg :body, String,
14
14
  description: "markdown/text payload for markdown-format entries; omit (use `content`) for json/yaml entries. Do not send both"
@@ -16,7 +16,7 @@ module Textus
16
16
  description: "structured payload for json/yaml-format entries; omit (use `body`) for markdown entries. Do not send both"
17
17
  arg :if_etag, String,
18
18
  description: "optimistic-concurrency guard: the etag you last read; the write is rejected if the entry changed since"
19
- response { |env| { "uid" => env.uid, "etag" => env.etag } }
19
+ view { |env| { "uid" => env.uid, "etag" => env.etag } }
20
20
 
21
21
  def initialize(container:, call:)
22
22
  @container = container
@@ -1,6 +1,14 @@
1
1
  module Textus
2
2
  module Write
3
3
  class Reject
4
+ extend Textus::Contract::DSL
5
+
6
+ verb :reject
7
+ summary "discard a queued proposal without applying it"
8
+ surfaces :cli, :mcp
9
+ cli "reject"
10
+ arg :pending_key, String, required: true, positional: true, description: "the queued proposal's key"
11
+
4
12
  def initialize(container:, call:)
5
13
  @container = container
6
14
  @call = call
@@ -7,6 +7,15 @@ module Textus
7
7
  # <root>/archive/<relative-path> first, then deletes. Rows whose zone the
8
8
  # caller's role cannot write surface in `failed` rather than aborting.
9
9
  class RetentionSweep
10
+ extend Textus::Contract::DSL
11
+
12
+ verb :retain
13
+ summary "Apply each entry's retention policy; prune expired versions."
14
+ surfaces :cli
15
+ cli "retain"
16
+ arg :prefix, String, description: "restrict to keys starting with this dotted prefix"
17
+ arg :zone, String, description: "restrict to entries in this zone"
18
+
10
19
  def initialize(container:, call:)
11
20
  @container = container
12
21
  @call = call
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: textus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.43.2
4
+ version: 0.46.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrick
@@ -127,46 +127,29 @@ files:
127
127
  - lib/textus/cli/group/rule.rb
128
128
  - lib/textus/cli/group/schema.rb
129
129
  - lib/textus/cli/group/zone.rb
130
+ - lib/textus/cli/runner.rb
130
131
  - lib/textus/cli/verb.rb
131
- - lib/textus/cli/verb/accept.rb
132
- - lib/textus/cli/verb/audit.rb
133
- - lib/textus/cli/verb/blame.rb
134
132
  - lib/textus/cli/verb/boot.rb
135
133
  - lib/textus/cli/verb/build.rb
136
- - lib/textus/cli/verb/delete.rb
137
- - lib/textus/cli/verb/deps.rb
138
134
  - lib/textus/cli/verb/doctor.rb
139
135
  - lib/textus/cli/verb/fetch.rb
140
- - lib/textus/cli/verb/fetch_stale.rb
141
- - lib/textus/cli/verb/freshness.rb
136
+ - lib/textus/cli/verb/fetch_all.rb
142
137
  - lib/textus/cli/verb/get.rb
143
138
  - lib/textus/cli/verb/hook_run.rb
144
139
  - lib/textus/cli/verb/hooks.rb
145
140
  - lib/textus/cli/verb/init.rb
146
- - lib/textus/cli/verb/key_delete.rb
147
- - lib/textus/cli/verb/list.rb
148
141
  - lib/textus/cli/verb/mcp_serve.rb
149
- - lib/textus/cli/verb/migrate.rb
150
- - lib/textus/cli/verb/mv.rb
151
- - lib/textus/cli/verb/propose.rb
152
- - lib/textus/cli/verb/published.rb
153
- - lib/textus/cli/verb/pulse.rb
154
142
  - lib/textus/cli/verb/put.rb
155
- - lib/textus/cli/verb/rdeps.rb
156
- - lib/textus/cli/verb/reject.rb
157
- - lib/textus/cli/verb/retain.rb
158
- - lib/textus/cli/verb/rule_explain.rb
159
- - lib/textus/cli/verb/rule_lint.rb
160
- - lib/textus/cli/verb/rule_list.rb
161
- - lib/textus/cli/verb/schema.rb
162
143
  - lib/textus/cli/verb/schema_diff.rb
163
144
  - lib/textus/cli/verb/schema_init.rb
164
145
  - lib/textus/cli/verb/schema_migrate.rb
165
- - lib/textus/cli/verb/uid.rb
166
- - lib/textus/cli/verb/where.rb
167
- - lib/textus/cli/verb/zone_mv.rb
168
146
  - lib/textus/container.rb
169
147
  - lib/textus/contract.rb
148
+ - lib/textus/contract/around.rb
149
+ - lib/textus/contract/binder.rb
150
+ - lib/textus/contract/resources/cursor.rb
151
+ - lib/textus/contract/sources.rb
152
+ - lib/textus/contract/view.rb
170
153
  - lib/textus/cursor_store.rb
171
154
  - lib/textus/dispatcher.rb
172
155
  - lib/textus/doctor.rb
@@ -299,18 +282,18 @@ files:
299
282
  - lib/textus/read/audit.rb
300
283
  - lib/textus/read/blame.rb
301
284
  - lib/textus/read/boot.rb
285
+ - lib/textus/read/capabilities.rb
302
286
  - lib/textus/read/deps.rb
303
287
  - lib/textus/read/doctor.rb
304
288
  - lib/textus/read/freshness.rb
305
289
  - lib/textus/read/get.rb
306
- - lib/textus/read/get_or_fetch.rb
307
290
  - lib/textus/read/list.rb
308
- - lib/textus/read/policy_explain.rb
309
291
  - lib/textus/read/published.rb
310
292
  - lib/textus/read/pulse.rb
311
293
  - lib/textus/read/rdeps.rb
312
294
  - lib/textus/read/retainable.rb
313
- - lib/textus/read/rules.rb
295
+ - lib/textus/read/rule_explain.rb
296
+ - lib/textus/read/rule_list.rb
314
297
  - lib/textus/read/schema_envelope.rb
315
298
  - lib/textus/read/stale.rb
316
299
  - lib/textus/read/uid.rb
@@ -327,6 +310,7 @@ files:
327
310
  - lib/textus/uid.rb
328
311
  - lib/textus/version.rb
329
312
  - lib/textus/write/accept.rb
313
+ - lib/textus/write/build.rb
330
314
  - lib/textus/write/delete.rb
331
315
  - lib/textus/write/fetch_all.rb
332
316
  - lib/textus/write/fetch_events.rb
@@ -336,7 +320,6 @@ files:
336
320
  - lib/textus/write/materializer.rb
337
321
  - lib/textus/write/mv.rb
338
322
  - lib/textus/write/propose.rb
339
- - lib/textus/write/publish.rb
340
323
  - lib/textus/write/put.rb
341
324
  - lib/textus/write/reject.rb
342
325
  - lib/textus/write/retention_sweep.rb
@@ -1,16 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- class Accept < Verb
5
- command_name "accept"
6
-
7
- option :as_flag, "--as=ROLE"
8
-
9
- def call(store)
10
- key = positional.shift or raise UsageError.new("accept requires a key")
11
- emit(session_for(store).accept(key))
12
- end
13
- end
14
- end
15
- end
16
- end
@@ -1,34 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- class Audit < Verb
5
- command_name "audit"
6
-
7
- option :key_filter, "--key=KEY"
8
- option :zone, "--zone=Z"
9
- option :role_filter, "--role=ROLE"
10
- option :verb_filter, "--verb=V"
11
- option :since, "--since=ISO8601|RELATIVE"
12
- option :seq_since, "--seq-since=N"
13
- option :correlation_id, "--correlation-id=ID"
14
- option :limit, "--limit=N"
15
-
16
- def call(store)
17
- ops = session_for(store)
18
- since_time = since && Textus::Read::Audit.parse_since(since, now: Time.now)
19
- rows = ops.audit(
20
- key: key_filter,
21
- zone: zone,
22
- role: role_filter,
23
- verb: verb_filter,
24
- since: since_time,
25
- seq_since: seq_since&.to_i,
26
- correlation_id: correlation_id,
27
- limit: limit&.to_i,
28
- )
29
- emit({ "verb" => "audit", "rows" => rows })
30
- end
31
- end
32
- end
33
- end
34
- end
@@ -1,17 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- class Blame < Verb
5
- command_name "blame"
6
-
7
- option :limit, "--limit=N"
8
-
9
- def call(store)
10
- key = positional.shift or raise UsageError.new("blame requires a key")
11
- rows = session_for(store).blame(key: key, limit: limit&.to_i)
12
- emit({ "verb" => "blame", "key" => key, "rows" => rows })
13
- end
14
- end
15
- end
16
- end
17
- end
@@ -1,17 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- class Delete < Verb
5
- command_name "delete"
6
-
7
- option :as_flag, "--as=ROLE"
8
- option :if_etag, "--if-etag=E"
9
-
10
- def call(store)
11
- key = positional.shift or raise UsageError.new("delete requires a key")
12
- emit(session_for(store).delete(key, if_etag: if_etag))
13
- end
14
- end
15
- end
16
- end
17
- end
@@ -1,14 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- class Deps < Verb
5
- command_name "deps"
6
-
7
- def call(store)
8
- key = positional.shift or raise UsageError.new("deps requires a key")
9
- emit({ "key" => key, "deps" => session_for(store).deps(key) })
10
- end
11
- end
12
- end
13
- end
14
- end
@@ -1,17 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- class Freshness < Verb
5
- command_name "freshness"
6
-
7
- option :prefix, "--prefix=KEY"
8
- option :zone, "--zone=Z"
9
-
10
- def call(store)
11
- rows = session_for(store).freshness(prefix: prefix, zone: zone)
12
- emit({ "verb" => "freshness", "rows" => rows })
13
- end
14
- end
15
- end
16
- end
17
- end
@@ -1,24 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- class KeyDelete < Verb
5
- command_name "delete"
6
- parent_group Group::Key
7
-
8
- option :as_flag, "--as=ROLE"
9
- option :dry_run, "--dry-run"
10
- option :prefix, "--prefix"
11
-
12
- def call(store)
13
- if prefix
14
- p = positional.shift or raise UsageError.new("key delete --prefix requires <prefix>")
15
- emit(session_for(store).key_delete_prefix(prefix: p, dry_run: dry_run || false).to_h)
16
- else
17
- key = positional.shift or raise UsageError.new("key delete requires <key>")
18
- emit(session_for(store).delete(key))
19
- end
20
- end
21
- end
22
- end
23
- end
24
- end