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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +37 -0
- data/README.md +8 -1
- data/SPEC.md +27 -0
- data/docs/architecture/README.md +20 -8
- data/docs/reference/conventions.md +1 -1
- data/exe/textus +1 -1
- data/lib/textus/action/accept.rb +23 -21
- data/lib/textus/action/audit.rb +24 -61
- data/lib/textus/action/base.rb +9 -9
- data/lib/textus/action/blame.rb +18 -36
- data/lib/textus/action/boot.rb +2 -4
- data/lib/textus/action/data_mv.rb +20 -31
- data/lib/textus/action/deps.rb +3 -18
- data/lib/textus/action/doctor.rb +2 -9
- data/lib/textus/action/drain.rb +11 -19
- data/lib/textus/action/enqueue.rb +14 -30
- data/lib/textus/action/get.rb +12 -56
- data/lib/textus/action/ingest.rb +74 -78
- data/lib/textus/action/jobs.rb +6 -15
- data/lib/textus/action/key_delete.rb +6 -16
- data/lib/textus/action/key_delete_prefix.rb +8 -17
- data/lib/textus/action/key_mv.rb +54 -61
- data/lib/textus/action/key_mv_prefix.rb +13 -22
- data/lib/textus/action/list.rb +7 -21
- data/lib/textus/action/propose.rb +16 -26
- data/lib/textus/action/published.rb +3 -5
- data/lib/textus/action/pulse.rb +19 -26
- data/lib/textus/action/put.rb +15 -29
- data/lib/textus/action/rdeps.rb +3 -18
- data/lib/textus/action/reject.rb +12 -21
- data/lib/textus/action/rule_explain.rb +12 -22
- data/lib/textus/action/rule_lint.rb +10 -16
- data/lib/textus/action/rule_list.rb +5 -9
- data/lib/textus/action/schema_envelope.rb +3 -10
- data/lib/textus/action/uid.rb +3 -17
- data/lib/textus/action/where.rb +3 -18
- data/lib/textus/boot.rb +7 -15
- data/lib/textus/contract/arg.rb +10 -0
- data/lib/textus/contract/dsl.rb +88 -0
- data/lib/textus/contract/spec.rb +25 -0
- data/lib/textus/contract.rb +0 -162
- data/lib/textus/doctor/check/audit_log.rb +2 -2
- data/lib/textus/doctor/check/generator_drift.rb +2 -2
- data/lib/textus/doctor/check/orphaned_publish_targets.rb +3 -3
- data/lib/textus/doctor/check/protocol_version.rb +2 -2
- data/lib/textus/doctor/check/raw_asset_paths.rb +1 -1
- data/lib/textus/doctor/check/schema_parse_error.rb +1 -1
- data/lib/textus/doctor/check/schema_violations.rb +2 -2
- data/lib/textus/doctor/check/schemas.rb +1 -1
- data/lib/textus/doctor/check/sentinels.rb +4 -4
- data/lib/textus/doctor/check/templates.rb +1 -1
- data/lib/textus/doctor/check/unowned_schema_fields.rb +1 -1
- data/lib/textus/doctor/check.rb +4 -7
- data/lib/textus/doctor.rb +1 -1
- data/lib/textus/errors.rb +6 -0
- data/lib/textus/format/base.rb +0 -4
- data/lib/textus/format/json.rb +5 -6
- data/lib/textus/format/markdown.rb +5 -6
- data/lib/textus/format/shared.rb +17 -0
- data/lib/textus/format/text.rb +5 -4
- data/lib/textus/format/yaml.rb +30 -6
- data/lib/textus/format.rb +6 -0
- data/lib/textus/gate/auth.rb +2 -17
- data/lib/textus/gate/binder.rb +50 -0
- data/lib/textus/gate.rb +64 -88
- data/lib/textus/init.rb +2 -4
- data/lib/textus/jobs.rb +3 -9
- data/lib/textus/manifest/capabilities.rb +3 -3
- data/lib/textus/manifest/entry/base.rb +1 -1
- data/lib/textus/manifest/entry/publish/subtree_mirror.rb +2 -2
- data/lib/textus/manifest/entry/publish/to_paths.rb +1 -1
- data/lib/textus/manifest/schema/semantics/cross_field.rb +53 -0
- data/lib/textus/manifest/schema/semantics/invariants.rb +125 -0
- data/lib/textus/manifest/schema/semantics/migration.rb +83 -0
- data/lib/textus/manifest/schema/semantics.rb +11 -216
- data/lib/textus/meta.rb +54 -0
- data/lib/textus/{ports → port}/audit_log.rb +44 -4
- data/lib/textus/{ports → port}/build_lock.rb +2 -2
- data/lib/textus/{ports → port}/clock.rb +1 -1
- data/lib/textus/{ports → port}/publisher.rb +5 -5
- data/lib/textus/{ports → port}/sentinel_store.rb +3 -3
- data/lib/textus/{ports → port}/storage/file_stat.rb +1 -1
- data/lib/textus/{ports → port}/storage/file_store.rb +2 -2
- data/lib/textus/port/store.rb +93 -0
- data/lib/textus/{ports → port}/watcher_lock.rb +3 -3
- data/lib/textus/produce/engine.rb +1 -1
- data/lib/textus/schema/tools.rb +11 -7
- data/lib/textus/store/compositor.rb +34 -0
- data/lib/textus/store/container.rb +43 -0
- data/lib/textus/store/cursor.rb +26 -0
- data/lib/textus/store/envelope/reader.rb +43 -0
- data/lib/textus/store/envelope/writer.rb +195 -0
- data/lib/textus/store/geometry.rb +81 -0
- data/lib/textus/store/index/builder.rb +74 -0
- data/lib/textus/store/index/lookup.rb +60 -0
- data/lib/textus/store/jobs/base.rb +13 -0
- data/lib/textus/store/jobs/index.rb +15 -0
- data/lib/textus/store/jobs/materialize.rb +15 -0
- data/lib/textus/store/jobs/plan.rb +11 -0
- data/lib/textus/store/jobs/planner.rb +104 -0
- data/lib/textus/store/jobs/queue.rb +154 -0
- data/lib/textus/store/jobs/registry.rb +19 -0
- data/lib/textus/store/jobs/retention.rb +50 -0
- data/lib/textus/store/jobs/sweep.rb +21 -0
- data/lib/textus/store/jobs/worker.rb +64 -0
- data/lib/textus/store/session.rb +37 -0
- data/lib/textus/store.rb +21 -13
- data/lib/textus/{surfaces → surface}/cli/group/data.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group/key.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group/mcp.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group/rule.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group/schema.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group.rb +2 -2
- data/lib/textus/{surfaces → surface}/cli/runner.rb +10 -63
- data/lib/textus/surface/cli/sources.rb +41 -0
- data/lib/textus/{surfaces → surface}/cli/verb/doctor.rb +4 -6
- data/lib/textus/{surfaces → surface}/cli/verb/get.rb +4 -4
- data/lib/textus/{surfaces → surface}/cli/verb/init.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/verb/mcp_serve.rb +3 -3
- data/lib/textus/{surfaces → surface}/cli/verb/put.rb +6 -11
- data/lib/textus/{surfaces → surface}/cli/verb/schema_diff.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/verb/schema_init.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/verb/schema_migrate.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/verb/watch.rb +2 -2
- data/lib/textus/{surfaces → surface}/cli/verb.rb +3 -8
- data/lib/textus/{surfaces → surface}/cli.rb +1 -1
- data/lib/textus/{surfaces → surface}/mcp/catalog.rb +9 -26
- data/lib/textus/{surfaces → surface}/mcp/errors.rb +1 -1
- data/lib/textus/{surfaces → surface}/mcp/server.rb +5 -5
- data/lib/textus/{surfaces → surface}/mcp.rb +2 -2
- data/lib/textus/surface/projector.rb +27 -0
- data/lib/textus/{surfaces → surface}/role_scope.rb +1 -1
- data/lib/textus/{surfaces → surface}/watcher.rb +8 -8
- data/lib/textus/value/call.rb +30 -0
- data/lib/textus/value/command.rb +16 -0
- data/lib/textus/value/envelope.rb +89 -0
- data/lib/textus/value/etag.rb +39 -0
- data/lib/textus/value/result.rb +26 -0
- data/lib/textus/value/role.rb +38 -0
- data/lib/textus/value/types.rb +13 -0
- data/lib/textus/{uid.rb → value/uid.rb} +9 -7
- data/lib/textus/version.rb +1 -1
- data/lib/textus/workflow/loader.rb +4 -4
- data/lib/textus/workflow/runner.rb +4 -18
- data/lib/textus.rb +9 -10
- metadata +100 -63
- data/lib/textus/action/write_verb.rb +0 -44
- data/lib/textus/call.rb +0 -28
- data/lib/textus/command.rb +0 -41
- data/lib/textus/container.rb +0 -26
- data/lib/textus/contract/around.rb +0 -29
- data/lib/textus/contract/binder.rb +0 -88
- data/lib/textus/contract/resources/build_lock.rb +0 -17
- data/lib/textus/contract/resources/cursor.rb +0 -26
- data/lib/textus/contract/sources.rb +0 -39
- data/lib/textus/contract/view.rb +0 -15
- data/lib/textus/cursor_store.rb +0 -24
- data/lib/textus/envelope/reader.rb +0 -46
- data/lib/textus/envelope/writer.rb +0 -209
- data/lib/textus/envelope.rb +0 -79
- data/lib/textus/etag.rb +0 -36
- data/lib/textus/jobs/base.rb +0 -23
- data/lib/textus/jobs/materialize.rb +0 -20
- data/lib/textus/jobs/plan.rb +0 -9
- data/lib/textus/jobs/planner.rb +0 -101
- data/lib/textus/jobs/retention.rb +0 -48
- data/lib/textus/jobs/sweep.rb +0 -27
- data/lib/textus/jobs/worker.rb +0 -67
- data/lib/textus/layout.rb +0 -91
- data/lib/textus/ports/job_store/job.rb +0 -65
- data/lib/textus/ports/job_store.rb +0 -123
- data/lib/textus/ports/raw_index.rb +0 -61
- data/lib/textus/role.rb +0 -36
- data/lib/textus/session.rb +0 -35
- 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
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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 =
|
|
57
|
-
winners =
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
16
|
+
mentry = manifest.resolver.resolve(key).entry
|
|
24
17
|
schema = schemas.fetch_or_nil(mentry.schema)
|
|
25
|
-
{ "protocol" => PROTOCOL, "key" =>
|
|
18
|
+
Success({ "protocol" => Textus::PROTOCOL, "key" => key, "schema_ref" => mentry.schema, "schema" => schema&.to_h })
|
|
26
19
|
end
|
|
27
20
|
end
|
|
28
21
|
end
|
data/lib/textus/action/uid.rb
CHANGED
|
@@ -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
|
|
16
|
-
|
|
17
|
-
|
|
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
|
data/lib/textus/action/where.rb
CHANGED
|
@@ -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
|
|
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(
|
|
14
|
+
res = manifest.resolver.resolve(key)
|
|
22
15
|
mentry = res.entry
|
|
23
16
|
path = res.path
|
|
24
|
-
{ "protocol" => PROTOCOL, "key" =>
|
|
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::
|
|
94
|
-
"write_verbs" => agent_role ? Textus::
|
|
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.
|
|
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.
|
|
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,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
|
data/lib/textus/contract.rb
CHANGED
|
@@ -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::
|
|
7
|
-
Textus::
|
|
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::
|
|
14
|
-
clock: Textus::
|
|
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
|
{
|