textus 0.54.2 → 0.55.2
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 +38 -1
- data/README.md +17 -10
- data/SPEC.md +39 -11
- data/docs/architecture/README.md +23 -11
- data/docs/reference/conventions.md +5 -2
- data/exe/textus +1 -1
- data/lib/textus/boot.rb +64 -93
- data/lib/textus/dispatch/binder.rb +48 -0
- data/lib/textus/dispatch/contracts.rb +63 -0
- data/lib/textus/dispatch/handler_registry.rb +21 -0
- data/lib/textus/dispatch/middleware/audit_index.rb +51 -0
- data/lib/textus/dispatch/middleware/auth.rb +40 -0
- data/lib/textus/dispatch/middleware/base.rb +26 -0
- data/lib/textus/dispatch/middleware/binder.rb +20 -0
- data/lib/textus/dispatch/middleware/cascade.rb +53 -0
- data/lib/textus/dispatch/pipeline.rb +35 -0
- data/lib/textus/doctor/check/audit_log.rb +2 -2
- data/lib/textus/doctor/check/generator_drift.rb +4 -4
- 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/{notebook_sources.rb → scratchpad_sources.rb} +10 -5
- 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 +9 -10
- data/lib/textus/doctor.rb +2 -2
- data/lib/textus/errors.rb +8 -0
- data/lib/textus/format/base.rb +34 -10
- data/lib/textus/format/json.rb +5 -27
- data/lib/textus/format/markdown.rb +5 -27
- data/lib/textus/format/text.rb +5 -4
- data/lib/textus/format/yaml.rb +30 -27
- data/lib/textus/format.rb +22 -1
- data/lib/textus/handlers/maintenance/boot_store.rb +15 -0
- data/lib/textus/handlers/maintenance/doctor_store.rb +15 -0
- data/lib/textus/handlers/maintenance/drain_store.rb +21 -0
- data/lib/textus/handlers/maintenance/ingest_entry.rb +159 -0
- data/lib/textus/handlers/maintenance/jobs_action.rb +21 -0
- data/lib/textus/handlers/maintenance/published_entries.rb +17 -0
- data/lib/textus/handlers/maintenance/rule_explain.rb +77 -0
- data/lib/textus/handlers/maintenance/rule_lint.rb +54 -0
- data/lib/textus/handlers/maintenance/rule_list.rb +32 -0
- data/lib/textus/handlers/maintenance/schema_envelope.rb +19 -0
- data/lib/textus/handlers/read/audit_entries.rb +48 -0
- data/lib/textus/handlers/read/blame_entry.rb +71 -0
- data/lib/textus/handlers/read/deps_entry.rb +17 -0
- data/lib/textus/handlers/read/get_entry.rb +68 -0
- data/lib/textus/handlers/read/list_keys.rb +36 -0
- data/lib/textus/handlers/read/pulse_entries.rb +66 -0
- data/lib/textus/handlers/read/rdeps_entry.rb +21 -0
- data/lib/textus/handlers/read/uid_entry.rb +18 -0
- data/lib/textus/handlers/read/where_entry.rb +18 -0
- data/lib/textus/handlers/write/accept_proposal.rb +39 -0
- data/lib/textus/handlers/write/data_mv.rb +55 -0
- data/lib/textus/handlers/write/delete_key.rb +17 -0
- data/lib/textus/handlers/write/enqueue_job.rb +27 -0
- data/lib/textus/handlers/write/key_delete_prefix.rb +32 -0
- data/lib/textus/handlers/write/key_mv_prefix.rb +45 -0
- data/lib/textus/handlers/write/move_key.rb +80 -0
- data/lib/textus/handlers/write/propose_entry.rb +29 -0
- data/lib/textus/handlers/write/put_entry.rb +29 -0
- data/lib/textus/handlers/write/reject_proposal.rb +29 -0
- data/lib/textus/init.rb +5 -7
- data/lib/textus/jobs.rb +3 -9
- data/lib/textus/manifest/capabilities.rb +4 -4
- data/lib/textus/manifest/entry/base.rb +3 -3
- data/lib/textus/manifest/entry/publish/subtree_mirror.rb +2 -2
- data/lib/textus/manifest/entry/publish/to_paths.rb +2 -2
- data/lib/textus/manifest/policy/predicates/author_held.rb +22 -0
- data/lib/textus/manifest/policy/predicates/etag_match.rb +18 -0
- data/lib/textus/manifest/policy/predicates/fresh_within.rb +13 -0
- data/lib/textus/manifest/policy/predicates/lane_deletable_by.rb +31 -0
- data/lib/textus/manifest/policy/predicates/lane_writable_by.rb +23 -0
- data/lib/textus/manifest/policy/predicates/raw_lane_ingest_only.rb +25 -0
- data/lib/textus/manifest/policy/predicates/raw_write_once.rb +24 -0
- data/lib/textus/manifest/policy/predicates/schema_valid.rb +41 -0
- data/lib/textus/manifest/policy/predicates/target_is_canon.rb +20 -0
- data/lib/textus/manifest/policy/predicates.rb +54 -0
- data/lib/textus/manifest/policy/retention.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/orchestration.rb +55 -0
- data/lib/textus/{ports → port}/audit_log.rb +46 -6
- 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/{core → port}/sentinel.rb +1 -6
- data/lib/textus/{ports → port}/sentinel_store.rb +4 -4
- data/lib/textus/{ports → port}/storage/file_stat.rb +1 -1
- data/lib/textus/port/storage/file_store.rb +49 -0
- data/lib/textus/port/storage/interface.rb +17 -0
- data/lib/textus/port/store.rb +149 -0
- data/lib/textus/{ports → port}/watcher_lock.rb +3 -3
- data/lib/textus/produce/engine.rb +1 -11
- data/lib/textus/produce/publisher.rb +21 -0
- data/lib/textus/schema/registry.rb +42 -0
- data/lib/textus/schema/tools.rb +7 -10
- data/lib/textus/store/container.rb +173 -0
- data/lib/textus/store/cursor.rb +26 -0
- data/lib/textus/store/entry/reader.rb +47 -0
- data/lib/textus/store/entry/writer.rb +219 -0
- data/lib/textus/store/envelope/meta.rb +61 -0
- data/lib/textus/store/freshness/drift_detector.rb +93 -0
- data/lib/textus/store/freshness/evaluator.rb +20 -0
- data/lib/textus/store/freshness/ttl_evaluator.rb +57 -0
- data/lib/textus/{core → store}/freshness/verdict.rb +1 -11
- data/lib/textus/store/freshness.rb +8 -0
- data/lib/textus/store/index/builder.rb +76 -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 +124 -0
- data/lib/textus/store/jobs/queue.rb +162 -0
- data/lib/textus/store/jobs/registry.rb +19 -0
- data/lib/textus/store/jobs/retention/base.rb +52 -0
- data/lib/textus/store/jobs/retention/sweep.rb +55 -0
- data/lib/textus/store/jobs/retention.rb +8 -0
- data/lib/textus/store/jobs/sweep.rb +21 -0
- data/lib/textus/store/jobs/worker.rb +64 -0
- data/lib/textus/store/layout.rb +97 -0
- data/lib/textus/store.rb +63 -32
- 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 +14 -68
- data/lib/textus/surface/cli/sources.rb +41 -0
- data/lib/textus/{surfaces → surface}/cli/verb/doctor.rb +5 -6
- data/lib/textus/{surfaces → surface}/cli/verb/get.rb +7 -5
- 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/surface/cli/verb/put.rb +27 -0
- 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/surface/mcp/catalog.rb +58 -0
- data/lib/textus/{surfaces → surface}/mcp/errors.rb +1 -11
- data/lib/textus/surface/mcp/projector.rb +20 -0
- data/lib/textus/{surfaces → surface}/mcp/server.rb +24 -35
- data/lib/textus/{surfaces → surface}/mcp.rb +2 -2
- 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/{core → value}/duration.rb +1 -4
- data/lib/textus/value/envelope.rb +90 -0
- data/lib/textus/value/etag.rb +39 -0
- data/lib/textus/value/payload.rb +7 -0
- data/lib/textus/value/result.rb +46 -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/verb_registry.rb +417 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus/workflow/loader.rb +4 -4
- data/lib/textus/workflow/runner.rb +11 -33
- data/lib/textus.rb +4 -69
- metadata +142 -105
- data/lib/textus/action/accept.rb +0 -44
- data/lib/textus/action/audit.rb +0 -131
- data/lib/textus/action/base.rb +0 -42
- data/lib/textus/action/blame.rb +0 -97
- data/lib/textus/action/boot.rb +0 -17
- data/lib/textus/action/data_mv.rb +0 -69
- data/lib/textus/action/deps.rb +0 -34
- data/lib/textus/action/doctor.rb +0 -24
- data/lib/textus/action/drain.rb +0 -39
- data/lib/textus/action/enqueue.rb +0 -53
- data/lib/textus/action/get.rb +0 -78
- data/lib/textus/action/ingest.rb +0 -203
- data/lib/textus/action/jobs.rb +0 -36
- data/lib/textus/action/key_delete.rb +0 -36
- data/lib/textus/action/key_delete_prefix.rb +0 -44
- data/lib/textus/action/key_mv.rb +0 -129
- data/lib/textus/action/key_mv_prefix.rb +0 -57
- data/lib/textus/action/list.rb +0 -42
- data/lib/textus/action/propose.rb +0 -52
- data/lib/textus/action/published.rb +0 -24
- data/lib/textus/action/pulse.rb +0 -56
- data/lib/textus/action/put.rb +0 -52
- data/lib/textus/action/rdeps.rb +0 -39
- data/lib/textus/action/reject.rb +0 -37
- data/lib/textus/action/rule_explain.rb +0 -91
- data/lib/textus/action/rule_lint.rb +0 -68
- data/lib/textus/action/rule_list.rb +0 -42
- data/lib/textus/action/schema_envelope.rb +0 -29
- data/lib/textus/action/uid.rb +0 -33
- data/lib/textus/action/where.rb +0 -36
- 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/contract.rb +0 -174
- data/lib/textus/core/freshness/evaluator.rb +0 -150
- data/lib/textus/core/freshness.rb +0 -11
- data/lib/textus/core/retention/sweep.rb +0 -57
- data/lib/textus/core/retention.rb +0 -11
- 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/gate/auth.rb +0 -227
- data/lib/textus/gate.rb +0 -116
- 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/ports/storage/file_store.rb +0 -26
- data/lib/textus/role.rb +0 -36
- data/lib/textus/schemas.rb +0 -54
- data/lib/textus/session.rb +0 -35
- data/lib/textus/surfaces/cli/verb/put.rb +0 -30
- data/lib/textus/surfaces/mcp/catalog.rb +0 -111
- data/lib/textus/surfaces/role_scope.rb +0 -34
- data/lib/textus/types.rb +0 -15
data/lib/textus/boot.rb
CHANGED
|
@@ -57,12 +57,8 @@ module Textus
|
|
|
57
57
|
{ "name" => "pulse" },
|
|
58
58
|
].freeze
|
|
59
59
|
|
|
60
|
-
# verb token => contract.summary, for every Dispatcher verb that carries a
|
|
61
|
-
# contract. The single source for a verb's one-line summary (ADR 0039).
|
|
62
60
|
def self.contract_summaries
|
|
63
|
-
Textus::
|
|
64
|
-
.select { |k| k.respond_to?(:contract?) && k.contract? }
|
|
65
|
-
.to_h { |k| [k.contract.verb.to_s, k.contract.summary] }
|
|
61
|
+
Textus::VerbRegistry.registered.to_h { |s| [s.verb.to_s, s.summary] }
|
|
66
62
|
end
|
|
67
63
|
|
|
68
64
|
# Build the CLI verb catalog: each summary is derived from its contract when
|
|
@@ -76,69 +72,70 @@ module Textus
|
|
|
76
72
|
end
|
|
77
73
|
end
|
|
78
74
|
|
|
79
|
-
def self.
|
|
80
|
-
|
|
75
|
+
def self.build(container:)
|
|
76
|
+
etag = Textus::Value::Etag.for_contract(container.root)
|
|
77
|
+
latest_seq = container.audit_log.latest_seq
|
|
78
|
+
artifact = read_artifact_content(container, "artifacts.boot")
|
|
79
|
+
context = read_boot_context(container)
|
|
80
|
+
|
|
81
|
+
# Prefer pre-computed artifact (drain computes, boot reads).
|
|
82
|
+
# Fall back to inline manifest projection for stores that have not yet
|
|
83
|
+
# run drain (test fixtures, fresh inits).
|
|
84
|
+
stable = artifact || inline_boot_content(container.manifest, latest_seq)
|
|
85
|
+
|
|
86
|
+
if stable["agent_quickstart"]
|
|
87
|
+
stable = stable.merge(
|
|
88
|
+
"agent_quickstart" => stable["agent_quickstart"].merge("latest_seq" => latest_seq),
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
payload = {
|
|
93
|
+
"protocol" => PROTOCOL_ID,
|
|
94
|
+
"store_root" => container.root,
|
|
95
|
+
"contract_etag" => etag,
|
|
96
|
+
}.merge(stable)
|
|
97
|
+
payload["context"] = context if context
|
|
98
|
+
payload
|
|
99
|
+
end
|
|
81
100
|
|
|
82
|
-
|
|
101
|
+
def self.inline_boot_content(manifest, _latest_seq)
|
|
102
|
+
agent_role = manifest.policy.proposer_role
|
|
103
|
+
writable_lanes = manifest.data.declared_lane_kinds.keys.each_with_object([]) do |ln, acc|
|
|
83
104
|
next unless agent_role
|
|
84
105
|
|
|
85
|
-
verb
|
|
106
|
+
verb = manifest.policy.verb_for_lane(ln)
|
|
86
107
|
writers = manifest.policy.roles_with_capability(verb)
|
|
87
|
-
acc <<
|
|
108
|
+
acc << ln if writers.include?(agent_role)
|
|
88
109
|
end
|
|
89
110
|
|
|
90
|
-
propose_lane = manifest.policy.propose_lane_for(agent_role)
|
|
91
|
-
|
|
92
|
-
{
|
|
93
|
-
"read_verbs" => Textus::Surfaces::MCP::Catalog.read_verbs,
|
|
94
|
-
"write_verbs" => agent_role ? Textus::Surfaces::MCP::Catalog.write_verbs : [],
|
|
95
|
-
"writable_lanes" => writable_lanes,
|
|
96
|
-
"propose_lane" => propose_lane,
|
|
97
|
-
"latest_seq" => audit_log.latest_seq,
|
|
98
|
-
}
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def self.recipes(manifest)
|
|
102
|
-
queue = manifest.policy.queue_lane
|
|
103
|
-
feeds = lane_label(manifest, :machine, "the machine lane")
|
|
104
111
|
{
|
|
105
|
-
"
|
|
106
|
-
|
|
107
|
-
"
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
},
|
|
112
|
-
"write" => {
|
|
113
|
-
"purpose" => "create or update an entry",
|
|
114
|
-
"steps" => [
|
|
115
|
-
"schema KEY — learn the _meta field shape (required, optional, field types) before writing",
|
|
116
|
-
"assemble an envelope: { _meta: {…}, body: \"…\" }",
|
|
117
|
-
"put KEY — persist it (role-gated); pass if_etag to guard a concurrent edit",
|
|
118
|
-
],
|
|
119
|
-
},
|
|
120
|
-
"propose" => {
|
|
121
|
-
"purpose" => "agent suggests a change for human review",
|
|
122
|
-
"agent_steps" => [
|
|
123
|
-
"propose KEY — writes the change into the #{queue} lane for review",
|
|
124
|
-
],
|
|
125
|
-
"human_steps" => [
|
|
126
|
-
"accept #{queue}.KEY — promotes the proposal into its target lane",
|
|
127
|
-
],
|
|
128
|
-
},
|
|
129
|
-
"drain" => {
|
|
130
|
-
"purpose" => "keep the machine-maintained lanes fresh — re-pull stale intake entries from their declared source",
|
|
131
|
-
"steps" => [
|
|
132
|
-
"pulse — its `stale` list names entries past their ttl",
|
|
133
|
-
"drain (lane: #{feeds}) — re-pull the stale entries",
|
|
134
|
-
],
|
|
112
|
+
"lanes" => lanes_for(manifest),
|
|
113
|
+
"agent_quickstart" => {
|
|
114
|
+
"read_verbs" => Textus::Surface::MCP::Catalog.read_verbs,
|
|
115
|
+
"write_verbs" => agent_role ? Textus::Surface::MCP::Catalog.write_verbs : [],
|
|
116
|
+
"writable_lanes" => writable_lanes,
|
|
117
|
+
"propose_lane" => manifest.policy.propose_lane_for(agent_role),
|
|
135
118
|
},
|
|
119
|
+
"agent_protocol" => agent_protocol(manifest),
|
|
136
120
|
}
|
|
137
121
|
end
|
|
138
122
|
|
|
139
123
|
def self.agent_protocol(manifest)
|
|
124
|
+
queue = manifest.policy.queue_lane
|
|
125
|
+
feeds = lane_label(manifest, :machine, "the machine lane")
|
|
140
126
|
AGENT_PROTOCOL_TEMPLATE.merge(
|
|
141
|
-
"recipes" =>
|
|
127
|
+
"recipes" => {
|
|
128
|
+
"read" => { "purpose" => "find and read an entry",
|
|
129
|
+
"steps" => ["list (lane:, prefix:) — discover keys", "get KEY — returns the entry envelope"] },
|
|
130
|
+
"write" => { "purpose" => "create or update an entry",
|
|
131
|
+
"steps" => ["schema KEY — learn field shape", "put KEY — persist it (role-gated)"] },
|
|
132
|
+
"propose" => { "purpose" => "agent suggests a change for human review",
|
|
133
|
+
"agent_steps" => ["propose KEY — writes to #{queue} lane"],
|
|
134
|
+
"human_steps" => ["accept #{queue}.KEY — promotes to target lane"] },
|
|
135
|
+
"drain" => { "purpose" => "keep machine lanes fresh",
|
|
136
|
+
"steps" => ["pulse — stale list names overdue entries",
|
|
137
|
+
"drain (lane: #{feeds}) — re-pull stale entries"] },
|
|
138
|
+
},
|
|
142
139
|
"role_resolution" => {
|
|
143
140
|
"summary" => "write role is resolved in order: --as flag, TEXTUS_ROLE env var, .textus/role file, " \
|
|
144
141
|
"then a transport default ('human' for CLI, 'agent' for MCP)",
|
|
@@ -148,29 +145,23 @@ module Textus
|
|
|
148
145
|
)
|
|
149
146
|
end
|
|
150
147
|
|
|
151
|
-
def self.
|
|
152
|
-
manifest
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
"
|
|
157
|
-
|
|
158
|
-
"
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
"orientation" => read_artifact_content(container, "artifacts.config.orientation"),
|
|
162
|
-
"context" => read_boot_context(container),
|
|
163
|
-
"index_key" => index_key_if_present(container),
|
|
164
|
-
"agent_protocol" => agent_protocol(manifest),
|
|
165
|
-
}.compact
|
|
148
|
+
def self.lanes_for(manifest)
|
|
149
|
+
manifest.data.declared_lane_kinds.keys.map do |name|
|
|
150
|
+
verb = manifest.policy.verb_for_lane(name)
|
|
151
|
+
row = { "name" => name, "writers" => manifest.policy.roles_with_capability(verb) }
|
|
152
|
+
kind = manifest.policy.declared_kind(name)
|
|
153
|
+
row["kind"] = kind.to_s if kind
|
|
154
|
+
purpose = manifest.data.lane_descs[name]
|
|
155
|
+
row["purpose"] = purpose if purpose && !purpose.empty?
|
|
156
|
+
row
|
|
157
|
+
end
|
|
166
158
|
end
|
|
167
159
|
|
|
168
160
|
def self.read_artifact_content(container, key)
|
|
169
161
|
res = container.manifest.resolver.resolve(key)
|
|
170
162
|
return nil unless res.path && File.exist?(res.path)
|
|
171
163
|
|
|
172
|
-
|
|
173
|
-
env = Textus::Action::Get.new(key: key).call(container: container, call: call)
|
|
164
|
+
env = Textus::Store::Entry::Reader.from(container: container).read(key)
|
|
174
165
|
env&.content
|
|
175
166
|
rescue Textus::Error
|
|
176
167
|
nil
|
|
@@ -180,31 +171,11 @@ module Textus
|
|
|
180
171
|
res = container.manifest.resolver.resolve("knowledge.boot")
|
|
181
172
|
return nil unless res.path && File.exist?(res.path)
|
|
182
173
|
|
|
183
|
-
|
|
184
|
-
env = Textus::Action::Get.new(key: "knowledge.boot").call(container: container, call: call)
|
|
174
|
+
env = Textus::Store::Entry::Reader.from(container: container).read("knowledge.boot")
|
|
185
175
|
body = env&.body&.strip
|
|
186
176
|
body.nil? || body.empty? ? nil : body
|
|
187
177
|
rescue Textus::Error
|
|
188
178
|
nil
|
|
189
179
|
end
|
|
190
|
-
|
|
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
|
-
def self.lanes_for(manifest)
|
|
199
|
-
manifest.data.declared_lane_kinds.keys.map do |name|
|
|
200
|
-
verb = manifest.policy.verb_for_lane(name)
|
|
201
|
-
row = { "name" => name, "writers" => manifest.policy.roles_with_capability(verb) }
|
|
202
|
-
kind = manifest.policy.declared_kind(name)
|
|
203
|
-
row["kind"] = kind.to_s if kind
|
|
204
|
-
purpose = manifest.data.lane_descs[name]
|
|
205
|
-
row["purpose"] = purpose if purpose && !purpose.empty?
|
|
206
|
-
row
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
180
|
end
|
|
210
181
|
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Dispatch
|
|
3
|
+
class MissingArgs < Textus::Error
|
|
4
|
+
attr_reader :spec, :missing
|
|
5
|
+
|
|
6
|
+
def initialize(spec, missing)
|
|
7
|
+
@spec = spec
|
|
8
|
+
@missing = missing
|
|
9
|
+
super("missing_args", "#{spec.verb}: missing #{missing.map(&:wire).join(", ")}")
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
module Binder
|
|
14
|
+
Pending = Data.define(:spec, :inputs)
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
def command(spec, inputs)
|
|
19
|
+
Pending.new(spec: spec, inputs: inputs)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def bind(spec, inputs)
|
|
23
|
+
missing = spec.required_args.reject { |a| inputs.key?(a.name) }
|
|
24
|
+
raise MissingArgs.new(spec, missing) unless missing.empty?
|
|
25
|
+
|
|
26
|
+
spec.args.each_with_object({}) do |a, h|
|
|
27
|
+
if inputs.key?(a.name)
|
|
28
|
+
h[a.name] = inputs[a.name]
|
|
29
|
+
elsif !a.default.nil?
|
|
30
|
+
h[a.name] = a.default
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def inputs_from_ordered(spec, ordered_positionals, by_name_keywords)
|
|
36
|
+
names = spec.args.select(&:positional).map(&:name)
|
|
37
|
+
names.zip(ordered_positionals).to_h.compact.merge(by_name_keywords)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def inputs_from_wire(spec, raw)
|
|
41
|
+
raw ||= {}
|
|
42
|
+
spec.args.each_with_object({}) do |a, h|
|
|
43
|
+
h[a.name] = raw[a.wire.to_s] if raw.key?(a.wire.to_s)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Dispatch
|
|
3
|
+
module Contracts
|
|
4
|
+
GetEntry = Data.define(:key)
|
|
5
|
+
|
|
6
|
+
PutEntry = Data.define(:key, :meta, :body, :content, :if_etag)
|
|
7
|
+
|
|
8
|
+
ListKeys = Data.define(:prefix, :lane, :q, :schema)
|
|
9
|
+
|
|
10
|
+
DeleteKey = Data.define(:key, :if_etag)
|
|
11
|
+
|
|
12
|
+
MoveKey = Data.define(:old_key, :new_key, :if_etag, :dry_run)
|
|
13
|
+
|
|
14
|
+
ProposeEntry = Data.define(:key, :meta, :body, :content)
|
|
15
|
+
|
|
16
|
+
AcceptProposal = Data.define(:pending_key)
|
|
17
|
+
|
|
18
|
+
RejectProposal = Data.define(:pending_key)
|
|
19
|
+
|
|
20
|
+
EnqueueJob = Data.define(:type, :args)
|
|
21
|
+
|
|
22
|
+
AuditEntries = Data.define(:key, :lane, :role, :verb, :since, :seq_since, :correlation_id, :limit)
|
|
23
|
+
|
|
24
|
+
PulseEntries = Data.define(:since)
|
|
25
|
+
|
|
26
|
+
BlameEntry = Data.define(:key, :limit)
|
|
27
|
+
|
|
28
|
+
WhereEntry = Data.define(:key)
|
|
29
|
+
|
|
30
|
+
UidEntry = Data.define(:key)
|
|
31
|
+
|
|
32
|
+
DepsEntry = Data.define(:key)
|
|
33
|
+
|
|
34
|
+
RdepsEntry = Data.define(:key)
|
|
35
|
+
|
|
36
|
+
BootStore = Data.define
|
|
37
|
+
|
|
38
|
+
DoctorStore = Data.define(:checks)
|
|
39
|
+
|
|
40
|
+
PublishedEntries = Data.define
|
|
41
|
+
|
|
42
|
+
RuleExplain = Data.define(:key, :detail)
|
|
43
|
+
|
|
44
|
+
RuleList = Data.define
|
|
45
|
+
|
|
46
|
+
SchemaEnvelope = Data.define(:key)
|
|
47
|
+
|
|
48
|
+
DrainStore = Data.define(:prefix, :lane)
|
|
49
|
+
|
|
50
|
+
IngestEntry = Data.define(:kind, :slug, :url, :path, :lane, :label)
|
|
51
|
+
|
|
52
|
+
JobsAction = Data.define(:state, :action, :job_id)
|
|
53
|
+
|
|
54
|
+
RuleLint = Data.define(:candidate_yaml)
|
|
55
|
+
|
|
56
|
+
DataMv = Data.define(:from, :to, :dry_run)
|
|
57
|
+
|
|
58
|
+
KeyMvPrefix = Data.define(:from_prefix, :to_prefix, :dry_run)
|
|
59
|
+
|
|
60
|
+
KeyDeletePrefix = Data.define(:prefix, :dry_run)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Dispatch
|
|
3
|
+
class HandlerRegistry
|
|
4
|
+
def initialize
|
|
5
|
+
@handlers = {}
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def register(command_class, handler)
|
|
9
|
+
@handlers[command_class] = handler
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def for(command_class)
|
|
13
|
+
@handlers[command_class] || raise("no handler registered for #{command_class}")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def registered?(command_class)
|
|
17
|
+
@handlers.key?(command_class)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Dispatch
|
|
5
|
+
module Middleware
|
|
6
|
+
# Shadows successful write operations into the SQLite audit_events table
|
|
7
|
+
# synchronously after dispatch, without the command knowing. Read verbs
|
|
8
|
+
# and failed writes pass through unchanged.
|
|
9
|
+
class AuditIndex < Base
|
|
10
|
+
middleware_name :audit_index
|
|
11
|
+
|
|
12
|
+
INDEXED_CONTRACTS = [
|
|
13
|
+
Contracts::PutEntry,
|
|
14
|
+
Contracts::DeleteKey,
|
|
15
|
+
Contracts::MoveKey,
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
def initialize(job_store:, audit_log:)
|
|
19
|
+
super()
|
|
20
|
+
@job_store = job_store
|
|
21
|
+
@audit_log = audit_log
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def call(container:, command:, call:, next_handler:) # rubocop:disable Lint/UnusedMethodArgument
|
|
25
|
+
result = next_handler.call(command, call)
|
|
26
|
+
return result unless result.success? && INDEXED_CONTRACTS.include?(command.class)
|
|
27
|
+
|
|
28
|
+
key = command.respond_to?(:key) ? command.key : nil
|
|
29
|
+
return result unless key
|
|
30
|
+
|
|
31
|
+
seq = @audit_log.latest_seq
|
|
32
|
+
verb = command.class.name.split("::").last
|
|
33
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
34
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
35
|
+
.downcase
|
|
36
|
+
|
|
37
|
+
@job_store.insert_audit_event(
|
|
38
|
+
seq: seq,
|
|
39
|
+
ts: Time.now.utc.iso8601,
|
|
40
|
+
role: call.role,
|
|
41
|
+
verb: verb,
|
|
42
|
+
key: key,
|
|
43
|
+
etag_before: nil,
|
|
44
|
+
etag_after: result.value.is_a?(Hash) ? result.value["etag"] : nil,
|
|
45
|
+
)
|
|
46
|
+
result
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Dispatch
|
|
3
|
+
module Middleware
|
|
4
|
+
class Auth < Base
|
|
5
|
+
middleware_name :auth
|
|
6
|
+
|
|
7
|
+
def call(container:, command:, call:, next_handler:)
|
|
8
|
+
verb = VerbRegistry.contract_to_verb!(command.class).to_sym
|
|
9
|
+
key = key_for(command)
|
|
10
|
+
|
|
11
|
+
rule_preds = key ? rule_declared_predicates(verb, container.manifest, key) : []
|
|
12
|
+
|
|
13
|
+
Manifest::Policy::Predicates.evaluate(
|
|
14
|
+
manifest: container.manifest, schemas: container.schemas,
|
|
15
|
+
action: verb, actor: call.role, key: key,
|
|
16
|
+
rule_predicates: rule_preds
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
next_handler.call(command, call)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def rule_declared_predicates(verb, manifest, key)
|
|
25
|
+
guard_map = manifest.rules.for(key).guard
|
|
26
|
+
return [] if guard_map.nil?
|
|
27
|
+
|
|
28
|
+
Array(guard_map[verb.to_s])
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def key_for(command)
|
|
32
|
+
if command.respond_to?(:key) then command.key
|
|
33
|
+
elsif command.respond_to?(:old_key) then command.old_key
|
|
34
|
+
elsif command.respond_to?(:pending_key) then command.pending_key
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Dispatch
|
|
3
|
+
module Middleware
|
|
4
|
+
class Base
|
|
5
|
+
def self.inherited(subclass)
|
|
6
|
+
super
|
|
7
|
+
subclass.instance_variable_set(:@middleware_name, nil)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def middleware_name(name = nil)
|
|
12
|
+
if name
|
|
13
|
+
@middleware_name = name.to_s
|
|
14
|
+
else
|
|
15
|
+
@middleware_name || name.split("::").last.downcase
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def call(container:, command:, call:, next_handler:)
|
|
21
|
+
raise NotImplementedError
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Dispatch
|
|
3
|
+
module Middleware
|
|
4
|
+
class Binder < Base
|
|
5
|
+
middleware_name :binder
|
|
6
|
+
|
|
7
|
+
def call(container:, command:, call:, next_handler:) # rubocop:disable Lint/UnusedMethodArgument
|
|
8
|
+
return next_handler.call(command, call) unless command.is_a?(Dispatch::Binder::Pending)
|
|
9
|
+
|
|
10
|
+
spec = command.spec
|
|
11
|
+
contract_class = VerbRegistry.contract_class_for(spec.verb) or
|
|
12
|
+
raise Textus::UsageError.new("unknown command verb: #{spec.verb}")
|
|
13
|
+
resolved = Dispatch::Binder.bind(spec, command.inputs)
|
|
14
|
+
built = Dispatch::Pipeline.build_command(contract_class, resolved)
|
|
15
|
+
next_handler.call(built, call)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Dispatch
|
|
3
|
+
module Middleware
|
|
4
|
+
class Cascade < Base
|
|
5
|
+
middleware_name :cascade
|
|
6
|
+
|
|
7
|
+
CASCADE_VERBS = %i[put propose accept reject key_mv key_delete].freeze
|
|
8
|
+
|
|
9
|
+
TRIGGER_TYPE_MAP = {
|
|
10
|
+
Contracts::PutEntry => "entry.written",
|
|
11
|
+
Contracts::ProposeEntry => "entry.written",
|
|
12
|
+
Contracts::DeleteKey => "entry.deleted",
|
|
13
|
+
Contracts::MoveKey => "entry.moved",
|
|
14
|
+
Contracts::AcceptProposal => "proposal.accepted",
|
|
15
|
+
Contracts::RejectProposal => "proposal.rejected",
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
def call(container:, command:, call:, next_handler:)
|
|
19
|
+
result = next_handler.call(command, call)
|
|
20
|
+
return result unless result.success? && cascadable?(command)
|
|
21
|
+
|
|
22
|
+
key = cascade_key(command)
|
|
23
|
+
return result unless key
|
|
24
|
+
|
|
25
|
+
trigger_type = TRIGGER_TYPE_MAP[command.class]
|
|
26
|
+
jobs = Textus::Store::Jobs::Planner.new(container: container).plan(
|
|
27
|
+
trigger: { "type" => trigger_type, "target" => key },
|
|
28
|
+
role: call.role,
|
|
29
|
+
)
|
|
30
|
+
queue = Textus::Store::Jobs::Queue.new(store: container.job_store)
|
|
31
|
+
jobs.each { |j| queue.enqueue(j) }
|
|
32
|
+
result
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def cascadable?(command)
|
|
38
|
+
CASCADE_VERBS.include?(VerbRegistry.contract_to_verb!(command.class).to_sym)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def cascade_key(command)
|
|
42
|
+
case command
|
|
43
|
+
when Contracts::PutEntry, Contracts::DeleteKey then command.key
|
|
44
|
+
when Contracts::MoveKey then command.new_key
|
|
45
|
+
when Contracts::AcceptProposal,
|
|
46
|
+
Contracts::RejectProposal then command.pending_key
|
|
47
|
+
when Contracts::ProposeEntry then command.key
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Dispatch
|
|
3
|
+
class Pipeline
|
|
4
|
+
attr_reader :container
|
|
5
|
+
|
|
6
|
+
def initialize(registry:, container:, middleware: [])
|
|
7
|
+
@registry = registry
|
|
8
|
+
@middleware = middleware
|
|
9
|
+
@container = container
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def dispatch(command, call:)
|
|
13
|
+
stack = @middleware.reverse.reduce(->(cmd, c) { execute(cmd, c) }) do |next_mw, mw|
|
|
14
|
+
->(cmd, c) { mw.call(container: @container, command: cmd, call: c, next_handler: next_mw) }
|
|
15
|
+
end
|
|
16
|
+
stack.call(command, call)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.build_command(contract_class, inputs)
|
|
20
|
+
members = contract_class.members
|
|
21
|
+
kwargs = members.to_h do |member|
|
|
22
|
+
[member, inputs[member]]
|
|
23
|
+
end
|
|
24
|
+
contract_class.new(**kwargs)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def execute(command, call)
|
|
30
|
+
handler = @registry.for(command.class)
|
|
31
|
+
handler.call(command, call)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -3,8 +3,8 @@ module Textus
|
|
|
3
3
|
class Check
|
|
4
4
|
class AuditLog < Check
|
|
5
5
|
def call
|
|
6
|
-
path = Textus::Layout.
|
|
7
|
-
Textus::
|
|
6
|
+
path = Textus::Store::Layout.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",
|
|
@@ -8,12 +8,12 @@ module Textus
|
|
|
8
8
|
# verb reported.
|
|
9
9
|
class GeneratorDrift < Check
|
|
10
10
|
def call
|
|
11
|
-
|
|
11
|
+
detector = Textus::Store::Freshness::DriftDetector.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
|
-
manifest.data.entries.flat_map { |m|
|
|
16
|
+
manifest.data.entries.flat_map { |m| detector.drift_rows(m) }.map do |row|
|
|
17
17
|
{
|
|
18
18
|
"code" => "generator_drift",
|
|
19
19
|
"level" => "warning",
|
|
@@ -8,12 +8,12 @@ module Textus
|
|
|
8
8
|
# that drift without making `build` scan globally.
|
|
9
9
|
class OrphanedPublishTargets < Check
|
|
10
10
|
def call
|
|
11
|
-
sdir = Textus::Layout.
|
|
11
|
+
sdir = Textus::Store::Layout.new(root).sentinels_root
|
|
12
12
|
return [] unless File.directory?(sdir)
|
|
13
13
|
|
|
14
14
|
repo_root = File.dirname(root)
|
|
15
|
-
store = Textus::
|
|
16
|
-
glob = File.join(sdir, "**", "*#{Textus::
|
|
15
|
+
store = Textus::Port::SentinelStore.new
|
|
16
|
+
glob = File.join(sdir, "**", "*#{Textus::Port::SentinelStore::SUFFIX}")
|
|
17
17
|
Dir.glob(glob).filter_map do |spath|
|
|
18
18
|
sentinel = store.load(spath, repo_root)
|
|
19
19
|
next nil if sentinel.nil? || sentinel.source.nil?
|
|
@@ -8,7 +8,7 @@ module Textus
|
|
|
8
8
|
class ProtocolVersion < Check
|
|
9
9
|
# Standalone interface: root is the project root (parent of .textus/).
|
|
10
10
|
def self.run(root:)
|
|
11
|
-
path = File.join(root, ".textus
|
|
11
|
+
path = File.join(root, ".textus", "manifest.yaml")
|
|
12
12
|
return [] unless File.exist?(path)
|
|
13
13
|
|
|
14
14
|
doc = YAML.safe_load_file(path, aliases: false) || {}
|
|
@@ -26,7 +26,7 @@ module Textus
|
|
|
26
26
|
# Doctor check interface: root is the .textus/ directory itself,
|
|
27
27
|
# so manifest.yaml lives directly inside it.
|
|
28
28
|
def call
|
|
29
|
-
path =
|
|
29
|
+
path = geometry.manifest_path
|
|
30
30
|
return [] unless File.exist?(path)
|
|
31
31
|
|
|
32
32
|
doc = YAML.safe_load_file(path, aliases: false) || {}
|
|
@@ -7,7 +7,7 @@ module Textus
|
|
|
7
7
|
# leaving the operator with no signal that a schema is broken.
|
|
8
8
|
class SchemaParseError < Check
|
|
9
9
|
def call
|
|
10
|
-
dir =
|
|
10
|
+
dir = geometry.schemas_dir
|
|
11
11
|
return [] unless File.directory?(dir)
|
|
12
12
|
|
|
13
13
|
Dir.glob(File.join(dir, "*.yaml")).each_with_object([]) do |path, out|
|
|
@@ -4,11 +4,11 @@ module Textus
|
|
|
4
4
|
class SchemaViolations < Check
|
|
5
5
|
def call
|
|
6
6
|
result = Textus::Doctor::Validator.new(
|
|
7
|
-
reader: ->(key, ctnr,
|
|
7
|
+
reader: ->(key, ctnr, _c) { Textus::Store::Entry::Reader.from(container: ctnr).read(key) },
|
|
8
8
|
manifest: @container.manifest,
|
|
9
9
|
audit_log: @container.audit_log,
|
|
10
10
|
schema_for: ->(name) { @container.schemas.fetch_or_nil(name) },
|
|
11
|
-
).call(container: @container, call: Textus::Call.build(role: Textus::Role::DEFAULT))
|
|
11
|
+
).call(container: @container, call: Textus::Value::Call.build(role: Textus::Value::Role::DEFAULT))
|
|
12
12
|
|
|
13
13
|
result["violations"].map do |v|
|
|
14
14
|
fix = v["expected"] &&
|