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
data/lib/textus/action/put.rb
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Action
|
|
5
|
-
class Put < Base
|
|
6
|
-
verb :put
|
|
7
|
-
summary "Create or update an entry. Schema-validated. Returns {uid, etag}."
|
|
8
|
-
surfaces :cli, :mcp
|
|
9
|
-
arg :key, String, required: true, positional: true,
|
|
10
|
-
description: "dotted entry key, e.g. 'knowledge.project'; must resolve to a zone the role may write"
|
|
11
|
-
arg :meta, Hash, required: false, wire_name: :_meta,
|
|
12
|
-
description: "frontmatter; reads back as `_meta` from `get`. Schema-validated — call `schema KEY` first"
|
|
13
|
-
arg :body, String,
|
|
14
|
-
description: "markdown/text payload for markdown-format entries; omit (use `content`) for json/yaml entries. Do not send both"
|
|
15
|
-
arg :content, Hash,
|
|
16
|
-
description: "structured payload for json/yaml-format entries; omit (use `body`) for markdown entries. Do not send both"
|
|
17
|
-
arg :if_etag, String,
|
|
18
|
-
description: "optimistic-concurrency guard: the etag you last read; the write is rejected if the entry changed since"
|
|
19
|
-
view { |env| { "uid" => env.uid, "etag" => env.etag } }
|
|
20
|
-
|
|
21
|
-
def self.call(container:, call:, key:, meta: nil, body: nil, content: nil, if_etag: nil) # rubocop:disable Metrics/ParameterLists
|
|
22
|
-
Textus::Manifest::Data.validate_key!(key)
|
|
23
|
-
mentry = container.manifest.resolver.resolve(key).entry
|
|
24
|
-
Success(container.compositor.write(
|
|
25
|
-
key,
|
|
26
|
-
mentry: mentry,
|
|
27
|
-
payload: Textus::Store::Envelope::Writer::Payload.new(
|
|
28
|
-
meta: meta,
|
|
29
|
-
body: body,
|
|
30
|
-
content: content,
|
|
31
|
-
),
|
|
32
|
-
if_etag: if_etag,
|
|
33
|
-
call: call,
|
|
34
|
-
))
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
data/lib/textus/action/rdeps.rb
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Action
|
|
5
|
-
class Rdeps < Base
|
|
6
|
-
verb :rdeps
|
|
7
|
-
summary "List the derived entries that depend on a key (reverse deps / impact set)."
|
|
8
|
-
surfaces :cli, :mcp
|
|
9
|
-
arg :key, String, required: true, positional: true,
|
|
10
|
-
description: "dotted key whose dependents (what would be stranded if it moved) you want"
|
|
11
|
-
|
|
12
|
-
def self.call(container:, key:, **)
|
|
13
|
-
manifest = container.manifest
|
|
14
|
-
rdeps = manifest.data.entries.each_with_object([]) do |entry, acc|
|
|
15
|
-
next unless entry.external?
|
|
16
|
-
|
|
17
|
-
sources = Array(entry.source&.sources).compact
|
|
18
|
-
acc << entry.key if sources.any? { |source| source == key || key.start_with?("#{source}.") }
|
|
19
|
-
end
|
|
20
|
-
Success({ "key" => key, "rdeps" => rdeps })
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
data/lib/textus/action/reject.rb
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Action
|
|
5
|
-
class Reject < Base
|
|
6
|
-
verb :reject
|
|
7
|
-
summary "discard a queued proposal without applying it"
|
|
8
|
-
surfaces :cli, :mcp
|
|
9
|
-
cli "reject"
|
|
10
|
-
arg :pending_key, String, required: true, positional: true, description: "the queued proposal's key"
|
|
11
|
-
|
|
12
|
-
def self.call(container:, call:, pending_key:)
|
|
13
|
-
mentry = container.manifest.resolver.resolve(pending_key).entry
|
|
14
|
-
unless mentry.in_proposal_lane?(container.manifest.policy)
|
|
15
|
-
return Failure(code: :proposal_error, message: "reject: '#{pending_key}' is not in a proposal zone (zone=#{mentry.lane})")
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
env = container.compositor.read(pending_key)
|
|
19
|
-
parsed = proposal_from(env, key: pending_key)
|
|
20
|
-
return parsed if parsed.is_a?(Dry::Monads::Result::Failure)
|
|
21
|
-
|
|
22
|
-
target_key = parsed[:target_key]
|
|
23
|
-
container.compositor.delete(pending_key, mentry: mentry, call: call)
|
|
24
|
-
Success("protocol" => Textus::PROTOCOL, "rejected" => pending_key, "target_key" => target_key)
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Action
|
|
5
|
-
class RuleExplain < Base
|
|
6
|
-
verb :rule_explain
|
|
7
|
-
summary "Effective rules for a key. Lean {lifecycle, guard} by default; detail: true adds matched blocks + guard predicates."
|
|
8
|
-
surfaces :cli, :mcp
|
|
9
|
-
cli "rule explain"
|
|
10
|
-
arg :key, String, required: true, positional: true,
|
|
11
|
-
description: "dotted key whose effective rules you want (lifecycle ttl/action, write guard, ...)"
|
|
12
|
-
arg :detail, :boolean,
|
|
13
|
-
description: "defaults false: lean {lifecycle, guard}. detail: true adds matched blocks + guard predicates per transition."
|
|
14
|
-
view(:cli) { |r| { "verb" => "rule_explain" }.merge(r.transform_keys(&:to_s)) }
|
|
15
|
-
|
|
16
|
-
def self.call(container:, call:, key:, detail: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
17
|
-
manifest = container.manifest
|
|
18
|
-
Success(detail ? explain(manifest, key) : effective(manifest, key))
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
REGISTRY = Textus::Manifest::Schema::FIELD_REGISTRY
|
|
22
|
-
LEAN_FIELDS = REGISTRY.select { |_, m| m[:in_rule_explain].include?(:lean) }.keys.freeze
|
|
23
|
-
DETAIL_FIELDS = REGISTRY.select { |_, m| m[:in_rule_explain].include?(:detail) }.keys.freeze
|
|
24
|
-
EFFECTIVE_FIELDS = DETAIL_FIELDS.select { |f| REGISTRY[f][:policy_class] }.freeze
|
|
25
|
-
|
|
26
|
-
def self.effective(manifest, key)
|
|
27
|
-
set = manifest.rules.for(key)
|
|
28
|
-
LEAN_FIELDS.each_with_object({}) do |field, out|
|
|
29
|
-
value = set.public_send(field)
|
|
30
|
-
out[field.to_s] = lean_value(field, value) unless value.nil?
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def self.lean_value(field, value)
|
|
35
|
-
case field
|
|
36
|
-
when :retention
|
|
37
|
-
retention_hash(value, string_keys: true)
|
|
38
|
-
when :react
|
|
39
|
-
value.to_h
|
|
40
|
-
else
|
|
41
|
-
value
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def self.explain(manifest, key)
|
|
46
|
-
matching = manifest.rules.explain(key)
|
|
47
|
-
winners = manifest.rules.for(key)
|
|
48
|
-
{
|
|
49
|
-
key: key,
|
|
50
|
-
matched_blocks: matching.map do |block|
|
|
51
|
-
{ match: block.match }.merge(DETAIL_FIELDS.to_h { |f| [f, !block.public_send(f).nil?] })
|
|
52
|
-
end,
|
|
53
|
-
effective: EFFECTIVE_FIELDS.to_h { |f| [f, effective_value(f, winners.public_send(f))] },
|
|
54
|
-
guards: Textus::Gate::Auth::FLOOR.keys.to_h do |action|
|
|
55
|
-
floor = Textus::Gate::Auth::FLOOR.fetch(action, [])
|
|
56
|
-
rule = Array(manifest.rules.for(key).guard&.dig(action.to_s))
|
|
57
|
-
[action, { floor: floor, rule: rule }]
|
|
58
|
-
end,
|
|
59
|
-
}
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def self.effective_value(field, value)
|
|
63
|
-
return nil if value.nil?
|
|
64
|
-
|
|
65
|
-
case field
|
|
66
|
-
when :retention
|
|
67
|
-
retention_hash(value, string_keys: false)
|
|
68
|
-
when :react
|
|
69
|
-
value.to_h
|
|
70
|
-
else
|
|
71
|
-
value
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def self.retention_hash(retention, string_keys:)
|
|
76
|
-
h = { ttl_seconds: retention.ttl_seconds, action: retention.action }
|
|
77
|
-
string_keys ? h.transform_keys(&:to_s) : h
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
end
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "yaml"
|
|
4
|
-
|
|
5
|
-
module Textus
|
|
6
|
-
module Action
|
|
7
|
-
class RuleLint < Base
|
|
8
|
-
verb :rule_lint
|
|
9
|
-
summary "Diff candidate manifest YAML's rules against the live manifest. No writes."
|
|
10
|
-
surfaces :cli, :mcp
|
|
11
|
-
cli "rule lint"
|
|
12
|
-
arg :candidate_yaml, String, required: true, wire_name: :against, source: :file,
|
|
13
|
-
description: "path to candidate manifest YAML; its `rules:` block is diffed against the live manifest"
|
|
14
|
-
view { |v, _i| v.to_h }
|
|
15
|
-
|
|
16
|
-
def self.call(container:, call:, candidate_yaml:) # rubocop:disable Lint/UnusedMethodArgument
|
|
17
|
-
root = container.root
|
|
18
|
-
live_rules = current_rules(root)
|
|
19
|
-
candidate_result = parse_candidate(candidate_yaml)
|
|
20
|
-
return candidate_result if candidate_result.is_a?(Dry::Monads::Result::Failure)
|
|
21
|
-
|
|
22
|
-
candidate_rules = candidate_result
|
|
23
|
-
|
|
24
|
-
live_by_match = live_rules.to_h { |rule| [rule["match"], rule] }
|
|
25
|
-
candidate_by_match = candidate_rules.to_h { |rule| [rule["match"], rule] }
|
|
26
|
-
|
|
27
|
-
steps = (candidate_by_match.keys - live_by_match.keys).map do |match|
|
|
28
|
-
{ "op" => "add_rule", "match" => match, "rule" => candidate_by_match[match] }
|
|
29
|
-
end
|
|
30
|
-
(live_by_match.keys - candidate_by_match.keys).each do |match|
|
|
31
|
-
steps << { "op" => "remove_rule", "match" => match }
|
|
32
|
-
end
|
|
33
|
-
(live_by_match.keys & candidate_by_match.keys).each do |match|
|
|
34
|
-
next if live_by_match[match] == candidate_by_match[match]
|
|
35
|
-
|
|
36
|
-
steps << {
|
|
37
|
-
"op" => "change_rule",
|
|
38
|
-
"match" => match,
|
|
39
|
-
"from" => live_by_match[match],
|
|
40
|
-
"to" => candidate_by_match[match],
|
|
41
|
-
}
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
Success(Textus::Store::Jobs::Plan.new(steps: steps, warnings: []))
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def self.current_rules(root)
|
|
48
|
-
raw = YAML.safe_load_file(File.join(root, "manifest.yaml"), permitted_classes: [Symbol], aliases: false)
|
|
49
|
-
Array(raw["rules"])
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def self.parse_candidate(yaml_text)
|
|
53
|
-
raw = YAML.safe_load(yaml_text, permitted_classes: [Symbol], aliases: false)
|
|
54
|
-
return Failure(code: :usage_error, message: "candidate is not a YAML mapping") unless raw.is_a?(Hash)
|
|
55
|
-
|
|
56
|
-
Array(raw["rules"])
|
|
57
|
-
rescue Psych::Exception => e
|
|
58
|
-
Failure(code: :usage_error, message: "candidate YAML parse error: #{e.message}")
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
end
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Action
|
|
5
|
-
class RuleList < Base
|
|
6
|
-
verb :rule_list
|
|
7
|
-
summary "List every rule block in the manifest."
|
|
8
|
-
surfaces :cli
|
|
9
|
-
cli "rule list"
|
|
10
|
-
view(:cli) { |policies| { "verb" => "rule_list", "policies" => policies } }
|
|
11
|
-
|
|
12
|
-
def self.call(container:, call:, **_options) # rubocop:disable Lint/UnusedMethodArgument
|
|
13
|
-
manifest = container.manifest
|
|
14
|
-
Success(manifest.rules.blocks.map do |block|
|
|
15
|
-
row = { "match" => block.match }
|
|
16
|
-
LIST_FIELDS.each do |field|
|
|
17
|
-
value = block.public_send(field)
|
|
18
|
-
row[field.to_s] = serialize(field, value) unless value.nil?
|
|
19
|
-
end
|
|
20
|
-
row
|
|
21
|
-
end)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
LIST_FIELDS = Textus::Manifest::Schema::FIELD_REGISTRY.select { |_, m| m[:in_rule_list] }.keys.freeze
|
|
25
|
-
|
|
26
|
-
def self.serialize(field, value)
|
|
27
|
-
case field
|
|
28
|
-
when :retention
|
|
29
|
-
{ "ttl_seconds" => value.ttl_seconds, "action" => value.action.to_s }
|
|
30
|
-
when :react
|
|
31
|
-
value.to_h
|
|
32
|
-
else
|
|
33
|
-
value
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Action
|
|
5
|
-
class SchemaEnvelope < Base
|
|
6
|
-
verb :schema_show
|
|
7
|
-
summary "Return the schema (field shape) for an entry's family, by key."
|
|
8
|
-
surfaces :cli, :mcp
|
|
9
|
-
cli "schema show"
|
|
10
|
-
arg :key, String, required: true, positional: true,
|
|
11
|
-
description: "any key in the family whose schema you want; returns required/optional fields and their types"
|
|
12
|
-
|
|
13
|
-
def self.call(container:, key:, **)
|
|
14
|
-
manifest = container.manifest
|
|
15
|
-
schemas = container.schemas
|
|
16
|
-
mentry = manifest.resolver.resolve(key).entry
|
|
17
|
-
schema = schemas.fetch_or_nil(mentry.schema)
|
|
18
|
-
Success({ "protocol" => Textus::PROTOCOL, "key" => key, "schema_ref" => mentry.schema, "schema" => schema&.to_h })
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
data/lib/textus/action/uid.rb
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Action
|
|
5
|
-
class Uid < Base
|
|
6
|
-
verb :uid
|
|
7
|
-
summary "Return the stable UID of an entry without reading its body."
|
|
8
|
-
surfaces :cli
|
|
9
|
-
cli "key uid"
|
|
10
|
-
arg :key, String, required: true, positional: true, description: "entry key"
|
|
11
|
-
view(:cli) { |uid, inputs| { "key" => inputs[:key], "uid" => uid } }
|
|
12
|
-
|
|
13
|
-
def self.call(container:, call:, key:)
|
|
14
|
-
envelope = Value::Result.unwrap(Textus::Action::Get.call(container: container, call: call, key: key))
|
|
15
|
-
Success(envelope.uid)
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
data/lib/textus/action/where.rb
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Action
|
|
5
|
-
class Where < Base
|
|
6
|
-
verb :where
|
|
7
|
-
summary "Resolve a key to its zone, owner, and path without reading the body."
|
|
8
|
-
surfaces :cli, :mcp
|
|
9
|
-
arg :key, String, required: true, positional: true,
|
|
10
|
-
description: "dotted key to locate (returns zone, owner, path; does not read content)"
|
|
11
|
-
|
|
12
|
-
def self.call(container:, key:, call: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
13
|
-
manifest = container.manifest
|
|
14
|
-
res = manifest.resolver.resolve(key)
|
|
15
|
-
mentry = res.entry
|
|
16
|
-
path = res.path
|
|
17
|
-
Success({ "protocol" => Textus::PROTOCOL, "key" => key, "lane" => mentry.lane, "owner" => mentry.owner, "path" => path })
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
data/lib/textus/contract/arg.rb
DELETED
data/lib/textus/contract/dsl.rb
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Contract
|
|
3
|
-
module DSL
|
|
4
|
-
def verb(name = nil)
|
|
5
|
-
if name
|
|
6
|
-
raise "contract already built; declare verb before reading .contract" if defined?(@contract) && @contract
|
|
7
|
-
|
|
8
|
-
@__verb = name
|
|
9
|
-
else
|
|
10
|
-
@__verb
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def summary(text = nil)
|
|
15
|
-
if text
|
|
16
|
-
raise "contract already built; declare summary before reading .contract" if defined?(@contract) && @contract
|
|
17
|
-
|
|
18
|
-
@__summary = text
|
|
19
|
-
else
|
|
20
|
-
@__summary
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def surfaces(*list)
|
|
25
|
-
if list.empty?
|
|
26
|
-
@__surfaces ||= []
|
|
27
|
-
else
|
|
28
|
-
raise "contract already built; declare surfaces before reading .contract" if defined?(@contract) && @contract
|
|
29
|
-
|
|
30
|
-
@__surfaces = list
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def cli(path = nil)
|
|
35
|
-
if path
|
|
36
|
-
raise "contract already built; declare cli before reading .contract" if defined?(@contract) && @contract
|
|
37
|
-
|
|
38
|
-
@__cli = path.to_s
|
|
39
|
-
else
|
|
40
|
-
@__cli
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def arg(name, type, required: false, positional: false, session_default: nil, description: nil, wire_name: nil, default: nil, source: nil, coerce: nil, cli_default: :__unset) # rubocop:disable Metrics/ParameterLists,Layout/LineLength
|
|
45
|
-
raise "contract already built; declare args before reading .contract" if defined?(@contract) && @contract
|
|
46
|
-
|
|
47
|
-
(@__args ||= []) << Arg.new(
|
|
48
|
-
name: name, type: type, required: required,
|
|
49
|
-
positional: positional, session_default: session_default,
|
|
50
|
-
description: description, wire_name: wire_name, default: default,
|
|
51
|
-
source: source, coerce: coerce, cli_default: cli_default
|
|
52
|
-
)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def cli_stdin(mode = :__read)
|
|
56
|
-
return @__cli_stdin if mode == :__read
|
|
57
|
-
|
|
58
|
-
raise "contract already built; declare cli_stdin before reading .contract" if defined?(@contract) && @contract
|
|
59
|
-
|
|
60
|
-
@__cli_stdin = mode
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def view(surface = :default, &blk)
|
|
64
|
-
return (@__views ||= {})[surface] unless blk
|
|
65
|
-
|
|
66
|
-
raise "contract already built; declare view before reading .contract" if defined?(@contract) && @contract
|
|
67
|
-
|
|
68
|
-
(@__views ||= {})[surface] = blk
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def contract?
|
|
72
|
-
!@__verb.nil?
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def contract
|
|
76
|
-
@contract ||= Spec.new(
|
|
77
|
-
verb: @__verb,
|
|
78
|
-
summary: @__summary,
|
|
79
|
-
args: (@__args || []).freeze,
|
|
80
|
-
surfaces: (@__surfaces || []).freeze,
|
|
81
|
-
views: ((@__views ||= {})[:default] ||= ->(v, _i) { v }) && @__views,
|
|
82
|
-
cli: @__cli,
|
|
83
|
-
cli_stdin: @__cli_stdin,
|
|
84
|
-
)
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
end
|
data/lib/textus/contract/spec.rb
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Contract
|
|
3
|
-
Spec = Data.define(:verb, :summary, :args, :surfaces, :views, :cli, :cli_stdin) do
|
|
4
|
-
def mcp? = surfaces.include?(:mcp)
|
|
5
|
-
def cli? = surfaces.include?(:cli)
|
|
6
|
-
|
|
7
|
-
def view(surface = :default) = views[surface] || views.fetch(:default)
|
|
8
|
-
def cli_path = cli || verb.to_s
|
|
9
|
-
def cli_words = cli_path.split
|
|
10
|
-
def cli_group = cli_words.size > 1 ? cli_words.first : nil
|
|
11
|
-
def cli_leaf = cli_words.last
|
|
12
|
-
|
|
13
|
-
def required_args = args.select(&:required)
|
|
14
|
-
|
|
15
|
-
def input_schema
|
|
16
|
-
props = args.to_h do |a|
|
|
17
|
-
h = { "type" => Contract.json_type(a.type) }
|
|
18
|
-
h["description"] = a.description if a.description
|
|
19
|
-
[a.wire.to_s, h]
|
|
20
|
-
end
|
|
21
|
-
{ type: "object", properties: props, required: required_args.map { |a| a.wire.to_s } }
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
data/lib/textus/contract.rb
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Contract
|
|
3
|
-
JSON_TYPES = {
|
|
4
|
-
String => "string", Integer => "integer", Hash => "object",
|
|
5
|
-
Array => "array", :boolean => "boolean"
|
|
6
|
-
}.freeze
|
|
7
|
-
|
|
8
|
-
def self.json_type(type)
|
|
9
|
-
JSON_TYPES.fetch(type) { raise ArgumentError.new("no JSON type mapping for #{type.inspect}") }
|
|
10
|
-
end
|
|
11
|
-
end
|
|
12
|
-
end
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
require "time"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Core
|
|
5
|
-
module Freshness
|
|
6
|
-
# The single currency evaluator (ADR 0099). Answers "is the stored data
|
|
7
|
-
# stale relative to its retention rule?" and detects generator drift for
|
|
8
|
-
# external entries.
|
|
9
|
-
# - retention rule TTL -> AGE signal: now - file_basis > ttl_seconds
|
|
10
|
-
# - external -> DRIFT signal: a source changed since generated.at
|
|
11
|
-
# (surfaced by the doctor generator_drift check).
|
|
12
|
-
class Evaluator
|
|
13
|
-
def initialize(manifest:, file_stat:, clock:)
|
|
14
|
-
@manifest = manifest
|
|
15
|
-
@file_stat = file_stat
|
|
16
|
-
@clock = clock
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
# Per-entry currency Verdict driven by the retention rule TTL (if any).
|
|
20
|
-
def verdict(mentry)
|
|
21
|
-
ttl = @manifest.rules.for(mentry.key).retention&.ttl_seconds
|
|
22
|
-
return fresh if ttl.nil?
|
|
23
|
-
|
|
24
|
-
stale = age_stale?(file_basis(mentry), ttl)
|
|
25
|
-
Verdict.build(stale: stale, reason: stale ? "ttl exceeded" : nil, fetching: false)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# Keys of entries past their retention rule TTL — the refresh produce scope.
|
|
29
|
-
def stale_keys(prefix: nil, lane: nil)
|
|
30
|
-
@manifest.data.entries.select { |m| due?(m, prefix: prefix, lane: lane) }.map(&:key)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
alias stale_intake_keys stale_keys
|
|
34
|
-
|
|
35
|
-
# File basis as a Time (or nil): file mtime when present, else nil.
|
|
36
|
-
def file_basis(mentry)
|
|
37
|
-
path = @manifest.resolver.resolve(mentry.key).path
|
|
38
|
-
return nil unless @file_stat.exists?(path)
|
|
39
|
-
|
|
40
|
-
@file_stat.mtime(path)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
# Generator-drift rows for one entry (replaces Staleness::GeneratorCheck#
|
|
44
|
-
# rows_for) — consumed by the doctor generator_drift check.
|
|
45
|
-
def drift_rows(mentry)
|
|
46
|
-
return [] unless drift_applicable?(mentry)
|
|
47
|
-
|
|
48
|
-
path = Textus::Key::Path.resolve(@manifest.data, mentry)
|
|
49
|
-
reason = drift_reason(mentry, path)
|
|
50
|
-
reason ? [drift_row(mentry, path, reason)] : []
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
private
|
|
54
|
-
|
|
55
|
-
def fresh = Verdict.build(stale: false, reason: nil, fetching: false)
|
|
56
|
-
|
|
57
|
-
def due?(mentry, prefix:, lane:)
|
|
58
|
-
return false if lane && mentry.lane != lane
|
|
59
|
-
return false if prefix && !mentry.key.start_with?(prefix)
|
|
60
|
-
|
|
61
|
-
ttl = @manifest.rules.for(mentry.key).retention&.ttl_seconds
|
|
62
|
-
return false if ttl.nil?
|
|
63
|
-
|
|
64
|
-
path = @manifest.resolver.resolve(mentry.key).path
|
|
65
|
-
return true unless @file_stat.exists?(path)
|
|
66
|
-
|
|
67
|
-
age_stale?(file_basis(mentry), ttl)
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# The one age comparison. A never-recorded entry (nil basis) is stale.
|
|
71
|
-
def age_stale?(basis, ttl)
|
|
72
|
-
return true if basis.nil?
|
|
73
|
-
|
|
74
|
-
(@clock.now - basis).to_i > ttl
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
# --- generator drift (lifted from Staleness::GeneratorCheck) ---
|
|
78
|
-
|
|
79
|
-
def drift_applicable?(mentry) = mentry.external?
|
|
80
|
-
|
|
81
|
-
def drift_reason(mentry, path)
|
|
82
|
-
return "derived entry has never been generated" unless @file_stat.exists?(path)
|
|
83
|
-
|
|
84
|
-
generated_at = generated_at_of(mentry, path)
|
|
85
|
-
return "missing generated.at frontmatter" unless generated_at
|
|
86
|
-
|
|
87
|
-
gen_time = parse_time(generated_at)
|
|
88
|
-
return "unparseable generated.at: #{generated_at.inspect}" unless gen_time
|
|
89
|
-
|
|
90
|
-
offender = newest_source_after(mentry.source, gen_time)
|
|
91
|
-
"source '#{offender}' modified after generated.at" if offender
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def generated_at_of(mentry, path)
|
|
95
|
-
Format.for(mentry.format).parse(@file_stat.read(path), path: path)["_meta"].dig("generated", "at")
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def parse_time(str)
|
|
99
|
-
Time.parse(str.to_s)
|
|
100
|
-
rescue StandardError
|
|
101
|
-
nil
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def newest_source_after(external_src, gen_time)
|
|
105
|
-
Array(external_src.sources).each do |src|
|
|
106
|
-
offender = check_source(src, gen_time)
|
|
107
|
-
return offender if offender
|
|
108
|
-
end
|
|
109
|
-
nil
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def check_source(src, gen_time)
|
|
113
|
-
if src.match?(/\A[a-z0-9.][a-z0-9._-]*\z/) && !src.include?("/")
|
|
114
|
-
@manifest.resolver.enumerate(prefix: src).each do |row|
|
|
115
|
-
return src if @file_stat.mtime(row[:path]) > gen_time
|
|
116
|
-
end
|
|
117
|
-
nil
|
|
118
|
-
else
|
|
119
|
-
check_filesystem_source(src, gen_time)
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def check_filesystem_source(src, gen_time)
|
|
124
|
-
abs = absolutize_source(src)
|
|
125
|
-
if @file_stat.directory?(abs)
|
|
126
|
-
dir_has_newer_file?(abs, gen_time) ? src : nil
|
|
127
|
-
elsif @file_stat.exists?(abs) && @file_stat.mtime(abs) > gen_time
|
|
128
|
-
src
|
|
129
|
-
end
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def absolutize_source(src)
|
|
133
|
-
File.absolute_path?(src) ? src : File.join(File.dirname(@manifest.data.root), src)
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def dir_has_newer_file?(abs, gen_time)
|
|
137
|
-
@file_stat.glob(File.join(abs, "**", "*")).any? do |fpath|
|
|
138
|
-
file?(fpath) && @file_stat.mtime(fpath) > gen_time
|
|
139
|
-
end
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
def file?(fpath) = !@file_stat.directory?(fpath) && @file_stat.exists?(fpath)
|
|
143
|
-
|
|
144
|
-
def drift_row(mentry, path, reason)
|
|
145
|
-
{ "key" => mentry.key, "path" => path, "generator" => mentry.source.command, "reason" => reason }
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
end
|
|
149
|
-
end
|
|
150
|
-
end
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Core
|
|
5
|
-
# Currency — "is the stored data stale relative to its source?" (ADR 0099).
|
|
6
|
-
# The home of the single Freshness evaluator and its Verdict value object.
|
|
7
|
-
# Distinct from Core::Retention (GC dueness, Q2).
|
|
8
|
-
module Freshness
|
|
9
|
-
end
|
|
10
|
-
end
|
|
11
|
-
end
|