textus 0.49.0 → 0.51.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 +45 -0
- data/README.md +41 -43
- data/SPEC.md +176 -197
- data/docs/architecture/README.md +46 -42
- data/docs/reference/conventions.md +33 -28
- data/lib/textus/boot.rb +58 -47
- data/lib/textus/call.rb +1 -1
- data/lib/textus/cli/runner.rb +15 -10
- data/lib/textus/cli/verb/get.rb +1 -3
- data/lib/textus/cli/verb/hook_run.rb +1 -1
- data/lib/textus/cli/verb/put.rb +4 -20
- data/lib/textus/cli.rb +1 -4
- data/lib/textus/dispatcher.rb +1 -3
- data/lib/textus/doctor/check/generator_drift.rb +4 -3
- data/lib/textus/doctor/check/handler_allowlist.rb +1 -1
- data/lib/textus/doctor/check/intake_registration.rb +5 -5
- data/lib/textus/doctor/check/rule_ambiguity.rb +3 -3
- data/lib/textus/doctor/check/sentinels.rb +2 -2
- data/lib/textus/doctor/check/templates.rb +13 -11
- data/lib/textus/doctor.rb +0 -2
- data/lib/textus/domain/freshness/evaluator.rb +150 -14
- data/lib/textus/domain/freshness/verdict.rb +28 -6
- data/lib/textus/domain/freshness.rb +4 -33
- data/lib/textus/domain/policy/base_guards.rb +1 -1
- data/lib/textus/domain/policy/predicates/fresh_within.rb +1 -1
- data/lib/textus/domain/policy/publish_target.rb +34 -0
- data/lib/textus/domain/policy/retention.rb +29 -0
- data/lib/textus/domain/policy/source.rb +79 -0
- data/lib/textus/domain/retention/sweep.rb +57 -0
- data/lib/textus/domain/retention.rb +11 -0
- data/lib/textus/errors.rb +4 -4
- data/lib/textus/hooks/builtin.rb +5 -5
- data/lib/textus/hooks/catalog.rb +8 -7
- data/lib/textus/hooks/context.rb +5 -10
- data/lib/textus/init/templates/machine_intake.rb +4 -4
- data/lib/textus/init.rb +47 -47
- data/lib/textus/key/matching.rb +24 -0
- data/lib/textus/maintenance/reconcile.rb +160 -0
- data/lib/textus/manifest/capabilities.rb +1 -1
- data/lib/textus/manifest/data.rb +2 -2
- data/lib/textus/manifest/entry/base.rb +28 -8
- data/lib/textus/manifest/entry/nested.rb +3 -4
- data/lib/textus/manifest/entry/parser.rb +25 -21
- data/lib/textus/manifest/entry/produced.rb +56 -0
- data/lib/textus/manifest/entry/publish/subtree_mirror.rb +7 -6
- data/lib/textus/manifest/entry/publish/to_paths.rb +62 -11
- data/lib/textus/manifest/entry/validators/format_matrix.rb +3 -11
- data/lib/textus/manifest/entry/validators/publish.rb +3 -1
- data/lib/textus/manifest/entry/validators.rb +0 -1
- data/lib/textus/manifest/policy.rb +16 -4
- data/lib/textus/manifest/resolver.rb +10 -4
- data/lib/textus/manifest/rules.rb +37 -36
- data/lib/textus/manifest/schema/keys.rb +98 -0
- data/lib/textus/manifest/schema/validator.rb +324 -0
- data/lib/textus/manifest/schema/vocabulary.rb +24 -0
- data/lib/textus/manifest/schema.rb +27 -247
- data/lib/textus/manifest.rb +5 -3
- data/lib/textus/mcp/server.rb +9 -2
- data/lib/textus/ports/audit_log.rb +6 -0
- data/lib/textus/ports/build_lock.rb +6 -0
- data/lib/textus/ports/clock.rb +4 -3
- data/lib/textus/ports/produce_on_write_subscriber.rb +69 -0
- data/lib/textus/ports/publisher.rb +11 -7
- data/lib/textus/produce/acquire/handler.rb +29 -0
- data/lib/textus/produce/acquire/intake.rb +130 -0
- data/lib/textus/produce/acquire/projection.rb +127 -0
- data/lib/textus/produce/acquire/serializer/json.rb +31 -0
- data/lib/textus/produce/acquire/serializer/text.rb +16 -0
- data/lib/textus/produce/acquire/serializer/yaml.rb +31 -0
- data/lib/textus/produce/acquire/serializer.rb +17 -0
- data/lib/textus/produce/engine.rb +143 -0
- data/lib/textus/produce/events.rb +36 -0
- data/lib/textus/produce/render.rb +23 -0
- data/lib/textus/projection.rb +17 -6
- data/lib/textus/read/boot.rb +4 -2
- data/lib/textus/read/deps.rb +3 -3
- data/lib/textus/read/freshness.rb +63 -29
- data/lib/textus/read/get.rb +20 -102
- data/lib/textus/read/rdeps.rb +3 -3
- data/lib/textus/read/rule_explain.rb +41 -23
- data/lib/textus/read/rule_list.rb +25 -8
- data/lib/textus/read/validate_all.rb +14 -0
- data/lib/textus/role.rb +2 -1
- data/lib/textus/schemas.rb +8 -0
- data/lib/textus/store.rb +1 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus/write/put.rb +1 -1
- metadata +23 -30
- data/lib/textus/builder/pipeline.rb +0 -88
- data/lib/textus/builder/renderer/json.rb +0 -45
- data/lib/textus/builder/renderer/markdown.rb +0 -24
- data/lib/textus/builder/renderer/text.rb +0 -14
- data/lib/textus/builder/renderer/yaml.rb +0 -45
- data/lib/textus/builder/renderer.rb +0 -17
- data/lib/textus/cli/verb/boot.rb +0 -13
- data/lib/textus/cli/verb/build.rb +0 -15
- data/lib/textus/doctor/check/fetch_locks.rb +0 -49
- data/lib/textus/doctor/check/lifecycle_action_invalid.rb +0 -39
- data/lib/textus/domain/freshness/policy.rb +0 -18
- data/lib/textus/domain/lifecycle.rb +0 -83
- data/lib/textus/domain/outcome.rb +0 -10
- data/lib/textus/domain/policy/lifecycle.rb +0 -35
- data/lib/textus/domain/staleness/generator_check.rb +0 -109
- data/lib/textus/domain/staleness.rb +0 -29
- data/lib/textus/maintenance/tend.rb +0 -110
- data/lib/textus/manifest/entry/derived.rb +0 -65
- data/lib/textus/manifest/entry/intake.rb +0 -31
- data/lib/textus/manifest/entry/validators/inject_boot.rb +0 -21
- data/lib/textus/mcp/tools.rb +0 -14
- data/lib/textus/ports/fetch/detached.rb +0 -52
- data/lib/textus/ports/fetch/lock.rb +0 -44
- data/lib/textus/write/build.rb +0 -90
- data/lib/textus/write/fetch_events.rb +0 -42
- data/lib/textus/write/fetch_orchestrator.rb +0 -101
- data/lib/textus/write/fetch_worker.rb +0 -127
- data/lib/textus/write/intake_fetch.rb +0 -25
- data/lib/textus/write/materializer.rb +0 -51
|
@@ -14,9 +14,9 @@ module Textus
|
|
|
14
14
|
surfaces :cli, :mcp
|
|
15
15
|
cli "rule explain"
|
|
16
16
|
arg :key, String, required: true, positional: true,
|
|
17
|
-
description: "dotted key whose effective rules you want (
|
|
17
|
+
description: "dotted key whose effective rules you want (lifecycle ttl/action, write guard, ...)"
|
|
18
18
|
arg :detail, :boolean,
|
|
19
|
-
description: "defaults false: lean {
|
|
19
|
+
description: "defaults false: lean {lifecycle, guard}. detail: true adds matched blocks + guard predicates per transition."
|
|
20
20
|
view(:cli) { |r| { "verb" => "rule_explain" }.merge(r.transform_keys(&:to_s)) }
|
|
21
21
|
|
|
22
22
|
def initialize(container:, call: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
@@ -24,6 +24,16 @@ module Textus
|
|
|
24
24
|
@schemas = container.schemas
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
REGISTRY = Textus::Manifest::Schema::FIELD_REGISTRY
|
|
28
|
+
# Field membership is registry-driven (WS3). Lean shows the fields tagged
|
|
29
|
+
# for :lean; detail's matched_blocks flag every :detail field. The
|
|
30
|
+
# `effective` value-block shows the instantiated-policy fields (those with
|
|
31
|
+
# a policy_class) — guard, being a raw deferred field, is surfaced through
|
|
32
|
+
# the dedicated `guards:` predicate section instead.
|
|
33
|
+
LEAN_FIELDS = REGISTRY.select { |_, m| m[:in_rule_explain].include?(:lean) }.keys.freeze
|
|
34
|
+
DETAIL_FIELDS = REGISTRY.select { |_, m| m[:in_rule_explain].include?(:detail) }.keys.freeze
|
|
35
|
+
EFFECTIVE_FIELDS = DETAIL_FIELDS.select { |f| REGISTRY[f][:policy_class] }.freeze
|
|
36
|
+
|
|
27
37
|
def call(key, detail: false)
|
|
28
38
|
detail ? explain(key) : effective(key)
|
|
29
39
|
end
|
|
@@ -33,14 +43,17 @@ module Textus
|
|
|
33
43
|
# Lean: the effective winners only (formerly Read::Rules / the `rules` verb).
|
|
34
44
|
def effective(key)
|
|
35
45
|
set = @manifest.rules.for(key)
|
|
36
|
-
{
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
LEAN_FIELDS.each_with_object({}) do |field, out|
|
|
47
|
+
value = set.public_send(field)
|
|
48
|
+
out[field.to_s] = lean_value(field, value) unless value.nil?
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def lean_value(field, value)
|
|
53
|
+
case field
|
|
54
|
+
when :retention then retention_hash(value, string_keys: true)
|
|
55
|
+
else value
|
|
56
|
+
end
|
|
44
57
|
end
|
|
45
58
|
|
|
46
59
|
# Verbose: every matching block, per-slot effective value, and the
|
|
@@ -54,25 +67,30 @@ module Textus
|
|
|
54
67
|
{
|
|
55
68
|
key: key,
|
|
56
69
|
matched_blocks: matching.map do |b|
|
|
57
|
-
{
|
|
58
|
-
match: b.match,
|
|
59
|
-
lifecycle: !b.lifecycle.nil?,
|
|
60
|
-
handler_allowlist: !b.handler_allowlist.nil?,
|
|
61
|
-
guard: !b.guard.nil?,
|
|
62
|
-
}
|
|
70
|
+
{ match: b.match }.merge(DETAIL_FIELDS.to_h { |f| [f, !b.public_send(f).nil?] })
|
|
63
71
|
end,
|
|
64
|
-
effective: {
|
|
65
|
-
lifecycle: winners.lifecycle && {
|
|
66
|
-
ttl_seconds: winners.lifecycle.ttl_seconds,
|
|
67
|
-
on_expire: winners.lifecycle.on_expire,
|
|
68
|
-
},
|
|
69
|
-
handler_allowlist: winners.handler_allowlist&.handlers,
|
|
70
|
-
},
|
|
72
|
+
effective: EFFECTIVE_FIELDS.to_h { |f| [f, effective_value(f, winners.public_send(f))] },
|
|
71
73
|
guards: Textus::Domain::Policy::BaseGuards::BASE.keys.to_h do |transition|
|
|
72
74
|
[transition, factory.for(transition, key).predicates.map(&:name)]
|
|
73
75
|
end,
|
|
74
76
|
}
|
|
75
77
|
end
|
|
78
|
+
|
|
79
|
+
def effective_value(field, value)
|
|
80
|
+
return nil if value.nil?
|
|
81
|
+
|
|
82
|
+
case field
|
|
83
|
+
when :retention then retention_hash(value, string_keys: false)
|
|
84
|
+
when :handler_allowlist then value.handlers
|
|
85
|
+
else value
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# ADR 0093: retention is a flat GC policy (ttl + drop/archive action).
|
|
90
|
+
def retention_hash(retention, string_keys:)
|
|
91
|
+
h = { ttl_seconds: retention.ttl_seconds, action: retention.action }
|
|
92
|
+
string_keys ? h.transform_keys(&:to_s) : h
|
|
93
|
+
end
|
|
76
94
|
end
|
|
77
95
|
end
|
|
78
96
|
end
|
|
@@ -17,21 +17,38 @@ module Textus
|
|
|
17
17
|
@manifest = container.manifest
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
# Fields shown here are driven by FIELD_REGISTRY (in_rule_list); only the
|
|
21
|
+
# per-field serialization below is field-specific.
|
|
22
|
+
LIST_FIELDS = Textus::Manifest::Schema::FIELD_REGISTRY.select { |_, m| m[:in_rule_list] }.keys.freeze
|
|
23
|
+
|
|
20
24
|
def call
|
|
21
25
|
@manifest.rules.blocks.map do |b|
|
|
22
26
|
row = { "match" => b.match }
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"on_expire" => b.lifecycle.on_expire,
|
|
27
|
-
"budget_ms" => b.lifecycle.budget_ms,
|
|
28
|
-
}
|
|
27
|
+
LIST_FIELDS.each do |field|
|
|
28
|
+
value = b.public_send(field)
|
|
29
|
+
row[field.to_s] = serialize(field, value) unless value.nil?
|
|
29
30
|
end
|
|
30
|
-
row["handler_allowlist"] = b.handler_allowlist.handlers if b.handler_allowlist
|
|
31
|
-
row["guard"] = b.guard if b.guard
|
|
32
31
|
row
|
|
33
32
|
end
|
|
34
33
|
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def serialize(field, value)
|
|
38
|
+
case field
|
|
39
|
+
when :retention
|
|
40
|
+
serialize_retention(value)
|
|
41
|
+
when :handler_allowlist
|
|
42
|
+
value.handlers
|
|
43
|
+
else
|
|
44
|
+
value
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# ADR 0093: retention is a flat GC policy.
|
|
49
|
+
def serialize_retention(retention)
|
|
50
|
+
{ "ttl_seconds" => retention.ttl_seconds, "action" => retention.action.to_s }
|
|
51
|
+
end
|
|
35
52
|
end
|
|
36
53
|
end
|
|
37
54
|
end
|
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
module Textus
|
|
2
2
|
module Read
|
|
3
|
+
# Store-wide schema + role-authority validation: walks every entry and runs
|
|
4
|
+
# the Validator over it. Consumed internally by `doctor`'s schema_violations
|
|
5
|
+
# check and exposed as a Ruby store method (`store.validate_all`).
|
|
6
|
+
#
|
|
7
|
+
# Ruby-only, like `Read::Freshness`: it declares a contract (so it round-trips
|
|
8
|
+
# through the routing<->contract bijection, ADR 0105) but omits `surfaces`, so
|
|
9
|
+
# it gets no CLI or MCP projection. The public `validate-all` CLI verb was
|
|
10
|
+
# removed in v0.5 (`doctor --check=schema_violations` replaces it).
|
|
3
11
|
class ValidateAll
|
|
12
|
+
extend Textus::Contract::DSL
|
|
13
|
+
|
|
14
|
+
verb :validate_all
|
|
15
|
+
summary "Internal store-wide schema + role-authority validation; backs " \
|
|
16
|
+
"doctor's schema_violations check. No public surface (ADR 0105)."
|
|
17
|
+
|
|
4
18
|
def initialize(container:, call:)
|
|
5
19
|
@container = container
|
|
6
20
|
@call = call
|
data/lib/textus/role.rb
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
module Textus
|
|
2
2
|
module Role
|
|
3
3
|
# The three role archetypes, each string sourced exactly once: human curates
|
|
4
|
-
# canon, agent proposes, automation
|
|
4
|
+
# canon, agent proposes, automation reconciles the machine-maintained lanes
|
|
5
|
+
# (refresh + materialize) (explanation/concepts.md).
|
|
5
6
|
# Reference these constants instead of bare literals (ADR 0044).
|
|
6
7
|
HUMAN = "human".freeze
|
|
7
8
|
AGENT = "agent".freeze
|
data/lib/textus/schemas.rb
CHANGED
|
@@ -24,6 +24,14 @@ module Textus
|
|
|
24
24
|
@schemas.values
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
# Name-keyed view: { canonical_name => Schema }. The key is the schema's
|
|
28
|
+
# file stem, which is authoritative even when a schema file carries no
|
|
29
|
+
# top-level `name:` (Schema#name reads the body and may be nil). Symmetric
|
|
30
|
+
# with #all (values); use this when you need the names too.
|
|
31
|
+
def by_name
|
|
32
|
+
@schemas.dup
|
|
33
|
+
end
|
|
34
|
+
|
|
27
35
|
private
|
|
28
36
|
|
|
29
37
|
def load_all
|
data/lib/textus/store.rb
CHANGED
|
@@ -90,6 +90,7 @@ module Textus
|
|
|
90
90
|
|
|
91
91
|
def bootstrap_hooks
|
|
92
92
|
Ports::AuditSubscriber.new(audit_log).attach(events)
|
|
93
|
+
Ports::ProduceOnWriteSubscriber.new(container).attach(events)
|
|
93
94
|
Hooks::Builtin.register_all(events: events, rpc: rpc)
|
|
94
95
|
Hooks::Loader.new(events: events, rpc: rpc).load_dir(File.join(root, "hooks"))
|
|
95
96
|
end
|
data/lib/textus/version.rb
CHANGED
data/lib/textus/write/put.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: textus
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.51.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrick
|
|
@@ -111,12 +111,6 @@ files:
|
|
|
111
111
|
- exe/textus
|
|
112
112
|
- lib/textus.rb
|
|
113
113
|
- lib/textus/boot.rb
|
|
114
|
-
- lib/textus/builder/pipeline.rb
|
|
115
|
-
- lib/textus/builder/renderer.rb
|
|
116
|
-
- lib/textus/builder/renderer/json.rb
|
|
117
|
-
- lib/textus/builder/renderer/markdown.rb
|
|
118
|
-
- lib/textus/builder/renderer/text.rb
|
|
119
|
-
- lib/textus/builder/renderer/yaml.rb
|
|
120
114
|
- lib/textus/call.rb
|
|
121
115
|
- lib/textus/cli.rb
|
|
122
116
|
- lib/textus/cli/group.rb
|
|
@@ -128,8 +122,6 @@ files:
|
|
|
128
122
|
- lib/textus/cli/group/zone.rb
|
|
129
123
|
- lib/textus/cli/runner.rb
|
|
130
124
|
- lib/textus/cli/verb.rb
|
|
131
|
-
- lib/textus/cli/verb/boot.rb
|
|
132
|
-
- lib/textus/cli/verb/build.rb
|
|
133
125
|
- lib/textus/cli/verb/doctor.rb
|
|
134
126
|
- lib/textus/cli/verb/get.rb
|
|
135
127
|
- lib/textus/cli/verb/hook_run.rb
|
|
@@ -153,13 +145,11 @@ files:
|
|
|
153
145
|
- lib/textus/doctor.rb
|
|
154
146
|
- lib/textus/doctor/check.rb
|
|
155
147
|
- lib/textus/doctor/check/audit_log.rb
|
|
156
|
-
- lib/textus/doctor/check/fetch_locks.rb
|
|
157
148
|
- lib/textus/doctor/check/generator_drift.rb
|
|
158
149
|
- lib/textus/doctor/check/handler_allowlist.rb
|
|
159
150
|
- lib/textus/doctor/check/hooks.rb
|
|
160
151
|
- lib/textus/doctor/check/illegal_keys.rb
|
|
161
152
|
- lib/textus/doctor/check/intake_registration.rb
|
|
162
|
-
- lib/textus/doctor/check/lifecycle_action_invalid.rb
|
|
163
153
|
- lib/textus/doctor/check/manifest_files.rb
|
|
164
154
|
- lib/textus/doctor/check/orphaned_publish_targets.rb
|
|
165
155
|
- lib/textus/doctor/check/proposal_targets.rb
|
|
@@ -176,17 +166,13 @@ files:
|
|
|
176
166
|
- lib/textus/domain/duration.rb
|
|
177
167
|
- lib/textus/domain/freshness.rb
|
|
178
168
|
- lib/textus/domain/freshness/evaluator.rb
|
|
179
|
-
- lib/textus/domain/freshness/policy.rb
|
|
180
169
|
- lib/textus/domain/freshness/verdict.rb
|
|
181
|
-
- lib/textus/domain/lifecycle.rb
|
|
182
|
-
- lib/textus/domain/outcome.rb
|
|
183
170
|
- lib/textus/domain/permission.rb
|
|
184
171
|
- lib/textus/domain/policy/base_guards.rb
|
|
185
172
|
- lib/textus/domain/policy/evaluation.rb
|
|
186
173
|
- lib/textus/domain/policy/guard.rb
|
|
187
174
|
- lib/textus/domain/policy/guard_factory.rb
|
|
188
175
|
- lib/textus/domain/policy/handler_allowlist.rb
|
|
189
|
-
- lib/textus/domain/policy/lifecycle.rb
|
|
190
176
|
- lib/textus/domain/policy/matcher.rb
|
|
191
177
|
- lib/textus/domain/policy/predicates/author_held.rb
|
|
192
178
|
- lib/textus/domain/policy/predicates/etag_match.rb
|
|
@@ -195,9 +181,12 @@ files:
|
|
|
195
181
|
- lib/textus/domain/policy/predicates/schema_valid.rb
|
|
196
182
|
- lib/textus/domain/policy/predicates/target_is_canon.rb
|
|
197
183
|
- lib/textus/domain/policy/predicates/zone_writable_by.rb
|
|
184
|
+
- lib/textus/domain/policy/publish_target.rb
|
|
185
|
+
- lib/textus/domain/policy/retention.rb
|
|
186
|
+
- lib/textus/domain/policy/source.rb
|
|
187
|
+
- lib/textus/domain/retention.rb
|
|
188
|
+
- lib/textus/domain/retention/sweep.rb
|
|
198
189
|
- lib/textus/domain/sentinel.rb
|
|
199
|
-
- lib/textus/domain/staleness.rb
|
|
200
|
-
- lib/textus/domain/staleness/generator_check.rb
|
|
201
190
|
- lib/textus/entry.rb
|
|
202
191
|
- lib/textus/entry/base.rb
|
|
203
192
|
- lib/textus/entry/json.rb
|
|
@@ -223,25 +212,25 @@ files:
|
|
|
223
212
|
- lib/textus/init/templates/orientation_reducer.rb
|
|
224
213
|
- lib/textus/key/distance.rb
|
|
225
214
|
- lib/textus/key/grammar.rb
|
|
215
|
+
- lib/textus/key/matching.rb
|
|
226
216
|
- lib/textus/key/path.rb
|
|
227
217
|
- lib/textus/layout.rb
|
|
228
218
|
- lib/textus/maintenance.rb
|
|
229
219
|
- lib/textus/maintenance/key_delete_prefix.rb
|
|
230
220
|
- lib/textus/maintenance/key_mv_prefix.rb
|
|
221
|
+
- lib/textus/maintenance/reconcile.rb
|
|
231
222
|
- lib/textus/maintenance/rule_lint.rb
|
|
232
|
-
- lib/textus/maintenance/tend.rb
|
|
233
223
|
- lib/textus/maintenance/zone_mv.rb
|
|
234
224
|
- lib/textus/manifest.rb
|
|
235
225
|
- lib/textus/manifest/capabilities.rb
|
|
236
226
|
- lib/textus/manifest/data.rb
|
|
237
227
|
- lib/textus/manifest/entry.rb
|
|
238
228
|
- lib/textus/manifest/entry/base.rb
|
|
239
|
-
- lib/textus/manifest/entry/derived.rb
|
|
240
229
|
- lib/textus/manifest/entry/ignore_matcher.rb
|
|
241
|
-
- lib/textus/manifest/entry/intake.rb
|
|
242
230
|
- lib/textus/manifest/entry/leaf.rb
|
|
243
231
|
- lib/textus/manifest/entry/nested.rb
|
|
244
232
|
- lib/textus/manifest/entry/parser.rb
|
|
233
|
+
- lib/textus/manifest/entry/produced.rb
|
|
245
234
|
- lib/textus/manifest/entry/publish.rb
|
|
246
235
|
- lib/textus/manifest/entry/publish/mode.rb
|
|
247
236
|
- lib/textus/manifest/entry/publish/none.rb
|
|
@@ -253,30 +242,40 @@ files:
|
|
|
253
242
|
- lib/textus/manifest/entry/validators/events.rb
|
|
254
243
|
- lib/textus/manifest/entry/validators/format_matrix.rb
|
|
255
244
|
- lib/textus/manifest/entry/validators/ignore.rb
|
|
256
|
-
- lib/textus/manifest/entry/validators/inject_boot.rb
|
|
257
245
|
- lib/textus/manifest/entry/validators/publish.rb
|
|
258
246
|
- lib/textus/manifest/policy.rb
|
|
259
247
|
- lib/textus/manifest/resolver.rb
|
|
260
248
|
- lib/textus/manifest/rules.rb
|
|
261
249
|
- lib/textus/manifest/schema.rb
|
|
250
|
+
- lib/textus/manifest/schema/keys.rb
|
|
251
|
+
- lib/textus/manifest/schema/validator.rb
|
|
252
|
+
- lib/textus/manifest/schema/vocabulary.rb
|
|
262
253
|
- lib/textus/mcp.rb
|
|
263
254
|
- lib/textus/mcp/catalog.rb
|
|
264
255
|
- lib/textus/mcp/errors.rb
|
|
265
256
|
- lib/textus/mcp/server.rb
|
|
266
257
|
- lib/textus/mcp/session.rb
|
|
267
258
|
- lib/textus/mcp/tool_schemas.rb
|
|
268
|
-
- lib/textus/mcp/tools.rb
|
|
269
259
|
- lib/textus/mustache.rb
|
|
270
260
|
- lib/textus/ports/audit_log.rb
|
|
271
261
|
- lib/textus/ports/audit_subscriber.rb
|
|
272
262
|
- lib/textus/ports/build_lock.rb
|
|
273
263
|
- lib/textus/ports/clock.rb
|
|
274
|
-
- lib/textus/ports/
|
|
275
|
-
- lib/textus/ports/fetch/lock.rb
|
|
264
|
+
- lib/textus/ports/produce_on_write_subscriber.rb
|
|
276
265
|
- lib/textus/ports/publisher.rb
|
|
277
266
|
- lib/textus/ports/sentinel_store.rb
|
|
278
267
|
- lib/textus/ports/storage/file_stat.rb
|
|
279
268
|
- lib/textus/ports/storage/file_store.rb
|
|
269
|
+
- lib/textus/produce/acquire/handler.rb
|
|
270
|
+
- lib/textus/produce/acquire/intake.rb
|
|
271
|
+
- lib/textus/produce/acquire/projection.rb
|
|
272
|
+
- lib/textus/produce/acquire/serializer.rb
|
|
273
|
+
- lib/textus/produce/acquire/serializer/json.rb
|
|
274
|
+
- lib/textus/produce/acquire/serializer/text.rb
|
|
275
|
+
- lib/textus/produce/acquire/serializer/yaml.rb
|
|
276
|
+
- lib/textus/produce/engine.rb
|
|
277
|
+
- lib/textus/produce/events.rb
|
|
278
|
+
- lib/textus/produce/render.rb
|
|
280
279
|
- lib/textus/projection.rb
|
|
281
280
|
- lib/textus/read/audit.rb
|
|
282
281
|
- lib/textus/read/blame.rb
|
|
@@ -307,14 +306,8 @@ files:
|
|
|
307
306
|
- lib/textus/uid.rb
|
|
308
307
|
- lib/textus/version.rb
|
|
309
308
|
- lib/textus/write/accept.rb
|
|
310
|
-
- lib/textus/write/build.rb
|
|
311
|
-
- lib/textus/write/fetch_events.rb
|
|
312
|
-
- lib/textus/write/fetch_orchestrator.rb
|
|
313
|
-
- lib/textus/write/fetch_worker.rb
|
|
314
|
-
- lib/textus/write/intake_fetch.rb
|
|
315
309
|
- lib/textus/write/key_delete.rb
|
|
316
310
|
- lib/textus/write/key_mv.rb
|
|
317
|
-
- lib/textus/write/materializer.rb
|
|
318
311
|
- lib/textus/write/propose.rb
|
|
319
312
|
- lib/textus/write/put.rb
|
|
320
313
|
- lib/textus/write/reject.rb
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
require "fileutils"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Builder
|
|
5
|
-
module InjectMeta
|
|
6
|
-
# Returns a new hash with _meta as the first key, per SPEC §6 ordering.
|
|
7
|
-
# Carries only deterministic provenance (`from`/`reduce`/`template`) — the
|
|
8
|
-
# volatile `generated_at` is deliberately NOT stamped, so the built
|
|
9
|
-
# artifact is content-addressed and a rebuild is a byte-for-byte no-op
|
|
10
|
-
# (ADR 0070). Build time lives out of the tracked artifact.
|
|
11
|
-
def self.call(content_hash, mentry)
|
|
12
|
-
meta = {}
|
|
13
|
-
if mentry.is_a?(Textus::Manifest::Entry::Derived)
|
|
14
|
-
src = mentry.source
|
|
15
|
-
if src.is_a?(Textus::Manifest::Entry::Derived::Projection)
|
|
16
|
-
from = Array(src.select).compact
|
|
17
|
-
meta["from"] = from unless from.empty?
|
|
18
|
-
meta["reduce"] = src.transform if src.transform
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
meta["template"] = mentry.template if mentry.template
|
|
22
|
-
|
|
23
|
-
out = { "_meta" => meta }
|
|
24
|
-
content_hash.each { |k, v| out[k] = v unless k == "_meta" }
|
|
25
|
-
out
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
module Pipeline
|
|
30
|
-
Deps = Data.define(
|
|
31
|
-
:manifest, :reader, :lister, :rpc, :template_loader, :transform_context, :inject_boot
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
def self.renderers
|
|
35
|
-
@renderers ||= {
|
|
36
|
-
"markdown" => Renderer::Markdown,
|
|
37
|
-
"text" => Renderer::Text,
|
|
38
|
-
"json" => Renderer::Json,
|
|
39
|
-
"yaml" => Renderer::Yaml,
|
|
40
|
-
}
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def self.run(mentry:, deps:)
|
|
44
|
-
# 1. Load sources + project + reduce. Only projection-derived entries are
|
|
45
|
-
# buildable in-process; External entries are generated out-of-band and are
|
|
46
|
-
# filtered out upstream (Derived#publish_via), so reaching here with a
|
|
47
|
-
# non-projection source is a wiring bug — fail loudly rather than emit an
|
|
48
|
-
# empty payload (and never re-stamp the volatile generated_at, ADR 0070).
|
|
49
|
-
unless mentry.is_a?(Textus::Manifest::Entry::Derived) && mentry.projection?
|
|
50
|
-
raise UsageError.new(
|
|
51
|
-
"builder: '#{mentry.key}' is not a projection-derived entry; only projections are buildable",
|
|
52
|
-
)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
data =
|
|
56
|
-
Textus::Projection.new(
|
|
57
|
-
reader: deps.reader,
|
|
58
|
-
spec: mentry.source.to_h.transform_keys(&:to_s),
|
|
59
|
-
lister: deps.lister,
|
|
60
|
-
rpc: deps.rpc,
|
|
61
|
-
transform_context: deps.transform_context,
|
|
62
|
-
).run
|
|
63
|
-
data = data.merge("boot" => deps.inject_boot.call) if mentry.inject_boot && deps.inject_boot
|
|
64
|
-
|
|
65
|
-
# 2. Render
|
|
66
|
-
klass = renderers[mentry.format] or
|
|
67
|
-
raise UsageError.new("builder: unsupported format #{mentry.format.inspect} for '#{mentry.key}'")
|
|
68
|
-
bytes = klass.new(template_loader: deps.template_loader).call(mentry: mentry, data: data)
|
|
69
|
-
|
|
70
|
-
# 3. Write (idempotent: skip if only generated_at would differ)
|
|
71
|
-
target_path = Key::Path.resolve(deps.manifest.data, mentry)
|
|
72
|
-
FileUtils.mkdir_p(File.dirname(target_path))
|
|
73
|
-
write_if_changed(target_path, bytes, mentry.format)
|
|
74
|
-
|
|
75
|
-
target_path
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# Built artifacts are content-addressed (no volatile timestamp, ADR 0070),
|
|
79
|
-
# so identity is plain byte-equality: skip the write when nothing changed.
|
|
80
|
-
# `format` is retained for signature stability across renderers.
|
|
81
|
-
def self.write_if_changed(target_path, bytes, _format)
|
|
82
|
-
return if File.exist?(target_path) && File.binread(target_path) == bytes
|
|
83
|
-
|
|
84
|
-
File.binwrite(target_path, bytes)
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
end
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
require "json"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Builder
|
|
5
|
-
class Renderer
|
|
6
|
-
class Json < Renderer
|
|
7
|
-
def call(mentry:, data:)
|
|
8
|
-
content = mentry.template ? parse_rendered_template!(mentry, data) : default_shape(mentry, data)
|
|
9
|
-
final = InjectMeta.call(content, mentry)
|
|
10
|
-
Entry.for_format("json").serialize(meta: {}, body: "", content: final)
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
private
|
|
14
|
-
|
|
15
|
-
def parse_rendered_template!(mentry, data)
|
|
16
|
-
rendered = Mustache.render(@template_loader.call(mentry.template), data)
|
|
17
|
-
begin
|
|
18
|
-
parsed = ::JSON.parse(rendered)
|
|
19
|
-
rescue ::JSON::ParserError => e
|
|
20
|
-
raise BadRender.new("entry '#{mentry.key}': template did not render valid json: #{e.message}", format: "json")
|
|
21
|
-
end
|
|
22
|
-
unless parsed.is_a?(Hash)
|
|
23
|
-
raise BadRender.new("entry '#{mentry.key}': template must render a top-level object/mapping",
|
|
24
|
-
format: "json")
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
parsed
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def default_shape(mentry, data)
|
|
31
|
-
has_transform = mentry.is_a?(Textus::Manifest::Entry::Derived) &&
|
|
32
|
-
mentry.source.is_a?(Textus::Manifest::Entry::Derived::Projection) &&
|
|
33
|
-
mentry.source.transform
|
|
34
|
-
if has_transform && data.is_a?(Hash) && !data.key?("entries")
|
|
35
|
-
data
|
|
36
|
-
elsif data.is_a?(Hash) && data["entries"].is_a?(Array)
|
|
37
|
-
{ "entries" => data["entries"] }
|
|
38
|
-
else
|
|
39
|
-
data.is_a?(Hash) ? data : { "entries" => Array(data) }
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Builder
|
|
3
|
-
class Renderer
|
|
4
|
-
class Markdown < Renderer
|
|
5
|
-
def call(mentry:, data:)
|
|
6
|
-
raise TemplateError.new("entry '#{mentry.key}': markdown build requires a template") unless mentry.template
|
|
7
|
-
|
|
8
|
-
body = Mustache.render(@template_loader.call(mentry.template), data)
|
|
9
|
-
from = if mentry.is_a?(Textus::Manifest::Entry::Derived) &&
|
|
10
|
-
mentry.source.is_a?(Textus::Manifest::Entry::Derived::Projection)
|
|
11
|
-
Array(mentry.source.select).compact
|
|
12
|
-
else
|
|
13
|
-
[]
|
|
14
|
-
end
|
|
15
|
-
# Deterministic frontmatter only — `from` (the source keys), never a
|
|
16
|
-
# volatile `generated.at` (ADR 0070): the artifact is content-addressed
|
|
17
|
-
# so a rebuild is a byte-for-byte no-op and a revert never drifts.
|
|
18
|
-
frontmatter = { "generated" => { "from" => from } }
|
|
19
|
-
Entry.for_format("markdown").serialize(meta: frontmatter, body: body)
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Builder
|
|
3
|
-
class Renderer
|
|
4
|
-
class Text < Renderer
|
|
5
|
-
def call(mentry:, data:)
|
|
6
|
-
raise TemplateError.new("entry '#{mentry.key}': text build requires a template") unless mentry.template
|
|
7
|
-
|
|
8
|
-
body = Mustache.render(@template_loader.call(mentry.template), data)
|
|
9
|
-
Entry.for_format("text").serialize(meta: {}, body: body)
|
|
10
|
-
end
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
require "yaml"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Builder
|
|
5
|
-
class Renderer
|
|
6
|
-
class Yaml < Renderer
|
|
7
|
-
def call(mentry:, data:)
|
|
8
|
-
content = mentry.template ? parse_rendered_template!(mentry, data) : default_shape(mentry, data)
|
|
9
|
-
final = InjectMeta.call(content, mentry)
|
|
10
|
-
Entry.for_format("yaml").serialize(meta: {}, body: "", content: final)
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
private
|
|
14
|
-
|
|
15
|
-
def parse_rendered_template!(mentry, data)
|
|
16
|
-
rendered = Mustache.render(@template_loader.call(mentry.template), data)
|
|
17
|
-
begin
|
|
18
|
-
parsed = ::YAML.safe_load(rendered, permitted_classes: [Date, Time], aliases: false)
|
|
19
|
-
rescue Psych::SyntaxError, Psych::DisallowedClass, Psych::AliasesNotEnabled => e
|
|
20
|
-
raise BadRender.new("entry '#{mentry.key}': template did not render valid yaml: #{e.message}", format: "yaml")
|
|
21
|
-
end
|
|
22
|
-
unless parsed.is_a?(Hash)
|
|
23
|
-
raise BadRender.new("entry '#{mentry.key}': template must render a top-level object/mapping",
|
|
24
|
-
format: "yaml")
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
parsed
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def default_shape(mentry, data)
|
|
31
|
-
has_transform = mentry.is_a?(Textus::Manifest::Entry::Derived) &&
|
|
32
|
-
mentry.source.is_a?(Textus::Manifest::Entry::Derived::Projection) &&
|
|
33
|
-
mentry.source.transform
|
|
34
|
-
if has_transform && data.is_a?(Hash) && !data.key?("entries")
|
|
35
|
-
data
|
|
36
|
-
elsif data.is_a?(Hash) && data["entries"].is_a?(Array)
|
|
37
|
-
{ "entries" => data["entries"] }
|
|
38
|
-
else
|
|
39
|
-
data.is_a?(Hash) ? data : { "entries" => Array(data) }
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Builder
|
|
3
|
-
# Abstract base for output renderers. Each concrete renderer owns
|
|
4
|
-
# producing the bytes for one manifest format (markdown/json/yaml/text).
|
|
5
|
-
class Renderer
|
|
6
|
-
def initialize(template_loader:)
|
|
7
|
-
@template_loader = template_loader
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def call(mentry:, data:)
|
|
11
|
-
_ = mentry
|
|
12
|
-
_ = data
|
|
13
|
-
raise NotImplementedError.new("#{self.class.name}#call not implemented")
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|
data/lib/textus/cli/verb/boot.rb
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class CLI
|
|
3
|
-
class Verb
|
|
4
|
-
class Build < Runner::Base
|
|
5
|
-
self.spec = Textus::Write::Build.contract
|
|
6
|
-
|
|
7
|
-
option :prefix, "--prefix=K"
|
|
8
|
-
|
|
9
|
-
def invoke(store)
|
|
10
|
-
emit(store.as(resolved_role(store)).build(prefix: prefix))
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|