textus 0.53.0 → 0.54.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.
Files changed (149) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -0
  3. data/SPEC.md +2 -5
  4. data/docs/architecture/README.md +149 -159
  5. data/lib/textus/action/accept.rb +0 -9
  6. data/lib/textus/action/audit.rb +0 -2
  7. data/lib/textus/action/blame.rb +0 -2
  8. data/lib/textus/action/boot.rb +2 -11
  9. data/lib/textus/action/data_mv.rb +1 -3
  10. data/lib/textus/action/deps.rb +1 -15
  11. data/lib/textus/action/doctor.rb +0 -2
  12. data/lib/textus/action/drain.rb +4 -6
  13. data/lib/textus/action/enqueue.rb +3 -5
  14. data/lib/textus/action/get.rb +1 -3
  15. data/lib/textus/action/ingest.rb +135 -0
  16. data/lib/textus/action/jobs.rb +1 -3
  17. data/lib/textus/action/key_delete.rb +0 -10
  18. data/lib/textus/action/key_delete_prefix.rb +1 -3
  19. data/lib/textus/action/key_mv.rb +1 -15
  20. data/lib/textus/action/key_mv_prefix.rb +1 -3
  21. data/lib/textus/action/list.rb +0 -2
  22. data/lib/textus/action/propose.rb +0 -2
  23. data/lib/textus/action/published.rb +0 -2
  24. data/lib/textus/action/pulse.rb +7 -38
  25. data/lib/textus/action/put.rb +1 -12
  26. data/lib/textus/action/rdeps.rb +2 -12
  27. data/lib/textus/action/reject.rb +0 -12
  28. data/lib/textus/action/rule_explain.rb +0 -4
  29. data/lib/textus/action/rule_lint.rb +1 -3
  30. data/lib/textus/action/rule_list.rb +0 -4
  31. data/lib/textus/action/schema_envelope.rb +0 -2
  32. data/lib/textus/action/uid.rb +0 -2
  33. data/lib/textus/action/where.rb +0 -2
  34. data/lib/textus/action/write_verb.rb +4 -18
  35. data/lib/textus/boot.rb +33 -109
  36. data/lib/textus/command.rb +6 -1
  37. data/lib/textus/container.rb +1 -1
  38. data/lib/textus/core/freshness/evaluator.rb +18 -34
  39. data/lib/textus/doctor/check/notebook_sources.rb +50 -0
  40. data/lib/textus/doctor/check/raw_asset_paths.rb +50 -0
  41. data/lib/textus/doctor/check.rb +0 -1
  42. data/lib/textus/doctor.rb +2 -42
  43. data/lib/textus/envelope/reader.rb +46 -0
  44. data/lib/textus/envelope/writer.rb +209 -0
  45. data/lib/textus/errors.rb +8 -4
  46. data/lib/textus/{entry → format}/base.rb +1 -6
  47. data/lib/textus/{entry → format}/json.rb +1 -4
  48. data/lib/textus/{entry → format}/markdown.rb +2 -6
  49. data/lib/textus/{entry → format}/text.rb +2 -6
  50. data/lib/textus/{entry → format}/yaml.rb +1 -4
  51. data/lib/textus/{entry.rb → format.rb} +9 -10
  52. data/lib/textus/gate/auth.rb +58 -12
  53. data/lib/textus/gate.rb +2 -0
  54. data/lib/textus/init.rb +12 -75
  55. data/lib/textus/jobs/base.rb +23 -0
  56. data/lib/textus/jobs/materialize.rb +20 -0
  57. data/lib/textus/{background → jobs}/plan.rb +1 -1
  58. data/lib/textus/jobs/planner.rb +101 -0
  59. data/lib/textus/jobs/retention.rb +48 -0
  60. data/lib/textus/jobs/sweep.rb +27 -0
  61. data/lib/textus/{background → jobs}/worker.rb +2 -2
  62. data/lib/textus/jobs.rb +15 -0
  63. data/lib/textus/key/path.rb +1 -1
  64. data/lib/textus/manifest/entry/base.rb +4 -19
  65. data/lib/textus/manifest/entry/parser.rb +27 -10
  66. data/lib/textus/manifest/entry/produced.rb +13 -34
  67. data/lib/textus/manifest/entry/publish/to_paths.rb +4 -4
  68. data/lib/textus/manifest/entry/publish.rb +0 -12
  69. data/lib/textus/manifest/entry/validators/format_matrix.rb +1 -1
  70. data/lib/textus/manifest/entry/validators.rb +0 -1
  71. data/lib/textus/manifest/policy/source.rb +14 -62
  72. data/lib/textus/manifest/policy.rb +2 -5
  73. data/lib/textus/manifest/resolver.rb +2 -2
  74. data/lib/textus/manifest/schema/keys.rb +6 -20
  75. data/lib/textus/manifest/schema/validator.rb +3 -11
  76. data/lib/textus/manifest/schema/vocabulary.rb +1 -0
  77. data/lib/textus/ports/{queue → job_store}/job.rb +1 -1
  78. data/lib/textus/ports/{queue.rb → job_store.rb} +6 -13
  79. data/lib/textus/produce/engine.rb +50 -0
  80. data/lib/textus/produce/render.rb +18 -0
  81. data/lib/textus/session.rb +1 -1
  82. data/lib/textus/store.rb +1 -10
  83. data/lib/textus/surfaces/mcp/catalog.rb +1 -1
  84. data/lib/textus/surfaces/mcp/errors.rb +0 -9
  85. data/lib/textus/surfaces/mcp/routing.rb +51 -0
  86. data/lib/textus/surfaces/mcp/server.rb +101 -62
  87. data/lib/textus/surfaces/role_scope.rb +0 -4
  88. data/lib/textus/surfaces/watcher.rb +3 -3
  89. data/lib/textus/version.rb +1 -1
  90. data/lib/textus/workflow/collector.rb +27 -0
  91. data/lib/textus/workflow/context.rb +5 -0
  92. data/lib/textus/workflow/dsl.rb +44 -0
  93. data/lib/textus/workflow/loader.rb +17 -0
  94. data/lib/textus/workflow/pattern.rb +18 -0
  95. data/lib/textus/workflow/registry.rb +21 -0
  96. data/lib/textus/workflow/runner.rb +84 -0
  97. data/lib/textus/workflow.rb +28 -0
  98. data/lib/textus.rb +12 -1
  99. metadata +33 -61
  100. data/lib/textus/action/pulse/scanner.rb +0 -118
  101. data/lib/textus/background/job/base.rb +0 -27
  102. data/lib/textus/background/job/materialize.rb +0 -31
  103. data/lib/textus/background/job/refresh.rb +0 -22
  104. data/lib/textus/background/job/sweep.rb +0 -31
  105. data/lib/textus/background/job.rb +0 -19
  106. data/lib/textus/background/planner/plan.rb +0 -113
  107. data/lib/textus/background/retention/apply.rb +0 -50
  108. data/lib/textus/doctor/check/handler_permit.rb +0 -34
  109. data/lib/textus/doctor/check/hooks.rb +0 -25
  110. data/lib/textus/doctor/check/intake_registration.rb +0 -46
  111. data/lib/textus/envelope/io/reader.rb +0 -48
  112. data/lib/textus/envelope/io/writer.rb +0 -211
  113. data/lib/textus/events.rb +0 -21
  114. data/lib/textus/init/templates/machine_intake.rb +0 -49
  115. data/lib/textus/init/templates/orientation_reducer.rb +0 -21
  116. data/lib/textus/manifest/entry/validators/events.rb +0 -22
  117. data/lib/textus/manifest/policy/handler_permit.rb +0 -17
  118. data/lib/textus/pipeline/acquire/handler.rb +0 -29
  119. data/lib/textus/pipeline/acquire/intake.rb +0 -132
  120. data/lib/textus/pipeline/acquire/projection.rb +0 -129
  121. data/lib/textus/pipeline/acquire/serializer/json.rb +0 -31
  122. data/lib/textus/pipeline/acquire/serializer/text.rb +0 -16
  123. data/lib/textus/pipeline/acquire/serializer/yaml.rb +0 -31
  124. data/lib/textus/pipeline/acquire/serializer.rb +0 -17
  125. data/lib/textus/pipeline/engine.rb +0 -97
  126. data/lib/textus/pipeline/render.rb +0 -25
  127. data/lib/textus/ports/audit_subscriber.rb +0 -42
  128. data/lib/textus/projection.rb +0 -103
  129. data/lib/textus/step/base.rb +0 -35
  130. data/lib/textus/step/builtin/csv_fetch.rb +0 -19
  131. data/lib/textus/step/builtin/ical_events_fetch.rb +0 -30
  132. data/lib/textus/step/builtin/json_fetch.rb +0 -18
  133. data/lib/textus/step/builtin/markdown_links_fetch.rb +0 -20
  134. data/lib/textus/step/builtin/rss_fetch.rb +0 -26
  135. data/lib/textus/step/builtin.rb +0 -22
  136. data/lib/textus/step/catalog.rb +0 -37
  137. data/lib/textus/step/context.rb +0 -74
  138. data/lib/textus/step/discovery.rb +0 -24
  139. data/lib/textus/step/error_log.rb +0 -32
  140. data/lib/textus/step/event_bus.rb +0 -113
  141. data/lib/textus/step/fetch.rb +0 -13
  142. data/lib/textus/step/fire_report.rb +0 -23
  143. data/lib/textus/step/loader.rb +0 -108
  144. data/lib/textus/step/observe.rb +0 -31
  145. data/lib/textus/step/registry_store.rb +0 -66
  146. data/lib/textus/step/signature.rb +0 -31
  147. data/lib/textus/step/transform.rb +0 -12
  148. data/lib/textus/step/validate.rb +0 -11
  149. data/lib/textus/step.rb +0 -10
