textus 0.54.2 → 0.55.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 (176) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -0
  3. data/README.md +8 -1
  4. data/SPEC.md +27 -0
  5. data/docs/architecture/README.md +20 -8
  6. data/docs/reference/conventions.md +1 -1
  7. data/exe/textus +1 -1
  8. data/lib/textus/action/accept.rb +23 -21
  9. data/lib/textus/action/audit.rb +24 -61
  10. data/lib/textus/action/base.rb +9 -9
  11. data/lib/textus/action/blame.rb +18 -36
  12. data/lib/textus/action/boot.rb +2 -4
  13. data/lib/textus/action/data_mv.rb +20 -31
  14. data/lib/textus/action/deps.rb +3 -18
  15. data/lib/textus/action/doctor.rb +2 -9
  16. data/lib/textus/action/drain.rb +11 -19
  17. data/lib/textus/action/enqueue.rb +14 -30
  18. data/lib/textus/action/get.rb +12 -56
  19. data/lib/textus/action/ingest.rb +74 -78
  20. data/lib/textus/action/jobs.rb +6 -15
  21. data/lib/textus/action/key_delete.rb +6 -16
  22. data/lib/textus/action/key_delete_prefix.rb +8 -17
  23. data/lib/textus/action/key_mv.rb +54 -61
  24. data/lib/textus/action/key_mv_prefix.rb +13 -22
  25. data/lib/textus/action/list.rb +7 -21
  26. data/lib/textus/action/propose.rb +16 -26
  27. data/lib/textus/action/published.rb +3 -5
  28. data/lib/textus/action/pulse.rb +19 -26
  29. data/lib/textus/action/put.rb +15 -29
  30. data/lib/textus/action/rdeps.rb +3 -18
  31. data/lib/textus/action/reject.rb +12 -21
  32. data/lib/textus/action/rule_explain.rb +12 -22
  33. data/lib/textus/action/rule_lint.rb +10 -16
  34. data/lib/textus/action/rule_list.rb +5 -9
  35. data/lib/textus/action/schema_envelope.rb +3 -10
  36. data/lib/textus/action/uid.rb +3 -17
  37. data/lib/textus/action/where.rb +3 -18
  38. data/lib/textus/boot.rb +7 -15
  39. data/lib/textus/contract/arg.rb +10 -0
  40. data/lib/textus/contract/dsl.rb +88 -0
  41. data/lib/textus/contract/spec.rb +25 -0
  42. data/lib/textus/contract.rb +0 -162
  43. data/lib/textus/doctor/check/audit_log.rb +2 -2
  44. data/lib/textus/doctor/check/generator_drift.rb +2 -2
  45. data/lib/textus/doctor/check/orphaned_publish_targets.rb +3 -3
  46. data/lib/textus/doctor/check/protocol_version.rb +2 -2
  47. data/lib/textus/doctor/check/raw_asset_paths.rb +1 -1
  48. data/lib/textus/doctor/check/schema_parse_error.rb +1 -1
  49. data/lib/textus/doctor/check/schema_violations.rb +2 -2
  50. data/lib/textus/doctor/check/schemas.rb +1 -1
  51. data/lib/textus/doctor/check/sentinels.rb +4 -4
  52. data/lib/textus/doctor/check/templates.rb +1 -1
  53. data/lib/textus/doctor/check/unowned_schema_fields.rb +1 -1
  54. data/lib/textus/doctor/check.rb +4 -7
  55. data/lib/textus/doctor.rb +1 -1
  56. data/lib/textus/errors.rb +6 -0
  57. data/lib/textus/format/base.rb +0 -4
  58. data/lib/textus/format/json.rb +5 -6
  59. data/lib/textus/format/markdown.rb +5 -6
  60. data/lib/textus/format/shared.rb +17 -0
  61. data/lib/textus/format/text.rb +5 -4
  62. data/lib/textus/format/yaml.rb +30 -6
  63. data/lib/textus/format.rb +6 -0
  64. data/lib/textus/gate/auth.rb +2 -17
  65. data/lib/textus/gate/binder.rb +50 -0
  66. data/lib/textus/gate.rb +64 -88
  67. data/lib/textus/init.rb +2 -4
  68. data/lib/textus/jobs.rb +3 -9
  69. data/lib/textus/manifest/capabilities.rb +3 -3
  70. data/lib/textus/manifest/entry/base.rb +1 -1
  71. data/lib/textus/manifest/entry/publish/subtree_mirror.rb +2 -2
  72. data/lib/textus/manifest/entry/publish/to_paths.rb +1 -1
  73. data/lib/textus/manifest/schema/semantics/cross_field.rb +53 -0
  74. data/lib/textus/manifest/schema/semantics/invariants.rb +125 -0
  75. data/lib/textus/manifest/schema/semantics/migration.rb +83 -0
  76. data/lib/textus/manifest/schema/semantics.rb +11 -216
  77. data/lib/textus/meta.rb +54 -0
  78. data/lib/textus/{ports → port}/audit_log.rb +44 -4
  79. data/lib/textus/{ports → port}/build_lock.rb +2 -2
  80. data/lib/textus/{ports → port}/clock.rb +1 -1
  81. data/lib/textus/{ports → port}/publisher.rb +5 -5
  82. data/lib/textus/{ports → port}/sentinel_store.rb +3 -3
  83. data/lib/textus/{ports → port}/storage/file_stat.rb +1 -1
  84. data/lib/textus/{ports → port}/storage/file_store.rb +2 -2
  85. data/lib/textus/port/store.rb +93 -0
  86. data/lib/textus/{ports → port}/watcher_lock.rb +3 -3
  87. data/lib/textus/produce/engine.rb +1 -1
  88. data/lib/textus/schema/tools.rb +11 -7
  89. data/lib/textus/store/compositor.rb +34 -0
  90. data/lib/textus/store/container.rb +43 -0
  91. data/lib/textus/store/cursor.rb +26 -0
  92. data/lib/textus/store/envelope/reader.rb +43 -0
  93. data/lib/textus/store/envelope/writer.rb +195 -0
  94. data/lib/textus/store/geometry.rb +81 -0
  95. data/lib/textus/store/index/builder.rb +74 -0
  96. data/lib/textus/store/index/lookup.rb +60 -0
  97. data/lib/textus/store/jobs/base.rb +13 -0
  98. data/lib/textus/store/jobs/index.rb +15 -0
  99. data/lib/textus/store/jobs/materialize.rb +15 -0
  100. data/lib/textus/store/jobs/plan.rb +11 -0
  101. data/lib/textus/store/jobs/planner.rb +104 -0
  102. data/lib/textus/store/jobs/queue.rb +154 -0
  103. data/lib/textus/store/jobs/registry.rb +19 -0
  104. data/lib/textus/store/jobs/retention.rb +50 -0
  105. data/lib/textus/store/jobs/sweep.rb +21 -0
  106. data/lib/textus/store/jobs/worker.rb +64 -0
  107. data/lib/textus/store/session.rb +37 -0
  108. data/lib/textus/store.rb +21 -13
  109. data/lib/textus/{surfaces → surface}/cli/group/data.rb +1 -1
  110. data/lib/textus/{surfaces → surface}/cli/group/key.rb +1 -1
  111. data/lib/textus/{surfaces → surface}/cli/group/mcp.rb +1 -1
  112. data/lib/textus/{surfaces → surface}/cli/group/rule.rb +1 -1
  113. data/lib/textus/{surfaces → surface}/cli/group/schema.rb +1 -1
  114. data/lib/textus/{surfaces → surface}/cli/group.rb +2 -2
  115. data/lib/textus/{surfaces → surface}/cli/runner.rb +10 -63
  116. data/lib/textus/surface/cli/sources.rb +41 -0
  117. data/lib/textus/{surfaces → surface}/cli/verb/doctor.rb +4 -6
  118. data/lib/textus/{surfaces → surface}/cli/verb/get.rb +4 -4
  119. data/lib/textus/{surfaces → surface}/cli/verb/init.rb +1 -1
  120. data/lib/textus/{surfaces → surface}/cli/verb/mcp_serve.rb +3 -3
  121. data/lib/textus/{surfaces → surface}/cli/verb/put.rb +6 -11
  122. data/lib/textus/{surfaces → surface}/cli/verb/schema_diff.rb +1 -1
  123. data/lib/textus/{surfaces → surface}/cli/verb/schema_init.rb +1 -1
  124. data/lib/textus/{surfaces → surface}/cli/verb/schema_migrate.rb +1 -1
  125. data/lib/textus/{surfaces → surface}/cli/verb/watch.rb +2 -2
  126. data/lib/textus/{surfaces → surface}/cli/verb.rb +3 -8
  127. data/lib/textus/{surfaces → surface}/cli.rb +1 -1
  128. data/lib/textus/{surfaces → surface}/mcp/catalog.rb +9 -26
  129. data/lib/textus/{surfaces → surface}/mcp/errors.rb +1 -1
  130. data/lib/textus/{surfaces → surface}/mcp/server.rb +5 -5
  131. data/lib/textus/{surfaces → surface}/mcp.rb +2 -2
  132. data/lib/textus/surface/projector.rb +27 -0
  133. data/lib/textus/{surfaces → surface}/role_scope.rb +1 -1
  134. data/lib/textus/{surfaces → surface}/watcher.rb +8 -8
  135. data/lib/textus/value/call.rb +30 -0
  136. data/lib/textus/value/command.rb +16 -0
  137. data/lib/textus/value/envelope.rb +89 -0
  138. data/lib/textus/value/etag.rb +39 -0
  139. data/lib/textus/value/result.rb +26 -0
  140. data/lib/textus/value/role.rb +38 -0
  141. data/lib/textus/value/types.rb +13 -0
  142. data/lib/textus/{uid.rb → value/uid.rb} +9 -7
  143. data/lib/textus/version.rb +1 -1
  144. data/lib/textus/workflow/loader.rb +4 -4
  145. data/lib/textus/workflow/runner.rb +4 -18
  146. data/lib/textus.rb +9 -10
  147. metadata +100 -63
  148. data/lib/textus/action/write_verb.rb +0 -44
  149. data/lib/textus/call.rb +0 -28
  150. data/lib/textus/command.rb +0 -41
  151. data/lib/textus/container.rb +0 -26
  152. data/lib/textus/contract/around.rb +0 -29
  153. data/lib/textus/contract/binder.rb +0 -88
  154. data/lib/textus/contract/resources/build_lock.rb +0 -17
  155. data/lib/textus/contract/resources/cursor.rb +0 -26
  156. data/lib/textus/contract/sources.rb +0 -39
  157. data/lib/textus/contract/view.rb +0 -15
  158. data/lib/textus/cursor_store.rb +0 -24
  159. data/lib/textus/envelope/reader.rb +0 -46
  160. data/lib/textus/envelope/writer.rb +0 -209
  161. data/lib/textus/envelope.rb +0 -79
  162. data/lib/textus/etag.rb +0 -36
  163. data/lib/textus/jobs/base.rb +0 -23
  164. data/lib/textus/jobs/materialize.rb +0 -20
  165. data/lib/textus/jobs/plan.rb +0 -9
  166. data/lib/textus/jobs/planner.rb +0 -101
  167. data/lib/textus/jobs/retention.rb +0 -48
  168. data/lib/textus/jobs/sweep.rb +0 -27
  169. data/lib/textus/jobs/worker.rb +0 -67
  170. data/lib/textus/layout.rb +0 -91
  171. data/lib/textus/ports/job_store/job.rb +0 -65
  172. data/lib/textus/ports/job_store.rb +0 -123
  173. data/lib/textus/ports/raw_index.rb +0 -61
  174. data/lib/textus/role.rb +0 -36
  175. data/lib/textus/session.rb +0 -35
  176. data/lib/textus/types.rb +0 -15
