textus 0.51.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 +37 -0
- data/README.md +74 -66
- data/SPEC.md +100 -223
- data/docs/architecture/README.md +91 -80
- data/docs/reference/conventions.md +8 -8
- 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/background/retention/apply.rb +50 -0
- data/lib/textus/background/worker.rb +67 -0
- data/lib/textus/boot.rb +59 -49
- 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 +13 -13
- 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 +2 -2
- 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/sentinels.rb +2 -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 +19 -11
- 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 +40 -36
- data/lib/textus/init/templates/orientation_reducer.rb +15 -11
- data/lib/textus/init.rb +93 -76
- data/lib/textus/key/path.rb +9 -2
- data/lib/textus/layout.rb +21 -0
- data/lib/textus/manifest/capabilities.rb +1 -1
- data/lib/textus/manifest/data.rb +30 -15
- 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 +4 -4
- data/lib/textus/{domain → manifest}/policy/source.rb +27 -28
- 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 +18 -12
- data/lib/textus/manifest/schema/validator.rb +27 -25
- data/lib/textus/manifest/schema/vocabulary.rb +3 -3
- data/lib/textus/manifest/schema.rb +2 -2
- data/lib/textus/manifest.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/handler.rb +3 -3
- data/lib/textus/{produce → pipeline}/acquire/intake.rb +23 -21
- 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 +17 -63
- 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/ports/build_lock.rb +1 -1
- data/lib/textus/ports/queue/job.rb +65 -0
- data/lib/textus/ports/queue.rb +130 -0
- 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/role.rb +1 -1
- 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 -4
- 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 +133 -107
- 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.rb +0 -116
- data/lib/textus/cli.rb +0 -138
- data/lib/textus/dispatcher.rb +0 -52
- data/lib/textus/doctor/check/handler_allowlist.rb +0 -34
- data/lib/textus/domain/action.rb +0 -9
- 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/maintenance/key_delete_prefix.rb +0 -48
- data/lib/textus/maintenance/key_mv_prefix.rb +0 -68
- data/lib/textus/maintenance/reconcile.rb +0 -160
- data/lib/textus/maintenance/rule_lint.rb +0 -66
- 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 -69
- 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/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/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/read/deps.rb
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Read
|
|
3
|
-
class Deps
|
|
4
|
-
extend Textus::Contract::DSL
|
|
5
|
-
|
|
6
|
-
verb :deps
|
|
7
|
-
summary "List the keys a derived entry depends on (its projection/external sources)."
|
|
8
|
-
surfaces :cli, :mcp
|
|
9
|
-
arg :key, String, required: true, positional: true,
|
|
10
|
-
description: "dotted key of the derived entry whose source keys you want"
|
|
11
|
-
|
|
12
|
-
def initialize(container:, call: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
13
|
-
@manifest = container.manifest
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def call(key)
|
|
17
|
-
{ "key" => key, "deps" => sources_for(key) }
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
private
|
|
21
|
-
|
|
22
|
-
def sources_for(key)
|
|
23
|
-
entry = @manifest.data.entries.find { |e| e.key == key }
|
|
24
|
-
return [] unless entry&.derived?
|
|
25
|
-
|
|
26
|
-
src = entry.source
|
|
27
|
-
result = if src.projection?
|
|
28
|
-
Array(src.select).compact
|
|
29
|
-
elsif src.external?
|
|
30
|
-
Array(src.sources).compact
|
|
31
|
-
else
|
|
32
|
-
[]
|
|
33
|
-
end
|
|
34
|
-
result.uniq
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
data/lib/textus/read/doctor.rb
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Read
|
|
3
|
-
# Dispatched use case for the `doctor` verb. The health-check report is
|
|
4
|
-
# built by the Textus::Doctor library module; this class is the uniform
|
|
5
|
-
# (container:, call:) entry point that Dispatcher::VERBS resolves to.
|
|
6
|
-
# The acting role is irrelevant to a read-only health check, so `call`
|
|
7
|
-
# is not consulted.
|
|
8
|
-
class Doctor
|
|
9
|
-
extend Textus::Contract::DSL
|
|
10
|
-
|
|
11
|
-
verb :doctor
|
|
12
|
-
summary "Run health checks on the textus store and report any issues."
|
|
13
|
-
surfaces :cli
|
|
14
|
-
cli "doctor"
|
|
15
|
-
arg :checks, Array, required: false, description: "subset of check names to run (default: all)"
|
|
16
|
-
|
|
17
|
-
def initialize(container:, call:)
|
|
18
|
-
@container = container
|
|
19
|
-
@call = call
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def call(checks: nil)
|
|
23
|
-
Textus::Doctor.build(container: @container, checks: checks)
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
require "time"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Read
|
|
5
|
-
# Per-entry staleness scan (ADR 0079, 0085, 0093). Walks every entry declared
|
|
6
|
-
# in the manifest and reports a staleness verdict sourced from the two new
|
|
7
|
-
# policy slots (ADR 0093):
|
|
8
|
-
# - intake entries: `entry.source.ttl_seconds` is the re-pull cadence;
|
|
9
|
-
# basis = `_meta.last_fetched_at` (else file mtime). Past ttl ⇒ :expired.
|
|
10
|
-
# - entries matched by a `retention:` rule: `retention.ttl_seconds` is the
|
|
11
|
-
# GC age; basis = file mtime. Past ttl ⇒ :expired (:action = drop/archive).
|
|
12
|
-
# Intake cadence wins when both apply (freshness is content currency; GC dueness
|
|
13
|
-
# shows via `reconcile --dry-run`).
|
|
14
|
-
# Status is one of :fresh, :expired, or :no_policy; the row also carries
|
|
15
|
-
# :action (:refresh for intake, :drop/:archive for retention).
|
|
16
|
-
#
|
|
17
|
-
# ADR 0085 removed the public `freshness` verb: there is no `:cli`/`:mcp`
|
|
18
|
-
# surface. This is now a Ruby-only internal scan consumed by `pulse` (which
|
|
19
|
-
# derives `stale` + `next_due_at` from it) and the hook `Context`. Humans drill
|
|
20
|
-
# into per-entry staleness detail via `get` (last_fetched_at) + `rule_explain`
|
|
21
|
-
# (the ttl / action policy) instead of a dedicated verb.
|
|
22
|
-
class Freshness
|
|
23
|
-
extend Textus::Contract::DSL
|
|
24
|
-
|
|
25
|
-
verb :freshness
|
|
26
|
-
summary "Internal per-entry lifecycle scan (status, age, ttl, action); backs pulse + hook context. No public surface (ADR 0085)."
|
|
27
|
-
arg :prefix, String, required: false, description: "filter to keys with this prefix"
|
|
28
|
-
arg :zone, String, required: false, description: "filter to entries in this zone"
|
|
29
|
-
|
|
30
|
-
def initialize(container:, call:)
|
|
31
|
-
@container = container
|
|
32
|
-
@call = call
|
|
33
|
-
@manifest = container.manifest
|
|
34
|
-
@file_store = container.file_store
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# Returns the soonest `next_due_at` across all entries with a fetch
|
|
38
|
-
# policy, as an ISO-8601 string, or nil if none.
|
|
39
|
-
def soonest_due(prefix: nil, zone: nil)
|
|
40
|
-
times = call(prefix: prefix, zone: zone)
|
|
41
|
-
.map { |r| r[:next_due_at] }
|
|
42
|
-
.compact
|
|
43
|
-
.map { |t| Time.parse(t) }
|
|
44
|
-
return nil if times.empty?
|
|
45
|
-
|
|
46
|
-
times.min.utc.iso8601
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def call(prefix: nil, zone: nil)
|
|
50
|
-
rows = []
|
|
51
|
-
@manifest.data.entries.each do |mentry|
|
|
52
|
-
next if prefix && !mentry.key.start_with?(prefix)
|
|
53
|
-
next if zone && mentry.zone != zone
|
|
54
|
-
|
|
55
|
-
rows << row_for(mentry)
|
|
56
|
-
end
|
|
57
|
-
rows
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
private
|
|
61
|
-
|
|
62
|
-
def row_for(mentry)
|
|
63
|
-
envelope = safe_get(mentry.key)
|
|
64
|
-
last = envelope&.meta&.dig("last_fetched_at")
|
|
65
|
-
ttl, action = policy_for(mentry)
|
|
66
|
-
return base_row(mentry, last).merge(status: :no_policy) if ttl.nil?
|
|
67
|
-
|
|
68
|
-
basis = basis_for(mentry)
|
|
69
|
-
expired = expired?(mentry, basis, ttl)
|
|
70
|
-
base_row(mentry, last).merge(
|
|
71
|
-
ttl_seconds: ttl,
|
|
72
|
-
action: action,
|
|
73
|
-
status: expired ? :expired : :fresh,
|
|
74
|
-
next_due_at: basis.nil? ? nil : (basis + ttl).utc.iso8601,
|
|
75
|
-
)
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# ADR 0093: staleness comes from the intake re-pull cadence (source.ttl)
|
|
79
|
-
# or a retention GC rule (retention.ttl). Intake cadence wins when an entry
|
|
80
|
-
# has both (freshness is about content currency; GC dueness still shows via
|
|
81
|
-
# `reconcile --dry-run`). Returns [ttl_seconds, action] or [nil, nil].
|
|
82
|
-
def policy_for(mentry)
|
|
83
|
-
if mentry.intake?
|
|
84
|
-
ttl = mentry.source.ttl_seconds
|
|
85
|
-
return [ttl, :refresh] unless ttl.nil?
|
|
86
|
-
end
|
|
87
|
-
ret = @manifest.rules.for(mentry.key).retention
|
|
88
|
-
return [ret.ttl_seconds, ret.action] unless ret.nil?
|
|
89
|
-
|
|
90
|
-
[nil, nil]
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
# Intake currency basis comes from the evaluator (single definition);
|
|
94
|
-
# retention dueness is keyed off file mtime.
|
|
95
|
-
def basis_for(mentry)
|
|
96
|
-
return evaluator.intake_basis(mentry) if mentry.intake? && mentry.source.ttl_seconds
|
|
97
|
-
|
|
98
|
-
mtime_for(mentry.key)
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def expired?(mentry, basis, ttl)
|
|
102
|
-
if mentry.intake? && mentry.source.ttl_seconds
|
|
103
|
-
evaluator.verdict(mentry).stale
|
|
104
|
-
else
|
|
105
|
-
# Preserve pre-0099 pulse semantics: a never-recorded retention entry
|
|
106
|
-
# (no file => nil basis) is past due. Retention::Sweep.expired? alone
|
|
107
|
-
# returns false on nil mtime (it runs post-exists? in the sweep).
|
|
108
|
-
basis.nil? || Textus::Domain::Retention::Sweep.expired?(ttl_seconds: ttl, mtime: basis, now: @call.now)
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def evaluator
|
|
113
|
-
@evaluator ||= Textus::Domain::Freshness::Evaluator.new(
|
|
114
|
-
manifest: @manifest, file_stat: Textus::Ports::Storage::FileStat.new, clock: @call,
|
|
115
|
-
)
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def mtime_for(key)
|
|
119
|
-
path = @manifest.resolver.resolve(key).path
|
|
120
|
-
@file_store.exists?(path) ? Textus::Ports::Storage::FileStat.new.mtime(path) : nil
|
|
121
|
-
rescue Textus::Error
|
|
122
|
-
nil
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def base_row(mentry, last)
|
|
126
|
-
{
|
|
127
|
-
key: mentry.key,
|
|
128
|
-
zone: mentry.zone,
|
|
129
|
-
last_fetched_at: last,
|
|
130
|
-
age_seconds: last ? (@call.now - Time.parse(last)).to_i : nil,
|
|
131
|
-
}
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
# Returns the raw envelope or nil. Nested entries (mentry.key is a
|
|
135
|
-
# prefix, not a leaf) and missing files both resolve to nil.
|
|
136
|
-
def safe_get(key)
|
|
137
|
-
res = @manifest.resolver.resolve(key)
|
|
138
|
-
return nil unless @file_store.exists?(res.path)
|
|
139
|
-
|
|
140
|
-
raw = @file_store.read(res.path)
|
|
141
|
-
parsed = Entry.for_format(res.entry.format).parse(raw, path: res.path)
|
|
142
|
-
Textus::Envelope.build(
|
|
143
|
-
key: key, mentry: res.entry, path: res.path,
|
|
144
|
-
meta: parsed["_meta"], body: parsed["body"],
|
|
145
|
-
etag: Etag.for_bytes(raw), content: parsed["content"]
|
|
146
|
-
)
|
|
147
|
-
rescue Textus::Error
|
|
148
|
-
nil
|
|
149
|
-
end
|
|
150
|
-
end
|
|
151
|
-
end
|
|
152
|
-
end
|
data/lib/textus/read/get.rb
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Read
|
|
3
|
-
# The one read path — a pure read (ADR 0089, 0093): the on-disk envelope
|
|
4
|
-
# annotated with a freshness annotation. It NEVER mutates and NEVER ingests.
|
|
5
|
-
# Quarantine freshness is system-pushed via `reconcile` (scheduled sweep) and
|
|
6
|
-
# `hook run` (event push). Lifecycle is removed from the get path (ADR 0093):
|
|
7
|
-
# intake cadence lives in `source.ttl`; GC lives in `retention:` rules; both
|
|
8
|
-
# are evaluated exclusively by the `reconcile` sweep, not by a read.
|
|
9
|
-
class Get
|
|
10
|
-
extend Textus::Contract::DSL
|
|
11
|
-
|
|
12
|
-
verb :get
|
|
13
|
-
summary "Read one entry — a pure on-disk read annotated with a freshness " \
|
|
14
|
-
"verdict; never ingests (quarantine freshness is reconcile + hook " \
|
|
15
|
-
"only, ADR 0089). Returns the envelope (uid, etag, _meta, body, " \
|
|
16
|
-
"freshness)."
|
|
17
|
-
surfaces :cli, :mcp
|
|
18
|
-
arg :key, String, required: true, positional: true,
|
|
19
|
-
description: "dotted entry key to read, e.g. 'knowledge.project'"
|
|
20
|
-
view { |v, _i| v.to_h_for_wire }
|
|
21
|
-
|
|
22
|
-
def initialize(container:, call:, file_stat: Textus::Ports::Storage::FileStat.new)
|
|
23
|
-
@container = container
|
|
24
|
-
@call = call
|
|
25
|
-
@manifest = container.manifest
|
|
26
|
-
@file_store = container.file_store
|
|
27
|
-
@file_stat = file_stat
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def call(key)
|
|
31
|
-
annotated_envelope(key)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# Strict variant: raises UnknownKey when the entry is missing.
|
|
35
|
-
# Used by consumers (e.g. uid, Validator) that distinguish absence.
|
|
36
|
-
def get(key)
|
|
37
|
-
call(key) ||
|
|
38
|
-
raise(UnknownKey.new(key, suggestions: @manifest.resolver.suggestions_for(key)))
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
private
|
|
42
|
-
|
|
43
|
-
def annotated_envelope(key)
|
|
44
|
-
envelope = read_raw_envelope(key)
|
|
45
|
-
return nil if envelope.nil?
|
|
46
|
-
|
|
47
|
-
entry = @manifest.resolver.resolve(key).entry
|
|
48
|
-
envelope.with(freshness: evaluator.verdict(entry))
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def evaluator
|
|
52
|
-
@evaluator ||= Textus::Domain::Freshness::Evaluator.new(
|
|
53
|
-
manifest: @manifest, file_stat: @file_stat, clock: @call,
|
|
54
|
-
)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def read_raw_envelope(key)
|
|
58
|
-
res = @manifest.resolver.resolve(key)
|
|
59
|
-
mentry = res.entry
|
|
60
|
-
path = res.path
|
|
61
|
-
return nil unless @file_store.exists?(path)
|
|
62
|
-
|
|
63
|
-
raw = @file_store.read(path)
|
|
64
|
-
parsed = Entry.for_format(mentry.format).parse(raw, path: path)
|
|
65
|
-
Textus::Envelope.build(
|
|
66
|
-
key: key, mentry: mentry, path: path,
|
|
67
|
-
meta: parsed["_meta"], body: parsed["body"],
|
|
68
|
-
etag: Etag.for_bytes(raw), content: parsed["content"]
|
|
69
|
-
)
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
end
|
data/lib/textus/read/list.rb
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Read
|
|
3
|
-
class List
|
|
4
|
-
extend Textus::Contract::DSL
|
|
5
|
-
|
|
6
|
-
verb :list
|
|
7
|
-
summary "List keys filtered by zone and/or prefix."
|
|
8
|
-
surfaces :cli, :mcp
|
|
9
|
-
arg :prefix, String, description: "restrict to keys starting with this dotted prefix, e.g. 'knowledge.runbooks'"
|
|
10
|
-
arg :zone, String, description: "restrict to one zone by name (see `boot` zones); combine with prefix to narrow further"
|
|
11
|
-
view(:cli) { |rows| { "entries" => rows } }
|
|
12
|
-
|
|
13
|
-
def initialize(container:, call: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
14
|
-
@manifest = container.manifest
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def call(prefix: nil, zone: nil)
|
|
18
|
-
rows = @manifest.resolver.enumerate(prefix: prefix)
|
|
19
|
-
rows = rows.select { |r| r[:manifest_entry].zone == zone } if zone
|
|
20
|
-
rows.map { |row| { "key" => row[:key], "zone" => row[:manifest_entry].zone, "path" => row[:path] } }
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Read
|
|
3
|
-
class Published
|
|
4
|
-
extend Textus::Contract::DSL
|
|
5
|
-
|
|
6
|
-
verb :published
|
|
7
|
-
summary "List all entries that declare a publish_to target."
|
|
8
|
-
surfaces :cli
|
|
9
|
-
cli "published"
|
|
10
|
-
|
|
11
|
-
def initialize(container:, call: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
12
|
-
@manifest = container.manifest
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def call
|
|
16
|
-
@manifest.data.entries.reject { |e| e.publish_to.empty? }.map do |e|
|
|
17
|
-
{ "key" => e.key, "publish_to" => e.publish_to }
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
data/lib/textus/read/pulse.rb
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
require "time"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Read
|
|
5
|
-
# Aggregator over audit + freshness + review + doctor. One round-trip
|
|
6
|
-
# for an agent's per-turn heartbeat. All component reads are existing
|
|
7
|
-
# APIs; pulse is sugar with a stable envelope shape and a monotonic
|
|
8
|
-
# cursor (seq).
|
|
9
|
-
class Pulse
|
|
10
|
-
extend Textus::Contract::DSL
|
|
11
|
-
|
|
12
|
-
verb :pulse
|
|
13
|
-
summary "Delta since cursor — changed entries, stale, pending proposals, doctor summary."
|
|
14
|
-
surfaces :cli, :mcp
|
|
15
|
-
around :cursor
|
|
16
|
-
arg :since, Integer, session_default: :cursor, description: "audit seq to diff from; defaults to the session cursor"
|
|
17
|
-
|
|
18
|
-
def initialize(container:, call:)
|
|
19
|
-
@container = container
|
|
20
|
-
@call = call
|
|
21
|
-
@manifest = container.manifest
|
|
22
|
-
@file_store = container.file_store
|
|
23
|
-
@audit_log = container.audit_log
|
|
24
|
-
@root = container.root
|
|
25
|
-
@events = container.events
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def call(since: 0)
|
|
29
|
-
freshness_rows = freshness.call
|
|
30
|
-
{
|
|
31
|
-
"cursor" => @audit_log.latest_seq,
|
|
32
|
-
"changed" => audit_changes_since(since),
|
|
33
|
-
"stale" => freshness_rows.select { |r| r[:status] == :expired }.map { |r| r[:key] },
|
|
34
|
-
"pending_review" => review_keys,
|
|
35
|
-
"doctor" => doctor_summary,
|
|
36
|
-
"contract_etag" => contract_etag,
|
|
37
|
-
"next_due_at" => soonest_due(freshness_rows),
|
|
38
|
-
"hook_errors" => hook_errors_since(since),
|
|
39
|
-
}
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
private
|
|
43
|
-
|
|
44
|
-
def audit_changes_since(seq)
|
|
45
|
-
Read::Audit.new(container: @container).call(seq_since: seq)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def freshness
|
|
49
|
-
@freshness ||= Read::Freshness.new(container: @container, call: @call)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def soonest_due(rows)
|
|
53
|
-
times = rows.map { |r| r[:next_due_at] }.compact.map { |t| Time.parse(t) }
|
|
54
|
-
return nil if times.empty?
|
|
55
|
-
|
|
56
|
-
times.min.utc.iso8601
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def review_keys
|
|
60
|
-
# The single queue zone (kind: queue; schema guarantees ≤1), derived
|
|
61
|
-
# from the manifest rather than a hardcoded zone name (ADR 0034 / D1).
|
|
62
|
-
queue = @manifest.policy.queue_zone
|
|
63
|
-
return [] unless queue
|
|
64
|
-
|
|
65
|
-
rows = Read::List.new(container: @container).call(zone: queue)
|
|
66
|
-
rows.map { |r| r.is_a?(Hash) ? (r["key"] || r[:key]) : r }
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def doctor_summary
|
|
70
|
-
result = Textus::Doctor.build(container: @container)
|
|
71
|
-
issues = result["issues"] || []
|
|
72
|
-
{
|
|
73
|
-
"ok" => result["ok"],
|
|
74
|
-
"warn" => issues.count { |i| i["level"] == "warning" },
|
|
75
|
-
"fail" => issues.count { |i| i["level"] == "error" },
|
|
76
|
-
}
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def contract_etag
|
|
80
|
-
Textus::Etag.for_contract(@root)
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def hook_errors_since(seq)
|
|
84
|
-
@events.error_log.since(seq).map do |r|
|
|
85
|
-
{
|
|
86
|
-
"seq" => r[:seq],
|
|
87
|
-
"event" => r[:event].to_s,
|
|
88
|
-
"hook" => r[:hook].to_s,
|
|
89
|
-
"key" => r[:key],
|
|
90
|
-
"error_class" => r[:error_class],
|
|
91
|
-
"error_message" => r[:error_message],
|
|
92
|
-
"at" => r[:at],
|
|
93
|
-
}
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
end
|
data/lib/textus/read/rdeps.rb
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Read
|
|
3
|
-
class Rdeps
|
|
4
|
-
extend Textus::Contract::DSL
|
|
5
|
-
|
|
6
|
-
verb :rdeps
|
|
7
|
-
summary "List the derived entries that depend on a key (reverse deps / impact set)."
|
|
8
|
-
surfaces :cli, :mcp
|
|
9
|
-
arg :key, String, required: true, positional: true,
|
|
10
|
-
description: "dotted key whose dependents (what would be stranded if it moved) you want"
|
|
11
|
-
|
|
12
|
-
def initialize(container:, call: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
13
|
-
@manifest = container.manifest
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def call(key)
|
|
17
|
-
{ "key" => key, "rdeps" => dependents_of(key) }
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
private
|
|
21
|
-
|
|
22
|
-
def dependents_of(key)
|
|
23
|
-
@manifest.data.entries.each_with_object([]) do |e, acc|
|
|
24
|
-
next unless e.derived?
|
|
25
|
-
|
|
26
|
-
src = e.source
|
|
27
|
-
sources = if src.projection?
|
|
28
|
-
Array(src.select).compact
|
|
29
|
-
elsif src.external?
|
|
30
|
-
Array(src.sources).compact
|
|
31
|
-
else
|
|
32
|
-
[]
|
|
33
|
-
end
|
|
34
|
-
acc << e.key if sources.any? { |s| s == key || key.start_with?("#{s}.") }
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Read
|
|
3
|
-
# Effective rules for a key, at two depths (ADR 0059). Lean by default —
|
|
4
|
-
# `{ lifecycle, guard }`, the agent-cheap read that was the `rules` verb. With
|
|
5
|
-
# `detail: true` it returns the verbose explanation — every matching policy
|
|
6
|
-
# block plus the per-transition guard predicate names — that was
|
|
7
|
-
# `policy_explain`. One verb, one name across CLI/MCP/method; the audience
|
|
8
|
-
# split is a parameter, not two tools.
|
|
9
|
-
class RuleExplain
|
|
10
|
-
extend Textus::Contract::DSL
|
|
11
|
-
|
|
12
|
-
verb :rule_explain
|
|
13
|
-
summary "Effective rules for a key. Lean {lifecycle, guard} by default; detail: true adds matched blocks + guard predicates."
|
|
14
|
-
surfaces :cli, :mcp
|
|
15
|
-
cli "rule explain"
|
|
16
|
-
arg :key, String, required: true, positional: true,
|
|
17
|
-
description: "dotted key whose effective rules you want (lifecycle ttl/action, write guard, ...)"
|
|
18
|
-
arg :detail, :boolean,
|
|
19
|
-
description: "defaults false: lean {lifecycle, guard}. detail: true adds matched blocks + guard predicates per transition."
|
|
20
|
-
view(:cli) { |r| { "verb" => "rule_explain" }.merge(r.transform_keys(&:to_s)) }
|
|
21
|
-
|
|
22
|
-
def initialize(container:, call: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
23
|
-
@manifest = container.manifest
|
|
24
|
-
@schemas = container.schemas
|
|
25
|
-
end
|
|
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
|
-
|
|
37
|
-
def call(key, detail: false)
|
|
38
|
-
detail ? explain(key) : effective(key)
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
private
|
|
42
|
-
|
|
43
|
-
# Lean: the effective winners only (formerly Read::Rules / the `rules` verb).
|
|
44
|
-
def effective(key)
|
|
45
|
-
set = @manifest.rules.for(key)
|
|
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
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# Verbose: every matching block, per-slot effective value, and the
|
|
60
|
-
# effective guard predicate names for each write transition (formerly
|
|
61
|
-
# Read::PolicyExplain, ADR 0031).
|
|
62
|
-
def explain(key)
|
|
63
|
-
matching = @manifest.rules.explain(key)
|
|
64
|
-
winners = @manifest.rules.for(key)
|
|
65
|
-
factory = Textus::Domain::Policy::GuardFactory.new(manifest: @manifest, schemas: @schemas)
|
|
66
|
-
|
|
67
|
-
{
|
|
68
|
-
key: key,
|
|
69
|
-
matched_blocks: matching.map do |b|
|
|
70
|
-
{ match: b.match }.merge(DETAIL_FIELDS.to_h { |f| [f, !b.public_send(f).nil?] })
|
|
71
|
-
end,
|
|
72
|
-
effective: EFFECTIVE_FIELDS.to_h { |f| [f, effective_value(f, winners.public_send(f))] },
|
|
73
|
-
guards: Textus::Domain::Policy::BaseGuards::BASE.keys.to_h do |transition|
|
|
74
|
-
[transition, factory.for(transition, key).predicates.map(&:name)]
|
|
75
|
-
end,
|
|
76
|
-
}
|
|
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
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
end
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Read
|
|
3
|
-
# Enumerate every declared rule block in the manifest, in order. This is
|
|
4
|
-
# the whole-manifest view; `rule_explain` is the for-key view. Extracted
|
|
5
|
-
# from the CLI verb so the rule family is fully use-case-backed (ADR 0059);
|
|
6
|
-
# CLI-only (no MCP contract) — an agent reasons per-key via rule_explain.
|
|
7
|
-
class RuleList
|
|
8
|
-
extend Textus::Contract::DSL
|
|
9
|
-
|
|
10
|
-
verb :rule_list
|
|
11
|
-
summary "List every rule block in the manifest."
|
|
12
|
-
surfaces :cli
|
|
13
|
-
cli "rule list"
|
|
14
|
-
view(:cli) { |policies| { "verb" => "rule_list", "policies" => policies } }
|
|
15
|
-
|
|
16
|
-
def initialize(container:, call: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
17
|
-
@manifest = container.manifest
|
|
18
|
-
end
|
|
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
|
-
|
|
24
|
-
def call
|
|
25
|
-
@manifest.rules.blocks.map do |b|
|
|
26
|
-
row = { "match" => b.match }
|
|
27
|
-
LIST_FIELDS.each do |field|
|
|
28
|
-
value = b.public_send(field)
|
|
29
|
-
row[field.to_s] = serialize(field, value) unless value.nil?
|
|
30
|
-
end
|
|
31
|
-
row
|
|
32
|
-
end
|
|
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
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Read
|
|
3
|
-
class SchemaEnvelope
|
|
4
|
-
extend Textus::Contract::DSL
|
|
5
|
-
|
|
6
|
-
verb :schema_show
|
|
7
|
-
summary "Return the schema (field shape) for an entry's family, by key."
|
|
8
|
-
surfaces :cli, :mcp
|
|
9
|
-
cli "schema show"
|
|
10
|
-
arg :key, String, required: true, positional: true,
|
|
11
|
-
description: "any key in the family whose schema you want; returns required/optional fields and their types"
|
|
12
|
-
|
|
13
|
-
def initialize(container:, call: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
14
|
-
@manifest = container.manifest
|
|
15
|
-
@schemas = container.schemas
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def call(key)
|
|
19
|
-
mentry = @manifest.resolver.resolve(key).entry
|
|
20
|
-
schema = @schemas.fetch_or_nil(mentry.schema)
|
|
21
|
-
{ "protocol" => PROTOCOL, "key" => key, "schema_ref" => mentry.schema, "schema" => schema&.to_h }
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
data/lib/textus/read/uid.rb
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Read
|
|
3
|
-
class Uid
|
|
4
|
-
extend Textus::Contract::DSL
|
|
5
|
-
|
|
6
|
-
verb :uid
|
|
7
|
-
summary "Return the stable UID of an entry without reading its body."
|
|
8
|
-
surfaces :cli
|
|
9
|
-
cli "key uid"
|
|
10
|
-
arg :key, String, required: true, positional: true, description: "entry key"
|
|
11
|
-
view(:cli) { |uid, inputs| { "key" => inputs[:key], "uid" => uid } }
|
|
12
|
-
|
|
13
|
-
def initialize(container:, call:)
|
|
14
|
-
@container = container
|
|
15
|
-
@call = call
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def call(key)
|
|
19
|
-
get.get(key).uid
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
private
|
|
23
|
-
|
|
24
|
-
def get
|
|
25
|
-
@get ||= Get.new(container: @container, call: @call)
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|