@@ -11,8 +11,6 @@ module Textus
11
11
  arg :key, String, required: true, positional: true,
12
12
  description: "dotted key of the derived entry whose source keys you want"
13
13
 
14
- BURN = :sync
15
-
16
14
  def initialize(key:)
17
15
  super()
18
16
  @key = key
@@ -20,19 +18,7 @@ module Textus
20
18
 
21
19
  def call(container:, **)
22
20
  entry = container.manifest.data.entries.find { |e| e.key == @key }
23
- deps =
24
- if entry&.derived?
25
- src = entry.source
26
- if src.projection?
27
- Array(src.select).compact
28
- elsif src.external?
29
- Array(src.sources).compact
30
- else
31
- []
32
- end
33
- else
34
- []
35
- end
21
+ deps = entry&.external? ? Array(entry.source&.sources).compact : []
36
22
  { "key" => @key, "deps" => deps.uniq }
37
23
  end
38
24
 
@@ -11,8 +11,6 @@ module Textus
11
11
  cli "doctor"
12
12
  arg :checks, Array, required: false, description: "subset of check names to run (default: all)"
13
13
 
14
- BURN = :sync
15
-
16
14
  def initialize(checks: nil)
17
15
  super()
18
16
  @checks = checks
@@ -6,14 +6,12 @@ module Textus
6
6
  extend Textus::Contract::DSL