@@ -3,8 +3,6 @@
3
3
  module Textus
4
4
  module Action
5
5
  class RuleExplain < Base
6
- extend Textus::Contract::DSL
7
-
8
6
  verb :rule_explain
9
7
  summary "Effective rules for a key. Lean {lifecycle, guard} by default; detail: true adds matched blocks + guard predicates."
10
8
  surfaces :cli, :mcp
@@ -15,15 +13,9 @@ module Textus
15
13
  description: "defaults false: lean {lifecycle, guard}. detail: true adds matched blocks + guard predicates per transition."
16
14
  view(:cli) { |r| { "verb" => "rule_explain" }.merge(r.transform_keys(&:to_s)) }
17
15
 
18
- def initialize(key:, detail: nil)
19
- super()
20
- @key = key
21
- @detail = detail
22
- end
23
-
24
- def call(container:, **)
25
- @manifest = container.manifest
26
- @detail ? explain(@key) : effective(@key)
16
+ def self.call(container:, call:, key:, detail: nil) # rubocop:disable Lint/UnusedMethodArgument
17
+ manifest = container.manifest
18
+ Success(detail ? explain(manifest, key) : effective(manifest, key))
27
19
  end
28
20
 
29
21
  REGISTRY = Textus::Manifest::Schema::FIELD_REGISTRY
