textus 0.55.1 → 0.55.2
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 +1 -1
- data/README.md +9 -9
- data/SPEC.md +14 -13
- data/docs/architecture/README.md +3 -3
- data/docs/reference/conventions.md +5 -2
- data/lib/textus/boot.rb +64 -85
- data/lib/textus/{gate → dispatch}/binder.rb +8 -10
- data/lib/textus/dispatch/contracts.rb +63 -0
- data/lib/textus/dispatch/handler_registry.rb +21 -0
- data/lib/textus/dispatch/middleware/audit_index.rb +51 -0
- data/lib/textus/dispatch/middleware/auth.rb +40 -0
- data/lib/textus/dispatch/middleware/base.rb +26 -0
- data/lib/textus/dispatch/middleware/binder.rb +20 -0
- data/lib/textus/dispatch/middleware/cascade.rb +53 -0
- data/lib/textus/dispatch/pipeline.rb +35 -0
- data/lib/textus/doctor/check/audit_log.rb +1 -1
- data/lib/textus/doctor/check/generator_drift.rb +2 -2
- data/lib/textus/doctor/check/orphaned_publish_targets.rb +1 -1
- data/lib/textus/doctor/check/schema_violations.rb +1 -1
- data/lib/textus/doctor/check/{notebook_sources.rb → scratchpad_sources.rb} +10 -5
- data/lib/textus/doctor/check/sentinels.rb +1 -1
- data/lib/textus/doctor/check.rb +8 -6
- data/lib/textus/doctor.rb +1 -1
- data/lib/textus/errors.rb +2 -0
- data/lib/textus/format/base.rb +36 -8
- data/lib/textus/format/json.rb +0 -21
- data/lib/textus/format/markdown.rb +0 -21
- data/lib/textus/format/yaml.rb +0 -21
- data/lib/textus/format.rb +16 -1
- data/lib/textus/handlers/maintenance/boot_store.rb +15 -0
- data/lib/textus/handlers/maintenance/doctor_store.rb +15 -0
- data/lib/textus/handlers/maintenance/drain_store.rb +21 -0
- data/lib/textus/handlers/maintenance/ingest_entry.rb +159 -0
- data/lib/textus/handlers/maintenance/jobs_action.rb +21 -0
- data/lib/textus/handlers/maintenance/published_entries.rb +17 -0
- data/lib/textus/handlers/maintenance/rule_explain.rb +77 -0
- data/lib/textus/handlers/maintenance/rule_lint.rb +54 -0
- data/lib/textus/handlers/maintenance/rule_list.rb +32 -0
- data/lib/textus/handlers/maintenance/schema_envelope.rb +19 -0
- data/lib/textus/handlers/read/audit_entries.rb +48 -0
- data/lib/textus/handlers/read/blame_entry.rb +71 -0
- data/lib/textus/handlers/read/deps_entry.rb +17 -0
- data/lib/textus/handlers/read/get_entry.rb +68 -0
- data/lib/textus/handlers/read/list_keys.rb +36 -0
- data/lib/textus/handlers/read/pulse_entries.rb +66 -0
- data/lib/textus/handlers/read/rdeps_entry.rb +21 -0
- data/lib/textus/handlers/read/uid_entry.rb +18 -0
- data/lib/textus/handlers/read/where_entry.rb +18 -0
- data/lib/textus/handlers/write/accept_proposal.rb +39 -0
- data/lib/textus/handlers/write/data_mv.rb +55 -0
- data/lib/textus/handlers/write/delete_key.rb +17 -0
- data/lib/textus/handlers/write/enqueue_job.rb +27 -0
- data/lib/textus/handlers/write/key_delete_prefix.rb +32 -0
- data/lib/textus/handlers/write/key_mv_prefix.rb +45 -0
- data/lib/textus/handlers/write/move_key.rb +80 -0
- data/lib/textus/handlers/write/propose_entry.rb +29 -0
- data/lib/textus/handlers/write/put_entry.rb +29 -0
- data/lib/textus/handlers/write/reject_proposal.rb +29 -0
- data/lib/textus/init.rb +5 -5
- data/lib/textus/manifest/capabilities.rb +1 -1
- data/lib/textus/manifest/entry/base.rb +3 -3
- data/lib/textus/manifest/entry/publish/to_paths.rb +1 -1
- data/lib/textus/manifest/policy/predicates/author_held.rb +22 -0
- data/lib/textus/manifest/policy/predicates/etag_match.rb +18 -0
- data/lib/textus/manifest/policy/predicates/fresh_within.rb +13 -0
- data/lib/textus/manifest/policy/predicates/lane_deletable_by.rb +31 -0
- data/lib/textus/manifest/policy/predicates/lane_writable_by.rb +23 -0
- data/lib/textus/manifest/policy/predicates/raw_lane_ingest_only.rb +25 -0
- data/lib/textus/manifest/policy/predicates/raw_write_once.rb +24 -0
- data/lib/textus/manifest/policy/predicates/schema_valid.rb +41 -0
- data/lib/textus/manifest/policy/predicates/target_is_canon.rb +20 -0
- data/lib/textus/manifest/policy/predicates.rb +54 -0
- data/lib/textus/manifest/policy/retention.rb +1 -1
- data/lib/textus/orchestration.rb +55 -0
- data/lib/textus/port/audit_log.rb +6 -6
- data/lib/textus/port/build_lock.rb +1 -1
- data/lib/textus/{core → port}/sentinel.rb +1 -6
- data/lib/textus/port/sentinel_store.rb +3 -3
- data/lib/textus/port/storage/file_store.rb +23 -0
- data/lib/textus/port/storage/interface.rb +17 -0
- data/lib/textus/port/store.rb +58 -2
- data/lib/textus/port/watcher_lock.rb +2 -2
- data/lib/textus/produce/engine.rb +1 -11
- data/lib/textus/produce/publisher.rb +21 -0
- data/lib/textus/schema/registry.rb +42 -0
- data/lib/textus/schema/tools.rb +3 -10
- data/lib/textus/store/container.rb +140 -10
- data/lib/textus/store/cursor.rb +1 -1
- data/lib/textus/store/{envelope → entry}/reader.rb +8 -4
- data/lib/textus/store/{envelope → entry}/writer.rb +53 -29
- data/lib/textus/store/envelope/meta.rb +61 -0
- data/lib/textus/store/freshness/drift_detector.rb +93 -0
- data/lib/textus/store/freshness/evaluator.rb +20 -0
- data/lib/textus/store/freshness/ttl_evaluator.rb +57 -0
- data/lib/textus/{core → store}/freshness/verdict.rb +1 -11
- data/lib/textus/store/freshness.rb +8 -0
- data/lib/textus/store/index/builder.rb +5 -3
- data/lib/textus/store/jobs/planner.rb +27 -7
- data/lib/textus/store/jobs/queue.rb +9 -1
- data/lib/textus/store/jobs/retention/base.rb +52 -0
- data/lib/textus/store/jobs/retention/sweep.rb +55 -0
- data/lib/textus/store/jobs/retention.rb +1 -43
- data/lib/textus/store/jobs/sweep.rb +2 -2
- data/lib/textus/store/{geometry.rb → layout.rb} +19 -3
- data/lib/textus/store.rb +53 -30
- data/lib/textus/surface/cli/runner.rb +8 -9
- data/lib/textus/surface/cli/verb/doctor.rb +3 -2
- data/lib/textus/surface/cli/verb/get.rb +5 -3
- data/lib/textus/surface/cli/verb/put.rb +5 -3
- data/lib/textus/surface/mcp/catalog.rb +26 -62
- data/lib/textus/surface/mcp/errors.rb +0 -10
- data/lib/textus/surface/mcp/projector.rb +20 -0
- data/lib/textus/surface/mcp/server.rb +20 -31
- data/lib/textus/{core → value}/duration.rb +1 -4
- data/lib/textus/value/envelope.rb +5 -4
- data/lib/textus/value/etag.rb +1 -1
- data/lib/textus/value/payload.rb +7 -0
- data/lib/textus/value/result.rb +36 -16
- data/lib/textus/verb_registry.rb +417 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus/workflow/loader.rb +1 -1
- data/lib/textus/workflow/runner.rb +10 -18
- data/lib/textus.rb +0 -64
- metadata +70 -70
- data/lib/textus/action/accept.rb +0 -46
- data/lib/textus/action/audit.rb +0 -94
- data/lib/textus/action/base.rb +0 -42
- data/lib/textus/action/blame.rb +0 -79
- data/lib/textus/action/boot.rb +0 -15
- data/lib/textus/action/data_mv.rb +0 -58
- data/lib/textus/action/deps.rb +0 -19
- data/lib/textus/action/doctor.rb +0 -17
- data/lib/textus/action/drain.rb +0 -31
- data/lib/textus/action/enqueue.rb +0 -37
- data/lib/textus/action/get.rb +0 -34
- data/lib/textus/action/ingest.rb +0 -199
- data/lib/textus/action/jobs.rb +0 -27
- data/lib/textus/action/key_delete.rb +0 -26
- data/lib/textus/action/key_delete_prefix.rb +0 -35
- data/lib/textus/action/key_mv.rb +0 -122
- data/lib/textus/action/key_mv_prefix.rb +0 -48
- data/lib/textus/action/list.rb +0 -28
- data/lib/textus/action/propose.rb +0 -42
- data/lib/textus/action/published.rb +0 -22
- data/lib/textus/action/pulse.rb +0 -49
- data/lib/textus/action/put.rb +0 -38
- data/lib/textus/action/rdeps.rb +0 -24
- data/lib/textus/action/reject.rb +0 -28
- data/lib/textus/action/rule_explain.rb +0 -81
- data/lib/textus/action/rule_lint.rb +0 -62
- data/lib/textus/action/rule_list.rb +0 -38
- data/lib/textus/action/schema_envelope.rb +0 -22
- data/lib/textus/action/uid.rb +0 -19
- data/lib/textus/action/where.rb +0 -21
- data/lib/textus/contract/arg.rb +0 -10
- data/lib/textus/contract/dsl.rb +0 -88
- data/lib/textus/contract/spec.rb +0 -25
- data/lib/textus/contract.rb +0 -12
- data/lib/textus/core/freshness/evaluator.rb +0 -150
- data/lib/textus/core/freshness.rb +0 -11
- data/lib/textus/core/retention/sweep.rb +0 -57
- data/lib/textus/core/retention.rb +0 -11
- data/lib/textus/format/shared.rb +0 -17
- data/lib/textus/gate/auth.rb +0 -212
- data/lib/textus/gate.rb +0 -92
- data/lib/textus/meta.rb +0 -54
- data/lib/textus/schemas.rb +0 -54
- data/lib/textus/store/compositor.rb +0 -34
- data/lib/textus/store/session.rb +0 -37
- data/lib/textus/surface/projector.rb +0 -27
- data/lib/textus/surface/role_scope.rb +0 -34
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Dispatch
|
|
5
|
+
module Middleware
|
|
6
|
+
# Shadows successful write operations into the SQLite audit_events table
|
|
7
|
+
# synchronously after dispatch, without the command knowing. Read verbs
|
|
8
|
+
# and failed writes pass through unchanged.
|
|
9
|
+
class AuditIndex < Base
|
|
10
|
+
middleware_name :audit_index
|
|
11
|
+
|
|
12
|
+
INDEXED_CONTRACTS = [
|
|
13
|
+
Contracts::PutEntry,
|
|
14
|
+
Contracts::DeleteKey,
|
|
15
|
+
Contracts::MoveKey,
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
def initialize(job_store:, audit_log:)
|
|
19
|
+
super()
|
|
20
|
+
@job_store = job_store
|
|
21
|
+
@audit_log = audit_log
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def call(container:, command:, call:, next_handler:) # rubocop:disable Lint/UnusedMethodArgument
|
|
25
|
+
result = next_handler.call(command, call)
|
|
26
|
+
return result unless result.success? && INDEXED_CONTRACTS.include?(command.class)
|
|
27
|
+
|
|
28
|
+
key = command.respond_to?(:key) ? command.key : nil
|
|
29
|
+
return result unless key
|
|
30
|
+
|
|
31
|
+
seq = @audit_log.latest_seq
|
|
32
|
+
verb = command.class.name.split("::").last
|
|
33
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
34
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
35
|
+
.downcase
|
|
36
|
+
|
|
37
|
+
@job_store.insert_audit_event(
|
|
38
|
+
seq: seq,
|
|
39
|
+
ts: Time.now.utc.iso8601,
|
|
40
|
+
role: call.role,
|
|
41
|
+
verb: verb,
|
|
42
|
+
key: key,
|
|
43
|
+
etag_before: nil,
|
|
44
|
+
etag_after: result.value.is_a?(Hash) ? result.value["etag"] : nil,
|
|
45
|
+
)
|
|
46
|
+
result
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Dispatch
|
|
3
|
+
module Middleware
|
|
4
|
+
class Auth < Base
|
|
5
|
+
middleware_name :auth
|
|
6
|
+
|
|
7
|
+
def call(container:, command:, call:, next_handler:)
|
|
8
|
+
verb = VerbRegistry.contract_to_verb!(command.class).to_sym
|
|
9
|
+
key = key_for(command)
|
|
10
|
+
|
|
11
|
+
rule_preds = key ? rule_declared_predicates(verb, container.manifest, key) : []
|
|
12
|
+
|
|
13
|
+
Manifest::Policy::Predicates.evaluate(
|
|
14
|
+
manifest: container.manifest, schemas: container.schemas,
|
|
15
|
+
action: verb, actor: call.role, key: key,
|
|
16
|
+
rule_predicates: rule_preds
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
next_handler.call(command, call)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def rule_declared_predicates(verb, manifest, key)
|
|
25
|
+
guard_map = manifest.rules.for(key).guard
|
|
26
|
+
return [] if guard_map.nil?
|
|
27
|
+
|
|
28
|
+
Array(guard_map[verb.to_s])
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def key_for(command)
|
|
32
|
+
if command.respond_to?(:key) then command.key
|
|
33
|
+
elsif command.respond_to?(:old_key) then command.old_key
|
|
34
|
+
elsif command.respond_to?(:pending_key) then command.pending_key
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Dispatch
|
|
3
|
+
module Middleware
|
|
4
|
+
class Base
|
|
5
|
+
def self.inherited(subclass)
|
|
6
|
+
super
|
|
7
|
+
subclass.instance_variable_set(:@middleware_name, nil)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def middleware_name(name = nil)
|
|
12
|
+
if name
|
|
13
|
+
@middleware_name = name.to_s
|
|
14
|
+
else
|
|
15
|
+
@middleware_name || name.split("::").last.downcase
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def call(container:, command:, call:, next_handler:)
|
|
21
|
+
raise NotImplementedError
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Dispatch
|
|
3
|
+
module Middleware
|
|
4
|
+
class Binder < Base
|
|
5
|
+
middleware_name :binder
|
|
6
|
+
|
|
7
|
+
def call(container:, command:, call:, next_handler:) # rubocop:disable Lint/UnusedMethodArgument
|
|
8
|
+
return next_handler.call(command, call) unless command.is_a?(Dispatch::Binder::Pending)
|
|
9
|
+
|
|
10
|
+
spec = command.spec
|
|
11
|
+
contract_class = VerbRegistry.contract_class_for(spec.verb) or
|
|
12
|
+
raise Textus::UsageError.new("unknown command verb: #{spec.verb}")
|
|
13
|
+
resolved = Dispatch::Binder.bind(spec, command.inputs)
|
|
14
|
+
built = Dispatch::Pipeline.build_command(contract_class, resolved)
|
|
15
|
+
next_handler.call(built, call)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Dispatch
|
|
3
|
+
module Middleware
|
|
4
|
+
class Cascade < Base
|
|
5
|
+
middleware_name :cascade
|
|
6
|
+
|
|
7
|
+
CASCADE_VERBS = %i[put propose accept reject key_mv key_delete].freeze
|
|
8
|
+
|
|
9
|
+
TRIGGER_TYPE_MAP = {
|
|
10
|
+
Contracts::PutEntry => "entry.written",
|
|
11
|
+
Contracts::ProposeEntry => "entry.written",
|
|
12
|
+
Contracts::DeleteKey => "entry.deleted",
|
|
13
|
+
Contracts::MoveKey => "entry.moved",
|
|
14
|
+
Contracts::AcceptProposal => "proposal.accepted",
|
|
15
|
+
Contracts::RejectProposal => "proposal.rejected",
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
def call(container:, command:, call:, next_handler:)
|
|
19
|
+
result = next_handler.call(command, call)
|
|
20
|
+
return result unless result.success? && cascadable?(command)
|
|
21
|
+
|
|
22
|
+
key = cascade_key(command)
|
|
23
|
+
return result unless key
|
|
24
|
+
|
|
25
|
+
trigger_type = TRIGGER_TYPE_MAP[command.class]
|
|
26
|
+
jobs = Textus::Store::Jobs::Planner.new(container: container).plan(
|
|
27
|
+
trigger: { "type" => trigger_type, "target" => key },
|
|
28
|
+
role: call.role,
|
|
29
|
+
)
|
|
30
|
+
queue = Textus::Store::Jobs::Queue.new(store: container.job_store)
|
|
31
|
+
jobs.each { |j| queue.enqueue(j) }
|
|
32
|
+
result
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def cascadable?(command)
|
|
38
|
+
CASCADE_VERBS.include?(VerbRegistry.contract_to_verb!(command.class).to_sym)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def cascade_key(command)
|
|
42
|
+
case command
|
|
43
|
+
when Contracts::PutEntry, Contracts::DeleteKey then command.key
|
|
44
|
+
when Contracts::MoveKey then command.new_key
|
|
45
|
+
when Contracts::AcceptProposal,
|
|
46
|
+
Contracts::RejectProposal then command.pending_key
|
|
47
|
+
when Contracts::ProposeEntry then command.key
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Dispatch
|
|
3
|
+
class Pipeline
|
|
4
|
+
attr_reader :container
|
|
5
|
+
|
|
6
|
+
def initialize(registry:, container:, middleware: [])
|
|
7
|
+
@registry = registry
|
|
8
|
+
@middleware = middleware
|
|
9
|
+
@container = container
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def dispatch(command, call:)
|
|
13
|
+
stack = @middleware.reverse.reduce(->(cmd, c) { execute(cmd, c) }) do |next_mw, mw|
|
|
14
|
+
->(cmd, c) { mw.call(container: @container, command: cmd, call: c, next_handler: next_mw) }
|
|
15
|
+
end
|
|
16
|
+
stack.call(command, call)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.build_command(contract_class, inputs)
|
|
20
|
+
members = contract_class.members
|
|
21
|
+
kwargs = members.to_h do |member|
|
|
22
|
+
[member, inputs[member]]
|
|
23
|
+
end
|
|
24
|
+
contract_class.new(**kwargs)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def execute(command, call)
|
|
30
|
+
handler = @registry.for(command.class)
|
|
31
|
+
handler.call(command, call)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -3,7 +3,7 @@ module Textus
|
|
|
3
3
|
class Check
|
|
4
4
|
class AuditLog < Check
|
|
5
5
|
def call
|
|
6
|
-
path = Textus::Store::
|
|
6
|
+
path = Textus::Store::Layout.new(root).audit_log_path
|
|
7
7
|
Textus::Port::AuditLog.new(root).verify_integrity.map do |v|
|
|
8
8
|
{
|
|
9
9
|
"code" => "audit.parse_error",
|
|
@@ -8,12 +8,12 @@ module Textus
|
|
|
8
8
|
# verb reported.
|
|
9
9
|
class GeneratorDrift < Check
|
|
10
10
|
def call
|
|
11
|
-
|
|
11
|
+
detector = Textus::Store::Freshness::DriftDetector.new(
|
|
12
12
|
manifest: manifest,
|
|
13
13
|
file_stat: Textus::Port::Storage::FileStat.new,
|
|
14
14
|
clock: Textus::Port::Clock.new,
|
|
15
15
|
)
|
|
16
|
-
manifest.data.entries.flat_map { |m|
|
|
16
|
+
manifest.data.entries.flat_map { |m| detector.drift_rows(m) }.map do |row|
|
|
17
17
|
{
|
|
18
18
|
"code" => "generator_drift",
|
|
19
19
|
"level" => "warning",
|
|
@@ -8,7 +8,7 @@ module Textus
|
|
|
8
8
|
# that drift without making `build` scan globally.
|
|
9
9
|
class OrphanedPublishTargets < Check
|
|
10
10
|
def call
|
|
11
|
-
sdir = Textus::Store::
|
|
11
|
+
sdir = Textus::Store::Layout.new(root).sentinels_root
|
|
12
12
|
return [] unless File.directory?(sdir)
|
|
13
13
|
|
|
14
14
|
repo_root = File.dirname(root)
|
|
@@ -4,7 +4,7 @@ module Textus
|
|
|
4
4
|
class SchemaViolations < Check
|
|
5
5
|
def call
|
|
6
6
|
result = Textus::Doctor::Validator.new(
|
|
7
|
-
reader: ->(key, ctnr,
|
|
7
|
+
reader: ->(key, ctnr, _c) { Textus::Store::Entry::Reader.from(container: ctnr).read(key) },
|
|
8
8
|
manifest: @container.manifest,
|
|
9
9
|
audit_log: @container.audit_log,
|
|
10
10
|
schema_for: ->(name) { @container.schemas.fetch_or_nil(name) },
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
module Textus
|
|
2
2
|
module Doctor
|
|
3
3
|
class Check
|
|
4
|
-
class
|
|
4
|
+
class ScratchpadSources < Check
|
|
5
5
|
def call
|
|
6
6
|
issues = []
|
|
7
7
|
manifest.resolver.enumerate.each do |row|
|
|
8
|
-
next unless row[:key].start_with?("
|
|
8
|
+
next unless row[:key].start_with?("scratchpad.notes.")
|
|
9
9
|
next unless row[:path] && File.exist?(row[:path])
|
|
10
10
|
|
|
11
11
|
sources = parse_sources(row[:path])
|
|
@@ -13,10 +13,10 @@ module Textus
|
|
|
13
13
|
next if raw_entry_exists?(raw_key)
|
|
14
14
|
|
|
15
15
|
issues << {
|
|
16
|
-
"code" => "
|
|
16
|
+
"code" => "scratchpad.source_missing",
|
|
17
17
|
"level" => "warning",
|
|
18
18
|
"subject" => row[:key],
|
|
19
|
-
"message" => "
|
|
19
|
+
"message" => "scratchpad entry '#{row[:key]}' references raw key '#{raw_key}' " \
|
|
20
20
|
"which does not exist in the store",
|
|
21
21
|
"fix" => "re-ingest the source: textus ingest ..., or remove the stale sources: entry",
|
|
22
22
|
}
|
|
@@ -33,7 +33,12 @@ module Textus
|
|
|
33
33
|
return [] unless match
|
|
34
34
|
|
|
35
35
|
front = YAML.safe_load(match[1])
|
|
36
|
-
Array(front&.dig("sources"))
|
|
36
|
+
Array(front&.dig("sources")).filter_map do |s|
|
|
37
|
+
case s
|
|
38
|
+
when String then s
|
|
39
|
+
when Hash then s["key"]
|
|
40
|
+
end
|
|
41
|
+
end
|
|
37
42
|
rescue StandardError
|
|
38
43
|
[]
|
|
39
44
|
end
|
|
@@ -5,7 +5,7 @@ module Textus
|
|
|
5
5
|
def call
|
|
6
6
|
store = Textus::Port::SentinelStore.new
|
|
7
7
|
file_stat = Textus::Port::Storage::FileStat.new
|
|
8
|
-
dir = Textus::Store::
|
|
8
|
+
dir = Textus::Store::Layout.new(root).sentinels_root
|
|
9
9
|
return [] unless file_stat.directory?(dir)
|
|
10
10
|
|
|
11
11
|
repo_root = File.dirname(root)
|
data/lib/textus/doctor/check.rb
CHANGED
|
@@ -26,15 +26,17 @@ module Textus
|
|
|
26
26
|
protected
|
|
27
27
|
|
|
28
28
|
def root = @container.root
|
|
29
|
-
def geometry = @container.
|
|
29
|
+
def geometry = @container.layout
|
|
30
30
|
def manifest = @container.manifest
|
|
31
31
|
|
|
32
|
-
# Dispatch a verb through Gate.
|
|
33
32
|
def dispatch(verb, *args, **kwargs)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
contract_class = Textus::VerbRegistry.contract_class_for(verb)
|
|
34
|
+
members = contract_class.members
|
|
35
|
+
command_kwargs = members.each_with_index.to_h { |m, i| [m, args[i] || kwargs[m]] }
|
|
36
|
+
command = contract_class.new(**command_kwargs)
|
|
37
|
+
call = Textus::Value::Call.build(role: @role)
|
|
38
|
+
result = @container.pipeline.dispatch(command, call: call)
|
|
39
|
+
Textus::Value::Result.extract(result)
|
|
38
40
|
end
|
|
39
41
|
end
|
|
40
42
|
end
|
data/lib/textus/doctor.rb
CHANGED
data/lib/textus/errors.rb
CHANGED
data/lib/textus/format/base.rb
CHANGED
|
@@ -25,14 +25,46 @@ module Textus
|
|
|
25
25
|
raise NotImplementedError.new("#{name}.nested_glob not implemented")
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
def self.validate_path_extension(
|
|
29
|
-
|
|
28
|
+
def self.validate_path_extension(path, nested)
|
|
29
|
+
ext = File.extname(path)
|
|
30
|
+
if nested
|
|
31
|
+
return if ext == ""
|
|
32
|
+
|
|
33
|
+
raise UsageError.new("#{format_name} nested path must not have an extension")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
return if extensions.include?(ext)
|
|
37
|
+
|
|
38
|
+
raise UsageError.new("#{format_name} format requires '#{extensions.join("' or '")}' path (got #{ext.inspect})")
|
|
30
39
|
end
|
|
31
40
|
|
|
32
|
-
def self.enforce_name_match!(
|
|
33
|
-
|
|
41
|
+
def self.enforce_name_match!(path, meta)
|
|
42
|
+
return unless meta.is_a?(Hash) && meta["name"]
|
|
43
|
+
|
|
44
|
+
ext = extensions.first
|
|
45
|
+
basename = File.basename(path, ext)
|
|
46
|
+
return if meta["name"] == basename
|
|
47
|
+
|
|
48
|
+
raise BadFrontmatter.new(path, "name '#{meta["name"]}' does not match basename '#{basename}'")
|
|
34
49
|
end
|
|
35
50
|
|
|
51
|
+
def self.rewrite_name(path, basename) # rubocop:disable Naming/PredicateMethod
|
|
52
|
+
raw = File.binread(path)
|
|
53
|
+
parsed = parse(raw, path: path)
|
|
54
|
+
meta = parsed["_meta"] || {}
|
|
55
|
+
return false unless meta.is_a?(Hash) && meta["name"].is_a?(String) && meta["name"] != basename
|
|
56
|
+
|
|
57
|
+
new_meta = meta.merge("name" => basename)
|
|
58
|
+
File.binwrite(path, serialize(meta: new_meta, body: parsed["body"] || "", content: parsed["content"]))
|
|
59
|
+
true
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.format_name
|
|
63
|
+
name.split("::").last.downcase
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.validate_raw_entry!(_parsed, _lane); end
|
|
67
|
+
|
|
36
68
|
def self.serialize_for_put(meta:, body:, content:, path:)
|
|
37
69
|
_ = meta
|
|
38
70
|
_ = body
|
|
@@ -40,10 +72,6 @@ module Textus
|
|
|
40
72
|
_ = path
|
|
41
73
|
raise NotImplementedError.new("#{name}.serialize_for_put not implemented")
|
|
42
74
|
end
|
|
43
|
-
|
|
44
|
-
def self.rewrite_name(_path, _basename)
|
|
45
|
-
raise NotImplementedError.new("#{name}.rewrite_name not implemented")
|
|
46
|
-
end
|
|
47
75
|
end
|
|
48
76
|
end
|
|
49
77
|
end
|
data/lib/textus/format/json.rb
CHANGED
|
@@ -59,27 +59,6 @@ module Textus
|
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
-
def self.rewrite_name(path, basename) # rubocop:disable Naming/PredicateMethod
|
|
63
|
-
raw = File.binread(path)
|
|
64
|
-
parsed = parse(raw, path: path)
|
|
65
|
-
meta = parsed["_meta"]
|
|
66
|
-
return false unless meta.is_a?(Hash) && meta["name"].is_a?(String) && meta["name"] != basename
|
|
67
|
-
|
|
68
|
-
new_meta = meta.merge("name" => basename)
|
|
69
|
-
File.binwrite(path, serialize(meta: new_meta, body: "", content: parsed["content"]))
|
|
70
|
-
true
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def self.enforce_name_match!(path, meta)
|
|
74
|
-
return unless meta.is_a?(Hash) && meta["name"]
|
|
75
|
-
|
|
76
|
-
ext = extensions.first
|
|
77
|
-
basename = File.basename(path, ext)
|
|
78
|
-
return if meta["name"] == basename
|
|
79
|
-
|
|
80
|
-
raise BadFrontmatter.new(path, "name '#{meta["name"]}' does not match basename '#{basename}'")
|
|
81
|
-
end
|
|
82
|
-
|
|
83
62
|
def self.validate_path_extension(path, nested)
|
|
84
63
|
ext = File.extname(path)
|
|
85
64
|
if nested
|
|
@@ -43,27 +43,6 @@ module Textus
|
|
|
43
43
|
[bytes, meta, body.to_s, nil]
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
def self.rewrite_name(path, basename) # rubocop:disable Naming/PredicateMethod
|
|
47
|
-
raw = File.binread(path)
|
|
48
|
-
parsed = parse(raw, path: path)
|
|
49
|
-
meta = parsed["_meta"] || {}
|
|
50
|
-
return false unless meta.is_a?(Hash) && meta["name"].is_a?(String) && meta["name"] != basename
|
|
51
|
-
|
|
52
|
-
new_meta = meta.merge("name" => basename)
|
|
53
|
-
File.binwrite(path, serialize(meta: new_meta, body: parsed["body"]))
|
|
54
|
-
true
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def self.enforce_name_match!(path, meta)
|
|
58
|
-
return unless meta.is_a?(Hash) && meta["name"]
|
|
59
|
-
|
|
60
|
-
ext = extensions.first
|
|
61
|
-
basename = File.basename(path, ext)
|
|
62
|
-
return if meta["name"] == basename
|
|
63
|
-
|
|
64
|
-
raise BadFrontmatter.new(path, "name '#{meta["name"]}' does not match basename '#{basename}'")
|
|
65
|
-
end
|
|
66
|
-
|
|
67
46
|
def self.validate_path_extension(path, _nested)
|
|
68
47
|
ext = File.extname(path)
|
|
69
48
|
return if ["", ".md"].include?(ext)
|
data/lib/textus/format/yaml.rb
CHANGED
|
@@ -82,27 +82,6 @@ module Textus
|
|
|
82
82
|
end
|
|
83
83
|
end
|
|
84
84
|
|
|
85
|
-
def self.rewrite_name(path, basename) # rubocop:disable Naming/PredicateMethod
|
|
86
|
-
raw = File.binread(path)
|
|
87
|
-
parsed = parse(raw, path: path)
|
|
88
|
-
meta = parsed["_meta"]
|
|
89
|
-
return false unless meta.is_a?(Hash) && meta["name"].is_a?(String) && meta["name"] != basename
|
|
90
|
-
|
|
91
|
-
new_meta = meta.merge("name" => basename)
|
|
92
|
-
File.binwrite(path, serialize(meta: new_meta, body: "", content: parsed["content"]))
|
|
93
|
-
true
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def self.enforce_name_match!(path, meta)
|
|
97
|
-
return unless meta.is_a?(Hash) && meta["name"]
|
|
98
|
-
|
|
99
|
-
ext = extensions.first
|
|
100
|
-
basename = File.basename(path, ext)
|
|
101
|
-
return if meta["name"] == basename
|
|
102
|
-
|
|
103
|
-
raise BadFrontmatter.new(path, "name '#{meta["name"]}' does not match basename '#{basename}'")
|
|
104
|
-
end
|
|
105
|
-
|
|
106
85
|
def self.validate_path_extension(path, nested)
|
|
107
86
|
ext = File.extname(path)
|
|
108
87
|
if nested
|
data/lib/textus/format.rb
CHANGED
|
@@ -9,6 +9,18 @@ module Textus
|
|
|
9
9
|
"text" => -> { Format::Text },
|
|
10
10
|
}.freeze
|
|
11
11
|
|
|
12
|
+
# Optional registry for injectable format strategies. Tests or app
|
|
13
|
+
# initializers can set Format.registry = { "custom" => -> { MyFormat }}
|
|
14
|
+
@registry = nil
|
|
15
|
+
|
|
16
|
+
def self.registry=(reg)
|
|
17
|
+
@registry = reg
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.registry
|
|
21
|
+
@registry
|
|
22
|
+
end
|
|
23
|
+
|
|
12
24
|
EXT_TO_FORMAT = {
|
|
13
25
|
".md" => "markdown",
|
|
14
26
|
".json" => "json",
|
|
@@ -18,7 +30,10 @@ module Textus
|
|
|
18
30
|
}.freeze
|
|
19
31
|
|
|
20
32
|
def self.for(format)
|
|
21
|
-
|
|
33
|
+
key = format.to_s
|
|
34
|
+
return registry.fetch(key).call if registry&.key?(key)
|
|
35
|
+
|
|
36
|
+
STRATEGIES.fetch(key) { raise Textus::UsageError.new("unknown entry format: #{format.inspect}") }.call
|
|
22
37
|
end
|
|
23
38
|
|
|
24
39
|
def self.infer_from_extension(ext)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Handlers
|
|
3
|
+
module Maintenance
|
|
4
|
+
class BootStore
|
|
5
|
+
def initialize(container:)
|
|
6
|
+
@container = container
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def call(_command, _call)
|
|
10
|
+
Value::Result.success(Textus::Boot.build(container: @container))
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Handlers
|
|
3
|
+
module Maintenance
|
|
4
|
+
class DoctorStore
|
|
5
|
+
def initialize(container:)
|
|
6
|
+
@container = container
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def call(command, call)
|
|
10
|
+
Value::Result.success(Textus::Doctor.build(container: @container, checks: command.checks, role: call.role))
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Handlers
|
|
3
|
+
module Maintenance
|
|
4
|
+
class DrainStore
|
|
5
|
+
def initialize(container:, job_store:)
|
|
6
|
+
@container = container
|
|
7
|
+
@job_store = job_store
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def call(_command, call)
|
|
11
|
+
queue = Textus::Store::Jobs::Queue.new(store: @job_store)
|
|
12
|
+
Textus::Store::Jobs::Planner.seed(container: @container, queue: queue, role: call.role)
|
|
13
|
+
queue.reclaim(now: Textus::Port::Clock.new.now)
|
|
14
|
+
summary = Textus::Store::Jobs::Worker.for(container: @container, queue: queue).drain
|
|
15
|
+
Value::Result.success("protocol" => Textus::PROTOCOL, "ok" => summary.failed.zero?,
|
|
16
|
+
"completed" => summary.completed, "failed" => summary.failed)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|