7
7
 
8
8
  verb :drain
9
- summary "Seed refresh + sweep jobs then drain the queue to empty. " \
9
+ summary "Seed materialize + sweep jobs then drain the queue to empty. " \
10
10
  "Identical to one Watcher tick. Use when no watcher is running."
11
11
  surfaces :cli, :mcp
12
12
  arg :prefix, String, description: "restrict to keys under this dotted prefix"
13
13
  arg :lane, String, description: "restrict to entries in this lane"
14
14
 
15
- BURN = :sync
16
-
17
15
  def initialize(prefix: nil, lane: nil)
18
16
  super()
19
17
  @prefix = prefix
@@ -21,14 +19,14 @@ module Textus
21
19
  end
22
20
 
23
21
  def call(container:, call:)
24
- queue = Textus::Ports::Queue.new(root: container.root)
25
- Textus::Background::Planner::Plan.seed(
22
+ queue = Textus::Ports::JobStore.new(root: container.root)
23
+ Textus::Jobs::Planner.seed(
26
24
  container: container,
27
25
  queue: queue,
28
26
  role: call.role,
29
27
  )
30
28
  queue.reclaim(now: Textus::Ports::Clock.new.now)
31
- summary = Textus::Background::Worker.for(container:, queue:).drain
29
+ summary = Textus::Jobs::Worker.for(container:, queue:).drain
32
30
  {
33
31
  "protocol" => Textus::PROTOCOL,
34
32
  "ok" => summary.failed.zero?,
@@ -14,8 +14,6 @@ module Textus
14
14
  arg :args, Hash, default: {},
15
15
  description: "type-specific arguments (e.g. { key: ... } or { scope: ... })"
16
16
 
17
- BURN = :sync
18
-
19
17
  def initialize(type:, args: {})
20
18
  super()
21
19
  @type = type
@@ -28,7 +26,7 @@ module Textus
28
26
 
29
27
  def call(container:, call:)
30
28
  action_class = begin
31
- Textus::Background::Job.fetch(@type.to_s)
29
+ Textus::Jobs.fetch(@type.to_s)
32
30
  rescue Textus::UsageError
33
31
  raise Textus::UsageError.new("unregistered job type '#{@type}'")
34
32
  end
@@ -41,13 +39,13 @@ module Textus
41
39
  )
42
40
  end
43
41
 
44
- job = Textus::Ports::Queue::Job.new(
42
+ job = Textus::Ports::JobStore::Job.new(
45
43
  type: @type,
46
44
  args: @job_args,
47
45
  enqueued_by: call.role,
48
46
  max_attempts: 3,
49
47
  )
50
- Textus::Ports::Queue.new(root: container.root).enqueue(job)
48
+ Textus::Ports::JobStore.new(root: container.root).enqueue(job)
51
49
  { "protocol" => Textus::PROTOCOL, "ok" => true, "id" => job.id }
52
50
  end
53
51
  end
@@ -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::Entry.for_format(mentry.format).parse(raw, path: path)
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
@@ -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::Queue.new(root: container.root)
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::Background::Plan.new(steps: steps, warnings: warnings)
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|
@@ -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::IO::Writer::Payload.new(
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::Background::Plan.new(steps: steps, warnings: warnings)
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|
@@ -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
@@ -10,8 +10,6 @@ module Textus
10
10
  surfaces :cli
11
11
  cli "published"
12
12
 
13
- BURN = :sync
14
-
15
13
  def args
16
14
  {}
17
15
  end
@@ -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, stale, pending proposals, doctor summary."
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
- "next_due_at" => soonest_due(freshness_rows),
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 doctor_summary
63
- result = Textus::Doctor.build(container: @container)
64
- issues = result["issues"] || []
65
- {
66
- "ok" => result["ok"],
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
@@ -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::IO::Writer::Payload.new(
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
@@ -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.derived?
22
+ next unless entry.external?
25
23
 
26
- src = entry.source
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 }
@@ -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::Background::Plan.new(steps: steps, warnings: [])
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
@@ -12,8 +12,6 @@ module Textus
12
12
  arg :key, String, required: true, positional: true,
13
13
  description: "any key in the family whose schema you want; returns required/optional fields and their types"
14
14
 
15
- BURN = :sync
16
-
17
15
  def initialize(key:)
18
16
  super()
19
17
  @key = key
@@ -12,8 +12,6 @@ module Textus
12
12
  arg :key, String, required: true, positional: true, description: "entry key"
13
13
  view(:cli) { |uid, inputs| { "key" => inputs[:key], "uid" => uid } }
14
14
 
15
- BURN = :sync
16
-
17
15
  def initialize(key:)
18
16
  super()
19
17
  @key = key