@@ -31,17 +23,15 @@ module Textus
31
23
  DETAIL_FIELDS = REGISTRY.select { |_, m| m[:in_rule_explain].include?(:detail) }.keys.freeze
32
24
  EFFECTIVE_FIELDS = DETAIL_FIELDS.select { |f| REGISTRY[f][:policy_class] }.freeze
33
25
 
34
- private
35
-
36
- def effective(key)
37
- set = @manifest.rules.for(key)
26
+ def self.effective(manifest, key)
27
+ set = manifest.rules.for(key)
38
28
  LEAN_FIELDS.each_with_object({}) do |field, out|
39
29
  value = set.public_send(field)
40
30
  out[field.to_s] = lean_value(field, value) unless value.nil?
41
31
  end
42
32
  end
43
33
 
44
- def lean_value(field, value)
34
+ def self.lean_value(field, value)
45
35
  case field
46
36
  when :retention
47
37
  retention_hash(value, string_keys: true)
@@ -52,9 +42,9 @@ module Textus
52
42
  end
53
43
  end
54
44
 
55
- def explain(key)
56
- matching = @manifest.rules.explain(key)
57
- winners = @manifest.rules.for(key)
45
+ def self.explain(manifest, key)
46
+ matching = manifest.rules.explain(key)
47
+ winners = manifest.rules.for(key)
58
48
  {
59
49
  key: key,
60
50
  matched_blocks: matching.map do |block|
@@ -63,13 +53,13 @@ module Textus
63
53
  effective: EFFECTIVE_FIELDS.to_h { |f| [f, effective_value(f, winners.public_send(f))] },
64
54
  guards: Textus::Gate::Auth::FLOOR.keys.to_h do |action|
65
55
  floor = Textus::Gate::Auth::FLOOR.fetch(action, [])
66
- rule = Array(@manifest.rules.for(key).guard&.dig(action.to_s))
56
+ rule = Array(manifest.rules.for(key).guard&.dig(action.to_s))
67
57
  [action, { floor: floor, rule: rule }]
68
58
  end,
69
59
  }
