textus 0.53.0 → 0.54.1
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 +52 -0
- data/README.md +53 -73
- data/SPEC.md +2 -5
- data/docs/architecture/README.md +149 -159
- data/lib/textus/action/accept.rb +0 -9
- data/lib/textus/action/audit.rb +0 -2
- data/lib/textus/action/blame.rb +0 -2
- data/lib/textus/action/boot.rb +2 -11
- data/lib/textus/action/data_mv.rb +1 -3
- data/lib/textus/action/deps.rb +1 -15
- data/lib/textus/action/doctor.rb +0 -2
- data/lib/textus/action/drain.rb +4 -6
- data/lib/textus/action/enqueue.rb +3 -5
- data/lib/textus/action/get.rb +1 -3
- data/lib/textus/action/ingest.rb +135 -0
- data/lib/textus/action/jobs.rb +1 -3
- data/lib/textus/action/key_delete.rb +0 -10
- data/lib/textus/action/key_delete_prefix.rb +1 -3
- data/lib/textus/action/key_mv.rb +1 -15
- data/lib/textus/action/key_mv_prefix.rb +1 -3
- data/lib/textus/action/list.rb +0 -2
- data/lib/textus/action/propose.rb +0 -2
- data/lib/textus/action/published.rb +0 -2
- data/lib/textus/action/pulse.rb +7 -38
- data/lib/textus/action/put.rb +1 -12
- data/lib/textus/action/rdeps.rb +2 -12
- data/lib/textus/action/reject.rb +0 -12
- data/lib/textus/action/rule_explain.rb +0 -4
- data/lib/textus/action/rule_lint.rb +1 -3
- data/lib/textus/action/rule_list.rb +0 -4
- data/lib/textus/action/schema_envelope.rb +0 -2
- data/lib/textus/action/uid.rb +0 -2
- data/lib/textus/action/where.rb +0 -2
- data/lib/textus/action/write_verb.rb +4 -18
- data/lib/textus/boot.rb +40 -109
- data/lib/textus/command.rb +6 -1
- data/lib/textus/container.rb +21 -5
- data/lib/textus/core/freshness/evaluator.rb +18 -34
- data/lib/textus/core/freshness/verdict.rb +1 -1
- data/lib/textus/doctor/check/notebook_sources.rb +50 -0
- data/lib/textus/doctor/check/protocol_version.rb +6 -6
- data/lib/textus/doctor/check/raw_asset_paths.rb +50 -0
- data/lib/textus/doctor/check/stale_reviewed_stamp.rb +54 -0
- data/lib/textus/doctor/check.rb +0 -1
- data/lib/textus/doctor.rb +3 -42
- data/lib/textus/envelope/reader.rb +46 -0
- data/lib/textus/envelope/writer.rb +209 -0
- data/lib/textus/envelope.rb +19 -6
- data/lib/textus/errors.rb +9 -5
- data/lib/textus/{entry → format}/base.rb +1 -6
- data/lib/textus/{entry → format}/json.rb +1 -4
- data/lib/textus/{entry → format}/markdown.rb +2 -6
- data/lib/textus/{entry → format}/text.rb +2 -6
- data/lib/textus/{entry → format}/yaml.rb +1 -4
- data/lib/textus/{entry.rb → format.rb} +9 -10
- data/lib/textus/gate/auth.rb +58 -12
- data/lib/textus/gate.rb +2 -0
- data/lib/textus/init.rb +16 -79
- data/lib/textus/jobs/base.rb +23 -0
- data/lib/textus/jobs/materialize.rb +20 -0
- data/lib/textus/{background → jobs}/plan.rb +1 -1
- data/lib/textus/jobs/planner.rb +101 -0
- data/lib/textus/jobs/retention.rb +48 -0
- data/lib/textus/jobs/sweep.rb +27 -0
- data/lib/textus/{background → jobs}/worker.rb +2 -2
- data/lib/textus/jobs.rb +15 -0
- data/lib/textus/key/path.rb +1 -1
- data/lib/textus/manifest/entry/base.rb +4 -19
- data/lib/textus/manifest/entry/parser.rb +27 -10
- data/lib/textus/manifest/entry/produced.rb +13 -34
- data/lib/textus/manifest/entry/publish/to_paths.rb +4 -4
- data/lib/textus/manifest/entry/publish.rb +0 -12
- data/lib/textus/manifest/entry/validators/format_matrix.rb +1 -1
- data/lib/textus/manifest/entry/validators.rb +0 -1
- data/lib/textus/manifest/policy/source.rb +14 -62
- data/lib/textus/manifest/policy.rb +2 -5
- data/lib/textus/manifest/resolver.rb +2 -2
- data/lib/textus/manifest/schema/contract.rb +61 -0
- data/lib/textus/manifest/schema/keys.rb +6 -20
- data/lib/textus/manifest/schema/semantics.rb +232 -0
- data/lib/textus/manifest/schema/validator.rb +24 -302
- data/lib/textus/manifest/schema/vocabulary.rb +1 -0
- data/lib/textus/manifest/schema.rb +1 -4
- data/lib/textus/manifest.rb +1 -3
- data/lib/textus/ports/{queue → job_store}/job.rb +1 -1
- data/lib/textus/ports/{queue.rb → job_store.rb} +6 -13
- data/lib/textus/produce/engine.rb +50 -0
- data/lib/textus/produce/render.rb +20 -0
- data/lib/textus/session.rb +15 -8
- data/lib/textus/store.rb +5 -14
- data/lib/textus/surfaces/mcp/catalog.rb +23 -11
- data/lib/textus/surfaces/mcp/errors.rb +0 -9
- data/lib/textus/surfaces/mcp/server.rb +87 -110
- data/lib/textus/surfaces/role_scope.rb +0 -4
- data/lib/textus/surfaces/watcher.rb +3 -3
- data/lib/textus/types.rb +15 -0
- data/lib/textus/version.rb +2 -2
- data/lib/textus/workflow/collector.rb +27 -0
- data/lib/textus/workflow/context.rb +5 -0
- data/lib/textus/workflow/dsl.rb +44 -0
- data/lib/textus/workflow/loader.rb +17 -0
- data/lib/textus/workflow/pattern.rb +18 -0
- data/lib/textus/workflow/registry.rb +21 -0
- data/lib/textus/workflow/runner.rb +99 -0
- data/lib/textus/workflow.rb +28 -0
- data/lib/textus.rb +12 -1
- metadata +67 -66
- data/lib/textus/action/pulse/scanner.rb +0 -118
- data/lib/textus/background/job/base.rb +0 -27
- data/lib/textus/background/job/materialize.rb +0 -31
- data/lib/textus/background/job/refresh.rb +0 -22
- data/lib/textus/background/job/sweep.rb +0 -31
- data/lib/textus/background/job.rb +0 -19
- data/lib/textus/background/planner/plan.rb +0 -113
- data/lib/textus/background/retention/apply.rb +0 -50
- data/lib/textus/doctor/check/handler_permit.rb +0 -34
- data/lib/textus/doctor/check/hooks.rb +0 -25
- data/lib/textus/doctor/check/intake_registration.rb +0 -46
- data/lib/textus/envelope/io/reader.rb +0 -48
- data/lib/textus/envelope/io/writer.rb +0 -211
- data/lib/textus/events.rb +0 -21
- data/lib/textus/init/templates/machine_intake.rb +0 -49
- data/lib/textus/init/templates/orientation_reducer.rb +0 -21
- data/lib/textus/manifest/entry/validators/events.rb +0 -22
- data/lib/textus/manifest/policy/handler_permit.rb +0 -17
- data/lib/textus/pipeline/acquire/handler.rb +0 -29
- data/lib/textus/pipeline/acquire/intake.rb +0 -132
- data/lib/textus/pipeline/acquire/projection.rb +0 -129
- data/lib/textus/pipeline/acquire/serializer/json.rb +0 -31
- data/lib/textus/pipeline/acquire/serializer/text.rb +0 -16
- data/lib/textus/pipeline/acquire/serializer/yaml.rb +0 -31
- data/lib/textus/pipeline/acquire/serializer.rb +0 -17
- data/lib/textus/pipeline/engine.rb +0 -97
- data/lib/textus/pipeline/render.rb +0 -25
- data/lib/textus/ports/audit_subscriber.rb +0 -42
- data/lib/textus/projection.rb +0 -103
- data/lib/textus/step/base.rb +0 -35
- data/lib/textus/step/builtin/csv_fetch.rb +0 -19
- data/lib/textus/step/builtin/ical_events_fetch.rb +0 -30
- data/lib/textus/step/builtin/json_fetch.rb +0 -18
- data/lib/textus/step/builtin/markdown_links_fetch.rb +0 -20
- data/lib/textus/step/builtin/rss_fetch.rb +0 -26
- data/lib/textus/step/builtin.rb +0 -22
- data/lib/textus/step/catalog.rb +0 -37
- data/lib/textus/step/context.rb +0 -74
- data/lib/textus/step/discovery.rb +0 -24
- data/lib/textus/step/error_log.rb +0 -32
- data/lib/textus/step/event_bus.rb +0 -113
- data/lib/textus/step/fetch.rb +0 -13
- data/lib/textus/step/fire_report.rb +0 -23
- data/lib/textus/step/loader.rb +0 -108
- data/lib/textus/step/observe.rb +0 -31
- data/lib/textus/step/registry_store.rb +0 -66
- data/lib/textus/step/signature.rb +0 -31
- data/lib/textus/step/transform.rb +0 -12
- data/lib/textus/step/validate.rb +0 -11
- data/lib/textus/step.rb +0 -10
- data/lib/textus/surfaces/mcp/session.rb +0 -9
- data/lib/textus/surfaces/mcp/tool_schemas.rb +0 -17
data/lib/textus/action/get.rb
CHANGED
|
@@ -15,8 +15,6 @@ module Textus
|
|
|
15
15
|
description: "dotted entry key to read, e.g. 'knowledge.project'"
|
|
16
16
|
view { |v, _i| v.to_h_for_wire }
|
|
17
17
|
|
|
18
|
-
BURN = :sync
|
|
19
|
-
|
|
20
18
|
def initialize(key:)
|
|
21
19
|
super()
|
|
22
20
|
@key = key
|
|
@@ -64,7 +62,7 @@ module Textus
|
|
|
64
62
|
return nil unless @file_store.exists?(path)
|
|
65
63
|
|
|
66
64
|
raw = @file_store.read(path)
|
|
67
|
-
parsed = Textus::
|
|
65
|
+
parsed = Textus::Format.for(mentry.format).parse(raw, path: path)
|
|
68
66
|
Textus::Envelope.build(
|
|
69
67
|
key: key,
|
|
70
68
|
mentry: mentry,
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "date"
|
|
5
|
+
|
|
6
|
+
module Textus
|
|
7
|
+
module Action
|
|
8
|
+
class Ingest < Base
|
|
9
|
+
extend Textus::Contract::DSL
|
|
10
|
+
|
|
11
|
+
verb :ingest
|
|
12
|
+
summary "Capture external source material into the raw lane. Write-once, agent-owned."
|
|
13
|
+
surfaces :cli, :mcp
|
|
14
|
+
arg :kind, String, required: true, positional: true,
|
|
15
|
+
description: "source kind: url | file | asset"
|
|
16
|
+
arg :slug, String, required: true,
|
|
17
|
+
description: "human slug for the key suffix (kebab-case)"
|
|
18
|
+
arg :url, String, description: "remote URL (required when kind=url)"
|
|
19
|
+
arg :path, String, description: "local file path (required when kind=file or kind=asset)"
|
|
20
|
+
arg :zone, String, description: "asset group subdirectory (required when kind=asset)"
|
|
21
|
+
arg :label, String, description: "human label stored in source.label"
|
|
22
|
+
view { |env| { "key" => env.key, "uid" => env.uid, "etag" => env.etag } }
|
|
23
|
+
|
|
24
|
+
SOURCE_KINDS = %w[url file asset].freeze
|
|
25
|
+
|
|
26
|
+
def initialize(kind:, slug:, url: nil, path: nil, zone: nil, label: nil)
|
|
27
|
+
super()
|
|
28
|
+
@kind = kind
|
|
29
|
+
@slug = slug
|
|
30
|
+
@url = url
|
|
31
|
+
@path = path
|
|
32
|
+
@zone = zone
|
|
33
|
+
@label = label
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def call(container:, call:)
|
|
37
|
+
validate_inputs!
|
|
38
|
+
|
|
39
|
+
now = Time.now.utc
|
|
40
|
+
key = derive_key(now)
|
|
41
|
+
|
|
42
|
+
Textus::Gate::Auth.new(container).check_action!(
|
|
43
|
+
action: :ingest, actor: call.role, key: key,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
write_raw_entry(key, now, container, call)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def validate_inputs!
|
|
52
|
+
unless SOURCE_KINDS.include?(@kind)
|
|
53
|
+
raise Textus::UsageError.new(
|
|
54
|
+
"ingest kind must be one of #{SOURCE_KINDS.join("|")}, got #{@kind.inspect}",
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
case @kind
|
|
58
|
+
when "url"
|
|
59
|
+
raise Textus::UsageError.new("ingest url requires --url") unless @url
|
|
60
|
+
when "file"
|
|
61
|
+
raise Textus::UsageError.new("ingest file requires --path") unless @path
|
|
62
|
+
when "asset"
|
|
63
|
+
raise Textus::UsageError.new("ingest asset requires --path") unless @path
|
|
64
|
+
raise Textus::UsageError.new("ingest asset requires --zone") unless @zone
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def derive_key(now)
|
|
69
|
+
date = now.strftime("%Y.%m.%d")
|
|
70
|
+
"raw.#{date}.#{@kind}-#{@slug}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def write_raw_entry(key, now, container, call)
|
|
74
|
+
ts = now.iso8601
|
|
75
|
+
mentry = container.manifest.resolver.resolve(key).entry
|
|
76
|
+
writer = Textus::Envelope::Writer.from(container: container, call: call)
|
|
77
|
+
|
|
78
|
+
case @kind
|
|
79
|
+
when "url"
|
|
80
|
+
structured = {
|
|
81
|
+
"ingested_at" => ts,
|
|
82
|
+
"source" => { "kind" => "url", "url" => @url, "label" => @label || @url },
|
|
83
|
+
"body" => nil,
|
|
84
|
+
}
|
|
85
|
+
writer.put(key, mentry: mentry,
|
|
86
|
+
payload: Textus::Envelope::Writer::Payload.new(
|
|
87
|
+
meta: nil, body: nil, content: structured,
|
|
88
|
+
))
|
|
89
|
+
when "file"
|
|
90
|
+
body_content = File.read(@path)
|
|
91
|
+
structured = {
|
|
92
|
+
"ingested_at" => ts,
|
|
93
|
+
"source" => { "kind" => "file", "path" => @path,
|
|
94
|
+
"label" => @label || File.basename(@path) },
|
|
95
|
+
"body" => body_content,
|
|
96
|
+
}
|
|
97
|
+
writer.put(key, mentry: mentry,
|
|
98
|
+
payload: Textus::Envelope::Writer::Payload.new(
|
|
99
|
+
meta: nil, body: nil, content: structured,
|
|
100
|
+
))
|
|
101
|
+
when "asset"
|
|
102
|
+
asset_rel = copy_asset_file(container, now)
|
|
103
|
+
structured = {
|
|
104
|
+
"ingested_at" => ts,
|
|
105
|
+
"source" => { "kind" => "asset",
|
|
106
|
+
"label" => @label || File.basename(@path) },
|
|
107
|
+
"asset" => asset_rel,
|
|
108
|
+
"body" => nil,
|
|
109
|
+
}
|
|
110
|
+
writer.put(key, mentry: mentry,
|
|
111
|
+
payload: Textus::Envelope::Writer::Payload.new(
|
|
112
|
+
meta: nil, body: nil, content: structured,
|
|
113
|
+
))
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def copy_asset_file(container, now)
|
|
118
|
+
date_path = now.strftime("%Y/%m/%d")
|
|
119
|
+
filename = File.basename(@path)
|
|
120
|
+
assets_dir = File.join(container.root, "assets", "raw", date_path, @zone)
|
|
121
|
+
FileUtils.mkdir_p(assets_dir)
|
|
122
|
+
FileUtils.cp(@path, File.join(assets_dir, filename))
|
|
123
|
+
create_gitignore_sentinel(container)
|
|
124
|
+
"raw/#{date_path}/#{@zone}/#{filename}"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def create_gitignore_sentinel(container)
|
|
128
|
+
assets_root = File.join(container.root, "assets")
|
|
129
|
+
FileUtils.mkdir_p(assets_root)
|
|
130
|
+
sentinel = File.join(assets_root, ".gitignore")
|
|
131
|
+
File.write(sentinel, "*\n") unless File.exist?(sentinel)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
data/lib/textus/action/jobs.rb
CHANGED
|
@@ -13,8 +13,6 @@ module Textus
|
|
|
13
13
|
arg :action, String, default: nil, description: "retry|purge (optional)"
|
|
14
14
|
arg :job_id, String, default: nil, description: "job id (required for action=retry)"
|
|
15
15
|
|
|
16
|
-
BURN = :sync
|
|
17
|
-
|
|
18
16
|
def initialize(state: "ready", action: nil, job_id: nil)
|
|
19
17
|
super()
|
|
20
18
|
@state = state
|
|
@@ -23,7 +21,7 @@ module Textus
|
|
|
23
21
|
end
|
|
24
22
|
|
|
25
23
|
def call(container:, **)
|
|
26
|
-
queue = Textus::Ports::
|
|
24
|
+
queue = Textus::Ports::JobStore.new(root: container.root)
|
|
27
25
|
case @action
|
|
28
26
|
when "retry"
|
|
29
27
|
queue.retry_failed(@job_id)
|
|
@@ -15,8 +15,6 @@ module Textus
|
|
|
15
15
|
arg :if_etag, String,
|
|
16
16
|
description: "optimistic-concurrency guard: the etag you last read; the delete is rejected if the entry changed since"
|
|
17
17
|
|
|
18
|
-
BURN = :sync
|
|
19
|
-
|
|
20
18
|
def initialize(key:, if_etag: nil)
|
|
21
19
|
super()
|
|
22
20
|
@key = key
|
|
@@ -28,16 +26,8 @@ module Textus
|
|
|
28
26
|
Textus::Manifest::Data.validate_key!(@key)
|
|
29
27
|
mentry = container.manifest.resolver.resolve(@key).entry
|
|
30
28
|
|
|
31
|
-
auth(container).check_action!(action: :key_delete, actor: call.role, key: @key, extra: { if_etag: @if_etag })
|
|
32
|
-
|
|
33
29
|
writer(container, call).delete(@key, mentry:, if_etag: @if_etag)
|
|
34
30
|
|
|
35
|
-
container.steps.publish(
|
|
36
|
-
:entry_deleted,
|
|
37
|
-
ctx: Textus::Step::Context.for(container: container, call: call),
|
|
38
|
-
key: @key,
|
|
39
|
-
)
|
|
40
|
-
|
|
41
31
|
{ "protocol" => Textus::PROTOCOL, "ok" => true, "key" => @key, "deleted" => true }
|
|
42
32
|
end
|
|
43
33
|
end
|
|
@@ -16,8 +16,6 @@ module Textus
|
|
|
16
16
|
"defaults to false, so omitting it deletes immediately"
|
|
17
17
|
view { |v, _i| v.to_h }
|
|
18
18
|
|
|
19
|
-
BURN = :sync
|
|
20
|
-
|
|
21
19
|
def initialize(prefix:, dry_run: false)
|
|
22
20
|
super()
|
|
23
21
|
@prefix = prefix
|
|
@@ -33,7 +31,7 @@ module Textus
|
|
|
33
31
|
warnings = leaves.empty? ? ["no keys under #{@prefix}"] : []
|
|
34
32
|
steps = leaves.map { |key| { "op" => "delete", "key" => key } }
|
|
35
33
|
|
|
36
|
-
plan = Textus::
|
|
34
|
+
plan = Textus::Jobs::Plan.new(steps: steps, warnings: warnings)
|
|
37
35
|
return plan if @dry_run
|
|
38
36
|
|
|
39
37
|
steps.each do |step|
|
data/lib/textus/action/key_mv.rb
CHANGED
|
@@ -18,8 +18,6 @@ module Textus
|
|
|
18
18
|
"defaults to false, so omitting it applies the move immediately " \
|
|
19
19
|
"(unlike the bulk key_mv_prefix, which defaults to a dry-run plan)"
|
|
20
20
|
|
|
21
|
-
BURN = :sync
|
|
22
|
-
|
|
23
21
|
def initialize(old_key:, new_key:, dry_run: false)
|
|
24
22
|
super()
|
|
25
23
|
@old_key = old_key
|
|
@@ -44,7 +42,6 @@ module Textus
|
|
|
44
42
|
return dry_run_result(container, old_res, new_res) if @dry_run
|
|
45
43
|
|
|
46
44
|
envelope = apply_move(container, call, old_res, new_res)
|
|
47
|
-
publish_rename(container, call, envelope)
|
|
48
45
|
success_result(old_res, new_res, envelope)
|
|
49
46
|
end
|
|
50
47
|
|
|
@@ -57,17 +54,6 @@ module Textus
|
|
|
57
54
|
)
|
|
58
55
|
end
|
|
59
56
|
|
|
60
|
-
def publish_rename(container, call, envelope)
|
|
61
|
-
container.steps.publish(
|
|
62
|
-
:entry_renamed,
|
|
63
|
-
ctx: Textus::Step::Context.for(container: container, call: call),
|
|
64
|
-
key: @new_key,
|
|
65
|
-
from_key: @old_key,
|
|
66
|
-
to_key: @new_key,
|
|
67
|
-
envelope: envelope,
|
|
68
|
-
)
|
|
69
|
-
end
|
|
70
|
-
|
|
71
57
|
def success_result(old_res, new_res, envelope)
|
|
72
58
|
{
|
|
73
59
|
"protocol" => PROTOCOL,
|
|
@@ -117,7 +103,7 @@ module Textus
|
|
|
117
103
|
writer(container, call).put(
|
|
118
104
|
@old_key,
|
|
119
105
|
mentry: old_mentry,
|
|
120
|
-
payload: Textus::Envelope::
|
|
106
|
+
payload: Textus::Envelope::Writer::Payload.new(
|
|
121
107
|
meta: pre_env.meta,
|
|
122
108
|
body: pre_env.body,
|
|
123
109
|
content: pre_env.content,
|
|
@@ -18,8 +18,6 @@ module Textus
|
|
|
18
18
|
"to false, so omitting it applies the rename immediately"
|
|
19
19
|
view { |v, _i| v.to_h }
|
|
20
20
|
|
|
21
|
-
BURN = :sync
|
|
22
|
-
|
|
23
21
|
def initialize(from_prefix:, to_prefix:, dry_run: false)
|
|
24
22
|
super()
|
|
25
23
|
@from_prefix = from_prefix
|
|
@@ -46,7 +44,7 @@ module Textus
|
|
|
46
44
|
{ "op" => "mv", "from" => old_key, "to" => new_key }
|
|
47
45
|
end
|
|
48
46
|
|
|
49
|
-
plan = Textus::
|
|
47
|
+
plan = Textus::Jobs::Plan.new(steps: steps, warnings: warnings)
|
|
50
48
|
return plan if @dry_run
|
|
51
49
|
|
|
52
50
|
steps.each do |step|
|
data/lib/textus/action/list.rb
CHANGED
|
@@ -14,8 +14,6 @@ module Textus
|
|
|
14
14
|
description: "restrict to one lane by name (see `boot` lanes); combine with prefix to narrow further"
|
|
15
15
|
view(:cli) { |rows| { "entries" => rows } }
|
|
16
16
|
|
|
17
|
-
BURN = :sync
|
|
18
|
-
|
|
19
17
|
def initialize(prefix: nil, lane: nil)
|
|
20
18
|
super()
|
|
21
19
|
@prefix = prefix
|
|
@@ -19,8 +19,6 @@ module Textus
|
|
|
19
19
|
description: "structured payload for json/yaml-format entries; omit (use `body`) for markdown entries. Do not send both"
|
|
20
20
|
view { |env, _i| env.to_h_for_wire }
|
|
21
21
|
|
|
22
|
-
BURN = :sync
|
|
23
|
-
|
|
24
22
|
def initialize(key:, meta: nil, body: nil, content: nil)
|
|
25
23
|
super()
|
|
26
24
|
@key = key
|
data/lib/textus/action/pulse.rb
CHANGED
|
@@ -8,14 +8,12 @@ module Textus
|
|
|
8
8
|
extend Textus::Contract::DSL
|
|
9
9
|
|
|
10
10
|
verb :pulse
|
|
11
|
-
summary "Delta since cursor — changed entries,
|
|
11
|
+
summary "Delta since cursor — changed entries, pending proposals, index freshness."
|
|
12
12
|
surfaces :cli, :mcp
|
|
13
13
|
around :cursor
|
|
14
14
|
arg :since, Integer, session_default: :cursor,
|
|
15
15
|
description: "audit seq to diff from; defaults to the session cursor"
|
|
16
16
|
|
|
17
|
-
BURN = :sync
|
|
18
|
-
|
|
19
17
|
def initialize(since: nil)
|
|
20
18
|
super()
|
|
21
19
|
@since = since
|
|
@@ -27,30 +25,18 @@ module Textus
|
|
|
27
25
|
@manifest = container.manifest
|
|
28
26
|
@audit_log = container.audit_log
|
|
29
27
|
@root = container.root
|
|
30
|
-
@steps = container.steps
|
|
31
28
|
|
|
32
|
-
freshness_rows = Pulse::Scanner.new.call(container: container, call: call)
|
|
33
29
|
{
|
|
34
30
|
"cursor" => @audit_log.latest_seq,
|
|
35
31
|
"changed" => Textus::Action::Audit.new(seq_since: @since).call(container: container),
|
|
36
|
-
"stale" => freshness_rows.select { |row| row[:status] == :expired }.map { |row| row[:key] },
|
|
37
32
|
"pending_review" => review_keys,
|
|
38
|
-
"doctor" => doctor_summary,
|
|
39
33
|
"contract_etag" => Textus::Etag.for_contract(@root),
|
|
40
|
-
"
|
|
41
|
-
"hook_errors" => hook_errors_since(@since || 0),
|
|
34
|
+
"index_etag" => index_etag(container),
|
|
42
35
|
}
|
|
43
36
|
end
|
|
44
37
|
|
|
45
38
|
private
|
|
46
39
|
|
|
47
|
-
def soonest_due(rows)
|
|
48
|
-
times = rows.map { |row| row[:next_due_at] }.compact.map { |t| Time.parse(t) }
|
|
49
|
-
return nil if times.empty?
|
|
50
|
-
|
|
51
|
-
times.min.utc.iso8601
|
|
52
|
-
end
|
|
53
|
-
|
|
54
40
|
def review_keys
|
|
55
41
|
queue = @manifest.policy.queue_lane
|
|
56
42
|
return [] unless queue
|
|
@@ -59,28 +45,11 @@ module Textus
|
|
|
59
45
|
rows.map { |row| row.is_a?(Hash) ? (row["key"] || row[:key]) : row }
|
|
60
46
|
end
|
|
61
47
|
|
|
62
|
-
def
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
"warn" => issues.count { |i| i["level"] == "warning" },
|
|
68
|
-
"fail" => issues.count { |i| i["level"] == "error" },
|
|
69
|
-
}
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def hook_errors_since(seq)
|
|
73
|
-
@steps.error_log.since(seq).map do |row|
|
|
74
|
-
{
|
|
75
|
-
"seq" => row[:seq],
|
|
76
|
-
"event" => row[:event].to_s,
|
|
77
|
-
"hook" => row[:hook].to_s,
|
|
78
|
-
"key" => row[:key],
|
|
79
|
-
"error_class" => row[:error_class],
|
|
80
|
-
"error_message" => row[:error_message],
|
|
81
|
-
"at" => row[:at],
|
|
82
|
-
}
|
|
83
|
-
end
|
|
48
|
+
def index_etag(container)
|
|
49
|
+
path = container.manifest.resolver.resolve("artifacts.index").path
|
|
50
|
+
File.exist?(path) ? container.file_store.etag(path) : nil
|
|
51
|
+
rescue Textus::Error
|
|
52
|
+
nil
|
|
84
53
|
end
|
|
85
54
|
end
|
|
86
55
|
end
|
data/lib/textus/action/put.rb
CHANGED
|
@@ -20,8 +20,6 @@ module Textus
|
|
|
20
20
|
description: "optimistic-concurrency guard: the etag you last read; the write is rejected if the entry changed since"
|
|
21
21
|
view { |env| { "uid" => env.uid, "etag" => env.etag } }
|
|
22
22
|
|
|
23
|
-
BURN = :sync
|
|
24
|
-
|
|
25
23
|
def initialize(key:, meta: nil, body: nil, content: nil, if_etag: nil)
|
|
26
24
|
super()
|
|
27
25
|
@key = key
|
|
@@ -35,12 +33,10 @@ module Textus
|
|
|
35
33
|
run_with_cascade(@key, container:, call:) do
|
|
36
34
|
Textus::Manifest::Data.validate_key!(@key)
|
|
37
35
|
mentry = container.manifest.resolver.resolve(@key).entry
|
|
38
|
-
auth(container).check_action!(action: :put, actor: call.role, key: @key, extra: { if_etag: @if_etag })
|
|
39
|
-
|
|
40
36
|
envelope = writer(container, call).put(
|
|
41
37
|
@key,
|
|
42
38
|
mentry: mentry,
|
|
43
|
-
payload: Textus::Envelope::
|
|
39
|
+
payload: Textus::Envelope::Writer::Payload.new(
|
|
44
40
|
meta: @meta,
|
|
45
41
|
body: @body,
|
|
46
42
|
content: @content,
|
|
@@ -48,13 +44,6 @@ module Textus
|
|
|
48
44
|
if_etag: @if_etag,
|
|
49
45
|
)
|
|
50
46
|
|
|
51
|
-
container.steps.publish(
|
|
52
|
-
:entry_written,
|
|
53
|
-
ctx: Textus::Step::Context.for(container: container, call: call),
|
|
54
|
-
key: @key,
|
|
55
|
-
envelope: envelope,
|
|
56
|
-
)
|
|
57
|
-
|
|
58
47
|
envelope
|
|
59
48
|
end
|
|
60
49
|
end
|
data/lib/textus/action/rdeps.rb
CHANGED
|
@@ -11,8 +11,6 @@ module Textus
|
|
|
11
11
|
arg :key, String, required: true, positional: true,
|
|
12
12
|
description: "dotted key whose dependents (what would be stranded if it moved) you want"
|
|
13
13
|
|
|
14
|
-
BURN = :sync
|
|
15
|
-
|
|
16
14
|
def initialize(key:)
|
|
17
15
|
super()
|
|
18
16
|
@key = key
|
|
@@ -21,17 +19,9 @@ module Textus
|
|
|
21
19
|
def call(container:, **)
|
|
22
20
|
manifest = container.manifest
|
|
23
21
|
rdeps = manifest.data.entries.each_with_object([]) do |entry, acc|
|
|
24
|
-
next unless entry.
|
|
22
|
+
next unless entry.external?
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
sources =
|
|
28
|
-
if src.projection?
|
|
29
|
-
Array(src.select).compact
|
|
30
|
-
elsif src.external?
|
|
31
|
-
Array(src.sources).compact
|
|
32
|
-
else
|
|
33
|
-
[]
|
|
34
|
-
end
|
|
24
|
+
sources = Array(entry.source&.sources).compact
|
|
35
25
|
acc << entry.key if sources.any? { |source| source == @key || @key.start_with?("#{source}.") }
|
|
36
26
|
end
|
|
37
27
|
{ "key" => @key, "rdeps" => rdeps }
|
data/lib/textus/action/reject.rb
CHANGED
|
@@ -11,8 +11,6 @@ module Textus
|
|
|
11
11
|
cli "reject"
|
|
12
12
|
arg :pending_key, String, required: true, positional: true, description: "the queued proposal's key"
|
|
13
13
|
|
|
14
|
-
BURN = :sync
|
|
15
|
-
|
|
16
14
|
def initialize(pending_key:)
|
|
17
15
|
super()
|
|
18
16
|
@pending_key = pending_key
|
|
@@ -20,9 +18,6 @@ module Textus
|
|
|
20
18
|
|
|
21
19
|
def call(container:, call:)
|
|
22
20
|
run_with_cascade(@pending_key, container:, call:) do
|
|
23
|
-
auth = Textus::Gate::Auth.new(container)
|
|
24
|
-
auth.check_action!(action: :reject, actor: call.role, key: @pending_key)
|
|
25
|
-
|
|
26
21
|
mentry = container.manifest.resolver.resolve(@pending_key).entry
|
|
27
22
|
unless mentry.in_proposal_lane?(container.manifest.policy)
|
|
28
23
|
raise ProposalError.new("reject: '#{@pending_key}' is not in a proposal zone (zone=#{mentry.lane})")
|
|
@@ -34,13 +29,6 @@ module Textus
|
|
|
34
29
|
|
|
35
30
|
writer(container, call).delete(@pending_key, mentry: mentry)
|
|
36
31
|
|
|
37
|
-
container.steps.publish(
|
|
38
|
-
:proposal_rejected,
|
|
39
|
-
ctx: Textus::Step::Context.for(container: container, call: call),
|
|
40
|
-
key: @pending_key,
|
|
41
|
-
target_key: target_key,
|
|
42
|
-
)
|
|
43
|
-
|
|
44
32
|
{ "protocol" => PROTOCOL, "rejected" => @pending_key, "target_key" => target_key }
|
|
45
33
|
end
|
|
46
34
|
end
|
|
@@ -15,8 +15,6 @@ module Textus
|
|
|
15
15
|
description: "defaults false: lean {lifecycle, guard}. detail: true adds matched blocks + guard predicates per transition."
|
|
16
16
|
view(:cli) { |r| { "verb" => "rule_explain" }.merge(r.transform_keys(&:to_s)) }
|
|
17
17
|
|
|
18
|
-
BURN = :sync
|
|
19
|
-
|
|
20
18
|
def initialize(key:, detail: nil)
|
|
21
19
|
super()
|
|
22
20
|
@key = key
|
|
@@ -79,8 +77,6 @@ module Textus
|
|
|
79
77
|
retention_hash(value, string_keys: false)
|
|
80
78
|
when :react
|
|
81
79
|
value.to_h
|
|
82
|
-
when :handler_permit
|
|
83
|
-
value.handlers
|
|
84
80
|
else
|
|
85
81
|
value
|
|
86
82
|
end
|
|
@@ -15,8 +15,6 @@ module Textus
|
|
|
15
15
|
description: "path to candidate manifest YAML; its `rules:` block is diffed against the live manifest"
|
|
16
16
|
view { |v, _i| v.to_h }
|
|
17
17
|
|
|
18
|
-
BURN = :sync
|
|
19
|
-
|
|
20
18
|
def initialize(candidate_yaml:)
|
|
21
19
|
super()
|
|
22
20
|
@candidate_yaml = candidate_yaml
|
|
@@ -47,7 +45,7 @@ module Textus
|
|
|
47
45
|
}
|
|
48
46
|
end
|
|
49
47
|
|
|
50
|
-
Textus::
|
|
48
|
+
Textus::Jobs::Plan.new(steps: steps, warnings: [])
|
|
51
49
|
end
|
|
52
50
|
|
|
53
51
|
private
|
|
@@ -11,8 +11,6 @@ module Textus
|
|
|
11
11
|
cli "rule list"
|
|
12
12
|
view(:cli) { |policies| { "verb" => "rule_list", "policies" => policies } }
|
|
13
13
|
|
|
14
|
-
BURN = :sync
|
|
15
|
-
|
|
16
14
|
def call(container:, **)
|
|
17
15
|
manifest = container.manifest
|
|
18
16
|
manifest.rules.blocks.map do |block|
|
|
@@ -35,8 +33,6 @@ module Textus
|
|
|
35
33
|
{ "ttl_seconds" => value.ttl_seconds, "action" => value.action.to_s }
|
|
36
34
|
when :react
|
|
37
35
|
value.to_h
|
|
38
|
-
when :handler_permit
|
|
39
|
-
value.handlers
|
|
40
36
|
else
|
|
41
37
|
value
|
|
42
38
|
end
|
data/lib/textus/action/uid.rb
CHANGED
data/lib/textus/action/where.rb
CHANGED
|
@@ -10,11 +10,11 @@ module Textus
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def writer(container, call)
|
|
13
|
-
Textus::Envelope::
|
|
13
|
+
Textus::Envelope::Writer.from(container: container, call: call)
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def reader(container)
|
|
17
|
-
Textus::Envelope::
|
|
17
|
+
Textus::Envelope::Reader.from(container: container)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def run_with_cascade(target_key, container:, call:)
|
|
@@ -24,32 +24,18 @@ module Textus
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def cascade_to_rdeps(key, container, call)
|
|
27
|
-
return if derived_write?(key, container)
|
|
28
|
-
|
|
29
27
|
rdeps = Textus::Action::Rdeps.new(key: key).call(container: container, call: call).fetch("rdeps", [])
|
|
30
28
|
producible = rdeps.select { |dep_key| producible?(dep_key, container) }
|
|
31
29
|
return if producible.empty?
|
|
32
30
|
|
|
33
31
|
producible.each do |dep_key|
|
|
34
|
-
Textus::
|
|
32
|
+
Textus::Jobs::Materialize.new(key: dep_key).call(container:, call:)
|
|
35
33
|
end
|
|
36
|
-
container.steps.publish(
|
|
37
|
-
:entry_written,
|
|
38
|
-
ctx: Textus::Step::Context.for(container: container, call: call),
|
|
39
|
-
key: key,
|
|
40
|
-
envelope: nil,
|
|
41
|
-
)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def derived_write?(key, container)
|
|
45
|
-
container.manifest.resolver.resolve(key).entry.derived?
|
|
46
|
-
rescue Textus::Error
|
|
47
|
-
false
|
|
48
34
|
end
|
|
49
35
|
|
|
50
36
|
def producible?(key, container)
|
|
51
37
|
entry = container.manifest.resolver.resolve(key).entry
|
|
52
|
-
|
|
38
|
+
!entry.publish_tree.nil?
|
|
53
39
|
rescue Textus::Error
|
|
54
40
|
false
|
|
55
41
|
end
|