textus 0.5.0 → 0.8.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 +83 -1
- data/README.md +29 -21
- data/SPEC.md +75 -142
- data/docs/architecture.md +42 -23
- data/lib/textus/builder/pipeline.rb +56 -0
- data/lib/textus/builder/renderer/json.rb +42 -0
- data/lib/textus/builder/renderer/markdown.rb +22 -0
- data/lib/textus/builder/renderer/text.rb +14 -0
- data/lib/textus/builder/renderer/yaml.rb +42 -0
- data/lib/textus/builder/renderer.rb +17 -0
- data/lib/textus/builder.rb +9 -114
- data/lib/textus/cli/group/hook.rb +11 -0
- data/lib/textus/cli/group/key.rb +12 -0
- data/lib/textus/cli/group/schema.rb +13 -0
- data/lib/textus/cli/verb/accept.rb +15 -0
- data/lib/textus/cli/verb/build.rb +13 -0
- data/lib/textus/cli/verb/delete.rb +16 -0
- data/lib/textus/cli/verb/deps.rb +12 -0
- data/lib/textus/cli/verb/doctor.rb +15 -0
- data/lib/textus/cli/verb/get.rb +12 -0
- data/lib/textus/cli/verb/hook_run.rb +48 -0
- data/lib/textus/cli/verb/hooks.rb +50 -0
- data/lib/textus/cli/verb/init.rb +14 -0
- data/lib/textus/cli/verb/intro.rb +11 -0
- data/lib/textus/cli/verb/list.rb +14 -0
- data/lib/textus/cli/verb/migrate_keys.rb +16 -0
- data/lib/textus/cli/verb/mv.rb +17 -0
- data/lib/textus/cli/verb/published.rb +11 -0
- data/lib/textus/cli/verb/put.rb +50 -0
- data/lib/textus/cli/verb/rdeps.rb +12 -0
- data/lib/textus/cli/verb/refresh.rb +15 -0
- data/lib/textus/cli/verb/schema.rb +12 -0
- data/lib/textus/cli/verb/schema_diff.rb +12 -0
- data/lib/textus/cli/verb/schema_init.rb +16 -0
- data/lib/textus/cli/verb/schema_migrate.rb +16 -0
- data/lib/textus/cli/verb/stale.rb +14 -0
- data/lib/textus/cli/verb/uid.rb +12 -0
- data/lib/textus/cli/verb/where.rb +12 -0
- data/lib/textus/cli.rb +23 -42
- data/lib/textus/doctor/check/audit_log.rb +50 -0
- data/lib/textus/doctor/check/hooks.rb +29 -0
- data/lib/textus/doctor/check/illegal_keys.rb +49 -0
- data/lib/textus/doctor/check/manifest_files.rb +38 -0
- data/lib/textus/doctor/check/schema_violations.rb +22 -0
- data/lib/textus/doctor/check/schemas.rb +26 -0
- data/lib/textus/doctor/check/sentinels.rb +57 -0
- data/lib/textus/doctor/check/templates.rb +26 -0
- data/lib/textus/doctor/check/unowned_schema_fields.rb +34 -0
- data/lib/textus/doctor/check.rb +30 -0
- data/lib/textus/doctor.rb +22 -288
- data/lib/textus/entry/base.rb +30 -0
- data/lib/textus/entry/json.rb +5 -1
- data/lib/textus/entry/markdown.rb +1 -1
- data/lib/textus/entry/text.rb +1 -1
- data/lib/textus/entry/yaml.rb +5 -1
- data/lib/textus/entry.rb +0 -5
- data/lib/textus/envelope.rb +30 -0
- data/lib/textus/hooks/builtin.rb +70 -0
- data/lib/textus/hooks/dispatcher.rb +49 -0
- data/lib/textus/hooks/loader.rb +26 -0
- data/lib/textus/hooks/registry.rb +73 -0
- data/lib/textus/init.rb +13 -10
- data/lib/textus/intro.rb +14 -16
- data/lib/textus/key/distance.rb +55 -0
- data/lib/textus/key/grammar.rb +33 -0
- data/lib/textus/key/path.rb +17 -0
- data/lib/textus/manifest/entry.rb +199 -0
- data/lib/textus/manifest.rb +10 -34
- data/lib/textus/migrate_keys.rb +1 -1
- data/lib/textus/projection.rb +5 -4
- data/lib/textus/proposal.rb +1 -1
- data/lib/textus/refresh.rb +11 -11
- data/lib/textus/schema/tools.rb +89 -0
- data/lib/textus/store/audit_log.rb +71 -0
- data/lib/textus/store/mover.rb +19 -16
- data/lib/textus/store/reader.rb +67 -0
- data/lib/textus/store/staleness.rb +10 -19
- data/lib/textus/store/validator.rb +11 -8
- data/lib/textus/store/view.rb +29 -0
- data/lib/textus/store/writer.rb +132 -0
- data/lib/textus/store.rb +25 -221
- data/lib/textus/version.rb +1 -1
- data/lib/textus.rb +14 -67
- metadata +73 -40
- data/lib/textus/audit_log.rb +0 -67
- data/lib/textus/builtin_actions.rb +0 -68
- data/lib/textus/cli/accept.rb +0 -13
- data/lib/textus/cli/action.rb +0 -51
- data/lib/textus/cli/build.rb +0 -11
- data/lib/textus/cli/delete.rb +0 -14
- data/lib/textus/cli/deprecated_alias.rb +0 -31
- data/lib/textus/cli/deps.rb +0 -10
- data/lib/textus/cli/doctor.rb +0 -13
- data/lib/textus/cli/extension_group.rb +0 -9
- data/lib/textus/cli/extensions.rb +0 -49
- data/lib/textus/cli/get.rb +0 -10
- data/lib/textus/cli/init.rb +0 -12
- data/lib/textus/cli/intro.rb +0 -9
- data/lib/textus/cli/key_group.rb +0 -10
- data/lib/textus/cli/list.rb +0 -12
- data/lib/textus/cli/migrate.rb +0 -41
- data/lib/textus/cli/migrate_keys.rb +0 -19
- data/lib/textus/cli/mv.rb +0 -20
- data/lib/textus/cli/published.rb +0 -9
- data/lib/textus/cli/put.rb +0 -48
- data/lib/textus/cli/rdeps.rb +0 -10
- data/lib/textus/cli/refresh.rb +0 -13
- data/lib/textus/cli/schema.rb +0 -10
- data/lib/textus/cli/schema_diff.rb +0 -15
- data/lib/textus/cli/schema_group.rb +0 -33
- data/lib/textus/cli/schema_init.rb +0 -19
- data/lib/textus/cli/schema_migrate.rb +0 -19
- data/lib/textus/cli/stale.rb +0 -12
- data/lib/textus/cli/uid.rb +0 -15
- data/lib/textus/cli/where.rb +0 -10
- data/lib/textus/extension_registry.rb +0 -61
- data/lib/textus/extensions.rb +0 -33
- data/lib/textus/key_distance.rb +0 -53
- data/lib/textus/manifest_entry.rb +0 -185
- data/lib/textus/migrate_v2.rb +0 -27
- data/lib/textus/schema_tools.rb +0 -87
- data/lib/textus/store/events.rb +0 -31
- data/lib/textus/store_view.rb +0 -27
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class Verb
|
|
4
|
+
class Refresh < Verb
|
|
5
|
+
option :as_flag, "--as=ROLE"
|
|
6
|
+
|
|
7
|
+
def call(store)
|
|
8
|
+
key = positional.shift or raise UsageError.new("refresh requires a key")
|
|
9
|
+
role = Role.resolve(flag: as_flag, env: ENV, root: store.root)
|
|
10
|
+
emit(Textus::Refresh.call(store, key, as: role))
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class Verb
|
|
4
|
+
class SchemaInit < Verb
|
|
5
|
+
option :from_key, "--from=KEY"
|
|
6
|
+
|
|
7
|
+
def call(store)
|
|
8
|
+
name = positional.shift or raise UsageError.new("schema init NAME")
|
|
9
|
+
raise UsageError.new("schema init requires --from=KEY") unless from_key
|
|
10
|
+
|
|
11
|
+
emit(Textus::Schema::Tools.init(store, name: name, from: from_key))
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class Verb
|
|
4
|
+
class SchemaMigrate < Verb
|
|
5
|
+
option :rename, "--rename=O:N"
|
|
6
|
+
|
|
7
|
+
def call(store)
|
|
8
|
+
name = positional.shift or raise UsageError.new("schema migrate NAME")
|
|
9
|
+
raise UsageError.new("schema migrate requires --rename=OLD:NEW") unless rename
|
|
10
|
+
|
|
11
|
+
emit(Textus::Schema::Tools.migrate(store, name: name, rename: rename))
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
data/lib/textus/cli.rb
CHANGED
|
@@ -6,40 +6,26 @@ module Textus
|
|
|
6
6
|
# verb name → Verb subclass. Adding a new verb is a one-line entry here
|
|
7
7
|
# plus a new file under lib/textus/cli/.
|
|
8
8
|
VERBS = {
|
|
9
|
-
"accept" => Accept,
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"rdeps" => Rdeps,
|
|
28
|
-
"refresh" => RefreshVerb,
|
|
29
|
-
"schema" => SchemaGroup,
|
|
30
|
-
"schema-diff" => SchemaDiff,
|
|
31
|
-
"schema-init" => SchemaInit,
|
|
32
|
-
"schema-migrate" => SchemaMigrate,
|
|
33
|
-
"stale" => Stale,
|
|
34
|
-
"uid" => Uid,
|
|
35
|
-
"where" => Where,
|
|
9
|
+
"accept" => Verb::Accept,
|
|
10
|
+
"build" => Verb::Build,
|
|
11
|
+
"delete" => Verb::Delete,
|
|
12
|
+
"deps" => Verb::Deps,
|
|
13
|
+
"doctor" => Verb::Doctor,
|
|
14
|
+
"get" => Verb::Get,
|
|
15
|
+
"hook" => Group::Hook,
|
|
16
|
+
"init" => Verb::Init,
|
|
17
|
+
"intro" => Verb::Intro,
|
|
18
|
+
"key" => Group::Key,
|
|
19
|
+
"list" => Verb::List,
|
|
20
|
+
"published" => Verb::Published,
|
|
21
|
+
"put" => Verb::Put,
|
|
22
|
+
"rdeps" => Verb::Rdeps,
|
|
23
|
+
"refresh" => Verb::Refresh,
|
|
24
|
+
"schema" => Group::Schema,
|
|
25
|
+
"stale" => Verb::Stale,
|
|
26
|
+
"where" => Verb::Where,
|
|
36
27
|
}.freeze
|
|
37
28
|
|
|
38
|
-
# Flat aliases kept for backward-compat through 0.5; emit deprecation warnings.
|
|
39
|
-
DEPRECATED_ALIASES = %w[
|
|
40
|
-
mv uid migrate-keys schema-init schema-diff schema-migrate extensions action
|
|
41
|
-
].freeze
|
|
42
|
-
|
|
43
29
|
def self.run(argv, stdin: $stdin, stdout: $stdout, stderr: $stderr, cwd: Dir.pwd)
|
|
44
30
|
new(stdin: stdin, stdout: stdout, stderr: stderr, cwd: cwd).run(argv)
|
|
45
31
|
end
|
|
@@ -64,7 +50,7 @@ module Textus
|
|
|
64
50
|
0
|
|
65
51
|
else
|
|
66
52
|
klass = VERBS[verb] or raise UsageError.new("unknown verb: #{verb}")
|
|
67
|
-
dispatch(klass, argv
|
|
53
|
+
dispatch(klass, argv)
|
|
68
54
|
end
|
|
69
55
|
rescue Textus::Error => e
|
|
70
56
|
emit_error(e)
|
|
@@ -76,9 +62,8 @@ module Textus
|
|
|
76
62
|
@store ||= Store.discover(@cwd, root: @root_arg)
|
|
77
63
|
end
|
|
78
64
|
|
|
79
|
-
def dispatch(klass, argv
|
|
65
|
+
def dispatch(klass, argv)
|
|
80
66
|
v = klass.new(stdin: @stdin, stdout: @stdout, stderr: @stderr, cwd: @cwd)
|
|
81
|
-
v.deprecated_alias = true if deprecated_alias && v.respond_to?(:deprecated_alias=)
|
|
82
67
|
v.parse(argv)
|
|
83
68
|
v.call(klass.needs_store? ? store : nil)
|
|
84
69
|
end
|
|
@@ -92,24 +77,20 @@ module Textus
|
|
|
92
77
|
|
|
93
78
|
def print_help
|
|
94
79
|
@stdout.puts <<~HELP
|
|
95
|
-
textus #{VERSION} — reference implementation of #{PROTOCOL}
|
|
80
|
+
textus #{VERSION} — reference implementation of #{PROTOCOL}
|
|
96
81
|
|
|
97
82
|
Usage (json output is the default; --format=json accepted for back-compat):
|
|
98
83
|
textus list [--prefix=KEY] [--zone=Z]
|
|
99
84
|
textus where KEY
|
|
100
85
|
textus get KEY
|
|
101
|
-
textus put KEY --stdin [--
|
|
86
|
+
textus put KEY --stdin [--fetch=NAME] --as=ROLE
|
|
102
87
|
textus stale [--prefix=KEY] [--zone=Z]
|
|
103
88
|
textus doctor
|
|
104
89
|
textus intro
|
|
105
|
-
textus migrate v2
|
|
106
90
|
|
|
107
91
|
textus key {mv,uid,migrate}
|
|
108
92
|
textus schema {show,init,diff,migrate}
|
|
109
|
-
textus
|
|
110
|
-
|
|
111
|
-
Deprecated (removed in 0.6): mv, uid, migrate-keys, schema-init,
|
|
112
|
-
schema-diff, schema-migrate, extensions, action.
|
|
93
|
+
textus hook {list,run}
|
|
113
94
|
HELP
|
|
114
95
|
end
|
|
115
96
|
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Doctor
|
|
5
|
+
class Check
|
|
6
|
+
class AuditLog < Check
|
|
7
|
+
def call
|
|
8
|
+
out = []
|
|
9
|
+
path = File.join(store.root, "audit.log")
|
|
10
|
+
return out unless File.exist?(path)
|
|
11
|
+
|
|
12
|
+
File.foreach(path).with_index(1) do |line, lineno| # rubocop:disable Metrics/BlockLength
|
|
13
|
+
stripped = line.chomp
|
|
14
|
+
next if stripped.empty?
|
|
15
|
+
|
|
16
|
+
if stripped.start_with?("{")
|
|
17
|
+
begin
|
|
18
|
+
JSON.parse(stripped)
|
|
19
|
+
rescue JSON::ParserError => e
|
|
20
|
+
out << {
|
|
21
|
+
"code" => "audit.parse_error",
|
|
22
|
+
"level" => "warning",
|
|
23
|
+
"subject" => "#{path}:#{lineno}",
|
|
24
|
+
"message" => "audit log line #{lineno} is invalid JSON: #{e.message}",
|
|
25
|
+
"fix" => "inspect #{path} at line #{lineno} and remove the corrupted row",
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
else
|
|
29
|
+
# Legacy TSV (pre-0.5): read-only support retained for on-disk logs
|
|
30
|
+
# written by older textus versions. Never written by current code.
|
|
31
|
+
# Minimum 6 fields.
|
|
32
|
+
fields = stripped.split("\t")
|
|
33
|
+
next if fields.length >= 6
|
|
34
|
+
|
|
35
|
+
out << {
|
|
36
|
+
"code" => "audit.parse_error",
|
|
37
|
+
"level" => "warning",
|
|
38
|
+
"subject" => "#{path}:#{lineno}",
|
|
39
|
+
"message" => "audit log line #{lineno} has #{fields.length} fields " \
|
|
40
|
+
"(expected >=6 for legacy TSV; consider migrating to NDJSON)",
|
|
41
|
+
"fix" => "inspect #{path} at line #{lineno} and remove the corrupted row",
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
out
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Doctor
|
|
3
|
+
class Check
|
|
4
|
+
class Hooks < Check
|
|
5
|
+
def call
|
|
6
|
+
out = []
|
|
7
|
+
dir = File.join(store.root, "hooks")
|
|
8
|
+
return out unless File.directory?(dir)
|
|
9
|
+
|
|
10
|
+
Dir.glob(File.join(dir, "*.rb")).sort.each do |f| # rubocop:disable Lint/RedundantDirGlobSort
|
|
11
|
+
registry = Textus::Hooks::Registry.new
|
|
12
|
+
Textus.with_registry(registry) do
|
|
13
|
+
load(f)
|
|
14
|
+
end
|
|
15
|
+
rescue StandardError, ScriptError => e
|
|
16
|
+
out << {
|
|
17
|
+
"code" => "hook.load_failed",
|
|
18
|
+
"level" => "error",
|
|
19
|
+
"subject" => File.basename(f),
|
|
20
|
+
"message" => "#{e.class}: #{e.message}",
|
|
21
|
+
"fix" => "open #{f} and fix the syntax/load error",
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
out
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Doctor
|
|
3
|
+
class Check
|
|
4
|
+
class IllegalKeys < Check
|
|
5
|
+
def call
|
|
6
|
+
out = []
|
|
7
|
+
store.manifest.entries.each do |entry|
|
|
8
|
+
next unless entry.nested
|
|
9
|
+
|
|
10
|
+
base = File.join(store.root, "zones", entry.path)
|
|
11
|
+
next unless File.directory?(base)
|
|
12
|
+
|
|
13
|
+
walk_nested(base) do |abs_path, is_dir|
|
|
14
|
+
basename = File.basename(abs_path)
|
|
15
|
+
stem = is_dir ? basename : basename.sub(/#{Regexp.escape(File.extname(basename))}\z/, "")
|
|
16
|
+
next if stem.match?(Key::Grammar::SEGMENT)
|
|
17
|
+
|
|
18
|
+
proposed = Textus::MigrateKeys.normalize(stem)
|
|
19
|
+
out << {
|
|
20
|
+
"code" => "key.illegal",
|
|
21
|
+
"level" => "error",
|
|
22
|
+
"subject" => abs_path,
|
|
23
|
+
"path" => abs_path,
|
|
24
|
+
"proposed_key" => proposed,
|
|
25
|
+
"message" => "illegal key segment '#{stem}' at #{abs_path}",
|
|
26
|
+
"fix" => "run 'textus key migrate --dry-run' then '--write' to rename to '#{proposed}'",
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
out
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def walk_nested(root, &block)
|
|
36
|
+
Dir.each_child(root) do |name|
|
|
37
|
+
abs = File.join(root, name)
|
|
38
|
+
if File.directory?(abs)
|
|
39
|
+
walk_nested(abs, &block)
|
|
40
|
+
yield abs, true
|
|
41
|
+
else
|
|
42
|
+
yield abs, false
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Doctor
|
|
3
|
+
class Check
|
|
4
|
+
class ManifestFiles < Check
|
|
5
|
+
def call
|
|
6
|
+
out = []
|
|
7
|
+
store.manifest.entries.each do |entry|
|
|
8
|
+
next if entry.nested
|
|
9
|
+
|
|
10
|
+
path = leaf_path_for(entry)
|
|
11
|
+
next if File.exist?(path)
|
|
12
|
+
|
|
13
|
+
out << {
|
|
14
|
+
"code" => "manifest.missing_file",
|
|
15
|
+
"level" => "info",
|
|
16
|
+
"subject" => entry.key,
|
|
17
|
+
"message" => "declared entry has no file on disk at #{path}",
|
|
18
|
+
"fix" => "create the entry with 'textus put #{entry.key} --stdin --as=<role>' " \
|
|
19
|
+
"(or leave empty if not yet authored)",
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
out
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def leaf_path_for(entry)
|
|
28
|
+
primary_ext = Entry.for_format(entry.format).extensions.first
|
|
29
|
+
if File.extname(entry.path) == ""
|
|
30
|
+
File.join(store.root, "zones", entry.path + primary_ext)
|
|
31
|
+
else
|
|
32
|
+
File.join(store.root, "zones", entry.path)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Doctor
|
|
3
|
+
class Check
|
|
4
|
+
class SchemaViolations < Check
|
|
5
|
+
def call
|
|
6
|
+
res = store.validate_all
|
|
7
|
+
res["violations"].map do |v|
|
|
8
|
+
fix = v["expected"] &&
|
|
9
|
+
"field '#{v["field"]}' should be written by '#{v["expected"]}' (last writer: #{v["last_writer"]})"
|
|
10
|
+
{
|
|
11
|
+
"code" => v["code"],
|
|
12
|
+
"level" => "error",
|
|
13
|
+
"subject" => v["key"],
|
|
14
|
+
"message" => v["message"] || "#{v["code"]} on #{v["key"]}",
|
|
15
|
+
"fix" => fix,
|
|
16
|
+
}.compact
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Doctor
|
|
3
|
+
class Check
|
|
4
|
+
class Schemas < Check
|
|
5
|
+
def call
|
|
6
|
+
out = []
|
|
7
|
+
store.manifest.entries.each do |entry|
|
|
8
|
+
next if entry.schema.nil?
|
|
9
|
+
|
|
10
|
+
sp = File.join(store.root, "schemas", "#{entry.schema}.yaml")
|
|
11
|
+
next if File.exist?(sp)
|
|
12
|
+
|
|
13
|
+
out << {
|
|
14
|
+
"code" => "schema.missing",
|
|
15
|
+
"level" => "error",
|
|
16
|
+
"subject" => entry.key,
|
|
17
|
+
"message" => "schema '#{entry.schema}' not found at #{sp}",
|
|
18
|
+
"fix" => "create the schema file or run 'textus schema init #{entry.schema} --from=<key>'",
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
out
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
require "digest"
|
|
2
|
+
require "json"
|
|
3
|
+
|
|
4
|
+
module Textus
|
|
5
|
+
module Doctor
|
|
6
|
+
class Check
|
|
7
|
+
class Sentinels < Check
|
|
8
|
+
def call
|
|
9
|
+
out = []
|
|
10
|
+
dir = File.join(store.root, "sentinels")
|
|
11
|
+
return out unless File.directory?(dir)
|
|
12
|
+
|
|
13
|
+
Dir.glob(File.join(dir, "**", "*.textus-managed.json")).each do |sp| # rubocop:disable Metrics/BlockLength
|
|
14
|
+
begin
|
|
15
|
+
data = JSON.parse(File.read(sp))
|
|
16
|
+
rescue JSON::ParserError => e
|
|
17
|
+
out << {
|
|
18
|
+
"code" => "sentinel.parse_error",
|
|
19
|
+
"level" => "warning",
|
|
20
|
+
"subject" => sp,
|
|
21
|
+
"message" => "sentinel is not valid JSON: #{e.message}",
|
|
22
|
+
"fix" => "delete #{sp} and re-run 'textus build' to regenerate",
|
|
23
|
+
}
|
|
24
|
+
next
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
target = data["target"]
|
|
28
|
+
recorded_sha = data["sha256"]
|
|
29
|
+
|
|
30
|
+
if target.nil? || !File.exist?(target)
|
|
31
|
+
out << {
|
|
32
|
+
"code" => "sentinel.orphan",
|
|
33
|
+
"level" => "warning",
|
|
34
|
+
"subject" => sp,
|
|
35
|
+
"message" => "sentinel target #{target.inspect} no longer exists",
|
|
36
|
+
"fix" => "delete #{sp} (the published file is gone) or restore the target",
|
|
37
|
+
}
|
|
38
|
+
next
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
current_sha = Digest::SHA256.hexdigest(File.binread(target))
|
|
42
|
+
next if recorded_sha.nil? || current_sha == recorded_sha
|
|
43
|
+
|
|
44
|
+
out << {
|
|
45
|
+
"code" => "sentinel.drift",
|
|
46
|
+
"level" => "warning",
|
|
47
|
+
"subject" => target,
|
|
48
|
+
"message" => "published file at #{target} was modified out-of-band",
|
|
49
|
+
"fix" => "re-run 'textus build' to overwrite, or copy the manual edit back into the store source",
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
out
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Doctor
|
|
3
|
+
class Check
|
|
4
|
+
class Templates < Check
|
|
5
|
+
def call
|
|
6
|
+
out = []
|
|
7
|
+
store.manifest.entries.each do |entry|
|
|
8
|
+
next if entry.template.nil?
|
|
9
|
+
|
|
10
|
+
tp = File.join(store.root, "templates", entry.template)
|
|
11
|
+
next if File.exist?(tp)
|
|
12
|
+
|
|
13
|
+
out << {
|
|
14
|
+
"code" => "template.missing",
|
|
15
|
+
"level" => "error",
|
|
16
|
+
"subject" => entry.key,
|
|
17
|
+
"message" => "template '#{entry.template}' not found at #{tp}",
|
|
18
|
+
"fix" => "create the file at #{tp} or update the entry's template: field",
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
out
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Doctor
|
|
3
|
+
class Check
|
|
4
|
+
class UnownedSchemaFields < Check
|
|
5
|
+
def call
|
|
6
|
+
out = []
|
|
7
|
+
dir = File.join(store.root, "schemas")
|
|
8
|
+
return out unless File.directory?(dir)
|
|
9
|
+
|
|
10
|
+
Dir.glob(File.join(dir, "*.yaml")).sort.each do |sp| # rubocop:disable Lint/RedundantDirGlobSort
|
|
11
|
+
schema = begin
|
|
12
|
+
Schema.load(sp)
|
|
13
|
+
rescue StandardError
|
|
14
|
+
next
|
|
15
|
+
end
|
|
16
|
+
unowned = schema.fields.each_with_object([]) do |(name, spec), acc|
|
|
17
|
+
acc << name if spec.is_a?(Hash) && spec["maintained_by"].nil?
|
|
18
|
+
end
|
|
19
|
+
next if unowned.empty?
|
|
20
|
+
|
|
21
|
+
out << {
|
|
22
|
+
"code" => "schema.unowned_fields",
|
|
23
|
+
"level" => "info",
|
|
24
|
+
"subject" => schema.name || File.basename(sp, ".yaml"),
|
|
25
|
+
"message" => "schema has fields without maintained_by: #{unowned.join(", ")}",
|
|
26
|
+
"fix" => "add 'maintained_by: <role>' to each field in #{sp} (optional but recommended)",
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
out
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Doctor
|
|
3
|
+
# Abstract base for a single doctor check. Each concrete check inspects
|
|
4
|
+
# one slice of store health and returns an array of issue hashes:
|
|
5
|
+
# { "code" => String, "level" => "error"|"warning"|"info",
|
|
6
|
+
# "subject" => String, "message" => String, "fix" => String (optional) }
|
|
7
|
+
class Check
|
|
8
|
+
# Snake-case name used in --checks flag and ALL_CHECKS list. Default
|
|
9
|
+
# derives from the class name; override if the SPEC name diverges.
|
|
10
|
+
def self.name_key
|
|
11
|
+
@name_key ||= name.split("::").last
|
|
12
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
13
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
14
|
+
.downcase
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(store)
|
|
18
|
+
@store = store
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def call
|
|
22
|
+
raise NotImplementedError.new("#{self.class.name}#call not implemented")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
protected
|
|
26
|
+
|
|
27
|
+
attr_reader :store
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|