70
60
  end
71
61
 
72
- def effective_value(field, value)
62
+ def self.effective_value(field, value)
73
63
  return nil if value.nil?
74
64
 
75
65
  case field
@@ -82,7 +72,7 @@ module Textus
82
72
  end
83
73
  end
84
74
 
85
- def retention_hash(retention, string_keys:)
75
+ def self.retention_hash(retention, string_keys:)
86
76
  h = { ttl_seconds: retention.ttl_seconds, action: retention.action }
87
77
  string_keys ? h.transform_keys(&:to_s) : h
88
78
  end
@@ -5,8 +5,6 @@ require "yaml"
5
5
  module Textus
6
6
  module Action
7
7
  class RuleLint < Base
8
- extend Textus::Contract::DSL
9
-
10
8
  verb :rule_lint
11
9
  summary "Diff candidate manifest YAML's rules against the live manifest. No writes."
12
10
  surfaces :cli, :mcp
@@ -15,15 +13,13 @@ module Textus
15
13
  description: "path to candidate manifest YAML; its `rules:` block is diffed against the live manifest"
16
14
  view { |v, _i| v.to_h }
17
15
 
18
- def initialize(candidate_yaml:)
19
- super()
20
- @candidate_yaml = candidate_yaml
21
- end
22
-
23
- def call(container:, **)
16
+ def self.call(container:, call:, candidate_yaml:) # rubocop:disable Lint/UnusedMethodArgument
24
17
  root = container.root
25
18
  live_rules = current_rules(root)
26
- candidate_rules = parse_candidate(@candidate_yaml)
19
+ candidate_result = parse_candidate(candidate_yaml)
20
+ return candidate_result if candidate_result.is_a?(Dry::Monads::Result::Failure)
21
+
22
+ candidate_rules = candidate_result
27
23
 
28
24
  live_by_match = live_rules.to_h { |rule| [rule["match"], rule] }
29
25
  candidate_by_match = candidate_rules.to_h { |rule| [rule["match"], rule] }
@@ -45,23 +41,21 @@ module Textus
45
41
  }
46
42
  end
47
43
 
48
- Textus::Jobs::Plan.new(steps: steps, warnings: [])
44
+ Success(Textus::Store::Jobs::Plan.new(steps: steps, warnings: []))
49
45
  end
50
46
 
51
- private
52
-
53
- def current_rules(root)
47
+ def self.current_rules(root)
54
48
  raw = YAML.safe_load_file(File.join(root, "manifest.yaml"), permitted_classes: [Symbol], aliases: false)
55
49
  Array(raw["rules"])
56
50
  end
57
51
 
58
- def parse_candidate(yaml_text)
52
+ def self.parse_candidate(yaml_text)
59
53
  raw = YAML.safe_load(yaml_text, permitted_classes: [Symbol], aliases: false)
60
- raise UsageError.new("candidate is not a YAML mapping") unless raw.is_a?(Hash)
54
+ return Failure(code: :usage_error, message: "candidate is not a YAML mapping") unless raw.is_a?(Hash)
61
55
 
62
56
  Array(raw["rules"])
63
57
  rescue Psych::Exception => e
64
- raise UsageError.new("candidate YAML parse error: #{e.message}")
58
+ Failure(code: :usage_error, message: "candidate YAML parse error: #{e.message}")
65
59
  end
66
60
  end
67
61
  end
@@ -3,31 +3,27 @@
3
3
  module Textus
4
4
  module Action
5
5
  class RuleList < Base
6
- extend Textus::Contract::DSL
7
-
8
6
  verb :rule_list
9
7
  summary "List every rule block in the manifest."
10
8
  surfaces :cli
11
9
  cli "rule list"
12
10
  view(:cli) { |policies| { "verb" => "rule_list", "policies" => policies } }
13
11
 
14
- def call(container:, **)
12
+ def self.call(container:, call:, **_options) # rubocop:disable Lint/UnusedMethodArgument
15
13
  manifest = container.manifest
16
- manifest.rules.blocks.map do |block|
14
+ Success(manifest.rules.blocks.map do |block|
17
15
  row = { "match" => block.match }
18
- self.class::LIST_FIELDS.each do |field|
16
+ LIST_FIELDS.each do |field|
19
17
  value = block.public_send(field)
20
18
  row[field.to_s] = serialize(field, value) unless value.nil?
21
19
  end
22
20
  row
23
- end
21
+ end)
24
22
  end
25
23
 
26
24
  LIST_FIELDS = Textus::Manifest::Schema::FIELD_REGISTRY.select { |_, m| m[:in_rule_list] }.keys.freeze
27
25
 
28
- private
29
-
30
- def serialize(field, value)
26
+ def self.serialize(field, value)
31
27
  case field
32
28
  when :retention
33
29
  { "ttl_seconds" => value.ttl_seconds, "action" => value.action.to_s }
@@ -3,8 +3,6 @@
3
3
  module Textus
4
4
  module Action
5
5
  class SchemaEnvelope < Base
6
- extend Textus::Contract::DSL
7
-
8
6
  verb :schema_show
