textus 0.14.4 → 0.18.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/ARCHITECTURE.md +14 -14
- data/CHANGELOG.md +378 -0
- data/README.md +13 -9
- data/SPEC.md +13 -10
- data/docs/conventions.md +11 -0
- data/lib/textus/application/context.rb +25 -7
- data/lib/textus/application/reads/audit.rb +1 -1
- data/lib/textus/application/reads/blame.rb +3 -1
- data/lib/textus/application/reads/deps.rb +1 -1
- data/lib/textus/application/reads/freshness.rb +12 -3
- data/lib/textus/application/reads/get.rb +38 -33
- data/lib/textus/application/reads/get_or_refresh.rb +51 -0
- data/lib/textus/application/reads/list.rb +3 -1
- data/lib/textus/application/reads/published.rb +1 -1
- data/lib/textus/application/reads/rdeps.rb +1 -1
- data/lib/textus/application/reads/schema_envelope.rb +3 -1
- data/lib/textus/application/reads/stale.rb +1 -1
- data/lib/textus/application/reads/uid.rb +1 -1
- data/lib/textus/application/reads/validate_all.rb +6 -1
- data/lib/textus/application/reads/validator.rb +84 -0
- data/lib/textus/application/reads/where.rb +4 -1
- data/lib/textus/application/refresh/all.rb +8 -1
- data/lib/textus/application/refresh/orchestrator.rb +11 -3
- data/lib/textus/application/refresh/worker.rb +27 -20
- data/lib/textus/application/writes/accept.rb +12 -12
- data/lib/textus/application/writes/build.rb +3 -4
- data/lib/textus/application/writes/delete.rb +10 -15
- data/lib/textus/application/writes/envelope_io.rb +106 -0
- data/lib/textus/application/writes/mv.rb +25 -27
- data/lib/textus/application/writes/publish.rb +8 -9
- data/lib/textus/application/writes/put.rb +12 -16
- data/lib/textus/application/writes/reject.rb +10 -10
- data/lib/textus/builder/pipeline.rb +8 -1
- data/lib/textus/cli/group/hook.rb +1 -3
- data/lib/textus/cli/group/key.rb +1 -4
- data/lib/textus/cli/group/refresh.rb +1 -2
- data/lib/textus/cli/group/rule.rb +1 -3
- data/lib/textus/cli/group/schema.rb +1 -5
- data/lib/textus/cli/group.rb +12 -16
- data/lib/textus/cli/verb/accept.rb +3 -1
- data/lib/textus/cli/verb/audit.rb +3 -1
- data/lib/textus/cli/verb/blame.rb +3 -1
- data/lib/textus/cli/verb/build.rb +4 -2
- data/lib/textus/cli/verb/delete.rb +3 -1
- data/lib/textus/cli/verb/deps.rb +3 -1
- data/lib/textus/cli/verb/doctor.rb +2 -0
- data/lib/textus/cli/verb/freshness.rb +3 -1
- data/lib/textus/cli/verb/get.rb +3 -1
- data/lib/textus/cli/verb/hook_run.rb +3 -0
- data/lib/textus/cli/verb/hooks.rb +3 -0
- data/lib/textus/cli/verb/init.rb +2 -0
- data/lib/textus/cli/verb/intro.rb +2 -0
- data/lib/textus/cli/verb/key_normalize.rb +3 -0
- data/lib/textus/cli/verb/list.rb +3 -1
- data/lib/textus/cli/verb/mv.rb +4 -1
- data/lib/textus/cli/verb/published.rb +3 -1
- data/lib/textus/cli/verb/put.rb +3 -1
- data/lib/textus/cli/verb/rdeps.rb +3 -1
- data/lib/textus/cli/verb/refresh.rb +1 -1
- data/lib/textus/cli/verb/refresh_stale.rb +4 -1
- data/lib/textus/cli/verb/reject.rb +3 -1
- data/lib/textus/cli/verb/rule_explain.rb +4 -1
- data/lib/textus/cli/verb/rule_list.rb +3 -0
- data/lib/textus/cli/verb/schema.rb +4 -1
- data/lib/textus/cli/verb/schema_diff.rb +3 -0
- data/lib/textus/cli/verb/schema_init.rb +3 -0
- data/lib/textus/cli/verb/schema_migrate.rb +3 -0
- data/lib/textus/cli/verb/uid.rb +4 -1
- data/lib/textus/cli/verb/where.rb +3 -1
- data/lib/textus/cli/verb.rb +30 -0
- data/lib/textus/cli.rb +40 -35
- data/lib/textus/doctor/check/audit_log.rb +1 -1
- data/lib/textus/doctor/check/hooks.rb +3 -1
- data/lib/textus/doctor/check/intake_registration.rb +3 -3
- data/lib/textus/doctor/check/schema_violations.rb +1 -1
- data/lib/textus/doctor/check/sentinels.rb +2 -2
- data/lib/textus/domain/freshness/evaluator.rb +1 -1
- data/lib/textus/domain/freshness/policy.rb +1 -1
- data/lib/textus/domain/freshness/verdict.rb +1 -1
- data/lib/textus/domain/freshness.rb +40 -0
- data/lib/textus/domain/policy/predicates/schema_valid.rb +2 -2
- data/lib/textus/{store → domain}/sentinel.rb +1 -1
- data/lib/textus/{store → domain}/staleness/generator_check.rb +1 -1
- data/lib/textus/{store → domain}/staleness/intake_check.rb +1 -1
- data/lib/textus/{store → domain}/staleness.rb +1 -1
- data/lib/textus/entry/json.rb +1 -1
- data/lib/textus/entry/markdown.rb +1 -1
- data/lib/textus/entry/yaml.rb +1 -1
- data/lib/textus/envelope.rb +7 -3
- data/lib/textus/errors.rb +19 -0
- data/lib/textus/hooks/builtin.rb +6 -6
- data/lib/textus/hooks/dispatcher.rb +17 -9
- data/lib/textus/hooks/loader.rb +20 -17
- data/lib/textus/hooks/registry.rb +4 -0
- data/lib/textus/{store → infra}/audit_log.rb +1 -1
- data/lib/textus/infra/audit_subscriber.rb +43 -0
- data/lib/textus/infra/publisher.rb +3 -3
- data/lib/textus/infra/storage/file_store.rb +26 -0
- data/lib/textus/init.rb +11 -9
- data/lib/textus/manifest/resolution.rb +5 -0
- data/lib/textus/manifest.rb +4 -3
- data/lib/textus/migrate_keys.rb +1 -1
- data/lib/textus/operations.rb +84 -17
- data/lib/textus/projection.rb +16 -11
- data/lib/textus/refresh.rb +1 -1
- data/lib/textus/schema/tools.rb +5 -5
- data/lib/textus/schemas.rb +46 -0
- data/lib/textus/store.rb +12 -49
- data/lib/textus/uid.rb +18 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus.rb +17 -1
- metadata +15 -13
- data/lib/textus/hooks/dsl.rb +0 -11
- data/lib/textus/operations/reads.rb +0 -39
- data/lib/textus/operations/refresh.rb +0 -27
- data/lib/textus/operations/writes.rb +0 -21
- data/lib/textus/store/reader.rb +0 -69
- data/lib/textus/store/validator.rb +0 -82
- data/lib/textus/store/writer.rb +0 -102
data/lib/textus/cli/verb/list.rb
CHANGED
|
@@ -2,11 +2,13 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class List < Verb
|
|
5
|
+
command_name "list"
|
|
6
|
+
|
|
5
7
|
option :prefix, "--prefix=KEY"
|
|
6
8
|
option :zone, "--zone=Z"
|
|
7
9
|
|
|
8
10
|
def call(store)
|
|
9
|
-
emit({ "entries" => operations_for(store).
|
|
11
|
+
emit({ "entries" => operations_for(store).list(prefix: prefix, zone: zone) })
|
|
10
12
|
end
|
|
11
13
|
end
|
|
12
14
|
end
|
data/lib/textus/cli/verb/mv.rb
CHANGED
|
@@ -2,13 +2,16 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class Mv < Verb
|
|
5
|
+
command_name "mv"
|
|
6
|
+
parent_group Group::Key
|
|
7
|
+
|
|
5
8
|
option :as_flag, "--as=ROLE"
|
|
6
9
|
option :dry_run, "--dry-run"
|
|
7
10
|
|
|
8
11
|
def call(store)
|
|
9
12
|
old_key = positional.shift or raise UsageError.new("mv requires <old-key> <new-key>")
|
|
10
13
|
new_key = positional.shift or raise UsageError.new("mv requires <old-key> <new-key>")
|
|
11
|
-
emit(operations_for(store).
|
|
14
|
+
emit(operations_for(store).mv(old_key, new_key, dry_run: dry_run || false))
|
|
12
15
|
end
|
|
13
16
|
end
|
|
14
17
|
end
|
data/lib/textus/cli/verb/put.rb
CHANGED
|
@@ -2,6 +2,8 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class Put < Verb
|
|
5
|
+
command_name "put"
|
|
6
|
+
|
|
5
7
|
option :as_flag, "--as=ROLE"
|
|
6
8
|
option :use_stdin, "--stdin"
|
|
7
9
|
option :fetch_name, "--fetch=NAME"
|
|
@@ -43,7 +45,7 @@ module Textus
|
|
|
43
45
|
meta = payload["_meta"] || {}
|
|
44
46
|
body = payload["body"] || ""
|
|
45
47
|
if_etag = payload["if_etag"]
|
|
46
|
-
result = Textus::Operations.for(store, role: role).
|
|
48
|
+
result = Textus::Operations.for(store, role: role).put(key, meta: meta, body: body, if_etag: if_etag)
|
|
47
49
|
emit(result.to_h_for_wire)
|
|
48
50
|
end
|
|
49
51
|
end
|
|
@@ -2,9 +2,11 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class Rdeps < Verb
|
|
5
|
+
command_name "rdeps"
|
|
6
|
+
|
|
5
7
|
def call(store)
|
|
6
8
|
key = positional.shift or raise UsageError.new("rdeps requires a key")
|
|
7
|
-
emit({ "key" => key, "rdeps" => operations_for(store).
|
|
9
|
+
emit({ "key" => key, "rdeps" => operations_for(store).rdeps(key) })
|
|
8
10
|
end
|
|
9
11
|
end
|
|
10
12
|
end
|
|
@@ -2,6 +2,9 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class RefreshStale < Verb
|
|
5
|
+
command_name "stale"
|
|
6
|
+
parent_group Group::Refresh
|
|
7
|
+
|
|
5
8
|
option :prefix, "--prefix=KEY"
|
|
6
9
|
option :zone, "--zone=Z"
|
|
7
10
|
option :as_flag, "--as=ROLE"
|
|
@@ -10,7 +13,7 @@ module Textus
|
|
|
10
13
|
ctx = context_for(store)
|
|
11
14
|
result = Textus::Application::Refresh::All.call(ctx, prefix: prefix, zone: zone)
|
|
12
15
|
emit(result)
|
|
13
|
-
|
|
16
|
+
result["ok"] ? 0 : 1
|
|
14
17
|
end
|
|
15
18
|
end
|
|
16
19
|
end
|
|
@@ -2,11 +2,13 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class Reject < Verb
|
|
5
|
+
command_name "reject"
|
|
6
|
+
|
|
5
7
|
option :as_flag, "--as=ROLE"
|
|
6
8
|
|
|
7
9
|
def call(store)
|
|
8
10
|
key = positional.shift or raise UsageError.new("reject requires a key")
|
|
9
|
-
emit(operations_for(store).
|
|
11
|
+
emit(operations_for(store).reject(key))
|
|
10
12
|
end
|
|
11
13
|
end
|
|
12
14
|
end
|
|
@@ -2,9 +2,12 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class RuleExplain < Verb
|
|
5
|
+
command_name "explain"
|
|
6
|
+
parent_group Group::Rule
|
|
7
|
+
|
|
5
8
|
def call(store)
|
|
6
9
|
key = positional.shift or raise UsageError.new("policy explain requires a KEY")
|
|
7
|
-
result = operations_for(store).
|
|
10
|
+
result = operations_for(store).policy_explain(key: key)
|
|
8
11
|
emit({ "verb" => "policy_explain" }.merge(result.transform_keys(&:to_s)))
|
|
9
12
|
end
|
|
10
13
|
end
|
|
@@ -2,9 +2,12 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class Schema < Verb
|
|
5
|
+
command_name "show"
|
|
6
|
+
parent_group Group::Schema
|
|
7
|
+
|
|
5
8
|
def call(store)
|
|
6
9
|
key = positional.shift or raise UsageError.new("schema requires a key")
|
|
7
|
-
emit(operations_for(store).
|
|
10
|
+
emit(operations_for(store).schema_envelope(key))
|
|
8
11
|
end
|
|
9
12
|
end
|
|
10
13
|
end
|
data/lib/textus/cli/verb/uid.rb
CHANGED
|
@@ -2,9 +2,12 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class Uid < Verb
|
|
5
|
+
command_name "uid"
|
|
6
|
+
parent_group Group::Key
|
|
7
|
+
|
|
5
8
|
def call(store)
|
|
6
9
|
key = positional.shift or raise UsageError.new("uid requires a key")
|
|
7
|
-
emit({ "key" => key, "uid" => operations_for(store).
|
|
10
|
+
emit({ "key" => key, "uid" => operations_for(store).uid(key) })
|
|
8
11
|
end
|
|
9
12
|
end
|
|
10
13
|
end
|
|
@@ -2,9 +2,11 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class Where < Verb
|
|
5
|
+
command_name "where"
|
|
6
|
+
|
|
5
7
|
def call(store)
|
|
6
8
|
key = positional.shift or raise UsageError.new("where requires a key")
|
|
7
|
-
emit(operations_for(store).
|
|
9
|
+
emit(operations_for(store).where(key))
|
|
8
10
|
end
|
|
9
11
|
end
|
|
10
12
|
end
|
data/lib/textus/cli/verb.rb
CHANGED
|
@@ -22,9 +22,39 @@ module Textus
|
|
|
22
22
|
true
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
# Declarative CLI name. Reader returns the registered name (or nil
|
|
26
|
+
# for verbs that aren't directly invokable, like the abstract
|
|
27
|
+
# Verb/Group base classes). Writer registers it.
|
|
28
|
+
def command_name(name = nil)
|
|
29
|
+
if name.nil?
|
|
30
|
+
@command_name
|
|
31
|
+
else
|
|
32
|
+
@command_name = name.to_s
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Declares that this verb is a subcommand of `group_klass`. When
|
|
37
|
+
# set, the verb is NOT a top-level CLI verb — it's listed under
|
|
38
|
+
# the group's subcommands instead.
|
|
39
|
+
def parent_group(group_klass = nil)
|
|
40
|
+
if group_klass.nil?
|
|
41
|
+
@parent_group
|
|
42
|
+
else
|
|
43
|
+
@parent_group = group_klass
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
25
47
|
def inherited(subclass)
|
|
26
48
|
super
|
|
27
49
|
subclass.instance_variable_set(:@options, [])
|
|
50
|
+
subclass.instance_variable_set(:@command_name, nil)
|
|
51
|
+
subclass.instance_variable_set(:@parent_group, nil)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Recursive subclass enumeration. Ruby 3.1 ships Class#subclasses
|
|
55
|
+
# but not Class#descendants, so we expand it ourselves.
|
|
56
|
+
def descendants
|
|
57
|
+
subclasses.flat_map { |k| [k] + k.descendants }
|
|
28
58
|
end
|
|
29
59
|
end
|
|
30
60
|
|
data/lib/textus/cli.rb
CHANGED
|
@@ -3,32 +3,23 @@ require "optparse"
|
|
|
3
3
|
|
|
4
4
|
module Textus
|
|
5
5
|
class CLI
|
|
6
|
-
# verb
|
|
7
|
-
#
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"list" => Verb::List,
|
|
24
|
-
"published" => Verb::Published,
|
|
25
|
-
"put" => Verb::Put,
|
|
26
|
-
"rdeps" => Verb::Rdeps,
|
|
27
|
-
"refresh" => Group::Refresh,
|
|
28
|
-
"rule" => Group::Rule,
|
|
29
|
-
"schema" => Group::Schema,
|
|
30
|
-
"where" => Verb::Where,
|
|
31
|
-
}.freeze
|
|
6
|
+
# Auto-derived verb table. Every CLI::Verb (or Group) subclass that
|
|
7
|
+
# declares `command_name "X"` and has no `parent_group` is a top-level
|
|
8
|
+
# verb. Sorted alphabetically for stable help output. Adding a new
|
|
9
|
+
# verb requires only a new file declaring its `command_name`.
|
|
10
|
+
def self.verbs
|
|
11
|
+
Verb.descendants
|
|
12
|
+
.select { |k| k.command_name && k.parent_group.nil? }
|
|
13
|
+
.sort_by(&:command_name)
|
|
14
|
+
.to_h { |k| [k.command_name, k] }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Backward-compat constant; callers should prefer `CLI.verbs`.
|
|
18
|
+
def self.const_missing(name)
|
|
19
|
+
return verbs.freeze if name == :VERBS
|
|
20
|
+
|
|
21
|
+
super
|
|
22
|
+
end
|
|
32
23
|
|
|
33
24
|
def self.run(argv, stdin: $stdin, stdout: $stdout, stderr: $stderr, cwd: Dir.pwd)
|
|
34
25
|
new(stdin: stdin, stdout: stdout, stderr: stderr, cwd: cwd).run(argv)
|
|
@@ -47,21 +38,35 @@ module Textus
|
|
|
47
38
|
verb = argv.shift
|
|
48
39
|
raise UsageError.new("missing verb") if verb.nil?
|
|
49
40
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
41
|
+
result =
|
|
42
|
+
case verb
|
|
43
|
+
when "--version", "-v" then @stdout.puts(VERSION)
|
|
44
|
+
0
|
|
45
|
+
when "--help", "-h" then print_help
|
|
46
|
+
0
|
|
47
|
+
else
|
|
48
|
+
klass = self.class.verbs[verb] or raise UsageError.new("unknown verb: #{verb}")
|
|
49
|
+
dispatch(klass, argv)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
coerce_exit_code(result)
|
|
59
53
|
rescue Textus::Error => e
|
|
60
54
|
emit_error(e)
|
|
61
55
|
end
|
|
62
56
|
|
|
63
57
|
private
|
|
64
58
|
|
|
59
|
+
def coerce_exit_code(value)
|
|
60
|
+
case value
|
|
61
|
+
when Integer then value
|
|
62
|
+
when true, nil then 0
|
|
63
|
+
when false then 1
|
|
64
|
+
else
|
|
65
|
+
@stderr.puts("warning: verb returned non-Integer #{value.class}; treating as 0")
|
|
66
|
+
0
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
65
70
|
def store
|
|
66
71
|
@store ||= Store.discover(@cwd, root: @root_arg)
|
|
67
72
|
end
|
|
@@ -4,7 +4,7 @@ module Textus
|
|
|
4
4
|
class AuditLog < Check
|
|
5
5
|
def call
|
|
6
6
|
path = File.join(store.root, "audit.log")
|
|
7
|
-
Textus::
|
|
7
|
+
Textus::Infra::AuditLog.new(store.root).verify_integrity.map do |v|
|
|
8
8
|
{
|
|
9
9
|
"code" => "audit.parse_error",
|
|
10
10
|
"level" => "warning",
|
|
@@ -9,8 +9,10 @@ module Textus
|
|
|
9
9
|
|
|
10
10
|
Dir.glob(File.join(dir, "*.rb")).sort.each do |f| # rubocop:disable Lint/RedundantDirGlobSort
|
|
11
11
|
registry = Textus::Hooks::Registry.new
|
|
12
|
-
Textus.
|
|
12
|
+
Textus.drain_hook_blocks
|
|
13
|
+
begin
|
|
13
14
|
load(f)
|
|
15
|
+
Textus.drain_hook_blocks.each { |b| b.call(registry) }
|
|
14
16
|
end
|
|
15
17
|
rescue StandardError, ScriptError => e
|
|
16
18
|
out << {
|
|
@@ -13,8 +13,8 @@ module Textus
|
|
|
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/hooks/#{name}.rb with `Textus.on(:resolve_intake, :#{name}) { ... }`",
|
|
16
|
+
"message" => "manifest references intake handler '#{name}' but no resolve_intake hook for '#{name}' is registered",
|
|
17
|
+
"fix" => "create .textus/hooks/#{name}.rb with `Textus.hook { |reg| reg.on(:resolve_intake, :#{name}) { ... } }`",
|
|
18
18
|
}
|
|
19
19
|
end
|
|
20
20
|
|
|
@@ -23,7 +23,7 @@ module Textus
|
|
|
23
23
|
"code" => "intake.handler_orphan",
|
|
24
24
|
"level" => "warning",
|
|
25
25
|
"subject" => name.to_s,
|
|
26
|
-
"message" => "
|
|
26
|
+
"message" => "resolve_intake hook '#{name}' is registered but no manifest entry references it",
|
|
27
27
|
"fix" => "remove the unused handler, or add an entry with `intake.handler: #{name}`",
|
|
28
28
|
}
|
|
29
29
|
end
|
|
@@ -3,7 +3,7 @@ module Textus
|
|
|
3
3
|
class Check
|
|
4
4
|
class SchemaViolations < Check
|
|
5
5
|
def call
|
|
6
|
-
res = Textus::Operations.for(store).
|
|
6
|
+
res = Textus::Operations.for(store).validate_all
|
|
7
7
|
res["violations"].map do |v|
|
|
8
8
|
fix = v["expected"] &&
|
|
9
9
|
"field '#{v["field"]}' should be written by '#{v["expected"]}' (last writer: #{v["last_writer"]})"
|
|
@@ -7,7 +7,7 @@ module Textus
|
|
|
7
7
|
return [] unless File.directory?(dir)
|
|
8
8
|
|
|
9
9
|
repo_root = File.dirname(store.root)
|
|
10
|
-
Dir.glob(File.join(dir, "**", "*#{Textus::
|
|
10
|
+
Dir.glob(File.join(dir, "**", "*#{Textus::Domain::Sentinel::SUFFIX}")).flat_map do |sentinel_path|
|
|
11
11
|
inspect_sentinel(sentinel_path, repo_root)
|
|
12
12
|
end
|
|
13
13
|
end
|
|
@@ -15,7 +15,7 @@ module Textus
|
|
|
15
15
|
private
|
|
16
16
|
|
|
17
17
|
def inspect_sentinel(sentinel_path, repo_root)
|
|
18
|
-
sentinel = Textus::
|
|
18
|
+
sentinel = Textus::Domain::Sentinel.load(sentinel_path, repo_root)
|
|
19
19
|
return [parse_error_issue(sentinel_path)] if sentinel.nil?
|
|
20
20
|
return [orphan_issue(sentinel_path, sentinel)] if sentinel.orphan?
|
|
21
21
|
return [drift_issue(sentinel)] if sentinel.drift?
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Domain
|
|
5
|
+
# Value object describing the freshness annotation attached to an Envelope
|
|
6
|
+
# after a freshness evaluation. Replaces the loose Hash that used to live
|
|
7
|
+
# on `Envelope#freshness`.
|
|
8
|
+
#
|
|
9
|
+
# Note on wire format: `#to_h_for_wire` is intentionally narrower than the
|
|
10
|
+
# full field set. It emits the legacy keys ("stale", "stale_reason",
|
|
11
|
+
# "refreshing", and "refresh_error" when present) so the CLI JSON wire
|
|
12
|
+
# stays byte-identical with textus/3. The gem-side fields `checked_at`
|
|
13
|
+
# and `ttl_remaining_ms` are NOT emitted on the wire in this phase.
|
|
14
|
+
Freshness = Data.define(
|
|
15
|
+
:stale, :refreshing, :reason, :refresh_error, :checked_at, :ttl_remaining_ms
|
|
16
|
+
) do
|
|
17
|
+
def self.build(stale:, refreshing: false, reason: nil, refresh_error: nil,
|
|
18
|
+
checked_at: nil, ttl_remaining_ms: nil)
|
|
19
|
+
new(
|
|
20
|
+
stale: stale,
|
|
21
|
+
refreshing: refreshing,
|
|
22
|
+
reason: reason,
|
|
23
|
+
refresh_error: refresh_error,
|
|
24
|
+
checked_at: checked_at,
|
|
25
|
+
ttl_remaining_ms: ttl_remaining_ms,
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_h_for_wire
|
|
30
|
+
h = {
|
|
31
|
+
"stale" => stale,
|
|
32
|
+
"stale_reason" => reason,
|
|
33
|
+
"refreshing" => refreshing,
|
|
34
|
+
}
|
|
35
|
+
h["refresh_error"] = refresh_error unless refresh_error.nil?
|
|
36
|
+
h
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -15,11 +15,11 @@ module Textus
|
|
|
15
15
|
target_key = entry.meta&.dig("proposal", "target_key")
|
|
16
16
|
return true unless target_key
|
|
17
17
|
|
|
18
|
-
mentry
|
|
18
|
+
mentry = store.manifest.resolve(target_key).entry
|
|
19
19
|
schema_ref = mentry&.schema
|
|
20
20
|
return true unless schema_ref
|
|
21
21
|
|
|
22
|
-
schema = store.
|
|
22
|
+
schema = store.schemas.fetch_or_nil(schema_ref)
|
|
23
23
|
return true unless schema
|
|
24
24
|
|
|
25
25
|
frontmatter = entry.meta&.dig("frontmatter") || {}
|
|
@@ -3,7 +3,7 @@ require "digest"
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
|
|
5
5
|
module Textus
|
|
6
|
-
|
|
6
|
+
module Domain
|
|
7
7
|
# Value object for sentinel files written by Infra::Publisher and inspected
|
|
8
8
|
# by Doctor::Check::Sentinels. Owns the JSON shape ({source, target,
|
|
9
9
|
# sha256, mode}) and the on-disk path layout (<store_root>/sentinels/
|
data/lib/textus/entry/json.rb
CHANGED
|
@@ -85,7 +85,7 @@ module Textus
|
|
|
85
85
|
|
|
86
86
|
def self.inject_uid(meta, content, existing_uid)
|
|
87
87
|
m = meta.is_a?(Hash) ? meta.dup : {}
|
|
88
|
-
m["uid"] = existing_uid || Textus::
|
|
88
|
+
m["uid"] = existing_uid || Textus::Uid.mint unless m["uid"].is_a?(String) && !m["uid"].empty?
|
|
89
89
|
[m, content]
|
|
90
90
|
end
|
|
91
91
|
|
|
@@ -70,7 +70,7 @@ module Textus
|
|
|
70
70
|
|
|
71
71
|
def self.inject_uid(meta, content, existing_uid)
|
|
72
72
|
m = meta.is_a?(Hash) ? meta.dup : {}
|
|
73
|
-
m["uid"] = existing_uid || Textus::
|
|
73
|
+
m["uid"] = existing_uid || Textus::Uid.mint unless m["uid"].is_a?(String) && !m["uid"].empty?
|
|
74
74
|
[m, content]
|
|
75
75
|
end
|
|
76
76
|
|
data/lib/textus/entry/yaml.rb
CHANGED
|
@@ -83,7 +83,7 @@ module Textus
|
|
|
83
83
|
|
|
84
84
|
def self.inject_uid(meta, content, existing_uid)
|
|
85
85
|
m = meta.is_a?(Hash) ? meta.dup : {}
|
|
86
|
-
m["uid"] = existing_uid || Textus::
|
|
86
|
+
m["uid"] = existing_uid || Textus::Uid.mint unless m["uid"].is_a?(String) && !m["uid"].empty?
|
|
87
87
|
[m, content]
|
|
88
88
|
end
|
|
89
89
|
|
data/lib/textus/envelope.rb
CHANGED
|
@@ -45,16 +45,20 @@ module Textus
|
|
|
45
45
|
"uid" => uid,
|
|
46
46
|
}
|
|
47
47
|
h["content"] = content unless content.nil?
|
|
48
|
-
freshness
|
|
48
|
+
freshness&.to_h_for_wire&.each { |k, v| h[k] = v }
|
|
49
49
|
h
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
def stale?
|
|
53
|
-
|
|
53
|
+
return false if freshness.nil?
|
|
54
|
+
|
|
55
|
+
freshness.stale == true
|
|
54
56
|
end
|
|
55
57
|
|
|
56
58
|
def refreshing?
|
|
57
|
-
|
|
59
|
+
return false if freshness.nil?
|
|
60
|
+
|
|
61
|
+
freshness.refreshing == true
|
|
58
62
|
end
|
|
59
63
|
end
|
|
60
64
|
end
|
data/lib/textus/errors.rb
CHANGED
|
@@ -108,6 +108,25 @@ module Textus
|
|
|
108
108
|
end
|
|
109
109
|
end
|
|
110
110
|
|
|
111
|
+
class ReadForbidden < Error
|
|
112
|
+
def initialize(k, z, readers: nil)
|
|
113
|
+
readers_str =
|
|
114
|
+
if readers && !readers.empty?
|
|
115
|
+
readers.join(", ")
|
|
116
|
+
else
|
|
117
|
+
"the role(s) listed in the manifest 'read_policy:'"
|
|
118
|
+
end
|
|
119
|
+
details = { "key" => k, "zone" => z }
|
|
120
|
+
details["readers"] = readers if readers
|
|
121
|
+
super(
|
|
122
|
+
"read_forbidden",
|
|
123
|
+
"zone '#{z}' is not readable by role for key '#{k}'",
|
|
124
|
+
details: details,
|
|
125
|
+
hint: "this zone is readable by #{readers_str}; pass --as=<role>",
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
111
130
|
class EtagMismatch < Error
|
|
112
131
|
def initialize(k, w, g)
|
|
113
132
|
super(
|