textus 0.52.0 → 0.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -0
- data/README.md +62 -54
- data/SPEC.md +62 -187
- data/docs/architecture/README.md +88 -77
- data/exe/textus +1 -1
- data/lib/textus/action/accept.rb +53 -0
- data/lib/textus/action/audit.rb +133 -0
- data/lib/textus/action/base.rb +42 -0
- data/lib/textus/{read → action}/blame.rb +30 -22
- data/lib/textus/action/boot.rb +26 -0
- data/lib/textus/action/data_mv.rb +71 -0
- data/lib/textus/action/deps.rb +48 -0
- data/lib/textus/action/doctor.rb +26 -0
- data/lib/textus/action/drain.rb +41 -0
- data/lib/textus/action/enqueue.rb +55 -0
- data/lib/textus/action/get.rb +80 -0
- data/lib/textus/action/jobs.rb +38 -0
- data/lib/textus/action/key_delete.rb +46 -0
- data/lib/textus/action/key_delete_prefix.rb +46 -0
- data/lib/textus/action/key_mv.rb +143 -0
- data/lib/textus/action/key_mv_prefix.rb +59 -0
- data/lib/textus/action/list.rb +44 -0
- data/lib/textus/action/propose.rb +54 -0
- data/lib/textus/action/published.rb +26 -0
- data/lib/textus/action/pulse/scanner.rb +118 -0
- data/lib/textus/action/pulse.rb +87 -0
- data/lib/textus/action/put.rb +63 -0
- data/lib/textus/action/rdeps.rb +49 -0
- data/lib/textus/action/reject.rb +49 -0
- data/lib/textus/action/rule_explain.rb +95 -0
- data/lib/textus/action/rule_lint.rb +70 -0
- data/lib/textus/action/rule_list.rb +46 -0
- data/lib/textus/action/schema_envelope.rb +31 -0
- data/lib/textus/action/uid.rb +35 -0
- data/lib/textus/action/where.rb +38 -0
- data/lib/textus/action/write_verb.rb +58 -0
- data/lib/textus/background/job/base.rb +27 -0
- data/lib/textus/background/job/materialize.rb +31 -0
- data/lib/textus/background/job/refresh.rb +22 -0
- data/lib/textus/background/job/sweep.rb +31 -0
- data/lib/textus/background/job.rb +19 -0
- data/lib/textus/background/plan.rb +9 -0
- data/lib/textus/background/planner/plan.rb +113 -0
- data/lib/textus/{maintenance → background}/retention/apply.rb +7 -9
- data/lib/textus/background/worker.rb +67 -0
- data/lib/textus/boot.rb +53 -45
- data/lib/textus/command.rb +36 -0
- data/lib/textus/container.rb +1 -1
- data/lib/textus/{domain → core}/duration.rb +1 -1
- data/lib/textus/{domain → core}/freshness/evaluator.rb +11 -11
- data/lib/textus/{domain → core}/freshness/verdict.rb +2 -2
- data/lib/textus/{domain → core}/freshness.rb +2 -2
- data/lib/textus/{domain → core}/retention/sweep.rb +7 -7
- data/lib/textus/{domain → core}/retention.rb +2 -2
- data/lib/textus/{domain → core}/sentinel.rb +1 -1
- data/lib/textus/doctor/check/generator_drift.rb +1 -1
- data/lib/textus/doctor/check/handler_permit.rb +34 -0
- data/lib/textus/doctor/check/hooks.rb +11 -18
- data/lib/textus/doctor/check/illegal_keys.rb +1 -1
- data/lib/textus/doctor/check/intake_registration.rb +5 -5
- data/lib/textus/doctor/check/proposal_targets.rb +3 -3
- data/lib/textus/doctor/check/rule_ambiguity.rb +2 -2
- data/lib/textus/doctor/check/schema_violations.rb +8 -2
- data/lib/textus/doctor/check.rb +12 -9
- data/lib/textus/{read → doctor}/validator.rb +22 -13
- data/lib/textus/doctor.rb +6 -6
- data/lib/textus/envelope/io/writer.rb +65 -36
- data/lib/textus/envelope.rb +5 -3
- data/lib/textus/errors.rb +17 -9
- data/lib/textus/events.rb +21 -0
- data/lib/textus/gate/auth.rb +181 -0
- data/lib/textus/gate.rb +114 -0
- data/lib/textus/init/templates/machine_intake.rb +39 -35
- data/lib/textus/init/templates/orientation_reducer.rb +15 -11
- data/lib/textus/init.rb +90 -73
- data/lib/textus/key/path.rb +9 -2
- data/lib/textus/layout.rb +13 -0
- data/lib/textus/manifest/data.rb +14 -14
- data/lib/textus/manifest/entry/base.rb +15 -11
- data/lib/textus/manifest/entry/parser.rb +6 -6
- data/lib/textus/manifest/entry/produced.rb +3 -2
- data/lib/textus/manifest/entry/publish/mode.rb +1 -1
- data/lib/textus/manifest/entry/publish/to_paths.rb +2 -1
- data/lib/textus/manifest/entry/validators/events.rb +1 -1
- data/lib/textus/{domain/policy/handler_allowlist.rb → manifest/policy/handler_permit.rb} +4 -4
- data/lib/textus/{domain → manifest}/policy/matcher.rb +2 -2
- data/lib/textus/{domain → manifest}/policy/publish_target.rb +2 -2
- data/lib/textus/manifest/policy/react.rb +30 -0
- data/lib/textus/{domain → manifest}/policy/retention.rb +3 -3
- data/lib/textus/{domain → manifest}/policy/source.rb +24 -19
- data/lib/textus/manifest/policy.rb +36 -48
- data/lib/textus/manifest/resolver.rb +3 -2
- data/lib/textus/manifest/rules.rb +4 -4
- data/lib/textus/manifest/schema/keys.rb +17 -11
- data/lib/textus/manifest/schema/validator.rb +24 -22
- data/lib/textus/manifest/schema/vocabulary.rb +1 -1
- data/lib/textus/manifest/schema.rb +2 -2
- data/lib/textus/manifest.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/handler.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/intake.rb +22 -20
- data/lib/textus/{produce → pipeline}/acquire/projection.rb +13 -11
- data/lib/textus/{produce → pipeline}/acquire/serializer/json.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/serializer/text.rb +1 -1
- data/lib/textus/{produce → pipeline}/acquire/serializer/yaml.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/serializer.rb +1 -1
- data/lib/textus/{produce → pipeline}/engine.rb +7 -5
- data/lib/textus/{produce → pipeline}/render.rb +3 -1
- data/lib/textus/ports/audit_log.rb +31 -5
- data/lib/textus/ports/audit_subscriber.rb +4 -4
- data/lib/textus/{domain/jobs → ports/queue}/job.rb +19 -12
- data/lib/textus/ports/queue.rb +1 -1
- data/lib/textus/ports/sentinel_store.rb +2 -2
- data/lib/textus/ports/watcher_lock.rb +48 -0
- data/lib/textus/projection.rb +8 -8
- data/lib/textus/schema/tools.rb +4 -3
- data/lib/textus/session.rb +6 -3
- data/lib/textus/step/base.rb +35 -0
- data/lib/textus/step/builtin/csv_fetch.rb +19 -0
- data/lib/textus/step/builtin/ical_events_fetch.rb +30 -0
- data/lib/textus/step/builtin/json_fetch.rb +18 -0
- data/lib/textus/step/builtin/markdown_links_fetch.rb +20 -0
- data/lib/textus/step/builtin/rss_fetch.rb +26 -0
- data/lib/textus/step/builtin.rb +22 -0
- data/lib/textus/{hooks → step}/catalog.rb +3 -3
- data/lib/textus/{hooks → step}/context.rb +15 -13
- data/lib/textus/step/discovery.rb +24 -0
- data/lib/textus/{hooks → step}/error_log.rb +1 -1
- data/lib/textus/{hooks → step}/event_bus.rb +15 -16
- data/lib/textus/step/fetch.rb +13 -0
- data/lib/textus/{hooks → step}/fire_report.rb +1 -1
- data/lib/textus/step/loader.rb +108 -0
- data/lib/textus/step/observe.rb +31 -0
- data/lib/textus/step/registry_store.rb +66 -0
- data/lib/textus/{hooks → step}/signature.rb +1 -1
- data/lib/textus/step/transform.rb +12 -0
- data/lib/textus/step/validate.rb +11 -0
- data/lib/textus/step.rb +10 -0
- data/lib/textus/store.rb +17 -15
- data/lib/textus/surfaces/cli/group/data.rb +11 -0
- data/lib/textus/surfaces/cli/group/key.rb +11 -0
- data/lib/textus/surfaces/cli/group/mcp.rb +11 -0
- data/lib/textus/surfaces/cli/group/rule.rb +11 -0
- data/lib/textus/surfaces/cli/group/schema.rb +11 -0
- data/lib/textus/surfaces/cli/group.rb +50 -0
- data/lib/textus/surfaces/cli/runner.rb +236 -0
- data/lib/textus/surfaces/cli/verb/doctor.rb +21 -0
- data/lib/textus/surfaces/cli/verb/get.rb +21 -0
- data/lib/textus/surfaces/cli/verb/init.rb +20 -0
- data/lib/textus/surfaces/cli/verb/mcp_serve.rb +24 -0
- data/lib/textus/surfaces/cli/verb/put.rb +30 -0
- data/lib/textus/surfaces/cli/verb/schema_diff.rb +17 -0
- data/lib/textus/surfaces/cli/verb/schema_init.rb +21 -0
- data/lib/textus/surfaces/cli/verb/schema_migrate.rb +21 -0
- data/lib/textus/surfaces/cli/verb/watch.rb +19 -0
- data/lib/textus/surfaces/cli/verb.rb +111 -0
- data/lib/textus/surfaces/cli.rb +148 -0
- data/lib/textus/surfaces/mcp/catalog.rb +99 -0
- data/lib/textus/surfaces/mcp/errors.rb +34 -0
- data/lib/textus/surfaces/mcp/server.rb +145 -0
- data/lib/textus/surfaces/mcp/session.rb +9 -0
- data/lib/textus/surfaces/mcp/tool_schemas.rb +17 -0
- data/lib/textus/surfaces/mcp.rb +8 -0
- data/lib/textus/surfaces/role_scope.rb +38 -0
- data/lib/textus/surfaces/watcher.rb +38 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus.rb +64 -22
- metadata +132 -118
- data/lib/textus/cli/group/hook.rb +0 -9
- data/lib/textus/cli/group/key.rb +0 -9
- data/lib/textus/cli/group/mcp.rb +0 -9
- data/lib/textus/cli/group/rule.rb +0 -9
- data/lib/textus/cli/group/schema.rb +0 -9
- data/lib/textus/cli/group/zone.rb +0 -9
- data/lib/textus/cli/group.rb +0 -48
- data/lib/textus/cli/runner.rb +0 -193
- data/lib/textus/cli/verb/doctor.rb +0 -17
- data/lib/textus/cli/verb/get.rb +0 -18
- data/lib/textus/cli/verb/hook_run.rb +0 -48
- data/lib/textus/cli/verb/hooks.rb +0 -50
- data/lib/textus/cli/verb/init.rb +0 -18
- data/lib/textus/cli/verb/mcp_serve.rb +0 -22
- data/lib/textus/cli/verb/put.rb +0 -30
- data/lib/textus/cli/verb/schema_diff.rb +0 -15
- data/lib/textus/cli/verb/schema_init.rb +0 -19
- data/lib/textus/cli/verb/schema_migrate.rb +0 -19
- data/lib/textus/cli/verb/serve.rb +0 -19
- data/lib/textus/cli/verb.rb +0 -116
- data/lib/textus/cli.rb +0 -138
- data/lib/textus/dispatcher.rb +0 -54
- data/lib/textus/doctor/check/handler_allowlist.rb +0 -34
- data/lib/textus/domain/action.rb +0 -9
- data/lib/textus/domain/jobs/registry.rb +0 -37
- data/lib/textus/domain/permission.rb +0 -7
- data/lib/textus/domain/policy/base_guards.rb +0 -25
- data/lib/textus/domain/policy/evaluation.rb +0 -15
- data/lib/textus/domain/policy/guard.rb +0 -35
- data/lib/textus/domain/policy/guard_factory.rb +0 -40
- data/lib/textus/domain/policy/predicates/author_held.rb +0 -33
- data/lib/textus/domain/policy/predicates/etag_match.rb +0 -32
- data/lib/textus/domain/policy/predicates/fresh_within.rb +0 -59
- data/lib/textus/domain/policy/predicates/registry.rb +0 -39
- data/lib/textus/domain/policy/predicates/schema_valid.rb +0 -61
- data/lib/textus/domain/policy/predicates/target_is_canon.rb +0 -33
- data/lib/textus/domain/policy/predicates/zone_writable_by.rb +0 -39
- data/lib/textus/hooks/builtin.rb +0 -70
- data/lib/textus/hooks/loader.rb +0 -54
- data/lib/textus/hooks/rpc_registry.rb +0 -43
- data/lib/textus/jobs/handlers.rb +0 -62
- data/lib/textus/jobs/scheduler.rb +0 -36
- data/lib/textus/jobs/seeder.rb +0 -57
- data/lib/textus/maintenance/drain.rb +0 -42
- data/lib/textus/maintenance/key_delete_prefix.rb +0 -48
- data/lib/textus/maintenance/key_mv_prefix.rb +0 -68
- data/lib/textus/maintenance/rule_lint.rb +0 -66
- data/lib/textus/maintenance/serve.rb +0 -30
- data/lib/textus/maintenance/worker.rb +0 -74
- data/lib/textus/maintenance/zone_mv.rb +0 -64
- data/lib/textus/maintenance.rb +0 -15
- data/lib/textus/mcp/catalog.rb +0 -70
- data/lib/textus/mcp/errors.rb +0 -32
- data/lib/textus/mcp/server.rb +0 -138
- data/lib/textus/mcp/session.rb +0 -7
- data/lib/textus/mcp/tool_schemas.rb +0 -15
- data/lib/textus/mcp.rb +0 -6
- data/lib/textus/mustache.rb +0 -117
- data/lib/textus/ports/produce_on_write_subscriber.rb +0 -73
- data/lib/textus/produce/events.rb +0 -36
- data/lib/textus/read/audit.rb +0 -130
- data/lib/textus/read/boot.rb +0 -26
- data/lib/textus/read/capabilities.rb +0 -70
- data/lib/textus/read/deps.rb +0 -38
- data/lib/textus/read/doctor.rb +0 -27
- data/lib/textus/read/freshness.rb +0 -152
- data/lib/textus/read/get.rb +0 -73
- data/lib/textus/read/jobs.rb +0 -31
- data/lib/textus/read/list.rb +0 -24
- data/lib/textus/read/published.rb +0 -22
- data/lib/textus/read/pulse.rb +0 -98
- data/lib/textus/read/rdeps.rb +0 -39
- data/lib/textus/read/rule_explain.rb +0 -96
- data/lib/textus/read/rule_list.rb +0 -54
- data/lib/textus/read/schema_envelope.rb +0 -25
- data/lib/textus/read/uid.rb +0 -29
- data/lib/textus/read/validate_all.rb +0 -36
- data/lib/textus/read/where.rb +0 -24
- data/lib/textus/role_scope.rb +0 -78
- data/lib/textus/write/accept.rb +0 -58
- data/lib/textus/write/enqueue.rb +0 -50
- data/lib/textus/write/key_delete.rb +0 -65
- data/lib/textus/write/key_mv.rb +0 -141
- data/lib/textus/write/propose.rb +0 -54
- data/lib/textus/write/put.rb +0 -74
- data/lib/textus/write/reject.rb +0 -68
data/lib/textus/boot.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module Textus
|
|
2
2
|
# Read-only "what's in this store and how do I use it" envelope.
|
|
3
3
|
# A single call gives an agent the working model of a textus-managed
|
|
4
|
-
# project:
|
|
4
|
+
# project: lanes and their write authority, entries and their flags,
|
|
5
5
|
# registered hooks, write flows, and the CLI verb catalog.
|
|
6
6
|
#
|
|
7
7
|
# Boot is side-effect-free.
|
|
@@ -10,24 +10,24 @@ module Textus
|
|
|
10
10
|
|
|
11
11
|
# Per-capability write-flow templates. Each lambda receives the user-facing
|
|
12
12
|
# role name and the manifest, and returns guidance for that verb with the
|
|
13
|
-
# live
|
|
13
|
+
# live lane named by kind (ADR 0034). A role holding multiple verbs gets one
|
|
14
14
|
# joined string; roles whose verbs have no template are omitted.
|
|
15
15
|
WRITE_FLOW_TEMPLATES = {
|
|
16
16
|
author: lambda do |name, manifest|
|
|
17
|
-
"edit files in #{
|
|
17
|
+
"edit files in #{lane_label(manifest, :canon, "your canon lane")}, " \
|
|
18
18
|
"then 'textus put KEY --as=#{name}'"
|
|
19
19
|
end,
|
|
20
20
|
keep: lambda do |name, manifest|
|
|
21
|
-
"keep durable notes in #{
|
|
21
|
+
"keep durable notes in #{lane_label(manifest, :workspace, "your workspace")}: " \
|
|
22
22
|
"'textus put KEY --as=#{name}' (no accept needed)"
|
|
23
23
|
end,
|
|
24
24
|
propose: lambda do |name, manifest|
|
|
25
25
|
authority = manifest.policy.roles_with_capability("author").first || "the author-holder"
|
|
26
|
-
"propose changes by writing #{manifest.policy.
|
|
26
|
+
"propose changes by writing #{manifest.policy.queue_lane}.* entries with --as=#{name} " \
|
|
27
27
|
"and a 'proposal:' frontmatter block; the #{authority} role runs 'textus accept' to apply"
|
|
28
28
|
end,
|
|
29
29
|
converge: lambda do |_name, manifest|
|
|
30
|
-
machine =
|
|
30
|
+
machine = lane_label(manifest, :machine, "machine")
|
|
31
31
|
"'textus drain' materializes derived #{machine} entries from their sources and " \
|
|
32
32
|
"refreshes stale intake #{machine} entries from their declared source; " \
|
|
33
33
|
"derived files are never hand-edited (reactive on canon writes, or a full pass on demand)"
|
|
@@ -44,17 +44,17 @@ module Textus
|
|
|
44
44
|
end
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
# Human-readable name(s) for the live
|
|
47
|
+
# Human-readable name(s) for the live lane(s) of a given kind, or `fallback`
|
|
48
48
|
# when the manifest declares none. Lets write-flow guidance name the live
|
|
49
|
-
#
|
|
50
|
-
def self.
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
# lane by kind instead of a hardcoded instance name (ADR 0034).
|
|
50
|
+
def self.lane_label(manifest, kind, fallback)
|
|
51
|
+
lanes = manifest.policy.lanes_of_kind(kind)
|
|
52
|
+
lanes.empty? ? fallback : lanes.join(", ")
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
# Static, store-independent parts of the agent-facing protocol. The
|
|
56
56
|
# `recipes` and `role_resolution` blocks are derived per-manifest in
|
|
57
|
-
# agent_protocol(...) because
|
|
57
|
+
# agent_protocol(...) because lane and role names are user-configurable.
|
|
58
58
|
AGENT_PROTOCOL_TEMPLATE = {
|
|
59
59
|
"envelope_shape" => {
|
|
60
60
|
"summary" => "every read/write payload is a JSON envelope with _meta, body, uid, and etag",
|
|
@@ -91,18 +91,16 @@ module Textus
|
|
|
91
91
|
{ "name" => "blame" },
|
|
92
92
|
{ "name" => "rule", "summary" => "inspect effective rules: 'rule list', 'rule explain KEY'" },
|
|
93
93
|
{ "name" => "doctor" },
|
|
94
|
-
{ "name" => "hook", "summary" => "list and run registered hooks: 'hook list', 'hook run NAME'" },
|
|
95
94
|
{ "name" => "jobs" },
|
|
96
95
|
{ "name" => "pulse" },
|
|
97
|
-
{ "name" => "capabilities" },
|
|
98
96
|
].freeze
|
|
99
97
|
|
|
100
98
|
# verb token => contract.summary, for every Dispatcher verb that carries a
|
|
101
99
|
# contract. The single source for a verb's one-line summary (ADR 0039).
|
|
102
100
|
def self.contract_summaries
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
101
|
+
Textus::Action::VERBS.values
|
|
102
|
+
.select { |k| k.respond_to?(:contract?) && k.contract? }
|
|
103
|
+
.to_h { |k| [k.contract.verb.to_s, k.contract.summary] }
|
|
106
104
|
end
|
|
107
105
|
|
|
108
106
|
# Build the CLI verb catalog: each summary is derived from its contract when
|
|
@@ -119,11 +117,15 @@ module Textus
|
|
|
119
117
|
def self.agent_quickstart(manifest, audit_log)
|
|
120
118
|
agent_role = manifest.policy.proposer_role
|
|
121
119
|
|
|
122
|
-
|
|
123
|
-
|
|
120
|
+
writable_lanes = manifest.data.declared_lane_kinds.keys.each_with_object([]) do |lane_name, acc|
|
|
121
|
+
next unless agent_role
|
|
122
|
+
|
|
123
|
+
verb = manifest.policy.verb_for_lane(lane_name)
|
|
124
|
+
writers = manifest.policy.roles_with_capability(verb)
|
|
125
|
+
acc << lane_name if writers.include?(agent_role)
|
|
124
126
|
end
|
|
125
127
|
|
|
126
|
-
|
|
128
|
+
propose_lane = manifest.policy.propose_lane_for(agent_role)
|
|
127
129
|
|
|
128
130
|
{
|
|
129
131
|
# Both verb lists derive from the MCP catalog (ADR 0056, ADR 0057): the
|
|
@@ -133,11 +135,11 @@ module Textus
|
|
|
133
135
|
# internal scan, ADR 0085) nor omit one it can
|
|
134
136
|
# (schema_show/rules); write_verbs drops the old `put KEY --as=… --stdin` CLI
|
|
135
137
|
# framing (role is connection-resolved over MCP; there is no stdin).
|
|
136
|
-
#
|
|
137
|
-
"read_verbs" => Textus::MCP::Catalog.read_verbs,
|
|
138
|
-
"write_verbs" => agent_role ? Textus::MCP::Catalog.write_verbs : [],
|
|
139
|
-
"
|
|
140
|
-
"
|
|
138
|
+
# writable_lanes / propose_lane below carry the agent's write authority.
|
|
139
|
+
"read_verbs" => Textus::Surfaces::MCP::Catalog.read_verbs,
|
|
140
|
+
"write_verbs" => agent_role ? Textus::Surfaces::MCP::Catalog.write_verbs : [],
|
|
141
|
+
"writable_lanes" => writable_lanes,
|
|
142
|
+
"propose_lane" => propose_lane,
|
|
141
143
|
"latest_seq" => audit_log.latest_seq,
|
|
142
144
|
}
|
|
143
145
|
end
|
|
@@ -147,13 +149,13 @@ module Textus
|
|
|
147
149
|
# `textus get KEY`, MCP as the `get` tool) or is a plain materialize step. This
|
|
148
150
|
# keeps shell lines out of the surface an MCP agent reads.
|
|
149
151
|
def self.recipes(manifest)
|
|
150
|
-
queue = manifest.policy.
|
|
151
|
-
feeds =
|
|
152
|
+
queue = manifest.policy.queue_lane
|
|
153
|
+
feeds = lane_label(manifest, :machine, "the machine lane")
|
|
152
154
|
{
|
|
153
155
|
"read" => {
|
|
154
156
|
"purpose" => "find and read an entry",
|
|
155
157
|
"steps" => [
|
|
156
|
-
"list (
|
|
158
|
+
"list (lane:, prefix:) — discover keys without reading bodies",
|
|
157
159
|
"get KEY — returns the entry envelope",
|
|
158
160
|
],
|
|
159
161
|
},
|
|
@@ -168,17 +170,17 @@ module Textus
|
|
|
168
170
|
"propose" => {
|
|
169
171
|
"purpose" => "agent suggests a change for human review",
|
|
170
172
|
"agent_steps" => [
|
|
171
|
-
"propose KEY — writes the change into the #{queue}
|
|
173
|
+
"propose KEY — writes the change into the #{queue} lane for review",
|
|
172
174
|
],
|
|
173
175
|
"human_steps" => [
|
|
174
|
-
"accept #{queue}.KEY — promotes the proposal into its target
|
|
176
|
+
"accept #{queue}.KEY — promotes the proposal into its target lane",
|
|
175
177
|
],
|
|
176
178
|
},
|
|
177
179
|
"drain" => {
|
|
178
180
|
"purpose" => "keep the machine-maintained lanes fresh — re-pull stale intake entries from their declared source",
|
|
179
181
|
"steps" => [
|
|
180
182
|
"pulse — its `stale` list names entries past their ttl",
|
|
181
|
-
"drain (
|
|
183
|
+
"drain (lane: #{feeds}) — re-pull the stale entries",
|
|
182
184
|
],
|
|
183
185
|
},
|
|
184
186
|
}
|
|
@@ -204,7 +206,7 @@ module Textus
|
|
|
204
206
|
return {
|
|
205
207
|
"protocol" => PROTOCOL_ID,
|
|
206
208
|
"store_root" => container.root,
|
|
207
|
-
"
|
|
209
|
+
"lanes" => lanes_for(manifest),
|
|
208
210
|
"agent_quickstart" => agent_quickstart(manifest, container.audit_log),
|
|
209
211
|
"contract_etag" => etag,
|
|
210
212
|
}
|
|
@@ -213,7 +215,7 @@ module Textus
|
|
|
213
215
|
{
|
|
214
216
|
"protocol" => PROTOCOL_ID,
|
|
215
217
|
"store_root" => container.root,
|
|
216
|
-
"
|
|
218
|
+
"lanes" => lanes_for(manifest),
|
|
217
219
|
"entries" => entries_for(manifest),
|
|
218
220
|
"hooks" => hooks_for_container(container),
|
|
219
221
|
"write_flows" => write_flows_for(manifest),
|
|
@@ -221,16 +223,17 @@ module Textus
|
|
|
221
223
|
"agent_protocol" => agent_protocol(manifest),
|
|
222
224
|
"agent_quickstart" => agent_quickstart(manifest, container.audit_log),
|
|
223
225
|
"contract_etag" => etag,
|
|
224
|
-
"docs" => { "spec" => "SPEC.md", "example" => "
|
|
226
|
+
"docs" => { "spec" => "SPEC.md", "example" => ".textus/" },
|
|
225
227
|
}
|
|
226
228
|
end
|
|
227
229
|
|
|
228
|
-
def self.
|
|
229
|
-
manifest.data.
|
|
230
|
-
|
|
230
|
+
def self.lanes_for(manifest)
|
|
231
|
+
manifest.data.declared_lane_kinds.keys.map do |name|
|
|
232
|
+
verb = manifest.policy.verb_for_lane(name)
|
|
233
|
+
row = { "name" => name, "writers" => manifest.policy.roles_with_capability(verb) }
|
|
231
234
|
kind = manifest.policy.declared_kind(name)
|
|
232
235
|
row["kind"] = kind.to_s if kind
|
|
233
|
-
purpose = manifest.data.
|
|
236
|
+
purpose = manifest.data.lane_descs[name]
|
|
234
237
|
row["purpose"] = purpose if purpose && !purpose.empty?
|
|
235
238
|
row
|
|
236
239
|
end
|
|
@@ -241,7 +244,7 @@ module Textus
|
|
|
241
244
|
derived = e.derived?
|
|
242
245
|
{
|
|
243
246
|
"key" => e.key,
|
|
244
|
-
"
|
|
247
|
+
"lane" => e.lane,
|
|
245
248
|
"schema" => e.schema,
|
|
246
249
|
"nested" => e.is_a?(Textus::Manifest::Entry::Nested),
|
|
247
250
|
"owner" => e.owner,
|
|
@@ -254,16 +257,21 @@ module Textus
|
|
|
254
257
|
end
|
|
255
258
|
|
|
256
259
|
def self.hooks_for_container(container)
|
|
257
|
-
hooks_for_container_internal(
|
|
260
|
+
hooks_for_container_internal(steps: container.steps)
|
|
258
261
|
end
|
|
259
262
|
|
|
260
|
-
def self.hooks_for_container_internal(
|
|
263
|
+
def self.hooks_for_container_internal(steps:)
|
|
261
264
|
sections = {}
|
|
262
|
-
|
|
263
|
-
|
|
265
|
+
rpc_kind_map = {
|
|
266
|
+
resolve_handler: :fetch,
|
|
267
|
+
transform_rows: :transform,
|
|
268
|
+
validate: :validate,
|
|
269
|
+
}
|
|
270
|
+
Step::Catalog::RPC.each_key do |event|
|
|
271
|
+
sections[event.to_s] = steps.names(rpc_kind_map.fetch(event)).map(&:to_s).sort
|
|
264
272
|
end
|
|
265
|
-
|
|
266
|
-
sections[event.to_s] =
|
|
273
|
+
Step::Catalog::PUBSUB.each_key do |event|
|
|
274
|
+
sections[event.to_s] = steps.pubsub_handlers(event).map { |h| h[:name].to_s }.sort
|
|
267
275
|
end
|
|
268
276
|
sections
|
|
269
277
|
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Command
|
|
3
|
+
Get = Data.define(:key, :role)
|
|
4
|
+
Put = Data.define(:key, :meta, :body, :content, :if_etag, :role)
|
|
5
|
+
Propose = Data.define(:key, :meta, :body, :content, :role, :pending_key) do
|
|
6
|
+
def initialize(key:, role:, meta: nil, body: nil, content: nil, pending_key: nil)
|
|
7
|
+
super
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
KeyDelete = Data.define(:key, :if_etag, :role)
|
|
11
|
+
KeyMv = Data.define(:old_key, :new_key, :dry_run, :role)
|
|
12
|
+
Accept = Data.define(:pending_key, :role)
|
|
13
|
+
Reject = Data.define(:pending_key, :role)
|
|
14
|
+
Enqueue = Data.define(:type, :args, :role)
|
|
15
|
+
List = Data.define(:prefix, :lane, :role)
|
|
16
|
+
Where = Data.define(:key, :role)
|
|
17
|
+
Uid = Data.define(:key, :role)
|
|
18
|
+
Blame = Data.define(:key, :limit, :role)
|
|
19
|
+
Audit = Data.define(:key, :lane, :verb, :since, :seq_since, :correlation_id, :limit, :role)
|
|
20
|
+
Deps = Data.define(:key, :role)
|
|
21
|
+
Rdeps = Data.define(:key, :role)
|
|
22
|
+
Pulse = Data.define(:since, :role)
|
|
23
|
+
RuleExplain = Data.define(:key, :detail, :role)
|
|
24
|
+
RuleList = Data.define(:role)
|
|
25
|
+
RuleLint = Data.define(:candidate_yaml, :role)
|
|
26
|
+
Published = Data.define(:role)
|
|
27
|
+
SchemaShow = Data.define(:key, :role)
|
|
28
|
+
Doctor = Data.define(:checks, :role)
|
|
29
|
+
Boot = Data.define(:lean, :role)
|
|
30
|
+
Jobs = Data.define(:state, :action, :job_id, :role)
|
|
31
|
+
DataMv = Data.define(:from, :to, :dry_run, :role)
|
|
32
|
+
KeyMvPrefix = Data.define(:from_prefix, :to_prefix, :dry_run, :role)
|
|
33
|
+
KeyDeletePrefix = Data.define(:prefix, :dry_run, :role)
|
|
34
|
+
Drain = Data.define(:prefix, :lane, :role)
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/textus/container.rb
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
require "time"
|
|
2
2
|
|
|
3
3
|
module Textus
|
|
4
|
-
module
|
|
4
|
+
module Core
|
|
5
5
|
module Freshness
|
|
6
6
|
# The single currency evaluator (ADR 0099). Answers "is the stored data
|
|
7
7
|
# stale relative to its source?" for every produce-method:
|
|
8
|
-
# - intake (source.from:
|
|
8
|
+
# - intake (source.from: fetch) -> AGE signal: now - basis > source.ttl,
|
|
9
9
|
# basis = _meta.last_fetched_at (else file mtime). No ttl -> :no_policy
|
|
10
10
|
# (skipped — a cadence-less handler is not auto-re-pulled).
|
|
11
|
-
# -
|
|
11
|
+
# - external -> DRIFT signal: a source changed since generated.at
|
|
12
12
|
# (surfaced by the doctor generator_drift check; derived entries annotate
|
|
13
13
|
# fresh at read time because converge runs them reactively).
|
|
14
|
-
# Replaces
|
|
14
|
+
# Replaces Core::IntakeStaleness and Core::Staleness::GeneratorCheck and
|
|
15
15
|
# the inline copies in Read::Get / Read::Freshness.
|
|
16
16
|
class Evaluator
|
|
17
17
|
def initialize(manifest:, file_stat:, clock:)
|
|
@@ -33,10 +33,10 @@ module Textus
|
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
# Keys of intake entries past their source.ttl — the converge produce
|
|
36
|
-
# scope (replaces
|
|
36
|
+
# scope (replaces Core::IntakeStaleness#call). A ttl-less intake entry
|
|
37
37
|
# is :no_policy and skipped; a never-recorded one (with a ttl) is stale.
|
|
38
|
-
def stale_intake_keys(prefix: nil,
|
|
39
|
-
@manifest.data.entries.select { |m| due?(m, prefix: prefix,
|
|
38
|
+
def stale_intake_keys(prefix: nil, lane: nil)
|
|
39
|
+
@manifest.data.entries.select { |m| due?(m, prefix: prefix, lane: lane) }.map(&:key)
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
# Age basis as a Time (or nil): _meta.last_fetched_at when present, else
|
|
@@ -62,9 +62,9 @@ module Textus
|
|
|
62
62
|
|
|
63
63
|
def fresh = Verdict.build(stale: false, reason: nil, fetching: false)
|
|
64
64
|
|
|
65
|
-
def due?(mentry, prefix:,
|
|
65
|
+
def due?(mentry, prefix:, lane:)
|
|
66
66
|
return false unless mentry.intake?
|
|
67
|
-
return false if
|
|
67
|
+
return false if lane && mentry.lane != lane
|
|
68
68
|
return false if prefix && !mentry.key.start_with?(prefix)
|
|
69
69
|
|
|
70
70
|
ttl = mentry.source.ttl_seconds
|
|
@@ -92,7 +92,7 @@ module Textus
|
|
|
92
92
|
|
|
93
93
|
# --- generator drift (lifted from Staleness::GeneratorCheck) ---
|
|
94
94
|
|
|
95
|
-
def drift_applicable?(mentry) = mentry.
|
|
95
|
+
def drift_applicable?(mentry) = mentry.external?
|
|
96
96
|
|
|
97
97
|
def drift_reason(mentry, path)
|
|
98
98
|
return "derived entry has never been generated" unless @file_stat.exists?(path)
|
|
@@ -158,7 +158,7 @@ module Textus
|
|
|
158
158
|
def file?(fpath) = !@file_stat.directory?(fpath) && @file_stat.exists?(fpath)
|
|
159
159
|
|
|
160
160
|
def drift_row(mentry, path, reason)
|
|
161
|
-
{ "key" => mentry.key, "path" => path, "generator" => mentry.
|
|
161
|
+
{ "key" => mentry.key, "path" => path, "generator" => mentry.source.command, "reason" => reason }
|
|
162
162
|
end
|
|
163
163
|
end
|
|
164
164
|
end
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Textus
|
|
4
|
-
module
|
|
4
|
+
module Core
|
|
5
5
|
module Freshness
|
|
6
6
|
# Value object describing the freshness annotation attached to an Envelope
|
|
7
|
-
# after a currency evaluation (ADR 0099 — was
|
|
7
|
+
# after a currency evaluation (ADR 0099 — was Core::Freshness).
|
|
8
8
|
#
|
|
9
9
|
# Note on wire format: `#to_h_for_wire` is intentionally narrower than the
|
|
10
10
|
# full field set. It emits the legacy keys ("stale", "stale_reason",
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Textus
|
|
4
|
-
module
|
|
4
|
+
module Core
|
|
5
5
|
# Currency — "is the stored data stale relative to its source?" (ADR 0099).
|
|
6
6
|
# The home of the single Freshness evaluator and its Verdict value object.
|
|
7
|
-
# Distinct from
|
|
7
|
+
# Distinct from Core::Retention (GC dueness, Q2).
|
|
8
8
|
module Freshness
|
|
9
9
|
end
|
|
10
10
|
end
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
require "time"
|
|
2
2
|
|
|
3
3
|
module Textus
|
|
4
|
-
module
|
|
4
|
+
module Core
|
|
5
5
|
module Retention
|
|
6
6
|
# Retention sweep reporter (ADR 0093/0099). Which entries are past their
|
|
7
7
|
# `retention:` ttl and the destructive action that applies. Age basis: file
|
|
8
|
-
# mtime. Only drop/archive. Renamed off the
|
|
9
|
-
#
|
|
8
|
+
# mtime. Only drop/archive. Renamed off the Core::Retention vs
|
|
9
|
+
# Manifest::Policy::Retention collision (ADR 0099).
|
|
10
10
|
class Sweep
|
|
11
11
|
def self.expired?(ttl_seconds:, mtime:, now:)
|
|
12
12
|
return false if ttl_seconds.nil? || mtime.nil?
|
|
@@ -20,16 +20,16 @@ module Textus
|
|
|
20
20
|
@clock = clock
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
def call(prefix: nil,
|
|
23
|
+
def call(prefix: nil, lane: nil)
|
|
24
24
|
@manifest.data.entries
|
|
25
|
-
.select { |m| matches?(m, prefix: prefix,
|
|
25
|
+
.select { |m| matches?(m, prefix: prefix, lane: lane) }
|
|
26
26
|
.flat_map { |m| rows_for(m) }
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
private
|
|
30
30
|
|
|
31
|
-
def matches?(mentry, prefix:,
|
|
32
|
-
return false if
|
|
31
|
+
def matches?(mentry, prefix:, lane:)
|
|
32
|
+
return false if lane && mentry.lane != lane
|
|
33
33
|
return false if prefix && !Textus::Key::Matching.matches_prefix?(
|
|
34
34
|
mentry.key, prefix, nested: mentry.is_a?(Textus::Manifest::Entry::Nested)
|
|
35
35
|
)
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Textus
|
|
4
|
-
module
|
|
4
|
+
module Core
|
|
5
5
|
# Retention — "is the entry old enough to retire?" (Q2, ADR 0093/0099).
|
|
6
6
|
# GC dueness, orthogonal to Freshness (content currency). The reporter is
|
|
7
|
-
#
|
|
7
|
+
# Core::Retention::Sweep; the manifest rule policy is Manifest::Policy::Retention.
|
|
8
8
|
module Retention
|
|
9
9
|
end
|
|
10
10
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
require "digest"
|
|
2
2
|
|
|
3
3
|
module Textus
|
|
4
|
-
module
|
|
4
|
+
module Core
|
|
5
5
|
# Pure value object representing a published-file sentinel. Holds the
|
|
6
6
|
# recorded target path, source path, sha256 checksum, and publish mode.
|
|
7
7
|
# Has no filesystem I/O — path layout and persistence live in
|
|
@@ -8,7 +8,7 @@ module Textus
|
|
|
8
8
|
# verb reported.
|
|
9
9
|
class GeneratorDrift < Check
|
|
10
10
|
def call
|
|
11
|
-
gen = Textus::
|
|
11
|
+
gen = Textus::Core::Freshness::Evaluator.new(
|
|
12
12
|
manifest: manifest,
|
|
13
13
|
file_stat: Textus::Ports::Storage::FileStat.new,
|
|
14
14
|
clock: Textus::Ports::Clock.new,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Doctor
|
|
3
|
+
class Check
|
|
4
|
+
# For every entry with a fetch handler, look up its handler_permit policy
|
|
5
|
+
# (if any) and verify the declared handler is permitted. Emits a failure
|
|
6
|
+
# when the handler is rejected by policy.
|
|
7
|
+
class HandlerPermit < Check
|
|
8
|
+
def call
|
|
9
|
+
out = []
|
|
10
|
+
manifest.data.entries.each do |mentry|
|
|
11
|
+
next unless mentry.intake?
|
|
12
|
+
|
|
13
|
+
handler = mentry.handler
|
|
14
|
+
|
|
15
|
+
permit = manifest.rules.for(mentry.key).handler_permit
|
|
16
|
+
next if permit.nil?
|
|
17
|
+
next if permit.permits?(handler)
|
|
18
|
+
|
|
19
|
+
out << {
|
|
20
|
+
"code" => "policy.handler_not_permitted",
|
|
21
|
+
"level" => "error",
|
|
22
|
+
"subject" => mentry.key,
|
|
23
|
+
"message" => "entry '#{mentry.key}' declares source.handler='#{handler}' but " \
|
|
24
|
+
"handler_permit allows only: #{permit.handlers.join(", ")}",
|
|
25
|
+
"fix" => "change handler to one of [#{permit.handlers.join(", ")}] or " \
|
|
26
|
+
"extend handler_permit in .textus/manifest.yaml",
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
out
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -4,26 +4,19 @@ module Textus
|
|
|
4
4
|
class Hooks < Check
|
|
5
5
|
def call
|
|
6
6
|
out = []
|
|
7
|
-
dir = File.join(root, "
|
|
7
|
+
dir = File.join(root, "steps")
|
|
8
8
|
return out unless File.directory?(dir)
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"code" => "hook.load_failed",
|
|
21
|
-
"level" => "error",
|
|
22
|
-
"subject" => File.basename(f),
|
|
23
|
-
"message" => "#{e.class}: #{e.message}",
|
|
24
|
-
"fix" => "open #{f} and fix the syntax/load error",
|
|
25
|
-
}
|
|
26
|
-
end
|
|
10
|
+
Textus::Step::Loader.new(registry: Textus::Step::RegistryStore.new).load_dir(dir)
|
|
11
|
+
out
|
|
12
|
+
rescue Textus::UsageError => e
|
|
13
|
+
out << {
|
|
14
|
+
"code" => "step.load_failed",
|
|
15
|
+
"level" => "error",
|
|
16
|
+
"subject" => "steps",
|
|
17
|
+
"message" => e.message,
|
|
18
|
+
"fix" => "open the named step file under .textus/steps/ and fix the error",
|
|
19
|
+
}
|
|
27
20
|
out
|
|
28
21
|
end
|
|
29
22
|
end
|
|
@@ -8,7 +8,7 @@ module Textus
|
|
|
8
8
|
next unless entry.nested?
|
|
9
9
|
next if entry.publish_mode.keyless? # publish_tree files are opaque payload, never keys (ADR 0047)
|
|
10
10
|
|
|
11
|
-
base = File.join(root,
|
|
11
|
+
base = File.join(root, Textus::Key::Path.normalize_relative_path(entry.path))
|
|
12
12
|
next unless File.directory?(base)
|
|
13
13
|
|
|
14
14
|
check_all_paths(entry, base, out)
|
|
@@ -6,15 +6,15 @@ module Textus
|
|
|
6
6
|
|
|
7
7
|
def call
|
|
8
8
|
declared = collect_declared_handlers
|
|
9
|
-
registered =
|
|
9
|
+
registered = steps.names(:fetch).to_set
|
|
10
10
|
|
|
11
11
|
out = (declared - registered).map do |name|
|
|
12
12
|
{
|
|
13
13
|
"code" => "intake.handler_missing",
|
|
14
14
|
"level" => "error",
|
|
15
15
|
"subject" => name.to_s,
|
|
16
|
-
"message" => "manifest references intake handler '#{name}' but no
|
|
17
|
-
"fix" => "create .textus/
|
|
16
|
+
"message" => "manifest references intake handler '#{name}' but no fetch step for '#{name}' is registered",
|
|
17
|
+
"fix" => "create .textus/steps/fetch/#{name}.rb with `class #{name.to_s.split(/[-_]/).map(&:capitalize).join}Fetch < Textus::Step::Fetch`",
|
|
18
18
|
}
|
|
19
19
|
end
|
|
20
20
|
|
|
@@ -23,8 +23,8 @@ module Textus
|
|
|
23
23
|
"code" => "intake.handler_orphan",
|
|
24
24
|
"level" => "warning",
|
|
25
25
|
"subject" => name.to_s,
|
|
26
|
-
"message" => "
|
|
27
|
-
"fix" => "remove the unused
|
|
26
|
+
"message" => "fetch step '#{name}' is registered but no manifest entry references it",
|
|
27
|
+
"fix" => "remove the unused step, or add an entry with `intake.handler: #{name}`",
|
|
28
28
|
}
|
|
29
29
|
end
|
|
30
30
|
|
|
@@ -8,10 +8,10 @@ module Textus
|
|
|
8
8
|
# corruption (the accept gate already refuses them).
|
|
9
9
|
class ProposalTargets < Check
|
|
10
10
|
def call
|
|
11
|
-
queue = manifest.policy.
|
|
11
|
+
queue = manifest.policy.queue_lane
|
|
12
12
|
return [] unless queue
|
|
13
13
|
|
|
14
|
-
dispatch(:list,
|
|
14
|
+
dispatch(:list, lane: queue).filter_map { |row| issue_for(row["key"]) }
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
private
|
|
@@ -20,7 +20,7 @@ module Textus
|
|
|
20
20
|
target = dispatch(:get, key).meta&.dig("proposal", "target_key")
|
|
21
21
|
return nil if target.nil? # not a proposal entry — skip
|
|
22
22
|
|
|
23
|
-
zone = manifest.resolver.resolve(target).entry.
|
|
23
|
+
zone = manifest.resolver.resolve(target).entry.lane
|
|
24
24
|
return nil if manifest.policy.declared_kind(zone.to_s) == :canon
|
|
25
25
|
|
|
26
26
|
{
|
|
@@ -2,7 +2,7 @@ module Textus
|
|
|
2
2
|
module Doctor
|
|
3
3
|
class Check
|
|
4
4
|
# Flags entries whose key is matched by two or more rule blocks of the
|
|
5
|
-
# SAME specificity in the same slot (lifecycle /
|
|
5
|
+
# SAME specificity in the same slot (lifecycle / handler_permit /
|
|
6
6
|
# guard / materialize). Ties are non-deterministic in the parser's pick step, so
|
|
7
7
|
# they're a configuration smell — surface them.
|
|
8
8
|
class RuleAmbiguity < Check
|
|
@@ -26,7 +26,7 @@ module Textus
|
|
|
26
26
|
carriers = matches.select { |b| b.public_send(slot) }
|
|
27
27
|
return [] if carriers.length < 2
|
|
28
28
|
|
|
29
|
-
by_specificity = carriers.group_by { |b| Textus::
|
|
29
|
+
by_specificity = carriers.group_by { |b| Textus::Manifest::Policy::Matcher.specificity(b.match) }
|
|
30
30
|
tied = by_specificity.values.select { |group| group.length > 1 }
|
|
31
31
|
tied.map { |group| issue_for(mentry, slot, group) }
|
|
32
32
|
end
|
|
@@ -3,8 +3,14 @@ module Textus
|
|
|
3
3
|
class Check
|
|
4
4
|
class SchemaViolations < Check
|
|
5
5
|
def call
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
result = Textus::Doctor::Validator.new(
|
|
7
|
+
reader: ->(key, ctnr, c) { Textus::Action::Get.new(key: key).call(container: ctnr, call: c) },
|
|
8
|
+
manifest: @container.manifest,
|
|
9
|
+
audit_log: @container.audit_log,
|
|
10
|
+
schema_for: ->(name) { @container.schemas.fetch_or_nil(name) },
|
|
11
|
+
).call(container: @container, call: Textus::Call.build(role: Textus::Role::DEFAULT))
|
|
12
|
+
|
|
13
|
+
result["violations"].map do |v|
|
|
8
14
|
fix = v["expected"] &&
|
|
9
15
|
"field '#{v["field"]}' should be written by '#{v["expected"]}' (last writer: #{v["last_writer"]})"
|
|
10
16
|
{
|