9
7
  summary "Return the schema (field shape) for an entry's family, by key."
10
8
  surfaces :cli, :mcp
@@ -12,17 +10,12 @@ module Textus
12
10
  arg :key, String, required: true, positional: true,
13
11
  description: "any key in the family whose schema you want; returns required/optional fields and their types"
14
12
 
15
- def initialize(key:)
16
- super()
17
- @key = key
18
- end
19
-
20
- def call(container:, **)
13
+ def self.call(container:, key:, **)
21
14
  manifest = container.manifest
22
15
  schemas = container.schemas
23
- mentry = manifest.resolver.resolve(@key).entry
16
+ mentry = manifest.resolver.resolve(key).entry
24
17
  schema = schemas.fetch_or_nil(mentry.schema)
25
- { "protocol" => PROTOCOL, "key" => @key, "schema_ref" => mentry.schema, "schema" => schema&.to_h }
18
+ Success({ "protocol" => Textus::PROTOCOL, "key" => key, "schema_ref" => mentry.schema, "schema" => schema&.to_h })
26
19
  end
27
20
  end
28
21
  end
@@ -3,8 +3,6 @@
3
3
  module Textus
4
4
  module Action
5
5
  class Uid < Base
6
- extend Textus::Contract::DSL
7
-
8
6
  verb :uid
9
7
  summary "Return the stable UID of an entry without reading its body."
10
8
  surfaces :cli
@@ -12,21 +10,9 @@ module Textus
12
10
  arg :key, String, required: true, positional: true, description: "entry key"
13
11
  view(:cli) { |uid, inputs| { "key" => inputs[:key], "uid" => uid } }
14
12
 
15
- def initialize(key:)
16
- super()
17
- @key = key
18
- end
19
-
20
- def call(container:, call:)
21
- Textus::Action::Get.new(key: @key).call(container: container, call: call).uid
22
- end
23
-
24
- def self.new(*args, **kwargs)
25
- return super(**kwargs) unless args.any?
26
-
27
- positional = instance_method(:initialize).parameters.slice(:keyreq, :key).map(&:last)
28
- mapped = positional.zip(args).to_h
29
- super(**mapped.merge(kwargs))
13
+ def self.call(container:, call:, key:)
14
+ envelope = Value::Result.unwrap(Textus::Action::Get.call(container: container, call: call, key: key))
15
+ Success(envelope.uid)
30
16
  end
31
17
  end
32
18
  end
@@ -3,33 +3,18 @@
3
3
  module Textus
4
4
  module Action
5
5
  class Where < Base
6
- extend Textus::Contract::DSL
7
-
8
6
  verb :where
9
7
  summary "Resolve a key to its zone, owner, and path without reading the body."
10
8
  surfaces :cli, :mcp
11
9
  arg :key, String, required: true, positional: true,
12
10
  description: "dotted key to locate (returns zone, owner, path; does not read content)"
13
11
 
14
- def initialize(key:)
15
- super()
16
- @key = key
17
- end
18
-
19
- def call(container:, call: nil) # rubocop:disable Lint/UnusedMethodArgument
12
+ def self.call(container:, key:, call: nil) # rubocop:disable Lint/UnusedMethodArgument
20
13
  manifest = container.manifest
21
- res = manifest.resolver.resolve(@key)
14
+ res = manifest.resolver.resolve(key)
22
15
  mentry = res.entry
23
16
  path = res.path
24
- { "protocol" => PROTOCOL, "key" => @key, "lane" => mentry.lane, "owner" => mentry.owner, "path" => path }
25
- end
26
-
27
- def self.new(*args, **kwargs)
28
- return super(**kwargs) unless args.any?
29
-
30
- positional = instance_method(:initialize).parameters.slice(:keyreq, :key).map(&:last)
31
- mapped = positional.zip(args).to_h
32
- super(**mapped.merge(kwargs))
17
+ Success({ "protocol" => Textus::PROTOCOL, "key" => key, "lane" => mentry.lane, "owner" => mentry.owner, "path" => path })
33
18
  end
34
19
  end
35
20
  end
data/lib/textus/boot.rb CHANGED
@@ -90,8 +90,8 @@ module Textus
90
90
  propose_lane = manifest.policy.propose_lane_for(agent_role)
91
91
 
92
92
  {
93
- "read_verbs" => Textus::Surfaces::MCP::Catalog.read_verbs,
94
- "write_verbs" => agent_role ? Textus::Surfaces::MCP::Catalog.write_verbs : [],
93
+ "read_verbs" => Textus::Surface::MCP::Catalog.read_verbs,
94
+ "write_verbs" => agent_role ? Textus::Surface::MCP::Catalog.write_verbs : [],
95
95
  "writable_lanes" => writable_lanes,
96
96
  "propose_lane" => propose_lane,
97
97
  "latest_seq" => audit_log.latest_seq,
@@ -150,7 +150,7 @@ module Textus
150
150
 
151
151
  def self.build(container:)
152
152
  manifest = container.manifest
153
- etag = Textus::Etag.for_contract(container.root)
153
+ etag = Textus::Value::Etag.for_contract(container.root)
154
154
 
155
155
  {
156
156
  "protocol" => PROTOCOL_ID,
@@ -160,7 +160,6 @@ module Textus
160
160
  "agent_quickstart" => agent_quickstart(manifest, container.audit_log),
161
161
  "orientation" => read_artifact_content(container, "artifacts.config.orientation"),
162
162
  "context" => read_boot_context(container),
163
- "index_key" => index_key_if_present(container),
164
163
  "agent_protocol" => agent_protocol(manifest),
165
164
  }.compact
166
165
  end
@@ -169,8 +168,8 @@ module Textus
169
168
  res = container.manifest.resolver.resolve(key)
170
169
  return nil unless res.path && File.exist?(res.path)
171
170
 
172
- call = Textus::Call.build(role: Textus::Role::DEFAULT)
173
- env = Textus::Action::Get.new(key: key).call(container: container, call: call)
171
+ call = Textus::Value::Call.build(role: Textus::Value::Role::DEFAULT)
172
+ env = Textus::Action::Get.call(container: container, call: call, key: key)
174
173
  env&.content
175
174
  rescue Textus::Error
176
175
  nil
@@ -180,21 +179,14 @@ module Textus
180
179
  res = container.manifest.resolver.resolve("knowledge.boot")
181
180
  return nil unless res.path && File.exist?(res.path)
182
181
 
183
- call = Textus::Call.build(role: Textus::Role::DEFAULT)
184
- env = Textus::Action::Get.new(key: "knowledge.boot").call(container: container, call: call)
182
+ call = Textus::Value::Call.build(role: Textus::Value::Role::DEFAULT)
183
+ env = Textus::Action::Get.call(container: container, call: call, key: "knowledge.boot")
185
184
  body = env&.body&.strip
186
185
  body.nil? || body.empty? ? nil : body
187
186
  rescue Textus::Error
188
187
  nil
189
188
  end
190
189
 
191
- def self.index_key_if_present(container)
192
- res = container.manifest.resolver.resolve("artifacts.system.index")
193
- res.path && File.exist?(res.path) ? "artifacts.system.index" : nil
194
- rescue Textus::Error
195
- nil
196
- end
197
-
198
190
  def self.lanes_for(manifest)
199
191
  manifest.data.declared_lane_kinds.keys.map do |name|
200
192
  verb = manifest.policy.verb_for_lane(name)
@@ -0,0 +1,10 @@
1
+ module Textus
2
+ module Contract
3
+ Arg = Data.define(
4
+ :name, :type, :required, :positional, :session_default,
5
+ :description, :wire_name, :default, :source, :coerce, :cli_default
6
+ ) do
7
+ def wire = wire_name || name
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,88 @@
1
+ module Textus
2
+ module Contract
3
+ module DSL
4
+ def verb(name = nil)
5
+ if name
6
+ raise "contract already built; declare verb before reading .contract" if defined?(@contract) && @contract
7
+
8
+ @__verb = name
9
+ else
10
+ @__verb
11
+ end
12
+ end
13
+
14
+ def summary(text = nil)
15
+ if text
16
+ raise "contract already built; declare summary before reading .contract" if defined?(@contract) && @contract
17
+
18
+ @__summary = text
19
+ else
20
+ @__summary
21
+ end
22
+ end
23
+
24
+ def surfaces(*list)
25
+ if list.empty?
26
+ @__surfaces ||= []
27
+ else
28
+ raise "contract already built; declare surfaces before reading .contract" if defined?(@contract) && @contract
29
+
30
+ @__surfaces = list
31
+ end
32
+ end
33
+
34
+ def cli(path = nil)
35
+ if path
36
+ raise "contract already built; declare cli before reading .contract" if defined?(@contract) && @contract
37
+
38
+ @__cli = path.to_s
39
+ else
40
+ @__cli
41
+ end
42
+ end
43
+
44
+ def arg(name, type, required: false, positional: false, session_default: nil, description: nil, wire_name: nil, default: nil, source: nil, coerce: nil, cli_default: :__unset) # rubocop:disable Metrics/ParameterLists,Layout/LineLength
45
+ raise "contract already built; declare args before reading .contract" if defined?(@contract) && @contract
46
+
47
+ (@__args ||= []) << Arg.new(
48
+ name: name, type: type, required: required,
49
+ positional: positional, session_default: session_default,
50
+ description: description, wire_name: wire_name, default: default,
51
+ source: source, coerce: coerce, cli_default: cli_default
52
+ )
53
+ end
54
+
55
+ def cli_stdin(mode = :__read)
56
+ return @__cli_stdin if mode == :__read
57
+
58
+ raise "contract already built; declare cli_stdin before reading .contract" if defined?(@contract) && @contract
59
+
60
+ @__cli_stdin = mode
61
+ end
62
+
63
+ def view(surface = :default, &blk)
64
+ return (@__views ||= {})[surface] unless blk
65
+
66
+ raise "contract already built; declare view before reading .contract" if defined?(@contract) && @contract
67
+
68
+ (@__views ||= {})[surface] = blk
69
+ end
70
+
71
+ def contract?
72
+ !@__verb.nil?
73
+ end
74
+
75
+ def contract
76
+ @contract ||= Spec.new(
77
+ verb: @__verb,
78
+ summary: @__summary,
79
+ args: (@__args || []).freeze,
80
+ surfaces: (@__surfaces || []).freeze,
81
+ views: ((@__views ||= {})[:default] ||= ->(v, _i) { v }) && @__views,
82
+ cli: @__cli,
83
+ cli_stdin: @__cli_stdin,
84
+ )
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,25 @@
1
+ module Textus
2
+ module Contract
3
+ Spec = Data.define(:verb, :summary, :args, :surfaces, :views, :cli, :cli_stdin) do
4
+ def mcp? = surfaces.include?(:mcp)
5
+ def cli? = surfaces.include?(:cli)
6
+
7
+ def view(surface = :default) = views[surface] || views.fetch(:default)
8
+ def cli_path = cli || verb.to_s
9
+ def cli_words = cli_path.split
10
+ def cli_group = cli_words.size > 1 ? cli_words.first : nil
11
+ def cli_leaf = cli_words.last
12
+
13
+ def required_args = args.select(&:required)
14
+
15
+ def input_schema
16
+ props = args.to_h do |a|
17
+ h = { "type" => Contract.json_type(a.type) }
18
+ h["description"] = a.description if a.description
19
+ [a.wire.to_s, h]
20
+ end
21
+ { type: "object", properties: props, required: required_args.map { |a| a.wire.to_s } }
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,29 +1,5 @@
1
1
  module Textus
2
- # Declarative, co-located interface contract for a verb. One source of truth
3
- # for the agent-facing summary, the argument schema, which transports expose
4
- # the verb, and how the return value is shaped for the wire. CLI/Ruby/MCP and
5
- # boot project from this; the MCP catalog is fully derived from it (ADR 0039).
6
2
  module Contract
7
- # One argument of a verb. `positional: true` means it is passed to the
8
- # use-case as a positional (e.g. `get(key)`); otherwise as a keyword.
9
- # `session_default` names a zero-arg method on `Textus::Session` (Symbol)
10
- # that supplies the value when the wire arg is absent; `nil` means no default.
11
- # `wire_name` is the name the arg carries on the wire (MCP JSON property / CLI
12
- # envelope key) when it must differ from the use-case kwarg `name` — e.g. `put`
13
- # takes the `meta:` kwarg but exposes `_meta` on the wire to match what `get`
14
- # returns and what the CLI `--stdin` envelope already speaks (ADR 0057).
15
- # `source: :file` (CLI only) reads the arg's value as a path -> file
16
- # contents; `coerce:` is a callable applied to the raw value (CLI only);
17
- # `cli_default:` supplies a CLI-specific default that diverges from the
18
- # contract `default` the agent surfaces use (`:__unset` sentinel = none).
19
- Arg = Data.define(
20
- :name, :type, :required, :positional, :session_default,
21
- :description, :wire_name, :default, :source, :coerce, :cli_default
22
- ) do
23
- # The name used on the wire (defaults to the kwarg name).
24
- def wire = wire_name || name
25
- end
26
-
27
3
  JSON_TYPES = {
28
4
  String => "string", Integer => "integer", Hash => "object",
29
5
  Array => "array", :boolean => "boolean"
@@ -32,143 +8,5 @@ module Textus
32
8
  def self.json_type(type)
33
9
  JSON_TYPES.fetch(type) { raise ArgumentError.new("no JSON type mapping for #{type.inspect}") }
34
10
  end
35
-
36
- Spec = Data.define(:verb, :summary, :args, :surfaces, :views, :cli, :around, :cli_stdin) do
37
- def mcp? = surfaces.include?(:mcp)
38
- def cli? = surfaces.include?(:cli)
39
-
40
- # The output shaper for a surface; falls back to the default view. Every
41
- # view is invoked uniformly as `view.call(result, inputs)` — a view that
42
- # declares one parameter ignores `inputs` (procs tolerate extra args).
43
- def view(surface = :default) = views[surface] || views.fetch(:default)
44
-
45
- # Operator-facing command path. Defaults to the verb token; grouped verbs
46
- # declare e.g. `cli "schema show"`.
47
- def cli_path = cli || verb.to_s
48
- def cli_words = cli_path.split
49
- def cli_group = cli_words.size > 1 ? cli_words.first : nil
50
- def cli_leaf = cli_words.last
51
-
52
- def required_args = args.select(&:required)
53
-
54
- # JSON-Schema object for MCP tools/list inputSchema.
55
- # Outer keys (:type, :properties, :required) are symbols; inner property
56
- # keys are strings — matches the MCP/JSON wire shape expected by clients.
57
- def input_schema
58
- props = args.to_h do |a|
59
- h = { "type" => Contract.json_type(a.type) }
60
- h["description"] = a.description if a.description
61
- [a.wire.to_s, h]
62
- end
63
- { type: "object", properties: props, required: required_args.map { |a| a.wire.to_s } }
64
- end
65
- end
66
-
67
- # Mixed onto a use-case class via `extend`. Calls accumulate into ivars,
68
- # frozen into a Spec on first read of `.contract`.
69
- module DSL
70
- def verb(name = nil)
71
- if name
72
- raise "contract already built; declare verb before reading .contract" if defined?(@__contract) && @__contract
73
-
74
- @__verb = name
75
- else
76
- @__verb
77
- end
78
- end
79
-
80
- def summary(text = nil)
81
- if text
82
- raise "contract already built; declare summary before reading .contract" if defined?(@__contract) && @__contract
83
-
84
- @__summary = text
85
- else
86
- @__summary
87
- end
88
- end
89
-
90
- def surfaces(*list)
91
- if list.empty?
92
- @__surfaces ||= []
93
- else
94
- raise "contract already built; declare surfaces before reading .contract" if defined?(@__contract) && @__contract
95
-
96
- @__surfaces = list
97
- end
98
- end
99
-
100
- def cli(path = nil)
101
- if path
102
- raise "contract already built; declare cli before reading .contract" if defined?(@__contract) && @__contract
103
-
104
- @__cli = path.to_s
105
- else
106
- @__cli
107
- end
108
- end
109
-
110
- # Declare a stateful wrapper resource (Contract::Around) to run around
111
- # dispatch — e.g. `around :cursor` (pulse) or `around :build_lock` (build).
112
- def around(name = nil)
113
- return @__around unless name
114
-
115
- raise "contract already built; declare around before reading .contract" if defined?(@__contract) && @__contract
116
-
117
- @__around = name
118
- end
119
-
120
- def arg(name, type, required: false, positional: false, session_default: nil, description: nil, wire_name: nil, default: nil, source: nil, coerce: nil, cli_default: :__unset) # rubocop:disable Metrics/ParameterLists,Layout/LineLength
121
- raise "contract already built; declare args before reading .contract" if defined?(@__contract) && @__contract
122
-
123
- (@__args ||= []) << Arg.new(
124
- name: name, type: type, required: required,
125
- positional: positional, session_default: session_default,
126
- description: description, wire_name: wire_name, default: default,
127
- source: source, coerce: coerce, cli_default: cli_default
128
- )
129
- end
130
-
131
- # Verb-level: the CLI reads its inputs from a stdin envelope of this mode.
132
- # `:json` parses stdin as a JSON object and distributes its keys to args
133
- # by wire-name. nil means no stdin acquisition.
134
- def cli_stdin(mode = :__read)
135
- return @__cli_stdin if mode == :__read
136
-
137
- raise "contract already built; declare cli_stdin before reading .contract" if defined?(@__contract) && @__contract
138
-
139
- @__cli_stdin = mode
140
- end
141
-
142
- # Declare an output shaper. `view { ... }` is the default (MCP + Ruby);
143
- # `view(:cli) { ... }` overrides for the CLI. Both receive (result, inputs).
144
- def view(surface = :default, &blk)
145
- return (@__views ||= {})[surface] unless blk
146
-
147
- raise "contract already built; declare view before reading .contract" if defined?(@__contract) && @__contract
148
-
149
- (@__views ||= {})[surface] = blk
150
- end
151
-
152
- def contract?
153
- !@__verb.nil?
154
- end
155
-
156
- # rubocop:disable Naming/MemoizedInstanceVariableName
157
- # @__contract uses double-underscore to match the other accumulator ivars
158
- # (@__verb, @__args, etc.) and avoid name collision with user-defined `@contract`.
159
- def contract
160
- @__contract ||= Spec.new(
161
- verb: @__verb,
162
- summary: @__summary,
163
- args: (@__args || []).freeze,
164
- surfaces: (@__surfaces || []).freeze,
165
- views: ((@__views ||= {})[:default] ||= ->(v, _i) { v }) && @__views,
166
- cli: @__cli,
167
- around: @__around,
168
- cli_stdin: @__cli_stdin,
169
- )
170
- end
171
- # rubocop:enable Naming/MemoizedInstanceVariableName
172
- end
173
11
  end
174
12
  end
@@ -3,8 +3,8 @@ module Textus
3
3
  class Check
4
4
  class AuditLog < Check
5
5
  def call
6
- path = Textus::Layout.audit_log(root)
7
- Textus::Ports::AuditLog.new(root).verify_integrity.map do |v|
6
+ path = Textus::Store::Geometry.new(root).audit_log_path
7
+ Textus::Port::AuditLog.new(root).verify_integrity.map do |v|
8
8
  {
9
9
  "code" => "audit.parse_error",
10
10
  "level" => "warning",
@@ -10,8 +10,8 @@ module Textus
10
10
  def call
11
11
  gen = Textus::Core::Freshness::Evaluator.new(
12
12
  manifest: manifest,
13
- file_stat: Textus::Ports::Storage::FileStat.new,
14
- clock: Textus::Ports::Clock.new,
13
+ file_stat: Textus::Port::Storage::FileStat.new,
14
+ clock: Textus::Port::Clock.new,
15
15
  )
16
16
  manifest.data.entries.flat_map { |m| gen.drift_rows(m) }.map do |row|
17
17
  {